Skip to content

Correct MT routing docs and add analyzer rules for mismatched declarations - #14809

Open
ViktorHofer wants to merge 4 commits into
dotnet:mainfrom
ViktorHofer:mt-routing-docs
Open

Correct MT routing docs and add analyzer rules for mismatched declarations#14809
ViktorHofer wants to merge 4 commits into
dotnet:mainfrom
ViktorHofer:mt-routing-docs

Conversation

@ViktorHofer

@ViktorHofer ViktorHofer commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes #14779
Fixes #14790

The problem

[MSBuildMultiThreadableTask] and IMultiThreadableTask do different jobs, but both the spec and TaskRouter's own doc comment described them as interchangeable alternatives:

Declaration Effect Read by
[MSBuildMultiThreadableTask] Runs in-process instead of an out-of-proc TaskHost TaskRouter.NeedsTaskHostInMultiThreadedMode
IMultiThreadableTask Receives a TaskEnvironment TaskExecutionHost

The spec went further and told authors on newer MSBuild versions to prefer the interface. Following that advice produces a task that resolves paths correctly and then runs in a TaskHost anyway, gaining nothing. This tripped up a real migration of ~150 tasks across dotnet/arcade, dotnet/source-build-assets and the VMR.

The interface cannot be promoted to a routing signal, which was the original suggestion in #14779. ToolTask implements IMultiThreadableTask (src/Utilities/ToolTask.cs), so routing on it would silently opt in every ToolTask-derived task in the ecosystem, none of which have been reviewed for thread safety. The attribute stays the routing signal; the docs and the analyzer are what need to change.

