Skip to content

Commit 9af8119

Browse files
kfikadumartinrrm
andauthored
[spec] Indicate analyzer assets and respect analyzer filtering in project.assets.json (#14455)
* Design spec for issue 6279 * Updated spec based on comments * Updated spec with more examples and information * Updated spec to resolve comments * Addressing comments * update rollout + cleanup * Update spec with addressed comments and new rollout strategy * PR comments --------- Co-authored-by: Martin Ruiz <martin.ruiz.mares@gmail.com>
1 parent c717831 commit 9af8119

1 file changed

Lines changed: 274 additions & 0 deletions

File tree

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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

Comments
 (0)