Skip to content

Commit 5046191

Browse files
Merge pull request #63 from messerli-informatik-ag/interface-analyzer
Create analyzer for `public` visibility in interfaces
2 parents da4e95e + 5c5e273 commit 5046191

16 files changed

+313
-2
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Build
2424
run: dotnet build --configuration Release --no-restore
2525
- name: Pack
26-
run: dotnet pack --configuration Release --no-restore --output nupkg
26+
run: dotnet pack CodeStyle/CodeStyle.csproj --configuration Release --no-restore --output nupkg
2727
- name: Test that files are importable by MSBuild
2828
run: ruby ./scripts/test_files_are_importable_by_msbuild.rb
2929
- name: Show NuGet package content
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CodeFixes;
5+
using Microsoft.CodeAnalysis.CSharp.Testing;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Testing;
8+
9+
namespace Messerli.CodeStyle.Analyzers.Test
10+
{
11+
public static class CSharpVerifier<TAnalyzer, TCodeFix, TVerifier>
12+
where TAnalyzer : DiagnosticAnalyzer, new()
13+
where TCodeFix : CodeFixProvider, new()
14+
where TVerifier : IVerifier, new()
15+
{
16+
public static DiagnosticResult Diagnostic()
17+
=> AnalyzerVerifier<TAnalyzer, CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>, TVerifier>.Diagnostic();
18+
19+
public static DiagnosticResult Diagnostic(string diagnosticId)
20+
=> AnalyzerVerifier<TAnalyzer, CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>, TVerifier>.Diagnostic(diagnosticId);
21+
22+
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
23+
=> AnalyzerVerifier<TAnalyzer, CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>, TVerifier>.Diagnostic(descriptor);
24+
25+
public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
26+
=> AnalyzerVerifier<TAnalyzer, CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>, TVerifier>.VerifyAnalyzerAsync(source, expected);
27+
28+
public static Task VerifyCodeFixAsync(string source, string fixedSource)
29+
=> VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
30+
31+
public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
32+
=> VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
33+
34+
public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
35+
{
36+
var test = new CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>
37+
{
38+
TestCode = source,
39+
FixedCode = fixedSource,
40+
ReferenceAssemblies = ReferenceAssemblies.Default,
41+
};
42+
43+
test.ExpectedDiagnostics.AddRange(expected);
44+
return test.RunAsync(CancellationToken.None);
45+
}
46+
}
47+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<RootNamespace>Messerli.CodeStyle.Analyzers.Test</RootNamespace>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.0-2.21262.14" />
10+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.0.1-beta1.20421.1" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
12+
<PackageReference Include="xunit" Version="2.4.0" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<ProjectReference Include="..\CodeStyle.Analyzers\CodeStyle.Analyzers.csproj" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Threading.Tasks;
2+
using Xunit;
3+
using VerifyCodeStyle = Messerli.CodeStyle.Analyzers.Test.CSharpVerifier<Messerli.CodeStyle.Analyzers.InterfaceAnalyzers.PublicModifier.PublicModifierInterfaceAnalyzer, Messerli.CodeStyle.Analyzers.InterfaceAnalyzers.PublicModifier.PublicModifierInterfaceCodeFix, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
4+
5+
namespace Messerli.CodeStyle.Analyzers.Test.PublicModifier
6+
{
7+
public class PublicModifierInterfaceAnalyzerTest
8+
{
9+
private const string CodeToFix = @"
10+
public interface ITest
11+
{
12+
[|public|] void DoSomething();
13+
}";
14+
15+
private const string FixedCode = @"
16+
public interface ITest
17+
{
18+
void DoSomething();
19+
}";
20+
21+
[Fact]
22+
public async Task AssertsNoFixNeedsToBeApplied()
23+
{
24+
await VerifyCodeStyle.VerifyAnalyzerAsync(FixedCode);
25+
}
26+
27+
[Fact]
28+
public async Task AssertsFixIsApplied()
29+
{
30+
await VerifyCodeStyle.VerifyAnalyzerAsync(CodeToFix);
31+
}
32+
33+
[Fact]
34+
public async Task AssertCodeFixCreatesExpectedCode()
35+
{
36+
await VerifyCodeStyle.VerifyCodeFixAsync(CodeToFix, FixedCode);
37+
}
38+
}
39+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|-------
8+
MESSERLI001 | Access Modifiers | Warning | RuleConstants
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
6+
<IsPackable>true</IsPackable>
7+
<AssemblyName>Messerli.CodeStyle.Analyzers</AssemblyName>
8+
<RootNamespace>Messerli.CodeStyle.Analyzers</RootNamespace>
9+
<LangVersion>9.0</LangVersion>
10+
<PackageId>Messerli.CodeStyle.Analyzers</PackageId>
11+
<PackageTags>analyzers</PackageTags>
12+
<PackageLicenseExpression>MIT OR Apache-2.0</PackageLicenseExpression>
13+
<Copyright>© Messerli Informatik AG. All rights reserved.</Copyright>
14+
<Description>Various analyzers bundled with opinionated configuration</Description>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" />
19+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
21+
<PackageReference Include="Funcky" Version="2.3.0" />
22+
</ItemGroup>
23+
</Project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using Funcky.Extensions;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
namespace Messerli.CodeStyle.Analyzers.InterfaceAnalyzers.PublicModifier
10+
{
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public class PublicModifierInterfaceAnalyzer : DiagnosticAnalyzer
13+
{
14+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(RuleConstants.Rule);
15+
16+
public override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
context.RegisterSyntaxNodeAction(AnalyzeInterfaceMethods, SyntaxKind.MethodDeclaration);
21+
}
22+
23+
private static void AnalyzeInterfaceMethods(SyntaxNodeAnalysisContext context)
24+
{
25+
var node = (BaseMethodDeclarationSyntax)context.Node;
26+
if (node.Parent.IsKind(SyntaxKind.InterfaceDeclaration))
27+
{
28+
HandleDefaultModifier(context, node.Modifiers, SyntaxKind.PublicKeyword);
29+
}
30+
}
31+
32+
private static void HandleDefaultModifier(SyntaxNodeAnalysisContext context, SyntaxTokenList modifiers, SyntaxKind defaultModifier)
33+
{
34+
modifiers.FirstOrNone(item => item.IsKind(defaultModifier))
35+
.AndThen(ReportDiagnostic(context));
36+
}
37+
38+
private static Action<SyntaxToken> ReportDiagnostic(SyntaxNodeAnalysisContext context)
39+
=> token
40+
=> context.ReportDiagnostic(Diagnostic.Create(RuleConstants.Rule, token.GetLocation()));
41+
}
42+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.Editing;
11+
12+
namespace Messerli.CodeStyle.Analyzers.InterfaceAnalyzers.PublicModifier
13+
{
14+
[ExportCodeFixProvider(LanguageNames.CSharp)]
15+
public class PublicModifierInterfaceCodeFix : CodeFixProvider
16+
{
17+
private static readonly string CodeFixTitle = "Remove unnecessary access modifier";
18+
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleConstants.DiagnosticId);
20+
21+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
22+
23+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
26+
27+
var diagnostic = context.Diagnostics.First();
28+
var diagnosticSpan = diagnostic.Location.SourceSpan;
29+
30+
var syntaxToken = root.FindToken(diagnosticSpan.Start);
31+
32+
context.RegisterCodeFix(
33+
CodeAction.Create(
34+
title: CodeFixTitle,
35+
createChangedDocument: cancellationToken => FixCode(context.Document, syntaxToken, cancellationToken),
36+
equivalenceKey: CodeFixTitle),
37+
diagnostic);
38+
}
39+
40+
private static async Task<Document> FixCode(Document document, SyntaxToken syntax, CancellationToken cancellationToken)
41+
{
42+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
43+
var nextToken = syntax.GetNextToken();
44+
editor.ReplaceNode(
45+
syntax.Parent,
46+
syntax.Parent.ReplaceTokens(new[] { syntax, nextToken },
47+
(current, _) => ExtractSyntaxToken(syntax, current, nextToken)));
48+
return editor.GetChangedDocument();
49+
}
50+
51+
private static SyntaxToken ExtractSyntaxToken(SyntaxToken syntax, SyntaxToken current, SyntaxToken next)
52+
=> current switch
53+
{
54+
_ when current == syntax => SyntaxFactory.Token(SyntaxKind.None),
55+
_ when current == next => next.WithLeadingTrivia(syntax.LeadingTrivia.AddRange(next.LeadingTrivia)),
56+
_ => throw new ArgumentOutOfRangeException(nameof(current), current, null)
57+
};
58+
}
59+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Messerli.CodeStyle.Analyzers.InterfaceAnalyzers.PublicModifier
4+
{
5+
public static class RuleConstants
6+
{
7+
private static readonly LocalizableString Title = "Interface method declaration contains public access modifiers";
8+
private static readonly LocalizableString MessageFormat = "Method contains public access modifiers";
9+
private static readonly LocalizableString Description = "Public access modifiers should all be omitted on interface method declaration.";
10+
private const string Category = "Access Modifiers";
11+
12+
public const string DiagnosticId = "MESSERLI001";
13+
14+
public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
15+
id: DiagnosticId,
16+
title: Title,
17+
messageFormat: MessageFormat,
18+
category: Category,
19+
defaultSeverity: DiagnosticSeverity.Warning,
20+
isEnabledByDefault: true,
21+
description: Description);
22+
}
23+
}

0 commit comments

Comments
 (0)