Documentation (#14790)

  • Rewrote the spec's Thread-Safe Capability Indicators section around the routing/injection split. Capability is declared by the attribute alone; the interface is an additional mechanism for TaskEnvironment access, and a task that needs only the attribute is fully migrated.
  • Removed "customers using newer MSBuild versions should prefer the Interface-Based Thread-Safe Capability Declaration".
  • Documented both silent failure modes, including what actually happens to an interface-only task: the out-of-proc host does not assign the property. It supplies TaskEnvironment.Fallback to a TaskEnvironment constructor when one is declared, and otherwise leaves the property at the task's own default. That is correct there, because Fallback is backed by MultiProcessTaskEnvironmentDriver and the host process is dedicated to a single task. Existing coverage: TaskHost_MultiThreadableTask_Tests.InheritedTask_InTaskHost_HasUsableTaskEnvironment.
  • Removed the MultiThreadableTask abstract base class section — that type was never added.
  • Fixed the attribute code snippet, which showed internal class and omitted Inherited = false — contradicting the text two lines below it and the actual declaration in src/Framework/MSBuildMultiThreadableTaskAttribute.cs.
  • Rewrote the TaskRouter class-level doc comment to state that the attribute is the only routing signal, and why the interface is deliberately not consulted.
  • Moved the "why not route on the interface" rationale into the spec's existing Appendix: Alternatives, where the other design alternatives already live.

Analyzer (#14779)

Three new rules turn these mismatches into build diagnostics instead of silent runtime behaviour.

MSBuildTask0012 — TaskEnvironment property is never assigned (Warning, on by default)

Fires when a task has the attribute, declares a settable TaskEnvironment property, and does not implement the interface. The engine never assigns the property, so it retains the task's own default and resolves every path against the shared process working directory — while the attribute has put the task in-process, which is exactly where that is wrong.

Two shapes are deliberately not reported:

  • Attribute without a TaskEnvironment property. This is the supported compatibility-bridge state and is correct for a task that does not resolve relative paths or read environment variables. 26 Arcade tasks intentionally sit here.
  • Attribute plus a public single-TaskEnvironment constructor, without the interface. LoadedType selects that constructor by signature alone, independently of the interface, so the task does receive an environment. Flagging it would have been a false positive.

MSBuildTask0013 — Missing [MSBuildMultiThreadableTask] (Info, off by default)

Fires when a task declares the interface but lacks the attribute, so it still pays for a TaskHost. Off by default because that is a valid intermediate migration state — path handling done, thread-safety review not yet complete.

Only a type that declares the interface in its own base list is reported. Because ToolTask implements IMultiThreadableTask, keying on AllInterfaces would have fired on every ToolTask subclass in the ecosystem — the same trap as the routing bug this PR documents.

MSBuildTask0014 — Attribute has no effect on this type (Warning, on by default)

TaskRouter reads [MSBuildMultiThreadableTask] with inherit: false, off the concrete type the engine has just instantiated as a task. The attribute therefore only takes effect on a non-abstract class that implements ITask. Two shapes are reported.

  • Not a task at all. Nothing ever reads the attribute. The motivating case is a file that declares a task alongside helper types, where the attribute lands on the wrong class — leaving the real task unmarked and still routed to a TaskHost. That occurred during the Arcade migration.
  • An abstract task. The engine never instantiates that type, and no subclass inherits the attribute, so every concrete task deriving from it is still routed out-of-proc while the base looks migrated. Usually means the attribute belongs on each derived task instead.

A concrete task MSBuild cannot construct — no public parameterless constructor and no public single-TaskEnvironment constructor — is a third inert shape, deliberately not reported: Task.RegisterTask(string, Func<TaskEnvironment, ITask>) lets a host supply an arbitrary factory, so such a task may be perfectly reachable.

Validation

  • src/TaskAnalyzer and src/Build build clean (0 warnings, 0 errors).
  • 18 tests in MultiThreadableTaskDeclarationAnalyzerTests, including negative cases for both MSBuildTask0012 exclusions above, for ToolTask-like bases, for a non-task type without the attribute, and for an abstract base without the attribute. The ToolTask and 0013 tests run with the rule explicitly enabled, so they cannot pass trivially by virtue of the rule being off by default.
  • Full TaskAnalyzer.Tests suite: 266/266.

…tions

`[MSBuildMultiThreadableTask]` and `IMultiThreadableTask` do different jobs, but
the spec and `TaskRouter`'s own doc comment both implied they were
interchangeable alternatives. The attribute is the routing signal read by
`TaskRouter.NeedsTaskHostInMultiThreadedMode`; the interface is the injection
signal read by `TaskExecutionHost`. The interface can never be a routing
signal, because `ToolTask` implements it -- routing on it would opt in every
`ToolTask`-derived task in the ecosystem, unaudited.

Docs:
- Rewrite the spec's "Thread-Safe Capability Indicators" section around the
  routing/injection split, with a comparison table and both silent failure modes.
- Drop "customers using newer MSBuild versions should prefer the Interface-Based
  declaration" -- the attribute is required either way.
- Note that deriving from `MultiThreadableTask` supplies the interface but not
  the attribute.
- Fix the attribute snippet, which showed `internal` and omitted
  `Inherited = false`, contradicting the surrounding text.
- Rewrite the `TaskRouter` class comment to say why the interface is not consulted.

Analyzer:
- MSBuildTask0012 (Warning, on by default): the task carries the attribute and
  declares a `TaskEnvironment` property but not the interface, so the engine
  never assigns the property and it silently stays `TaskEnvironment.Fallback`
  while the task runs in-process. Attribute-only without the property is a
  supported compatibility-bridge state and is not reported. A task declaring a
  public single-`TaskEnvironment` constructor is also not reported: the engine
  selects that constructor by signature, independently of the interface.
- MSBuildTask0013 (Info, off by default): the task declares the interface but
  lacks the attribute, so it still pays for a TaskHost. Only a type declaring the
  interface in its own base list is reported -- inheriting it from `ToolTask`
  says nothing about the derived task's intent.

Fixes dotnet#14779
Fixes dotnet#14790

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
Copilot AI lite review requested due to automatic review settings August 24, 2026 06:05
@ViktorHofer
ViktorHofer deployed to copilot-pat-pool August 24, 2026 06:05 — with GitHub Actions Active
@ViktorHofer
ViktorHofer deployed to copilot-pat-pool August 24, 2026 06:06 — with GitHub Actions Active

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR clarifies the multithreaded-task contract in MSBuild by documenting the distinct roles of [MSBuildMultiThreadableTask] (routing) vs IMultiThreadableTask (TaskEnvironment injection), and adds analyzer diagnostics to catch the two common “half-migrated” shapes that previously failed silently.

Changes:

  • Updates multithreading docs (spec + TaskRouter comment) to state that the attribute is the only routing signal, and explains why the interface cannot be used for routing (ToolTask inheritance).
  • Adds two new TaskAnalyzer rules: MSBuildTask0012 (enabled Warning) and MSBuildTask0013 (disabled-by-default Info) plus unshipped analyzer release notes.
  • Adds a new analyzer implementation and a dedicated test suite covering positive/negative cases (including ToolTask-like inheritance).
Show a summary per file
File Description
src/TaskAnalyzer/README.md Documents the new MSBuildTask0012/0013 rules and their intended migration guidance.
src/TaskAnalyzer/MultiThreadableTaskDeclarationAnalyzer.cs Implements the new analyzer that detects mismatches between routing and injection declarations.
src/TaskAnalyzer/DiagnosticIds.cs Adds IDs MSBuildTask0012 and MSBuildTask0013.
src/TaskAnalyzer/DiagnosticDescriptors.cs Adds descriptors (severity/default-enabled behavior) for the new diagnostics.
src/TaskAnalyzer/AnalyzerReleases.Unshipped.md Records the new analyzer rules for release tracking.
src/TaskAnalyzer.Tests/MultiThreadableTaskDeclarationAnalyzerTests.cs Adds coverage for both diagnostics, including the ToolTask-like inheritance exclusion.
src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs Fixes the class-level comment to match the attribute-only routing behavior and rationale.
documentation/specs/multithreading/thread-safe-tasks.md Rewrites the “capability indicators” section around the routing/injection split and documents silent failure modes.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (1)

src/TaskAnalyzer/DiagnosticDescriptors.cs:122

  • The message/description currently assert the property "stays TaskEnvironment.Fallback". That is not guaranteed: the task may initialize TaskEnvironment to a different value, but MSBuild still won’t inject the proper environment unless the interface (or a TaskEnvironment constructor) is used. Rewording to "MSBuild never assigns it" + "retains its default" would be more accurate.
            messageFormat: "Task '{0}' declares a TaskEnvironment property but does not implement IMultiThreadableTask, so the engine never assigns it and it stays TaskEnvironment.Fallback; implement IMultiThreadableTask or add a public constructor taking a TaskEnvironment",
            category: "MSBuild.TaskAuthoring",
            defaultSeverity: DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: "The MSBuild engine assigns TaskEnvironment only to tasks that implement IMultiThreadableTask, or through a public constructor that takes a single TaskEnvironment. A task that declares the property without either receives no environment: the property silently remains TaskEnvironment.Fallback and every path is resolved against the shared process working directory rather than the project directory. Because the task carries [MSBuildMultiThreadableTask] it runs in-process, which is exactly where that resolution is wrong.");
  • Files reviewed: 8/8 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread src/TaskAnalyzer/DiagnosticDescriptors.cs Outdated
Comment thread documentation/specs/multithreading/thread-safe-tasks.md Outdated
Comment thread src/TaskAnalyzer/README.md Outdated
Comment thread src/TaskAnalyzer/README.md Outdated
…nt both fixes

The PR text claimed an unassigned `TaskEnvironment` property "stays
`TaskEnvironment.Fallback`". MSBuild only guarantees that it never assigns the
property; the value that survives is whatever the task itself initialized it to,
which is `null` when there is no initializer. Asserting a specific default was
exactly the kind of imprecision this PR set out to fix. Reworded the diagnostic
title, message, description, release-tracking note, spec bullet and README
example accordingly.

The README also offered only one fix. The analyzer treats a public
single-`TaskEnvironment` constructor as equally valid, and the diagnostic message
already mentioned it, so document both. Verified while writing it that the
constructor body must assign the property itself: `TaskExecutionHost` gates its
post-construction assignment on `is IMultiThreadableTask`
(TaskExecutionHost.cs:545), so a task without the interface gets no second
chance. `LoadedType` picks the constructor purely by parameter type name
(LoadedType.cs:341-346), independent of the interface, which is what makes the
exclusion correct in the first place.

Also fixed a wrong cross-reference anchor to the MSBuildTask0011 section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
@ViktorHofer
ViktorHofer requested a review from AR-May August 24, 2026 08:57

@AR-May AR-May left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall new analyzer looks good to me. Have some comments concerning docs update.

Task authors can declare thread-safe capabilities in two different ways:
1. **Interface-Based Thread-Safe Capability Declaration** - Provides access to thread-safe APIs through `TaskEnvironment` to be used in the task code.
2. **Attribute-Based Thread-Safe Capability Declaration** - Allows existing tasks to declare its ability run in multithreaded mode without code changes. It is a **compatibility bridge option**.
Task authors declare thread-safe capabilities through two mechanisms that do **different** jobs. They are not alternatives, and a fully migrated task uses both:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not exactly correct, the declaration of mt capabilities is only the attribute. Task then may use the interface. But some very simple tasks may need only the attribute and still be fully and properly migrated. It is explained further and I would like to avoid misunderstanding here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I conflated the two. Fixed in 1b3a222 — the section now opens:

Thread-safe capability is declared by a single mechanism: the [MSBuildMultiThreadableTask] attribute. A task may additionally implement the IMultiThreadableTask interface to gain access to thread-safe APIs. The two do different jobs and are not alternatives:

I also strengthened the attribute-only bullet, which undersold the same point — it said "a supported state", now:

Attribute only — a complete, properly migrated state for a task that does not resolve relative paths or read environment variables.

Declaring one without the other is legal, and each half fails quietly:

- **Attribute only** — a supported state. The task runs in-process without `TaskEnvironment`, which is correct for a task that does not resolve relative paths or read environment variables. If the task *does* declare a `TaskEnvironment` property, MSBuild never assigns it: the property silently retains whatever the task itself initialized it to — commonly `TaskEnvironment.Fallback`, or `null` when there is no initializer — so paths resolve against the shared process working directory. The task-authoring analyzer reports `MSBuildTask0012` for this shape.
- **Interface only** — a useful intermediate state. The task resolves paths correctly but still pays for a TaskHost. `MSBuildTask0013` reports it, disabled by default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uff, out of my mind, I am not fully sure whether the msbuild would set the environment in this case. Probably yes. Can we verify that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does. Let me see if I can add a test for that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified — and my "yes it does" above was wrong. Thanks for pushing on it.

MSBuild does not assign the property in the TaskHost. TaskExecutionHost.cs#L545 is the in-proc path only. The out-of-proc path is OutOfProcTaskAppDomainWrapperBase.CreateTask, which passes TaskEnvironment.Fallback to TaskLoader.CreateTask solely so that a task whose only constructor takes a TaskEnvironment stays instantiable. It never touches the property, and its own comment says why:

The out-of-proc task host runs tasks in multi-process mode, where each process provides its own isolated environment.

It still works, but through two mechanisms rather than injection:

  1. TaskEnvironment.Fallback is new(MultiProcessTaskEnvironmentDriver.Instance) (TaskEnvironment.cs:34) — it reads real process state, which is correct in a host process dedicated to one task.
  2. The task keeps its own initializer. Every in-box declaration sets = TaskEnvironment.Fallback, including ToolTask.cs#L217 — 36 declarations, none uninitialized.

So the "resolves paths correctly" claim holds, but the reason is process isolation plus the task's own default. The bullet now says that explicitly in 1b3a222.

On the test: one already exists and covers exactly this — TaskHost_MultiThreadableTask_Tests.InheritedTask_InTaskHost_HasUsableTaskEnvironment builds a project with TaskFactory="TaskHostFactory", asserts the task really ran in the task host, and asserts it read TaskEnvironment.ProjectDirectory without an NRE. Its comment names the mechanism as "the inherited default", not injection. So I did not add a duplicate.

One corollary worth flagging: an IMultiThreadableTask whose property is an auto-property with no initializer would be null out-of-proc, since nothing assigns it there. Nothing in-box is in that shape, which is presumably why it has never bitten us. Say the word if you want a rule for it — otherwise I will leave it alone.

- **Attribute only** — a supported state. The task runs in-process without `TaskEnvironment`, which is correct for a task that does not resolve relative paths or read environment variables. If the task *does* declare a `TaskEnvironment` property, MSBuild never assigns it: the property silently retains whatever the task itself initialized it to — commonly `TaskEnvironment.Fallback`, or `null` when there is no initializer — so paths resolve against the shared process working directory. The task-authoring analyzer reports `MSBuildTask0012` for this shape.
- **Interface only** — a useful intermediate state. The task resolves paths correctly but still pays for a TaskHost. `MSBuildTask0013` reports it, disabled by default.

The interface cannot also serve as the routing signal, because `ToolTask` implements `IMultiThreadableTask`. Routing on the interface would opt in every `ToolTask`-derived task in the ecosystem, none of which have been reviewed for thread safety.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this information in specs? This is an alternative design question and not information what is happening in the code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. Moved to the existing ## Appendix: Alternatives section in 1b3a222, which is already where the design alternatives live, as a new "Alternative: Routing on IMultiThreadableTask Instead of the Attribute" entry. The body of the spec now just states the behaviour.

}
```

Deriving from `MultiThreadableTask` supplies the interface but not the attribute, so a derived task must still apply `[MSBuildMultiThreadableTask]` to run in-process.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we decided not to provide the MultiThreadableTask. We should remove this whole section.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the whole section in 1b3a222 — the prose, the code block, and the sentence I had added to it. Confirmed there is no such type: git grep 'class MultiThreadableTask' under src/ returns only analyzer/test types with similar names, nothing in Microsoft.Build.Utilities.

I kept the following paragraph, since it is about built-in tasks rather than the abstract class and is still accurate:

Built-in MSBuild tasks initialize TaskEnvironment with a MultiProcessTaskEnvironmentDriver-backed default. [...]

```

