Skip to content

Commit

Permalink
Added Roslyn Refactorings
Browse files Browse the repository at this point in the history
  • Loading branch information
pamidur committed Jun 17, 2020
1 parent 116ff4d commit 5780c17
Show file tree
Hide file tree
Showing 10 changed files with 546 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
|| method.Parameters.Length != 1
|| method.Parameters[0].Type.ToDisplayString() != WellKnown.Type)
{
context.ReportDiagnostic(Diagnostic.Create(AspectRules.AspectFactoryMustContainFactoryMethod.AsDescriptor(), location, symbol.Name));
context.ReportDiagnostic(Diagnostic.Create(AspectRules.AspectFactoryMustContainFactoryMethod.AsDescriptor(), location, named.Name));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Threading.Tasks;

namespace AspectInjector.Analyzer.Refactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AdviceAttributeCodeRefactoringProvider))]
public class AdviceAttributeCodeRefactoringProvider : AdviceCodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span);

var attrSyntax = (node as AttributeSyntax) ?? node.Parent as AttributeSyntax;

if (attrSyntax != null)
{
var attr = semanticModel.GetSymbolInfo(attrSyntax, context.CancellationToken);
if (attr.Symbol?.ContainingSymbol is INamedTypeSymbol named && named.ToDisplayString() == WellKnown.AdviceType)
{
var method = attrSyntax.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault();
if (method != null)
await RegisterRefactorings(context, method);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using AspectInjector.Broker;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AspectInjector.Analyzer.Refactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AdviceCodeRefactoringProvider))]
public class AdviceCodeRefactoringProvider : CodeRefactoringProvider
{
private static readonly ImmutableArray<ParameterSample> _shared = new List<ParameterSample>
{
Samples.Parameters.Arguments,
Samples.Parameters.Instance,
Samples.Parameters.Name,
Samples.Parameters.Type,
Samples.Parameters.Metadata,
Samples.Parameters.ReturnType,
Samples.Parameters.Triggers,
}.ToImmutableArray();

private static readonly ImmutableArray<ParameterSample> _after = _shared.AddRange(new List<ParameterSample>
{
Samples.Parameters.ReturnValue,
});

private static readonly ImmutableArray<ParameterSample> _around = _shared.AddRange(new List<ParameterSample>
{
Samples.Parameters.Target,
});

public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

var node = root.FindNode(context.Span);
var method = node as MethodDeclarationSyntax;
if (method != null)
{
await RegisterRefactorings(context, method);
}
}

protected async Task RegisterRefactorings(CodeRefactoringContext context, MethodDeclarationSyntax method)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var methodSymbol = semanticModel.GetDeclaredSymbol(method);
var attr = methodSymbol.GetAdviceAttribute();

if (attr == null)
return;

var kindArg = attr.ConstructorArguments.FirstOrDefault();

if (kindArg.IsNull)
return;

var kind = (Kind)kindArg.Value;

var list = _shared;
if (kind == Kind.After)
list = _after;
if (kind == Kind.Around)
list = _around;

var currentAttrs = methodSymbol.Parameters.SelectMany(p => p.GetAttributes()).ToImmutableArray();

foreach (var entry in list)
{
if (HasParameter(currentAttrs, entry.Source))
continue;

context.RegisterRefactoring(CodeAction.Create(
$"Add '{entry.Source}' parameter to this advice.",
ct => AddParameter(context, method, entry, ct)
));
}
}

private async Task<Document> AddParameter(CodeRefactoringContext context, MethodDeclarationSyntax method, ParameterSample parameter, CancellationToken ct)
{
var root = await context.Document.GetSyntaxRootAsync(ct).ConfigureAwait(false);
var newMethod = method.WithParameterList(method.ParameterList.AddParameters(parameter.ParameterSyntax)/*.WithAdditionalAnnotations(Formatter.Annotation)*/);

root = root.ReplaceNode(method, newMethod);
root = root.WithUpdatedUsings(new[] { parameter.UsingSyntax });

return context.Document.WithSyntaxRoot(root);
}

