using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using XFEExtension.NetCore.AutoConfig.Diagnostics; namespace XFEExtension.NetCore.AutoConfig.CodeFix; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoConfigCodeFixProvider)), Shared] public sealed class AutoConfigCodeFixProvider : CodeFixProvider { private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute"; private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute"; private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute"; public override ImmutableArray FixableDiagnosticIds => [AutoConfigDiagnostics.AddGetNoResultErrorId, AutoConfigDiagnostics.AddSetNoSetResultWarningId, AutoConfigDiagnostics.FieldCanUsePartialPropertyId]; public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public override Task RegisterCodeFixesAsync(CodeFixContext context) { foreach (var diagnostic in context.Diagnostics) { if (diagnostic.Id == AutoConfigDiagnostics.AddGetNoResultErrorId) { context.RegisterCodeFix( CodeAction.Create("添加返回值方法", cancellationToken => AddAttributeAsync(context.Document, diagnostic.Location.SourceSpan, "ProfilePropertyAddGet", "return Current.{0}", cancellationToken), "添加返回值"), diagnostic); } else if (diagnostic.Id == AutoConfigDiagnostics.AddSetNoSetResultWarningId) { context.RegisterCodeFix( CodeAction.Create("添加字段的设置方法", cancellationToken => AddAttributeAsync(context.Document, diagnostic.Location.SourceSpan, "ProfilePropertyAddSet", "Current.{0} = value", cancellationToken), "添加字段的设置方法"), diagnostic); } else if (diagnostic.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId && diagnostic.Properties.TryGetValue("PropertyName", out var propertyName) && !string.IsNullOrWhiteSpace(propertyName)) { context.RegisterCodeFix( CodeAction.Create( "转换为 .NET 10 部分配置属性", cancellationToken => ConvertToPartialPropertyAsync(context.Document, diagnostic.Location.SourceSpan, propertyName!, cancellationToken), "转换为部分配置属性"), diagnostic); } } return Task.CompletedTask; } private static async Task AddAttributeAsync(Document document, TextSpan sourceSpan, string attributeName, string codeFormat, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root is null) return document; var diagnosticNode = root.FindToken(sourceSpan.Start).Parent; var fieldDeclaration = diagnosticNode?.AncestorsAndSelf().OfType().FirstOrDefault(); var propertyDeclaration = diagnosticNode?.AncestorsAndSelf().OfType().FirstOrDefault(); if (fieldDeclaration is null && propertyDeclaration is null) return document; var code = propertyDeclaration is null ? string.Format(System.Globalization.CultureInfo.InvariantCulture, codeFormat, fieldDeclaration!.Declaration.Variables.First().Identifier.ValueText) : attributeName == AddGetAttributeName || attributeName == "ProfilePropertyAddGet" ? "return field" : "field = value"; var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(attributeName)) .AddArgumentListArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(code)))); var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute)); var newRoot = fieldDeclaration is not null ? root.ReplaceNode(fieldDeclaration, fieldDeclaration.AddAttributeLists(attributeList)) : root.ReplaceNode(propertyDeclaration!, propertyDeclaration!.AddAttributeLists(attributeList)); return document.WithSyntaxRoot(newRoot); } private static async Task ConvertToPartialPropertyAsync(Document document, TextSpan sourceSpan, string propertyName, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (root is null || semanticModel is null) return document.Project.Solution; var variable = root.FindToken(sourceSpan.Start).Parent?.AncestorsAndSelf().OfType().FirstOrDefault(); var fieldDeclaration = variable?.Parent?.Parent as FieldDeclarationSyntax; if (variable is null || fieldDeclaration is null || fieldDeclaration.Declaration.Variables.Count != 1 || semanticModel.GetDeclaredSymbol(variable, cancellationToken) is not IFieldSymbol field) return document.Project.Solution; var originalFieldName = field.Name; var profileAttributeNames = new HashSet(StringComparer.Ordinal); var getHookAttributeNames = new HashSet(StringComparer.Ordinal); var setHookAttributeNames = new HashSet(StringComparer.Ordinal); foreach (var attribute in fieldDeclaration.AttributeLists.SelectMany(static list => list.Attributes)) { var attributeTypeName = (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol as IMethodSymbol)?.ContainingType.ToDisplayString(); if (attributeTypeName != ProfilePropertyAttributeName && attributeTypeName != AddGetAttributeName && attributeTypeName != AddSetAttributeName) continue; var sourceName = attribute.Name.ToString(); profileAttributeNames.Add(sourceName); if (attributeTypeName == AddGetAttributeName) getHookAttributeNames.Add(sourceName); else if (attributeTypeName == AddSetAttributeName) setHookAttributeNames.Add(sourceName); } var solution = document.Project.Solution; var declarationAnnotation = new SyntaxAnnotation(); solution = solution.WithDocumentSyntaxRoot( document.Id, root.ReplaceNode(fieldDeclaration, fieldDeclaration.WithAdditionalAnnotations(declarationAnnotation))); var referencedSymbols = await SymbolFinder.FindReferencesAsync(field, solution, cancellationToken).ConfigureAwait(false); var referenceLocations = referencedSymbols .SelectMany(static referencedSymbol => referencedSymbol.Locations) .Where(static location => location.Location.IsInSource) .GroupBy(static location => location.Document.Id); foreach (var documentLocations in referenceLocations) { var referenceDocument = solution.GetDocument(documentLocations.Key); var referenceRoot = referenceDocument is null ? null : await referenceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (referenceDocument is null || referenceRoot is null) continue; var tokens = documentLocations .Select(location => referenceRoot.FindToken(location.Location.SourceSpan.Start)) .Where(token => token.IsKind(SyntaxKind.IdentifierToken) && token.ValueText == originalFieldName) .Distinct() .ToArray(); if (tokens.Length == 0) continue; var renamedRoot = referenceRoot.ReplaceTokens(tokens, (_, rewritten) => SyntaxFactory.Identifier(rewritten.LeadingTrivia, propertyName, rewritten.TrailingTrivia)); solution = solution.WithDocumentSyntaxRoot(referenceDocument.Id, renamedRoot); } var declarationDocument = solution.GetDocument(document.Id); var declarationRoot = declarationDocument is null ? null : await declarationDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (declarationDocument is null || declarationRoot is null) return solution; var currentFieldDeclaration = declarationRoot.GetAnnotatedNodes(declarationAnnotation) .OfType() .Where(static candidate => candidate.Declaration.Variables.Count == 1) .FirstOrDefault(candidate => candidate.Declaration.Variables[0].Identifier.ValueText == originalFieldName); if (currentFieldDeclaration is null) return solution; var currentVariable = currentFieldDeclaration.Declaration.Variables[0]; var attributeLists = PreparePartialPropertyAttributes( currentFieldDeclaration.AttributeLists, originalFieldName, "field", profileAttributeNames, getHookAttributeNames, setHookAttributeNames); var partialProperty = SyntaxFactory.PropertyDeclaration(currentFieldDeclaration.Declaration.Type.WithoutTrivia(), SyntaxFactory.Identifier(propertyName)) .WithAttributeLists(attributeLists) .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword))) .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List([ SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) ]))) .WithInitializer(currentVariable.Initializer) .WithSemicolonToken(currentVariable.Initializer is null ? default : SyntaxFactory.Token(SyntaxKind.SemicolonToken)) .WithLeadingTrivia(currentFieldDeclaration.GetLeadingTrivia()) .WithTrailingTrivia(currentFieldDeclaration.GetTrailingTrivia()) .WithAdditionalAnnotations(Formatter.Annotation); var updatedRoot = declarationRoot.ReplaceNode(currentFieldDeclaration, partialProperty); return solution.WithDocumentSyntaxRoot(declarationDocument.Id, updatedRoot); } private static SyntaxList RewriteHookAttributeStrings( SyntaxList attributeLists, string fieldName, string backingFieldName, HashSet getHookAttributeNames, HashSet setHookAttributeNames) { return SyntaxFactory.List(attributeLists.Select(attributeList => attributeList.ReplaceNodes( attributeList.Attributes, (_, attribute) => { var attributeName = attribute.Name.ToString(); var isGetHook = getHookAttributeNames.Contains(attributeName); if (!isGetHook && !setHookAttributeNames.Contains(attributeName)) return attribute; var arguments = attribute.ArgumentList?.Arguments; if (arguments is not { Count: > 0 } || arguments.Value[0].Expression is not LiteralExpressionSyntax literal || !literal.IsKind(SyntaxKind.StringLiteralExpression)) return attribute; var rewrittenCode = literal.Token.ValueText; var qualifiedFieldPattern = $@"(?:(?:\b@?[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*)(?:Current|this)\s*\.\s*{Regex.Escape(fieldName)}\b"; rewrittenCode = Regex.Replace(rewrittenCode, qualifiedFieldPattern, backingFieldName); if (isGetHook || fieldName != "value") { rewrittenCode = Regex.Replace(rewrittenCode, $@"\b{Regex.Escape(fieldName)}\b", backingFieldName); } var rewrittenLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(rewrittenCode)); return attribute.ReplaceNode(literal, rewrittenLiteral); }))); } private static SyntaxList PreparePartialPropertyAttributes( SyntaxList attributeLists, string fieldName, string backingFieldName, HashSet profileAttributeNames, HashSet getHookAttributeNames, HashSet setHookAttributeNames) { var rewrittenLists = RewriteHookAttributeStrings(attributeLists, fieldName, backingFieldName, getHookAttributeNames, setHookAttributeNames); var result = new List(); foreach (var attributeList in rewrittenLists) { var propertyAttributes = attributeList.Attributes.Where(attribute => profileAttributeNames.Contains(attribute.Name.ToString())).ToArray(); var backingFieldAttributes = attributeList.Attributes.Where(attribute => !profileAttributeNames.Contains(attribute.Name.ToString())).ToArray(); if (propertyAttributes.Length > 0) { result.Add(attributeList .WithTarget(null) .WithAttributes(SyntaxFactory.SeparatedList(propertyAttributes))); } if (backingFieldAttributes.Length > 0) { result.Add(attributeList .WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.FieldKeyword)) .WithColonToken(SyntaxFactory.Token(SyntaxKind.ColonToken))) .WithAttributes(SyntaxFactory.SeparatedList(backingFieldAttributes)) .WithLeadingTrivia(propertyAttributes.Length == 0 ? attributeList.GetLeadingTrivia() : default)); } } return SyntaxFactory.List(result); } }