Skip to content
Merged
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
36 changes: 28 additions & 8 deletions documentation/general/analyzer-redirecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,36 @@ Targeting an SDK (and hence also loading analyzers) with newer major version in

- Note that when `IAnalyzerAssemblyRedirector` is involved, Roslyn is free to not use shadow copy loading and instead load the DLLs directly.

- It is possible to opt out of analyzer redirecting by setting environment variable `DOTNET_ANALYZER_REDIRECTING=0`.
That is an unsupported scenario though and compiler version mismatch errors will likely occur.

## Details

The VSIX contains some analyzers, for example:

```
AspNetCoreAnalyzers\9.0.0-preview.5.24306.11\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll
NetCoreAnalyzers\9.0.0-preview.5.24306.7\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll
WindowsDesktopAnalyzers\9.0.0-preview.5.24306.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
WebSDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll
AspNetCoreAnalyzers\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll
NetCoreAnalyzers\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll
WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
WebSDKAnalyzers\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll
```

And metadata at `metadata.json`:

```json
{
"AspNetCoreAnalyzers": "9.0.0-preview.5.24306.11",
"NetCoreAnalyzers": "9.0.0-preview.5.24306.7",
"WindowsDesktopAnalyzers": "9.0.0-preview.5.24306.8",
"SDKAnalyzers": "9.0.100-dev",
"WebSDKAnalyzers": "9.0.100-dev",
}
```

Given an analyzer assembly load going through our `IAnalyzerAssemblyRedirector`,
we will redirect it if the original path of the assembly being loaded matches the path of a VSIX-deployed analyzer -
only segments of these paths starting after the version segment are compared,
only relevant segments (see example below) of these paths are compared,
plus the major and minor component of the versions must match.

For example, the analyzer
Expand All @@ -54,15 +69,20 @@ C:\Program Files\dotnet\sdk\9.0.100-preview.5.24307.3\Sdks\Microsoft.NET.Sdk\ana
will be redirected to

```
{VSIX}\SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
{VSIX}\SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
```

because
where `metadata.json` has `"SDKAnalyzers": "9.0.100-dev"`, because
1. the suffix `Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll` matches, and
2. the version `9.0.100-preview.5.24307.3` has the same major and minor component (`9.0`) as the version `9.0.100-dev`
(both versions are read from the paths, not DLL metadata).

Analyzers that cannot be matched will continue to be loaded from the SDK
(and will fail to load if they reference Roslyn that is newer than is in VS).

### Implementation

Analyzer DLLs are contained in transport package `VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers`.
The redirecting logic lives in "system" VS extension `Microsoft.Net.Sdk.AnalyzerRedirecting`.

[torn-sdk]: https://github.com/dotnet/sdk/issues/42087
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.Build.NoTargets">
<Project Sdk="Microsoft.Build.NoTargets">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
Expand All @@ -21,6 +21,7 @@
<Target Name="GenerateLayout" Condition="'$(IsPackable)' == 'true'" DependsOnTargets="ResolveProjectReferences">
<PropertyGroup>
<SdkRuntimeAnalyzersSwrFile>$(ArtifactsNonShippingPackagesDir)VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.swr</SdkRuntimeAnalyzersSwrFile>
<SdkRuntimeAnalyzersMetadataFile>$(OutputPath)\metadata.json</SdkRuntimeAnalyzersMetadataFile>
</PropertyGroup>

<ItemGroup>
Expand All @@ -35,21 +36,51 @@
<Error Condition="'@(RedistRuntimeAnalyzersContent)' == ''" Text="The 'RedistRuntimeAnalyzersContent' items are empty. This shouldn't happen!" />
<Error Condition="'@(RedirectingRuntimeAnalyzersContent)' == ''" Text="The 'RedirectingRuntimeAnalyzersContent' items are empty. This shouldn't happen!" />

<!-- The custom task cannot access %(RecursiveDir) without this. -->
<ItemGroup>
<RuntimeAnalyzersContent Include="@(RedistRuntimeAnalyzersContent);@(RedirectingRuntimeAnalyzersContent)" />
<RedistRuntimeAnalyzersContent Update="@(RedistRuntimeAnalyzersContent)">
<CustomRecursiveDir>%(RecursiveDir)</CustomRecursiveDir>
</RedistRuntimeAnalyzersContent>
<RedirectingRuntimeAnalyzersContent Update="@(RedirectingRuntimeAnalyzersContent)">
<CustomRecursiveDir>%(RecursiveDir)</CustomRecursiveDir>
</RedirectingRuntimeAnalyzersContent>
</ItemGroup>

<ProcessRuntimeAnalyzerVersions Inputs="@(RedistRuntimeAnalyzersContent)" MetadataFilePath="$(SdkRuntimeAnalyzersMetadataFile)">
<Output TaskParameter="Outputs" ItemName="ProcessedRedistRuntimeAnalyzersContent" />
</ProcessRuntimeAnalyzerVersions>

<ItemGroup>
<RuntimeAnalyzersContent Include="@(ProcessedRedistRuntimeAnalyzersContent);@(RedirectingRuntimeAnalyzersContent)" />
</ItemGroup>

<Copy SourceFiles="@(RuntimeAnalyzersContent)"
DestinationFiles="@(RuntimeAnalyzersContent->'$(OutputPath)\%(DeploymentSubpath)\%(RecursiveDir)%(Filename)%(Extension)')"
DestinationFiles="@(RuntimeAnalyzersContent->'$(OutputPath)\%(DeploymentSubpath)\%(CustomRecursiveDir)%(Filename)%(Extension)')"
UseHardlinksIfPossible="true" />

<!-- Replace Experimental=true with SystemComponent=true in the VS extension manifest. -->
<PropertyGroup>
<_VsixNamespace>
<Namespace Prefix="pm" Uri="http://schemas.microsoft.com/developer/vsx-schema/2011" />
</_VsixNamespace>
</PropertyGroup>
<XmlPoke XmlInputPath="$(OutputPath)\AnalyzerRedirecting\extension.vsixmanifest"
Namespaces="$(_VsixNamespace)"
Query="/pm:PackageManifest/pm:Installation/@Experimental"
Value="false" />
<XmlPoke XmlInputPath="$(OutputPath)\AnalyzerRedirecting\extension.vsixmanifest"
Namespaces="$(_VsixNamespace)"
Query="/pm:PackageManifest/pm:Installation/@SystemComponent"
Value="true" />

<GenerateRuntimeAnalyzersSWR RuntimeAnalyzersLayoutDirectory="$(OutputPath)"
OutputFile="$(SdkRuntimeAnalyzersSwrFile)" />

<ItemGroup>
<!-- Include the swr file in the nuget package for VS authoring -->
<Content Include="$(SdkRuntimeAnalyzersSwrFile)" PackagePath="/" />
<Content Include="@(RuntimeAnalyzersContent)" PackagePath="/%(RuntimeAnalyzersContent.DeploymentSubpath)/%(RecursiveDir)%(Filename)%(Extension)" />
<Content Include="$(SdkRuntimeAnalyzersMetadataFile)" PackagePath="/" />
<Content Include="@(RuntimeAnalyzersContent)" PackagePath="/%(RuntimeAnalyzersContent.DeploymentSubpath)/%(RuntimeAnalyzersContent.CustomRecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.VisualStudio.Sdk" />
<PackageReference Include="Microsoft.VSSDK.BuildTools" PrivateAssets="all" />
<PackageReference Include="System.Text.Json" VersionOverride="9.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Text.Json;
using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

// Example:
// FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll"
Expand All @@ -13,9 +16,16 @@

namespace Microsoft.Net.Sdk.AnalyzerRedirecting;

/// <summary>
/// See <c>documentation/general/analyzer-redirecting.md</c>.
/// </summary>
[Export(typeof(IAnalyzerAssemblyRedirector))]
public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
{
private readonly IVsActivityLog? _log;

private readonly bool _enabled;

private readonly string? _insertedAnalyzersDirectory;

/// <summary>
Expand All @@ -24,62 +34,91 @@ public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
private readonly ImmutableDictionary<string, List<AnalyzerInfo>> _analyzerMap;

[ImportingConstructor]
public SdkAnalyzerAssemblyRedirector()
: this(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\DotNetRuntimeAnalyzers"))) { }
public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this(
Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")),
serviceProvider.GetService<SVsActivityLog, IVsActivityLog>())
{
}

// Internal for testing.
internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory)
internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory, IVsActivityLog? log = null)
{
_log = log;
var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING");
_enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase);
_insertedAnalyzersDirectory = insertedAnalyzersDirectory;
_analyzerMap = CreateAnalyzerMap();
}

private ImmutableDictionary<string, List<AnalyzerInfo>> CreateAnalyzerMap()
{
if (!_enabled)
{
Log("Analyzer redirecting is disabled.");
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
}

var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json");
if (!File.Exists(metadataFilePath))
{
Log($"File does not exist: {metadataFilePath}");
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
}

var versions = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(metadataFilePath));
if (versions is null || versions.Count == 0)
{
Log($"Versions are empty: {metadataFilePath}");
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
}