private bool HasParameter(ImmutableArray<AttributeData> attributes, string source)
{
return attributes.Any(a => a.ConstructorArguments.Any(ca => ((Source)ca.Value).ToString() == source));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Threading.Tasks;

namespace AspectInjector.Analyzer.Refactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AspectAttributeCodeRefactoringProvider))]
public class AspectAttributeCodeRefactoringProvider : AspectCodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var root = await context.Document
.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);
var node = root.FindNode(context.Span);

var attrSyntax = (node as AttributeSyntax) ?? node.Parent as AttributeSyntax;


if (attrSyntax != null)
{
var attr = semanticModel.GetSymbolInfo(attrSyntax, context.CancellationToken);
if (attr.Symbol?.ContainingSymbol is INamedTypeSymbol named && named.ToDisplayString() == WellKnown.AspectType)
{
var @class = attrSyntax.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (@class != null)
RegisterRefactoring(context, @class);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Threading;
using System.Threading.Tasks;

namespace AspectInjector.Analyzer.Refactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AspectCodeRefactoringProvider))]
public class AspectCodeRefactoringProvider : CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var root = await context.Document
.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);
var node = root.FindNode(context.Span);
var @class = node as ClassDeclarationSyntax;
if (@class == null)
return;

var attr = semanticModel.GetDeclaredSymbol(@class).GetAspectAttribute();

if (attr == null)
return;

RegisterRefactoring(context, @class);
}

protected void RegisterRefactoring(CodeRefactoringContext context, ClassDeclarationSyntax @class)
{
context.RegisterRefactoring(CodeAction.Create(
"Add 'Before' advice to this aspect.",
ct =>
CreateAdvice(context, @class, Samples.Advices.Before, ct)));
context.RegisterRefactoring(CodeAction.Create(
"Add 'After' advice to this aspect.",
ct =>
CreateAdvice(context, @class, Samples.Advices.After, ct)));
context.RegisterRefactoring(CodeAction.Create(
"Add 'Around' advice to this aspect.",
ct =>
CreateAdvice(context, @class, Samples.Advices.Around, ct)));
}

private async Task<Document> CreateAdvice(CodeRefactoringContext context, ClassDeclarationSyntax @class, (MethodDeclarationSyntax Method, UsingDirectiveSyntax[] Usings) advice, CancellationToken cancellationToken)
{
var root = await context.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

var newClass = @class.AddMembers(advice.Method);

root = root.ReplaceNode(@class, newClass);
root = root.WithUpdatedUsings(advice.Usings);

return context.Document.WithSyntaxRoot(root);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Formatting;
using AspectInjector.Broker;

namespace AspectInjector.Analyzer.Refactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(PossibleMixinCodeRefactoringProvider))]
public class PossibleMixinCodeRefactoringProvider : CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span);
var @base = node as BaseTypeSyntax;
if (@base != null)
{
var baseType = semanticModel.GetSymbolInfo(@base.Type).Symbol as ITypeSymbol;
if (baseType != null && baseType.TypeKind == TypeKind.Interface)
{
var @class = @base.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (@class != null)
{
var classSymbol = semanticModel.GetDeclaredSymbol(@class);
var attr = classSymbol?.GetAspectAttribute();
if (attr != null)
{
var currentMixins = classSymbol.GetMixinAttributes();
if (!currentMixins.Any(m => m.ConstructorArguments.Length > 0 &&
m.ConstructorArguments[0].Value is ITypeSymbol ts &&
ts == baseType))
{
context.RegisterRefactoring(CodeAction.Create(
"Register this interface with Aspect as Mixin.",
ct => AddMixinAttribute(context, @class, @base.Type, ct)));
}
}
}
}
}
}

