File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\VisualBasic\Editing\VisualBasicImportAdder.vb
Web Access
Project: src\src\CodeStyle\VisualBasic\CodeFixes\Microsoft.CodeAnalysis.VisualBasic.CodeStyle.Fixes.vbproj (Microsoft.CodeAnalysis.VisualBasic.CodeStyle.Fixes)
' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.
 
Imports System.Collections.Immutable
Imports System.Composition
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Editing
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.VisualBasic.Editing
    <ExportLanguageService(GetType(ImportAdderService), LanguageNames.VisualBasic), [Shared]>
    Partial Friend NotInheritable Class VisualBasicImportAdder
        Inherits ImportAdderService
 
        <ImportingConstructor>
        <Obsolete(MefConstruction.ImportingConstructorMessage, True)>
        Public Sub New()
        End Sub
 
        Protected Overrides Function GetExplicitNamespaceSymbol(node As SyntaxNode, model As SemanticModel) As INamespaceSymbol
            Dim qname = TryCast(node, QualifiedNameSyntax)
            If qname IsNot Nothing Then
                Return GetExplicitNamespaceSymbol(qname, qname.Left, model)
            End If
 
            Dim maccess = TryCast(node, MemberAccessExpressionSyntax)
            If maccess IsNot Nothing Then
                Return GetExplicitNamespaceSymbol(maccess, maccess.Expression, model)
            End If
 
            Return Nothing
        End Function
 
        Protected Overrides Function AddPotentiallyConflictingImportsAsync(model As SemanticModel, container As SyntaxNode, namespaceSymbols As ImmutableArray(Of INamespaceSymbol), conflicts As HashSet(Of INamespaceSymbol), cancellationToken As CancellationToken) As Task
            Dim conflictFinder = New ConflictFinder(model, namespaceSymbols)
            Return conflictFinder.AddPotentiallyConflictingImportsAsync(container, conflicts, cancellationToken)
        End Function
 
        Private Overloads Shared Function GetExplicitNamespaceSymbol(fullName As ExpressionSyntax, namespacePart As ExpressionSyntax, model As SemanticModel) As INamespaceSymbol
            ' name must refer to something that is not a namespace, but be qualified with a namespace.
            Dim Symbol = model.GetSymbolInfo(fullName).Symbol
            Dim nsSymbol = TryCast(model.GetSymbolInfo(namespacePart).Symbol, INamespaceSymbol)
 
            If Symbol IsNot Nothing AndAlso Symbol.Kind <> SymbolKind.Namespace AndAlso nsSymbol IsNot Nothing Then
                ' use the symbols containing namespace, and not the potentially less than fully qualified namespace in the full name expression.
                Dim ns = Symbol.ContainingNamespace
                If ns IsNot Nothing Then
                    Return model.Compilation.GetCompilationNamespace(ns)
                End If
            End If
 
            Return Nothing
        End Function
 
        Private Class ConflictFinder
            Inherits VisualBasicSyntaxWalker
            Implements IEqualityComparer(Of (name As String, arity As Integer))
 
            Private ReadOnly _model As SemanticModel
 
            ''' <summary>
            ''' A mapping containing the simple names And arity of all namespace members, mapped to the import that
            ''' they're brought in by.
            ''' </summary>
            Private ReadOnly _importedTypesAndNamespaces As MultiDictionary(Of (name As String, arity As Integer), INamespaceSymbol)
 
            ''' <summary>
            ''' A mapping containing the simple names of all members, mapped to the import that they're brought in by.
            ''' Members are imported in through modules in vb. This doesn't keep track of arity because methods can be
            ''' called with type arguments.
            ''' </summary>
            Private ReadOnly _importedMembers As MultiDictionary(Of String, INamespaceSymbol)
 
            ''' <summary>
            ''' A mapping containing the simple names of all extension methods, mapped to the import that they're
            ''' brought in by. This doesn't keep track of arity because methods can be called with type arguments.
            ''' </summary>
            Private ReadOnly _importedExtensionMethods As MultiDictionary(Of String, INamespaceSymbol)
 
            Public Sub New(
                    model As SemanticModel,
                    namespaceSymbols As ImmutableArray(Of INamespaceSymbol))
                MyBase.New(SyntaxWalkerDepth.StructuredTrivia)
                _model = model
 
                _importedTypesAndNamespaces = New MultiDictionary(Of (name As String, arity As Integer), INamespaceSymbol)(Me)
                _importedMembers = New MultiDictionary(Of String, INamespaceSymbol)(VisualBasicSyntaxFacts.Instance.StringComparer)
                _importedExtensionMethods = New MultiDictionary(Of String, INamespaceSymbol)(VisualBasicSyntaxFacts.Instance.StringComparer)
 
                AddImportedMembers(namespaceSymbols)
            End Sub
 
            Private Sub AddImportedMembers(namespaceSymbols As ImmutableArray(Of INamespaceSymbol))
                For Each ns In namespaceSymbols
                    For Each typeOrNamespace In ns.GetMembers()
                        _importedTypesAndNamespaces.Add((typeOrNamespace.Name, typeOrNamespace.GetArity()), ns)
 
                        Dim type = TryCast(typeOrNamespace, INamedTypeSymbol)
                        If type?.MightContainExtensionMethods Then
                            For Each member In type.GetMembers()
                                Dim method = TryCast(member, IMethodSymbol)
                                If method?.IsExtensionMethod Then
                                    _importedExtensionMethods.Add(method.Name, ns)
                                End If
                            Next
                        End If
 
                        If type?.TypeKind = TypeKind.Module Then
                            ' modules make their members available to the containing scope.
                            For Each member In type.GetMembers()
                                Dim moduleType = TryCast(member, INamedTypeSymbol)
                                If moduleType IsNot Nothing Then
                                    _importedTypesAndNamespaces.Add((moduleType.Name, moduleType.GetArity()), ns)
                                Else
                                    _importedMembers.Add(member.Name, ns)
                                End If
                            Next
                        End If
                    Next
                Next
            End Sub
 
            Public Async Function AddPotentiallyConflictingImportsAsync(container As SyntaxNode, conflicts As HashSet(Of INamespaceSymbol), cancellationToken As CancellationToken) As Task
                Dim nodes = ArrayBuilder(Of SyntaxNode).GetInstance()
                Dim containsAnonymousMethods = False
 
                CollectInfoFromContainer(container, nodes, containsAnonymousMethods)
 
                Dim produceItems =
                    Function(node As SyntaxNode, onItemsFound As Action(Of INamespaceSymbol), conflictFinder As ConflictFinder, cancellationToken1 As CancellationToken) As Task
                        If TypeOf node Is SimpleNameSyntax Then
                            Me.ProduceConflicts(TryCast(node, SimpleNameSyntax), onItemsFound, cancellationToken)
                        ElseIf TypeOf node Is MemberAccessExpressionSyntax Then
                            Me.ProduceConflicts(TryCast(node, MemberAccessExpressionSyntax), containsAnonymousMethods, onItemsFound, cancellationToken)
                        Else
                            Throw ExceptionUtilities.Unreachable()
                        End If
                        Return Task.CompletedTask
                    End Function
 
                Dim items = Await ProducerConsumer(Of INamespaceSymbol).RunParallelAsync(
                    source:=nodes,
                    produceItems:=produceItems,
                    args:=Me,
                    cancellationToken).ConfigureAwait(False)
 
                conflicts.AddRange(items)
            End Function
 
            Private Sub CollectInfoFromContainer(container As SyntaxNode, nodes As ArrayBuilder(Of SyntaxNode), ByRef containsAnonymousMethods As Boolean)
                For Each node In container.DescendantNodesAndSelf()
                    Select Case node.Kind()
                        Case SyntaxKind.IdentifierName,
                             SyntaxKind.GenericName
                            If IsPotentialConflictWithImportedTypeNamespaceOrMember(TryCast(node, SimpleNameSyntax)) Then
                                nodes.Add(node)
                            End If
                        Case SyntaxKind.SimpleMemberAccessExpression,
                             SyntaxKind.DictionaryAccessExpression
                            If IsPotentialConflictWithImportedExtensionMethod(TryCast(node, MemberAccessExpressionSyntax)) Then
                                nodes.Add(node)
                            End If
                        Case SyntaxKind.MultiLineFunctionLambdaExpression,
                             SyntaxKind.MultiLineSubLambdaExpression,
                             SyntaxKind.SingleLineFunctionLambdaExpression,
                             SyntaxKind.SingleLineSubLambdaExpression
                            ' Track if we've seen an anonymous method or not.  If so, because of how the language binds lambdas and
                            ' overloads, we'll assume any method access we see inside (instance or otherwise) could end up conflicting
                            ' with an extension method we might pull in.
                            containsAnonymousMethods = True
                    End Select
                Next
            End Sub
 
            Private Function IsPotentialConflictWithImportedTypeNamespaceOrMember(node As SimpleNameSyntax) As Boolean
                ' Check to see if we have an standalone identifier (Or identifier on the left of a dot). If so, then we
                ' don't want to bring in any imports that would bring in the same name And could then potentially
                ' conflict here.
                If node.IsRightSideOfDotOrBang Then
                    Return False
                End If
 
                ' Drastically reduce the number of nodes that need to be inspected by filtering
                ' out nodes whose identifier isn't a potential conflict.
                If _importedTypesAndNamespaces.ContainsKey((node.Identifier.Text, node.Arity)) Then
                    Return True
                End If
 
                If _importedMembers.ContainsKey(node.Identifier.Text) Then
                    Return True
                End If
 
                Return False
            End Function
 
            Private Function IsPotentialConflictWithImportedExtensionMethod(node As MemberAccessExpressionSyntax) As Boolean
                Return _importedExtensionMethods.ContainsKey(node.Name.Identifier.Text)
            End Function
 
            Private Sub ProduceConflicts(node As SimpleNameSyntax, addConflict As Action(Of INamespaceSymbol), cancellationToken As CancellationToken)
                For Each conflictingSymbol In _importedTypesAndNamespaces.Item((node.Identifier.ValueText, node.Arity))
                    addConflict(conflictingSymbol)
                Next
 
                For Each conflictingSymbol In _importedMembers.Item(node.Identifier.ValueText)
                    addConflict(conflictingSymbol)
                Next
            End Sub
 
            Private Sub ProduceConflicts(node As MemberAccessExpressionSyntax, containsAnonymousMethods As Boolean, addConflict As Action(Of INamespaceSymbol), cancellationToken As CancellationToken)
                ' Check to see if we have a reference to an extension method.  If so, then pulling in an import could
                ' bring in an extension that conflicts with that.
                Dim method = TryCast(_model.GetSymbolInfo(node.Name, cancellationToken).GetAnySymbol(), IMethodSymbol)
                If method IsNot Nothing Then
                    Dim isConflicting = method.IsReducedExtension()
 
                    If Not isConflicting And containsAnonymousMethods Then
                        ' lambdas are interesting.  Say you have
                        '
                        '      Goo(sub (x) x.M())
                        '
                        '      sub Goo(act as Action(of C))
                        '      sub Goo(act as Action(of integer))
                        '
                        '      class C : public sub M()
                        '
                        ' This Is legal code where the lambda body Is calling the instance method.  However, if we introduce a
                        ' using that brings in an extension method 'M' on 'int', then the above will become ambiguous.  This is
                        ' because lambda binding will try each interpretation separately And eliminate the ones that fail.
                        ' Adding the import will make the int form succeed, causing ambiguity.
                        '
                        ' To deal with that, we keep track of if we're in a lambda, and we conservatively assume that a method
                        ' access (even to a non-extension method) could conflict with an extension method brought in.
 
                        isConflicting = node.HasAncestor(Of LambdaExpressionSyntax)()
                    End If
 
                    If isConflicting Then
                        For Each conflictingSymbol In _importedExtensionMethods.Item(method.Name)
                            addConflict(conflictingSymbol)
                        Next
                    End If
                End If
            End Sub
 
            Public Shadows Function Equals(
                    x As (name As String, arity As Integer),
                    y As (name As String, arity As Integer)) As Boolean Implements IEqualityComparer(Of (name As String, arity As Integer)).Equals
 
                Return x.arity = y.arity AndAlso
                    VisualBasicSyntaxFacts.Instance.StringComparer.Equals(x.name, y.name)
            End Function
 
            Public Shadows Function GetHashCode(obj As (name As String, arity As Integer)) As Integer Implements IEqualityComparer(Of (name As String, arity As Integer)).GetHashCode
                Return Hash.Combine(obj.arity,
                    VisualBasicSyntaxFacts.Instance.StringComparer.GetHashCode(obj.name))
            End Function
        End Class
    End Class
End Namespace