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;
}
}