From f4c215385447c082e7b7e1b6fc8c0da7698a2003 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Sat, 16 Aug 2025 13:53:30 +1000 Subject: [PATCH] Get Code Fixes (#4) --- .../Analysis/CodeFixProviderLoader.cs | 70 ++++++++++ .../Analysis/CodeRefactoringProviderLoader.cs | 57 ++++++++ .../Features/Analysis/RoslynAnalysis.cs | 125 +++++++++++++++--- .../SharpIDE.Application.csproj | 2 + 4 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 src/SharpIDE.Application/Features/Analysis/CodeFixProviderLoader.cs create mode 100644 src/SharpIDE.Application/Features/Analysis/CodeRefactoringProviderLoader.cs diff --git a/src/SharpIDE.Application/Features/Analysis/CodeFixProviderLoader.cs b/src/SharpIDE.Application/Features/Analysis/CodeFixProviderLoader.cs new file mode 100644 index 0000000..0563232 --- /dev/null +++ b/src/SharpIDE.Application/Features/Analysis/CodeFixProviderLoader.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Composition.Hosting; +using System.Reflection; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace SharpIDE.Application.Features.Analysis; + +public static class CodeFixProviderLoader +{ + // Alterative way, appears to be slower + private static List LoadProviders(IEnumerable assemblies) + { + // Create an assembly catalog containing your current assembly + var configuration = new ContainerConfiguration().WithAssemblies(assemblies); + + using var container = configuration.CreateContainer(); + // Get all exported CodeFixProviders + var providers = container.GetExports().ToList(); + return providers; + } + + // https://github.com/KirillOsenkov/CodeCleanupTools/blob/main/CodeFixer/FixerLoader.cs + public static ImmutableArray LoadCodeFixProviders(IEnumerable assemblies, string language) + { + return assemblies + .SelectMany(GetConcreteTypes) + .Where(t => typeof(CodeFixProvider).IsAssignableFrom(t)) + .Where(t => IsExportedForLanguage(t, language)) + .Select(CreateInstanceOfCodeFix) + .OfType() + .ToImmutableArray(); + } + + private static bool IsExportedForLanguage(Type codeFixProvider, string language) + { + var exportAttribute = codeFixProvider.GetCustomAttribute(inherit: false); + return exportAttribute is not null && exportAttribute.Languages.Contains(language); + } + + private static CodeFixProvider? CreateInstanceOfCodeFix(Type codeFixProvider) + { + try + { + return (CodeFixProvider)Activator.CreateInstance(codeFixProvider)!; + } + catch + { + return null; + } + } + + private static IEnumerable GetConcreteTypes(Assembly assembly) + { + try + { + var concreteTypes = assembly + .GetTypes() + .Where(type => !type.GetTypeInfo().IsInterface + && !type.GetTypeInfo().IsAbstract + && !type.GetTypeInfo().ContainsGenericParameters); + + // Realize the collection to ensure exceptions are caught + return concreteTypes.ToList(); + } + catch + { + return Type.EmptyTypes; + } + } +} diff --git a/src/SharpIDE.Application/Features/Analysis/CodeRefactoringProviderLoader.cs b/src/SharpIDE.Application/Features/Analysis/CodeRefactoringProviderLoader.cs new file mode 100644 index 0000000..2da86f2 --- /dev/null +++ b/src/SharpIDE.Application/Features/Analysis/CodeRefactoringProviderLoader.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.CodeAnalysis.CodeRefactorings; + +namespace SharpIDE.Application.Features.Analysis; + +public static class CodeRefactoringProviderLoader +{ + public static ImmutableArray LoadCodeRefactoringProviders(IEnumerable assemblies, string language) + { + return assemblies + .SelectMany(GetConcreteTypes) + .Where(t => typeof(CodeRefactoringProvider).IsAssignableFrom(t)) + .Where(t => IsExportedForLanguage(t, language)) + .Select(CreateInstanceOfCodeRefactoring) + .OfType() + .ToImmutableArray(); + } + + private static bool IsExportedForLanguage(Type refactoringProvider, string language) + { + var exportAttribute = refactoringProvider.GetCustomAttribute(inherit: false); + return exportAttribute is not null && exportAttribute.Languages.Contains(language); + } + + private static CodeRefactoringProvider? CreateInstanceOfCodeRefactoring(Type refactoringProvider) + { + try + { + return (CodeRefactoringProvider)Activator.CreateInstance(refactoringProvider)!; + } + catch + { + return null; + } + } + + private static IEnumerable GetConcreteTypes(Assembly assembly) + { + try + { + var concreteTypes = assembly + .GetTypes() + .Where(type => !type.GetTypeInfo().IsInterface + && !type.GetTypeInfo().IsAbstract + && !type.GetTypeInfo().ContainsGenericParameters); + + // Realize the collection to ensure exceptions are caught + return concreteTypes.ToList(); + } + catch + { + return Type.EmptyTypes; + } + } + +} diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 3d519d8..71eec36 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -1,13 +1,23 @@ -using System.Diagnostics; +using System.Collections.Immutable; +using System.Diagnostics; using Ardalis.GuardClauses; -using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Text; +using NuGet.Packaging; namespace SharpIDE.Application.Features.Analysis; public static class RoslynAnalysis { private static MSBuildWorkspace? _workspace; + private static HashSet _codeFixProviders = []; + private static HashSet _codeRefactoringProviders = []; public static void StartSolutionAnalysis(string solutionFilePath) { _ = Task.Run(async () => @@ -26,40 +36,117 @@ public static class RoslynAnalysis { Console.WriteLine($"RoslynAnalysis: Loading solution"); var timer = Stopwatch.StartNew(); - _workspace ??= MSBuildWorkspace.Create(); - _workspace.WorkspaceFailed += (o, e) => throw new InvalidOperationException($"Workspace failed: {e.Diagnostic.Message}"); + if (_workspace is null) + { + // is this hostServices necessary? test without it - just getting providers from assemblies instead + var host = MefHostServices.Create(MefHostServices.DefaultAssemblies); + _workspace ??= MSBuildWorkspace.Create(host); + _workspace.WorkspaceFailed += (o, e) => throw new InvalidOperationException($"Workspace failed: {e.Diagnostic.Message}"); + } var solution = await _workspace.OpenSolutionAsync(solutionFilePath, new Progress()); timer.Stop(); Console.WriteLine($"RoslynAnalysis: Solution loaded in {timer.ElapsedMilliseconds}ms"); Console.WriteLine(); + foreach (var assembly in MefHostServices.DefaultAssemblies) + { + //var assembly = analyzer.GetAssembly(); + var fixers = CodeFixProviderLoader.LoadCodeFixProviders([assembly], LanguageNames.CSharp); + _codeFixProviders.AddRange(fixers); + var refactoringProviders = CodeRefactoringProviderLoader.LoadCodeRefactoringProviders([assembly], LanguageNames.CSharp); + _codeRefactoringProviders.AddRange(refactoringProviders); + } + + // // TODO: Distinct on the assemblies first + // foreach (var project in solution.Projects) + // { + // var relevantAnalyzerReferences = project.AnalyzerReferences.OfType().ToArray(); + // var assemblies = relevantAnalyzerReferences.Select(a => a.GetAssembly()).ToArray(); + // var language = project.Language; + // //var analyzers = relevantAnalyzerReferences.SelectMany(a => a.GetAnalyzers(language)); + // var fixers = CodeFixProviderLoader.LoadCodeFixProviders(assemblies, language); + // _codeFixProviders.AddRange(fixers); + // var refactoringProviders = CodeRefactoringProviderLoader.LoadCodeRefactoringProviders(assemblies, language); + // _codeRefactoringProviders.AddRange(refactoringProviders); + // } + + _codeFixProviders = _codeFixProviders.DistinctBy(s => s.GetType().Name).ToHashSet(); + _codeRefactoringProviders = _codeRefactoringProviders.DistinctBy(s => s.GetType().Name).ToHashSet(); + foreach (var project in solution.Projects) { //Console.WriteLine($"Project: {project.Name}"); var compilation = await project.GetCompilationAsync(); Guard.Against.Null(compilation, nameof(compilation)); - // Get diagnostics (built-in or custom analyzers) var diagnostics = compilation.GetDiagnostics(); - var nonHiddenDiagnostics = diagnostics.Where(d => d.Severity is not Microsoft.CodeAnalysis.DiagnosticSeverity.Hidden).ToList(); - + var nonHiddenDiagnostics = diagnostics.Where(d => d.Severity is not DiagnosticSeverity.Hidden).ToList(); + // foreach (var diagnostic in nonHiddenDiagnostics) { Console.WriteLine(diagnostic); // Optionally run CodeFixProviders here } - foreach (var document in project.Documents) - { - // var syntaxTree = await document.GetSyntaxTreeAsync(); - // var root = await syntaxTree!.GetRootAsync(); - // var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, root.FullSpan); - // foreach (var span in classifiedSpans) - // { - // var classifiedSpan = root.GetText().GetSubText(span.TextSpan); - // Console.WriteLine($"{span.TextSpan}: {span.ClassificationType}"); - // Console.WriteLine(classifiedSpan); - // } - } + // foreach (var document in project.Documents) + // { + // var semanticModel = await document.GetSemanticModelAsync(); + // Guard.Against.Null(semanticModel, nameof(semanticModel)); + // var documentDiagnostics = semanticModel.GetDiagnostics().Where(d => d.Severity is not DiagnosticSeverity.Hidden).ToList(); + // foreach (var diagnostic in documentDiagnostics) + // { + // var test = await GetCodeFixesAsync(document, diagnostic); + // } + // // var syntaxTree = await document.GetSyntaxTreeAsync(); + // // var root = await syntaxTree!.GetRootAsync(); + // // var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, root.FullSpan); + // // foreach (var span in classifiedSpans) + // // { + // // var classifiedSpan = root.GetText().GetSubText(span.TextSpan); + // // Console.WriteLine($"{span.TextSpan}: {span.ClassificationType}"); + // // Console.WriteLine(classifiedSpan); + // // } + // } } + Console.WriteLine("RoslynAnalysis: Analysis completed."); + } + + public static async Task> GetCodeFixesAsync(Document document, Diagnostic diagnostic) + { + var cancellationToken = CancellationToken.None; + var position = diagnostic.Location.SourceSpan.Start; + var codeActions = new List(); + var context = new CodeFixContext( + document, + diagnostic, + (action, _) => codeActions.Add(action), // callback collects fixes + cancellationToken + ); + + var relevantProviders = _codeFixProviders + .Where(provider => provider.FixableDiagnosticIds.Contains(diagnostic.Id)); + + foreach (var provider in relevantProviders) + { + await provider.RegisterCodeFixesAsync(context); + } + + var refactorContext = new CodeRefactoringContext( + document, + diagnostic.Location.SourceSpan, + action => codeActions.Add(action), + cancellationToken + ); + + foreach (var provider in _codeRefactoringProviders) + { + await provider.ComputeRefactoringsAsync(refactorContext).ConfigureAwait(false); + } + + if (codeActions.Count is not 0) + { + ; + } + + return codeActions.ToImmutableArray(); } } diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index ae0e63c..03414d7 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -16,7 +16,9 @@ + +