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; /// /// ECO002 — UnattachedBudgetExpenditure. /// /// Toute propriété ou champ décoré [Expenditure] doit être déclaré /// dans une classe qui implémente IAction<TProgram>. Une dépense /// orpheline ne peut remonter à aucun programme et donc à aucune mission /// LOLF (art. 7 de la loi organique du 1er août 2001 / OMB Circular A-11 /// section 51 pour l'équivalent fédéral US). L'analyzer rend impossible la /// compilation d'un crédit suspendu dans le vide. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class Eco002UnattachedBudgetExpenditureAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "ECO002"; private const string Title = "Dépense budgétaire non rattachée à une action LOLF"; private const string MessageFormat = "Le membre '{0}' porte [Expenditure] mais sa classe déclarante '{1}' n'implémente " + "pas IAction. Toute dépense doit appartenir à une action d'un programme " + "d'une mission (LOLF art. 7 / OMB A-11 §51)."; private const string Description = "La hiérarchie Mission → Programme → Action est le principe de spécialité " + "budgétaire. Une dépense orpheline viole la chaîne de responsabilité " + "entre gestionnaire (RPROG) et parlement."; private const string Category = "StateEconomy"; private const string HelpLink = "https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000394028/"; 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.RegisterSymbolAction(AnalyzeMember, SymbolKind.Property, SymbolKind.Field); } private static void AnalyzeMember(SymbolAnalysisContext ctx) { var member = ctx.Symbol; if (!HasExpenditureAttribute(member)) return; var containing = member.ContainingType; if (containing is null) return; if (ImplementsIAction(containing)) return; var loc = member.Locations.FirstOrDefault() ?? Location.None; var diagnostic = Diagnostic.Create(Rule, loc, member.Name, containing.Name); ctx.ReportDiagnostic(diagnostic); } private static bool HasExpenditureAttribute(ISymbol symbol) => symbol.GetAttributes().Any(a => a.AttributeClass?.Name == "ExpenditureAttribute"); private static bool ImplementsIAction(INamedTypeSymbol type) { foreach (var iface in type.AllInterfaces) { if (iface.Name == "IAction" && iface.TypeArguments.Length == 1) return true; } return false; } }