using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace FrenchExDev.Net.State.Economy.Dsl.Analyzers; /// /// ECO001 — DeficitAboveMaastricht. /// /// Émet un diagnostic quand une classe /// décorée [LoiDeFinances] expose une propriété retournant /// DeficitRatio dont la valeur littérale dépasse le seuil Maastricht /// (3% du PIB, art. 126 TFUE + protocole n°12) sans attribut de justification. /// /// Attributs acceptés comme justification : /// /// [Article126TfeuJustification(CouncilRecommendation, CorrectiveDeadline)] /// [ExcessiveDeficitProcedure(Opened: true, OpenedDate)] /// /// /// Le diagnostic est voulu compile-time : un commentaire en prose /// ne suffit pas, seul l'attribut compte. Principe « zéro string-as-pivot » /// (part 6 de la série metacratie-compilateur). /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class Eco001DeficitAboveMaastrichtAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "ECO001"; private const string Title = "Déficit projeté au-delà du seuil Maastricht"; private const string MessageFormat = "Déficit projeté {0:P2} > seuil Maastricht 3% sans [Article126TfeuJustification] ni " + "[ExcessiveDeficitProcedure(Opened: true)]. Joignez une justification ou ramenez " + "le ratio sous le seuil."; private const string Description = "SEC 2010 / art. 126 TFUE + protocole n°12 : le déficit des administrations " + "publiques (S13) ne doit pas dépasser 3% du PIB sauf justification formelle. " + "Voir procédure pour déficit excessif (PDE)."; private const string Category = "StateEconomy"; private const string HelpLink = "https://eur-lex.europa.eu/legal-content/FR/TXT/?uri=CELEX:12016E126"; private static readonly DiagnosticDescriptor Rule = new( id: DiagnosticId, title: Title, messageFormat: MessageFormat, category: Category, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description, helpLinkUri: HelpLink); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration); } private static void AnalyzeClass(SyntaxNodeAnalysisContext ctx) { var classDecl = (ClassDeclarationSyntax)ctx.Node; var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl, ctx.CancellationToken); if (symbol is null) return; if (!IsBudgetAct(symbol)) return; if (HasJustification(symbol)) return; foreach (var prop in classDecl.Members.OfType()) { if (!TryGetLiteralDeficit(ctx, prop, out var value)) continue; if (value <= DeficitThreshold) continue; var diagnostic = Diagnostic.Create(Rule, prop.GetLocation(), value); ctx.ReportDiagnostic(diagnostic); } } private const decimal DeficitThreshold = 0.03m; private static bool IsBudgetAct(INamedTypeSymbol symbol) => symbol.GetAttributes().Any(a => a.AttributeClass?.Name == "LoiDeFinancesAttribute"); private static bool HasJustification(INamedTypeSymbol symbol) { foreach (var attr in symbol.GetAttributes()) { var name = attr.AttributeClass?.Name; if (name == "Article126TfeuJustificationAttribute") return true; if (name == "ExcessiveDeficitProcedureAttribute" && attr.ConstructorArguments.Length >= 1 && attr.ConstructorArguments[0].Value is bool opened && opened) { return true; } } return false; } private static bool TryGetLiteralDeficit( SyntaxNodeAnalysisContext ctx, PropertyDeclarationSyntax prop, out decimal value) { value = 0m; var propSymbol = ctx.SemanticModel.GetDeclaredSymbol(prop, ctx.CancellationToken); if (propSymbol is null) return false; if (propSymbol.Type.Name != "DeficitRatio") return false; // Supporte: public DeficitRatio X => DeficitRatio.Of(0.048m); ExpressionSyntax? expr = prop.ExpressionBody?.Expression; if (expr is null) return false; if (expr is not InvocationExpressionSyntax invocation) return false; if (invocation.ArgumentList.Arguments.Count != 1) return false; var arg = invocation.ArgumentList.Arguments[0].Expression; if (arg is not LiteralExpressionSyntax literal) return false; if (!literal.IsKind(SyntaxKind.NumericLiteralExpression)) return false; if (literal.Token.Value is decimal d) { value = d; return true; } return false; } }