From e8ed0d8db2a77b69aa57e4a3933fd4a428af11f3 Mon Sep 17 00:00:00 2001 From: wuke32767 <2446214230@qq.com> Date: Sun, 6 Jul 2025 07:42:29 +0800 Subject: [PATCH] Implemented code fix --- .gitignore | 4 +- .../CodeFixProvider.cs | 103 ++++++++++++++++++ .../ModInteropImportGenerator.CodeFix.csproj | 30 +++++ .../CodeFixTest.cs | 60 ++++++++++ .../ModInteropImportGenerator.Sample.csproj | 1 + .../ModInteropImportGenerator.Tests.csproj | 1 + ModInteropImportGenerator.sln | 6 + .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 ++ .../InternalsVisibleTo.cs | 3 + .../ModInteropImportDiagnosticAnalyzer.cs | 52 +++++++++ .../ModInteropImportSourceGenerator.cs | 2 +- README.md | 37 +++++++ 13 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 ModInteropImportGenerator.CodeFix/CodeFixProvider.cs create mode 100644 ModInteropImportGenerator.CodeFix/ModInteropImportGenerator.CodeFix.csproj create mode 100644 ModInteropImportGenerator.Sample/CodeFixTest.cs create mode 100644 ModInteropImportGenerator/AnalyzerReleases.Shipped.md create mode 100644 ModInteropImportGenerator/AnalyzerReleases.Unshipped.md create mode 100644 ModInteropImportGenerator/InternalsVisibleTo.cs create mode 100644 ModInteropImportGenerator/ModInteropImportDiagnosticAnalyzer.cs diff --git a/.gitignore b/.gitignore index add57be..f9fee1f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +/.vs +/.idea diff --git a/ModInteropImportGenerator.CodeFix/CodeFixProvider.cs b/ModInteropImportGenerator.CodeFix/CodeFixProvider.cs new file mode 100644 index 0000000..af4d4ec --- /dev/null +++ b/ModInteropImportGenerator.CodeFix/CodeFixProvider.cs @@ -0,0 +1,103 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +// see https://github.com/dotnet/roslyn/blob/main/docs/roslyn-analyzers/rules/RS1038.md + +namespace ModInteropImportGenerator.CodeFix +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CodeGenerator))] + public class CodeGenerator : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => [ModInteropImportDiagnosticAnalyzer.PreparedForCodeFixID]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var act = + CodeAction.Create("Strip Import Generator", + cancel => StripAsync(context, cancel), + nameof(CodeGenerator)); + context.RegisterCodeFix(act, context.Diagnostics); + return Task.CompletedTask; + } + + private static async Task StripAsync(CodeFixContext context, System.Threading.CancellationToken cancel) + { + var model = await context.Document.GetSemanticModelAsync(cancel); + var root = await context.Document.GetSyntaxRootAsync(cancel); + if (model == null || root == null) + { + return context.Document; + } + + var _attrNode = root.FindNode(context.Span); + if (_attrNode is not AttributeSyntax attrNode) + { + return context.Document; + } + // if it's a nested class, we should mark all containing classes as partial + var list = attrNode.AncestorsAndSelf().OfType().ToList(); + // perform the rest actions in the walker rewriter + var newRoot = new StripWalker(list).Visit(root); + return context.Document.WithSyntaxRoot(newRoot); + } + + public sealed override FixAllProvider? GetFixAllProvider() + { + return null; + } + } + class StripWalker(List clas) + : CSharpSyntaxRewriter() + { + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + // the classes we are interested in are all in the `clas` + if (clas.Count == 0 || node != clas.Last()) + { + return node; + } + clas.RemoveAt(clas.Count - 1); + + ClassDeclarationSyntax newNode; + if (clas.Count == 0) + { + // walk through all the methods + // add partial and remove body + var newMembers = SyntaxFactory.List(node.Members.Select(mem => + { + if (mem is MethodDeclarationSyntax method) + { + if (!method.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + method = method.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + } + return method + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithBody(null) + .WithExpressionBody(null); + } + return mem; + })); + newNode = node.WithMembers(newMembers); + } + else + { + newNode = (ClassDeclarationSyntax)base.VisitClassDeclaration(node)!; + } + // mark all classes as partial + if (!newNode.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PartialKeyword))) + { + newNode = newNode.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + } + return newNode; + } + } +} diff --git a/ModInteropImportGenerator.CodeFix/ModInteropImportGenerator.CodeFix.csproj b/ModInteropImportGenerator.CodeFix/ModInteropImportGenerator.CodeFix.csproj new file mode 100644 index 0000000..753c994 --- /dev/null +++ b/ModInteropImportGenerator.CodeFix/ModInteropImportGenerator.CodeFix.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + ModInteropImportGenerator + ModInteropImportGenerator.CodeFix + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/ModInteropImportGenerator.Sample/CodeFixTest.cs b/ModInteropImportGenerator.Sample/CodeFixTest.cs new file mode 100644 index 0000000..3cad19e --- /dev/null +++ b/ModInteropImportGenerator.Sample/CodeFixTest.cs @@ -0,0 +1,60 @@ +using ModInteropImportGenerator.Sample.Stubs; +using MonoMod.ModInterop; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace ModInteropImportGenerator.Sample +{ + class Nest + { + [GenerateImports("CommunalHelper.DashStates")] + public static class DashStates1 + { + #region DreamTunnel + + public static int GetDreamTunnelDashState() + { + return 0; + } + + public static bool HasDreamTunnelDash() + { + return false; + } + + public static int GetDreamTunnelDashCount() + { + return 0; + } + + public static ComponentStub DreamTunnelInteraction( + Action onPlayerEnter, + Action onPlayerExit) + { + return null; + } + + #endregion + + #region Seeker + + public static bool HasSeekerDash() + { + return false; + } + + public static bool IsSeekerDashAttacking() + { + return false; + } + + #endregion + } + + } +} diff --git a/ModInteropImportGenerator.Sample/ModInteropImportGenerator.Sample.csproj b/ModInteropImportGenerator.Sample/ModInteropImportGenerator.Sample.csproj index 3e5996d..e3071f5 100644 --- a/ModInteropImportGenerator.Sample/ModInteropImportGenerator.Sample.csproj +++ b/ModInteropImportGenerator.Sample/ModInteropImportGenerator.Sample.csproj @@ -9,6 +9,7 @@ + diff --git a/ModInteropImportGenerator.Tests/ModInteropImportGenerator.Tests.csproj b/ModInteropImportGenerator.Tests/ModInteropImportGenerator.Tests.csproj index 1fc0a1b..56d8c3e 100644 --- a/ModInteropImportGenerator.Tests/ModInteropImportGenerator.Tests.csproj +++ b/ModInteropImportGenerator.Tests/ModInteropImportGenerator.Tests.csproj @@ -21,6 +21,7 @@ + diff --git a/ModInteropImportGenerator.sln b/ModInteropImportGenerator.sln index 07d00ee..0ff395e 100644 --- a/ModInteropImportGenerator.sln +++ b/ModInteropImportGenerator.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModInteropImportGenerator.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModInteropImportGenerator.Tests", "ModInteropImportGenerator.Tests\ModInteropImportGenerator.Tests.csproj", "{4B56916D-9625-4D9C-818F-23213C1CA0CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModInteropImportGenerator.CodeFix", "ModInteropImportGenerator.CodeFix\ModInteropImportGenerator.CodeFix.csproj", "{D56DAE86-5562-4BCD-A271-70245293A2F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {4B56916D-9625-4D9C-818F-23213C1CA0CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B56916D-9625-4D9C-818F-23213C1CA0CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B56916D-9625-4D9C-818F-23213C1CA0CD}.Release|Any CPU.Build.0 = Release|Any CPU + {D56DAE86-5562-4BCD-A271-70245293A2F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D56DAE86-5562-4BCD-A271-70245293A2F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D56DAE86-5562-4BCD-A271-70245293A2F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D56DAE86-5562-4BCD-A271-70245293A2F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ModInteropImportGenerator/AnalyzerReleases.Shipped.md b/ModInteropImportGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..2080dcd --- /dev/null +++ b/ModInteropImportGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/ModInteropImportGenerator/AnalyzerReleases.Unshipped.md b/ModInteropImportGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..2173d03 --- /dev/null +++ b/ModInteropImportGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +CLII0001 | Usage | Warning | ModInteropImportDiagnosticAnalyzer \ No newline at end of file diff --git a/ModInteropImportGenerator/InternalsVisibleTo.cs b/ModInteropImportGenerator/InternalsVisibleTo.cs new file mode 100644 index 0000000..31c7870 --- /dev/null +++ b/ModInteropImportGenerator/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ModInteropImportGenerator.CodeFix")] diff --git a/ModInteropImportGenerator/ModInteropImportDiagnosticAnalyzer.cs b/ModInteropImportGenerator/ModInteropImportDiagnosticAnalyzer.cs new file mode 100644 index 0000000..6410467 --- /dev/null +++ b/ModInteropImportGenerator/ModInteropImportDiagnosticAnalyzer.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using ModInteropImportGenerator.Helpers; + +namespace ModInteropImportGenerator; + +// see https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ModInteropImportDiagnosticAnalyzer : DiagnosticAnalyzer +{ + public const string PreparedForCodeFixID = "CLII0001"; + internal const string CheckTypeFqn = + ModInteropImportSourceGenerator.GenerateImportsAttributeFqn; + internal DiagnosticDescriptor PreparedForCodeFix = + new(PreparedForCodeFixID, "Import Generator is not good", "Any method in the Import Generator should be partial and not implemented, and containing classes should also be partial", "Usage", DiagnosticSeverity.Warning, true); + public override ImmutableArray SupportedDiagnostics + => [PreparedForCodeFix]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterSyntaxNodeAction(cxt => + { + if (cxt.Node is AttributeSyntax node + && node.Parent is AttributeListSyntax list + && list.Parent is ClassDeclarationSyntax clas + && cxt.SemanticModel.GetTypeInfo(node).Type?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) == CheckTypeFqn + && (clas.AncestorsAndSelf().Any(ac => ac is ClassDeclarationSyntax c + && c.Modifiers.All(mod => !mod.IsKind(SyntaxKind.PartialKeyword))) + || clas.Members.Any(mem => mem is MethodDeclarationSyntax method + && (method.SemicolonToken == default + || method.Body is { } + || method.ExpressionBody is { }))) + ) + { + cxt.ReportDiagnostic(Diagnostic.Create(PreparedForCodeFix, node.GetLocation())); + } + }, SyntaxKind.Attribute); + } +} diff --git a/ModInteropImportGenerator/ModInteropImportSourceGenerator.cs b/ModInteropImportGenerator/ModInteropImportSourceGenerator.cs index 2673d9b..974afe9 100644 --- a/ModInteropImportGenerator/ModInteropImportSourceGenerator.cs +++ b/ModInteropImportGenerator/ModInteropImportSourceGenerator.cs @@ -20,7 +20,7 @@ public class ModInteropImportSourceGenerator : IIncrementalGenerator private const string GenerateImportsAttributeTypeName = "GenerateImportsAttribute"; - private const string GenerateImportsAttributeFqn + internal const string GenerateImportsAttributeFqn = $"{ImportGeneratorNamespace}.{GenerateImportsAttributeTypeName}"; [LanguageInjection("C#")] diff --git a/README.md b/README.md index 63cb3a7..f9b3212 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,43 @@ public static partial class DashStates - You get parameter names as a bonus - You don't have to constantly slap an `?.Invoke(...)` on the imported methods if the dependency is optional +If you are tooooo lazy to remove the method body and mark them as `partial`, +we have also provided a code fixer. + +### Example +Assume the previous mod export: + +```cs +[ModExportName("CommunalHelper.DashStates")] +public static class DashStates +{ + #region DreamTunnel + + public static int GetDreamTunnelDashState() + { + return St.DreamTunnelDash; + } +... +``` + +You replace it with `GenerateImports`, + +```cs +[GenerateImports("CommunalHelper.DashStates")] +public static class DashStates +{ + #region DreamTunnel + + public static int GetDreamTunnelDashState() + { + return St.DreamTunnelDash; + } +... +``` +Your IDE should show a warning, and provide a lightbulb for this. + +Just accept it. + ## Referencing Add the `ModInteropImportGenerator` NuGet package to your csproj like so: