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 0161 F5: forge REST client surface (<c>Features.Forge.Infrastructure</c>, <c>Features.Forge.Lens</c>)
11/// импортируется только из vertical slice и явных spine hooks.
12/// </summary>
13[DiagnosticAnalyzer(LanguageNames.CSharp)]
14public sealed class ForgeVerticalBoundaryAnalyzer : DiagnosticAnalyzer
15{
16 public const string ForbiddenForgeImportId = "CASCOPE043";
17
18 private static readonly DiagnosticDescriptor ForbiddenForgeImportRule = new(
19 ForbiddenForgeImportId,
20 "Forge vertical imports only from allowlisted spine hooks",
21 "Файл вне Features/Forge не должен импортировать '{0}' (ADR 0161); используйте публичный hook или перенесите код в vertical slice",
22 "Architecture",
23 DiagnosticSeverity.Error,
24 isEnabledByDefault: true,
25 description: "Forge REST/Lens client namespaces belong to Features/Forge except Chat overlay merge, MCP dispatch, CRS, bracket consumers.");
26
27 private static readonly ImmutableHashSet<string> RestrictedNamespaces =
28 ImmutableHashSet.Create(
29 StringComparer.Ordinal,
30 "CascadeIDE.Features.Forge.Infrastructure",
31 "CascadeIDE.Features.Forge.Lens");
32
33 public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
34 ImmutableArray.Create(ForbiddenForgeImportRule);
35
36 public override void Initialize(AnalysisContext context)
37 {
38 context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
39 context.EnableConcurrentExecution();
40 context.RegisterSyntaxNodeAction(AnalyzeUsingDirective, SyntaxKind.UsingDirective);
41 }
42
43 private static void AnalyzeUsingDirective(SyntaxNodeAnalysisContext context)
44 {
45 if (context.Node is not UsingDirectiveSyntax u)
46 return;
47
48 var filePath = context.Node.SyntaxTree.FilePath;
49 if (IsForgeVerticalPath(filePath) || IsAllowlistedConsumerPath(filePath))
50 return;
51
52 var ns = u.Name?.ToString() ?? "";
53 if (!RestrictedNamespaces.Contains(ns))
54 return;
55
56 context.ReportDiagnostic(Diagnostic.Create(ForbiddenForgeImportRule, u.GetLocation(), ns));
57 }
58
59 private static bool IsForgeVerticalPath(string? filePath)
60 {
61 if (string.IsNullOrWhiteSpace(filePath))
62 return false;
63 var n = Normalize(filePath);
64 return n.Contains("/Features/Forge/", StringComparison.OrdinalIgnoreCase);
65 }
66
67 private static bool IsAllowlistedConsumerPath(string? filePath)
68 {
69 if (string.IsNullOrWhiteSpace(filePath))
70 return false;
71 var n = Normalize(filePath);
72 return n.Contains("/Features/Chat/", StringComparison.OrdinalIgnoreCase)
73 || n.Contains("/Features/IdeMcp/", StringComparison.OrdinalIgnoreCase)
74 || n.Contains("/Features/WorkspaceNavigation/", StringComparison.OrdinalIgnoreCase)
75 || n.Contains("/ViewModels/", StringComparison.OrdinalIgnoreCase)
76 || n.Contains("/Services/MarkdownPreview/", StringComparison.OrdinalIgnoreCase)
77 || n.Contains("/CascadeIDE.Tests/", StringComparison.OrdinalIgnoreCase);
78 }
79
80 private static string Normalize(string filePath) => filePath.Replace('\\', '/');
81}
82
View only · write via MCP/CIDE