From d633de5c3fe472e368461d83889991fd2d1dad5f Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Sat, 31 Jan 2026 17:56:42 -0500 Subject: [PATCH] [2.x] fix: Detect alias/task key name conflicts (#8659) Problem When a user defines an alias with a name that matches an existing task or setting key (e.g., `alias c = compile` when a custom task `c` exists), the alias silently wins and shadows the task. Solution Detect conflicts at alias creation time and fail with an error message: ``` Alias 'c' conflicts with a task or setting key of the same name. Use a different alias name to avoid ambiguity. ``` --- .../src/main/scala/sbt/BasicCommands.scala | 31 ++++++++++++++++--- main/src/main/scala/sbt/Main.scala | 13 +++++++- main/src/main/scala/sbt/ProjectExtra.scala | 15 +++++++-- .../actions/alias-key-conflict/build.sbt | 2 ++ .../sbt-test/actions/alias-key-conflict/test | 6 ++++ 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 sbt-app/src/sbt-test/actions/alias-key-conflict/build.sbt create mode 100644 sbt-app/src/sbt-test/actions/alias-key-conflict/test diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index fe933815d..dd97d3deb 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -471,19 +471,40 @@ object BasicCommands { }(runAlias) def runAlias(s: State, args: Option[(String, Option[Option[String]])]): State = + runAlias(s, args, _ => Set.empty) + + def runAlias( + s: State, + args: Option[(String, Option[Option[String]])], + definedKeyNames: State => Set[String] + ): State = args match { case Some(x ~ None) if !x.isEmpty => printAlias(s, x.trim); s case Some(name ~ Some(None)) => removeAlias(s, name.trim) - case Some(name ~ Some(Some(value))) => addAlias(s, name.trim, value.trim) + case Some(name ~ Some(Some(value))) => addAlias(s, name.trim, value.trim, definedKeyNames) case _ => printAliases(s); s } def addAlias(s: State, name: String, value: String): State = - if Command.validID(name) then - val removed = removeAlias(s, name) - if value.isEmpty then removed else addAlias0(removed, name, value) - else + addAlias(s, name, value, _ => Set.empty) + + def addAlias( + s: State, + name: String, + value: String, + definedKeyNames: State => Set[String] + ): State = + if !Command.validID(name) then System.err.println("Invalid alias name '" + name + "'.") s.fail + else if definedKeyNames(s).contains(name) then + System.err.println( + s"Alias '$name' conflicts with a task or setting key of the same name. " + + "Use a different alias name to avoid ambiguity." + ) + s.fail + else + val removed = removeAlias(s, name) + if value.isEmpty then removed else addAlias0(removed, name, value) private def addAlias0(s: State, name: String, value: String): State = s.copy(definedCommands = newAlias(name, value) +: s.definedCommands) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index fbabef52f..d322eac41 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -305,6 +305,16 @@ import sbt.internal.util.complete.DefaultParsers.* object BuiltinCommands { def initialAttributes = AttributeMap.empty import BasicCommands.exit + + def aliasWithKeyConflictCheck: Command = + Command(AliasCommand, Help.more(AliasCommand, AliasDetailed)) { s => + val name = token(OpOrID.examples(aliasNames(s)*)) + val assign = token(OptSpace ~ '=' ~ OptSpace) + val sfree = removeAliases(s) + val to = matched(sfree.combinedParser, partial = true).failOnException | any.+.string + OptSpace ~> (name ~ (assign ~> to.?).?).? + }((s, args) => runAlias(s, args, Project.definedKeyNames)) + def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop) @@ -360,8 +370,9 @@ object BuiltinCommands { waitCmd, promptChannel, TestCommand.testOnly, + aliasWithKeyConflictCheck, ) ++ - allBasicCommands ++ + allBasicCommands.filterNot(_.nameOption.contains(AliasCommand)) ++ ContinuousCommands.value ++ BuildServerProtocol.commands diff --git a/main/src/main/scala/sbt/ProjectExtra.scala b/main/src/main/scala/sbt/ProjectExtra.scala index d241a880b..637c5c20d 100755 --- a/main/src/main/scala/sbt/ProjectExtra.scala +++ b/main/src/main/scala/sbt/ProjectExtra.scala @@ -237,6 +237,18 @@ trait ProjectExtra extends Scoped.Syntax: def isProjectLoaded(state: State): Boolean = (state has Keys.sessionSettings) && (state has Keys.stateBuildStructure) + def definedKeyNames(state: State): Set[String] = + if !isProjectLoaded(state) then Set.empty + else + val keyIndex = structure(state).index.keyIndex + val globalKeys = keyIndex.keys(None) + val projectKeys = for + uri <- keyIndex.buildURIs + project <- keyIndex.projects(uri) + key <- keyIndex.keys(Some(ProjectRef(uri, project))) + yield key + globalKeys ++ projectKeys + def extract(state: State): Extracted = { val se = Project.session(state) val st = Project.structure(state) @@ -281,11 +293,10 @@ trait ProjectExtra extends Scoped.Syntax: .put(sessionSettings, session) .put(Keys.onUnload.key, onUnload) val newState = unloaded.copy(attributes = newAttrs) - // TODO: Fix this onLoad( preOnLoad( updateCurrent(newState) - ) /*LogManager.setGlobalLogLevels(updateCurrent(newState), structure.data)*/ + ) ) } diff --git a/sbt-app/src/sbt-test/actions/alias-key-conflict/build.sbt b/sbt-app/src/sbt-test/actions/alias-key-conflict/build.sbt new file mode 100644 index 000000000..7ad66df93 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/alias-key-conflict/build.sbt @@ -0,0 +1,2 @@ +val c = taskKey[Unit]("A custom task named 'c'") +c := { println("Custom task 'c' executed") } diff --git a/sbt-app/src/sbt-test/actions/alias-key-conflict/test b/sbt-app/src/sbt-test/actions/alias-key-conflict/test new file mode 100644 index 000000000..39e37bda8 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/alias-key-conflict/test @@ -0,0 +1,6 @@ +# Test that alias creation fails when it conflicts with a task key name +# The task 'c' is defined in build.sbt, so 'alias c = compile' should fail +-> alias c = compile + +# Verify the task 'c' still works +> c