Add analyzer section in project.assets.json file#7464
Conversation
jebriede
left a comment
There was a problem hiding this comment.
Approving with a few suggestions.
| private static bool IsAnalyzerAssetPath(string path) | ||
| { | ||
| return path.StartsWith("analyzers/", StringComparison.Ordinal) | ||
| && path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) |
There was a problem hiding this comment.
The "analyzers/" prefix check is StringComparison.Ordinal (case-sensitive) while the .dll / .resources.dll checks are OrdinalIgnoreCase. If a package authored the folder as Analyzers/ this would skip it while the SDK's discovery (which we're mirroring) may still pick it up. Was case-sensitivity on the folder segment intentional? If not, consider OrdinalIgnoreCase for consistency with the extension checks.
There was a problem hiding this comment.
Yes this was intentional, we are mirroring sdk analysis discovery from https://github.com/dotnet/sdk/blob/8283adc1579c8bc2afb4458c81cbda0c6eac4466/src/Common/NuGetUtils.NuGet.cs#L43-L53.
jebriede
left a comment
There was a problem hiding this comment.
Approving with an optimization suggestion.
nkolev92
left a comment
There was a problem hiding this comment.
I think we're introducing a new implementation pattern in a couple of different places, but we already have an established one.
| return lockFileItems; | ||
| } | ||
|
|
||
| private static bool IsAnalyzerAssetPath(string path) |
There was a problem hiding this comment.
I think we should be using ManagedCodeConventions here.
That's what knows how to find the patterns.
There was a problem hiding this comment.
Added an AnalyzerAssemblies PatternSet to ManagedCodeConventions (a terminal {analyzerAssembly} token matching any .dll under analyzers/ at any depth, excluding .resources.dll satellites) and removed the hand-rolled IsAnalyzerAssetPath .
GetAnalyzerLockFileItems now consumes contentItems.FindItems(...). I kept the metadata derivation (codeLanguage / compilerApiVersion) as a segment scan rather than content-model patterns, because the pattern engine extracts properties at fixed positions and the language segment isn't at a fixed position in practice.
I sampled 12 popular analyzer packages (67 analyzer DLLs): 66% put the language after the compiler version (analyzers/dotnet/roslynX.Y/cs/…), with the language appearing at depth 1, 2, or 3 depending on the package. Expressing that in the content model would need an enumerated set of depth-specific patterns plus new parsers and would silently mis-tag anything deeper. The scan handles all positions uniformly. Happy to revisit if you'd prefer the pattern approach.
There was a problem hiding this comment.
Looking at the implementation, it looks like we can add multiple PatternDefinition(s) to locate analyzer assemblies. For example, the below implementation shows that there are 3 patterns to identify a runtime assembly. If we know the exact patterns for analyzers, then I think we can add multiple patterns to locate the analyzer assembly.
There was a problem hiding this comment.
I agree with @kartheekp-ms
I think it's possible to use mutliple pattern definitions.
Or at least we should try. That way we don't have to worry about maintainbility of that code at all, since it's generic.
There was a problem hiding this comment.
Did you have a chance to try this out?
| /// the group has items. Empty groups are left alone. | ||
| /// </summary> | ||
| private static void ClearIfExists<T>(IList<T> group, Func<string, T> factory) where T : LockFileItem | ||
| private static void ClearIfExists<T>(IList<T> group, Func<string, T> factory, bool copyProperties = true) where T : LockFileItem |
There was a problem hiding this comment.
Can you explain what are the properties we're stopping from copying in the analyzers scenario?
There was a problem hiding this comment.
The analyzer LockFileItems carry two metadata properties codeLanguage (cs / vb / fs / any) and, when present, compilerApiVersion ( roslynX.Y )
There was a problem hiding this comment.
So we're basically removing this for analyzers?
Aren't those analyzer specific?
|
This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 7 days of this comment. If it is closed, you may reopen it anytime when you're ready again, as long as you don't delete the branch. |
…ventions detection Move the RestoreEnableAnalyzerAssets opt-in gating into code, per target framework. The value now flows through TargetFrameworkInformation and is read by every restore entry point, including static-graph restore which previously ignored the property entirely. Removes the project-level ProjectRestoreMetadata.RestoreEnableAnalyzerAssets and the NuGet.targets OR. Detect analyzers via a new AnalyzerAssemblies pattern in ManagedCodeConventions instead of the hand-rolled IsAnalyzerAssetPath, so detection is shared with the content model and matches the analyzers folder case-insensitively. Clarify the analyzer metadata derivation comment and trim an allocation in the compiler-version normalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6d6559e to
963cbee
Compare
Add five properties to the ProjectRestoreInformation event to size the impact of enabling RestoreEnableAnalyzerAssets by default before the rollout: - AnalyzerAssets.Excluded.Count - AnalyzerAssets.PackagesWithAnalyzers.Count - AnalyzerAssets.PackagesWithExcludedAnalyzers.Count - AnalyzerAssets.ExcludedByPrivateAssets.Count - AnalyzerAssets.ExcludedByExcludeAssets.Count The counts are derived from the resolved dependency graphs and the package file lists, so they are reported on every full restore regardless of whether the feature is currently enabled. This lets us measure how many packages and analyzer assemblies would stop being applied once PrivateAssets/ExcludeAssets are honored. AnalyzerAssets.Count is redefined as the number of analyzer assemblies that would apply (computed the same way), replacing the previous count of analyzer assets written to the assets file. Analyzer detection mirrors ManagedCodeConventions.ManagedCodePatterns. AnalyzerAssemblies as a cheap string check, avoiding a content-item collection allocation per package on the restore path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
963cbee to
b7099ed
Compare
Address spec review feedback (Client.Engineering#3855): the rollout decision is driven by whether a package's analyzers are affected, not by how many individual analyzer assemblies it ships. - Remove AnalyzerAssets.Count (per-assembly applied count). - Replace AnalyzerAssets.Excluded.Count with a binary AnalyzerAssets.Excluded (true when any package's analyzers would be filtered out for the project). The package-level counts (PackagesWithAnalyzers.Count, PackagesWithExcludedAnalyzers.Count, ExcludedByPrivateAssets.Count, ExcludedByExcludeAssets.Count) are unchanged. Per-package detection now uses a HashSet rather than per-assembly counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR feedback (#7464): use the NoAllocEnumerate helper for the IList iterations in PopulateAnalyzerAssetsTelemetry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR feedback (#7464): look up TargetFrameworkInformation by (framework, alias) via GetNearestTargetFramework instead of framework-only, in LockFileBuilder and the analyzer-assets telemetry, so multi-targeted projects with aliased frameworks resolve to the correct target framework. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR feedback (#7464): treat RestoreEnableAnalyzerAssets as a project-wide opt-in instead of per target framework. When any target framework opts in, analyzer assets are honored for every target framework, mirroring how package pruning is enabled project-wide once any framework qualifies (PackageSpecFactory.GetPackagePruningDefault). The per-framework opt-in is already gated to .NET 11+ in NuGet.targets, so only a qualifying framework can enable it. Add a multi-targeted restore test asserting a non-opted-in framework still honors analyzers when another framework opts in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR feedback (#7464): analyzer assets restore was force-disabled below .NET 11 even when explicitly opted in. Mirror package pruning instead — default to off, but honor an explicit RestoreEnableAnalyzerAssets opt-in (or opt-out) on any target framework, so users can opt into the new behavior before it is enabled by default. The net11 version gate only ever forced off the explicit opt-in; a future default-on can gate itself the way RestorePackagePruningDefault does. Update the availability tests: the opt-in is now honored on all frameworks, and the value defaults to false when unset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| Imports = cloneFrom.Imports; | ||
| AssetTargetFallback = cloneFrom.AssetTargetFallback; | ||
| Warn = cloneFrom.Warn; | ||
| RestoreEnableAnalyzerAssets = cloneFrom.RestoreEnableAnalyzerAssets; |
There was a problem hiding this comment.
I think I brought it up somewhere else, but I'm curious what @zivkan thinks too.
I'm wondering if this should be on https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.ProjectModel/ProjectRestoreMetadata.cs, similar to CPM, Audit and other non TFM props.
Pruning has props that are per project and this doesn't.
| TargetAlias = targetAlias, | ||
| Warn = warn | ||
| Warn = warn, | ||
| RestoreEnableAnalyzerAssets = msBuildProjectInstance.IsPropertyTrue("RestoreEnableAnalyzerAssets") |
There was a problem hiding this comment.
See other comment, the equivalent would be RestoreAuditProperties and GetRestoreAuditProperties in MSBuildRestoreUtility.
| return lockFileItems; | ||
| } | ||
|
|
||
| private static bool IsAnalyzerAssetPath(string path) |
There was a problem hiding this comment.
Did you have a chance to try this out?
| /// the group has items. Empty groups are left alone. | ||
| /// </summary> | ||
| private static void ClearIfExists<T>(IList<T> group, Func<string, T> factory) where T : LockFileItem | ||
| private static void ClearIfExists<T>(IList<T> group, Func<string, T> factory, bool copyProperties = true) where T : LockFileItem |
There was a problem hiding this comment.
So we're basically removing this for analyzers?
Aren't those analyzer specific?
Bug
Progress: NuGet/Home#6279
SDK PR (draft): dotnet/sdk#54646
Description
Today
project.assets.jsondoes not record which analyzers a package contributes, and analyzers are applied regardless ofPrivateAssets/ExcludeAssets/IncludeAssets. This PR implements the NuGet restore half of the "indicate analyzer assets in project.assets.json" feature (spec: NuGet/Home#14455). The SDK consumption half ships as a separate dotnet/sdk PR.When a project opts in with
<RestoreEnableAnalyzerAssets>true</RestoreEnableAnalyzerAssets>, restore now:analyzersgroup under each package in the assets file, listing every analyzer assembly. Detection uses a newAnalyzerAssembliespattern inManagedCodeConventions(any.dllunderanalyzers/at any depth, excluding satellite.resources.dll), so the layout isn't assumed to be fixed and analyzer discovery is shared with the rest of the content model.PrivateAssets,ExcludeAssets, andIncludeAssetsfilter analyzers like any other asset type. Analyzers excluded or transitively suppressed are written as a_._placeholder (consistent withcompile/runtime/native).codeLanguage(cs/vb/fs/any) and, when present in the path,compilerApiVersion(roslynX.Y) — mirroring how content files carrycodeLanguage— so the SDK selects applicable analyzers from metadata instead of parsing paths. The metadata is derived by scanning the path segments, since real packages place the language at a variable depth (for exampleanalyzers/dotnet/roslynX.Y/cs/).Gating. The feature is opt-in and only honored for projects targeting .NET 11 or greater. The
.NET 11+gate is applied at evaluation time inNuGet.targets(mirroringNuGetAuditMode), and the resulting value is honored per target framework: it flows throughTargetFrameworkInformation.RestoreEnableAnalyzerAssetsand is read in code by every restore entry point — CLI/DG-spec (MSBuildRestoreUtility), VS nomination (PackageSpecFactory), and static-graph restore (MSBuildStaticGraphRestore, which previously did not honor the property at all). A multi-targetednet10.0;net11.0project therefore emits theanalyzersgroup only fornet11.0.Telemetry.
ProjectRestoreInformationreportsAnalyzerAssets.Enabled(opt-in state) andAnalyzerAssets.Count(number of analyzer assets emitted, excluding_._placeholders). Richer adoption/impact telemetry to inform the default-on rollout ships as a follow-up.Public API & serialization. Adds
TargetFrameworkInformation.RestoreEnableAnalyzerAssets,LockFileTargetLibrary.AnalyzerAssets,LockFileItem.CompilerApiVersionProperty, and theManagedCodeConventions.ManagedCodePatterns.AnalyzerAssembliespattern (with theAnalyzerAssemblyproperty name). The Newtonsoft.Json and System.Text.Json paths round-trip the newanalyzersgroup and its metadata; the per-framework restore property round-trips throughPackageSpecWriterand the streaming reader. The lock-file cache key includes the per-framework flag so toggling it invalidates correctly.PR Checklist