You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ResolveCodeAnalysisRuleSet is one of the remaining tasks listed in the migration epic (#11834). It resolves a code-analysis ruleset reference (a simple file name, project-relative path, or full path) against the project directory plus a list of additional search directories, returning the resolved location as the [Output] ResolvedCodeAnalysisRuleSet string.
The task already accepts MSBuildProjectDirectory as an explicit input from Microsoft.Common.CurrentVersion.targets, which is helpful — most cwd-dependence is mediated through that property. However, the task still has cwd-dependent surfaces that matter under multithreaded execution:
Path.IsPathRooted(...) and Path.GetFileName(...) are pure (no cwd consumption).
FileSystems.Default.FileExists(absolutePath) is cwd-independent iff the argument is actually absolute.
FileSystems.Default.FileExists(CodeAnalysisRuleSet) on the rooted branch is gated by Path.IsPathRooted(...) == true, so it's cwd-independent.
The risky cases are the two Path.Combine(MSBuildProjectDirectory, ...) branches and each Path.Combine(directory, CodeAnalysisRuleSet) in CodeAnalysisRuleSetDirectories. If MSBuildProjectDirectory is empty (the targets don't set it, the user doesn't supply it, or a custom invocation forgets it) or if any entry of CodeAnalysisRuleSetDirectories is relative, the resulting FileExists call silently consumes process Environment.CurrentDirectory.
In legacy execution that's invisible because process cwd happens to match the project directory. Under MT execution, multiple ResolveCodeAnalysisRuleSet instances may be running concurrently in the same process for projects with different directories — there is only one cwd, so any relative resolution is a correctness bug.
The trustworthy source of "what directory is this task associated with?" in MT mode is TaskEnvironment.ProjectDirectory, not the user-supplied MSBuildProjectDirectory property. The cleanest migration is to absolutize all candidate paths through TaskEnvironment before handing them to FileExists.
Approach
Apply the PR #13267 (MSBuild/CallTarget) pattern: absolutize at the task boundary, then run the existing branching logic on absolute strings.
Mark ResolveCodeAnalysisRuleSet with [MSBuildMultiThreadableTask] and implement IMultiThreadableTask with public TaskEnvironment TaskEnvironment { get; set; } (no default initializer — matches the built-in style used in Enlighten public versions of intrinsic tasks. #13267; the runtime default is supplied by TaskRouter).
In GetResolvedRuleSetPath:
Keep the early-exit on string.IsNullOrEmpty(CodeAnalysisRuleSet).
Keep the existing classification (CodeAnalysisRuleSet == Path.GetFileName(CodeAnalysisRuleSet) → simple name; Path.IsPathRooted(...) → rooted; else → relative). These are pure string operations and remain valid.
Replace each Path.Combine(<base>, CodeAnalysisRuleSet) site with TaskEnvironment.GetAbsolutePath(Path.Combine(<base>, CodeAnalysisRuleSet)) (or, for entries that may themselves be relative, absolutize the base first via TaskEnvironment.GetAbsolutePath(directory) and then Path.Combine). Either form ends up calling FileExists on a guaranteed-absolute string.
For the rooted-path FileSystems.Default.FileExists(CodeAnalysisRuleSet) branch, keep as-is (already cwd-independent).
simple-name branch (file found in project dir) → returns CodeAnalysisRuleSet (the original input string)
simple-name branch (file found in a search dir) → returns Path.Combine(directory, CodeAnalysisRuleSet) (the original combined string, not absolutized)
relative-path branch → returns CodeAnalysisRuleSet (the original input)
rooted-path branch → returns CodeAnalysisRuleSet (the original input)
any branch where the file doesn't exist → returns null
Do not let an AbsolutePath value or its .Value leak into the assignment to ResolvedCodeAnalysisRuleSet. Keep a separate "original form" string variable for the output, and use absolutized strings only for the FileExists check.
Preserve warning message text (Sin 2).Log.LogWarningWithCodeFromResources("Compiler.UnableToFindRuleSet", CodeAnalysisRuleSet) must continue to receive the original input string, not an absolutized form. (Today it's already the original CodeAnalysisRuleSet; just don't accidentally swap it for an absolutized variable during the refactor.)
Decide on MSBuildProjectDirectory vs TaskEnvironment.ProjectDirectory. Recommended: continue to honor MSBuildProjectDirectory as the explicit input (preserves backwards compatibility for callers who already set it from targets), but when it is null/empty, fall back to TaskEnvironment.ProjectDirectory rather than skipping the project-directory branches entirely. This is a strict superset of today's behavior — when both are equal (the common case), output is identical; when MSBuildProjectDirectory is empty and TaskEnvironment.ProjectDirectory is meaningful, we now find the file instead of warning MSB3884. If reviewers consider that a breaking-enough widening, gate it behind a ChangeWave; otherwise ship it as a bugfix.
Why no ChangeWave (likely)
For the dominant path — MSBuildProjectDirectory is supplied by the targets and matches the project directory — output is byte-identical to today: same [Output] string, same warning text, same Execute() return value. The semantic change manifests only when (a) running under MT with different per-task project directories, where today's behavior was already wrong, or (b) MSBuildProjectDirectory is empty and we now consult TaskEnvironment.ProjectDirectory (a strict improvement, but reviewers may still want a wave for safety — see Approach §5).
If unforeseen behavioral diffs surface during review, gate the diff behind a new ChangeWave following the precedent set in PR #13069.
Test coverage assessment
Existing direct unit tests (src/Tasks.UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs, 11 cases)
Coverage of the four input categories (null/empty, simple name, relative, rooted) crossed with project-dir / search-dir presence is good. Coverage of MT-specific concerns (concurrency, project-relative resolution when cwd ≠ project dir, TaskEnvironment injection) is zero.
Integration tests in this repo
None directly.ResolveCodeAnalysisRuleSet is invoked from Microsoft.Common.CurrentVersion.targets in the ResolveCodeAnalysisRuleSet target, feeding compiler invocations downstream. End-to-end coverage of that flow lives in the C#/VB compiler integration tests in dotnet/sdk and Roslyn, not here. We will not add E2E compile tests in this repo as part of this issue.
Gaps to fill in this PR
G1 — Concurrency test. Two ResolveCodeAnalysisRuleSet instances with differentTaskEnvironment.ProjectDirectory values, each with a ruleset file present only in its own project directory under the same simple name. Assert each task resolves to its own file (i.e., resolution is correctly scoped per-task, not via shared process cwd). (SKILL red-team Phase 4.)
G2 — Project-relative resolution when cwd ≠ project dir. Set Environment.CurrentDirectory (via TestEnvironment.SetCurrentDirectory) to a directory that does not contain the ruleset, set TaskEnvironment.ProjectDirectory to a directory that does, leave MSBuildProjectDirectory empty, and assert the file is still found. Documents the intentional behavior under MT and the fallback described in Approach §5.
G3 — Inject TaskEnvironment into existing tests. Set task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest() in every existing test so that the MSBuildProjectDirectory = null / empty branches still go through a defined fallback (cwd-backed TaskEnvironment.Fallback) instead of NRE'ing on the new code path.
G4 — Baseline parity matrix. Reassert each of the existing semantics on the migrated code:
simple name + project dir + file present → [Output] is original simple-name string (not absolutized)
simple name + search dir + file present → [Output] is Path.Combine(directory, simpleName) exact string (not absolutized)
relative path + project dir + file present → [Output] is original relative-path string (not absolutized)
rooted path + file present → [Output] is original rooted string (not absolutized)
missing file in any branch → [Output] is null and warning text contains the originalCodeAnalysisRuleSet input verbatim (Sin 2)
G5 — Relative entry in CodeAnalysisRuleSetDirectories. Pass a relative directory in CodeAnalysisRuleSetDirectories. Today this is silently cwd-dependent; under MT it must be absolutized via TaskEnvironment before FileExists. Assert the file is found when present under TaskEnvironment.ProjectDirectory + relativeSearchDir and the resulting [Output] retains the original combined string (preserving the existing behavior that this branch returns the un-absolutized combined path).
Acceptance criteria
ResolveCodeAnalysisRuleSet decorated [MSBuildMultiThreadableTask] and implements IMultiThreadableTask with a TaskEnvironment property (no default initializer — matches built-in style).
Every FileSystems.Default.FileExists(...) call in GetResolvedRuleSetPath receives an absolute path (either inherently rooted, or absolutized via TaskEnvironment.GetAbsolutePath).
The four classification branches (null/empty, simple name, relative, rooted) are preserved; Path.IsPathRooted and Path.GetFileName are unchanged.
[Output] ResolvedCodeAnalysisRuleSet exact string is byte-identical to current behavior in all five existing-success branches (simple-name in project dir, simple-name in search dir, relative in project dir, rooted, and null when missing). No AbsolutePath leak (Sin 1).
MSB3884 (Compiler.UnableToFindRuleSet) warning text contains the originalCodeAnalysisRuleSet input string verbatim, not an absolutized form (Sin 2).
All existing 11 ResolveCodeAnalysisRuleSet_Tests pass on net472 and net10.0 with no behavior changes.
MSBuildProjectDirectory continues to be honored when supplied; falls back to TaskEnvironment.ProjectDirectory when null/empty (or, if reviewers prefer, gated behind a ChangeWave).
Tests G1, G2, G3, G4, G5 added and pass.
.\build.cmd -v quiet succeeds; Tasks.UnitTests and Build.UnitTests pass.
No new compiler warnings (warnings-as-errors in official build).
No ChangeWave required; if any test required modification beyond G3, document why in the PR and gate the diff behind the next wave.
multithreaded-task-migration SKILL sign-off checklist walked and passes.
Implementation order
Apply attribute + interface to ResolveCodeAnalysisRuleSet. Add TaskEnvironment property.
Update GetResolvedRuleSetPath to absolutize each Path.Combine(...) candidate with TaskEnvironment.GetAbsolutePath before calling FileExists. Hold the original (un-absolutized) candidate string in a separate variable; only the [Output] and warning paths see the original.
Implement the MSBuildProjectDirectory → TaskEnvironment.ProjectDirectory fallback (or skip if reviewers prefer to keep behavior strictly identical and ChangeWave-gate the widening).
Update existing 11 ResolveCodeAnalysisRuleSet_Tests to inject test TaskEnvironment (G3).
Run full Tasks.UnitTests and Build.UnitTests; verify clean.
Run repo build with -v quiet to ensure no new warnings.
Risks / open questions
The fallback from empty MSBuildProjectDirectory to TaskEnvironment.ProjectDirectory (Approach §5) widens the set of files we'll now successfully resolve. In legacy mode the user could see a previously-warning build start succeeding silently. Likely benign (and arguably a bugfix), but reviewers may request ChangeWave gating.
Compiler integration under MT is not exercised by this repo's tests. Coordination with Roslyn / dotnet/sdk recommended before MT mode ships generally, but not blocking for this PR — the task itself is correct in isolation once migrated.
Enlighten ResolveCodeAnalysisRuleSet task for multithreaded mode
Parent: #11834
Context
ResolveCodeAnalysisRuleSetis one of the remaining tasks listed in the migration epic (#11834). It resolves a code-analysis ruleset reference (a simple file name, project-relative path, or full path) against the project directory plus a list of additional search directories, returning the resolved location as the[Output] ResolvedCodeAnalysisRuleSetstring.The task already accepts
MSBuildProjectDirectoryas an explicit input fromMicrosoft.Common.CurrentVersion.targets, which is helpful — most cwd-dependence is mediated through that property. However, the task still has cwd-dependent surfaces that matter under multithreaded execution:Cwd-dependence audit:
Path.IsPathRooted(...)andPath.GetFileName(...)are pure (no cwd consumption).FileSystems.Default.FileExists(absolutePath)is cwd-independent iff the argument is actually absolute.FileSystems.Default.FileExists(CodeAnalysisRuleSet)on the rooted branch is gated byPath.IsPathRooted(...) == true, so it's cwd-independent.Path.Combine(MSBuildProjectDirectory, ...)branches and eachPath.Combine(directory, CodeAnalysisRuleSet)inCodeAnalysisRuleSetDirectories. IfMSBuildProjectDirectoryis empty (the targets don't set it, the user doesn't supply it, or a custom invocation forgets it) or if any entry ofCodeAnalysisRuleSetDirectoriesis relative, the resultingFileExistscall silently consumes processEnvironment.CurrentDirectory.In legacy execution that's invisible because process cwd happens to match the project directory. Under MT execution, multiple
ResolveCodeAnalysisRuleSetinstances may be running concurrently in the same process for projects with different directories — there is only one cwd, so any relative resolution is a correctness bug.The trustworthy source of "what directory is this task associated with?" in MT mode is
TaskEnvironment.ProjectDirectory, not the user-suppliedMSBuildProjectDirectoryproperty. The cleanest migration is to absolutize all candidate paths throughTaskEnvironmentbefore handing them toFileExists.Approach
Apply the PR #13267 (MSBuild/CallTarget) pattern: absolutize at the task boundary, then run the existing branching logic on absolute strings.
ResolveCodeAnalysisRuleSetwith[MSBuildMultiThreadableTask]and implementIMultiThreadableTaskwithpublic TaskEnvironment TaskEnvironment { get; set; }(no default initializer — matches the built-in style used in Enlighten public versions of intrinsic tasks. #13267; the runtime default is supplied byTaskRouter).GetResolvedRuleSetPath:string.IsNullOrEmpty(CodeAnalysisRuleSet).CodeAnalysisRuleSet == Path.GetFileName(CodeAnalysisRuleSet)→ simple name;Path.IsPathRooted(...)→ rooted; else → relative). These are pure string operations and remain valid.Path.Combine(<base>, CodeAnalysisRuleSet)site withTaskEnvironment.GetAbsolutePath(Path.Combine(<base>, CodeAnalysisRuleSet))(or, for entries that may themselves be relative, absolutize the base first viaTaskEnvironment.GetAbsolutePath(directory)and thenPath.Combine). Either form ends up callingFileExistson a guaranteed-absolute string.FileSystems.Default.FileExists(CodeAnalysisRuleSet)branch, keep as-is (already cwd-independent).[Output] ResolvedCodeAnalysisRuleSetexact string semantics (Sin 1). Today:CodeAnalysisRuleSet(the original input string)Path.Combine(directory, CodeAnalysisRuleSet)(the original combined string, not absolutized)CodeAnalysisRuleSet(the original input)CodeAnalysisRuleSet(the original input)nullDo not let an
AbsolutePathvalue or its.Valueleak into the assignment toResolvedCodeAnalysisRuleSet. Keep a separate "original form" string variable for the output, and use absolutized strings only for theFileExistscheck.Log.LogWarningWithCodeFromResources("Compiler.UnableToFindRuleSet", CodeAnalysisRuleSet)must continue to receive the original input string, not an absolutized form. (Today it's already the originalCodeAnalysisRuleSet; just don't accidentally swap it for an absolutized variable during the refactor.)MSBuildProjectDirectoryvsTaskEnvironment.ProjectDirectory. Recommended: continue to honorMSBuildProjectDirectoryas the explicit input (preserves backwards compatibility for callers who already set it from targets), but when it is null/empty, fall back toTaskEnvironment.ProjectDirectoryrather than skipping the project-directory branches entirely. This is a strict superset of today's behavior — when both are equal (the common case), output is identical; whenMSBuildProjectDirectoryis empty andTaskEnvironment.ProjectDirectoryis meaningful, we now find the file instead of warning MSB3884. If reviewers consider that a breaking-enough widening, gate it behind a ChangeWave; otherwise ship it as a bugfix.Why no ChangeWave (likely)
For the dominant path —
MSBuildProjectDirectoryis supplied by the targets and matches the project directory — output is byte-identical to today: same[Output]string, same warning text, sameExecute()return value. The semantic change manifests only when (a) running under MT with different per-task project directories, where today's behavior was already wrong, or (b)MSBuildProjectDirectoryis empty and we now consultTaskEnvironment.ProjectDirectory(a strict improvement, but reviewers may still want a wave for safety — see Approach §5).If unforeseen behavioral diffs surface during review, gate the diff behind a new ChangeWave following the precedent set in PR #13069.
Test coverage assessment
Existing direct unit tests (
src/Tasks.UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs, 11 cases)GetResolvedRuleSetPath_FullPath_NonExistent,GetResolvedRuleSetPath_FullPath_Existent,GetResolvedRuleSetPath_SimpleNameAlone_NonExistent,GetResolvedRuleSetPath_SimpleNameAndProjectDirectory_Existent,GetResolvedRuleSetPath_SimpleNameAndProjectDirectory_NonExistent,GetResolvedRuleSetPath_SimpleNameAndDirectories_Existent,GetResolvedRuleSetPath_SimpleNameAndDirectories_NonExistent,GetResolvedRuleSetPath_RelativePath_WithProject_NonExistent,GetResolvedRuleSetPath_RelativePath_WithProject_Existent,GetResolvedRuleSetPath_RelativePath_NoProject,GetResolvedRuleSetPath_EmptyString,GetResolvedRuleSetPath_Null.Coverage of the four input categories (null/empty, simple name, relative, rooted) crossed with project-dir / search-dir presence is good. Coverage of MT-specific concerns (concurrency, project-relative resolution when cwd ≠ project dir,
TaskEnvironmentinjection) is zero.Integration tests in this repo
None directly.
ResolveCodeAnalysisRuleSetis invoked fromMicrosoft.Common.CurrentVersion.targetsin theResolveCodeAnalysisRuleSettarget, feeding compiler invocations downstream. End-to-end coverage of that flow lives in the C#/VB compiler integration tests indotnet/sdkand Roslyn, not here. We will not add E2E compile tests in this repo as part of this issue.Gaps to fill in this PR
ResolveCodeAnalysisRuleSetinstances with differentTaskEnvironment.ProjectDirectoryvalues, each with a ruleset file present only in its own project directory under the same simple name. Assert each task resolves to its own file (i.e., resolution is correctly scoped per-task, not via shared process cwd). (SKILL red-team Phase 4.)Environment.CurrentDirectory(viaTestEnvironment.SetCurrentDirectory) to a directory that does not contain the ruleset, setTaskEnvironment.ProjectDirectoryto a directory that does, leaveMSBuildProjectDirectoryempty, and assert the file is still found. Documents the intentional behavior under MT and the fallback described in Approach §5.TaskEnvironmentinto existing tests. Settask.TaskEnvironment = TaskEnvironmentHelper.CreateForTest()in every existing test so that theMSBuildProjectDirectory = null/ empty branches still go through a defined fallback (cwd-backedTaskEnvironment.Fallback) instead of NRE'ing on the new code path.[Output]is original simple-name string (not absolutized)[Output]isPath.Combine(directory, simpleName)exact string (not absolutized)[Output]is original relative-path string (not absolutized)[Output]is original rooted string (not absolutized)[Output]isnulland warning text contains the originalCodeAnalysisRuleSetinput verbatim (Sin 2)CodeAnalysisRuleSetDirectories. Pass a relative directory inCodeAnalysisRuleSetDirectories. Today this is silently cwd-dependent; under MT it must be absolutized viaTaskEnvironmentbeforeFileExists. Assert the file is found when present underTaskEnvironment.ProjectDirectory + relativeSearchDirand the resulting[Output]retains the original combined string (preserving the existing behavior that this branch returns the un-absolutized combined path).Acceptance criteria
ResolveCodeAnalysisRuleSetdecorated[MSBuildMultiThreadableTask]and implementsIMultiThreadableTaskwith aTaskEnvironmentproperty (no default initializer — matches built-in style).FileSystems.Default.FileExists(...)call inGetResolvedRuleSetPathreceives an absolute path (either inherently rooted, or absolutized viaTaskEnvironment.GetAbsolutePath).Path.IsPathRootedandPath.GetFileNameare unchanged.[Output] ResolvedCodeAnalysisRuleSetexact string is byte-identical to current behavior in all five existing-success branches (simple-name in project dir, simple-name in search dir, relative in project dir, rooted, andnullwhen missing). NoAbsolutePathleak (Sin 1).MSB3884(Compiler.UnableToFindRuleSet) warning text contains the originalCodeAnalysisRuleSetinput string verbatim, not an absolutized form (Sin 2).ResolveCodeAnalysisRuleSet_Testspass onnet472andnet10.0with no behavior changes.MSBuildProjectDirectorycontinues to be honored when supplied; falls back toTaskEnvironment.ProjectDirectorywhen null/empty (or, if reviewers prefer, gated behind a ChangeWave)..\build.cmd -v quietsucceeds;Tasks.UnitTestsandBuild.UnitTestspass.ChangeWaverequired; if any test required modification beyond G3, document why in the PR and gate the diff behind the next wave.multithreaded-task-migrationSKILL sign-off checklist walked and passes.Implementation order
ResolveCodeAnalysisRuleSet. AddTaskEnvironmentproperty.GetResolvedRuleSetPathto absolutize eachPath.Combine(...)candidate withTaskEnvironment.GetAbsolutePathbefore callingFileExists. Hold the original (un-absolutized) candidate string in a separate variable; only the[Output]and warning paths see the original.MSBuildProjectDirectory→TaskEnvironment.ProjectDirectoryfallback (or skip if reviewers prefer to keep behavior strictly identical and ChangeWave-gate the widening).ResolveCodeAnalysisRuleSet_Teststo inject testTaskEnvironment(G3).Tasks.UnitTestsandBuild.UnitTests; verify clean.-v quietto ensure no new warnings.Risks / open questions
MSBuildProjectDirectorytoTaskEnvironment.ProjectDirectory(Approach §5) widens the set of files we'll now successfully resolve. In legacy mode the user could see a previously-warning build start succeeding silently. Likely benign (and arguably a bugfix), but reviewers may request ChangeWave gating.dotnet/sdkrecommended before MT mode ships generally, but not blocking for this PR — the task itself is correct in isolation once migrated.References
.github/skills/multithreaded-task-migration/SKILL.md