MSBuild detects `MSBuildMultiThreadableTaskAttribute` by its namespace and name only, ignoring the defining assembly, which allows customers to define the attribute in their own assemblies alongside their tasks. Since MSBuild does not ship the attribute, customers using newer MSBuild versions should prefer the Interface-Based Thread-Safe Capability Declaration.
MSBuild detects `MSBuildMultiThreadableTaskAttribute` by its namespace and name only, ignoring the defining assembly, which allows customers to define the attribute in their own assemblies alongside their tasks. Because the attribute is the routing signal, every task that should run in-process must carry it — including tasks that also implement `IMultiThreadableTask`. The attribute is not inherited (`Inherited = false`, and `TaskRouter` reads it with `inherit: false`), so it must be applied to each concrete task class rather than to a shared base.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Because the attribute is the routing signal, every task that should run in-process must carry it — including tasks that also implement IMultiThreadableTask "I think at this point we repeat this too much in this document. It is getting too verbose.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — dropped that sentence in 1b3a222. The paragraph now keeps only the part that is not stated anywhere else, the non-inheritance:

MSBuild detects MSBuildMultiThreadableTaskAttribute by its namespace and name only, ignoring the defining assembly, which allows customers to define the attribute in their own assemblies alongside their tasks. The attribute is not inherited (Inherited = false, and TaskRouter reads it with inherit: false), so it must be applied to each concrete task class rather than to a shared base.

