Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow VS Code to provide razor source geneator references. #72482

Merged
merged 4 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -32,6 +32,7 @@ public static Task<ExportProvider> CreateExportProviderAsync(ILoggerFactory logg
SessionId: null,
ExtensionAssemblyPaths: [],
DevKitDependencyPath: devKitDependencyPath,
DevKitRazorOutputPath: null,
ExtensionLogDirectory: string.Empty);
var extensionAssemblyManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory);
return ExportProviderBuilder.CreateExportProviderAsync(extensionAssemblyManager, devKitDependencyPath, loggerFactory: loggerFactory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,72 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Reflection;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
internal class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider
{
/// <summary>
/// The directory where the analyzers are located
/// </summary>
public string? AnalyzerDirectory { get; set; }

private const string RazorVsixExtensionId = "Microsoft.VisualStudio.RazorExtension";
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
private static readonly HashSet<string> s_razorSourceGeneratorAssemblyNames = new[] {
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
"Microsoft.NET.Sdk.Razor.SourceGenerators",
"Microsoft.CodeAnalysis.Razor.Compiler.SourceGenerators",
"Microsoft.CodeAnalysis.Razor.Compiler",
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
}.ToHashSet<string>();

public HostDiagnosticAnalyzerProvider(string? analyzerDirectory)
{
AnalyzerDirectory = analyzerDirectory;
}

public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
{
// Right now we don't expose any way for the extensions in VS Code to provide analyzer references.
return ImmutableArray<(AnalyzerFileReference reference, string extensionId)>.Empty;
if (AnalyzerDirectory == null)
{
return ImmutableArray<(AnalyzerFileReference reference, string extensionId)>.Empty;
}

var analyzerReferences = new List<(AnalyzerFileReference, string)>();

// Get all the .dll files in the directory
var analyzerFiles = Directory.GetFiles(AnalyzerDirectory, "*.dll");
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved

foreach (var analyzerFile in analyzerFiles)
{
// Create an AnalyzerFileReference for each file
var analyzerReference = new AnalyzerFileReference(analyzerFile, new SimpleAnalyzerAssemblyLoader());

if (s_razorSourceGeneratorAssemblyNames.Any(
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
name => analyzerReference.FullPath.Contains(name, StringComparison.OrdinalIgnoreCase)))
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
{
// Add the reference to the list, using the file name as the extension ID
analyzerReferences.Add((analyzerReference, RazorVsixExtensionId));
}
}

return analyzerReferences.ToImmutableArray();
}

private class SimpleAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
{
public void AddDependencyLocation(string fullPath)
{
// This method is used to add a path that should be probed for analyzer dependencies.
// In this simple implementation, we do nothing.
}

public Assembly LoadFromPath(string fullPath)
{
// This method is used to load an analyzer assembly from the specified path.
// In this simple implementation, we use Assembly.LoadFrom to load the assembly.
return Assembly.LoadFrom(fullPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you talk with Jason at all about which assembly load context the source generator should be loaded in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't recall that conversation.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public LanguageServerWorkspaceFactory(
IFileChangeWatcher fileChangeWatcher,
[ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, Host.Mef.FileExtensionsMetadata>> dynamicFileInfoProviders,
ProjectTargetFrameworkManager projectTargetFrameworkManager,
ServerConfigurationFactory serverConfigurationFactory,
ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger(nameof(LanguageServerWorkspaceFactory));
Expand All @@ -40,10 +41,11 @@ public LanguageServerWorkspaceFactory(

analyzerLoader.InitializeDiagnosticsServices(Workspace);

var devKitRazorOutputPath = serverConfigurationFactory?.ServerConfiguration?.DevKitRazorOutputPath;
ProjectSystemHostInfo = new ProjectSystemHostInfo(
DynamicFileInfoProviders: dynamicFileInfoProviders.ToImmutableArray(),
new ProjectSystemDiagnosticSource(),
new HostDiagnosticAnalyzerProvider());
new HostDiagnosticAnalyzerProvider(devKitRazorOutputPath));

TargetFrameworkManager = projectTargetFrameworkManager;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ static CliRootCommand CreateCommandLineParser()
Required = false
};

var devKitRazorOutputPathOption = new CliOption<string?>("--devKitRazorOutputPath")
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
{
Description = "Full path to the Razor output path used with DevKit (optional).",
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
Required = false
};

var rootCommand = new CliRootCommand()
{
debugOption,
Expand All @@ -208,6 +214,7 @@ static CliRootCommand CreateCommandLineParser()
sessionIdOption,
extensionAssemblyPathsOption,
devKitDependencyPathOption,
devKitRazorOutputPathOption,
extensionLogDirectoryOption
};
rootCommand.SetAction((parseResult, cancellationToken) =>
Expand All @@ -219,6 +226,7 @@ static CliRootCommand CreateCommandLineParser()
var sessionId = parseResult.GetValue(sessionIdOption);
var extensionAssemblyPaths = parseResult.GetValue(extensionAssemblyPathsOption) ?? [];
var devKitDependencyPath = parseResult.GetValue(devKitDependencyPathOption);
var devKitRazorOutputPath = parseResult.GetValue(devKitRazorOutputPathOption);
var extensionLogDirectory = parseResult.GetValue(extensionLogDirectoryOption)!;

var serverConfiguration = new ServerConfiguration(
Expand All @@ -229,6 +237,7 @@ static CliRootCommand CreateCommandLineParser()
SessionId: sessionId,
ExtensionAssemblyPaths: extensionAssemblyPaths,
DevKitDependencyPath: devKitDependencyPath,
DevKitRazorOutputPath: devKitRazorOutputPath,
ExtensionLogDirectory: extensionLogDirectory);

return RunAsync(serverConfiguration, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ internal record class ServerConfiguration(
string? SessionId,
IEnumerable<string> ExtensionAssemblyPaths,
string? DevKitDependencyPath,
string? DevKitRazorOutputPath,
string ExtensionLogDirectory);
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,7 @@ public void RemoveAnalyzerReference(string fullPath)

private OneOrMany<string> GetMappedAnalyzerPaths(string fullPath)
{
fullPath = Path.GetFullPath(fullPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still necessary, now that the full path is being passed in by the client?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is needed.

Without this change, variable will be:

s_razorSourceGeneratorSdkDirectory

"Sdks\\Microsoft.NET.Sdk.Razor\\source-generators\\"

fullPath

"C:\\Program Files\\dotnet\\sdk\\6.0.420\\Sdks\\Microsoft.NET.Sdk.Razor\\targets\\..\\\\source-generators\\Microsoft.NET.Sdk.Razor.SourceGenerators.dll"

Path.DirectorySeparatorChar

 92 '\\'

and the if condition below would be false and we won't go through getting analyzer references at all:

if (fullPath.LastIndexOf(s_razorSourceGeneratorSdkDirectory, StringComparison.OrdinalIgnoreCase) + s_razorSourceGeneratorSdkDirectory.Length - 1 ==
    fullPath.LastIndexOf(Path.DirectorySeparatorChar))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a surprising value for fullPath. I thought this code also ran in VS, so I'm confused as to how it works there.

// Map all files in the SDK directory that contains the Razor source generator to source generator files loaded from VSIX.
// Include the generator and all its dependencies shipped in VSIX, discard the generator and all dependencies in the SDK
if (fullPath.LastIndexOf(s_razorSourceGeneratorSdkDirectory, StringComparison.OrdinalIgnoreCase) + s_razorSourceGeneratorSdkDirectory.Length - 1 ==
Expand Down
Loading