// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.NetCore.Analyzers.InteropServices;
namespace Microsoft.NetCore.CSharp.Analyzers.InteropServices
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpDynamicInterfaceCastableImplementationFixer : DynamicInterfaceCastableImplementationFixer
{
protected override async Task ImplementInterfacesOnDynamicCastableImplementationAsync(
SyntaxNode declaration,
Document document,
SyntaxEditor editor,
CancellationToken cancellationToken)
{
var generator = editor.Generator;
var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var type = (INamedTypeSymbol)model.GetDeclaredSymbol(declaration, cancellationToken)!;
var defaultMethodBodyStatements = generator.DefaultMethodBody(model.Compilation).ToArray();
var generatedMembers = new List<SyntaxNode>();
foreach (var iface in type.AllInterfaces)
{
foreach (var member in iface.GetMembers())
{
if (member.IsAbstract && type.FindImplementationForInterfaceMember(member) is null)
{
var implementation = member.Kind switch
{
SymbolKind.Method => GenerateMethodImplementation((IMethodSymbol)member),
SymbolKind.Property => GeneratePropertyImplementation((IPropertySymbol)member),
SymbolKind.Event => GenerateEventImplementation((IEventSymbol)member, generator, defaultMethodBodyStatements),
_ => null
};
if (implementation is not null)
{
if (generator.AsPrivateInterfaceImplementation(
implementation,
generator.NameExpression(member.ContainingType)) is not SyntaxNode privateImplementation)
{
return;
}
generatedMembers.Add(privateImplementation);
}
}
}
}
// Explicitly use the C# syntax APIs to work around https://github.com/dotnet/roslyn/issues/53605
var members = generatedMembers.Cast<MemberDeclarationSyntax>().ToArray();
editor.ReplaceNode(declaration, (currentNode, _) => ((TypeDeclarationSyntax)currentNode).AddMembers(members));
SyntaxNode? GenerateMethodImplementation(IMethodSymbol method)
{
// Method with associated symbols are not standalone methods that a user would write in C#.
// They're methods like property or event accessors, which are covered in the following local functions.
if (method.AssociatedSymbol is not null)
{
return null;
}
var methodDeclaration = generator.MethodDeclaration(method);
methodDeclaration = generator.WithModifiers(methodDeclaration, generator.GetModifiers(methodDeclaration).WithIsAbstract(false));
return generator.WithStatements(methodDeclaration, defaultMethodBodyStatements);
}
SyntaxNode GeneratePropertyImplementation(IPropertySymbol property)
{
var propertyDeclaration = property.IsIndexer
? generator.IndexerDeclaration(property)
: generator.PropertyDeclaration(property);
propertyDeclaration = generator.WithModifiers(propertyDeclaration, generator.GetModifiers(propertyDeclaration).WithIsAbstract(false));
// Remove default property accessors.
propertyDeclaration = generator.WithAccessorDeclarations(propertyDeclaration);
if (property.GetMethod is not null
&& model.Compilation.IsSymbolAccessibleWithin(property.GetMethod, type))
{
propertyDeclaration = generator.WithGetAccessorStatements(propertyDeclaration, defaultMethodBodyStatements);
}
if (property.SetMethod is not null
&& model.Compilation.IsSymbolAccessibleWithin(property.SetMethod, type))
{
propertyDeclaration = AddSetAccessor(property, propertyDeclaration, generator, defaultMethodBodyStatements, includeAccessibility: false);
}
return propertyDeclaration;
}
}
private static SyntaxNode AddSetAccessor(
IPropertySymbol property,
SyntaxNode declaration,
SyntaxGenerator generator,
SyntaxNode[] defaultMethodBodyStatements,
bool includeAccessibility)
{
if (!property.SetMethod!.IsInitOnly)
{
return generator.WithSetAccessorStatements(declaration, defaultMethodBodyStatements);
}
var setAccessorAccessibility = includeAccessibility && property.DeclaredAccessibility != property.SetMethod!.DeclaredAccessibility
? property.SetMethod.DeclaredAccessibility
: Accessibility.NotApplicable;
var setAccessor = (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(setAccessorAccessibility, defaultMethodBodyStatements);
var propertyDeclaration = (PropertyDeclarationSyntax)declaration;
SyntaxNode? oldInitAccessor = null;
foreach (var accessor in propertyDeclaration.AccessorList!.Accessors)
{
if (accessor.IsKind(SyntaxKind.InitAccessorDeclaration))
{
oldInitAccessor = accessor;
break;
}
}
if (oldInitAccessor is not null)
{
propertyDeclaration = propertyDeclaration.WithAccessorList(propertyDeclaration.AccessorList.RemoveNode(oldInitAccessor, SyntaxRemoveOptions.KeepNoTrivia));
}
return propertyDeclaration.WithAccessorList(propertyDeclaration.AccessorList!.AddAccessors(
SyntaxFactory.AccessorDeclaration(
SyntaxKind.InitAccessorDeclaration,
setAccessor.AttributeLists,
setAccessor.Modifiers,
SyntaxFactory.Token(SyntaxKind.InitKeyword),
setAccessor.Body,
setAccessor.ExpressionBody,
setAccessor.SemicolonToken)));
}
private static SyntaxNode? GenerateEventImplementation(
IEventSymbol evt,
SyntaxGenerator generator,
SyntaxNode[] defaultMethodBodyStatements)
{
var eventDeclaration = generator.CustomEventDeclaration(evt);
eventDeclaration = generator.WithModifiers(eventDeclaration, generator.GetModifiers(eventDeclaration).WithIsAbstract(false));
if (generator.GetAccessor(eventDeclaration, DeclarationKind.AddAccessor) is not SyntaxNode addAccessor ||
generator.GetAccessor(eventDeclaration, DeclarationKind.RemoveAccessor) is not SyntaxNode removeAccessor)
{
return null;
}
// Explicitly use the C# syntax APIs to work around https://github.com/dotnet/roslyn/issues/53649
return ((EventDeclarationSyntax)eventDeclaration).WithAccessorList(
SyntaxFactory.AccessorList(
SyntaxFactory.List(
new[]
{
(AccessorDeclarationSyntax)generator.WithStatements(addAccessor, defaultMethodBodyStatements),
(AccessorDeclarationSyntax)generator.WithStatements(removeAccessor, defaultMethodBodyStatements),
})));
}
protected override async Task MakeMemberDeclaredOnImplementationTypeStaticAsync(SyntaxNode declaration, Document document, SyntaxEditor editor, CancellationToken cancellationToken)
{
var root = editor.OriginalRoot;
var generator = editor.Generator;
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var defaultMethodBodyStatements = generator.DefaultMethodBody(semanticModel.Compilation).ToArray();
var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken);
if (symbol is not IMethodSymbol)
{
// We can't automatically make properties or events static.
return;
}
// We're going to convert the this parameter to a @this parameter at the start of the parameter list,
// so we need to warn if the symbol already exists in scope since the fix may produce broken code.
SymbolInfo introducedThisParamInfo = semanticModel.GetSpeculativeSymbolInfo(
declaration.SpanStart,
SyntaxFactory.IdentifierName(EscapedThisToken),
SpeculativeBindingOption.BindAsExpression);
bool shouldWarn = false;
if (introducedThisParamInfo.Symbol is not null)
{
shouldWarn = true;
}
var referencedSymbols = await SymbolFinder.FindReferencesAsync(
symbol, document.Project.Solution, cancellationToken).ConfigureAwait(false);
List<(InvocationExpressionSyntax invocation, ExpressionSyntax target)> invocations = new();
foreach (var referencedSymbol in referencedSymbols)
{
foreach (var location in referencedSymbol.Locations)
{
if (!location.Document.Id.Equals(document.Id))
{
shouldWarn = true;
continue;
}
// We limited the search scope to the single document,
// so all reference should be in the same tree.
var referenceNode = root.FindNode(location.Location.SourceSpan);
if (referenceNode is not IdentifierNameSyntax identifierNode)
{
// Unexpected scenario, skip and warn.
shouldWarn = true;
continue;
}
if (identifierNode.Parent is InvocationExpressionSyntax invocationOnThis)
{
invocations.Add((invocationOnThis, SyntaxFactory.ThisExpression()));
}
else if (identifierNode.Parent is MemberAccessExpressionSyntax methodAccess
&& methodAccess.Parent is InvocationExpressionSyntax invocation)
{
invocations.Add((invocation, methodAccess.Expression));
}
else
{
// We won't be able to fix non-invocation references,
// e.g. creating a delegate.
shouldWarn = true;
}
}
}
// Fix all invocations by passing in this argument.
foreach (var invocation in invocations)
{
editor.ReplaceNode(
invocation.invocation,
(node, generator) =>
{
var currentInvocation = (InvocationExpressionSyntax)node;
var newArgList = currentInvocation.ArgumentList.WithArguments(
SyntaxFactory.SingletonSeparatedList((ArgumentSyntax)generator.Argument(invocation.target))
.AddRange(currentInvocation.ArgumentList.Arguments));
return currentInvocation.WithArgumentList(newArgList).WithExpression(SyntaxFactory.IdentifierName(symbol.Name));
});
}
editor.ReplaceNode(
declaration,
(node, generator) =>
{
var updatedMethod = generator.WithModifiers(node, DeclarationModifiers.From(symbol)
.WithIsAbstract(false)
.WithIsVirtual(false)
.WithIsSealed(false)
.WithIsStatic(true));
if (symbol.IsAbstract)
{
updatedMethod = generator.WithStatements(updatedMethod, defaultMethodBodyStatements);
}
updatedMethod = ((MethodDeclarationSyntax)updatedMethod).WithParameterList(
SyntaxFactory.ParameterList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Parameter(EscapedThisToken)
.WithType(SyntaxFactory.ParseTypeName(symbol.ContainingType.Name)))
.AddRange(((MethodDeclarationSyntax)updatedMethod).ParameterList.Parameters)));
ThisToIntroducedParameterRewriter rewriter = new ThisToIntroducedParameterRewriter();
updatedMethod = rewriter.Visit(updatedMethod);
if (shouldWarn)
{
updatedMethod = updatedMethod.WithAdditionalAnnotations(CreatePossibleInvalidCodeWarning());
}
return updatedMethod;
});
}
private static readonly SyntaxToken EscapedThisToken = SyntaxFactory.Identifier(
SyntaxFactory.TriviaList(),
SyntaxKind.IdentifierToken,
"@this",
"this",
SyntaxFactory.TriviaList());
protected override bool CodeFixSupportsDeclaration(SyntaxNode declaration)
{
return !declaration.IsKind(SyntaxKind.VariableDeclarator);
}
private class ThisToIntroducedParameterRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitThisExpression(ThisExpressionSyntax node)
{
return SyntaxFactory.IdentifierName(EscapedThisToken);
}
}
}
}