Skip to content

Commit 5448b59

Browse files
committed
draft
1 parent 23c5d10 commit 5448b59

19 files changed

+356
-4
lines changed

Directory.Packages.props

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
</ItemGroup>
7+
</Project>

NuGet.config

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="local-MustBePartial" value="./src/analyzers/SourceKit.Analyzers.MustBePartial/bin/Debug"/>
5+
<add key="local-MustBePartial.Annotations" value="./src/analyzers/SourceKit.Analyzers.MustBePartial.Annotations/bin/Debug"/>
6+
</packageSources>
7+
</configuration>

SourceKit.Sample/.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[*.cs]
2+
3+
dotnet_diagnostic.SK1000.severity = warning
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using SourceKit.Analyzers.MustBePartial.Annotations;
2+
3+
namespace SourceKit.Sample.MustBePartial;
4+
5+
[DerivativesMustBePartial]
6+
public interface IPartialBase { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace SourceKit.Sample.MustBePartial;
2+
3+
public class NotPartialDerivative : IPartialBase
4+
{
5+
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace SourceKit.Sample.MustBePartial;
2+
3+
public partial class PartialDerivative : IPartialBase
4+
{
5+
6+
}
+6-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>11</LangVersion>
56
<Nullable>enable</Nullable>
67
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="SourceKit.Analyzers.MustBePartial" Version="1.0.0" />
11+
</ItemGroup>
712

813
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Microsoft.CodeAnalysis.CSharp.Testing;
2+
using Microsoft.CodeAnalysis.Testing.Verifiers;
3+
using SourceKit.Analyzers.MustBePartial.Analyzers;
4+
using Xunit;
5+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<
6+
SourceKit.Analyzers.MustBePartial.Analyzers.DerivativesMustBePartialAnalyzer>;
7+
8+
namespace SourceKit.Tests.Analyzers;
9+
10+
public class MustBePartialTests
11+
{
12+
[Fact]
13+
public async Task A()
14+
{
15+
var subject = await File.ReadAllTextAsync("MustBePartial/NotPartialDerivative.cs");
16+
17+
var test = new CSharpAnalyzerTest<DerivativesMustBePartialAnalyzer, XUnitVerifier>
18+
{
19+
TestState =
20+
{
21+
Sources = { subject },
22+
AdditionalFiles = { await LoadFileAsync("MustBePartial/IPartialBase.cs") },
23+
ExpectedDiagnostics = { Verify.Diagnostic(DerivativesMustBePartialAnalyzer.Descriptor) },
24+
},
25+
};
26+
27+
await test.RunAsync();
28+
}
29+
30+
private static async Task<(string name, string content)> LoadFileAsync(string path)
31+
{
32+
var name = Path.ChangeExtension(Path.GetFileName(path), null);
33+
var content = await File.ReadAllTextAsync(path);
34+
35+
return (name, content);
36+
}
37+
}

SourceKit.Tests/SourceKit.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.1" />
1314
<PackageReference Include="Sigil" Version="5.0.0" />
1415
<PackageReference Include="FluentAssertions" Version="6.10.0" />
1516
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" />
@@ -33,6 +34,7 @@
3334

3435
<ItemGroup>
3536
<ProjectReference Include="..\SourceKit.Sample\SourceKit.Sample.csproj" />
37+
<ProjectReference Include="..\src\analyzers\SourceKit.Analyzers.MustBePartial\SourceKit.Analyzers.MustBePartial.csproj" />
3638
<ProjectReference Include="..\src\SourceKit.Reflect\SourceKit.Reflect.csproj" />
3739
</ItemGroup>
3840

SourceKit.sln

+17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceKit.Sample", "SourceK
1212
EndProject
1313
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceKit.Tests", "SourceKit.Tests\SourceKit.Tests.csproj", "{EE7B76B2-A19F-4F9D-B091-9E6EF9151164}"
1414
EndProject
15+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzers", "analyzers", "{0506CF17-3170-420E-B120-3BEB0BDE68CF}"
16+
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceKit.Analyzers.MustBePartial.Annotations", "src\analyzers\SourceKit.Analyzers.MustBePartial.Annotations\SourceKit.Analyzers.MustBePartial.Annotations.csproj", "{B94E98ED-AD2A-4DE0-B405-2C38F75FA000}"
18+
EndProject
19+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceKit.Analyzers.MustBePartial", "src\analyzers\SourceKit.Analyzers.MustBePartial\SourceKit.Analyzers.MustBePartial.csproj", "{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F}"
20+
EndProject
1521
Global
1622
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1723
Debug|Any CPU = Debug|Any CPU
@@ -22,6 +28,9 @@ Global
2228
{8EED1912-BB7A-4C97-9032-08E3219724DE} = {BBA45AFB-5727-47BB-9BBB-B05559BE892E}
2329
{701CE759-EAF3-439D-91BF-C158F5860B72} = {4B7FE6F2-BF92-4698-A01F-4C1E111B62CF}
2430
{EE7B76B2-A19F-4F9D-B091-9E6EF9151164} = {4B7FE6F2-BF92-4698-A01F-4C1E111B62CF}
31+
{0506CF17-3170-420E-B120-3BEB0BDE68CF} = {BBA45AFB-5727-47BB-9BBB-B05559BE892E}
32+
{B94E98ED-AD2A-4DE0-B405-2C38F75FA000} = {0506CF17-3170-420E-B120-3BEB0BDE68CF}
33+
{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F} = {0506CF17-3170-420E-B120-3BEB0BDE68CF}
2534
EndGlobalSection
2635
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2736
{637C01C1-3A3C-4FC6-9874-6CFBA4319A79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -40,5 +49,13 @@ Global
4049
{EE7B76B2-A19F-4F9D-B091-9E6EF9151164}.Debug|Any CPU.Build.0 = Debug|Any CPU
4150
{EE7B76B2-A19F-4F9D-B091-9E6EF9151164}.Release|Any CPU.ActiveCfg = Release|Any CPU
4251
{EE7B76B2-A19F-4F9D-B091-9E6EF9151164}.Release|Any CPU.Build.0 = Release|Any CPU
52+
{B94E98ED-AD2A-4DE0-B405-2C38F75FA000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53+
{B94E98ED-AD2A-4DE0-B405-2C38F75FA000}.Debug|Any CPU.Build.0 = Debug|Any CPU
54+
{B94E98ED-AD2A-4DE0-B405-2C38F75FA000}.Release|Any CPU.ActiveCfg = Release|Any CPU
55+
{B94E98ED-AD2A-4DE0-B405-2C38F75FA000}.Release|Any CPU.Build.0 = Release|Any CPU
56+
{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57+
{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
58+
{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{18E723E4-9DA2-49D3-9B08-0923BEC3FD9F}.Release|Any CPU.Build.0 = Release|Any CPU
4360
EndGlobalSection
4461
EndGlobal

src/SourceKit.Reflect/SourceKit.Reflect.csproj

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<ItemGroup>
2020
<PackageReference Include="Lokad.ILPack" Version="0.2.0" />
21-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
2222
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" PrivateAssets="all" />
2323
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
2424
<PackageReference Include="PolySharp" Version="1.12.1">
@@ -43,4 +43,11 @@
4343
<ProjectReference Include="..\SourceKit\SourceKit.csproj" />
4444
</ItemGroup>
4545

46+
<ItemGroup>
47+
<PackageVersion Update="PolySharp" Version="1.13.1">
48+
<PrivateAssets>all</PrivateAssets>
49+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
50+
</PackageVersion>
51+
</ItemGroup>
52+
4653
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
5+
namespace SourceKit.Extensions;
6+
7+
public static class NamedTypeSymbolExtensions
8+
{
9+
public static IEnumerable<INamedTypeSymbol> GetBaseTypes(this INamedTypeSymbol symbol)
10+
{
11+
return symbol.BaseType is null
12+
? Enumerable.Empty<INamedTypeSymbol>()
13+
: Enumerable.Repeat(symbol.BaseType, 1).Concat(symbol.BaseType.GetBaseTypes());
14+
}
15+
16+
public static IEnumerable<INamedTypeSymbol> GetBaseTypesAndInterfaces(this INamedTypeSymbol symbol)
17+
{
18+
return symbol.GetBaseTypes().Concat(symbol.AllInterfaces);
19+
}
20+
21+
public static bool HasAttribute(this INamedTypeSymbol symbol, INamedTypeSymbol attribute)
22+
{
23+
return symbol
24+
.GetAttributes()
25+
.Select(x => x.AttributeClass)
26+
.WhereNotNull()
27+
.Contains(attribute, SymbolEqualityComparer.Default);
28+
}
29+
30+
public static IEnumerable<TypeDeclarationSyntax> GetDeclarations(this INamedTypeSymbol symbol)
31+
{
32+
return symbol.Locations
33+
.Select(x => (location: x, x.SourceTree))
34+
.Where(x => x.SourceTree is not null)
35+
.Select(x => x.SourceTree!.GetRoot().FindNode(x.location.SourceSpan))
36+
.OfType<TypeDeclarationSyntax>();
37+
}
38+
39+
public static bool IsPartial(this INamedTypeSymbol symbol)
40+
{
41+
return symbol
42+
.GetDeclarations()
43+
.SelectMany(x => x.Modifiers)
44+
.Any(x => x.IsKind(SyntaxKind.PartialKeyword));
45+
}
46+
47+
public static IEnumerable<Location> GetSignatureLocations(this INamedTypeSymbol symbol)
48+
{
49+
return symbol.GetDeclarations().Select(x => x.Identifier.GetLocation());
50+
}
51+
}

src/SourceKit/SourceKit.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
1616
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" PrivateAssets="all" />
1717
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
1818
</ItemGroup>
19-
19+
2020
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace SourceKit.Analyzers.MustBePartial.Annotations;
2+
3+
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
4+
public class DerivativesMustBePartialAttribute : Attribute { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<LangVersion>11</LangVersion>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
12+
</PropertyGroup>
13+
14+
<PropertyGroup>
15+
<PackageId>SourceKit.Analyzers.MustBePartial.Annotations</PackageId>
16+
<Title>SourceKit.Analyzers.MustBePartial.Annotations</Title>
17+
<Authors>ronimizy</Authors>
18+
</PropertyGroup>
19+
20+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Immutable;
2+
using System.Diagnostics;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using SourceKit.Analyzers.MustBePartial.Tools;
6+
using SourceKit.Extensions;
7+
8+
namespace SourceKit.Analyzers.MustBePartial.Analyzers;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
11+
public class DerivativesMustBePartialAnalyzer : DiagnosticAnalyzer
12+
{
13+
public const string DiagnosticId = "SK1000";
14+
public const string Title = nameof(DerivativesMustBePartialAnalyzer);
15+
16+
public const string Format = """Type "{0}" must be partial""";
17+
18+
public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
19+
DiagnosticId,
20+
Title,
21+
Format,
22+
"Class definition",
23+
DiagnosticSeverity.Error,
24+
true);
25+
26+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
27+
ImmutableArray.Create(Descriptor);
28+
29+
public override void Initialize(AnalysisContext context)
30+
{
31+
context.EnableConcurrentExecution();
32+
context.ConfigureGeneratedCodeAnalysis(
33+
GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
34+
35+
context.RegisterCompilationStartAction(compilationContext =>
36+
{
37+
var derivativesMustBePartialAttributeType = compilationContext.Compilation
38+
.GetTypeByMetadataName(Constants.DerivativesMustBePartialAttributeFullyQualifiedName);
39+
40+
if (derivativesMustBePartialAttributeType is null)
41+
return;
42+
43+
compilationContext.RegisterSymbolAction(
44+
x => AnalyzeTypeSymbol(x, derivativesMustBePartialAttributeType),
45+
SymbolKind.NamedType);
46+
});
47+
}
48+
49+
private static void AnalyzeTypeSymbol(SymbolAnalysisContext context, INamedTypeSymbol attributeSymbol)
50+
{
51+
context.CancellationToken.ThrowIfCancellationRequested();
52+
53+
var typeSymbol = (INamedTypeSymbol)context.Symbol;
54+
55+
IEnumerable<INamedTypeSymbol> baseTypesAndInterfaces = typeSymbol.GetBaseTypesAndInterfaces();
56+
57+
var mustBePartial = baseTypesAndInterfaces.Any(x => x.HasAttribute(attributeSymbol));
58+
var isPartial = typeSymbol.IsPartial();
59+
60+
if ((mustBePartial, isPartial) is not (true, false))
61+
return;
62+
63+
var location = typeSymbol.GetSignatureLocations().Single();
64+
var diagnostic = Diagnostic.Create(Descriptor, location, typeSymbol.Name);
65+
66+
context.ReportDiagnostic(diagnostic);
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CodeActions;
4+
using Microsoft.CodeAnalysis.CodeFixes;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using SourceKit.Analyzers.MustBePartial.Analyzers;
8+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
9+
10+
namespace SourceKit.Analyzers.MustBePartial.CodeFixes;
11+
12+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MakeTypePartialCodeFixProvider))]
13+
public class MakeTypePartialCodeFixProvider : CodeFixProvider
14+
{
15+
public const string Title = "Make type partial";
16+
17+
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
18+
ImmutableArray.Create(DerivativesMustBePartialAnalyzer.DiagnosticId);
19+
20+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
21+
{
22+
context.CancellationToken.ThrowIfCancellationRequested();
23+
24+
IEnumerable<Task> derivativesMustBePartialDiagnostics = context.Diagnostics
25+
.Where(x => x.Id.Equals(DerivativesMustBePartialAnalyzer.DiagnosticId))
26+
.Select(x => ProvideDerivativesMustBePartial(context, x));
27+
28+
await Task.WhenAll(derivativesMustBePartialDiagnostics);
29+
}
30+
31+
private static async Task ProvideDerivativesMustBePartial(CodeFixContext context, Diagnostic diagnostic)
32+
{
33+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
34+
35+
if (root?.FindNode(context.Span) is not TypeDeclarationSyntax syntax)
36+
return;
37+
38+
var action = CodeAction.Create(
39+
Title,
40+
equivalenceKey: nameof(Title),
41+
createChangedDocument: async _ =>
42+
{
43+
var newSyntax = syntax.AddModifiers(Token(SyntaxKind.PartialKeyword)).NormalizeWhitespace();
44+
var newRoot = root.ReplaceNode(syntax, newSyntax);
45+
46+
return context.Document.WithSyntaxRoot(newRoot);
47+
});
48+
49+
context.RegisterCodeFix(action, diagnostic);
50+
}
51+
}

0 commit comments

Comments
 (0)