var builder = ImmutableDictionary.CreateBuilder<string, List<AnalyzerInfo>>(StringComparer.OrdinalIgnoreCase);

// Expects layout like:
// VsInstallDir\SDK\RuntimeAnalyzers\WindowsDesktopAnalyzers\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
// ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory
// ~~~~~ = versionDirectory
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath
// VsInstallDir\DotNetRuntimeAnalyzers\WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
// ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath

foreach (string topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory))
{
foreach (string versionDirectory in Directory.EnumerateDirectories(topLevelDirectory))
foreach (string analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories))
{
foreach (string analyzerPath in Directory.EnumerateFiles(versionDirectory, "*.dll", SearchOption.AllDirectories))
if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase))
{
if (!analyzerPath.StartsWith(versionDirectory, StringComparison.OrdinalIgnoreCase))
{
continue;
}

string version = Path.GetFileName(versionDirectory);
string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath);
string pathSuffix = analyzerPath.Substring(versionDirectory.Length + 1 /* slash */);
pathSuffix = Path.GetDirectoryName(pathSuffix);

AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix };

if (builder.TryGetValue(analyzerName, out var existing))
{
existing.Add(analyzer);
}
else
{
builder.Add(analyzerName, [analyzer]);
}
continue;
}

string subsetName = Path.GetFileName(topLevelDirectory);
if (!versions.TryGetValue(subsetName, out string version))
{
continue;
}

string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath);
string pathSuffix = analyzerPath.Substring(topLevelDirectory.Length + 1 /* slash */);
pathSuffix = Path.GetDirectoryName(pathSuffix);

AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix };

if (builder.TryGetValue(analyzerName, out var existing))
{
existing.Add(analyzer);
}
else
{
builder.Add(analyzerName, [analyzer]);
}
}
}

Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}");

return builder.ToImmutable();
}

public string? RedirectPath(string fullPath)
{
if (_analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers))
if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers))
{
foreach (AnalyzerInfo analyzer in analyzers)
{
Expand Down Expand Up @@ -134,4 +173,12 @@ static bool areVersionMajorMinorPartEqual(string version1, string version2)
return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase);
}
}

private void Log(string message)
{
_log?.LogEntry(
(uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,
nameof(SdkAnalyzerAssemblyRedirector),
message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
<Asset Type="Microsoft.VisualStudio.VsPackage" Path="|%CurrentProject%;PkgdefProjectOutputGroup|" d:Source="Project" d:ProjectName="%CurrentProject%" />
</Assets>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,18.0)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,19.0)" DisplayName="Visual Studio core editor" />
</Prerequisites>
<Installation Experimental="true">
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,18.0)">
<Installation Experimental="true" SystemComponent="false">
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,19.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
</Installation>
Expand Down
24 changes: 16 additions & 8 deletions src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ public override bool Execute()

// NOTE: Keep in sync with SdkAnalyzerAssemblyRedirector.
// This is intentionally short to avoid long path problems.
const string installDir = @"DotNetRuntimeAnalyzers";
const string installDir = @"Common7\IDE\CommonExtensions\Microsoft\DotNet";

AddFolder(sb,
@"AnalyzerRedirecting",
@"Common7\IDE\CommonExtensions\Microsoft\AnalyzerRedirecting",
"",
installDir,
filesToInclude:
[
"metadata.json",
]);

AddFolder(sb,
"AnalyzerRedirecting",
@$"{installDir}\AnalyzerRedirecting",
filesToInclude:
[
"Microsoft.Net.Sdk.AnalyzerRedirecting.dll",
Expand All @@ -32,23 +40,23 @@ public override bool Execute()
]);

AddFolder(sb,
@"AspNetCoreAnalyzers",
"AspNetCoreAnalyzers",
@$"{installDir}\AspNetCoreAnalyzers");

AddFolder(sb,
@"NetCoreAnalyzers",
"NetCoreAnalyzers",
@$"{installDir}\NetCoreAnalyzers");

AddFolder(sb,
@"WindowsDesktopAnalyzers",
"WindowsDesktopAnalyzers",
@$"{installDir}\WindowsDesktopAnalyzers");

AddFolder(sb,
@"SDKAnalyzers",
"SDKAnalyzers",
@$"{installDir}\SDKAnalyzers");

AddFolder(sb,
@"WebSDKAnalyzers",
"WebSDKAnalyzers",
@$"{installDir}\WebSDKAnalyzers");

File.WriteAllText(OutputFile, sb.ToString());
Expand Down
Loading
Loading