// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; using ICSharpCode.NRefactory6.CSharp; using ICSharpCode.NRefactory6.CSharp.Refactoring; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; namespace MonoDevelop.CSharp.CodeFixes { internal abstract partial class AbstractAddImportCodeFixProvider : CodeFixProvider { protected abstract bool IgnoreCase { get; } protected abstract bool CanAddImport(SyntaxNode node, CancellationToken cancellationToken); protected abstract bool CanAddImportForMethod(Diagnostic diagnostic, ref SyntaxNode node); protected abstract bool CanAddImportForNamespace(Diagnostic diagnostic, ref SyntaxNode node); protected abstract bool CanAddImportForQuery(Diagnostic diagnostic, ref SyntaxNode node); protected abstract bool CanAddImportForType(Diagnostic diagnostic, ref SyntaxNode node); protected abstract ISet GetNamespacesInScope(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); protected abstract ITypeSymbol GetQueryClauseInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); protected abstract string GetDescription(INamespaceOrTypeSymbol symbol, SemanticModel semanticModel, SyntaxNode root); protected abstract Task AddImportAsync(SyntaxNode contextNode, INamespaceOrTypeSymbol symbol, Document documemt, bool specialCaseSystem, CancellationToken cancellationToken); protected abstract bool IsViableExtensionMethod(IMethodSymbol method, SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); protected abstract IEnumerable GetProposedTypes(string name, List accessibleTypeSymbols, SemanticModel semanticModel, ISet namespacesInScope); internal abstract bool IsViableField(IFieldSymbol field, SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); internal abstract bool IsViableProperty(IPropertySymbol property, SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); internal abstract bool IsAddMethodContext(SyntaxNode node, SemanticModel semanticModel); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var document = context.Document; var span = context.Span; var diagnostics = context.Diagnostics; var cancellationToken = context.CancellationToken; var project = document.Project; var diagnostic = diagnostics.First(); var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait (false); if (model.IsFromGeneratedCode (context.CancellationToken)) return; var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var ancestors = root.FindToken(span.Start, findInsideTrivia: true).GetAncestors(); if (!ancestors.Any()) { return; } var node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root); if (node == null) { return; } var placeSystemNamespaceFirst = true; //document.Project.Solution.Workspace.Options.GetOption(Microsoft.CodeAnalysis.Shared.Options.OrganizerOptions.PlaceSystemNamespaceFirst, document.Project.Language); if (!cancellationToken.IsCancellationRequested) { if (this.CanAddImport(node, cancellationToken)) { var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); var containingType = semanticModel.GetEnclosingNamedType(node.SpanStart, cancellationToken); var containingTypeOrAssembly = containingType ?? (ISymbol)semanticModel.Compilation.Assembly; var namespacesInScope = this.GetNamespacesInScope(semanticModel, node, cancellationToken); var matchingTypesNamespaces = await this.GetNamespacesForMatchingTypesAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); var matchingTypes = await this.GetMatchingTypesAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); var matchingNamespaces = await this.GetNamespacesForMatchingNamespacesAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); var matchingExtensionMethodsNamespaces = await this.GetNamespacesForMatchingExtensionMethodsAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); var matchingFieldsAndPropertiesAsync = await this.GetNamespacesForMatchingFieldsAndPropertiesAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); var queryPatternsNamespaces = await this.GetNamespacesForQueryPatternsAsync(project, diagnostic, node, semanticModel, namespacesInScope, cancellationToken).ConfigureAwait(false); if (matchingTypesNamespaces != null || matchingNamespaces != null || matchingExtensionMethodsNamespaces != null || matchingFieldsAndPropertiesAsync != null || queryPatternsNamespaces != null || matchingTypes != null) { matchingTypesNamespaces = matchingTypesNamespaces ?? SpecializedCollections.EmptyList(); matchingNamespaces = matchingNamespaces ?? SpecializedCollections.EmptyList(); matchingExtensionMethodsNamespaces = matchingExtensionMethodsNamespaces ?? SpecializedCollections.EmptyList(); matchingFieldsAndPropertiesAsync = matchingFieldsAndPropertiesAsync ?? SpecializedCollections.EmptyList(); queryPatternsNamespaces = queryPatternsNamespaces ?? SpecializedCollections.EmptyList(); matchingTypes = matchingTypes ?? SpecializedCollections.EmptyList(); var proposedImports = matchingTypesNamespaces.Cast () .Concat (matchingNamespaces.Cast ()) .Concat (matchingExtensionMethodsNamespaces.Cast ()) .Concat (matchingFieldsAndPropertiesAsync.Cast ()) .Concat (queryPatternsNamespaces.Cast ()) .Concat (matchingTypes.Cast ()) .Distinct () .Where (NotNull) .Where (NotGlobalNamespace) .ToList (); proposedImports.Sort (INamespaceOrTypeSymbolExtensions.CompareNamespaceOrTypeSymbols); proposedImports = proposedImports.Take (8).ToList (); if (proposedImports.Count > 0) { cancellationToken.ThrowIfCancellationRequested(); foreach (var import in proposedImports) { var action = new DocumentChangeAction( node.Span, DiagnosticSeverity.Error, this.GetDescription(import, semanticModel, node), (c) => this.AddImportAsync(node, import, document, placeSystemNamespaceFirst, cancellationToken) ); context.RegisterCodeFix(action, diagnostic); } } } } } } private async Task> GetNamespacesForMatchingTypesAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForType(diagnostic, ref node)) { return null; } string name; int arity; bool inAttributeContext, hasIncompleteParentMember; CalculateContext(node, out name, out arity, out inAttributeContext, out hasIncompleteParentMember); var symbols = await GetTypeSymbols(project, node, semanticModel, name, inAttributeContext, cancellationToken).ConfigureAwait(false); if (symbols == null) { return null; } return GetNamespacesForMatchingTypesAsync(semanticModel, namespacesInScope, arity, inAttributeContext, hasIncompleteParentMember, symbols); } private IEnumerable GetNamespacesForMatchingTypesAsync(SemanticModel semanticModel, ISet namespacesInScope, int arity, bool inAttributeContext, bool hasIncompleteParentMember, IEnumerable symbols) { var accessibleTypeSymbols = symbols .Where(s => s.ContainingSymbol is INamespaceSymbol && ArityAccessibilityAndAttributeContextAreCorrect( semanticModel, s, arity, inAttributeContext, hasIncompleteParentMember)) .ToList(); return GetProposedNamespaces( accessibleTypeSymbols.Select(s => s.ContainingNamespace), semanticModel, namespacesInScope); } private async Task> GetMatchingTypesAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForType(diagnostic, ref node)) { return null; } string name; int arity; bool inAttributeContext, hasIncompleteParentMember; CalculateContext(node, out name, out arity, out inAttributeContext, out hasIncompleteParentMember); var symbols = await GetTypeSymbols(project, node, semanticModel, name, inAttributeContext, cancellationToken).ConfigureAwait(false); if (symbols == null) { return null; } return GetMatchingTypes(semanticModel, namespacesInScope, name, arity, inAttributeContext, symbols, hasIncompleteParentMember); } private async Task> GetNamespacesForMatchingNamespacesAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForNamespace(diagnostic, ref node)) { return null; } string name; int arity; node.GetNameAndArityOfSimpleName(out name, out arity); if (ExpressionBinds(node, semanticModel, cancellationToken)) { return null; } var symbols = await SymbolFinder.FindDeclarationsAsync( project, name, this.IgnoreCase, SymbolFilter.Namespace, cancellationToken).ConfigureAwait(false); return GetProposedNamespaces( symbols.OfType().Select(n => n.ContainingNamespace), semanticModel, namespacesInScope); } private async Task> GetNamespacesForMatchingExtensionMethodsAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForMethod(diagnostic, ref node)) { return null; } var expression = node.Parent; var extensionMethods = SpecializedCollections.EmptyEnumerable(); var symbols = await GetSymbolsAsync(project, node, semanticModel, cancellationToken).ConfigureAwait(false); if (symbols != null) { extensionMethods = FilterForExtensionMethods(semanticModel, namespacesInScope, expression, symbols, cancellationToken); } var addMethods = SpecializedCollections.EmptyEnumerable(); var methodSymbols = await GetAddMethodsAsync(project, diagnostic, node, semanticModel, namespacesInScope, expression, cancellationToken).ConfigureAwait(false); if (methodSymbols != null) { addMethods = GetProposedNamespaces( methodSymbols.Select(s => s.ContainingNamespace), semanticModel, namespacesInScope); } return extensionMethods.Concat(addMethods); } private async Task> GetNamespacesForMatchingFieldsAndPropertiesAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForMethod(diagnostic, ref node)) { return null; } var expression = node.Parent; var symbols = await GetSymbolsAsync(project, node, semanticModel, cancellationToken).ConfigureAwait(false); if (symbols != null) { return FilterForFieldsAndProperties(semanticModel, namespacesInScope, expression, symbols, cancellationToken); } return null; } private IEnumerable FilterForFieldsAndProperties(SemanticModel semanticModel, ISet namespacesInScope, SyntaxNode expression, IEnumerable symbols, CancellationToken cancellationToken) { var propertySymbols = symbols .OfType() .Where(property => property.ContainingType?.IsAccessibleWithin(semanticModel.Compilation.Assembly) == true && IsViableProperty(property, expression, semanticModel, cancellationToken)) .ToList(); var fieldSymbols = symbols .OfType() .Where(field => field.ContainingType?.IsAccessibleWithin(semanticModel.Compilation.Assembly) == true && IsViableField(field, expression, semanticModel, cancellationToken)) .ToList(); return GetProposedNamespaces( propertySymbols.Select(s => s.ContainingNamespace).Concat(fieldSymbols.Select(s => s.ContainingNamespace)), semanticModel, namespacesInScope); } private Task> GetSymbolsAsync( Microsoft.CodeAnalysis.Project project, SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // See if the name binds. If it does, there's nothing further we need to do. if (ExpressionBinds(node, semanticModel, cancellationToken, checkForExtensionMethods: true)) { return Task.FromResult (Enumerable.Empty()); } string name; int arity; node.GetNameAndArityOfSimpleName(out name, out arity); if (name == null) { return Task.FromResult (Enumerable.Empty()); } return SymbolFinder.FindDeclarationsAsync(project, name, this.IgnoreCase, SymbolFilter.Member, cancellationToken); } private async Task> GetAddMethodsAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, SyntaxNode expression, CancellationToken cancellationToken) { string name; int arity; node.GetNameAndArityOfSimpleName(out name, out arity); if (name != null) { return SpecializedCollections.EmptyEnumerable(); } if (IsAddMethodContext(node, semanticModel)) { var symbols = await SymbolFinder.FindDeclarationsAsync(project, "Add", this.IgnoreCase, SymbolFilter.Member, cancellationToken).ConfigureAwait(false); return symbols .OfType() .Where(method => method.IsExtensionMethod && method.ContainingType?.IsAccessibleWithin(semanticModel.Compilation.Assembly) == true && IsViableExtensionMethod(method, expression, semanticModel, cancellationToken)); } return SpecializedCollections.EmptyEnumerable(); } private IEnumerable FilterForExtensionMethods(SemanticModel semanticModel, ISet namespacesInScope, SyntaxNode expression, IEnumerable symbols, CancellationToken cancellationToken) { var extensionMethodSymbols = symbols .OfType() .Where(method => method.IsExtensionMethod && method.ContainingType?.IsAccessibleWithin(semanticModel.Compilation.Assembly) == true && IsViableExtensionMethod(method, expression, semanticModel, cancellationToken)) .ToList(); return GetProposedNamespaces( extensionMethodSymbols.Select(s => s.ContainingNamespace), semanticModel, namespacesInScope); } private async Task> GetNamespacesForQueryPatternsAsync( Microsoft.CodeAnalysis.Project project, Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, ISet namespacesInScope, CancellationToken cancellationToken) { if (!this.CanAddImportForQuery(diagnostic, ref node)) { return null; } ITypeSymbol type = this.GetQueryClauseInfo(semanticModel, node, cancellationToken); if (type == null) { return null; } // find extension methods named "Select" var symbols = await SymbolFinder.FindDeclarationsAsync(project, "Select", this.IgnoreCase, SymbolFilter.Member, cancellationToken).ConfigureAwait(false); var extensionMethodSymbols = symbols .OfType() .Where(s => s.IsExtensionMethod && IsViableExtensionMethod(type, s)) .ToList(); return GetProposedNamespaces( extensionMethodSymbols.Select(s => s.ContainingNamespace), semanticModel, namespacesInScope); } private bool IsViableExtensionMethod( ITypeSymbol typeSymbol, IMethodSymbol method) { return typeSymbol != null && method.ReduceExtensionMethod(typeSymbol) != null; } private static bool ArityAccessibilityAndAttributeContextAreCorrect( SemanticModel semanticModel, ITypeSymbol symbol, int arity, bool inAttributeContext, bool hasIncompleteParentMember) { return (arity == 0 || symbol.GetArity() == arity || hasIncompleteParentMember) && symbol.IsAccessibleWithin(semanticModel.Compilation.Assembly) && (!inAttributeContext || symbol.IsAttribute()); } private async Task> GetTypeSymbols( Microsoft.CodeAnalysis.Project project, SyntaxNode node, SemanticModel semanticModel, string name, bool inAttributeContext, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return null; } if (ExpressionBinds(node, semanticModel, cancellationToken)) { return null; } var symbols = await SymbolFinder.FindDeclarationsAsync(project, name, this.IgnoreCase, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); // also lookup type symbols with the "Attribute" suffix. if (inAttributeContext) { symbols = symbols.Concat( await SymbolFinder.FindDeclarationsAsync(project, name + "Attribute", this.IgnoreCase, SymbolFilter.Type, cancellationToken).ConfigureAwait(false)); } return symbols.OfType(); } private IEnumerable GetMatchingTypes(SemanticModel semanticModel, ISet namespacesInScope, string name, int arity, bool inAttributeContext, IEnumerable symbols, bool hasIncompleteParentMember) { var accessibleTypeSymbols = symbols .Where(s => ArityAccessibilityAndAttributeContextAreCorrect( semanticModel, s, arity, inAttributeContext, hasIncompleteParentMember)) .ToList(); return GetProposedTypes( name, accessibleTypeSymbols, semanticModel, namespacesInScope); } private static void CalculateContext(SyntaxNode node, out string name, out int arity, out bool inAttributeContext, out bool hasIncompleteParentMember) { // Has to be a simple identifier or generic name. node.GetNameAndArityOfSimpleName(out name, out arity); inAttributeContext = node.IsAttributeName(); hasIncompleteParentMember = node.HasIncompleteParentMember(); } protected bool ExpressionBinds(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken, bool checkForExtensionMethods = false) { // See if the name binds to something other then the error type. If it does, there's nothing further we need to do. // For extension methods, however, we will continue to search if there exists any better matched method. cancellationToken.ThrowIfCancellationRequested(); var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && !checkForExtensionMethods) { return true; } return symbolInfo.Symbol != null; } protected IEnumerable GetProposedNamespaces( IEnumerable namespaces, SemanticModel semanticModel, ISet namespacesInScope) { // We only want to offer to add a using if we don't already have one. return namespaces.Where(n => !n.IsGlobalNamespace) .Select(n => semanticModel.Compilation.GetCompilationNamespace(n) ?? n) .Where(n => n != null && !namespacesInScope.Contains(n)); } private static bool NotGlobalNamespace(INamespaceOrTypeSymbol symbol) { return symbol.IsNamespace ? !((INamespaceSymbol)symbol).IsGlobalNamespace : true; } private static bool NotNull(INamespaceOrTypeSymbol symbol) { return symbol != null; } } }