Skip to content

Commit 188977f

Browse files
committed
Add view model conventions roslyn analyzers & code fixes
1 parent 51f53b5 commit 188977f

File tree

11 files changed

+1181
-0
lines changed

11 files changed

+1181
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@
164164
<ItemGroup>
165165
<ProjectReference Include="..\Avalonia.Gif\Avalonia.Gif.csproj" />
166166
<ProjectReference Include="..\StabilityMatrix.Core\StabilityMatrix.Core.csproj" />
167+
<ProjectReference Include="..\analyzers\StabilityMatrix.Analyzers\StabilityMatrix.Analyzers.csproj"
168+
PrivateAssets="all"
169+
ReferenceOutputAssembly="false"
170+
OutputItemType="Analyzer" />
171+
<ProjectReference Include="..\analyzers\StabilityMatrix.Analyzers.CodeFixes\StabilityMatrix.Analyzers.CodeFixes.csproj"
172+
PrivateAssets="all"
173+
ReferenceOutputAssembly="false"
174+
OutputItemType="Analyzer" />
167175
</ItemGroup>
168176

169177
<ItemGroup Condition="$(SM_IncludeLogWindow) == 'true'">

StabilityMatrix.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Native.Abst
2525
EndProject
2626
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Native.macOS", "StabilityMatrix.Native.macOS\StabilityMatrix.Native.macOS.csproj", "{473AE646-17E4-4247-9271-858D257DFFE1}"
2727
EndProject
28+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzers", "analyzers", "{87BDF27C-A933-4FE5-A734-57AB61BFF15A}"
29+
EndProject
30+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Analyzers", "analyzers\StabilityMatrix.Analyzers\StabilityMatrix.Analyzers.csproj", "{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4}"
31+
EndProject
32+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Analyzers.CodeFixes", "analyzers\StabilityMatrix.Analyzers.CodeFixes\StabilityMatrix.Analyzers.CodeFixes.csproj", "{32B22C7D-BF47-4D77-A9D7-FFD863650FCB}"
33+
EndProject
2834
Global
2935
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3036
Debug|Any CPU = Debug|Any CPU
@@ -73,11 +79,23 @@ Global
7379
{473AE646-17E4-4247-9271-858D257DFFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
7480
{473AE646-17E4-4247-9271-858D257DFFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
7581
{473AE646-17E4-4247-9271-858D257DFFE1}.Release|Any CPU.Build.0 = Release|Any CPU
82+
{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
83+
{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
84+
{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
85+
{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4}.Release|Any CPU.Build.0 = Release|Any CPU
86+
{32B22C7D-BF47-4D77-A9D7-FFD863650FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
87+
{32B22C7D-BF47-4D77-A9D7-FFD863650FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
88+
{32B22C7D-BF47-4D77-A9D7-FFD863650FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
89+
{32B22C7D-BF47-4D77-A9D7-FFD863650FCB}.Release|Any CPU.Build.0 = Release|Any CPU
7690
EndGlobalSection
7791
GlobalSection(SolutionProperties) = preSolution
7892
HideSolutionNode = FALSE
7993
EndGlobalSection
8094
GlobalSection(ExtensibilityGlobals) = postSolution
8195
SolutionGuid = {97DDAF21-661E-4E36-ABC3-BF2052415919}
8296
EndGlobalSection
97+
GlobalSection(NestedProjects) = preSolution
98+
{BDE3B28C-F4AD-4A00-AA91-A1DA706AE4E4} = {87BDF27C-A933-4FE5-A734-57AB61BFF15A}
99+
{32B22C7D-BF47-4D77-A9D7-FFD863650FCB} = {87BDF27C-A933-4FE5-A734-57AB61BFF15A}
100+
EndGlobalSection
83101
EndGlobal
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Runtime.Serialization;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CodeActions;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Editing;
10+
using Microsoft.CodeAnalysis.Options;
11+
using Microsoft.CodeAnalysis.Simplification;
12+
using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter;
13+
14+
namespace StabilityMatrix.Analyzers.CodeFixes;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ControlMustInheritBaseFixProvider)), Shared]
17+
public class ControlMustInheritBaseFixProvider : CodeFixProvider
18+
{
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("SM0001");
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+
if (root == null)
27+
return;
28+
29+
var diagnostic = context.Diagnostics.First();
30+
var diagnosticSpan = diagnostic.Location.SourceSpan;
31+
32+
if (
33+
root.FindNode(diagnosticSpan, getInnermostNodeForTie: true)
34+
is not TypeOfExpressionSyntax typeOfExpression
35+
)
36+
return;
37+
38+
var semanticModel = await context
39+
.Document.GetSemanticModelAsync(context.CancellationToken)
40+
.ConfigureAwait(false);
41+
if (semanticModel == null)
42+
return;
43+
44+
var controlTypeSymbol =
45+
semanticModel.GetSymbolInfo(typeOfExpression.Type, context.CancellationToken).Symbol
46+
as INamedTypeSymbol;
47+
if (controlTypeSymbol == null || controlTypeSymbol.DeclaringSyntaxReferences.IsDefaultOrEmpty)
48+
return;
49+
50+
// --- CRITICAL CHANGE: Get the Document for the Control Type ---
51+
var controlSyntaxRef = controlTypeSymbol.DeclaringSyntaxReferences.FirstOrDefault();
52+
if (controlSyntaxRef == null)
53+
return;
54+
55+
var controlDocument = context.Document.Project.Solution.GetDocument(controlSyntaxRef.SyntaxTree);
56+
if (controlDocument == null)
57+
return;
58+
// --- END CRITICAL CHANGE ---
59+
60+
var userControlSymbol = semanticModel.Compilation.GetTypeByMetadataName(
61+
"Avalonia.Controls.UserControl"
62+
);
63+
var templatedControlSymbol = semanticModel.Compilation.GetTypeByMetadataName(
64+
"Avalonia.Controls.TemplatedControl"
65+
);
66+
67+
var suggestedBaseTypeName = "UserControlBase";
68+
var suggestedBaseTypeFullName = "StabilityMatrix.Avalonia.Controls.UserControlBase";
69+
70+
if (templatedControlSymbol != null && DoesInheritFrom(controlTypeSymbol, templatedControlSymbol))
71+
{
72+
suggestedBaseTypeName = "TemplatedControlBase";
73+
suggestedBaseTypeFullName = "StabilityMatrix.Avalonia.Controls.TemplatedControlBase";
74+
}
75+
76+
var title = $"Make '{controlTypeSymbol.Name}' inherit from {suggestedBaseTypeName}";
77+
context.RegisterCodeFix(
78+
CodeAction.Create(
79+
title: title,
80+
// Pass the controlDocument to the fix method
81+
createChangedSolution: c =>
82+
MakeControlInheritBaseAsync(
83+
controlDocument,
84+
controlTypeSymbol,
85+
suggestedBaseTypeFullName,
86+
c
87+
),
88+
equivalenceKey: title
89+
),
90+
diagnostic
91+
);
92+
}
93+
94+
// MakeControlInheritBaseAsync now takes a Document parameter which is the control's document
95+
private async Task<Solution> MakeControlInheritBaseAsync(
96+
Document controlDocument,
97+
INamedTypeSymbol controlTypeSymbol,
98+
string suggestedBaseTypeFullName,
99+
CancellationToken cancellationToken
100+
)
101+
{
102+
var editor = await DocumentEditor
103+
.CreateAsync(controlDocument, cancellationToken)
104+
.ConfigureAwait(false);
105+
var semanticModel = await controlDocument
106+
.GetSemanticModelAsync(cancellationToken)
107+
.ConfigureAwait(false);
108+
if (semanticModel == null)
109+
return controlDocument.Project.Solution;
110+
111+
if (
112+
controlTypeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken)
113+
is not ClassDeclarationSyntax controlDeclarationSyntaxFromSymbol
114+
)
115+
return controlDocument.Project.Solution;
116+
117+
var classNodeToModify = editor
118+
.OriginalRoot.DescendantNodesAndSelf()
119+
.OfType<ClassDeclarationSyntax>()
120+
.FirstOrDefault(c =>
121+
c.Span == controlDeclarationSyntaxFromSymbol.Span
122+
&& c.IsEquivalentTo(controlDeclarationSyntaxFromSymbol)
123+
);
124+
125+
if (classNodeToModify == null)
126+
return controlDocument.Project.Solution;
127+
128+
var suggestedBaseSymbol = semanticModel.Compilation.GetTypeByMetadataName(suggestedBaseTypeFullName);
129+
if (suggestedBaseSymbol == null)
130+
return controlDocument.Project.Solution;
131+
132+
// If it already inherits the target, no change needed.
133+
if (DoesInheritFrom(controlTypeSymbol, suggestedBaseSymbol))
134+
{
135+
return controlDocument.Project.Solution;
136+
}
137+
138+
var finalClassNode = classNodeToModify; // Start with the original
139+
var baseListModified = false;
140+
141+
if (classNodeToModify.BaseList != null && classNodeToModify.BaseList.Types.Any())
142+
{
143+
var existingBaseList = classNodeToModify.BaseList;
144+
var newTypes = new List<BaseTypeSyntax>();
145+
var replacedExisting = false;
146+
147+
var typeToReplaceSimpleName = "";
148+
var replacementSimpleName = "";
149+
150+
if (suggestedBaseTypeFullName.EndsWith("UserControlBase"))
151+
{
152+
typeToReplaceSimpleName = "UserControl";
153+
replacementSimpleName = "UserControlBase";
154+
}
155+
else if (suggestedBaseTypeFullName.EndsWith("TemplatedControlBase"))
156+
{
157+
typeToReplaceSimpleName = "TemplatedControl";
158+
replacementSimpleName = "TemplatedControlBase";
159+
}
160+
161+
if (!string.IsNullOrEmpty(typeToReplaceSimpleName))
162+
{
163+
foreach (var baseTypeSyntax in existingBaseList.Types)
164+
{
165+
// Check if the current baseTypeSyntax's Type is a SimpleNameSyntax matching typeToReplaceSimpleName
166+
if (
167+
baseTypeSyntax.Type is SimpleNameSyntax simpleName
168+
&& simpleName.Identifier.ValueText == typeToReplaceSimpleName
169+
)
170+
{
171+
// Replace it
172+
var replacementIdentifier = SyntaxFactory
173+
.IdentifierName(replacementSimpleName)
174+
.WithLeadingTrivia(simpleName.GetLeadingTrivia()) // Preserve trivia
175+
.WithTrailingTrivia(simpleName.GetTrailingTrivia());
176+
newTypes.Add(SyntaxFactory.SimpleBaseType(replacementIdentifier));
177+
replacedExisting = true;
178+
baseListModified = true;
179+
}
180+
// Check if it's a QualifiedNameSyntax ending with the typeToReplaceSimpleName
181+
else if (
182+
baseTypeSyntax.Type is QualifiedNameSyntax { Right: { } rightName }
183+
&& rightName.Identifier.ValueText == typeToReplaceSimpleName
184+
)
185+
{
186+
// More complex: replace Namespace.UserControl with Namespace.UserControlBase
187+
// Or, ideally, just use the minimally qualified name here too.
188+
// For simplicity, let's assume we aim for simple names in base list and handle with usings.
189+
var replacementIdentifier = SyntaxFactory
190+
.IdentifierName(replacementSimpleName)
191+
.WithLeadingTrivia(baseTypeSyntax.Type.GetLeadingTrivia())
192+
.WithTrailingTrivia(baseTypeSyntax.Type.GetTrailingTrivia());
193+
newTypes.Add(SyntaxFactory.SimpleBaseType(replacementIdentifier));
194+
replacedExisting = true;
195+
baseListModified = true;
196+
}
197+
else
198+
{
199+
newTypes.Add(baseTypeSyntax); // Keep other bases
200+
}
201+
}
202+
}
203+
204+
if (replacedExisting)
205+
{
206+
finalClassNode = classNodeToModify.WithBaseList(
207+
existingBaseList.WithTypes(SyntaxFactory.SeparatedList(newTypes))
208+
);
209+
}
210+
else // Didn't replace, so add the suggested base type (if not already present)
211+
{
212+
var minimallyQualifiedSuggestedName = suggestedBaseSymbol.ToDisplayString(
213+
SymbolDisplayFormat.MinimallyQualifiedFormat
214+
);
215+
var alreadyPresent = existingBaseList.Types.Any(bt =>
216+
bt.Type.ToString() == minimallyQualifiedSuggestedName
217+
);
218+
if (!alreadyPresent)
219+
{
220+
var newSimpleBaseType = SyntaxFactory.SimpleBaseType(
221+
SyntaxFactory.ParseTypeName(minimallyQualifiedSuggestedName)
222+
);
223+
finalClassNode = classNodeToModify.WithBaseList(
224+
existingBaseList.AddTypes(newSimpleBaseType)
225+
);
226+
baseListModified = true;
227+
}
228+
}
229+
}
230+
else // No base list, add a new one
231+
{
232+
var newSimpleBaseType = SyntaxFactory.SimpleBaseType(
233+
SyntaxFactory.ParseTypeName(
234+
suggestedBaseSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
235+
)
236+
);
237+
finalClassNode = classNodeToModify.WithBaseList(
238+
SyntaxFactory.BaseList(
239+
SyntaxFactory.SingletonSeparatedList<BaseTypeSyntax>(newSimpleBaseType)
240+
)
241+
);
242+
baseListModified = true;
243+
}
244+
245+
if (baseListModified)
246+
{
247+
// Add Formatter.Annotation to help with curly brace placement
248+
editor.ReplaceNode(
249+
classNodeToModify,
250+
finalClassNode.WithAdditionalAnnotations(Formatter.Annotation)
251+
);
252+
}
253+
254+
// --- Add Using Directive ---
255+
if (
256+
suggestedBaseSymbol.ContainingNamespace != null
257+
&& !IsGlobalNamespace(suggestedBaseSymbol.ContainingNamespace)
258+
)
259+
{
260+
// await editor.AddUsingDirectiveIfNotPresentAsync(suggestedBaseSymbol.ContainingNamespace, cancellationToken);
261+
}
262+
263+
var finalDocument = editor.GetChangedDocument();
264+
265+
// Simplifier should ensure minimally qualified names are used where possible
266+
var simplifiedDocument = await Simplifier
267+
.ReduceAsync(
268+
finalDocument,
269+
await controlDocument.GetOptionsAsync(cancellationToken),
270+
cancellationToken
271+
)
272+
.ConfigureAwait(false);
273+
274+
return simplifiedDocument.Project.Solution;
275+
}
276+
277+
private bool IsGlobalNamespace(INamespaceSymbol? namespaceSymbol)
278+
{
279+
return namespaceSymbol == null || namespaceSymbol.IsGlobalNamespace;
280+
}
281+
282+
private bool HasUsingDirective(CompilationUnitSyntax root, string namespaceName)
283+
{
284+
return root.Usings.Any(u => u.Name?.ToString() == namespaceName);
285+
}
286+
287+
private static bool DoesInheritFrom(INamedTypeSymbol? type, INamedTypeSymbol? baseType)
288+
{
289+
// ... (implementation remains the same)
290+
if (type == null || baseType == null)
291+
return false;
292+
if (SymbolEqualityComparer.Default.Equals(type, baseType))
293+
return true;
294+
var currentBase = type.BaseType;
295+
while (currentBase != null)
296+
{
297+
if (SymbolEqualityComparer.Default.Equals(currentBase, baseType))
298+
return true;
299+
if (currentBase.SpecialType == SpecialType.System_Object)
300+
break;
301+
currentBase = currentBase.BaseType;
302+
}
303+
return false;
304+
}
305+
}

0 commit comments

Comments
 (0)