/// This class should only be used when in multi-threaded mode. Traditional multi-proc builds
/// have different semantics and should not use this routing logic.
/// <para>
/// The attribute is the only routing signal. Implementing <see cref="Microsoft.Build.Framework.IMultiThreadableTask"/>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to repeat this point? I think spec and summary should make it clear enough.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep it here as well as not everyone reads the spec. The source code is the authority at the end of the day.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the verbosity point even while keeping it here — condensed from five lines to four in 1b3a222, dropping the restatement of what the attribute does and keeping only why the interface is deliberately not consulted:

/// The attribute is the only routing signal. IMultiThreadableTask is
/// deliberately not consulted here: Microsoft.Build.Utilities.ToolTask implements it, so honoring it
/// would silently opt in every ToolTask-derived task in the ecosystem. The interface instead controls
/// TaskEnvironment injection, which TaskExecutionHost handles separately.

}

[Fact]
public async Task NonTaskTypeWithTaskEnvironmentProperty_DoesNotProduceDiagnostic()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, maybe we should call this out as warning instead? the attribute is supposed to be used only on tasks?

@ViktorHofer ViktorHofer Aug 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — implemented as MSBuildTask0014 (Warning, on by default) in 1b3a222. TaskRouter only ever inspects types the engine is about to run as a task, so on anything that is not an ITask the attribute is genuinely inert.