private async Task<Document> AddMixinAttribute(CodeRefactoringContext context, ClassDeclarationSyntax @class, TypeSyntax baseType, CancellationToken ct)
{
var root = await context.Document.GetSyntaxRootAsync(ct).ConfigureAwait(false);

var newClass = @class.WithAttributeLists(@class.AttributeLists.Add(
AttributeList(
SeparatedList(new[]{
Attribute(IdentifierName(nameof(Mixin)),
AttributeArgumentList(SeparatedList(new[]{ AttributeArgument(TypeOfExpression(baseType.WithoutTrivia())) }))
)
})
).WithAdditionalAnnotations(Formatter.Annotation)
));

root = root.ReplaceNode(@class, newClass);

return context.Document.WithSyntaxRoot(root);
}
}
}
68 changes: 68 additions & 0 deletions src/AspectInjector.Analyzer/Refactorings/SampleAdvices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Generic;
using System.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace AspectInjector.Analyzer.Refactorings
{
public static partial class Samples
{
public static class Advices
{
public static readonly (MethodDeclarationSyntax Method, UsingDirectiveSyntax[] Usings) Before = CreateAdviceSample(
Broker.Kind.Before, "Before", PredefinedType(Token(SyntaxKind.VoidKeyword)),
new[] { Parameters.Name, Parameters.Arguments, Parameters.Type },
@"{
//Don't forget to remove unused parameters as it improves performance!
//Alt+Enter or Ctrl+. on Method name or Advice attribute to add more Arguments
Console.WriteLine($""Calling {name} from {hostType.Name} with args: {string.Join(',', args.Select(a => ToString()))}"");
}",
new[] { SyntaxBasicBlocks.Namespaces.SystemLinq, SyntaxBasicBlocks.Namespaces.System });


public static readonly (MethodDeclarationSyntax Method, UsingDirectiveSyntax[] Usings) After = CreateAdviceSample(
Broker.Kind.After, "After", PredefinedType(Token(SyntaxKind.VoidKeyword)),
new[] { Parameters.Name, Parameters.ReturnValue, Parameters.Type },
@"{
//Don't forget to remove unused parameters as it improves performance!
//Alt+Enter or Ctrl+. on Method name or Advice attribute to add more Arguments
Console.WriteLine($""Finished {name} from {hostType.Name} with result: {retValue}"");
}",
new[] { SyntaxBasicBlocks.Namespaces.System });

public static readonly (MethodDeclarationSyntax Method, UsingDirectiveSyntax[] Usings) Around = CreateAdviceSample(
Broker.Kind.Around, "Around", PredefinedType(Token(SyntaxKind.ObjectKeyword)),
new[] { Parameters.Name, Parameters.Arguments, Parameters.Type, Parameters.Target },
@"{
//Don't forget to remove unused parameters as it improves performance!
//Alt+Enter or Ctrl+. on Method name or Advice attribute to add more Arguments
Console.WriteLine($""Entering {name} from {hostType.Name}"");
var result = target(args);
Console.WriteLine($""Leaving {name} from {hostType.Name}"");
return result;
}",
new[] { SyntaxBasicBlocks.Namespaces.System });

private static (MethodDeclarationSyntax Method, UsingDirectiveSyntax[] Usings) CreateAdviceSample(Broker.Kind kind, string name, TypeSyntax returnType, IReadOnlyList<ParameterSample> parameters, string body, UsingDirectiveSyntax[] usings = null)
{
usings = usings ?? new UsingDirectiveSyntax[0];
usings = usings.Concat(parameters.Select(p => p.UsingSyntax)).Where(s => s != null).ToArray();

var method = MethodDeclaration(returnType, Identifier(name))
.WithAdviceAttribute(kind)
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
.WithParameterList(ParameterList(SeparatedList(parameters.Select(p => p.ParameterSyntax))))
.WithBody(ParseCompilationUnit(body, options: new CSharpParseOptions(kind: SourceCodeKind.Script)).ChildNodes().First().ChildNodes().OfType<BlockSyntax>().First())
.WithAdditionalAnnotations(Formatter.Annotation);

return (method, usings);
}
}
}
}
Loading

0 comments on commit 5780c17

Please sign in to comment.