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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
/_ReSharper.Caches/
/.vs
/.idea
103 changes: 103 additions & 0 deletions ModInteropImportGenerator.CodeFix/CodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<Document> 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<ClassDeclarationSyntax>().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<ClassDeclarationSyntax> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>

<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>

<RootNamespace>ModInteropImportGenerator</RootNamespace>
<PackageId>ModInteropImportGenerator.CodeFix</PackageId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ModInteropImportGenerator\ModInteropImportGenerator.csproj" />
</ItemGroup>

</Project>
60 changes: 60 additions & 0 deletions ModInteropImportGenerator.Sample/CodeFixTest.cs
Original file line number Diff line number Diff line change
@@ -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<PlayerStub> onPlayerEnter,
Action<PlayerStub> onPlayerExit)
{
return null;
}

#endregion

#region Seeker

public static bool HasSeekerDash()
{
return false;
}

public static bool IsSeekerDashAttacking()
{
return false;
}

#endregion
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ModInteropImportGenerator.CodeFix\ModInteropImportGenerator.CodeFix.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\ModInteropImportGenerator\ModInteropImportGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ModInteropImportGenerator.CodeFix\ModInteropImportGenerator.CodeFix.csproj" />
<ProjectReference Include="..\ModInteropImportGenerator\ModInteropImportGenerator.csproj"/>
</ItemGroup>

Expand Down
6 changes: 6 additions & 0 deletions ModInteropImportGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions ModInteropImportGenerator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

8 changes: 8 additions & 0 deletions ModInteropImportGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions ModInteropImportGenerator/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ModInteropImportGenerator.CodeFix")]
52 changes: 52 additions & 0 deletions ModInteropImportGenerator/ModInteropImportDiagnosticAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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#")]
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down