Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,8 @@ internal static List<TargetFrameworkInformation> GetTargetFrameworkInfos(IReadOn
PackagesToPrune = prunedReferences,
RuntimeIdentifierGraphPath = msBuildProjectInstance.GetProperty(nameof(TargetFrameworkInformation.RuntimeIdentifierGraphPath)),
TargetAlias = targetAlias,
Warn = warn
Warn = warn,
RestoreEnableAnalyzerAssets = msBuildProjectInstance.IsPropertyTrue("RestoreEnableAnalyzerAssets")
};

targetFrameworkInfos.Add(targetFrameworkInformation);
Expand Down
7 changes: 7 additions & 0 deletions src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ Copyright (c) .NET Foundation. All rights reserved.
AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp'
AND $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '10.0'))">all</NuGetAuditMode>
<NuGetAuditMode Condition=" '$(NuGetAuditMode)' == '' ">direct</NuGetAuditMode>
<!-- Analyzer assets restore is only available for projects targeting .NET 11 or greater. Force the opt-in off for older frameworks. -->
<RestoreEnableAnalyzerAssets Condition="'$(RestoreEnableAnalyzerAssets)' == 'true'
AND !('$(TargetFrameworkIdentifier)' == '.NETCoreApp'
AND '$(TargetFrameworkVersion)' != ''
AND $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '11.0')))">false</RestoreEnableAnalyzerAssets>
<RestoreEnableAnalyzerAssets Condition="'$(RestoreEnableAnalyzerAssets)' == '' AND '$(TargetFrameworks)' == ''">false</RestoreEnableAnalyzerAssets>
</PropertyGroup>

<!-- Package pruning is enabled for all projects targeting .NET 10 or greater, including multi-targeted projects. -->
Expand Down Expand Up @@ -1185,6 +1191,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<WindowsTargetPlatformMinVersion>$(WindowsTargetPlatformMinVersion)</WindowsTargetPlatformMinVersion>
<RestoreEnablePackagePruning>$(RestoreEnablePackagePruning)</RestoreEnablePackagePruning>
<RestorePackagePruningDefault>$(RestorePackagePruningDefault)</RestorePackagePruningDefault>
<RestoreEnableAnalyzerAssets>$(RestoreEnableAnalyzerAssets)</RestoreEnableAnalyzerAssets>
<NuGetAuditMode>$(NuGetAuditMode)</NuGetAuditMode>
</_RestoreGraphEntry>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ public LockFile CreateLockFile(LockFile previousLockFile,

var librariesWithWarnings = new HashSet<LibraryIdentity>();

var rootProjectStyle = project.RestoreMetadata?.ProjectStyle ?? ProjectStyle.Unknown;
var restoreMetadata = project.RestoreMetadata;
var rootProjectStyle = restoreMetadata?.ProjectStyle ?? ProjectStyle.Unknown;

// Add the targets
foreach (var targetGraph in targetGraphs
Expand Down Expand Up @@ -184,6 +185,9 @@ public LockFile CreateLockFile(LockFile previousLockFile,
// Check if warnings should be displayed for the current framework.
var tfi = project.GetTargetFramework(targetGraph.Framework);

// Analyzer assets are honored per target framework (gated to .NET 11+ via the opt-in value).
bool restoreEnableAnalyzerAssets = tfi.RestoreEnableAnalyzerAssets;

bool warnForImportsOnGraph = tfi.Warn
&& (target.TargetFramework is FallbackFramework
|| target.TargetFramework is AssetTargetFallbackFramework);
Expand Down Expand Up @@ -238,6 +242,7 @@ public LockFile CreateLockFile(LockFile previousLockFile,
dependencyType: includeFlags,
targetFrameworkOverride: null,
dependencies: graphItem.Data.Dependencies,
restoreEnableAnalyzerAssets: restoreEnableAnalyzerAssets,
cache: lockFileBuilderCache);

target.Libraries.Add(targetLibrary);
Expand All @@ -258,6 +263,7 @@ public LockFile CreateLockFile(LockFile previousLockFile,
targetFrameworkOverride: nonFallbackFramework,
dependencyType: includeFlags,
dependencies: graphItem.Data.Dependencies,
restoreEnableAnalyzerAssets: restoreEnableAnalyzerAssets,
cache: lockFileBuilderCache);
usedFallbackFramework = !targetLibrary.Equals(targetLibraryWithoutFallback);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class LockFileBuilderCache
private readonly ConcurrentDictionary<CriteriaKey, List<(List<SelectionCriteria>, bool)>> _criteriaSets =
new();

private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount), Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>> _lockFileTargetLibraryCache =
private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount, bool restoreEnableAnalyzerAssets), Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>> _lockFileTargetLibraryCache =
new();

/// <summary>
Expand Down Expand Up @@ -106,7 +106,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka
/// <summary>
/// Try to get a LockFileTargetLibrary from the cache.
/// </summary>
internal (LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List<LibraryDependency> dependencies, Func<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)> valueFactory)
internal (LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List<LibraryDependency> dependencies, bool restoreEnableAnalyzerAssets, Func<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)> valueFactory)
{
// Comparing RuntimeGraph for equality is very expensive,
// so in case of a request where the RuntimeGraph is not empty we avoid using the cache.
Expand All @@ -116,7 +116,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka
localPackageInfo = localPackageInfo ?? throw new ArgumentNullException(nameof(localPackageInfo));
var criteriaKey = new CriteriaKey(graph.TargetGraphName, framework);
var packagePath = localPackageInfo.ExpandedPath;
return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags, dependencies.Count),
return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags, dependencies.Count, restoreEnableAnalyzerAssets),
key => new Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>(valueFactory)).Value;
}

Expand Down
140 changes: 140 additions & 0 deletions src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ private readonly Dictionary<RestoreTargetGraph, Dictionary<string, LibraryInclud
private const string PackagePruningRemovablePackagesCount = "Pruning.RemovablePackages.Count";
private const string PackagePruningDirectCount = "Pruning.Pruned.Direct.Count";

// Analyzer assets names
private const string AnalyzerAssetsEnabled = "AnalyzerAssets.Enabled";
private const string AnalyzerAssetsCount = "AnalyzerAssets.Count";
private const string AnalyzerAssetsExcludedCount = "AnalyzerAssets.Excluded.Count";
private const string AnalyzerAssetsPackagesWithAnalyzersCount = "AnalyzerAssets.PackagesWithAnalyzers.Count";
private const string AnalyzerAssetsPackagesWithExcludedAnalyzersCount = "AnalyzerAssets.PackagesWithExcludedAnalyzers.Count";
private const string AnalyzerAssetsExcludedByPrivateAssetsCount = "AnalyzerAssets.ExcludedByPrivateAssets.Count";
private const string AnalyzerAssetsExcludedByExcludeAssetsCount = "AnalyzerAssets.ExcludedByExcludeAssets.Count";

internal readonly bool _enableNewDependencyResolver;
private readonly bool _isLockFileEnabled;

Expand Down Expand Up @@ -354,6 +363,7 @@ public async Task<RestoreResult> ExecuteAsync(CancellationToken token)

telemetry.TelemetryEvent[UpdatedAssetsFile] = restoreResult._isAssetsFileDirty.Value;
telemetry.TelemetryEvent[UpdatedMSBuildFiles] = restoreResult._dirtyMSBuildFiles.Value.Count > 0;
PopulateAnalyzerAssetsTelemetry(telemetry.TelemetryEvent, assetsFile, graphs, _request.Project);

return restoreResult;
}
Expand Down Expand Up @@ -408,6 +418,7 @@ private void InitializeTelemetry(TelemetryActivity telemetry, int httpSourcesCou
}

telemetry.TelemetryEvent[AuditEnabled] = auditEnabled ? "enabled" : "disabled";
telemetry.TelemetryEvent[AnalyzerAssetsEnabled] = _request.Project.TargetFrameworks.Any(e => e.RestoreEnableAnalyzerAssets);

PopulatePruningEnabledTelemetry(_request.Project, telemetry.TelemetryEvent);
}
Expand Down Expand Up @@ -453,6 +464,135 @@ internal static void PopulatePruningEnabledTelemetry(PackageSpec project, Teleme
telemetryEvent[PackagePruningFrameworksUnsupportedCount] = pruningNotApplicableCount;
}

