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<DiagnosticDescriptor> 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<string, string?>.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();
}
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<DiagnosticDescriptor> 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<string, string?>.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();
}