[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.
```
This commit is contained in:
bitloi 2026-01-31 17:56:42 -05:00 committed by GitHub
parent 9388c140fa
commit d633de5c3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 8 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)*/
)
)
}

View File

@ -0,0 +1,2 @@
val c = taskKey[Unit]("A custom task named 'c'")
c := { println("Custom task 'c' executed") }

View File

@ -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