/// <summary>
/// Reports analyzer-asset usage so the impact of enabling <c>RestoreEnableAnalyzerAssets</c> by
/// default can be measured ahead of the rollout. The counts are derived from the resolved dependency
/// graphs and the package file lists, so they are reported on every restore regardless of whether the
/// feature is currently enabled. This lets us see, before flipping the default, how many packages and
/// analyzer assemblies would stop being applied because <c>PrivateAssets</c>/<c>ExcludeAssets</c> would
/// finally be honored.
/// </summary>
/// <remarks>
/// Analyzers are not runtime-identifier specific, so only the target-framework graphs (those with a
/// null runtime identifier) are inspected to avoid counting the same package once per RID. This runs on
/// the full-restore path only (after the no-op short-circuit), where the dependency graphs are available.
/// </remarks>
private void PopulateAnalyzerAssetsTelemetry(TelemetryEvent telemetryEvent, LockFile assetsFile, List<RestoreTargetGraph> graphs, PackageSpec project)
{
// Count the analyzer assemblies each package contributes, from the always-present libraries section.
var analyzerAssemblyCountByPackage = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (LockFileLibrary library in assetsFile.Libraries)
{
int analyzerAssemblyCount = 0;
foreach (string file in library.Files)
{
if (IsAnalyzerAssemblyPath(file))
{
analyzerAssemblyCount++;
}
}

if (analyzerAssemblyCount > 0)
{
analyzerAssemblyCountByPackage[GetAnalyzerPackageKey(library.Name, library.Version)] = analyzerAssemblyCount;
}
}

int appliedAnalyzerAssemblies = 0;
int excludedAnalyzerAssemblies = 0;
int packagesWithAnalyzers = 0;
int packagesWithExcludedAnalyzers = 0;
int excludedByPrivateAssets = 0;
int excludedByExcludeAssets = 0;

foreach (RestoreTargetGraph graph in graphs)
{
// Analyzers are not runtime specific; only inspect the target framework graphs.
if (graph.RuntimeIdentifier != null)
{
continue;
}

Dictionary<string, LibraryIncludeFlags> flattenedFlags = IncludeFlagUtils.FlattenDependencyTypes(_includeFlagGraphs, project, graph);
TargetFrameworkInformation targetFrameworkInformation = project.GetTargetFramework(graph.Framework);

foreach (GraphItem<RemoteResolveResult> graphItem in graph.Flattened)
{
LibraryIdentity library = graphItem.Key;
if (library.Type != LibraryType.Package)
{
continue;
}

if (!analyzerAssemblyCountByPackage.TryGetValue(GetAnalyzerPackageKey(library.Name, library.Version), out int analyzerAssemblyCount))
{
continue;
}

packagesWithAnalyzers++;

if (!flattenedFlags.TryGetValue(library.Name, out LibraryIncludeFlags includeFlags))
{
includeFlags = ~LibraryIncludeFlags.ContentFiles;
}

if ((includeFlags & LibraryIncludeFlags.Analyzers) != LibraryIncludeFlags.None)
{
appliedAnalyzerAssemblies += analyzerAssemblyCount;
continue;
}

// The package contributes analyzers, but they would be filtered out for this project.
excludedAnalyzerAssemblies += analyzerAssemblyCount;
packagesWithExcludedAnalyzers++;

// Attribute the exclusion: a direct reference whose own IncludeAssets/ExcludeAssets drops
// analyzers is counted separately from analyzers suppressed transitively via PrivateAssets
// (the default for analyzers), since the transitive case is the surprising one for customers.
LibraryDependency directDependency = targetFrameworkInformation?.Dependencies.FirstOrDefault(
dependency => dependency.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase));

bool excludedByOwnAssetsFilter = directDependency != null
&& (directDependency.IncludeType & LibraryIncludeFlags.Analyzers) == LibraryIncludeFlags.None;

if (excludedByOwnAssetsFilter)
{
excludedByExcludeAssets++;
}
else
{
excludedByPrivateAssets++;
}
}
}

telemetryEvent[AnalyzerAssetsCount] = appliedAnalyzerAssemblies;
telemetryEvent[AnalyzerAssetsExcludedCount] = excludedAnalyzerAssemblies;
telemetryEvent[AnalyzerAssetsPackagesWithAnalyzersCount] = packagesWithAnalyzers;
telemetryEvent[AnalyzerAssetsPackagesWithExcludedAnalyzersCount] = packagesWithExcludedAnalyzers;
telemetryEvent[AnalyzerAssetsExcludedByPrivateAssetsCount] = excludedByPrivateAssets;
telemetryEvent[AnalyzerAssetsExcludedByExcludeAssetsCount] = excludedByExcludeAssets;
}

private static string GetAnalyzerPackageKey(string id, NuGetVersion version)
{
return id + "/" + version?.ToNormalizedString();
}

/// <summary>
/// Determines whether a package file path is an analyzer assembly. This intentionally mirrors the
/// detection used by ManagedCodeConventions.ManagedCodePatterns.AnalyzerAssemblies (any '.dll' under
/// 'analyzers/' at any depth, excluding satellite '.resources.dll' assemblies), but as a cheap string
/// check so analyzer packages can be counted from the package file list for telemetry even when analyzer
/// assets are not selected (the feature is off), without allocating a content-item collection per package.
/// </summary>
private static bool IsAnalyzerAssemblyPath(string path)
{
return path.StartsWith("analyzers/", StringComparison.Ordinal)
&& path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
&& !path.EndsWith(".resources.dll", StringComparison.OrdinalIgnoreCase);
}

private async Task<(RestoreResult, bool, CacheFile)> EvaluateNoOpAsync(TelemetryActivity telemetry, CacheFile cacheFile, Stopwatch restoreTime)
{
telemetry.StartIntervalMeasure();
Expand Down
Loading