using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; namespace XFEExtension.NetCore.AutoConfig.Diagnostics; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer { private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute"; private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute"; private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute"; private const string GeneratedProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute"; private const string ProfileBaseTypeName = "XFEExtension.NetCore.AutoConfig.XFEProfile"; private const string TargetFrameworkAttributeName = "System.Runtime.Versioning.TargetFrameworkAttribute"; public const string AddGetNoResultErrorId = "XFE0002"; public const string FieldCanUsePartialPropertyId = "XFE0003"; public const string AddSetNoSetResultWarningId = "XFW0001"; public static readonly DiagnosticDescriptor AddGetNoResultError = new( AddGetNoResultErrorId, "Get方法没有返回值", "设置了自定义的Get方法但是没有返回值:'{0}'", "XFEExtension.NetCore.AutoConfig.Diagnostics", DiagnosticSeverity.Error, true, "设置了自定义的Get方法但是没有返回值.", "https://www.xfegzs.com/Docs/View/Errors%2FAutoConfig%2FXFE0002"); public static readonly DiagnosticDescriptor AddSetNoSetResultWarning = new( AddSetNoSetResultWarningId, "Set方法没有设置值", "设置了自定义的Set方法但是没有对实际字段进行操作:'{0}'", "XFEExtension.NetCore.AutoConfig.Diagnostics", DiagnosticSeverity.Warning, true, "设置了自定义的Set方法但是没有对实际字段进行操作.", "https://www.xfegzs.com/Docs/View/Errors%2FAutoConfig%2FXFW0001"); public static readonly DiagnosticDescriptor FieldCanUsePartialProperty = new( FieldCanUsePartialPropertyId, "配置字段可升级为部分属性", "配置字段“{0}”可升级为 .NET 10 静态部分属性“{1}”,并保留现有静态调用方式", "XFEExtension.NetCore.AutoConfig.Diagnostics", DiagnosticSeverity.Info, true, "目标项目为 .NET 10 或更高版本时,可使用 C# 14 static field-backed partial property,并通过代码修复完成迁移."); public override ImmutableArray SupportedDiagnostics => [AddGetNoResultError, AddSetNoSetResultWarning, FieldCanUsePartialProperty]; public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeFieldDeclaration, SyntaxKind.FieldDeclaration); context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration); } private static void AnalyzeFieldDeclaration(SyntaxNodeAnalysisContext context) { var declaration = (FieldDeclarationSyntax)context.Node; foreach (var variable in declaration.Declaration.Variables) { if (context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) is not IFieldSymbol field) continue; var getAttributes = field.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddGetAttributeName).ToArray(); if (getAttributes.Length > 0 && !getAttributes.Select(GetCode).Any(static code => code?.Contains("return") == true)) { context.ReportDiagnostic(Diagnostic.Create(AddGetNoResultError, GetAttributeLocation(getAttributes[getAttributes.Length - 1], variable), field.Name)); } var setAttributes = field.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddSetAttributeName).ToArray(); if (setAttributes.Length == 0) { ReportPartialPropertySuggestion(context, declaration, variable, field); continue; } var assignmentPattern = $@"\b{Regex.Escape(field.Name)}\s*=\s*value\b"; if (!setAttributes.Select(GetCode).Any(code => code is not null && Regex.IsMatch(code, assignmentPattern))) context.ReportDiagnostic(Diagnostic.Create(AddSetNoSetResultWarning, GetAttributeLocation(setAttributes[setAttributes.Length - 1], variable), field.Name)); ReportPartialPropertySuggestion(context, declaration, variable, field); } } private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context) { var declaration = (PropertyDeclarationSyntax)context.Node; if (context.SemanticModel.GetDeclaredSymbol(declaration, context.CancellationToken) is not IPropertySymbol property || !property.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName)) return; var getAttributes = property.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddGetAttributeName).ToArray(); if (getAttributes.Length > 0 && !getAttributes.Select(GetCode).Any(static code => code?.Contains("return") == true)) context.ReportDiagnostic(Diagnostic.Create(AddGetNoResultError, GetAttributeLocation(getAttributes[getAttributes.Length - 1], declaration), property.Name)); var setAttributes = property.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddSetAttributeName).ToArray(); if (setAttributes.Length == 0) return; var assignmentPattern = @"\bfield\s*=\s*value\b"; if (!setAttributes.Select(GetCode).Any(code => code is not null && Regex.IsMatch(code, assignmentPattern))) context.ReportDiagnostic(Diagnostic.Create(AddSetNoSetResultWarning, GetAttributeLocation(setAttributes[setAttributes.Length - 1], declaration), property.Name)); } private static void ReportPartialPropertySuggestion(SyntaxNodeAnalysisContext context, FieldDeclarationSyntax declaration, VariableDeclaratorSyntax variable, IFieldSymbol field) { if (declaration.Declaration.Variables.Count != 1 || field.IsStatic || field.IsConst || field.IsReadOnly || field.IsFixedSizeBuffer || field.Type is IPointerTypeSymbol || field.Type is IFunctionPointerTypeSymbol || declaration.Modifiers.Any(SyntaxKind.VolatileKeyword) || declaration.Modifiers.Any(SyntaxKind.UnsafeKeyword) || !field.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName) || !DerivesFromXfeProfile(field.ContainingType) || !SupportsNet10PartialProperties(context)) return; var propertyName = GetGeneratedPropertyName(field); if (string.IsNullOrWhiteSpace(propertyName) || !SyntaxFacts.IsValidIdentifier(propertyName) || SyntaxFacts.GetKeywordKind(propertyName) != SyntaxKind.None || SyntaxFacts.GetContextualKeywordKind(propertyName) != SyntaxKind.None) return; if (HasMemberConflict(field, propertyName)) return; var properties = ImmutableDictionary.Empty .Add("PropertyName", propertyName); context.ReportDiagnostic(Diagnostic.Create(FieldCanUsePartialProperty, variable.Identifier.GetLocation(), properties, field.Name, propertyName)); } private static bool HasMemberConflict(IFieldSymbol field, string memberName) => field.ContainingType .GetMembers(memberName) .Any(member => !SymbolEqualityComparer.Default.Equals(member, field) && !member.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == GeneratedProfilePropertyAttributeName)); private static bool SupportsNet10PartialProperties(SyntaxNodeAnalysisContext context) { if (context.Node.SyntaxTree.Options is not CSharpParseOptions parseOptions || parseOptions.LanguageVersion < LanguageVersion.CSharp14) return false; if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.TargetFramework", out var targetFramework)) return IsNet10OrGreater(targetFramework); var targetFrameworkAttribute = context.Compilation.Assembly.GetAttributes().FirstOrDefault(static attribute => attribute.AttributeClass?.ToDisplayString() == TargetFrameworkAttributeName); return targetFrameworkAttribute is not null && targetFrameworkAttribute.ConstructorArguments.Length > 0 && targetFrameworkAttribute.ConstructorArguments[0].Value is string frameworkName && IsNet10OrGreater(frameworkName); } private static bool IsNet10OrGreater(string targetFramework) { if (targetFramework.StartsWith("net", StringComparison.OrdinalIgnoreCase) && !targetFramework.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && !targetFramework.StartsWith("netcoreapp", StringComparison.OrdinalIgnoreCase)) { var versionText = targetFramework.Substring(3).Split('-')[0]; return versionText.Contains(".") && Version.TryParse(versionText, out var version) && version.Major >= 10; } const string corePrefix = ".NETCoreApp,Version=v"; const string netPrefix = ".NET,Version=v"; var prefix = targetFramework.StartsWith(corePrefix, StringComparison.OrdinalIgnoreCase) ? corePrefix : targetFramework.StartsWith(netPrefix, StringComparison.OrdinalIgnoreCase) ? netPrefix : null; return prefix is not null && Version.TryParse(targetFramework.Substring(prefix.Length), out var frameworkVersion) && frameworkVersion.Major >= 10; } private static string GetGeneratedPropertyName(IFieldSymbol field) { var attribute = field.GetAttributes().FirstOrDefault(static candidate => candidate.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName); var explicitName = attribute?.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null; if (string.IsNullOrWhiteSpace(explicitName) && attribute is not null) { foreach (var namedArgument in attribute.NamedArguments) if (namedArgument.Key == "PropertyName" && namedArgument.Value.Value is string namedValue) explicitName = namedValue; } if (!string.IsNullOrWhiteSpace(explicitName)) return explicitName!; var fieldName = field.Name.StartsWith("_", StringComparison.Ordinal) ? field.Name.Substring(1) : field.Name; return fieldName.Length == 0 ? fieldName : char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1); } private static bool DerivesFromXfeProfile(INamedTypeSymbol? type) { for (var current = type?.BaseType; current is not null; current = current.BaseType) if (current.ToDisplayString() == ProfileBaseTypeName) return true; return false; } private static string? GetCode(AttributeData attribute) => attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null; private static Location GetAttributeLocation(AttributeData attribute, VariableDeclaratorSyntax fallback) => attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? fallback.GetLocation(); private static Location GetAttributeLocation(AttributeData attribute, PropertyDeclarationSyntax fallback) => attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? fallback.GetLocation(); }