|
| 1 | +# project.assets.json should indicate analyzer assets |
| 2 | +<!-- Replace `Title` with an appropriate title for your design --> |
| 3 | + |
| 4 | +- Author Name: [kfikadu](https://github.com/kfikadu) |
| 5 | +- GitHub Issue: [Issue #6279](https://github.com/NuGet/Home/issues/6279) |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +`project.assets.json` does not currently record which analyzers a package contributes. |
| 10 | +Because that information is missing, the .NET SDK has to work around the gap by scanning each package's files to discover analyzers itself. |
| 11 | +That workaround has no knowledge of asset filtering, so analyzers are applied to the project's compilation even when `PrivateAssets` or `ExcludeAssets` should exclude them. |
| 12 | +Surfacing analyzer assets in `project.assets.json` closes this gap and gives the SDK the information it needs to decide which analyzers to apply. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +- Analyzer assets currently don't respect standard NuGet asset filtering options like `ExcludeAssets` and `PrivateAssets`. |
| 17 | +- Developers can't control analyzer inclusion in their projects, leading to unexpected warnings or errors. |
| 18 | +- Users are required to use custom MSBuild scripts or abandon packages due to inability to control analyzer inclusion. |
| 19 | +- Package authors can't reliably control analyzer distribution |
| 20 | + |
| 21 | +## Explanation |
| 22 | + |
| 23 | +### Functional explanation |
| 24 | + |
| 25 | +Enable analyzer assets to respect asset filtering options like other asset types. |
| 26 | +When enabled via `<RestoreEnableAnalyzerAssets>true</RestoreEnableAnalyzerAssets>`: |
| 27 | +- Analyzers will be tracked in the `project.assets.json` file under a new "analyzers" group. |
| 28 | +- PrivateAssets/ExcludeAssets will correctly filter analyzers, preventing them from being included in projects that should not have them. |
| 29 | + |
| 30 | +This feature will initially be behind a feature flag defined as `<RestoreEnableAnalyzerAssets>`. |
| 31 | +This property should be set to true in the project file. |
| 32 | +This is done to avoid a breaking change. |
| 33 | + |
| 34 | +### Technical explanation |
| 35 | + |
| 36 | +#### File Structure |
| 37 | + |
| 38 | +All `.dll` files under `analyzers/` are tracked, at any depth, excluding satellite `.resources.dll` assemblies. |
| 39 | +This mirrors the SDK's analyzer discovery, so the assets file does not assume a fixed folder layout. |
| 40 | + |
| 41 | +NuGet decides whether a file is an analyzer purely from the `analyzers/` prefix and the `.dll` extension. |
| 42 | +The remaining path segments are not used to decide whether a file is tracked, so every matching analyzer is listed regardless of its language or compiler version. |
| 43 | +NuGet inspects the optional language and compiler-version segments only to derive selection metadata: |
| 44 | + |
| 45 | +|Path segment|Metadata derived| |
| 46 | +|---|---| |
| 47 | +|A `cs`, `vb`, or `fs` segment (e.g. `analyzers/dotnet/cs/{name}.dll`)|`codeLanguage`; defaults to `any` when no language segment is present| |
| 48 | +|A `roslynX.Y` segment (e.g. `analyzers/dotnet/roslyn4.0/cs/{name}.dll`)|`compilerApiVersion`; omitted when no such segment is present| |
| 49 | + |
| 50 | +The `{language}` and `{compilerApiVersion}` segments are optional and may appear at any depth. |
| 51 | +The SDK uses this metadata to select which analyzers to apply, the same way `codeLanguage` is used for content files. |
| 52 | +Because all analyzers are listed, an `analyzers/dotnet/fs/MyAnalyzer.dll` entry still appears for a C# project, but the SDK does not apply it since its `codeLanguage` is `fs`. |
| 53 | +Excluded analyzers (for example via `PrivateAssets` or `ExcludeAssets`) are written as a `analyzers/.../_._` placeholder with no metadata. |
| 54 | + |
| 55 | +Examples: |
| 56 | +- `analyzers/dotnet/cs/MyAnalyzer.dll` (C# analyzer) |
| 57 | +- `analyzers/dotnet/vb/MyAnalyzer.dll` (VB.NET analyzer) |
| 58 | +- `analyzers/dotnet/roslyn4.0/cs/MyAnalyzer.dll` (C# analyzer for Roslyn 4.0+) |
| 59 | + |
| 60 | +**NuGet Changes** |
| 61 | + |
| 62 | +- Add "analyzers" group to project.assets.json during restore. |
| 63 | +- Respect `PrivateAssets` and `ExcludeAssets` when populating this group. |
| 64 | + |
| 65 | +**SDK Changes** |
| 66 | + |
| 67 | +Update SDK asset resolution logic (e.g., ResolvePackageAssets.cs) to: |
| 68 | +- Read analyzer assets directly from the "analyzers" group in "targets", instead of scanning all files in "libraries". |
| 69 | +- Only load analyzers that are listed in this group. |
| 70 | +- Select the applicable analyzers from the group using the `codeLanguage` and `compilerApiVersion` metadata (the group lists analyzers for all languages and compiler versions), mirroring how `codeLanguage` selects content files. When several analyzers apply to the same language, the SDK picks the highest applicable compiler API version. |
| 71 | +- When `<RestoreEnableAnalyzerAssets>` is set to true, the SDK will only use the analyzer group that is in the assets file to determine which analyzers to include. |
| 72 | +- If the analyzers group is missing from the assets file and the feature flag is enabled, the SDK won't fall back to legacy scanning. |
| 73 | +- If the feature flag is not set or is false, the SDK will use the legacy scanning behavior to discover analyzers and preserve compatibility. |
| 74 | + |
| 75 | +**Example Output** |
| 76 | +```json |
| 77 | +"version": 4, |
| 78 | +"targets": { |
| 79 | + ".NETCoreApp,Version=v11.0": { |
| 80 | + "My.Analyzer.Package/1.0.0": { |
| 81 | + "type": "package", |
| 82 | + "compile": { ... }, |
| 83 | + "runtime": { ... }, |
| 84 | + "analyzers": { |
| 85 | + "analyzers/dotnet/cs/MyAnalyzer.dll": { |
| 86 | + "codeLanguage": "cs" |
| 87 | + }, |
| 88 | + "analyzers/dotnet/roslyn4.0/cs/MyAnalyzer.dll": { |
| 89 | + "codeLanguage": "cs", |
| 90 | + "compilerApiVersion": "roslyn4.0" |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +## Private Assets and Transitive Behavior |
| 99 | + |
| 100 | +### Current Behavior (Without Feature Flag) |
| 101 | + |
| 102 | +| Attribute | What It Should Do | What Happens | |
| 103 | +|-----------|-------------------|--------------| |
| 104 | +| `PrivateAssets="analyzers"` | Prevent analyzers from flowing to dependent projects | Analyzers still flow transitively | |
| 105 | +| `ExcludeAssets="analyzers"` | Exclude analyzers from this project entirely | Analyzers still included | |
| 106 | +| `IncludeAssets="compile;runtime"` | Only include compile and runtime assets (exclude analyzers) | **Analyzers still included** - not controlled by asset filtering | |
| 107 | + |
| 108 | +### New Behavior (With RestoreEnableAnalyzerAssets=true) |
| 109 | + |
| 110 | +**Scenario 1: Library with PrivateAssets** |
| 111 | +```xml |
| 112 | +<!-- Library.csproj --> |
| 113 | +<PackageReference Include="MyAnalyzer" Version="1.0.0" |
| 114 | + PrivateAssets="analyzers" /> <!--default value--> |
| 115 | +``` |
| 116 | + |
| 117 | +**Library's project.assets.json:** |
| 118 | +```json |
| 119 | +"MyAnalyzer/1.0.0": { |
| 120 | + "type": "package", |
| 121 | + "analyzers": { |
| 122 | + "analyzers/dotnet/cs/MyAnalyzer.dll": { |
| 123 | + "codeLanguage": "cs" |
| 124 | + } |
| 125 | + } |
| 126 | +} |
| 127 | +``` |
| 128 | +**App's project.assets.json (references Library):** |
| 129 | +```json |
| 130 | +"MyAnalyzer/1.0.0": { |
| 131 | + "type": "package", |
| 132 | + "analyzers": { |
| 133 | + "analyzers/dotnet/cs/_._": {} |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +**Scenario 2: App with ExcludeAssets** |
| 139 | +```xml |
| 140 | +<!-- App.csproj --> |
| 141 | +<PackageReference Include="LibraryWithAnalyzers" Version="1.0.0" |
| 142 | + ExcludeAssets="analyzers" /> |
| 143 | +``` |
| 144 | +**App's project.assets.json:** |
| 145 | +```json |
| 146 | +"LibraryWithAnalyzers/1.0.0": { |
| 147 | + "type": "package", |
| 148 | + "analyzers": { |
| 149 | + "analyzers/dotnet/cs/_._": {} |
| 150 | + } |
| 151 | +} |
| 152 | +``` |
| 153 | +The excluded analyzers are written as a `_._` placeholder, consistent with how other excluded asset groups (such as `compile` and `runtime`) are represented. |
| 154 | + |
| 155 | +### How Transitivity Works |
| 156 | + |
| 157 | +``` |
| 158 | +Project A -> Project B -> Package C (with analyzers) |
| 159 | +``` |
| 160 | +- **Default Behavior of Analyzers:** `PrivateAssets=build;contentFiles;analyzers` |
| 161 | +- **Without PrivateAssets:** Analyzers flow from C to B to A |
| 162 | +- **With PrivateAssets on B's reference to C:** Analyzers stop at B |
| 163 | + |
| 164 | +## Breaking Changes |
| 165 | + |
| 166 | +### No Breaking Changes Initially |
| 167 | + |
| 168 | +Initially this feature is opt-in and is only honored for projects targeting .NET 11 or greater; on earlier target frameworks the property is ignored. Enabling it by default for the latest TFM is a later step in the rollout. |
| 169 | + |
| 170 | +The opt-in is via `<RestoreEnableAnalyzerAssets>true</RestoreEnableAnalyzerAssets>`, so existing projects will not be affected until they explicitly enable it on a supported target framework. |
| 171 | + |
| 172 | +### When Enabled by Default |
| 173 | + |
| 174 | +Projects using `PrivateAssets="analyzers"` or `ExcludeAssets="analyzers"` will start working as intended |
| 175 | + |
| 176 | +**Build Behavior Changes:** |
| 177 | +- Missing analyzer diagnostics in dependent projects |
| 178 | +- Different warning/error counts in build output |
| 179 | +- TreatWarningsAsErrors outcomes may change |
| 180 | + |
| 181 | +**CI/CD Pipeline Impact:** |
| 182 | +- Builds expecting certain analyzers may fail |
| 183 | +- Builds may pass that previously failed on analyzer warnings |
| 184 | +- Since analyzers are also source generators, it may fail because transitive analyzers that were included are no longer applied due to them respecting asset filtering. |
| 185 | + |
| 186 | +**Mitigation:** |
| 187 | +```xml |
| 188 | +<PropertyGroup> |
| 189 | + <RestoreEnableAnalyzerAssets>false</RestoreEnableAnalyzerAssets> |
| 190 | +</PropertyGroup> |
| 191 | +``` |
| 192 | + |
| 193 | +### Rollout Strategy |
| 194 | + |
| 195 | +The initial rollout is opt-in via `<RestoreEnableAnalyzerAssets>true</RestoreEnableAnalyzerAssets>`, and the opt-in is only available for projects targeting .NET 11 or greater; on earlier target frameworks it is ignored. |
| 196 | +The next step is to enable it by default for projects that target .NET 11 or greater, while remaining opt-in elsewhere. |
| 197 | +The last step will be to remove the feature flag. |
| 198 | + |
| 199 | + |
| 200 | +## Drawbacks |
| 201 | + |
| 202 | +- Requires coordinated changes to NuGet and SDK |
| 203 | +- Breaking change when enabled by default |
| 204 | +- Potential minor performance impact during restore |
| 205 | +- Older tools may not understand the new project.assets.json format |
| 206 | + |
| 207 | +## Rationale and alternatives |
| 208 | + |
| 209 | +This design is chosen to align with existing NuGet asset filtering patterns while providing a clear, structured way to manage analyzers. |
| 210 | +By introducing a new "analyzers" group in `project.assets.json`, it allows for consistent handling of analyzers similar to other asset types, while respecting existing filtering options like `PrivateAssets` and `ExcludeAssets`. |
| 211 | +It also maintains backward compatibility by being opt-in via a feature flag, allowing developers to adopt it gradually without breaking existing projects. |
| 212 | +<!-- Why is this the best design compared to other designs? --> |
| 213 | +<!-- What other designs have been considered and why weren't they chosen? --> |
| 214 | +<!-- What is the impact of not doing this? --> |
| 215 | + |
| 216 | +## Prior Art |
| 217 | + |
| 218 | +NuGet's existing asset filtering system (compile, runtime assets) provides the model. |
| 219 | + |
| 220 | +**Content Files** |
| 221 | +Language-specific content selection (`contentFiles/{language}/`) demonstrates similar filtering patterns already work in the ecosystem. |
| 222 | + |
| 223 | +The SDK is able to determine the project language by the type of project file: |
| 224 | +- `.csproj` files are treated as C# projects |
| 225 | +- `.vbproj` files are treated as VB.NET projects |
| 226 | + |
| 227 | +The NuGet package can contain analyzers and content files for multiple languages, and they are sorted into folders such as `contentFiles/cs/` and `contentFiles/vb/`. |
| 228 | +During the restore process, NuGet lists all of the content files into the `project.assets.json` file, regardless of the language. |
| 229 | + |
| 230 | +In the `project.assets.json` file, each content file entry includes metadata, such as `"buildAction"`, `"codeLanguage"`, `"copyToOutput"`, etc. |
| 231 | +- The `codeLanguage` property is what enables the SDK to filter and include the files that are relevant to the project's language |
| 232 | + |
| 233 | +For each file, MSBuild checks the `"codeLanguage"` property to determine if it should be included in the build |
| 234 | +- If `"codeLanguage"` matches the project language, the file is included in the build. |
| 235 | +As a result, only the correct assets for the current project's are compiled, copied, or referenced. |
| 236 | + |
| 237 | +This is similar to language-specific analyzer selection (`analyzers/dotnet/{language}/`). |
| 238 | +- The SDK determines the project language from the project file type (e.g., `.csproj`, `.vbproj`, `.fsproj`) |
| 239 | + |
| 240 | +Example: |
| 241 | +- If a package contains `analyzers/dotnet/cs/Analyzer.dll` and `analyzers/dotnet/vb/VBAnalyzer.dll`: |
| 242 | + - The C# project will only include `Analyzer.dll` |
| 243 | + - The VB.NET project will only include `VBAnalyzer.dll` |
| 244 | + |
| 245 | +## Unresolved Questions |
| 246 | + |
| 247 | +<!-- What parts of the proposal do you expect to resolve before this gets accepted? --> |
| 248 | +<!-- What parts of the proposal need to be resolved before the proposal is stabilized? --> |
| 249 | +<!-- What related issues would you consider out of scope for this proposal but can be addressed in the future? --> |
| 250 | + |
| 251 | +## Future Possibilities |
| 252 | + |
| 253 | +This is a feature that is going to be enabled by default for all builds. |
| 254 | + |
| 255 | +Something that would be valuable would be an analysis of NuGet packages that have analyzers, and focusing on analyzers as dependencies. |
| 256 | +It would also look at the impact of the `IncludeAssets` and `ExcludeAssets` settings. |
| 257 | + |
| 258 | +Ideally, this analysis would: |
| 259 | +- Identify packages that rely on analyzers as dependencies and determine whether the new asset selection mechanics would change their behavior. |
| 260 | +- Provide data on how many packages (and consumers) would be impacted by a change in default behavior, helping to mitigate surprises or unintended consequences. |
| 261 | +- Affirm the correctness and robustness of the proposed design prior to broad deployment. |
| 262 | + |
| 263 | +Also, provide analysis on source generators as analyzers. Since it is possible for people to depend on source generators in a way that their build will fail without them, there should be data on which ones may fail. |
| 264 | + |
| 265 | +Additional follow up should also be done in Component Governance. Currently, packages that exclusively contribute to `Compile` assets are treated as development dependencies. |
| 266 | +There are special case analyzers, such as in the SDK, that should be removed once this feature is implemented. |
| 267 | + |
| 268 | +We should also consider creating a dedicated design time package graph. This will explicitly resolve and track packages that are meant to run as analyzers or tasks, making the process more accurate and maintainable |
| 269 | +<!-- What future possibilities can you think of that this proposal would help with? --> |
| 270 | + |
| 271 | +### References |
| 272 | + |
| 273 | +- [Controlling dependency assets](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#controlling-dependency-assets) |
| 274 | +- [Analyzer conventions](https://learn.microsoft.com/en-us/nuget/create-packages/analyzers) |
0 commit comments