Skip to content

Commit c342c78

Browse files
YusyurivJack251970
andcommitted
Initial commit
Co-authored-by: Jack Ye <[email protected]>
0 parents  commit c342c78

17 files changed

+1234
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea/
2+
.vs/
3+
bin/
4+
obj/
5+
/packages/
6+
riderModule.iml
7+
/_ReSharper.Caches/
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Flow.Launcher.Localization.Analyzers
4+
{
5+
public static class AnalyzerDiagnostics
6+
{
7+
public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new DiagnosticDescriptor(
8+
"FLAN0001",
9+
"Old localization API used",
10+
"Use `Localize.{0}({1})` instead",
11+
"Localization",
12+
DiagnosticSeverity.Warning,
13+
isEnabledByDefault: true
14+
);
15+
16+
public static readonly DiagnosticDescriptor ContextIsAField = new DiagnosticDescriptor(
17+
"FLAN0002",
18+
"Plugin context is a field",
19+
"Plugin context must be at least internal static property",
20+
"Localization",
21+
DiagnosticSeverity.Error,
22+
isEnabledByDefault: true
23+
);
24+
25+
public static readonly DiagnosticDescriptor ContextIsNotStatic = new DiagnosticDescriptor(
26+
"FLAN0003",
27+
"Plugin context is not static",
28+
"Plugin context must be at least internal static property",
29+
"Localization",
30+
DiagnosticSeverity.Error,
31+
isEnabledByDefault: true
32+
);
33+
34+
public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new DiagnosticDescriptor(
35+
"FLAN0004",
36+
"Plugin context property access modifier is too restrictive",
37+
"Plugin context property must be at least internal static property",
38+
"Localization",
39+
DiagnosticSeverity.Error,
40+
isEnabledByDefault: true
41+
);
42+
43+
public static readonly DiagnosticDescriptor ContextIsNotDeclared = new DiagnosticDescriptor(
44+
"FLAN0005",
45+
"Plugin context is not declared",
46+
"Plugin context must be at least internal static property of type `PluginInitContext`",
47+
"Localization",
48+
DiagnosticSeverity.Error,
49+
isEnabledByDefault: true
50+
);
51+
}
52+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|-------
7+
FLAN0001 | Localization | Warning | FLAN0001_OldLocalizationApiUsed
8+
FLAN0002 | Localization | Error | FLAN0002_ContextIsAField
9+
FLAN0003 | Localization | Error | FLAN0003_ContextIsNotStatic
10+
FLAN0004 | Localization | Error | FLAN0004_ContextAccessIsTooRestrictive
11+
FLAN0005 | Localization | Error | FLAN0005_ContextIsNotDeclared
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+
<Version>1.0.0</Version>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
7+
<RootNamespace>Flow.Launcher.Localization.Analyzers</RootNamespace>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
12+
<PrivateAssets>all</PrivateAssets>
13+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
14+
</PackageReference>
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
16+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2"/>
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
namespace Flow.Launcher.Localization.Analyzers.Localize
9+
{
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer
12+
{
13+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
14+
AnalyzerDiagnostics.ContextIsAField,
15+
AnalyzerDiagnostics.ContextIsNotStatic,
16+
AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
17+
AnalyzerDiagnostics.ContextIsNotDeclared
18+
);
19+
20+
private const string PluginContextTypeName = "PluginInitContext";
21+
22+
private const string PluginInterfaceName = "IPluginI18n";
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
27+
context.EnableConcurrentExecution();
28+
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
29+
}
30+
31+
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
32+
{
33+
var classDeclaration = (ClassDeclarationSyntax)context.Node;
34+
var semanticModel = context.SemanticModel;
35+
var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
36+
37+
if (!IsPluginEntryClass(classSymbol)) return;
38+
39+
var contextProperty = classDeclaration.Members.OfType<PropertyDeclarationSyntax>()
40+
.Select(p => semanticModel.GetDeclaredSymbol(p))
41+
.FirstOrDefault(p => p?.Type.Name is PluginContextTypeName);
42+
43+
if (contextProperty != null)
44+
{
45+
if (!contextProperty.IsStatic)
46+
{
47+
context.ReportDiagnostic(Diagnostic.Create(
48+
AnalyzerDiagnostics.ContextIsNotStatic,
49+
contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation()
50+
));
51+
return;
52+
}
53+
54+
if (contextProperty.DeclaredAccessibility is Accessibility.Private || contextProperty.DeclaredAccessibility is Accessibility.Protected)
55+
{
56+
context.ReportDiagnostic(Diagnostic.Create(
57+
AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
58+
contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation()
59+
));
60+
return;
61+
}
62+
63+
return;
64+
}
65+
66+
var fieldDeclaration = classDeclaration.Members
67+
.OfType<FieldDeclarationSyntax>()
68+
.SelectMany(f => f.Declaration.Variables)
69+
.Select(f => semanticModel.GetDeclaredSymbol(f))
70+
.FirstOrDefault(f => f is IFieldSymbol fs && fs.Type.Name is PluginContextTypeName);
71+
var parentSyntax = fieldDeclaration
72+
?.DeclaringSyntaxReferences[0]
73+
.GetSyntax()
74+
.FirstAncestorOrSelf<FieldDeclarationSyntax>();
75+
76+
if (parentSyntax != null)
77+
{
78+
context.ReportDiagnostic(Diagnostic.Create(
79+
AnalyzerDiagnostics.ContextIsAField,
80+
parentSyntax.GetLocation()
81+
));
82+
return;
83+
}
84+
85+
context.ReportDiagnostic(Diagnostic.Create(
86+
AnalyzerDiagnostics.ContextIsNotDeclared,
87+
classDeclaration.Identifier.GetLocation()
88+
));
89+
}
90+
91+
private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) =>
92+
namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false;
93+
}
94+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Formatting;
11+
using Microsoft.CodeAnalysis.Simplification;
12+
using Microsoft.CodeAnalysis.Text;
13+
14+
namespace Flow.Launcher.Localization.Analyzers.Localize
15+
{
16+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared]
17+
public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider
18+
{
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
20+
AnalyzerDiagnostics.ContextIsAField.Id,
21+
AnalyzerDiagnostics.ContextIsNotStatic.Id,
22+
AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id,
23+
AnalyzerDiagnostics.ContextIsNotDeclared.Id
24+
);
25+
26+
public sealed override FixAllProvider GetFixAllProvider()
27+
{
28+
return WellKnownFixAllProviders.BatchFixer;
29+
}
30+
31+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34+
35+
var diagnostic = context.Diagnostics.First();
36+
var diagnosticSpan = diagnostic.Location.SourceSpan;
37+
38+
if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id)
39+
{
40+
context.RegisterCodeFix(
41+
CodeAction.Create(
42+
title: "Replace with static property",
43+
createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)),
44+
equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id
45+
),
46+
diagnostic
47+
);
48+
}
49+
else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id)
50+
{
51+
context.RegisterCodeFix(
52+
CodeAction.Create(
53+
title: "Make static",
54+
createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)),
55+
equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id
56+
),
57+
diagnostic
58+
);
59+
}
60+
else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id)
61+
{
62+
context.RegisterCodeFix(
63+
CodeAction.Create(
64+
title: "Make internal",
65+
createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)),
66+
equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id
67+
),
68+
diagnostic
69+
);
70+
}
71+
else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id)
72+
{
73+
context.RegisterCodeFix(
74+
CodeAction.Create(
75+
title: "Declare context property",
76+
createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)),
77+
equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id
78+
),
79+
diagnostic
80+
);
81+
}
82+
}
83+
84+
private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") =>
85+
SyntaxFactory.ParseMemberDeclaration(
86+
$"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;"
87+
);
88+
89+
private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root)
90+
{
91+
var formattedRoot = Formatter.Format(
92+
root,
93+
Formatter.Annotation,
94+
context.Document.Project.Solution.Workspace
95+
);
96+
97+
return context.Document.WithSyntaxRoot(formattedRoot);
98+
}
99+
100+
private static Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
101+
{
102+
var classDeclaration = GetDeclarationSyntax<ClassDeclarationSyntax>(root, diagnosticSpan);
103+
if (classDeclaration?.BaseList is null) return context.Document;
104+
105+
var newPropertyDeclaration = GetStaticContextPropertyDeclaration();
106+
if (newPropertyDeclaration is null) return context.Document;
107+
108+
var annotatedNewPropertyDeclaration = newPropertyDeclaration
109+
.WithLeadingTrivia(SyntaxFactory.ElasticLineFeed)
110+
.WithTrailingTrivia(SyntaxFactory.ElasticLineFeed)
111+
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
112+
113+
var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration);
114+
var newClassDeclaration = classDeclaration.WithMembers(newMembers);
115+
116+
var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
117+
118+
return GetFormattedDocument(context, newRoot);
119+
}
120+
121+
private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
122+
{
123+
var propertyDeclaration = GetDeclarationSyntax<PropertyDeclarationSyntax>(root, diagnosticSpan);
124+
if (propertyDeclaration is null) return context.Document;
125+
126+
var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword));
127+
128+
var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration);
129+
return context.Document.WithSyntaxRoot(newRoot);
130+
}
131+
132+
private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
133+
{
134+
var propertyDeclaration = GetDeclarationSyntax<PropertyDeclarationSyntax>(root, diagnosticSpan);
135+
if (propertyDeclaration is null) return context.Document;
136+
137+
var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration);
138+
139+
var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration);
140+
return context.Document.WithSyntaxRoot(newRoot);
141+
}
142+
143+
private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) {
144+
var fieldDeclaration = GetDeclarationSyntax<FieldDeclarationSyntax>(root, diagnosticSpan);
145+
if (fieldDeclaration is null) return context.Document;
146+
147+
var field = fieldDeclaration.Declaration.Variables.First();
148+
var fieldIdentifier = field.Identifier.ToString();
149+
150+
var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier);
151+
if (propertyDeclaration is null) return context.Document;
152+
153+
var annotatedNewPropertyDeclaration = propertyDeclaration
154+
.WithTriviaFrom(fieldDeclaration)
155+
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
156+
157+
var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration);
158+
159+
return GetFormattedDocument(context, newRoot);
160+
}
161+
162+
private static PropertyDeclarationSyntax FixRestrictivePropertyModifiers(PropertyDeclarationSyntax propertyDeclaration)
163+
{
164+
var newModifiers = SyntaxFactory.TokenList();
165+
foreach (var modifier in propertyDeclaration.Modifiers)
166+
{
167+
if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword))
168+
{
169+
newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword));
170+
}
171+
else
172+
{
173+
newModifiers = newModifiers.Add(modifier);
174+
}
175+
}
176+
177+
return propertyDeclaration.WithModifiers(newModifiers);
178+
}
179+
180+
private static T GetDeclarationSyntax<T>(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode =>
181+
root
182+
.FindToken(diagnosticSpan.Start)
183+
.Parent
184+
?.AncestorsAndSelf()
185+
.OfType<T>()
186+
.First();
187+
}
188+
}

0 commit comments

Comments
 (0)