Forge
csharpdeeb25a2
1using System.Collections.Immutable;
2using Microsoft.CodeAnalysis;
3using Microsoft.CodeAnalysis.CSharp;
4using Microsoft.CodeAnalysis.CSharp.Syntax;
5using Microsoft.CodeAnalysis.Diagnostics;
6
7namespace CascadeIDE.ArchitectureAnalyzers;
8
9/// <summary>
10/// ADR 0079: <c>IdeDisplay/</c> — intent/композитор/снимок без Avalonia, без кабины и без хрома <c>UiChrome</c>.
11/// </summary>
12[DiagnosticAnalyzer(LanguageNames.CSharp)]
13public sealed class IdeDisplayLayerArchitectureAnalyzer : DiagnosticAnalyzer
14{
15 public const string CockpitNamespaceId = "CASCOPE013";
16 public const string AvaloniaNamespaceId = "CASCOPE014";
17 public const string FeaturesUiChromeId = "CASCOPE015";
18
19 private static readonly DiagnosticDescriptor CockpitRule = new(
20 CockpitNamespaceId,
21 "IdeDisplay не должен ссылаться на CascadeIDE.Cockpit",
22 "В IdeDisplay (ADR 0079) запрещён импорт из кабины (найдено: {0})",
23 "Architecture",
24 DiagnosticSeverity.Error,
25 isEnabledByDefault: true,
26 description: "Композиторы и снимки оверлеев IDE не зависят от канала/CDS/примитивов кабины.");
27
28 private static readonly DiagnosticDescriptor AvaloniaRule = new(
29 AvaloniaNamespaceId,
30 "IdeDisplay не должен ссылаться на Avalonia",
31 "В IdeDisplay (ADR 0079) запрещены типы Avalonia UI (найдено: {0})",
32 "Architecture",
33 DiagnosticSeverity.Error,
34 isEnabledByDefault: true,
35 description: "Снимок и композитор IDS остаются переносимыми и тестируемыми без UI-фреймворка.");
36
37 private static readonly DiagnosticDescriptor FeaturesUiChromeRule = new(
38 FeaturesUiChromeId,
39 "IdeDisplay не должен импортировать CascadeIDE.Features.UiChrome",
40 "В IdeDisplay запрещён using CascadeIDE.Features.UiChrome (ADR 0079, хром вне IDS)",
41 "Architecture",
42 DiagnosticSeverity.Error,
43 isEnabledByDefault: true,
44 description: "Семантика оверлея не смешивается с контролами зон IDE.");
45
46 public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
47 ImmutableArray.Create(CockpitRule, AvaloniaRule, FeaturesUiChromeRule);
48
49 public override void Initialize(AnalysisContext context)
50 {
51 context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
52 context.EnableConcurrentExecution();
53 context.RegisterSymbolAction(AnalyzeNamedTypeSymbol, SymbolKind.NamedType);
54 context.RegisterSyntaxNodeAction(AnalyzeUsingDirective, SyntaxKind.UsingDirective);
55 }
56
57 private static void AnalyzeUsingDirective(SyntaxNodeAnalysisContext context)
58 {
59 if (context.Node is not UsingDirectiveSyntax u)
60 return;
61 if (!IsIdeDisplayFilePath(context.Node.SyntaxTree.FilePath))
62 return;
63
64 var name = u.Name?.ToString() ?? "";
65 if (name.StartsWith("CascadeIDE.Cockpit", StringComparison.Ordinal) || name == "CascadeIDE.Cockpit")
66 {
67 context.ReportDiagnostic(Diagnostic.Create(CockpitRule, u.GetLocation(), name));
68 return;
69 }
70
71 if (name.StartsWith("Avalonia", StringComparison.Ordinal) || name == "Avalonia")
72 {
73 context.ReportDiagnostic(Diagnostic.Create(AvaloniaRule, u.GetLocation(), name));
74 return;
75 }
76
77 if (name.StartsWith("CascadeIDE.Features.UiChrome", StringComparison.Ordinal)
78 || name == "CascadeIDE.Features.UiChrome")
79 {
80 context.ReportDiagnostic(Diagnostic.Create(FeaturesUiChromeRule, u.GetLocation()));
81 }
82 }
83
84 private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context)
85 {
86 if (context.Symbol is not INamedTypeSymbol named)
87 return;
88 if (named.TypeKind == TypeKind.Enum)
89 return;
90 if (!IsIdeDisplayNamespace(named.ContainingNamespace))
91 return;
92
93 foreach (var iface in named.AllInterfaces)
94 CheckType(context, named, iface, named.DeclaringSyntaxReferences);
95
96 if (named.BaseType is { SpecialType: not SpecialType.System_Object })
97 CheckType(context, named, named.BaseType, named.DeclaringSyntaxReferences);
98
99 foreach (var member in named.GetMembers())
100 {
101 switch (member)
102 {
103 case IFieldSymbol f when !f.IsImplicitlyDeclared:
104 CheckType(context, named, f.Type, member.DeclaringSyntaxReferences);
105 break;
106 case IPropertySymbol p when !p.IsImplicitlyDeclared:
107 CheckType(context, named, p.Type, member.DeclaringSyntaxReferences);
108 break;
109 case IMethodSymbol m when m is { MethodKind: MethodKind.Ordinary, IsImplicitlyDeclared: false }:
110 CheckType(context, named, m.ReturnType, member.DeclaringSyntaxReferences);
111 foreach (var par in m.Parameters)
112 CheckType(context, named, par.Type, member.DeclaringSyntaxReferences);
113 break;
114 }
115 }
116 }
117
118 private static void CheckType(
119 SymbolAnalysisContext context,
120 INamedTypeSymbol declaring,
121 ITypeSymbol type,
122 ImmutableArray<SyntaxReference> fallbackLocations)
123 {
124 VisitTypesForAvaloniaBan(type, context, declaring, fallbackLocations);
125 }
126
127 private static void VisitTypesForAvaloniaBan(
128 ITypeSymbol type,
129 SymbolAnalysisContext context,
130 INamedTypeSymbol declaring,
131 ImmutableArray<SyntaxReference> fallbackLocations)
132 {
133 switch (type)
134 {
135 case IArrayTypeSymbol arr:
136 VisitTypesForAvaloniaBan(arr.ElementType, context, declaring, fallbackLocations);
137 return;
138 case ITypeParameterSymbol:
139 return;
140 case INamedTypeSymbol named:
141 if (named.IsGenericType)
142 {
143 foreach (var arg in named.TypeArguments)
144 VisitTypesForAvaloniaBan(arg, context, declaring, fallbackLocations);
145 }
146
147 if (IsCockpitFamily(named))
148 {
149 Report(context, CockpitRule, fallbackLocations, named.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
150 return;
151 }
152
153 if (IsFeaturesUiChromeFamily(named))
154 {
155 Report(context, FeaturesUiChromeRule, fallbackLocations);
156 return;
157 }
158
159 if (IsAvaloniaUiFamily(named))
160 {
161 Report(
162 context,
163 AvaloniaRule,
164 fallbackLocations,
165 named.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
166 }
167
168 return;
169 }
170 }
171
172 private static void Report(
173 SymbolAnalysisContext context,
174 DiagnosticDescriptor rule,
175 ImmutableArray<SyntaxReference> fallbackLocations,
176 string? formatArg = null)
177 {
178 var location = PickLocation(fallbackLocations);
179 if (location is null)
180 return;
181
182 var diagnostic = formatArg is null
183 ? Diagnostic.Create(rule, location)
184 : Diagnostic.Create(rule, location, formatArg);
185 context.ReportDiagnostic(diagnostic);
186 }
187
188 private static Location? PickLocation(ImmutableArray<SyntaxReference> refs)
189 {
190 foreach (var r in refs)
191 {
192 var syntax = r.GetSyntax();
193 if (syntax is null)
194 continue;
195 var loc = syntax.GetLocation();
196 if (loc.IsInSource)
197 return loc;
198 }
199
200 return null;
201 }
202
203 private static bool IsIdeDisplayNamespace(INamespaceSymbol? ns)
204 {
205 if (ns is null)
206 return false;
207 var display = ns.ToDisplayString();
208 return display.StartsWith("CascadeIDE.IdeDisplay", StringComparison.Ordinal);
209 }
210
211 private static bool IsIdeDisplayFilePath(string? filePath)
212 {
213 if (string.IsNullOrEmpty(filePath))
214 return false;
215 var n = filePath.Replace('\\', '/');
216 return n.Contains("/IdeDisplay/", StringComparison.OrdinalIgnoreCase);
217 }
218
219 private static bool IsAvaloniaUiFamily(ITypeSymbol type)
220 {
221 if (type is not INamedTypeSymbol named)
222 return false;
223
224 for (var ns = type.ContainingNamespace; ns != null && !ns.IsGlobalNamespace; ns = ns.ContainingNamespace)
225 {
226 if (ns.Name == "Avalonia")
227 return true;
228 }
229
230 var asm = named.ContainingAssembly?.Name ?? "";
231 if (asm.Equals("Avalonia", StringComparison.OrdinalIgnoreCase) || asm.StartsWith("Avalonia.", StringComparison.OrdinalIgnoreCase))
232 return true;
233 if (asm.StartsWith("Dock.Avalonia", StringComparison.OrdinalIgnoreCase))
234 return true;
235
236 return false;
237 }
238
239 private static bool IsCockpitFamily(INamedTypeSymbol named)
240 {
241 var display = named.ContainingNamespace?.ToDisplayString() ?? "";
242 return display.StartsWith("CascadeIDE.Cockpit", StringComparison.Ordinal);
243 }
244
245 private static bool IsFeaturesUiChromeFamily(INamedTypeSymbol named)
246 {
247 var display = named.ContainingNamespace?.ToDisplayString() ?? "";
248 return display.StartsWith("CascadeIDE.Features.UiChrome", StringComparison.Ordinal);
249 }
250}
251
View only · write via MCP/CIDE