Summary
A task that constructs another task instance directly does not get TaskEnvironment propagated into it. MSBuild only injects into tasks it instantiates itself (TaskExecutionHost.cs), so the inner task silently falls back to TaskEnvironment.Fallback and resolves paths against the shared node's current directory.
There is no diagnostic for this, and it is invisible to every existing rule because the outer task looks fully migrated.
Evidence
From dotnet/arcade ExecWithRetries:
_runningExec = new Exec
{
BuildEngine = BuildEngine,
Command = Command,
WorkingDirectory = WorkingDirectory, // resolved by ToolTask via *its* TaskEnvironment
...
};
Exec derives from ToolTask, which uses TaskEnvironment for both WorkingDirectory and GetProcessStartInfo(). The outer task was annotated, used TaskEnvironment correctly everywhere in its own body, and passed the analyzer cleanly — while handing the real work to an inner task that had no environment at all.
The fix is a one-liner, which is what makes the missing diagnostic worth having:
_runningExec = new Exec
{
BuildEngine = BuildEngine,
TaskEnvironment = TaskEnvironment, // <-- required
...
};
Proposed rule
Inside a type that is [MSBuildMultiThreadableTask]-annotated (or implements IMultiThreadableTask), flag an object-creation expression whose type implements ITask when TaskEnvironment is not assigned — via object initializer, property assignment, or constructor.
Severity: Warning. The false-positive rate should be near zero, since constructing an ITask inside a task is rare and always needs this.
Note that ToolTask.TaskEnvironment is public virtual, so the fix is always expressible; a code fix that adds the initializer entry would be straightforward.
Summary
A task that constructs another task instance directly does not get
TaskEnvironmentpropagated into it. MSBuild only injects into tasks it instantiates itself (TaskExecutionHost.cs), so the inner task silently falls back toTaskEnvironment.Fallbackand resolves paths against the shared node's current directory.There is no diagnostic for this, and it is invisible to every existing rule because the outer task looks fully migrated.
Evidence
From dotnet/arcade
ExecWithRetries:Execderives fromToolTask, which usesTaskEnvironmentfor bothWorkingDirectoryandGetProcessStartInfo(). The outer task was annotated, usedTaskEnvironmentcorrectly everywhere in its own body, and passed the analyzer cleanly — while handing the real work to an inner task that had no environment at all.The fix is a one-liner, which is what makes the missing diagnostic worth having:
Proposed rule
Inside a type that is
[MSBuildMultiThreadableTask]-annotated (or implementsIMultiThreadableTask), flag an object-creation expression whose type implementsITaskwhenTaskEnvironmentis not assigned — via object initializer, property assignment, or constructor.Severity: Warning. The false-positive rate should be near zero, since constructing an
ITaskinside a task is rare and always needs this.Note that
ToolTask.TaskEnvironmentispublic virtual, so the fix is always expressible; a code fix that adds the initializer entry would be straightforward.