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 0097 + ADR 0102:
11/// <c>Cockpit/ComputingUnits/*</c> — слой вычисления снимков/DTO, без прямой добычи внешних данных и без UI-зависимостей.
12/// </summary>
13[DiagnosticAnalyzer(LanguageNames.CSharp)]
14public sealed class CockpitComputeUnitBoundaryAnalyzer : DiagnosticAnalyzer
15{
16 public const string ForbiddenExternalAccessId = "CASCOPE020";
17 public const string ForbiddenLayerDependencyId = "CASCOPE021";
18
19 private static readonly ImmutableHashSet<string> ForbiddenExternalTypeNames =
20 ImmutableHashSet.Create(
21 StringComparer.Ordinal,
22 "File",
23 "Directory",
24 "FileInfo",
25 "DirectoryInfo",
26 "Process",
27 "HttpClient",
28 "JsonDocument",
29 "JsonSerializer");
30
31 private static readonly DiagnosticDescriptor ForbiddenExternalAccessRule = new(
32 ForbiddenExternalAccessId,
33 "CCU не должен добывать внешние данные напрямую",
34 "В Cockpit/ComputingUnits запрещён прямой доступ к внешнему источнику через '{0}' (ADR 0097 + ADR 0102 DAL)",
35 "Architecture",
36 DiagnosticSeverity.Error,
37 isEnabledByDefault: true,
38 description: "CCU должен работать с подготовленными входными данными; fs/process/http/json parsing — в Data Acquisition Layer.");
39
40 private static readonly DiagnosticDescriptor ForbiddenLayerDependencyRule = new(
41 ForbiddenLayerDependencyId,
42 "CCU не должен зависеть от UI-слоёв",
43 "В Cockpit/ComputingUnits запрещён using '{0}' (CCU должен быть независим от ViewModels/Views/UI)",
44 "Architecture",
45 DiagnosticSeverity.Error,
46 isEnabledByDefault: true,
47 description: "Compute units не импортируют UI-представления и UI-фреймворки.");
48
49 public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
50 ImmutableArray.Create(ForbiddenExternalAccessRule, ForbiddenLayerDependencyRule);
51
52 public override void Initialize(AnalysisContext context)
53 {
54 context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
55 context.EnableConcurrentExecution();
56 context.RegisterSyntaxNodeAction(AnalyzeUsingDirective, SyntaxKind.UsingDirective);
57 context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
58 context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
59 }
60
61 private static void AnalyzeUsingDirective(SyntaxNodeAnalysisContext context)
62 {
63 if (!IsComputingUnitFile(context.Node.SyntaxTree.FilePath))
64 return;
65 if (context.Node is not UsingDirectiveSyntax u)
66 return;
67
68 var ns = u.Name?.ToString() ?? "";
69 if (IsForbiddenUiNamespace(ns))
70 context.ReportDiagnostic(Diagnostic.Create(ForbiddenLayerDependencyRule, u.GetLocation(), ns));
71 }
72
73 private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
74 {
75 if (!IsComputingUnitFile(context.Node.SyntaxTree.FilePath))
76 return;
77 if (context.Node is not InvocationExpressionSyntax invocation)
78 return;
79 if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
80 return;
81
82 if (memberAccess.Expression is IdentifierNameSyntax id &&
83 ForbiddenExternalTypeNames.Contains(id.Identifier.ValueText))
84 {
85 context.ReportDiagnostic(Diagnostic.Create(
86 ForbiddenExternalAccessRule,
87 memberAccess.GetLocation(),
88 id.Identifier.ValueText));
89 }
90 }
91
92 private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
93 {
94 if (!IsComputingUnitFile(context.Node.SyntaxTree.FilePath))
95 return;
96 if (context.Node is not ObjectCreationExpressionSyntax creation)
97 return;
98 var typeName = ExtractSimpleTypeName(creation.Type);
99 if (typeName is null || !ForbiddenExternalTypeNames.Contains(typeName))
100 return;
101
102 context.ReportDiagnostic(Diagnostic.Create(
103 ForbiddenExternalAccessRule,
104 creation.GetLocation(),
105 typeName));
106 }
107
108 private static string? ExtractSimpleTypeName(TypeSyntax typeSyntax) =>
109 typeSyntax switch
110 {
111 IdentifierNameSyntax i => i.Identifier.ValueText,
112 QualifiedNameSyntax q => q.Right.Identifier.ValueText,
113 GenericNameSyntax g => g.Identifier.ValueText,
114 AliasQualifiedNameSyntax a => a.Name.Identifier.ValueText,
115 _ => null
116 };
117
118 private static bool IsComputingUnitFile(string? filePath)
119 {
120 if (string.IsNullOrWhiteSpace(filePath))
121 return false;
122 var normalized = filePath.Replace('\\', '/');
123 return normalized.Contains("/Cockpit/ComputingUnits/", StringComparison.OrdinalIgnoreCase);
124 }
125
126 private static bool IsForbiddenUiNamespace(string namespaceName)
127 {
128 if (namespaceName.StartsWith("CascadeIDE.ViewModels", StringComparison.Ordinal))
129 return true;
130 if (namespaceName.StartsWith("CascadeIDE.Views", StringComparison.Ordinal))
131 return true;
132 if (namespaceName.StartsWith("CascadeIDE.Features.Ui", StringComparison.Ordinal))
133 return true;
134 if (namespaceName.StartsWith("Avalonia", StringComparison.Ordinal))
135 return true;
136 return false;
137 }
138}
139
View only · write via MCP/CIDE