Repository navigation
Rewrite all extension-form calls of a document in one Fix-All pass #1013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeAnalysisExtensions/ExtensionMemberRewrite.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace Funcky.Analyzers.CodeAnalysisExtensions; | ||
|
|
||
| /// <summary>An expression to replace with a call of a static extension member of a class in <c>Funcky.Extensions</c>.</summary> | ||
| public sealed record ExtensionMemberRewrite(ExpressionSyntax Original, ExpressionSyntax Replacement); |
68 changes: 60 additions & 8 deletions
68
Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeAnalysisExtensions/FunckyExtensionsImport.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,74 @@ | ||
| using System.Collections.Immutable; | ||
| using System.Globalization; | ||
| using Funcky.Analyzers.ExtensionForms; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace Funcky.Analyzers.CodeAnalysisExtensions; | ||
|
|
||
| /// <summary>Replaces an expression with one that calls a static extension member of a class in <c>Funcky.Extensions</c>, and | ||
| /// adds the <c>using</c> for that namespace when the replacement does not bind to such a member without it.</summary> | ||
| /// <summary>Replaces expressions with ones that call a static extension member of a class in <c>Funcky.Extensions</c>, and | ||
| /// adds the <c>using</c> for that namespace when a replacement does not bind to such a member without it.</summary> | ||
| internal static class FunckyExtensionsImport | ||
| { | ||
| private static readonly SyntaxAnnotation ReplacementAnnotation = new(); | ||
| private const string ReplacementAnnotationKind = "Funcky.Analyzers.FunckyExtensionsImport.Replacement"; | ||
|
|
||
| public static Document ReplaceWithExtensionMemberCall(Document document, SyntaxNode root, SemanticModel semanticModel, ExpressionSyntax original, ExpressionSyntax replacement) | ||
| => ReplaceWithExtensionMemberCalls(document, root, semanticModel, ImmutableArray.Create(new ExtensionMemberRewrite(original, replacement)), ExtensionMemberSupport.IsDeclaredInFunckyExtensions); | ||
|
|
||
| /// <summary>Applies all rewrites to the document at once. The <c>using</c> is added once per group of usings (the enclosing | ||
| /// namespace declaration or the file) that has a replacement in need of it, so that any number of rewrites work in one pass. | ||
| /// A rewrite nested in another one is skipped, the outer replacement is built from the original and would discard it anyway; | ||
| /// its diagnostic stays and is fixed by a subsequent pass. <paramref name="bindsToExtensionMember"/> tells whether the symbol a | ||
| /// replacement binds to without the <c>using</c> is the intended extension member.</summary> | ||
| public static Document ReplaceWithExtensionMemberCalls(Document document, SyntaxNode root, SemanticModel semanticModel, ImmutableArray<ExtensionMemberRewrite> rewrites, Func<ISymbol?, bool> bindsToExtensionMember) | ||
| => document.WithSyntaxRoot(WithoutReplacementAnnotations(ReplaceWithAnnotatedExtensionMemberCalls(root, semanticModel, rewrites, bindsToExtensionMember))); | ||
|
|
||
| /// <summary>Like <see cref="ReplaceWithExtensionMemberCalls"/>, but the replacements in the returned root still carry the annotations | ||
| /// that <see cref="Replacements"/> finds, so that a caller can bind them in the changed document before it strips them with | ||
| /// <see cref="WithoutReplacementAnnotations"/>. A document handed to the host must not keep them: a later fix on the same document | ||
| /// would take them for its own.</summary> | ||
| public static SyntaxNode ReplaceWithAnnotatedExtensionMemberCalls(SyntaxNode root, SemanticModel semanticModel, ImmutableArray<ExtensionMemberRewrite> rewrites, Func<ISymbol?, bool> bindsToExtensionMember) | ||
| { | ||
| var annotatedReplacement = replacement.WithAdditionalAnnotations(ReplacementAnnotation); | ||
| var rootWithReplacement = root.ReplaceNode(original, annotatedReplacement); | ||
| var originals = rewrites.Select(rewrite => rewrite.Original).ToImmutableHashSet(); | ||
| var outermostRewrites = rewrites | ||
| .Select((rewrite, index) => (Rewrite: rewrite, Index: index)) | ||
| .Where(pair => !pair.Rewrite.Original.Ancestors().Any(originals.Contains)) | ||
| .GroupBy(pair => pair.Rewrite.Original) | ||
| .ToDictionary(group => group.Key, group => group.First()); | ||
| var rootWithReplacements = root.ReplaceNodes(outermostRewrites.Keys, (original, _) => outermostRewrites[original].Rewrite.Replacement.WithAdditionalAnnotations(Annotation(outermostRewrites[original].Index))); | ||
| var replacementsInNeedOfImport = outermostRewrites.Values | ||
| .Where(pair => !bindsToExtensionMember(semanticModel.GetSpeculativeSymbolInfo(pair.Rewrite.Original.SpanStart, pair.Rewrite.Replacement, SpeculativeBindingOption.BindAsExpression).Symbol)) | ||
| .Select(pair => pair.Index) | ||
| .ToImmutableArray(); | ||
|
|
||
| return ExtensionMemberSupport.IsDeclaredInFunckyExtensions(semanticModel.GetSpeculativeSymbolInfo(original.SpanStart, replacement, SpeculativeBindingOption.BindAsExpression).Symbol) | ||
| ? document.WithSyntaxRoot(rootWithReplacement) | ||
| : document.WithSyntaxRoot(UsingDirectives.AddUsing(rootWithReplacement, rootWithReplacement.GetAnnotatedNodes(ReplacementAnnotation).Single(), ExtensionMemberSupport.ExtensionsNamespace)); | ||
| return AddUsings(rootWithReplacements, replacementsInNeedOfImport); | ||
| } | ||
|
|
||
| /// <summary>The replacements in a root produced by <see cref="ReplaceWithAnnotatedExtensionMemberCalls"/>, each with the index of the rewrite it stems from.</summary> | ||
| public static IEnumerable<(int Index, SyntaxNode Replacement)> Replacements(SyntaxNode root) | ||
| => root.GetAnnotatedNodes(ReplacementAnnotationKind).Select(node => (Index(node), node)); | ||
|
|
||
| public static SyntaxNode WithoutReplacementAnnotations(SyntaxNode root) | ||
| => root.ReplaceNodes(root.GetAnnotatedNodes(ReplacementAnnotationKind), (_, replacement) => replacement.WithoutAnnotations(ReplacementAnnotationKind)); | ||
|
|
||
| private static SyntaxNode AddUsings(SyntaxNode root, ImmutableArray<int> replacementsInNeedOfImport) | ||
| { | ||
| var anchors = Replacements(root) | ||
| .Where(replacement => replacementsInNeedOfImport.Contains(replacement.Index)) | ||
| .GroupBy(replacement => UsingContainer(replacement.Replacement)?.Span ?? default) | ||
| .Select(group => group.First().Index) | ||
| .ToImmutableArray(); | ||
|
|
||
| return anchors.Aggregate(root, (current, anchor) => UsingDirectives.AddUsing(current, Replacements(current).Single(replacement => replacement.Index == anchor).Replacement, ExtensionMemberSupport.ExtensionsNamespace)); | ||
| } | ||
|
|
||
| // Where UsingDirectives.AddUsing puts the directive: the enclosing namespace declaration with usings, or the file. | ||
| private static BaseNamespaceDeclarationSyntax? UsingContainer(SyntaxNode node) | ||
| => node.FirstAncestorOrSelf<BaseNamespaceDeclarationSyntax>(namespaceDeclaration => namespaceDeclaration.Usings.Count > 0); | ||
|
|
||
| private static SyntaxAnnotation Annotation(int index) | ||
| => new(ReplacementAnnotationKind, index.ToString(CultureInfo.InvariantCulture)); | ||
|
|
||
| private static int Index(SyntaxNode replacement) | ||
| => int.Parse(replacement.GetAnnotations(ReplacementAnnotationKind).Single().Data!, CultureInfo.InvariantCulture); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.