The shape I actually care about is the second test:

[MSBuildMultiThreadableTask]     // ⚠️ MSBuildTask0014
public class MyTaskHelper { ... }

public class MyTask : Task       // the real task, still routed to a TaskHost
{
    public override bool Execute() => true;
}

The attribute lands on the wrong class of a multi-class file, and the task is silently left unmigrated — which is exactly the kind of failure this PR is trying to make loud. This turned up for real during the Arcade migration.

Three tests added: the plain non-task case, the helper-beside-task case, and a negative case for a non-task type without the attribute.

@ViktorHofer ViktorHofer Aug 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Widened the rule to cover the abstract case too, in 3f7af01 — you were asking about exactly this shape.

TaskRouter reads the attribute with inherit: false, off the concrete type the engine has just instantiated. So it only takes effect on a non-abstract class implementing ITask, and there are two inert shapes rather than one:

  • not a task at all — nothing ever reads it;
  • an abstract task — the engine never instantiates that type, and no subclass inherits the attribute, so every concrete task deriving from it is still routed to a TaskHost while the base looks migrated.

The second one is the quieter of the two: annotating a shared base is an easy thing to do, and nothing else in the system signals that the derived tasks did not actually opt in. The rule is now "attribute has no effect on this type" with the reason carried in the message. Both shapes stay zero-false-positive.

ViktorHofer and others added 2 commits August 25, 2026 12:19
…SBuildTask0014

Spec:
- The attribute alone is the capability declaration; the interface is an
  additional mechanism for TaskEnvironment access. A task needing only the
  attribute is fully migrated.
- Correct the "interface only" bullet: the out-of-proc host does not assign
  the property. It supplies TaskEnvironment.Fallback to a TaskEnvironment
  constructor when one is declared, and otherwise leaves the property at the
  task's own default, which is correct because Fallback is backed by
  MultiProcessTaskEnvironmentDriver and the host process is dedicated.
- Remove the MultiThreadableTask abstract class section; that type was never
  added.
- Move the ToolTask routing rationale to the existing Alternatives appendix
  and drop a repeated sentence from the attribute section.

Analyzer:
- Add MSBuildTask0014, a warning for [MSBuildMultiThreadableTask] applied to
  a type that does not implement ITask, where nothing reads it. The common
  cause is the attribute landing on a helper type beside the real task,
  leaving the task unmarked and still routed to a TaskHost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
TaskRouter reads [MSBuildMultiThreadableTask] with inherit: false, off the
concrete type the engine has just instantiated as a task. The attribute
therefore only takes effect on a non-abstract class that implements ITask.

The rule previously covered only one of the two inert shapes. An abstract task
carrying the attribute is equally inert, and quieter: the engine never
instantiates that type, and because the attribute is not inherited every
concrete subclass is still routed to an out-of-proc TaskHost while looking
fully migrated. Both shapes usually mean the attribute landed on the wrong
class -- a helper type beside the real task, or a shared base instead of each
task deriving from it.

Rule is now 'attribute has no effect on this type', with the reason carried in
the message. Both shapes remain zero-false-positive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
@ViktorHofer
ViktorHofer requested a review from AR-May August 25, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants