diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs index f776a0892545e..0fcf6330761a3 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs @@ -24,7 +24,7 @@ protected override bool IsPredefinedTypeReplaceableWithFrameworkType(PredefinedT => node.Keyword.Kind() != SyntaxKind.VoidKeyword; protected override bool IsInMemberAccessOrCrefReferenceContext(ExpressionSyntax node) - => node.IsInMemberAccessContext() || node.InsideCrefReference(); + => node.IsDirectChildOfMemberAccessExpression() || node.InsideCrefReference(); protected override string GetLanguageName() => LanguageNames.CSharp; diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs index f384fd12b377e..f4c9fd2e34620 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs @@ -8,6 +8,7 @@ using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; @@ -75,15 +76,14 @@ internal override bool CanSimplifyTypeNameExpression( SyntaxNode replacementSyntax; if (node.IsKind(SyntaxKind.QualifiedCref, out QualifiedCrefSyntax crefSyntax)) { - if (!crefSyntax.TryReduceOrSimplifyExplicitName(model, out var replacement, out issueSpan, optionSet, cancellationToken)) + if (!QualifiedCrefSimplifier.Instance.TrySimplify(crefSyntax, model, optionSet, out var replacement, out issueSpan, cancellationToken)) return false; replacementSyntax = replacement; } else { - var expression = (ExpressionSyntax)node; - if (!expression.TryReduceOrSimplifyExplicitName(model, out var replacement, out issueSpan, optionSet, cancellationToken)) + if (!ExpressionSimplifier.Instance.TrySimplify((ExpressionSyntax)node, model, optionSet, out var replacement, out issueSpan, cancellationToken)) return false; replacementSyntax = replacement; diff --git a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs index 41f17abaa96e5..e454e12c7519a 100644 --- a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageServices; @@ -37,7 +38,7 @@ protected override bool CanSimplifyTypeNameExpression( SemanticModel model, MemberAccessExpressionSyntax node, OptionSet optionSet, out TextSpan issueSpan, CancellationToken cancellationToken) { - return node.TryReduceOrSimplifyExplicitName(model, out _, out issueSpan, optionSet, cancellationToken); + return ExpressionSimplifier.Instance.TrySimplify(node, model, optionSet, out _, out issueSpan, cancellationToken); } } } diff --git a/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicPreferFrameworkTypeDiagnosticAnalyzer.vb b/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicPreferFrameworkTypeDiagnosticAnalyzer.vb index 08f3a3a7b171b..bab2c4ee1d367 100644 --- a/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicPreferFrameworkTypeDiagnosticAnalyzer.vb +++ b/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicPreferFrameworkTypeDiagnosticAnalyzer.vb @@ -16,7 +16,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Diagnostics.Analyzers ImmutableArray.Create(SyntaxKind.PredefinedType) Protected Overrides Function IsInMemberAccessOrCrefReferenceContext(node As ExpressionSyntax) As Boolean - Return node.IsInMemberAccessContext() OrElse node.InsideCrefReference() + Return node.IsDirectChildOfMemberAccessExpression() OrElse node.InsideCrefReference() End Function Protected Overrides Function IsPredefinedTypeReplaceableWithFrameworkType(node As PredefinedTypeSyntax) As Boolean diff --git a/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicSimplifyTypeNamesDiagnosticAnalyzer.vb b/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicSimplifyTypeNamesDiagnosticAnalyzer.vb index 76ec9aaa13c53..c3e6d4b4bb58c 100644 --- a/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicSimplifyTypeNamesDiagnosticAnalyzer.vb +++ b/src/Features/VisualBasic/Portable/Diagnostics/Analyzers/VisualBasicSimplifyTypeNamesDiagnosticAnalyzer.vb @@ -9,6 +9,7 @@ Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.Options Imports Microsoft.CodeAnalysis.SimplifyTypeNames Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.CodeFixes.SimplifyTypeNames @@ -68,7 +69,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.CodeFixes.SimplifyTypeNames End If Dim replacementSyntax As ExpressionSyntax = Nothing - If Not expression.TryReduceOrSimplifyExplicitName(model, replacementSyntax, issueSpan, optionSet, cancellationToken) Then + If Not ExpressionSimplifier.Instance.TrySimplify(expression, model, optionSet, replacementSyntax, issueSpan, cancellationToken) Then Return False End If diff --git a/src/Features/VisualBasic/Portable/SimplifyThisOrMe/VisualBasicSimplifyThisOrMeDiagnosticAnalyzer.vb b/src/Features/VisualBasic/Portable/SimplifyThisOrMe/VisualBasicSimplifyThisOrMeDiagnosticAnalyzer.vb index 00ce0c46b7da4..cd1e7ea5ede8a 100644 --- a/src/Features/VisualBasic/Portable/SimplifyThisOrMe/VisualBasicSimplifyThisOrMeDiagnosticAnalyzer.vb +++ b/src/Features/VisualBasic/Portable/SimplifyThisOrMe/VisualBasicSimplifyThisOrMeDiagnosticAnalyzer.vb @@ -9,6 +9,7 @@ Imports Microsoft.CodeAnalysis.LanguageServices Imports Microsoft.CodeAnalysis.Options Imports Microsoft.CodeAnalysis.SimplifyThisOrMe Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.SimplifyThisOrMe @@ -38,7 +39,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.SimplifyThisOrMe cancellationToken As CancellationToken) As Boolean Dim replacementSyntax As ExpressionSyntax = Nothing - Return memberAccess.TryReduceOrSimplifyExplicitName(model, replacementSyntax, issueSpan, optionSet, cancellationToken) + Return ExpressionSimplifier.Instance.TrySimplify(memberAccess, model, optionSet, replacementSyntax, issueSpan, cancellationToken) End Function End Class End Namespace diff --git a/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs index 4913ff201bea3..465b52bf00c72 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs @@ -2,22 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; -using Microsoft.CodeAnalysis.CodeStyle; -using Microsoft.CodeAnalysis.CSharp.Simplification; -using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Utilities; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Rename.ConflictEngine; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Simplification; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Extensions @@ -749,1719 +741,20 @@ public static bool CanAccessInstanceAndStaticMembersOffOf( return false; } - public static bool TryReduceOrSimplifyExplicitName( - this ExpressionSyntax expression, - SemanticModel semanticModel, - out ExpressionSyntax replacementNode, - out TextSpan issueSpan, - OptionSet optionSet, - CancellationToken cancellationToken) - { - if (TryReduceExplicitName(expression, semanticModel, out var replacementTypeNode, out issueSpan, optionSet, cancellationToken)) - { - replacementNode = replacementTypeNode; - return true; - } - - return TrySimplify(expression, semanticModel, out replacementNode, out issueSpan, cancellationToken); - } - - private static bool TryReduceExplicitName( - ExpressionSyntax expression, - SemanticModel semanticModel, - out TypeSyntax replacementNode, - out TextSpan issueSpan, - OptionSet optionSet, - CancellationToken cancellationToken) - { - replacementNode = null; - issueSpan = default; - - if (expression.ContainsInterleavedDirective(cancellationToken)) - return false; - - if (expression.IsKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax memberAccess)) - return TryReduceMemberAccessExpression(memberAccess, semanticModel, out replacementNode, out issueSpan, optionSet, cancellationToken); - - if (expression is NameSyntax name) - return TryReduceName(name, semanticModel, out replacementNode, out issueSpan, optionSet, cancellationToken); - - return false; - } - - private static bool TryReduceMemberAccessExpression( - MemberAccessExpressionSyntax memberAccess, - SemanticModel semanticModel, - out TypeSyntax replacementNode, - out TextSpan issueSpan, - OptionSet optionSet, - CancellationToken cancellationToken) - { - replacementNode = null; - issueSpan = default; - - if (memberAccess.Name == null || memberAccess.Expression == null) - return false; - - // if this node is annotated as being a SpecialType, let's use this information. - if (memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind)) - { - replacementNode = SyntaxFactory.PredefinedType( - SyntaxFactory.Token( - memberAccess.GetLeadingTrivia(), - GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First())), - memberAccess.GetTrailingTrivia())); - - issueSpan = memberAccess.Span; - return true; - } - - // See https://github.com/dotnet/roslyn/issues/40974 - // - // To be very safe, we only support simplifying code that bound to a symbol without any - // sort of problems. We could potentially relax this in the future. However, we would - // need to be very careful about the implications of us offering to fixup 'broken' code - // in a manner that might end up making things worse or confusing the user. - var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess); - if (symbol == null) - return false; - - if (memberAccess.Expression.IsKind(SyntaxKind.ThisExpression) && - !SimplificationHelpers.ShouldSimplifyThisOrMeMemberAccessExpression(semanticModel, optionSet, symbol)) - { - return false; - } - - // if this node is on the left side, we could simplify to aliases - if (!memberAccess.IsRightSideOfDot()) - { - // Check if we need to replace this syntax with an alias identifier - if (TryReplaceExpressionWithAlias( - memberAccess, semanticModel, symbol, - cancellationToken, out var aliasReplacement)) - { - // get the token text as it appears in source code to preserve e.g. unicode character escaping - var text = aliasReplacement.Name; - var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault(); - - if (syntaxRef != null) - { - var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias.Name.Identifier; - text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString().Substring(1) : declIdentifier.ToString(); - } - - replacementNode = SyntaxFactory.IdentifierName( - memberAccess.Name.Identifier.CopyAnnotationsTo(SyntaxFactory.Identifier( - memberAccess.GetLeadingTrivia(), - SyntaxKind.IdentifierToken, - text, - aliasReplacement.Name, - memberAccess.GetTrailingTrivia()))); - - replacementNode = memberAccess.CopyAnnotationsTo(replacementNode); - replacementNode = memberAccess.Name.CopyAnnotationsTo(replacementNode); - - issueSpan = memberAccess.Span; - - // In case the alias name is the same as the last name of the alias target, we only include - // the left part of the name in the unnecessary span to Not confuse uses. - if (memberAccess.Name.Identifier.ValueText == ((IdentifierNameSyntax)replacementNode).Identifier.ValueText) - { - issueSpan = memberAccess.Expression.Span; - } - - return true; - } - - // Check if the Expression can be replaced by Predefined Type keyword - if (PreferPredefinedTypeKeywordInMemberAccess(memberAccess, optionSet, semanticModel)) - { - if (symbol != null && symbol.IsKind(SymbolKind.NamedType)) - { - var keywordKind = GetPredefinedKeywordKind(((INamedTypeSymbol)symbol).SpecialType); - if (keywordKind != SyntaxKind.None) - { - replacementNode = CreatePredefinedTypeSyntax(memberAccess, keywordKind); - - replacementNode = replacementNode - .WithAdditionalAnnotations(new SyntaxAnnotation( - nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))); - - issueSpan = memberAccess.Span; // we want to show the whole expression as unnecessary - - return true; - } - } - } - } - - // Try to eliminate cases without actually calling CanReplaceWithReducedName. For expressions of the form - // 'this.Name' or 'base.Name', no additional check here is required. - if (!memberAccess.Expression.IsKind(SyntaxKind.ThisExpression, SyntaxKind.BaseExpression)) - { - GetReplacementCandidates( - semanticModel, - memberAccess, - symbol, - out var speculativeSymbols, - out var speculativeNamespacesAndTypes); - - if (!IsReplacementCandidate(symbol, speculativeSymbols, speculativeNamespacesAndTypes)) - { - return false; - } - } - - replacementNode = memberAccess.GetNameWithTriviaMoved(); - issueSpan = memberAccess.Expression.Span; - - return CanReplaceWithReducedName( - memberAccess, replacementNode, semanticModel, symbol, cancellationToken); - } - - public static SimpleNameSyntax GetNameWithTriviaMoved(this MemberAccessExpressionSyntax memberAccess) - => memberAccess.Name - .WithLeadingTrivia(GetLeadingTriviaForSimplifiedMemberAccess(memberAccess)) - .WithTrailingTrivia(memberAccess.GetTrailingTrivia()); - - private static void GetReplacementCandidates( - SemanticModel semanticModel, - MemberAccessExpressionSyntax memberAccess, - ISymbol actualSymbol, - out ImmutableArray speculativeSymbols, - out ImmutableArray speculativeNamespacesAndTypes) - { - var containsNamespaceOrTypeSymbol = actualSymbol is INamespaceOrTypeSymbol; - var containsOtherSymbol = !containsNamespaceOrTypeSymbol; - - speculativeSymbols = containsOtherSymbol - ? semanticModel.LookupSymbols(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText) - : ImmutableArray.Empty; - speculativeNamespacesAndTypes = containsNamespaceOrTypeSymbol - ? semanticModel.LookupNamespacesAndTypes(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText) - : ImmutableArray.Empty; - } - - /// - /// Determines if and - /// together contain a superset of the symbols in . - /// - private static bool IsReplacementCandidate(ISymbol actualSymbol, ImmutableArray speculativeSymbols, ImmutableArray speculativeNamespacesAndTypes) - { - if (speculativeSymbols.IsEmpty && speculativeNamespacesAndTypes.IsEmpty) - { - return false; - } - - if (actualSymbol is object) - { - return speculativeSymbols.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance) - || speculativeNamespacesAndTypes.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance); - } - - return true; - } - - /// - /// Compares symbols by their original definition. - /// - private sealed class CandidateSymbolEqualityComparer : IEqualityComparer - { - public static CandidateSymbolEqualityComparer Instance { get; } = new CandidateSymbolEqualityComparer(); - - private CandidateSymbolEqualityComparer() - { - } - - public bool Equals(ISymbol x, ISymbol y) - { - if (x is null || y is null) - { - return x == y; - } - - return x.OriginalDefinition.Equals(y.OriginalDefinition); - } - - public int GetHashCode(ISymbol obj) - { - return obj?.OriginalDefinition.GetHashCode() ?? 0; - } - } - - private static SyntaxTriviaList GetLeadingTriviaForSimplifiedMemberAccess(MemberAccessExpressionSyntax memberAccess) - { - // We want to include any user-typed trivia that may be present between the 'Expression', 'OperatorToken' and 'Identifier' of the MemberAccessExpression. - // However, we don't want to include any elastic trivia that may have been introduced by the expander in these locations. This is to avoid triggering - // aggressive formatting. Otherwise, formatter will see this elastic trivia added by the expander and use that as a cue to introduce unnecessary blank lines - // etc. around the user's original code. - return memberAccess.GetLeadingTrivia() - .AddRange(memberAccess.Expression.GetTrailingTrivia().WithoutElasticTrivia()) - .AddRange(memberAccess.OperatorToken.LeadingTrivia.WithoutElasticTrivia()) - .AddRange(memberAccess.OperatorToken.TrailingTrivia.WithoutElasticTrivia()) - .AddRange(memberAccess.Name.GetLeadingTrivia().WithoutElasticTrivia()); - } - - private static IEnumerable WithoutElasticTrivia(this IEnumerable list) - { - return list.Where(t => !t.IsElastic()); - } - - public static bool InsideCrefReference(this ExpressionSyntax expression) - { - var crefAttribute = expression.FirstAncestorOrSelf(); - return crefAttribute != null; - } - - private static bool InsideNameOfExpression(ExpressionSyntax expression, SemanticModel semanticModel) - { - var nameOfInvocationExpr = expression.FirstAncestorOrSelf( - invocationExpr => - { - return invocationExpr.Expression is IdentifierNameSyntax identifierName && - identifierName.Identifier.Text == "nameof" && - semanticModel.GetConstantValue(invocationExpr).HasValue && - semanticModel.GetTypeInfo(invocationExpr).Type.SpecialType == SpecialType.System_String; - }); - - return nameOfInvocationExpr != null; - } - - private static bool PreferPredefinedTypeKeywordInDeclarations(NameSyntax name, OptionSet optionSet, SemanticModel semanticModel) - { - return !IsInMemberAccessContext(name) && - !InsideCrefReference(name) && - !InsideNameOfExpression(name, semanticModel) && - SimplificationHelpers.PreferPredefinedTypeKeywordInDeclarations(optionSet, semanticModel.Language); - } - - private static bool PreferPredefinedTypeKeywordInMemberAccess(ExpressionSyntax expression, OptionSet optionSet, SemanticModel semanticModel) - { - if (!SimplificationHelpers.PreferPredefinedTypeKeywordInMemberAccess(optionSet, semanticModel.Language)) - return false; - - return (IsInMemberAccessContext(expression) || InsideCrefReference(expression)) && - !InsideNameOfExpression(expression, semanticModel); - } - - public static bool IsInMemberAccessContext(this ExpressionSyntax expression) => - expression?.Parent is MemberAccessExpressionSyntax; - - private static bool IsAliasReplaceableExpression(ExpressionSyntax expression) - { - var current = expression; - while (current.IsKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax currentMember)) - { - current = currentMember.Expression; - continue; - } - - return current.IsKind(SyntaxKind.AliasQualifiedName, - SyntaxKind.IdentifierName, - SyntaxKind.GenericName, - SyntaxKind.QualifiedName); - } - - [PerformanceSensitive( - "https://github.com/dotnet/roslyn/issues/23582", - Constraint = "Most trees do not have using alias directives, so avoid the expensive " + nameof(CSharpExtensions.GetSymbolInfo) + " call for this case.")] - private static bool TryReplaceExpressionWithAlias( - ExpressionSyntax node, SemanticModel semanticModel, - ISymbol symbol, CancellationToken cancellationToken, out IAliasSymbol aliasReplacement) - { - aliasReplacement = null; - - if (!IsAliasReplaceableExpression(node)) - return false; - - // Avoid the TryReplaceWithAlias algorithm if the tree has no using alias directives. Since the input node - // might be a speculative node (not fully rooted in a tree), we use the original semantic model to find the - // equivalent node in the original tree, and from there determine if the tree has any using alias - // directives. - var originalModel = semanticModel.GetOriginalSemanticModel(); - - // Perf: We are only using the syntax tree root in a fast-path syntax check. If the root is not readily - // available, it is fine to continue through the normal algorithm. - if (originalModel.SyntaxTree.TryGetRoot(out var root)) - { - if (!HasUsingAliasDirective(root)) - { - return false; - } - } - - // If the Symbol is a constructor get its containing type - if (symbol.IsConstructor()) - { - symbol = symbol.ContainingType; - } - - if (node is QualifiedNameSyntax || node is AliasQualifiedNameSyntax) - { - SyntaxAnnotation aliasAnnotationInfo = null; - - // The following condition checks if the user has used alias in the original code and - // if so the expression is replaced with the Alias - if (node is QualifiedNameSyntax qualifiedNameNode) - { - if (qualifiedNameNode.Right.Identifier.HasAnnotations(AliasAnnotation.Kind)) - { - aliasAnnotationInfo = qualifiedNameNode.Right.Identifier.GetAnnotations(AliasAnnotation.Kind).Single(); - } - } - - if (node is AliasQualifiedNameSyntax aliasQualifiedNameNode) - { - if (aliasQualifiedNameNode.Name.Identifier.HasAnnotations(AliasAnnotation.Kind)) - { - aliasAnnotationInfo = aliasQualifiedNameNode.Name.Identifier.GetAnnotations(AliasAnnotation.Kind).Single(); - } - } - - if (aliasAnnotationInfo != null) - { - var aliasName = AliasAnnotation.GetAliasName(aliasAnnotationInfo); - var aliasIdentifier = SyntaxFactory.IdentifierName(aliasName); - - var aliasTypeInfo = semanticModel.GetSpeculativeAliasInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsTypeOrNamespace); - - if (aliasTypeInfo != null) - { - aliasReplacement = aliasTypeInfo; - return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol); - } - } - } - - if (node.Kind() == SyntaxKind.IdentifierName && - semanticModel.GetAliasInfo((IdentifierNameSyntax)node, cancellationToken) != null) - { - return false; - } - - // an alias can only replace a type or namespace - if (symbol == null || - (symbol.Kind != SymbolKind.Namespace && symbol.Kind != SymbolKind.NamedType)) - { - return false; - } - - var preferAliasToQualifiedName = true; - if (node is QualifiedNameSyntax qualifiedName) - { - if (!qualifiedName.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation)) - { - var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; - if (type != null) - { - var keywordKind = GetPredefinedKeywordKind(type.SpecialType); - if (keywordKind != SyntaxKind.None) - { - preferAliasToQualifiedName = false; - } - } - } - } - - if (node is AliasQualifiedNameSyntax aliasQualifiedNameSyntax) - { - if (!aliasQualifiedNameSyntax.Name.HasAnnotation(Simplifier.SpecialTypeAnnotation)) - { - var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; - if (type != null) - { - var keywordKind = GetPredefinedKeywordKind(type.SpecialType); - if (keywordKind != SyntaxKind.None) - { - preferAliasToQualifiedName = false; - } - } - } - } - - aliasReplacement = GetAliasForSymbol((INamespaceOrTypeSymbol)symbol, node.GetFirstToken(), semanticModel, cancellationToken); - if (aliasReplacement != null && preferAliasToQualifiedName) - { - return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol); - } - - return false; - } - - private static bool HasUsingAliasDirective(SyntaxNode syntax) - { - SyntaxList usings; - SyntaxList members; - if (syntax.IsKind(SyntaxKind.NamespaceDeclaration, out NamespaceDeclarationSyntax namespaceDeclaration)) - { - usings = namespaceDeclaration.Usings; - members = namespaceDeclaration.Members; - } - else if (syntax.IsKind(SyntaxKind.CompilationUnit, out CompilationUnitSyntax compilationUnit)) - { - usings = compilationUnit.Usings; - members = compilationUnit.Members; - } - else - { - return false; - } - - foreach (var usingDirective in usings) - { - if (usingDirective.Alias != null) - { - return true; - } - } - - foreach (var member in members) - { - if (HasUsingAliasDirective(member)) - { - return true; - } - } - - return false; - } - - // We must verify that the alias actually binds back to the thing it's aliasing. - // It's possible there's another symbol with the same name as the alias that binds - // first - private static bool ValidateAliasForTarget(IAliasSymbol aliasReplacement, SemanticModel semanticModel, ExpressionSyntax node, ISymbol symbol) - { - var aliasName = aliasReplacement.Name; - - // If we're the argument of a nameof(X.Y) call, then we can't simplify to an - // alias unless the alias has the same name as us (i.e. 'Y'). - if (node.IsNameOfArgumentExpression()) - { - var nameofValueOpt = semanticModel.GetConstantValue(node.Parent.Parent.Parent); - if (!nameofValueOpt.HasValue) - { - return false; - } - - if (nameofValueOpt.Value is string existingVal && - existingVal != aliasName) - { - return false; - } - } - - var boundSymbols = semanticModel.LookupNamespacesAndTypes(node.SpanStart, name: aliasName); - - if (boundSymbols.Length == 1) - { - if (boundSymbols[0] is IAliasSymbol boundAlias && aliasReplacement.Target.Equals(symbol)) - { - return true; - } - } - - return false; - } - - public static bool IsNameOfArgumentExpression(this ExpressionSyntax expression) - { - return expression.IsParentKind(SyntaxKind.Argument) && - expression.Parent.IsParentKind(SyntaxKind.ArgumentList) && - expression.Parent.Parent.Parent is InvocationExpressionSyntax invocation && - invocation.IsNameOfInvocation(); - } - - public static bool IsNameOfInvocation(this InvocationExpressionSyntax invocation) - { - return invocation.Expression is IdentifierNameSyntax identifierName && - identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.NameOfKeyword); - } - - public static IAliasSymbol GetAliasForSymbol(INamespaceOrTypeSymbol symbol, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var originalSemanticModel = semanticModel.GetOriginalSemanticModel(); - if (!originalSemanticModel.SyntaxTree.HasCompilationUnitRoot) - { - return null; - } - - var namespaceId = GetNamespaceIdForAliasSearch(semanticModel, token, cancellationToken); - if (namespaceId < 0) - { - return null; - } - - if (!AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId, symbol, out var aliasSymbol)) - { - // add cache - AliasSymbolCache.AddAliasSymbols(originalSemanticModel, namespaceId, semanticModel.LookupNamespacesAndTypes(token.SpanStart).OfType()); - - // retry - AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId, symbol, out aliasSymbol); - } - - return aliasSymbol; - } - - private static SyntaxNode GetStartNodeForNamespaceId(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) - { - if (!semanticModel.IsSpeculativeSemanticModel) - { - return token.Parent; - } - - var originalSemanticMode = semanticModel.GetOriginalSemanticModel(); - token = originalSemanticMode.SyntaxTree.GetRoot(cancellationToken).FindToken(semanticModel.OriginalPositionForSpeculation); - - return token.Parent; - } - - private static int GetNamespaceIdForAliasSearch(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) - { - var startNode = GetStartNodeForNamespaceId(semanticModel, token, cancellationToken); - if (!startNode.SyntaxTree.HasCompilationUnitRoot) - { - return -1; - } - - // NOTE: If we're currently in a block of usings, then we want to collect the - // aliases that are higher up than this block. Using aliases declared in a block of - // usings are not usable from within that same block. - var usingDirective = startNode.GetAncestorOrThis(); - if (usingDirective != null) - { - startNode = usingDirective.Parent.Parent; - if (startNode == null) - { - return -1; - } - } - - // check whether I am under a namespace - var @namespace = startNode.GetAncestorOrThis(); - if (@namespace != null) - { - // since we have node inside of the root, root should be already there - // search for namespace id should be quite cheap since normally there should be - // only a few namespace defined in a source file if it is not 1. that is why it is - // not cached. - var startIndex = 1; - return GetNamespaceId(startNode.SyntaxTree.GetRoot(cancellationToken), @namespace, ref startIndex); - } - - // no namespace, under compilation unit directly - return 0; - } - - private static int GetNamespaceId(SyntaxNode container, NamespaceDeclarationSyntax target, ref int index) - { - if (container is CompilationUnitSyntax compilation) - { - return GetNamespaceId(compilation.Members, target, ref index); - } - - if (container is NamespaceDeclarationSyntax @namespace) - { - return GetNamespaceId(@namespace.Members, target, ref index); - } - - return Contract.FailWithReturn("shouldn't reach here"); - } - - private static int GetNamespaceId(SyntaxList members, NamespaceDeclarationSyntax target, ref int index) - { - foreach (var member in members) - { - if (!(member is NamespaceDeclarationSyntax childNamespace)) - { - continue; - } - - if (childNamespace == target) - { - return index; - } - - index++; - var result = GetNamespaceId(childNamespace, target, ref index); - if (result > 0) - { - return result; - } - } - - return -1; - } - - private static bool TryReduceName( - NameSyntax name, - SemanticModel semanticModel, - out TypeSyntax replacementNode, - out TextSpan issueSpan, - OptionSet optionSet, - CancellationToken cancellationToken) - { - replacementNode = null; - issueSpan = default; - - if (name.IsVar) - { - return false; - } - - // we should not simplify a name of a namespace declaration - if (IsPartOfNamespaceDeclarationName(name)) - { - return false; - } - - // We can simplify Qualified names and AliasQualifiedNames. Generally, if we have - // something like "A.B.C.D", we only consider the full thing something we can simplify. - // However, in the case of "A.B.C<>.D", then we'll only consider simplifying up to the - // first open name. This is because if we remove the open name, we'll often change - // meaning as "D" will bind to C.D which is different than C<>.D! - if (name is QualifiedNameSyntax qualifiedName) - { - var left = qualifiedName.Left; - if (ContainsOpenName(left)) - { - // Don't simplify A.B<>.C - return false; - } - } - - // 1. see whether binding the name binds to a symbol/type. if not, it is ambiguous and - // nothing we can do here. - var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, name); - if (symbol == null) - { - return false; - } - - // treat constructor names as types - var method = symbol as IMethodSymbol; - if (method.IsConstructor()) - { - symbol = method.ContainingType; - } - - if (symbol.Kind == SymbolKind.Method && name.Kind() == SyntaxKind.GenericName) - { - var genericName = (GenericNameSyntax)name; - replacementNode = SyntaxFactory.IdentifierName(genericName.Identifier) - .WithLeadingTrivia(genericName.GetLeadingTrivia()) - .WithTrailingTrivia(genericName.GetTrailingTrivia()); - - issueSpan = genericName.TypeArgumentList.Span; - return CanReplaceWithReducedName( - name, replacementNode, semanticModel, cancellationToken); - } - - if (!(symbol is INamespaceOrTypeSymbol)) - { - return false; - } - - if (name.HasAnnotations(SpecialTypeAnnotation.Kind)) - { - replacementNode = SyntaxFactory.PredefinedType( - SyntaxFactory.Token( - name.GetLeadingTrivia(), - GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(name.GetAnnotations(SpecialTypeAnnotation.Kind).First())), - name.GetTrailingTrivia())); - - issueSpan = name.Span; - - return name.CanReplaceWithReducedNameInContext(replacementNode, semanticModel); - } - else - { - if (!name.IsRightSideOfDotOrColonColon()) - { - if (TryReplaceExpressionWithAlias(name, semanticModel, symbol, cancellationToken, out var aliasReplacement)) - { - // get the token text as it appears in source code to preserve e.g. Unicode character escaping - var text = aliasReplacement.Name; - var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault(); - - if (syntaxRef != null) - { - var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias.Name.Identifier; - text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString().Substring(1) : declIdentifier.ToString(); - } - - var identifierToken = SyntaxFactory.Identifier( - name.GetLeadingTrivia(), - SyntaxKind.IdentifierToken, - text, - aliasReplacement.Name, - name.GetTrailingTrivia()); - - identifierToken = CSharpSimplificationService.TryEscapeIdentifierToken(identifierToken, name, semanticModel); - replacementNode = SyntaxFactory.IdentifierName(identifierToken); - - // Merge annotation to new syntax node - var annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(RenameAnnotation.Kind); - foreach (var annotatedNodeOrToken in annotatedNodesOrTokens) - { - if (annotatedNodeOrToken.IsToken) - { - identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken); - } - else - { - replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode); - } - } - - annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(AliasAnnotation.Kind); - foreach (var annotatedNodeOrToken in annotatedNodesOrTokens) - { - if (annotatedNodeOrToken.IsToken) - { - identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken); - } - else - { - replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode); - } - } - - replacementNode = ((SimpleNameSyntax)replacementNode).WithIdentifier(identifierToken); - issueSpan = name.Span; - - // In case the alias name is the same as the last name of the alias target, we only include - // the left part of the name in the unnecessary span to Not confuse uses. - if (name.IsKind(SyntaxKind.QualifiedName, out QualifiedNameSyntax qualifiedName3)) - { - if (qualifiedName3.Right.Identifier.ValueText == identifierToken.ValueText) - { - issueSpan = qualifiedName3.Left.Span; - } - } - - // first check if this would be a valid reduction - if (name.CanReplaceWithReducedNameInContext(replacementNode, semanticModel)) - { - // in case this alias name ends with "Attribute", we're going to see if we can also - // remove that suffix. - if (TryReduceAttributeSuffix( - name, - identifierToken, - out var replacementNodeWithoutAttributeSuffix, - out var issueSpanWithoutAttributeSuffix)) - { - if (CanReplaceWithReducedName(name, replacementNodeWithoutAttributeSuffix, semanticModel, cancellationToken)) - { - replacementNode = replacementNode.CopyAnnotationsTo(replacementNodeWithoutAttributeSuffix); - issueSpan = issueSpanWithoutAttributeSuffix; - } - } - - return true; - } - - return false; - } - - var nameHasNoAlias = false; - - if (name is SimpleNameSyntax simpleName) - { - if (!simpleName.Identifier.HasAnnotations(AliasAnnotation.Kind)) - { - nameHasNoAlias = true; - } - } - - if (name is QualifiedNameSyntax qualifiedName2) - { - if (!qualifiedName2.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation)) - { - nameHasNoAlias = true; - } - } - - if (name is AliasQualifiedNameSyntax aliasQualifiedName) - { - if (aliasQualifiedName.Name is SimpleNameSyntax && - !aliasQualifiedName.Name.Identifier.HasAnnotations(AliasAnnotation.Kind) && - !aliasQualifiedName.Name.HasAnnotation(Simplifier.SpecialTypeAnnotation)) - { - nameHasNoAlias = true; - } - } - - var aliasInfo = semanticModel.GetAliasInfo(name, cancellationToken); - if (nameHasNoAlias && aliasInfo == null) - { - // Don't simplify to predefined type if name is part of a QualifiedName. - // QualifiedNames can't contain PredefinedTypeNames (although MemberAccessExpressions can). - // In other words, the left side of a QualifiedName can't be a PredefinedTypeName. - var inDeclarationContext = PreferPredefinedTypeKeywordInDeclarations(name, optionSet, semanticModel); - var inMemberAccessContext = PreferPredefinedTypeKeywordInMemberAccess(name, optionSet, semanticModel); - - if (!name.Parent.IsKind(SyntaxKind.QualifiedName) && (inDeclarationContext || inMemberAccessContext)) - { - // See if we can simplify this name (like System.Int32) to a built-in type (like 'int'). - // If not, we'll still fall through and see if we can convert it to Int32. - - var codeStyleOptionName = inDeclarationContext - ? nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInDeclaration) - : nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess); - - var type = semanticModel.GetTypeInfo(name, cancellationToken).Type; - if (type != null) - { - var keywordKind = GetPredefinedKeywordKind(type.SpecialType); - if (keywordKind != SyntaxKind.None && - CanReplaceWithPredefinedTypeKeywordInContext(name, semanticModel, out replacementNode, ref issueSpan, keywordKind, codeStyleOptionName)) - { - return true; - } - } - else - { - var typeSymbol = semanticModel.GetSymbolInfo(name, cancellationToken).Symbol; - if (typeSymbol.IsKind(SymbolKind.NamedType)) - { - var keywordKind = GetPredefinedKeywordKind(((INamedTypeSymbol)typeSymbol).SpecialType); - if (keywordKind != SyntaxKind.None && - CanReplaceWithPredefinedTypeKeywordInContext(name, semanticModel, out replacementNode, ref issueSpan, keywordKind, codeStyleOptionName)) - { - return true; - } - } - } - } - } - - // Nullable rewrite: Nullable -> int? - // Don't rewrite in the case where Nullable is part of some qualified name like Nullable.Something - if (!name.IsVar && symbol.Kind == SymbolKind.NamedType && !name.IsLeftSideOfQualifiedName()) - { - var type = (INamedTypeSymbol)symbol; - if (aliasInfo == null && CanSimplifyNullable(type, name, semanticModel)) - { - GenericNameSyntax genericName; - if (name.Kind() == SyntaxKind.QualifiedName) - { - genericName = (GenericNameSyntax)((QualifiedNameSyntax)name).Right; - } - else - { - genericName = (GenericNameSyntax)name; - } - - var oldType = genericName.TypeArgumentList.Arguments.First(); - if (oldType.Kind() == SyntaxKind.OmittedTypeArgument) - { - return false; - } - - replacementNode = SyntaxFactory.NullableType(oldType) - .WithLeadingTrivia(name.GetLeadingTrivia()) - .WithTrailingTrivia(name.GetTrailingTrivia()); - issueSpan = name.Span; - - // we need to simplify the whole qualified name at once, because replacing the identifier on the left in - // System.Nullable alone would be illegal. - // If this fails we want to continue to try at least to remove the System if possible. - if (name.CanReplaceWithReducedNameInContext(replacementNode, semanticModel)) - { - return true; - } - } - } - } - - SyntaxToken identifier; - switch (name.Kind()) - { - case SyntaxKind.AliasQualifiedName: - var simpleName = ((AliasQualifiedNameSyntax)name).Name - .WithLeadingTrivia(name.GetLeadingTrivia()); - - simpleName = simpleName.ReplaceToken(simpleName.Identifier, - ((AliasQualifiedNameSyntax)name).Name.Identifier.CopyAnnotationsTo( - simpleName.Identifier.WithLeadingTrivia( - ((AliasQualifiedNameSyntax)name).Alias.Identifier.LeadingTrivia))); - - replacementNode = simpleName; - - issueSpan = ((AliasQualifiedNameSyntax)name).Alias.Span; - - break; - - case SyntaxKind.QualifiedName: - replacementNode = ((QualifiedNameSyntax)name).Right.WithLeadingTrivia(name.GetLeadingTrivia()); - issueSpan = ((QualifiedNameSyntax)name).Left.Span; - - break; - - case SyntaxKind.IdentifierName: - identifier = ((IdentifierNameSyntax)name).Identifier; - - // we can try to remove the Attribute suffix if this is the attribute name - TryReduceAttributeSuffix(name, identifier, out replacementNode, out issueSpan); - break; - } - } - - if (replacementNode == null) - { - return false; - } - - // We may be looking at a name `X.Y` seeing if we can replace it with `Y`. However, in - // order to know for sure, we actually have to look slightly higher at `X.Y.Z` to see if - // it can simplify to `Y.Z`. This is because in the `Color Color` case we can only tell - // if we can reduce by looking by also looking at what comes next to see if it will - // cause the simplified name to bind to the instance or static side. - if (TryReduceCrefColorColor(name, replacementNode, semanticModel, cancellationToken)) - { - return true; - } - - return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken); - } - - private static bool TryReduceCrefColorColor( - NameSyntax name, TypeSyntax replacement, - SemanticModel semanticModel, CancellationToken cancellationToken) - { - if (!InsideCrefReference(name)) - return false; - - if (name.Parent is QualifiedCrefSyntax qualifiedCrefParent && qualifiedCrefParent.Container == name) - { - // we have and we're trying to see if we can replace - // A.B.C with C. In this case the parent of A.B.C is A.B.C.D which is a - // QualifiedCrefSyntax - - var qualifiedReplacement = SyntaxFactory.QualifiedCref(replacement, qualifiedCrefParent.Member); - if (qualifiedCrefParent.TryReduceOrSimplifyQualifiedCref( - semanticModel, qualifiedReplacement, out _, out _, cancellationToken)) - { - return true; - } - } - else if (name.Parent is QualifiedNameSyntax qualifiedParent && qualifiedParent.Left == name && - replacement is NameSyntax replacementName) - { - // we have and we're trying to see if we can replace - // A.B with B. In this case the parent of A.B is A.B.C which is a - // QualifiedNameSyntax - - var qualifiedReplacement = SyntaxFactory.QualifiedName(replacementName, qualifiedParent.Right); - return CanReplaceWithReducedName( - qualifiedParent, qualifiedReplacement, semanticModel, cancellationToken); - } - - return false; - } - - private static bool CanSimplifyNullable(INamedTypeSymbol type, NameSyntax name, SemanticModel semanticModel) - { - if (!type.IsNullable()) - { - return false; - } - - if (type.IsUnboundGenericType) - { - // Don't simplify unbound generic type "Nullable<>". - return false; - } - - if (InsideNameOfExpression(name, semanticModel)) - { - // Nullable can't be simplified to T? in nameof expressions. - return false; - } - - if (!InsideCrefReference(name)) - { - // Nullable can always be simplified to T? outside crefs. - return true; - } - - if (name.Parent is NameMemberCrefSyntax) - return false; - - // Inside crefs, if the T in this Nullable{T} is being declared right here - // then this Nullable{T} is not a constructed generic type and we should - // not offer to simplify this to T?. - // - // For example, we should not offer the simplification in the following cases where - // T does not bind to an existing type / type parameter in the user's code. - // - - // - - // - // And we should offer the simplification in the following cases where SomeType and - // SomeMethod bind to a type and method declared elsewhere in the users code. - // - - - var argument = type.TypeArguments.SingleOrDefault(); - if (argument == null || argument.IsErrorType()) - { - return false; - } - - var argumentDecl = argument.DeclaringSyntaxReferences.FirstOrDefault(); - if (argumentDecl == null) - { - // The type argument is a type from metadata - so this is a constructed generic nullable type that can be simplified (e.g. Nullable(Of Integer)). - return true; - } - - return !name.Span.Contains(argumentDecl.Span); - } - - private static bool CanReplaceWithPredefinedTypeKeywordInContext( - NameSyntax name, - SemanticModel semanticModel, - out TypeSyntax replacementNode, - ref TextSpan issueSpan, - SyntaxKind keywordKind, - string codeStyleOptionName) - { - replacementNode = CreatePredefinedTypeSyntax(name, keywordKind); - - issueSpan = name.Span; // we want to show the whole name expression as unnecessary - - var canReduce = name.CanReplaceWithReducedNameInContext(replacementNode, semanticModel); - - if (canReduce) - { - replacementNode = replacementNode.WithAdditionalAnnotations(new SyntaxAnnotation(codeStyleOptionName)); - } - - return canReduce; - } - - private static TypeSyntax CreatePredefinedTypeSyntax(ExpressionSyntax expression, SyntaxKind keywordKind) - { - return SyntaxFactory.PredefinedType(SyntaxFactory.Token(expression.GetLeadingTrivia(), keywordKind, expression.GetTrailingTrivia())); - } - - private static bool TryReduceAttributeSuffix( - NameSyntax name, - SyntaxToken identifierToken, - out TypeSyntax replacementNode, - out TextSpan issueSpan) - { - issueSpan = default; - replacementNode = default; - - // we can try to remove the Attribute suffix if this is the attribute name - if (SyntaxFacts.IsAttributeName(name)) - { - if (name.Parent.Kind() == SyntaxKind.Attribute || name.IsRightSideOfDotOrColonColon()) - { - const string AttributeName = "Attribute"; - - // an attribute that should keep it (unnecessary "Attribute" suffix should be annotated with a DontSimplifyAnnotation - if (identifierToken.ValueText != AttributeName && identifierToken.ValueText.EndsWith(AttributeName, StringComparison.Ordinal) && !identifierToken.HasAnnotation(SimplificationHelpers.DontSimplifyAnnotation)) - { - // weird. the semantic model is able to bind attribute syntax like "[as()]" although it's not valid code. - // so we need another check for keywords manually. - var newAttributeName = identifierToken.ValueText.Substring(0, identifierToken.ValueText.Length - 9); - if (SyntaxFacts.GetKeywordKind(newAttributeName) != SyntaxKind.None) - { - return false; - } - - // if this attribute name in source contained Unicode escaping, we will loose it now - // because there is no easy way to determine the substring from identifier->ToString() - // which would be needed to pass to SyntaxFactory.Identifier - // The result is an unescaped Unicode character in source. - - // once we remove the Attribute suffix, we can't use an escaped identifier - var newIdentifierToken = identifierToken.CopyAnnotationsTo( - SyntaxFactory.Identifier( - identifierToken.LeadingTrivia, - newAttributeName, - identifierToken.TrailingTrivia)); - - replacementNode = SyntaxFactory.IdentifierName(newIdentifierToken) - .WithLeadingTrivia(name.GetLeadingTrivia()); - issueSpan = new TextSpan(identifierToken.Span.End - 9, 9); - - return true; - } - } - } - - return false; - } - - /// - /// Checks if the SyntaxNode is a name of a namespace declaration. To be a namespace name, the syntax - /// must be parented by an namespace declaration and the node itself must be equal to the declaration's Name - /// property. - /// - /// - /// - private static bool IsPartOfNamespaceDeclarationName(SyntaxNode node) - { - var parent = node; - - while (parent != null) - { - switch (parent.Kind()) - { - case SyntaxKind.IdentifierName: - case SyntaxKind.QualifiedName: - node = parent; - parent = parent.Parent; - break; - - case SyntaxKind.NamespaceDeclaration: - var namespaceDeclaration = (NamespaceDeclarationSyntax)parent; - return object.Equals(namespaceDeclaration.Name, node); - - default: - return false; - } - } - - return false; - } - - private static bool TrySimplify( - ExpressionSyntax expression, - SemanticModel semanticModel, - out ExpressionSyntax replacementNode, - out TextSpan issueSpan, - CancellationToken cancellationToken) - { - replacementNode = null; - issueSpan = default; - - switch (expression.Kind()) - { - case SyntaxKind.SimpleMemberAccessExpression: - { - var memberAccess = (MemberAccessExpressionSyntax)expression; - if (IsMemberAccessADynamicInvocation(memberAccess, semanticModel)) - { - return false; - } - - if (TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, out var newLeft, out issueSpan)) - { - // replacement node might not be in it's simplest form, so add simplify annotation to it. - replacementNode = memberAccess.Update(newLeft, memberAccess.OperatorToken, memberAccess.Name) - .WithAdditionalAnnotations(Simplifier.Annotation); - - // Ensure that replacement doesn't change semantics. - return !ReplacementChangesSemantics(memberAccess, replacementNode, semanticModel, cancellationToken); - } - - return false; - } - - case SyntaxKind.QualifiedName: - { - var qualifiedName = (QualifiedNameSyntax)expression; - if (TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, out var newLeft, out issueSpan)) - { - // replacement node might not be in it's simplest form, so add simplify annotation to it. - replacementNode = qualifiedName.Update((NameSyntax)newLeft, qualifiedName.DotToken, qualifiedName.Right) - .WithAdditionalAnnotations(Simplifier.Annotation); - - // Ensure that replacement doesn't change semantics. - return !ReplacementChangesSemantics(qualifiedName, replacementNode, semanticModel, cancellationToken); - } - - return false; - } - } - - return false; - } - - private static bool ReplacementChangesSemantics( - ExpressionSyntax originalExpression, ExpressionSyntax replacedExpression, - SemanticModel semanticModel, CancellationToken cancellationToken) - { - var speculationAnalyzer = new SpeculationAnalyzer(originalExpression, replacedExpression, semanticModel, cancellationToken); - return speculationAnalyzer.ReplacementChangesSemantics(); - } - - // Note: The caller needs to verify that replacement doesn't change semantics of the original expression. - private static bool TrySimplifyMemberAccessOrQualifiedName( - ExpressionSyntax left, - ExpressionSyntax right, - SemanticModel semanticModel, - out ExpressionSyntax replacementNode, - out TextSpan issueSpan) - { - replacementNode = null; - issueSpan = default; - - if (left != null && right != null) - { - var leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left); - if (leftSymbol != null && leftSymbol.Kind == SymbolKind.NamedType) - { - var rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right); - if (rightSymbol != null && (rightSymbol.IsStatic || rightSymbol.Kind == SymbolKind.NamedType)) - { - // Static member access or nested type member access. - var containingType = rightSymbol.ContainingType; - - var enclosingSymbol = semanticModel.GetEnclosingSymbol(left.SpanStart); - var enclosingTypeParametersInsideOut = new List(); - - while (enclosingSymbol != null) - { - if (enclosingSymbol is IMethodSymbol methodSymbol) - { - if (methodSymbol.TypeArguments.Length != 0) - { - enclosingTypeParametersInsideOut.AddRange(methodSymbol.TypeArguments); - } - } - - if (enclosingSymbol is INamedTypeSymbol namedTypeSymbol) - { - if (namedTypeSymbol.TypeArguments.Length != 0) - { - enclosingTypeParametersInsideOut.AddRange(namedTypeSymbol.TypeArguments); - } - } - - enclosingSymbol = enclosingSymbol.ContainingSymbol; - } - - if (containingType != null && !containingType.Equals(leftSymbol)) - { - if (leftSymbol is INamedTypeSymbol namedType && - containingType.TypeArguments.Length != 0) - { - return false; - } - - // We have a static member access or a nested type member access using a more derived type. - // Simplify syntax so as to use accessed member's most immediate containing type instead of the derived type. - replacementNode = containingType.GenerateTypeSyntax() - .WithLeadingTrivia(left.GetLeadingTrivia()) - .WithTrailingTrivia(left.GetTrailingTrivia()); - issueSpan = left.Span; - return true; - } - } - } - } - - return false; - } - - private static bool CanReplaceWithReducedName( - MemberAccessExpressionSyntax memberAccess, - ExpressionSyntax reducedName, - SemanticModel semanticModel, - ISymbol symbol, - CancellationToken cancellationToken) - { - if (!IsThisOrTypeOrNamespace(memberAccess, semanticModel)) - { - return false; - } - - var speculationAnalyzer = new SpeculationAnalyzer(memberAccess, reducedName, semanticModel, cancellationToken); - if (!speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() || - speculationAnalyzer.ReplacementChangesSemantics()) - { - return false; - } - - if (WillConflictWithExistingLocal(memberAccess, reducedName, semanticModel)) - { - return false; - } - - if (IsMemberAccessADynamicInvocation(memberAccess, semanticModel)) - { - return false; - } - - if (AccessMethodWithDynamicArgumentInsideStructConstructor(memberAccess, semanticModel)) - { - return false; - } - - if (memberAccess.Expression.Kind() == SyntaxKind.BaseExpression) - { - var enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken); - if (enclosingNamedType != null && - !enclosingNamedType.IsSealed && - symbol != null && - symbol.IsOverridable()) - { - return false; - } - } - - var invalidTransformation1 = ParserWouldTreatExpressionAsCast(reducedName, memberAccess); - - return !invalidTransformation1; - } - - private static bool ParserWouldTreatExpressionAsCast(ExpressionSyntax reducedNode, MemberAccessExpressionSyntax originalNode) - { - SyntaxNode parent = originalNode; - while (parent != null) - { - if (parent.IsParentKind(SyntaxKind.SimpleMemberAccessExpression)) - { - parent = parent.Parent; - continue; - } - - if (!parent.IsParentKind(SyntaxKind.ParenthesizedExpression)) - { - return false; - } - - break; - } - - var newExpression = parent.ReplaceNode(originalNode, reducedNode); - - // detect cast ambiguities according to C# spec #7.7.6 - if (IsNameOrMemberAccessButNoExpression(newExpression)) - { - var nextToken = parent.Parent.GetLastToken().GetNextToken(); - - return nextToken.Kind() == SyntaxKind.OpenParenToken || - nextToken.Kind() == SyntaxKind.TildeToken || - nextToken.Kind() == SyntaxKind.ExclamationToken || - (SyntaxFacts.IsKeywordKind(nextToken.Kind()) && !(nextToken.Kind() == SyntaxKind.AsKeyword || nextToken.Kind() == SyntaxKind.IsKeyword)); - } - - return false; - } - - private static bool IsNameOrMemberAccessButNoExpression(SyntaxNode node) - { - if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax memberAccess)) - { - return memberAccess.Expression.IsKind(SyntaxKind.IdentifierName) || - IsNameOrMemberAccessButNoExpression(memberAccess.Expression); - } - - return node.IsKind(SyntaxKind.IdentifierName); - } - - /// - /// Tells if the Member access is the starting part of a Dynamic Invocation - /// - /// - /// - /// Return true, if the member access is the starting point of a Dynamic Invocation - private static bool IsMemberAccessADynamicInvocation(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) - { - var ancestorInvocation = memberAccess.FirstAncestorOrSelf(); - - if (ancestorInvocation != null && ancestorInvocation.SpanStart == memberAccess.SpanStart) - { - var typeInfo = semanticModel.GetTypeInfo(ancestorInvocation); - if (typeInfo.Type != null && - typeInfo.Type.Kind == SymbolKind.DynamicType) - { - return true; - } - } - - return false; - } - - /* - * Name Reduction, to implicitly mean "this", is possible only after the initialization of all member variables but - * since the check for initialization of all member variable is a lot of work for this simplification we don't simplify - * even if all the member variables are initialized - */ - private static bool AccessMethodWithDynamicArgumentInsideStructConstructor(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) - { - var constructor = memberAccess.Ancestors().OfType().SingleOrDefault(); - - if (constructor == null || constructor.Parent.Kind() != SyntaxKind.StructDeclaration) - { - return false; - } - - return semanticModel.GetSymbolInfo(memberAccess.Name).CandidateReason == CandidateReason.LateBound; - } - - private static bool CanReplaceWithReducedName(NameSyntax name, TypeSyntax reducedName, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var speculationAnalyzer = new SpeculationAnalyzer(name, reducedName, semanticModel, cancellationToken); - if (speculationAnalyzer.ReplacementChangesSemantics()) - { - return false; - } - - return CanReplaceWithReducedNameInContext(name, reducedName, semanticModel); - } - - private static bool CanReplaceWithReducedNameInContext( - this NameSyntax name, TypeSyntax reducedName, SemanticModel semanticModel) - { - // Check for certain things that would prevent us from reducing this name in this context. - // For example, you can simplify "using a = System.Int32" to "using a = int" as it's simply - // not allowed in the C# grammar. - - if (IsNonNameSyntaxInUsingDirective(name, reducedName) || - WillConflictWithExistingLocal(name, reducedName, semanticModel) || - IsAmbiguousCast(name, reducedName) || - IsNullableTypeInPointerExpression(reducedName) || - IsNotNullableReplaceable(name, reducedName) || - IsNonReducableQualifiedNameInUsingDirective(semanticModel, name)) - { - return false; - } - - return true; - } - - private static bool IsNonReducableQualifiedNameInUsingDirective(SemanticModel model, NameSyntax name) - { - // Whereas most of the time we do not want to reduce namespace names, We will - // make an exception for namespaces with the global:: alias. - return IsQualifiedNameInUsingDirective(model, name) && - !IsGlobalAliasQualifiedName(name); - } - - private static bool IsQualifiedNameInUsingDirective(SemanticModel model, NameSyntax name) - { - while (name.IsLeftSideOfQualifiedName()) - { - name = (NameSyntax)name.Parent; - } - - if (name.IsParentKind(SyntaxKind.UsingDirective) && - ((UsingDirectiveSyntax)name.Parent).Alias == null) - { - // We're a qualified name in a using. We don't want to reduce this name as people like - // fully qualified names in usings so they can properly tell what the name is resolving - // to. - // However, if this name is actually referencing the special Script class, then we do - // want to allow that to be reduced. - - return !IsInScriptClass(model, name); - } - - return false; - } - - private static bool IsGlobalAliasQualifiedName(NameSyntax name) - { - // Checks whether the `global::` alias is applied to the name - return name is AliasQualifiedNameSyntax aliasName && - aliasName.Alias.Identifier.IsKind(SyntaxKind.GlobalKeyword); - } - - private static bool IsInScriptClass(SemanticModel model, NameSyntax name) - { - var symbol = model.GetSymbolInfo(name).Symbol as INamedTypeSymbol; - while (symbol != null) - { - if (symbol.IsScriptClass) - { - return true; - } - - symbol = symbol.ContainingType; - } - - return false; - } - - private static bool IsNotNullableReplaceable(NameSyntax name, TypeSyntax reducedName) - { - if (reducedName.IsKind(SyntaxKind.NullableType, out NullableTypeSyntax nullableType)) - { - if (nullableType.ElementType.Kind() == SyntaxKind.OmittedTypeArgument) - return true; - - return name.IsLeftSideOfDot() || name.IsRightSideOfDot(); - } - - return false; - } - - private static bool IsThisOrTypeOrNamespace(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) - { - if (memberAccess.Expression.Kind() == SyntaxKind.ThisExpression) - { - var previousToken = memberAccess.Expression.GetFirstToken().GetPreviousToken(); - - var symbol = semanticModel.GetSymbolInfo(memberAccess.Name).Symbol; - - if (previousToken.Kind() == SyntaxKind.OpenParenToken && - previousToken.Parent.IsKind(SyntaxKind.ParenthesizedExpression) && - !previousToken.Parent.IsParentKind(SyntaxKind.ParenthesizedExpression) && - ((ParenthesizedExpressionSyntax)previousToken.Parent).Expression.Kind() == SyntaxKind.SimpleMemberAccessExpression && - symbol != null && symbol.Kind == SymbolKind.Method) - { - return false; - } - - return true; - } - - var expressionInfo = semanticModel.GetSymbolInfo(memberAccess.Expression); - if (SimplificationHelpers.IsValidSymbolInfo(expressionInfo.Symbol)) - { - if (expressionInfo.Symbol is INamespaceOrTypeSymbol) - { - return true; - } - - if (expressionInfo.Symbol.IsThisParameter()) - { - return true; - } - } - - return false; - } - - private static bool ContainsOpenName(NameSyntax name) - { - if (name is QualifiedNameSyntax qualifiedName) - { - return ContainsOpenName(qualifiedName.Left) || ContainsOpenName(qualifiedName.Right); - } - else if (name is GenericNameSyntax genericName) - { - return genericName.IsUnboundGenericName; - } - else - { - return false; - } - } - - private static bool IsNullableTypeInPointerExpression(ExpressionSyntax simplifiedNode) - { - // Note: nullable type syntax is not allowed in pointer type syntax - if (simplifiedNode.Kind() == SyntaxKind.NullableType && - simplifiedNode.DescendantNodes().Any(n => n is PointerTypeSyntax)) - { - return true; - } - - return false; - } - - private static bool IsNonNameSyntaxInUsingDirective(ExpressionSyntax expression, ExpressionSyntax simplifiedNode) - { - return - expression.IsParentKind(SyntaxKind.UsingDirective) && - !(simplifiedNode is NameSyntax); - } - - private static bool WillConflictWithExistingLocal( - ExpressionSyntax expression, ExpressionSyntax simplifiedNode, SemanticModel semanticModel) - { - if (simplifiedNode is IdentifierNameSyntax identifierName && - !SyntaxFacts.IsInNamespaceOrTypeContext(expression)) - { - var symbols = semanticModel.LookupSymbols(expression.SpanStart, name: identifierName.Identifier.ValueText); - return symbols.Any(s => s is ILocalSymbol); - } - - return false; - } - - private static bool IsAmbiguousCast(ExpressionSyntax expression, ExpressionSyntax simplifiedNode) + public static bool IsNameOfArgumentExpression(this ExpressionSyntax expression) { - // Can't simplify a type name in a cast expression if it would then cause the cast to be - // parsed differently. For example: (Goo::Bar)+1 is a cast. But if that simplifies to - // (Bar)+1 then that's an arithmetic expression. - if (expression.IsParentKind(SyntaxKind.CastExpression)) - { - var castExpression = (CastExpressionSyntax)expression.Parent; - if (castExpression.Type == expression) - { - var newCastExpression = castExpression.ReplaceNode(castExpression.Type, simplifiedNode); - var reparsedCastExpression = SyntaxFactory.ParseExpression(newCastExpression.ToString()); - - if (!reparsedCastExpression.IsKind(SyntaxKind.CastExpression)) - { - return true; - } - } - } - - return false; + return expression.IsParentKind(SyntaxKind.Argument) && + expression.Parent.IsParentKind(SyntaxKind.ArgumentList) && + expression.Parent.Parent.Parent is InvocationExpressionSyntax invocation && + invocation.IsNameOfInvocation(); } - private static SyntaxNode FindImmediatelyEnclosingLocalVariableDeclarationSpace(SyntaxNode syntax) + public static bool IsNameOfInvocation(this InvocationExpressionSyntax invocation) { - for (var declSpace = syntax; declSpace != null; declSpace = declSpace.Parent) - { - switch (declSpace.Kind()) - { - // These are declaration-space-defining syntaxes, by the spec: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.IndexerDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.Block: - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.SwitchStatement: - case SyntaxKind.ForEachKeyword: - case SyntaxKind.ForStatement: - case SyntaxKind.UsingStatement: - - // SPEC VIOLATION: We also want to stop walking out if, say, we are in a field - // initializer. Technically according to the wording of the spec it should be - // legal to use a simple name inconsistently inside a field initializer because - // it does not define a local variable declaration space. In practice of course - // we want to check for that. (As the native compiler does as well.) - - case SyntaxKind.FieldDeclaration: - return declSpace; - } - } - - return null; + return invocation.Expression is IdentifierNameSyntax identifierName && + identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.NameOfKeyword); } - /// - /// Returns the predefined keyword kind for a given . - /// - /// The of this type. - /// The keyword kind for a given special type, or SyntaxKind.None if the type name is not a predefined type. - public static SyntaxKind GetPredefinedKeywordKind(SpecialType specialType) - => specialType switch - { - SpecialType.System_Boolean => SyntaxKind.BoolKeyword, - SpecialType.System_Byte => SyntaxKind.ByteKeyword, - SpecialType.System_SByte => SyntaxKind.SByteKeyword, - SpecialType.System_Int32 => SyntaxKind.IntKeyword, - SpecialType.System_UInt32 => SyntaxKind.UIntKeyword, - SpecialType.System_Int16 => SyntaxKind.ShortKeyword, - SpecialType.System_UInt16 => SyntaxKind.UShortKeyword, - SpecialType.System_Int64 => SyntaxKind.LongKeyword, - SpecialType.System_UInt64 => SyntaxKind.ULongKeyword, - SpecialType.System_Single => SyntaxKind.FloatKeyword, - SpecialType.System_Double => SyntaxKind.DoubleKeyword, - SpecialType.System_Decimal => SyntaxKind.DecimalKeyword, - SpecialType.System_String => SyntaxKind.StringKeyword, - SpecialType.System_Char => SyntaxKind.CharKeyword, - SpecialType.System_Object => SyntaxKind.ObjectKeyword, - SpecialType.System_Void => SyntaxKind.VoidKeyword, - _ => SyntaxKind.None, - }; - public static SimpleNameSyntax GetRightmostName(this ExpressionSyntax node) { if (node is MemberAccessExpressionSyntax memberAccess && memberAccess.Name != null) @@ -2692,5 +985,11 @@ private static StatementSyntax ConvertToStatement(ExpressionSyntax expression, S .WithSemicolonToken(semicolonToken); } } + + public static bool IsDirectChildOfMemberAccessExpression(this ExpressionSyntax expression) => + expression?.Parent is MemberAccessExpressionSyntax; + + public static bool InsideCrefReference(this ExpressionSyntax expression) + => expression.FirstAncestorOrSelf() != null; } } diff --git a/src/Workspaces/CSharp/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.cs new file mode 100644 index 0000000000000..191b542d3afe2 --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.cs @@ -0,0 +1,36 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CSharp.Extensions +{ + internal static class MemberAccessExpressionSyntaxExtensions + { + public static SimpleNameSyntax GetNameWithTriviaMoved(this MemberAccessExpressionSyntax memberAccess) + => memberAccess.Name + .WithLeadingTrivia(GetLeadingTriviaForSimplifiedMemberAccess(memberAccess)) + .WithTrailingTrivia(memberAccess.GetTrailingTrivia()); + + private static SyntaxTriviaList GetLeadingTriviaForSimplifiedMemberAccess(MemberAccessExpressionSyntax memberAccess) + { + // We want to include any user-typed trivia that may be present between the 'Expression', 'OperatorToken' and 'Identifier' of the MemberAccessExpression. + // However, we don't want to include any elastic trivia that may have been introduced by the expander in these locations. This is to avoid triggering + // aggressive formatting. Otherwise, formatter will see this elastic trivia added by the expander and use that as a cue to introduce unnecessary blank lines + // etc. around the user's original code. + return SyntaxFactory.TriviaList(WithoutElasticTrivia( + memberAccess.GetLeadingTrivia() + .AddRange(memberAccess.Expression.GetTrailingTrivia()) + .AddRange(memberAccess.OperatorToken.LeadingTrivia) + .AddRange(memberAccess.OperatorToken.TrailingTrivia) + .AddRange(memberAccess.Name.GetLeadingTrivia()))); + } + + private static IEnumerable WithoutElasticTrivia(IEnumerable list) + { + return list.Where(t => !t.IsElastic()); + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Simplification/Reducers/CSharpNameReducer.cs b/src/Workspaces/CSharp/Portable/Simplification/Reducers/CSharpNameReducer.cs index 5faae019dd115..1b30eb2984950 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/Reducers/CSharpNameReducer.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/Reducers/CSharpNameReducer.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Options; @@ -36,7 +37,9 @@ private static SyntaxNode SimplifyName( if (node.IsKind(SyntaxKind.QualifiedCref, out QualifiedCrefSyntax crefSyntax)) { - if (!crefSyntax.TryReduceOrSimplifyExplicitName(semanticModel, out var crefReplacement, out issueSpan, optionSet, cancellationToken)) + if (!QualifiedCrefSimplifier.Instance.TrySimplify( + crefSyntax, semanticModel, optionSet, + out var crefReplacement, out issueSpan, cancellationToken)) { return node; } @@ -46,7 +49,7 @@ private static SyntaxNode SimplifyName( else { var expressionSyntax = (ExpressionSyntax)node; - if (!expressionSyntax.TryReduceOrSimplifyExplicitName(semanticModel, out var expressionReplacement, out issueSpan, optionSet, cancellationToken)) + if (!ExpressionSimplifier.Instance.TrySimplify(expressionSyntax, semanticModel, optionSet, out var expressionReplacement, out issueSpan, cancellationToken)) { return node; } diff --git a/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/AbstractCSharpSimplifier.cs b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/AbstractCSharpSimplifier.cs new file mode 100644 index 0000000000000..ba1c8c6028d5b --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/AbstractCSharpSimplifier.cs @@ -0,0 +1,425 @@ +// 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.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Utilities; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Simplification.Simplifiers; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers +{ + /// + /// Contains helpers used by several simplifier subclasses. + /// + internal abstract class AbstractCSharpSimplifier + : AbstractSimplifier + where TSyntax : SyntaxNode + where TSimplifiedSyntax : SyntaxNode + { + /// + /// Returns the predefined keyword kind for a given . + /// + /// The of this type. + /// The keyword kind for a given special type, or SyntaxKind.None if the type name is not a predefined type. + protected static SyntaxKind GetPredefinedKeywordKind(SpecialType specialType) + => specialType switch + { + SpecialType.System_Boolean => SyntaxKind.BoolKeyword, + SpecialType.System_Byte => SyntaxKind.ByteKeyword, + SpecialType.System_SByte => SyntaxKind.SByteKeyword, + SpecialType.System_Int32 => SyntaxKind.IntKeyword, + SpecialType.System_UInt32 => SyntaxKind.UIntKeyword, + SpecialType.System_Int16 => SyntaxKind.ShortKeyword, + SpecialType.System_UInt16 => SyntaxKind.UShortKeyword, + SpecialType.System_Int64 => SyntaxKind.LongKeyword, + SpecialType.System_UInt64 => SyntaxKind.ULongKeyword, + SpecialType.System_Single => SyntaxKind.FloatKeyword, + SpecialType.System_Double => SyntaxKind.DoubleKeyword, + SpecialType.System_Decimal => SyntaxKind.DecimalKeyword, + SpecialType.System_String => SyntaxKind.StringKeyword, + SpecialType.System_Char => SyntaxKind.CharKeyword, + SpecialType.System_Object => SyntaxKind.ObjectKeyword, + SpecialType.System_Void => SyntaxKind.VoidKeyword, + _ => SyntaxKind.None, + }; + + [PerformanceSensitive( + "https://github.com/dotnet/roslyn/issues/23582", + Constraint = "Most trees do not have using alias directives, so avoid the expensive " + nameof(CSharpExtensions.GetSymbolInfo) + " call for this case.")] + protected static bool TryReplaceExpressionWithAlias( + ExpressionSyntax node, SemanticModel semanticModel, + ISymbol symbol, CancellationToken cancellationToken, out IAliasSymbol aliasReplacement) + { + aliasReplacement = null; + + if (!IsAliasReplaceableExpression(node)) + return false; + + // Avoid the TryReplaceWithAlias algorithm if the tree has no using alias directives. Since the input node + // might be a speculative node (not fully rooted in a tree), we use the original semantic model to find the + // equivalent node in the original tree, and from there determine if the tree has any using alias + // directives. + var originalModel = semanticModel.GetOriginalSemanticModel(); + + // Perf: We are only using the syntax tree root in a fast-path syntax check. If the root is not readily + // available, it is fine to continue through the normal algorithm. + if (originalModel.SyntaxTree.TryGetRoot(out var root)) + { + if (!HasUsingAliasDirective(root)) + { + return false; + } + } + + // If the Symbol is a constructor get its containing type + if (symbol.IsConstructor()) + { + symbol = symbol.ContainingType; + } + + if (node is QualifiedNameSyntax || node is AliasQualifiedNameSyntax) + { + SyntaxAnnotation aliasAnnotationInfo = null; + + // The following condition checks if the user has used alias in the original code and + // if so the expression is replaced with the Alias + if (node is QualifiedNameSyntax qualifiedNameNode) + { + if (qualifiedNameNode.Right.Identifier.HasAnnotations(AliasAnnotation.Kind)) + { + aliasAnnotationInfo = qualifiedNameNode.Right.Identifier.GetAnnotations(AliasAnnotation.Kind).Single(); + } + } + + if (node is AliasQualifiedNameSyntax aliasQualifiedNameNode) + { + if (aliasQualifiedNameNode.Name.Identifier.HasAnnotations(AliasAnnotation.Kind)) + { + aliasAnnotationInfo = aliasQualifiedNameNode.Name.Identifier.GetAnnotations(AliasAnnotation.Kind).Single(); + } + } + + if (aliasAnnotationInfo != null) + { + var aliasName = AliasAnnotation.GetAliasName(aliasAnnotationInfo); + var aliasIdentifier = SyntaxFactory.IdentifierName(aliasName); + + var aliasTypeInfo = semanticModel.GetSpeculativeAliasInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsTypeOrNamespace); + + if (aliasTypeInfo != null) + { + aliasReplacement = aliasTypeInfo; + return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol); + } + } + } + + if (node.Kind() == SyntaxKind.IdentifierName && + semanticModel.GetAliasInfo((IdentifierNameSyntax)node, cancellationToken) != null) + { + return false; + } + + // an alias can only replace a type or namespace + if (symbol == null || + (symbol.Kind != SymbolKind.Namespace && symbol.Kind != SymbolKind.NamedType)) + { + return false; + } + + var preferAliasToQualifiedName = true; + if (node is QualifiedNameSyntax qualifiedName) + { + if (!qualifiedName.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation)) + { + var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; + if (type != null) + { + var keywordKind = GetPredefinedKeywordKind(type.SpecialType); + if (keywordKind != SyntaxKind.None) + { + preferAliasToQualifiedName = false; + } + } + } + } + + if (node is AliasQualifiedNameSyntax aliasQualifiedNameSyntax) + { + if (!aliasQualifiedNameSyntax.Name.HasAnnotation(Simplifier.SpecialTypeAnnotation)) + { + var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; + if (type != null) + { + var keywordKind = GetPredefinedKeywordKind(type.SpecialType); + if (keywordKind != SyntaxKind.None) + { + preferAliasToQualifiedName = false; + } + } + } + } + + aliasReplacement = GetAliasForSymbol((INamespaceOrTypeSymbol)symbol, node.GetFirstToken(), semanticModel, cancellationToken); + if (aliasReplacement != null && preferAliasToQualifiedName) + { + return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol); + } + + return false; + } + + private static bool IsAliasReplaceableExpression(ExpressionSyntax expression) + { + var current = expression; + while (current.IsKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax currentMember)) + { + current = currentMember.Expression; + continue; + } + + return current.IsKind(SyntaxKind.AliasQualifiedName, + SyntaxKind.IdentifierName, + SyntaxKind.GenericName, + SyntaxKind.QualifiedName); + } + + private static bool HasUsingAliasDirective(SyntaxNode syntax) + { + SyntaxList usings; + SyntaxList members; + if (syntax.IsKind(SyntaxKind.NamespaceDeclaration, out NamespaceDeclarationSyntax namespaceDeclaration)) + { + usings = namespaceDeclaration.Usings; + members = namespaceDeclaration.Members; + } + else if (syntax.IsKind(SyntaxKind.CompilationUnit, out CompilationUnitSyntax compilationUnit)) + { + usings = compilationUnit.Usings; + members = compilationUnit.Members; + } + else + { + return false; + } + + foreach (var usingDirective in usings) + { + if (usingDirective.Alias != null) + { + return true; + } + } + + foreach (var member in members) + { + if (HasUsingAliasDirective(member)) + { + return true; + } + } + + return false; + } + + // We must verify that the alias actually binds back to the thing it's aliasing. + // It's possible there's another symbol with the same name as the alias that binds + // first + private static bool ValidateAliasForTarget(IAliasSymbol aliasReplacement, SemanticModel semanticModel, ExpressionSyntax node, ISymbol symbol) + { + var aliasName = aliasReplacement.Name; + + // If we're the argument of a nameof(X.Y) call, then we can't simplify to an + // alias unless the alias has the same name as us (i.e. 'Y'). + if (node.IsNameOfArgumentExpression()) + { + var nameofValueOpt = semanticModel.GetConstantValue(node.Parent.Parent.Parent); + if (!nameofValueOpt.HasValue) + { + return false; + } + + if (nameofValueOpt.Value is string existingVal && + existingVal != aliasName) + { + return false; + } + } + + var boundSymbols = semanticModel.LookupNamespacesAndTypes(node.SpanStart, name: aliasName); + + if (boundSymbols.Length == 1) + { + if (boundSymbols[0] is IAliasSymbol boundAlias && aliasReplacement.Target.Equals(symbol)) + { + return true; + } + } + + return false; + } + + private static IAliasSymbol GetAliasForSymbol(INamespaceOrTypeSymbol symbol, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var originalSemanticModel = semanticModel.GetOriginalSemanticModel(); + if (!originalSemanticModel.SyntaxTree.HasCompilationUnitRoot) + { + return null; + } + + var namespaceId = GetNamespaceIdForAliasSearch(semanticModel, token, cancellationToken); + if (namespaceId < 0) + { + return null; + } + + if (!AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId, symbol, out var aliasSymbol)) + { + // add cache + AliasSymbolCache.AddAliasSymbols(originalSemanticModel, namespaceId, semanticModel.LookupNamespacesAndTypes(token.SpanStart).OfType()); + + // retry + AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId, symbol, out aliasSymbol); + } + + return aliasSymbol; + } + + private static int GetNamespaceIdForAliasSearch(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + { + var startNode = GetStartNodeForNamespaceId(semanticModel, token, cancellationToken); + if (!startNode.SyntaxTree.HasCompilationUnitRoot) + { + return -1; + } + + // NOTE: If we're currently in a block of usings, then we want to collect the + // aliases that are higher up than this block. Using aliases declared in a block of + // usings are not usable from within that same block. + var usingDirective = startNode.GetAncestorOrThis(); + if (usingDirective != null) + { + startNode = usingDirective.Parent.Parent; + if (startNode == null) + { + return -1; + } + } + + // check whether I am under a namespace + var @namespace = startNode.GetAncestorOrThis(); + if (@namespace != null) + { + // since we have node inside of the root, root should be already there + // search for namespace id should be quite cheap since normally there should be + // only a few namespace defined in a source file if it is not 1. that is why it is + // not cached. + var startIndex = 1; + return GetNamespaceId(startNode.SyntaxTree.GetRoot(cancellationToken), @namespace, ref startIndex); + } + + // no namespace, under compilation unit directly + return 0; + } + + private static SyntaxNode GetStartNodeForNamespaceId(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + { + if (!semanticModel.IsSpeculativeSemanticModel) + { + return token.Parent; + } + + var originalSemanticMode = semanticModel.GetOriginalSemanticModel(); + token = originalSemanticMode.SyntaxTree.GetRoot(cancellationToken).FindToken(semanticModel.OriginalPositionForSpeculation); + + return token.Parent; + } + + private static int GetNamespaceId(SyntaxNode container, NamespaceDeclarationSyntax target, ref int index) + { + if (container is CompilationUnitSyntax compilation) + { + return GetNamespaceId(compilation.Members, target, ref index); + } + + if (container is NamespaceDeclarationSyntax @namespace) + { + return GetNamespaceId(@namespace.Members, target, ref index); + } + + return Contract.FailWithReturn("shouldn't reach here"); + } + + private static int GetNamespaceId(SyntaxList members, NamespaceDeclarationSyntax target, ref int index) + { + foreach (var member in members) + { + if (!(member is NamespaceDeclarationSyntax childNamespace)) + { + continue; + } + + if (childNamespace == target) + { + return index; + } + + index++; + var result = GetNamespaceId(childNamespace, target, ref index); + if (result > 0) + { + return result; + } + } + + return -1; + } + + protected static TypeSyntax CreatePredefinedTypeSyntax(ExpressionSyntax expression, SyntaxKind keywordKind) + => SyntaxFactory.PredefinedType(SyntaxFactory.Token(expression.GetLeadingTrivia(), keywordKind, expression.GetTrailingTrivia())); + + protected static bool InsideNameOfExpression(ExpressionSyntax expression, SemanticModel semanticModel) + { + var nameOfInvocationExpr = expression.FirstAncestorOrSelf( + invocationExpr => + { + return invocationExpr.Expression is IdentifierNameSyntax identifierName && + identifierName.Identifier.Text == "nameof" && + semanticModel.GetConstantValue(invocationExpr).HasValue && + semanticModel.GetTypeInfo(invocationExpr).Type.SpecialType == SpecialType.System_String; + }); + + return nameOfInvocationExpr != null; + } + + protected static bool PreferPredefinedTypeKeywordInMemberAccess(ExpressionSyntax expression, OptionSet optionSet, SemanticModel semanticModel) + { + if (!SimplificationHelpers.PreferPredefinedTypeKeywordInMemberAccess(optionSet, semanticModel.Language)) + return false; + + return (expression.IsDirectChildOfMemberAccessExpression() || expression.InsideCrefReference()) && + !InsideNameOfExpression(expression, semanticModel); + } + + protected static bool WillConflictWithExistingLocal( + ExpressionSyntax expression, ExpressionSyntax simplifiedNode, SemanticModel semanticModel) + { + if (simplifiedNode is IdentifierNameSyntax identifierName && + !SyntaxFacts.IsInNamespaceOrTypeContext(expression)) + { + var symbols = semanticModel.LookupSymbols(expression.SpanStart, name: identifierName.Identifier.ValueText); + return symbols.Any(s => s is ILocalSymbol); + } + + return false; + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/ExpressionSimplifier.cs b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/ExpressionSimplifier.cs new file mode 100644 index 0000000000000..507043bced2ac --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/ExpressionSimplifier.cs @@ -0,0 +1,567 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Utilities; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers +{ + internal class ExpressionSimplifier : AbstractCSharpSimplifier + { + public static readonly ExpressionSimplifier Instance = new ExpressionSimplifier(); + + private ExpressionSimplifier() + { + } + + public override bool TrySimplify( + ExpressionSyntax expression, + SemanticModel semanticModel, + OptionSet optionSet, + out ExpressionSyntax replacementNode, + out TextSpan issueSpan, + CancellationToken cancellationToken) + { + if (TryReduceExplicitName(expression, semanticModel, out var replacementTypeNode, out issueSpan, optionSet, cancellationToken)) + { + replacementNode = replacementTypeNode; + return true; + } + + return TrySimplify(expression, semanticModel, out replacementNode, out issueSpan); + } + + private static bool TryReduceExplicitName( + ExpressionSyntax expression, + SemanticModel semanticModel, + out TypeSyntax replacementNode, + out TextSpan issueSpan, + OptionSet optionSet, + CancellationToken cancellationToken) + { + replacementNode = null; + issueSpan = default; + + if (expression.ContainsInterleavedDirective(cancellationToken)) + return false; + + if (expression.IsKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax memberAccess)) + return TryReduceMemberAccessExpression(memberAccess, semanticModel, out replacementNode, out issueSpan, optionSet, cancellationToken); + + if (expression is NameSyntax name) + return NameSimplifier.Instance.TrySimplify(name, semanticModel, optionSet, out replacementNode, out issueSpan, cancellationToken); + + return false; + } + + private static bool TryReduceMemberAccessExpression( + MemberAccessExpressionSyntax memberAccess, + SemanticModel semanticModel, + out TypeSyntax replacementNode, + out TextSpan issueSpan, + OptionSet optionSet, + CancellationToken cancellationToken) + { + replacementNode = null; + issueSpan = default; + + if (memberAccess.Name == null || memberAccess.Expression == null) + return false; + + // if this node is annotated as being a SpecialType, let's use this information. + if (memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind)) + { + replacementNode = SyntaxFactory.PredefinedType( + SyntaxFactory.Token( + memberAccess.GetLeadingTrivia(), + GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First())), + memberAccess.GetTrailingTrivia())); + + issueSpan = memberAccess.Span; + return true; + } + + // See https://github.com/dotnet/roslyn/issues/40974 + // + // To be very safe, we only support simplifying code that bound to a symbol without any + // sort of problems. We could potentially relax this in the future. However, we would + // need to be very careful about the implications of us offering to fixup 'broken' code + // in a manner that might end up making things worse or confusing the user. + var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess); + if (symbol == null) + return false; + + if (memberAccess.Expression.IsKind(SyntaxKind.ThisExpression) && + !SimplificationHelpers.ShouldSimplifyThisOrMeMemberAccessExpression(semanticModel, optionSet, symbol)) + { + return false; + } + + // if this node is on the left side, we could simplify to aliases + if (!memberAccess.IsRightSideOfDot()) + { + // Check if we need to replace this syntax with an alias identifier + if (TryReplaceExpressionWithAlias( + memberAccess, semanticModel, symbol, + cancellationToken, out var aliasReplacement)) + { + // get the token text as it appears in source code to preserve e.g. unicode character escaping + var text = aliasReplacement.Name; + var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault(); + + if (syntaxRef != null) + { + var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias.Name.Identifier; + text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString().Substring(1) : declIdentifier.ToString(); + } + + replacementNode = SyntaxFactory.IdentifierName( + memberAccess.Name.Identifier.CopyAnnotationsTo(SyntaxFactory.Identifier( + memberAccess.GetLeadingTrivia(), + SyntaxKind.IdentifierToken, + text, + aliasReplacement.Name, + memberAccess.GetTrailingTrivia()))); + + replacementNode = memberAccess.CopyAnnotationsTo(replacementNode); + replacementNode = memberAccess.Name.CopyAnnotationsTo(replacementNode); + + issueSpan = memberAccess.Span; + + // In case the alias name is the same as the last name of the alias target, we only include + // the left part of the name in the unnecessary span to Not confuse uses. + if (memberAccess.Name.Identifier.ValueText == ((IdentifierNameSyntax)replacementNode).Identifier.ValueText) + { + issueSpan = memberAccess.Expression.Span; + } + + return true; + } + + // Check if the Expression can be replaced by Predefined Type keyword + if (PreferPredefinedTypeKeywordInMemberAccess(memberAccess, optionSet, semanticModel)) + { + if (symbol != null && symbol.IsKind(SymbolKind.NamedType)) + { + var keywordKind = GetPredefinedKeywordKind(((INamedTypeSymbol)symbol).SpecialType); + if (keywordKind != SyntaxKind.None) + { + replacementNode = CreatePredefinedTypeSyntax(memberAccess, keywordKind); + + replacementNode = replacementNode + .WithAdditionalAnnotations(new SyntaxAnnotation( + nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))); + + issueSpan = memberAccess.Span; // we want to show the whole expression as unnecessary + + return true; + } + } + } + } + + // Try to eliminate cases without actually calling CanReplaceWithReducedName. For expressions of the form + // 'this.Name' or 'base.Name', no additional check here is required. + if (!memberAccess.Expression.IsKind(SyntaxKind.ThisExpression, SyntaxKind.BaseExpression)) + { + GetReplacementCandidates( + semanticModel, + memberAccess, + symbol, + out var speculativeSymbols, + out var speculativeNamespacesAndTypes); + + if (!IsReplacementCandidate(symbol, speculativeSymbols, speculativeNamespacesAndTypes)) + { + return false; + } + } + + replacementNode = memberAccess.GetNameWithTriviaMoved(); + issueSpan = memberAccess.Expression.Span; + + return CanReplaceWithReducedName( + memberAccess, replacementNode, semanticModel, symbol, cancellationToken); + } + + private static void GetReplacementCandidates( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + ISymbol actualSymbol, + out ImmutableArray speculativeSymbols, + out ImmutableArray speculativeNamespacesAndTypes) + { + var containsNamespaceOrTypeSymbol = actualSymbol is INamespaceOrTypeSymbol; + var containsOtherSymbol = !containsNamespaceOrTypeSymbol; + + speculativeSymbols = containsOtherSymbol + ? semanticModel.LookupSymbols(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText) + : ImmutableArray.Empty; + speculativeNamespacesAndTypes = containsNamespaceOrTypeSymbol + ? semanticModel.LookupNamespacesAndTypes(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText) + : ImmutableArray.Empty; + } + + /// + /// Determines if and + /// together contain a superset of the symbols in . + /// + private static bool IsReplacementCandidate(ISymbol actualSymbol, ImmutableArray speculativeSymbols, ImmutableArray speculativeNamespacesAndTypes) + { + if (speculativeSymbols.IsEmpty && speculativeNamespacesAndTypes.IsEmpty) + { + return false; + } + + if (actualSymbol is object) + { + return speculativeSymbols.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance) + || speculativeNamespacesAndTypes.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance); + } + + return true; + } + + /// + /// Compares symbols by their original definition. + /// + private sealed class CandidateSymbolEqualityComparer : IEqualityComparer + { + public static CandidateSymbolEqualityComparer Instance { get; } = new CandidateSymbolEqualityComparer(); + + private CandidateSymbolEqualityComparer() + { + } + + public bool Equals(ISymbol x, ISymbol y) + { + if (x is null || y is null) + { + return x == y; + } + + return x.OriginalDefinition.Equals(y.OriginalDefinition); + } + + public int GetHashCode(ISymbol obj) + { + return obj?.OriginalDefinition.GetHashCode() ?? 0; + } + } + + private static bool TrySimplify( + ExpressionSyntax expression, + SemanticModel semanticModel, + out ExpressionSyntax replacementNode, + out TextSpan issueSpan) + { + replacementNode = null; + issueSpan = default; + + switch (expression.Kind()) + { + case SyntaxKind.SimpleMemberAccessExpression: + { + var memberAccess = (MemberAccessExpressionSyntax)expression; + if (IsMemberAccessADynamicInvocation(memberAccess, semanticModel)) + { + return false; + } + + if (TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, out var newLeft, out issueSpan)) + { + // replacement node might not be in it's simplest form, so add simplify annotation to it. + replacementNode = memberAccess.Update(newLeft, memberAccess.OperatorToken, memberAccess.Name) + .WithAdditionalAnnotations(Simplifier.Annotation); + + // Ensure that replacement doesn't change semantics. + return !ReplacementChangesSemantics(memberAccess, replacementNode, semanticModel); + } + + return false; + } + + case SyntaxKind.QualifiedName: + { + var qualifiedName = (QualifiedNameSyntax)expression; + if (TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, out var newLeft, out issueSpan)) + { + // replacement node might not be in it's simplest form, so add simplify annotation to it. + replacementNode = qualifiedName.Update((NameSyntax)newLeft, qualifiedName.DotToken, qualifiedName.Right) + .WithAdditionalAnnotations(Simplifier.Annotation); + + // Ensure that replacement doesn't change semantics. + return !ReplacementChangesSemantics(qualifiedName, replacementNode, semanticModel); + } + + return false; + } + } + + return false; + } + + private static bool CanReplaceWithReducedName( + MemberAccessExpressionSyntax memberAccess, + ExpressionSyntax reducedName, + SemanticModel semanticModel, + ISymbol symbol, + CancellationToken cancellationToken) + { + if (!IsThisOrTypeOrNamespace(memberAccess, semanticModel)) + { + return false; + } + + var speculationAnalyzer = new SpeculationAnalyzer(memberAccess, reducedName, semanticModel, cancellationToken); + if (!speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() || + speculationAnalyzer.ReplacementChangesSemantics()) + { + return false; + } + + if (WillConflictWithExistingLocal(memberAccess, reducedName, semanticModel)) + { + return false; + } + + if (IsMemberAccessADynamicInvocation(memberAccess, semanticModel)) + { + return false; + } + + if (AccessMethodWithDynamicArgumentInsideStructConstructor(memberAccess, semanticModel)) + { + return false; + } + + if (memberAccess.Expression.Kind() == SyntaxKind.BaseExpression) + { + var enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken); + if (enclosingNamedType != null && + !enclosingNamedType.IsSealed && + symbol != null && + symbol.IsOverridable()) + { + return false; + } + } + + var invalidTransformation1 = ParserWouldTreatExpressionAsCast(reducedName, memberAccess); + + return !invalidTransformation1; + } + + /// + /// Tells if the Member access is the starting part of a Dynamic Invocation + /// + /// + /// + /// Return true, if the member access is the starting point of a Dynamic Invocation + private static bool IsMemberAccessADynamicInvocation(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + { + var ancestorInvocation = memberAccess.FirstAncestorOrSelf(); + + if (ancestorInvocation != null && ancestorInvocation.SpanStart == memberAccess.SpanStart) + { + var typeInfo = semanticModel.GetTypeInfo(ancestorInvocation); + if (typeInfo.Type != null && + typeInfo.Type.Kind == SymbolKind.DynamicType) + { + return true; + } + } + + return false; + } + + /* + * Name Reduction, to implicitly mean "this", is possible only after the initialization of all member variables but + * since the check for initialization of all member variable is a lot of work for this simplification we don't simplify + * even if all the member variables are initialized + */ + private static bool AccessMethodWithDynamicArgumentInsideStructConstructor(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + { + var constructor = memberAccess.Ancestors().OfType().SingleOrDefault(); + + if (constructor == null || constructor.Parent.Kind() != SyntaxKind.StructDeclaration) + { + return false; + } + + return semanticModel.GetSymbolInfo(memberAccess.Name).CandidateReason == CandidateReason.LateBound; + } + + // Note: The caller needs to verify that replacement doesn't change semantics of the original expression. + private static bool TrySimplifyMemberAccessOrQualifiedName( + ExpressionSyntax left, + ExpressionSyntax right, + SemanticModel semanticModel, + out ExpressionSyntax replacementNode, + out TextSpan issueSpan) + { + replacementNode = null; + issueSpan = default; + + if (left != null && right != null) + { + var leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left); + if (leftSymbol != null && leftSymbol.Kind == SymbolKind.NamedType) + { + var rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right); + if (rightSymbol != null && (rightSymbol.IsStatic || rightSymbol.Kind == SymbolKind.NamedType)) + { + // Static member access or nested type member access. + var containingType = rightSymbol.ContainingType; + + var enclosingSymbol = semanticModel.GetEnclosingSymbol(left.SpanStart); + var enclosingTypeParametersInsideOut = new List(); + + while (enclosingSymbol != null) + { + if (enclosingSymbol is IMethodSymbol methodSymbol) + { + if (methodSymbol.TypeArguments.Length != 0) + { + enclosingTypeParametersInsideOut.AddRange(methodSymbol.TypeArguments); + } + } + + if (enclosingSymbol is INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.TypeArguments.Length != 0) + { + enclosingTypeParametersInsideOut.AddRange(namedTypeSymbol.TypeArguments); + } + } + + enclosingSymbol = enclosingSymbol.ContainingSymbol; + } + + if (containingType != null && !containingType.Equals(leftSymbol)) + { + if (leftSymbol is INamedTypeSymbol namedType && + containingType.TypeArguments.Length != 0) + { + return false; + } + + // We have a static member access or a nested type member access using a more derived type. + // Simplify syntax so as to use accessed member's most immediate containing type instead of the derived type. + replacementNode = containingType.GenerateTypeSyntax() + .WithLeadingTrivia(left.GetLeadingTrivia()) + .WithTrailingTrivia(left.GetTrailingTrivia()); + issueSpan = left.Span; + return true; + } + } + } + } + + return false; + } + + private static bool IsThisOrTypeOrNamespace(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + { + if (memberAccess.Expression.Kind() == SyntaxKind.ThisExpression) + { + var previousToken = memberAccess.Expression.GetFirstToken().GetPreviousToken(); + + var symbol = semanticModel.GetSymbolInfo(memberAccess.Name).Symbol; + + if (previousToken.Kind() == SyntaxKind.OpenParenToken && + previousToken.Parent.IsKind(SyntaxKind.ParenthesizedExpression) && + !previousToken.Parent.IsParentKind(SyntaxKind.ParenthesizedExpression) && + ((ParenthesizedExpressionSyntax)previousToken.Parent).Expression.Kind() == SyntaxKind.SimpleMemberAccessExpression && + symbol != null && symbol.Kind == SymbolKind.Method) + { + return false; + } + + return true; + } + + var expressionInfo = semanticModel.GetSymbolInfo(memberAccess.Expression); + if (SimplificationHelpers.IsValidSymbolInfo(expressionInfo.Symbol)) + { + if (expressionInfo.Symbol is INamespaceOrTypeSymbol) + { + return true; + } + + if (expressionInfo.Symbol.IsThisParameter()) + { + return true; + } + } + + return false; + } + + private static bool ParserWouldTreatExpressionAsCast(ExpressionSyntax reducedNode, MemberAccessExpressionSyntax originalNode) + { + SyntaxNode parent = originalNode; + while (parent != null) + { + if (parent.IsParentKind(SyntaxKind.SimpleMemberAccessExpression)) + { + parent = parent.Parent; + continue; + } + + if (!parent.IsParentKind(SyntaxKind.ParenthesizedExpression)) + { + return false; + } + + break; + } + + var newExpression = parent.ReplaceNode(originalNode, reducedNode); + + // detect cast ambiguities according to C# spec #7.7.6 + if (IsNameOrMemberAccessButNoExpression(newExpression)) + { + var nextToken = parent.Parent.GetLastToken().GetNextToken(); + + return nextToken.Kind() == SyntaxKind.OpenParenToken || + nextToken.Kind() == SyntaxKind.TildeToken || + nextToken.Kind() == SyntaxKind.ExclamationToken || + (SyntaxFacts.IsKeywordKind(nextToken.Kind()) && !(nextToken.Kind() == SyntaxKind.AsKeyword || nextToken.Kind() == SyntaxKind.IsKeyword)); + } + + return false; + } + + private static bool IsNameOrMemberAccessButNoExpression(SyntaxNode node) + { + if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + { + var memberAccess = (MemberAccessExpressionSyntax)node; + + return memberAccess.Expression.IsKind(SyntaxKind.IdentifierName) || + IsNameOrMemberAccessButNoExpression(memberAccess.Expression); + } + + return node.IsKind(SyntaxKind.IdentifierName); + } + + protected static bool ReplacementChangesSemantics(ExpressionSyntax originalExpression, ExpressionSyntax replacedExpression, SemanticModel semanticModel) + { + var speculationAnalyzer = new SpeculationAnalyzer(originalExpression, replacedExpression, semanticModel, CancellationToken.None); + return speculationAnalyzer.ReplacementChangesSemantics(); + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/NameSimplifier.cs b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/NameSimplifier.cs new file mode 100644 index 0000000000000..41b54e95ce3aa --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/NameSimplifier.cs @@ -0,0 +1,723 @@ +// 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.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Utilities; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers +{ + using Microsoft.CodeAnalysis.Rename.ConflictEngine; + + internal class NameSimplifier : AbstractCSharpSimplifier + { + public static readonly NameSimplifier Instance = new NameSimplifier(); + + private NameSimplifier() + { + } + + public override bool TrySimplify( + NameSyntax name, + SemanticModel semanticModel, + OptionSet optionSet, + out TypeSyntax replacementNode, + out TextSpan issueSpan, + CancellationToken cancellationToken) + { + replacementNode = null; + issueSpan = default; + + if (name.IsVar) + { + return false; + } + + // we should not simplify a name of a namespace declaration + if (IsPartOfNamespaceDeclarationName(name)) + { + return false; + } + + // We can simplify Qualified names and AliasQualifiedNames. Generally, if we have + // something like "A.B.C.D", we only consider the full thing something we can simplify. + // However, in the case of "A.B.C<>.D", then we'll only consider simplifying up to the + // first open name. This is because if we remove the open name, we'll often change + // meaning as "D" will bind to C.D which is different than C<>.D! + if (name is QualifiedNameSyntax qualifiedName) + { + var left = qualifiedName.Left; + if (ContainsOpenName(left)) + { + // Don't simplify A.B<>.C + return false; + } + } + + // 1. see whether binding the name binds to a symbol/type. if not, it is ambiguous and + // nothing we can do here. + var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, name); + if (symbol == null) + { + return false; + } + + // treat constructor names as types + var method = symbol as IMethodSymbol; + if (method.IsConstructor()) + { + symbol = method.ContainingType; + } + + if (symbol.Kind == SymbolKind.Method && name.Kind() == SyntaxKind.GenericName) + { + var genericName = (GenericNameSyntax)name; + replacementNode = SyntaxFactory.IdentifierName(genericName.Identifier) + .WithLeadingTrivia(genericName.GetLeadingTrivia()) + .WithTrailingTrivia(genericName.GetTrailingTrivia()); + + issueSpan = genericName.TypeArgumentList.Span; + return CanReplaceWithReducedName( + name, replacementNode, semanticModel, cancellationToken); + } + + if (!(symbol is INamespaceOrTypeSymbol)) + { + return false; + } + + if (name.HasAnnotations(SpecialTypeAnnotation.Kind)) + { + replacementNode = SyntaxFactory.PredefinedType( + SyntaxFactory.Token( + name.GetLeadingTrivia(), + GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(name.GetAnnotations(SpecialTypeAnnotation.Kind).First())), + name.GetTrailingTrivia())); + + issueSpan = name.Span; + + return CanReplaceWithReducedNameInContext(name, replacementNode, semanticModel); + } + else + { + if (!name.IsRightSideOfDotOrColonColon()) + { + if (TryReplaceExpressionWithAlias(name, semanticModel, symbol, cancellationToken, out var aliasReplacement)) + { + // get the token text as it appears in source code to preserve e.g. Unicode character escaping + var text = aliasReplacement.Name; + var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault(); + + if (syntaxRef != null) + { + var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias.Name.Identifier; + text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString().Substring(1) : declIdentifier.ToString(); + } + + var identifierToken = SyntaxFactory.Identifier( + name.GetLeadingTrivia(), + SyntaxKind.IdentifierToken, + text, + aliasReplacement.Name, + name.GetTrailingTrivia()); + + identifierToken = CSharpSimplificationService.TryEscapeIdentifierToken(identifierToken, name, semanticModel); + replacementNode = SyntaxFactory.IdentifierName(identifierToken); + + // Merge annotation to new syntax node + var annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(RenameAnnotation.Kind); + foreach (var annotatedNodeOrToken in annotatedNodesOrTokens) + { + if (annotatedNodeOrToken.IsToken) + { + identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken); + } + else + { + replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode); + } + } + + annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(AliasAnnotation.Kind); + foreach (var annotatedNodeOrToken in annotatedNodesOrTokens) + { + if (annotatedNodeOrToken.IsToken) + { + identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken); + } + else + { + replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode); + } + } + + replacementNode = ((SimpleNameSyntax)replacementNode).WithIdentifier(identifierToken); + issueSpan = name.Span; + + // In case the alias name is the same as the last name of the alias target, we only include + // the left part of the name in the unnecessary span to Not confuse uses. + if (name.Kind() == SyntaxKind.QualifiedName) + { + var qualifiedName3 = (QualifiedNameSyntax)name; + + if (qualifiedName3.Right.Identifier.ValueText == identifierToken.ValueText) + { + issueSpan = qualifiedName3.Left.Span; + } + } + + // first check if this would be a valid reduction + if (CanReplaceWithReducedNameInContext(name, replacementNode, semanticModel)) + { + // in case this alias name ends with "Attribute", we're going to see if we can also + // remove that suffix. + if (TryReduceAttributeSuffix( + name, + identifierToken, + out var replacementNodeWithoutAttributeSuffix, + out var issueSpanWithoutAttributeSuffix)) + { + if (CanReplaceWithReducedName(name, replacementNodeWithoutAttributeSuffix, semanticModel, cancellationToken)) + { + replacementNode = replacementNode.CopyAnnotationsTo(replacementNodeWithoutAttributeSuffix); + issueSpan = issueSpanWithoutAttributeSuffix; + } + } + + return true; + } + + return false; + } + + var nameHasNoAlias = false; + + if (name is SimpleNameSyntax simpleName) + { + if (!simpleName.Identifier.HasAnnotations(AliasAnnotation.Kind)) + { + nameHasNoAlias = true; + } + } + + if (name is QualifiedNameSyntax qualifiedName2) + { + if (!qualifiedName2.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation)) + { + nameHasNoAlias = true; + } + } + + if (name is AliasQualifiedNameSyntax aliasQualifiedName) + { + if (aliasQualifiedName.Name is SimpleNameSyntax && + !aliasQualifiedName.Name.Identifier.HasAnnotations(AliasAnnotation.Kind) && + !aliasQualifiedName.Name.HasAnnotation(Simplifier.SpecialTypeAnnotation)) + { + nameHasNoAlias = true; + } + } + + var aliasInfo = semanticModel.GetAliasInfo(name, cancellationToken); + if (nameHasNoAlias && aliasInfo == null) + { + // Don't simplify to predefined type if name is part of a QualifiedName. + // QualifiedNames can't contain PredefinedTypeNames (although MemberAccessExpressions can). + // In other words, the left side of a QualifiedName can't be a PredefinedTypeName. + var inDeclarationContext = PreferPredefinedTypeKeywordInDeclarations(name, optionSet, semanticModel); + var inMemberAccessContext = PreferPredefinedTypeKeywordInMemberAccess(name, optionSet, semanticModel); + + if (!name.Parent.IsKind(SyntaxKind.QualifiedName) && (inDeclarationContext || inMemberAccessContext)) + { + // See if we can simplify this name (like System.Int32) to a built-in type (like 'int'). + // If not, we'll still fall through and see if we can convert it to Int32. + + var codeStyleOptionName = inDeclarationContext + ? nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInDeclaration) + : nameof(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess); + + var type = semanticModel.GetTypeInfo(name, cancellationToken).Type; + if (type != null) + { + var keywordKind = GetPredefinedKeywordKind(type.SpecialType); + if (keywordKind != SyntaxKind.None && + CanReplaceWithPredefinedTypeKeywordInContext(name, semanticModel, out replacementNode, ref issueSpan, keywordKind, codeStyleOptionName)) + { + return true; + } + } + else + { + var typeSymbol = semanticModel.GetSymbolInfo(name, cancellationToken).Symbol; + if (typeSymbol.IsKind(SymbolKind.NamedType)) + { + var keywordKind = GetPredefinedKeywordKind(((INamedTypeSymbol)typeSymbol).SpecialType); + if (keywordKind != SyntaxKind.None && + CanReplaceWithPredefinedTypeKeywordInContext(name, semanticModel, out replacementNode, ref issueSpan, keywordKind, codeStyleOptionName)) + { + return true; + } + } + } + } + } + + // Nullable rewrite: Nullable -> int? + // Don't rewrite in the case where Nullable is part of some qualified name like Nullable.Something + if (!name.IsVar && symbol.Kind == SymbolKind.NamedType && !name.IsLeftSideOfQualifiedName()) + { + var type = (INamedTypeSymbol)symbol; + if (aliasInfo == null && CanSimplifyNullable(type, name, semanticModel)) + { + GenericNameSyntax genericName; + if (name.Kind() == SyntaxKind.QualifiedName) + { + genericName = (GenericNameSyntax)((QualifiedNameSyntax)name).Right; + } + else + { + genericName = (GenericNameSyntax)name; + } + + var oldType = genericName.TypeArgumentList.Arguments.First(); + if (oldType.Kind() == SyntaxKind.OmittedTypeArgument) + { + return false; + } + + replacementNode = SyntaxFactory.NullableType(oldType) + .WithLeadingTrivia(name.GetLeadingTrivia()) + .WithTrailingTrivia(name.GetTrailingTrivia()); + issueSpan = name.Span; + + // we need to simplify the whole qualified name at once, because replacing the identifier on the left in + // System.Nullable alone would be illegal. + // If this fails we want to continue to try at least to remove the System if possible. + if (CanReplaceWithReducedNameInContext(name, replacementNode, semanticModel)) + { + return true; + } + } + } + } + + SyntaxToken identifier; + switch (name.Kind()) + { + case SyntaxKind.AliasQualifiedName: + var simpleName = ((AliasQualifiedNameSyntax)name).Name + .WithLeadingTrivia(name.GetLeadingTrivia()); + + simpleName = simpleName.ReplaceToken(simpleName.Identifier, + ((AliasQualifiedNameSyntax)name).Name.Identifier.CopyAnnotationsTo( + simpleName.Identifier.WithLeadingTrivia( + ((AliasQualifiedNameSyntax)name).Alias.Identifier.LeadingTrivia))); + + replacementNode = simpleName; + + issueSpan = ((AliasQualifiedNameSyntax)name).Alias.Span; + + break; + + case SyntaxKind.QualifiedName: + replacementNode = ((QualifiedNameSyntax)name).Right.WithLeadingTrivia(name.GetLeadingTrivia()); + issueSpan = ((QualifiedNameSyntax)name).Left.Span; + + break; + + case SyntaxKind.IdentifierName: + identifier = ((IdentifierNameSyntax)name).Identifier; + + // we can try to remove the Attribute suffix if this is the attribute name + TryReduceAttributeSuffix(name, identifier, out replacementNode, out issueSpan); + break; + } + } + + if (replacementNode == null) + { + return false; + } + + // We may be looking at a name `X.Y` seeing if we can replace it with `Y`. However, in + // order to know for sure, we actually have to look slightly higher at `X.Y.Z` to see if + // it can simplify to `Y.Z`. This is because in the `Color Color` case we can only tell + // if we can reduce by looking by also looking at what comes next to see if it will + // cause the simplified name to bind to the instance or static side. + if (TryReduceCrefColorColor(name, replacementNode, semanticModel, cancellationToken)) + { + return true; + } + + return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken); + } + + private static bool TryReduceCrefColorColor( + NameSyntax name, TypeSyntax replacement, + SemanticModel semanticModel, CancellationToken cancellationToken) + { + if (!name.InsideCrefReference()) + return false; + + if (name.Parent is QualifiedCrefSyntax qualifiedCrefParent && qualifiedCrefParent.Container == name) + { + // we have and we're trying to see if we can replace + // A.B.C with C. In this case the parent of A.B.C is A.B.C.D which is a + // QualifiedCrefSyntax + + var qualifiedReplacement = SyntaxFactory.QualifiedCref(replacement, qualifiedCrefParent.Member); + if (QualifiedCrefSimplifier.CanSimplifyWithReplacement(qualifiedCrefParent, semanticModel, qualifiedReplacement, cancellationToken)) + return true; + } + else if (name.Parent is QualifiedNameSyntax qualifiedParent && qualifiedParent.Left == name && + replacement is NameSyntax replacementName) + { + // we have and we're trying to see if we can replace + // A.B with B. In this case the parent of A.B is A.B.C which is a + // QualifiedNameSyntax + + var qualifiedReplacement = SyntaxFactory.QualifiedName(replacementName, qualifiedParent.Right); + return CanReplaceWithReducedName( + qualifiedParent, qualifiedReplacement, semanticModel, cancellationToken); + } + + return false; + } + + private static bool CanSimplifyNullable(INamedTypeSymbol type, NameSyntax name, SemanticModel semanticModel) + { + if (!type.IsNullable()) + { + return false; + } + + if (type.IsUnboundGenericType) + { + // Don't simplify unbound generic type "Nullable<>". + return false; + } + + if (InsideNameOfExpression(name, semanticModel)) + { + // Nullable can't be simplified to T? in nameof expressions. + return false; + } + + if (!name.InsideCrefReference()) + { + // Nullable can always be simplified to T? outside crefs. + return true; + } + + if (name.Parent is NameMemberCrefSyntax) + return false; + + // Inside crefs, if the T in this Nullable{T} is being declared right here + // then this Nullable{T} is not a constructed generic type and we should + // not offer to simplify this to T?. + // + // For example, we should not offer the simplification in the following cases where + // T does not bind to an existing type / type parameter in the user's code. + // - + // - + // + // And we should offer the simplification in the following cases where SomeType and + // SomeMethod bind to a type and method declared elsewhere in the users code. + // - + + var argument = type.TypeArguments.SingleOrDefault(); + if (argument == null || argument.IsErrorType()) + { + return false; + } + + var argumentDecl = argument.DeclaringSyntaxReferences.FirstOrDefault(); + if (argumentDecl == null) + { + // The type argument is a type from metadata - so this is a constructed generic nullable type that can be simplified (e.g. Nullable(Of Integer)). + return true; + } + + return !name.Span.Contains(argumentDecl.Span); + } + + private static bool CanReplaceWithPredefinedTypeKeywordInContext( + NameSyntax name, + SemanticModel semanticModel, + out TypeSyntax replacementNode, + ref TextSpan issueSpan, + SyntaxKind keywordKind, + string codeStyleOptionName) + { + replacementNode = CreatePredefinedTypeSyntax(name, keywordKind); + + issueSpan = name.Span; // we want to show the whole name expression as unnecessary + + var canReduce = CanReplaceWithReducedNameInContext(name, replacementNode, semanticModel); + + if (canReduce) + { + replacementNode = replacementNode.WithAdditionalAnnotations(new SyntaxAnnotation(codeStyleOptionName)); + } + + return canReduce; + } + + private static bool TryReduceAttributeSuffix( + NameSyntax name, + SyntaxToken identifierToken, + out TypeSyntax replacementNode, + out TextSpan issueSpan) + { + issueSpan = default; + replacementNode = default; + + // we can try to remove the Attribute suffix if this is the attribute name + if (SyntaxFacts.IsAttributeName(name)) + { + if (name.Parent.Kind() == SyntaxKind.Attribute || name.IsRightSideOfDotOrColonColon()) + { + const string AttributeName = "Attribute"; + + // an attribute that should keep it (unnecessary "Attribute" suffix should be annotated with a DontSimplifyAnnotation + if (identifierToken.ValueText != AttributeName && identifierToken.ValueText.EndsWith(AttributeName, StringComparison.Ordinal) && !identifierToken.HasAnnotation(SimplificationHelpers.DontSimplifyAnnotation)) + { + // weird. the semantic model is able to bind attribute syntax like "[as()]" although it's not valid code. + // so we need another check for keywords manually. + var newAttributeName = identifierToken.ValueText.Substring(0, identifierToken.ValueText.Length - 9); + if (SyntaxFacts.GetKeywordKind(newAttributeName) != SyntaxKind.None) + { + return false; + } + + // if this attribute name in source contained Unicode escaping, we will loose it now + // because there is no easy way to determine the substring from identifier->ToString() + // which would be needed to pass to SyntaxFactory.Identifier + // The result is an unescaped Unicode character in source. + + // once we remove the Attribute suffix, we can't use an escaped identifier + var newIdentifierToken = identifierToken.CopyAnnotationsTo( + SyntaxFactory.Identifier( + identifierToken.LeadingTrivia, + newAttributeName, + identifierToken.TrailingTrivia)); + + replacementNode = SyntaxFactory.IdentifierName(newIdentifierToken) + .WithLeadingTrivia(name.GetLeadingTrivia()); + issueSpan = new TextSpan(identifierToken.Span.End - 9, 9); + + return true; + } + } + } + + return false; + } + + /// + /// Checks if the SyntaxNode is a name of a namespace declaration. To be a namespace name, the syntax + /// must be parented by an namespace declaration and the node itself must be equal to the declaration's Name + /// property. + /// + /// + /// + private static bool IsPartOfNamespaceDeclarationName(SyntaxNode node) + { + var parent = node; + + while (parent != null) + { + switch (parent.Kind()) + { + case SyntaxKind.IdentifierName: + case SyntaxKind.QualifiedName: + node = parent; + parent = parent.Parent; + break; + + case SyntaxKind.NamespaceDeclaration: + var namespaceDeclaration = (NamespaceDeclarationSyntax)parent; + return object.Equals(namespaceDeclaration.Name, node); + + default: + return false; + } + } + + return false; + } + + public static bool CanReplaceWithReducedNameInContext( + NameSyntax name, TypeSyntax reducedName, SemanticModel semanticModel) + { + // Check for certain things that would prevent us from reducing this name in this context. + // For example, you can simplify "using a = System.Int32" to "using a = int" as it's simply + // not allowed in the C# grammar. + + if (IsNonNameSyntaxInUsingDirective(name, reducedName) || + WillConflictWithExistingLocal(name, reducedName, semanticModel) || + IsAmbiguousCast(name, reducedName) || + IsNullableTypeInPointerExpression(reducedName) || + IsNotNullableReplaceable(name, reducedName) || + IsNonReducableQualifiedNameInUsingDirective(semanticModel, name)) + { + return false; + } + + return true; + } + + private static bool ContainsOpenName(NameSyntax name) + { + if (name is QualifiedNameSyntax qualifiedName) + { + return ContainsOpenName(qualifiedName.Left) || ContainsOpenName(qualifiedName.Right); + } + else if (name is GenericNameSyntax genericName) + { + return genericName.IsUnboundGenericName; + } + else + { + return false; + } + } + + private static bool CanReplaceWithReducedName(NameSyntax name, TypeSyntax reducedName, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var speculationAnalyzer = new SpeculationAnalyzer(name, reducedName, semanticModel, cancellationToken); + if (speculationAnalyzer.ReplacementChangesSemantics()) + { + return false; + } + + return NameSimplifier.CanReplaceWithReducedNameInContext(name, reducedName, semanticModel); + } + + private static bool IsNotNullableReplaceable(NameSyntax name, TypeSyntax reducedName) + { + if (reducedName.IsKind(SyntaxKind.NullableType, out NullableTypeSyntax nullableType)) + { + if (nullableType.ElementType.Kind() == SyntaxKind.OmittedTypeArgument) + return true; + + return name.IsLeftSideOfDot() || name.IsRightSideOfDot(); + } + + return false; + } + + private static bool IsNullableTypeInPointerExpression(ExpressionSyntax simplifiedNode) + { + // Note: nullable type syntax is not allowed in pointer type syntax + if (simplifiedNode.Kind() == SyntaxKind.NullableType && + simplifiedNode.DescendantNodes().Any(n => n is PointerTypeSyntax)) + { + return true; + } + + return false; + } + + private static bool IsNonNameSyntaxInUsingDirective(ExpressionSyntax expression, ExpressionSyntax simplifiedNode) + { + return + expression.IsParentKind(SyntaxKind.UsingDirective) && + !(simplifiedNode is NameSyntax); + } + + private static bool IsAmbiguousCast(ExpressionSyntax expression, ExpressionSyntax simplifiedNode) + { + // Can't simplify a type name in a cast expression if it would then cause the cast to be + // parsed differently. For example: (Goo::Bar)+1 is a cast. But if that simplifies to + // (Bar)+1 then that's an arithmetic expression. + if (expression.IsParentKind(SyntaxKind.CastExpression)) + { + var castExpression = (CastExpressionSyntax)expression.Parent; + if (castExpression.Type == expression) + { + var newCastExpression = castExpression.ReplaceNode(castExpression.Type, simplifiedNode); + var reparsedCastExpression = SyntaxFactory.ParseExpression(newCastExpression.ToString()); + + if (!reparsedCastExpression.IsKind(SyntaxKind.CastExpression)) + { + return true; + } + } + } + + return false; + } + + private static bool IsNonReducableQualifiedNameInUsingDirective(SemanticModel model, NameSyntax name) + { + // Whereas most of the time we do not want to reduce namespace names, We will + // make an exception for namespaces with the global:: alias. + return IsQualifiedNameInUsingDirective(model, name) && + !IsGlobalAliasQualifiedName(name); + } + + private static bool IsQualifiedNameInUsingDirective(SemanticModel model, NameSyntax name) + { + while (name.IsLeftSideOfQualifiedName()) + { + name = (NameSyntax)name.Parent; + } + + if (name.IsParentKind(SyntaxKind.UsingDirective) && + ((UsingDirectiveSyntax)name.Parent).Alias == null) + { + // We're a qualified name in a using. We don't want to reduce this name as people like + // fully qualified names in usings so they can properly tell what the name is resolving + // to. + // However, if this name is actually referencing the special Script class, then we do + // want to allow that to be reduced. + + return !IsInScriptClass(model, name); + } + + return false; + } + + private static bool IsGlobalAliasQualifiedName(NameSyntax name) + { + // Checks whether the `global::` alias is applied to the name + return name is AliasQualifiedNameSyntax aliasName && + aliasName.Alias.Identifier.IsKind(SyntaxKind.GlobalKeyword); + } + + private static bool IsInScriptClass(SemanticModel model, NameSyntax name) + { + var symbol = model.GetSymbolInfo(name).Symbol as INamedTypeSymbol; + while (symbol != null) + { + if (symbol.IsScriptClass) + { + return true; + } + + symbol = symbol.ContainingType; + } + + return false; + } + + private static bool PreferPredefinedTypeKeywordInDeclarations(NameSyntax name, OptionSet optionSet, SemanticModel semanticModel) + { + return !name.IsDirectChildOfMemberAccessExpression() && + !name.InsideCrefReference() && + !InsideNameOfExpression(name, semanticModel) && + SimplificationHelpers.PreferPredefinedTypeKeywordInDeclarations(optionSet, semanticModel.Language); + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Extensions/CrefSyntaxExtensions.cs b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/QualifiedCrefSimplifier.cs similarity index 72% rename from src/Workspaces/CSharp/Portable/Extensions/CrefSyntaxExtensions.cs rename to src/Workspaces/CSharp/Portable/Simplification/Simplifiers/QualifiedCrefSimplifier.cs index 5ca6dd9520ebd..c7f38db0c3e9e 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/CrefSyntaxExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/Simplifiers/QualifiedCrefSimplifier.cs @@ -4,23 +4,30 @@ using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Extensions +namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers { - using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + using static SyntaxFactory; - internal static class CrefSyntaxExtensions + internal class QualifiedCrefSimplifier : AbstractCSharpSimplifier { - public static bool TryReduceOrSimplifyExplicitName( - this QualifiedCrefSyntax crefSyntax, + public static readonly QualifiedCrefSimplifier Instance = new QualifiedCrefSimplifier(); + + private QualifiedCrefSimplifier() + { + } + + public override bool TrySimplify( + QualifiedCrefSyntax crefSyntax, SemanticModel semanticModel, + OptionSet optionSet, out CrefSyntax replacementNode, out TextSpan issueSpan, - OptionSet optionSet, CancellationToken cancellationToken) { replacementNode = null; @@ -41,7 +48,7 @@ public static bool TryReduceOrSimplifyExplicitName( // 1. Check for Predefined Types if (symbol is INamedTypeSymbol namedSymbol) { - var keywordKind = ExpressionSyntaxExtensions.GetPredefinedKeywordKind(namedSymbol.SpecialType); + var keywordKind = GetPredefinedKeywordKind(namedSymbol.SpecialType); if (keywordKind != SyntaxKind.None) { @@ -56,7 +63,7 @@ public static bool TryReduceOrSimplifyExplicitName( } } - return TryReduceOrSimplifyQualifiedCref( + return CanSimplifyWithReplacement( crefSyntax, semanticModel, memberCref, out replacementNode, out issueSpan, cancellationToken); } @@ -68,19 +75,24 @@ private static TypeCrefSyntax CreateReplacement(QualifiedCrefSyntax crefSyntax, return TypeCref(PredefinedType(token)).WithAdditionalAnnotations(annotation); } - public static bool TryReduceOrSimplifyQualifiedCref( - this QualifiedCrefSyntax crefSyntax, SemanticModel semanticModel, + public static bool CanSimplifyWithReplacement( + QualifiedCrefSyntax crefSyntax, SemanticModel semanticModel, + CrefSyntax replacement, CancellationToken cancellationToken) + { + return CanSimplifyWithReplacement(crefSyntax, semanticModel, replacement, out _, out _, cancellationToken); + } + + private static bool CanSimplifyWithReplacement( + QualifiedCrefSyntax crefSyntax, SemanticModel semanticModel, CrefSyntax replacement, out CrefSyntax replacementNode, out TextSpan issueSpan, CancellationToken cancellationToken) { var oldSymbol = semanticModel.GetSymbolInfo(crefSyntax, cancellationToken).Symbol; if (oldSymbol != null) { - var speculativeBindingOption = SpeculativeBindingOption.BindAsExpression; - if (oldSymbol is INamespaceOrTypeSymbol) - { - speculativeBindingOption = SpeculativeBindingOption.BindAsTypeOrNamespace; - } + var speculativeBindingOption = oldSymbol is INamespaceOrTypeSymbol + ? SpeculativeBindingOption.BindAsTypeOrNamespace + : SpeculativeBindingOption.BindAsExpression; var newSymbol = semanticModel.GetSpeculativeSymbolInfo(crefSyntax.SpanStart, replacement, speculativeBindingOption).Symbol; diff --git a/src/Workspaces/Core/Portable/Simplification/Simplifiers/AbstractSimplifier.cs b/src/Workspaces/Core/Portable/Simplification/Simplifiers/AbstractSimplifier.cs new file mode 100644 index 0000000000000..37155171e7a7c --- /dev/null +++ b/src/Workspaces/Core/Portable/Simplification/Simplifiers/AbstractSimplifier.cs @@ -0,0 +1,21 @@ +// 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.Threading; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Simplification.Simplifiers +{ + internal abstract class AbstractSimplifier + where TSyntax : SyntaxNode + where TSimplifiedSyntax : SyntaxNode + { + public abstract bool TrySimplify( + TSyntax syntax, + SemanticModel semanticModel, + OptionSet optionSet, + out TSimplifiedSyntax replacementNode, + out TextSpan issueSpan, + CancellationToken cancellationToken); + } +} diff --git a/src/Workspaces/VisualBasic/Portable/Extensions/ExpressionSyntaxExtensions.vb b/src/Workspaces/VisualBasic/Portable/Extensions/ExpressionSyntaxExtensions.vb index 88811fbdacba4..ace041bdc6f1b 100644 --- a/src/Workspaces/VisualBasic/Portable/Extensions/ExpressionSyntaxExtensions.vb +++ b/src/Workspaces/VisualBasic/Portable/Extensions/ExpressionSyntaxExtensions.vb @@ -6,13 +6,8 @@ Imports System.Runtime.CompilerServices Imports System.Runtime.InteropServices Imports System.Threading Imports Microsoft.CodeAnalysis -Imports Microsoft.CodeAnalysis.CodeStyle -Imports Microsoft.CodeAnalysis.Options -Imports Microsoft.CodeAnalysis.Rename.ConflictEngine Imports Microsoft.CodeAnalysis.Simplification Imports Microsoft.CodeAnalysis.Text -Imports Microsoft.CodeAnalysis.VisualBasic.Simplification -Imports Microsoft.CodeAnalysis.VisualBasic.Symbols Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.CodeAnalysis.VisualBasic.Utilities @@ -62,23 +57,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions result) End Function - - - Public Function IsAliasReplaceableExpression(expression As ExpressionSyntax) As Boolean - If expression.Kind = SyntaxKind.IdentifierName OrElse - expression.Kind = SyntaxKind.QualifiedName Then - Return True - End If - - If expression.Kind = SyntaxKind.SimpleMemberAccessExpression Then - Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) - - Return memberAccess.Expression IsNot Nothing AndAlso memberAccess.Expression.IsAliasReplaceableExpression() - End If - - Return False - End Function - Public Function IsMemberAccessExpressionName(expression As ExpressionSyntax) As Boolean Return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso @@ -788,351 +766,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Return semanticModel.Compilation.CreateArrayTypeSymbol(type, rank) End Function - - Public Function TryReduceVariableDeclaratorWithoutType( - variableDeclarator As VariableDeclaratorSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As SyntaxNode, - ByRef issueSpan As TextSpan) As Boolean - - replacementNode = Nothing - issueSpan = Nothing - - ' Failfast Conditions - If variableDeclarator.AsClause Is Nothing OrElse - Not variableDeclarator.Parent.IsKind( - SyntaxKind.LocalDeclarationStatement, - SyntaxKind.UsingStatement, - SyntaxKind.ForStatement, - SyntaxKind.ForEachStatement, - SyntaxKind.FieldDeclaration) Then - Return False - End If - - If variableDeclarator.Names.Count <> 1 Then - Return False - End If - - Dim parent = variableDeclarator.Parent - Dim modifiedIdentifier = variableDeclarator.Names.Single() - - Dim simpleAsClause = TryCast(variableDeclarator.AsClause, SimpleAsClauseSyntax) - If simpleAsClause Is Nothing Then - Return False - End If - - If (parent.IsKind(SyntaxKind.LocalDeclarationStatement, SyntaxKind.UsingStatement, SyntaxKind.FieldDeclaration) AndAlso - variableDeclarator.Initializer IsNot Nothing) Then - - ' Type Check - - Dim declaredSymbolType As ITypeSymbol = Nothing - If Not HasValidDeclaredTypeSymbol(modifiedIdentifier, semanticModel, declaredSymbolType) Then - Return False - End If - - Dim initializerType As ITypeSymbol - - If declaredSymbolType.IsArrayType() AndAlso variableDeclarator.Initializer.Value.Kind() = SyntaxKind.CollectionInitializer Then - ' Get type of the array literal in context without the target type - initializerType = semanticModel.GetSpeculativeTypeInfo(variableDeclarator.Initializer.Value.SpanStart, variableDeclarator.Initializer.Value, SpeculativeBindingOption.BindAsExpression).ConvertedType - Else - initializerType = semanticModel.GetTypeInfo(variableDeclarator.Initializer.Value).Type - End If - - If Not declaredSymbolType.Equals(initializerType) Then - Return False - End If - - Dim newModifiedIdentifier = SyntaxFactory.ModifiedIdentifier(modifiedIdentifier.Identifier) ' LeadingTrivia is copied here - replacementNode = SyntaxFactory.VariableDeclarator(SyntaxFactory.SingletonSeparatedList(newModifiedIdentifier.WithTrailingTrivia(variableDeclarator.AsClause.GetTrailingTrivia())), - asClause:=Nothing, - initializer:=variableDeclarator.Initializer) 'TrailingTrivia is copied here - issueSpan = variableDeclarator.Span - Return True - End If - - If (parent.IsKind(SyntaxKind.ForEachStatement, SyntaxKind.ForStatement)) Then - ' Type Check for ForStatement - If parent.IsKind(SyntaxKind.ForStatement) Then - Dim declaredSymbolType As ITypeSymbol = Nothing - If Not HasValidDeclaredTypeSymbol(modifiedIdentifier, semanticModel, declaredSymbolType) Then - Return False - End If - - Dim valueType = semanticModel.GetTypeInfo(DirectCast(parent, ForStatementSyntax).ToValue).Type - - If Not valueType.Equals(declaredSymbolType) Then - Return False - End If - End If - - If parent.IsKind(SyntaxKind.ForEachStatement) Then - Dim forEachStatementInfo = semanticModel.GetForEachStatementInfo(DirectCast(parent, ForEachStatementSyntax)) - If Not forEachStatementInfo.ElementConversion.IsIdentity Then - Return False - End If - End If - - Dim newIdentifierName = SyntaxFactory.IdentifierName(modifiedIdentifier.Identifier) ' Leading Trivia is copied here - replacementNode = newIdentifierName.WithTrailingTrivia(variableDeclarator.AsClause.GetTrailingTrivia()) ' Trailing Trivia is copied here - issueSpan = variableDeclarator.Span - Return True - End If - - Return False - End Function - - Private Function HasValidDeclaredTypeSymbol( - modifiedIdentifier As ModifiedIdentifierSyntax, - semanticModel As SemanticModel, - ByRef typeSymbol As ITypeSymbol) As Boolean - - Dim declaredSymbol = semanticModel.GetDeclaredSymbol(modifiedIdentifier) - If declaredSymbol Is Nothing OrElse - (Not TypeOf declaredSymbol Is ILocalSymbol AndAlso Not TypeOf declaredSymbol Is IFieldSymbol) Then - Return False - End If - - Dim localSymbol = TryCast(declaredSymbol, ILocalSymbol) - If localSymbol IsNot Nothing AndAlso TypeOf localSymbol IsNot IErrorTypeSymbol AndAlso TypeOf localSymbol.Type IsNot IErrorTypeSymbol Then - typeSymbol = localSymbol.Type - Return True - End If - - Dim fieldSymbol = TryCast(declaredSymbol, IFieldSymbol) - If fieldSymbol IsNot Nothing AndAlso TypeOf fieldSymbol IsNot IErrorTypeSymbol AndAlso TypeOf fieldSymbol.Type IsNot IErrorTypeSymbol Then - typeSymbol = fieldSymbol.Type - Return True - End If - - Return False - End Function - - - Public Function TryReduceOrSimplifyExplicitName( - expression As ExpressionSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - optionSet As OptionSet, - cancellationToken As CancellationToken - ) As Boolean - If expression.TryReduceExplicitName(semanticModel, replacementNode, issueSpan, optionSet, cancellationToken) Then - Return True - End If - - Return expression.TrySimplify(semanticModel, replacementNode, issueSpan, cancellationToken) - End Function - - - Public Function TryReduceExplicitName( - expression As ExpressionSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - optionSet As OptionSet, - cancellationToken As CancellationToken - ) As Boolean - replacementNode = Nothing - issueSpan = Nothing - - If expression.Kind = SyntaxKind.SimpleMemberAccessExpression Then - Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) - Return memberAccess.TryReduce(semanticModel, - replacementNode, - issueSpan, - optionSet, - cancellationToken) - ElseIf TypeOf (expression) Is NameSyntax Then - Dim name = DirectCast(expression, NameSyntax) - Return name.TryReduce(semanticModel, - replacementNode, - issueSpan, - optionSet, - cancellationToken) - End If - - Return False - End Function - - - Private Function TryReduce( - memberAccess As MemberAccessExpressionSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - optionSet As OptionSet, - cancellationToken As CancellationToken - ) As Boolean - If memberAccess.Expression Is Nothing OrElse memberAccess.Name Is Nothing Then - Return False - End If - - If memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind) Then - replacementNode = SyntaxFactory.PredefinedType( - SyntaxFactory.Token( - GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First())))) _ - .WithLeadingTrivia(memberAccess.GetLeadingTrivia()) - - issueSpan = memberAccess.Span - Return True - End If - - ' See https//github.com/dotnet/roslyn/issues/40974 - ' - ' To be very safe, we only support simplifying code that bound to a symbol without any - ' sort of problems. We could potentially relax this in the future. However, we would - ' need to be very careful about the implications of us offering to fixup 'broken' code - ' in a manner that might end up making things worse Or confusing the user. - Dim symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess) - If symbol Is Nothing Then - Return False - End If - - If memberAccess.Expression.IsKind(SyntaxKind.MeExpression) AndAlso - Not SimplificationHelpers.ShouldSimplifyThisOrMeMemberAccessExpression(semanticModel, optionSet, symbol) Then - Return False - End If - - If Not memberAccess.IsRightSideOfDot() Then - Dim aliasReplacement As IAliasSymbol = Nothing - - If memberAccess.TryReplaceWithAlias(semanticModel, aliasReplacement) Then - Dim identifierToken = SyntaxFactory.Identifier( - memberAccess.GetLeadingTrivia(), - aliasReplacement.Name, - memberAccess.GetTrailingTrivia()) - - identifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken( - identifierToken, - semanticModel) - replacementNode = SyntaxFactory.IdentifierName(identifierToken) - - issueSpan = memberAccess.Span - - ' In case the alias name is the same as the last name of the alias target, we only include - ' the left part of the name in the unnecessary span to Not confuse uses. - If memberAccess.Name.Identifier.ValueText = identifierToken.ValueText Then - issueSpan = memberAccess.Expression.Span - End If - - Return True - End If - - If PreferPredefinedTypeKeywordInMemberAccess(memberAccess, optionSet) Then - If (symbol IsNot Nothing AndAlso symbol.IsKind(SymbolKind.NamedType)) Then - Dim keywordKind = GetPredefinedKeywordKind(DirectCast(symbol, INamedTypeSymbol).SpecialType) - If keywordKind <> SyntaxKind.None Then - replacementNode = SyntaxFactory.PredefinedType( - SyntaxFactory.Token( - memberAccess.GetLeadingTrivia(), - keywordKind, - memberAccess.GetTrailingTrivia())) - - replacementNode = replacementNode.WithAdditionalAnnotations( - New SyntaxAnnotation(NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))) - - issueSpan = memberAccess.Span - Return True - End If - End If - End If - End If - - ' a module name was inserted by the name expansion, so removing this should be tried first. - If memberAccess.HasAnnotation(SimplificationHelpers.SimplifyModuleNameAnnotation) Then - If TryOmitModuleName(memberAccess, semanticModel, symbol, replacementNode, issueSpan, cancellationToken) Then - Return True - End If - End If - - replacementNode = memberAccess.GetNameWithTriviaMoved(semanticModel) - issueSpan = memberAccess.Expression.Span - - If memberAccess.CanReplaceWithReducedName(replacementNode, semanticModel, symbol, cancellationToken) Then - Return True - End If - - If TryOmitModuleName(memberAccess, semanticModel, symbol, replacementNode, issueSpan, cancellationToken) Then - Return True - End If - - Return False - End Function - - - Public Function GetNameWithTriviaMoved(memberAccess As MemberAccessExpressionSyntax, - semanticModel As SemanticModel) As SimpleNameSyntax - Dim replacementNode = memberAccess.Name - replacementNode = DirectCast(replacementNode, SimpleNameSyntax) _ - .WithIdentifier(VisualBasicSimplificationService.TryEscapeIdentifierToken( - memberAccess.Name.Identifier, - semanticModel)) _ - .WithLeadingTrivia(memberAccess.GetLeadingTriviaForSimplifiedMemberAccess()) _ - .WithTrailingTrivia(memberAccess.GetTrailingTrivia()) - - Return replacementNode - End Function - - - Private Function GetLeadingTriviaForSimplifiedMemberAccess(memberAccess As MemberAccessExpressionSyntax) As SyntaxTriviaList - ' We want to include any user-typed trivia that may be present between the 'Expression', 'OperatorToken' and 'Identifier' of the MemberAccessExpression. - ' However, we don't want to include any elastic trivia that may have been introduced by the expander in these locations. This is to avoid triggering - ' aggressive formatting. Otherwise, formatter will see this elastic trivia added by the expander And use that as a cue to introduce unnecessary blank lines - ' etc. around the user's original code. - Return memberAccess.GetLeadingTrivia(). - AddRange(memberAccess.Expression.GetTrailingTrivia().WithoutElasticTrivia()). - AddRange(memberAccess.OperatorToken.LeadingTrivia.WithoutElasticTrivia()). - AddRange(memberAccess.OperatorToken.TrailingTrivia.WithoutElasticTrivia()). - AddRange(memberAccess.Name.GetLeadingTrivia().WithoutElasticTrivia()) - End Function - - - Private Function WithoutElasticTrivia(list As IEnumerable(Of SyntaxTrivia)) As IEnumerable(Of SyntaxTrivia) - Return list.Where(Function(t) Not t.IsElastic()) - End Function - Public Function InsideCrefReference(expression As ExpressionSyntax) As Boolean Dim crefAttribute = expression.FirstAncestorOrSelf(Of XmlCrefAttributeSyntax)() Return crefAttribute IsNot Nothing End Function - Private Function InsideNameOfExpression(expr As ExpressionSyntax) As Boolean - Dim nameOfExpression = expr.FirstAncestorOrSelf(Of NameOfExpressionSyntax)() - Return nameOfExpression IsNot Nothing - End Function - - Private Function PreferPredefinedTypeKeywordInMemberAccess(expression As ExpressionSyntax, optionSet As OptionSet) As Boolean - Return (IsInMemberAccessContext(expression) OrElse IsInCrefReferenceForPredefinedTypeInMemberAccessContext(expression)) AndAlso - (Not InsideNameOfExpression(expression)) AndAlso - SimplificationHelpers.PreferPredefinedTypeKeywordInMemberAccess(optionSet, LanguageNames.VisualBasic) - End Function - - ''' - ''' Note: This helper exists solely to work around Bug 1012713. Once it is fixed, this helper must be - ''' deleted in favor of . - ''' Context: Bug 1012713 makes it so that the compiler doesn't support PredefinedType.Member inside crefs - ''' (i.e. System.Int32.MaxValue is supported but Integer.MaxValue isn't). Until this bug is fixed, we don't - ''' support simplifying types names Like System.Int32.MaxValue to Integer.MaxValue. - ''' - Private Function IsInCrefReferenceForPredefinedTypeInMemberAccessContext(expression As ExpressionSyntax) As Boolean - Return (InsideCrefReference(expression) AndAlso Not expression.IsLeftSideOfQualifiedName) - End Function - - Public Function IsInMemberAccessContext(expression As ExpressionSyntax) As Boolean + Public Function IsDirectChildOfMemberAccessExpression(expression As ExpressionSyntax) As Boolean Return TypeOf expression?.Parent Is MemberAccessExpressionSyntax End Function - Private Function PreferPredefinedTypeKeywordInDeclarations(name As NameSyntax, optionSet As OptionSet) As Boolean - Return (Not IsInMemberAccessContext(name)) AndAlso - (Not InsideCrefReference(name)) AndAlso - (Not InsideNameOfExpression(name)) AndAlso - SimplificationHelpers.PreferPredefinedTypeKeywordInDeclarations(optionSet, LanguageNames.VisualBasic) - End Function - Public Function GetRightmostName(node As ExpressionSyntax) As NameSyntax Dim memberAccess = TryCast(node, MemberAccessExpressionSyntax) @@ -1153,777 +797,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Return Nothing End Function - Private Function TryOmitModuleName(memberAccess As MemberAccessExpressionSyntax, - semanticModel As SemanticModel, - symbol As ISymbol, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - cancellationToken As CancellationToken) As Boolean - If memberAccess.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) Then - Dim symbolForMemberAccess = semanticModel.GetSymbolInfo(DirectCast(memberAccess.Parent, MemberAccessExpressionSyntax)).Symbol - If symbolForMemberAccess.IsModuleMember Then - replacementNode = memberAccess.Expression.WithLeadingTrivia(memberAccess.GetLeadingTrivia()) - issueSpan = memberAccess.Name.Span - - Dim parent = DirectCast(memberAccess.Parent, MemberAccessExpressionSyntax) - Dim parentReplacement = parent.ReplaceNode(parent.Expression, replacementNode) - - If parent.CanReplaceWithReducedName(parentReplacement, semanticModel, symbol, cancellationToken) Then - Return True - End If - End If - End If - - Return False - End Function - - - Private Function CanReplaceWithReducedName( - memberAccess As MemberAccessExpressionSyntax, - reducedNode As ExpressionSyntax, - semanticModel As SemanticModel, - symbol As ISymbol, - cancellationToken As CancellationToken - ) As Boolean - If Not IsMeOrNamedTypeOrNamespace(memberAccess.Expression, semanticModel) Then - Return False - End If - - ' See if we can simplify a member access expression of the form E.M or E.M() to M or M() - Dim speculationAnalyzer = New SpeculationAnalyzer(memberAccess, reducedNode, semanticModel, cancellationToken) - If Not speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() OrElse - speculationAnalyzer.ReplacementChangesSemantics() Then - Return False - End If - - If memberAccess.Expression.IsKind(SyntaxKind.MyBaseExpression) Then - Dim enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken) - If enclosingNamedType IsNot Nothing AndAlso - Not enclosingNamedType.IsSealed AndAlso - symbol IsNot Nothing AndAlso - symbol.IsOverridable() Then - Return False - End If - End If - - Return True - End Function - - - Private Function TryReduce( - name As NameSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - optionSet As OptionSet, - cancellationToken As CancellationToken - ) As Boolean - - ' do not simplify names of a namespace declaration - If IsPartOfNamespaceDeclarationName(name) Then - Return False - End If - - ' see whether binding the name binds to a symbol/type. if not, it is ambiguous and - ' nothing we can do here. - Dim symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, name) - If SimplificationHelpers.IsValidSymbolInfo(symbol) Then - If symbol.Kind = SymbolKind.Method AndAlso symbol.IsConstructor() Then - symbol = symbol.ContainingType - End If - - If symbol.Kind = SymbolKind.Method AndAlso name.Kind = SyntaxKind.GenericName Then - Dim genericName = DirectCast(name, GenericNameSyntax) - replacementNode = SyntaxFactory.IdentifierName(genericName.Identifier).WithLeadingTrivia(genericName.GetLeadingTrivia()).WithTrailingTrivia(genericName.GetTrailingTrivia()) - - issueSpan = genericName.TypeArgumentList.Span - Return name.CanReplaceWithReducedName(replacementNode, semanticModel, cancellationToken) - End If - - If Not TypeOf symbol Is INamespaceOrTypeSymbol Then - Return False - End If - Else - Return False - End If - - If name.HasAnnotations(SpecialTypeAnnotation.Kind) Then - replacementNode = SyntaxFactory.PredefinedType( - SyntaxFactory.Token(name.GetLeadingTrivia(), - GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(name.GetAnnotations(SpecialTypeAnnotation.Kind).First())), - name.GetTrailingTrivia())) - - issueSpan = name.Span - - Return name.CanReplaceWithReducedNameInContext(replacementNode) - Else - - If Not name.IsRightSideOfDot() Then - - Dim aliasReplacement As IAliasSymbol = Nothing - If name.TryReplaceWithAlias(semanticModel, aliasReplacement) Then - Dim identifierToken = SyntaxFactory.Identifier( - name.GetLeadingTrivia(), - aliasReplacement.Name, - name.GetTrailingTrivia()) - - identifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken( - identifierToken, - semanticModel) - - replacementNode = SyntaxFactory.IdentifierName(identifierToken) - - Dim annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(RenameAnnotation.Kind) - For Each annotatedNodeOrToken In annotatedNodesOrTokens - If annotatedNodeOrToken.IsToken Then - identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken) - Else - replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode) - End If - Next - - annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(AliasAnnotation.Kind) - For Each annotatedNodeOrToken In annotatedNodesOrTokens - If annotatedNodeOrToken.IsToken Then - identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken) - Else - replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode) - End If - Next - - replacementNode = DirectCast(replacementNode, SimpleNameSyntax).WithIdentifier(identifierToken) - issueSpan = name.Span - - ' In case the alias name is the same as the last name of the alias target, we only include - ' the left part of the name in the unnecessary span to Not confuse uses. - If name.Kind = SyntaxKind.QualifiedName Then - Dim qualifiedName As QualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax) - - If qualifiedName.Right.Identifier.ValueText = identifierToken.ValueText Then - issueSpan = qualifiedName.Left.Span - End If - End If - - If name.CanReplaceWithReducedNameInContext(replacementNode) Then - ' check if the alias name ends with an Attribute suffix that can be omitted. - Dim replacementNodeWithoutAttributeSuffix As ExpressionSyntax = Nothing - Dim issueSpanWithoutAttributeSuffix As TextSpan = Nothing - If TryReduceAttributeSuffix(name, identifierToken, semanticModel, aliasReplacement IsNot Nothing, replacementNodeWithoutAttributeSuffix, issueSpanWithoutAttributeSuffix, cancellationToken) Then - If name.CanReplaceWithReducedName(replacementNodeWithoutAttributeSuffix, semanticModel, cancellationToken) Then - replacementNode = replacementNode.CopyAnnotationsTo(replacementNodeWithoutAttributeSuffix) - issueSpan = issueSpanWithoutAttributeSuffix - End If - End If - - Return True - End If - - Return False - End If - - Dim nameHasNoAlias = False - - If TypeOf name Is SimpleNameSyntax Then - Dim simpleName = DirectCast(name, SimpleNameSyntax) - If Not simpleName.Identifier.HasAnnotations(AliasAnnotation.Kind) Then - nameHasNoAlias = True - End If - End If - - If TypeOf name Is QualifiedNameSyntax Then - Dim qualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax) - If Not qualifiedNameSyntax.Right.Identifier.HasAnnotations(AliasAnnotation.Kind) Then - nameHasNoAlias = True - End If - End If - - Dim aliasInfo = semanticModel.GetAliasInfo(name, cancellationToken) - - ' Don't simplify to predefined type if name is part of a QualifiedName. - ' QualifiedNames can't contain PredefinedTypeNames (although MemberAccessExpressions can). - ' In other words, the left side of a QualifiedName can't be a PredefinedTypeName. - If nameHasNoAlias AndAlso aliasInfo Is Nothing AndAlso Not name.Parent.IsKind(SyntaxKind.QualifiedName) Then - Dim type = semanticModel.GetTypeInfo(name).Type - If type IsNot Nothing Then - Dim keywordKind = GetPredefinedKeywordKind(type.SpecialType) - If keywordKind <> SyntaxKind.None Then - ' But do simplify to predefined type if not simplifying results in just the addition of escaping - ' brackets. E.g., even if specified otherwise, prefer `String` to `[String]`. - Dim token = SyntaxFactory.Token( - name.GetLeadingTrivia(), - keywordKind, - name.GetTrailingTrivia()) - Dim valueText = TryCast(name, IdentifierNameSyntax)?.Identifier.ValueText - Dim inDeclarationContext = PreferPredefinedTypeKeywordInDeclarations(name, optionSet) - Dim inMemberAccessContext = PreferPredefinedTypeKeywordInMemberAccess(name, optionSet) - If token.Text = valueText OrElse (inDeclarationContext OrElse inMemberAccessContext) Then - - Dim codeStyleOptionName As String = Nothing - If inDeclarationContext Then - codeStyleOptionName = NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInDeclaration) - ElseIf inMemberAccessContext Then - codeStyleOptionName = NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess) - End If - - replacementNode = SyntaxFactory.PredefinedType(token) - issueSpan = name.Span - - Dim canReplace = name.CanReplaceWithReducedNameInContext(replacementNode) - If canReplace Then - replacementNode = replacementNode.WithAdditionalAnnotations(New SyntaxAnnotation(codeStyleOptionName)) - End If - - Return canReplace - End If - End If - End If - End If - - ' Nullable rewrite: Nullable(Of Integer) -> Integer? - ' Don't rewrite in the case where Nullable(Of Integer) is part of some qualified name like Nullable(Of Integer).Something - If (symbol.Kind = SymbolKind.NamedType) AndAlso (Not name.IsLeftSideOfQualifiedName) Then - Dim type = DirectCast(symbol, INamedTypeSymbol) - If aliasInfo Is Nothing AndAlso CanSimplifyNullable(type, name) Then - Dim genericName As GenericNameSyntax - If name.Kind = SyntaxKind.QualifiedName Then - genericName = DirectCast(DirectCast(name, QualifiedNameSyntax).Right, GenericNameSyntax) - Else - genericName = DirectCast(name, GenericNameSyntax) - End If - - Dim oldType = genericName.TypeArgumentList.Arguments.First() - replacementNode = SyntaxFactory.NullableType(oldType).WithLeadingTrivia(name.GetLeadingTrivia()) - - issueSpan = name.Span - - If name.CanReplaceWithReducedNameInContext(replacementNode) Then - Return True - End If - End If - End If - End If - - Select Case name.Kind - Case SyntaxKind.QualifiedName - ' a module name was inserted by the name expansion, so removing this should be tried first. - Dim qualifiedName = DirectCast(name, QualifiedNameSyntax) - If qualifiedName.HasAnnotation(SimplificationHelpers.SimplifyModuleNameAnnotation) Then - If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then - Return True - End If - End If - - replacementNode = qualifiedName.Right.WithLeadingTrivia(name.GetLeadingTrivia()) - replacementNode = DirectCast(replacementNode, SimpleNameSyntax) _ - .WithIdentifier(VisualBasicSimplificationService.TryEscapeIdentifierToken( - DirectCast(replacementNode, SimpleNameSyntax).Identifier, - semanticModel)) - issueSpan = qualifiedName.Left.Span - - - If name.CanReplaceWithReducedName(replacementNode, semanticModel, cancellationToken) Then - Return True - End If - - If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then - Return True - End If - - Case SyntaxKind.IdentifierName - Dim identifier = DirectCast(name, IdentifierNameSyntax).Identifier - TryReduceAttributeSuffix(name, identifier, semanticModel, False, replacementNode, issueSpan, cancellationToken) - End Select - End If - - If replacementNode Is Nothing Then - Return False - End If - - Return name.CanReplaceWithReducedName(replacementNode, semanticModel, cancellationToken) - End Function - - Private Function CanSimplifyNullable(type As INamedTypeSymbol, name As NameSyntax) As Boolean - If Not type.IsNullable Then - Return False - End If - - If type.IsUnboundGenericType Then - ' Don't simplify unbound generic type "Nullable(Of )". - Return False - End If - - If InsideNameOfExpression(name) Then - ' Nullable(Of T) can't be simplified to T? in nameof expressions. - Return False - End If - - If Not InsideCrefReference(name) Then - ' Nullable(Of T) can always be simplified to T? outside crefs. - Return True - End If - - ' Inside crefs, if the T in this Nullable(Of T) is being declared right here - ' then this Nullable(Of T) is not a constructed generic type and we should - ' not offer to simplify this to T?. - ' - ' For example, we should not offer the simplification in the following cases where - ' T does not bind to an existing type / type parameter in the user's code. - ' - - ' - - ' - ' And we should offer the simplification in the following cases where SomeType and - ' SomeMethod bind to a type and method declared elsewhere in the users code. - ' - - - If name.IsKind(SyntaxKind.GenericName) Then - If (name.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="Nullable(Of T)" - (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso name.Parent?.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="System.Nullable(Of T)" - (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso (name.Parent?.IsParentKind(SyntaxKind.QualifiedName)).GetValueOrDefault() AndAlso name.Parent.Parent?.IsParentKind(SyntaxKind.CrefReference)) Then ' cref="System.Nullable(Of T).Value" - ' Unfortunately, unlike in corresponding C# case, we need syntax based checking to detect these cases because of bugs in the VB SemanticModel. - ' See https://github.com/dotnet/roslyn/issues/2196, https://github.com/dotnet/roslyn/issues/2197 - Return False - End If - End If - - Dim argument = type.TypeArguments.SingleOrDefault() - If argument Is Nothing OrElse argument.IsErrorType() Then - Return False - End If - - Dim argumentDecl = argument.DeclaringSyntaxReferences.FirstOrDefault() - If argumentDecl Is Nothing Then - ' The type argument is a type from metadata - so this is a constructed generic nullable type that can be simplified (e.g. Nullable(Of Integer)). - Return True - End If - - Return Not name.Span.Contains(argumentDecl.Span) - End Function - - Private Function TryReduceAttributeSuffix( - name As NameSyntax, - identifierToken As SyntaxToken, - semanticModel As SemanticModel, - isIdentifierNameFromAlias As Boolean, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - cancellationToken As CancellationToken - ) As Boolean - If SyntaxFacts.IsAttributeName(name) AndAlso Not isIdentifierNameFromAlias Then - - ' When the replacement is an Alias we don't want the "Attribute" Suffix to be removed because this will result in symbol change - Dim aliasSymbol = semanticModel.GetAliasInfo(name, cancellationToken) - If aliasSymbol IsNot Nothing AndAlso - String.Compare(aliasSymbol.Name, identifierToken.ValueText, StringComparison.OrdinalIgnoreCase) = 0 Then - Return False - End If - - If name.Parent.Kind = SyntaxKind.Attribute OrElse name.IsRightSideOfDot() Then - Dim newIdentifierText = String.Empty - - ' an attribute that should keep it (unnecessary "Attribute" suffix should be annotated with a DontSimplifyAnnotation - If identifierToken.HasAnnotation(SimplificationHelpers.DontSimplifyAnnotation) Then - newIdentifierText = identifierToken.ValueText + "Attribute" - ElseIf identifierToken.ValueText.TryReduceAttributeSuffix(newIdentifierText) Then - issueSpan = New TextSpan(name.Span.End - 9, 9) - Else - Return False - End If - - ' escape it (VB allows escaping even for abbreviated identifiers, C# does not!) - Dim newIdentifierToken = identifierToken.CopyAnnotationsTo( - SyntaxFactory.Identifier( - identifierToken.LeadingTrivia, - newIdentifierText, - identifierToken.TrailingTrivia)) - newIdentifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken(newIdentifierToken, semanticModel) - replacementNode = SyntaxFactory.IdentifierName(newIdentifierToken).WithLeadingTrivia(name.GetLeadingTrivia()) - Return True - End If - End If - - Return False - End Function - - ''' - ''' Checks if the SyntaxNode is a name of a namespace declaration. To be a namespace name, the syntax - ''' must be parented by an namespace declaration and the node itself must be equal to the declaration's Name - ''' property. - ''' - Private Function IsPartOfNamespaceDeclarationName(node As SyntaxNode) As Boolean - - Dim nextNode As SyntaxNode = node - - Do While nextNode IsNot Nothing - - Select Case nextNode.Kind - - Case SyntaxKind.IdentifierName, SyntaxKind.QualifiedName - node = nextNode - nextNode = nextNode.Parent - - Case SyntaxKind.NamespaceStatement - Dim namespaceStatement = DirectCast(nextNode, NamespaceStatementSyntax) - Return namespaceStatement.Name Is node - - Case Else - Return False - - End Select - - Loop - - Return False - End Function - - Private Function TryOmitModuleName(name As QualifiedNameSyntax, semanticModel As SemanticModel, ByRef replacementNode As ExpressionSyntax, ByRef issueSpan As TextSpan, cancellationToken As CancellationToken) As Boolean - If name.IsParentKind(SyntaxKind.QualifiedName) Then - Dim symbolForName = semanticModel.GetSymbolInfo(DirectCast(name.Parent, QualifiedNameSyntax)).Symbol - - ' in case this QN is used in a "New NSName.ModuleName.MemberName()" expression - ' the returned symbol is a constructor. Then we need to get the containing type. - If symbolForName.IsConstructor Then - symbolForName = symbolForName.ContainingType - End If - - If symbolForName.IsModuleMember Then - - replacementNode = name.Left.WithLeadingTrivia(name.GetLeadingTrivia()) - issueSpan = name.Right.Span - - Dim parent = DirectCast(name.Parent, QualifiedNameSyntax) - Dim parentReplacement = parent.ReplaceNode(parent.Left, replacementNode) - - If parent.CanReplaceWithReducedName(parentReplacement, semanticModel, cancellationToken) Then - Return True - End If - End If - End If - - Return False - End Function - - - Private Function TrySimplify( - expression As ExpressionSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan, - cancellationToken As CancellationToken) As Boolean - - replacementNode = Nothing - issueSpan = Nothing - - Select Case expression.Kind - Case SyntaxKind.SimpleMemberAccessExpression - If True Then - Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) - Dim newLeft As ExpressionSyntax = Nothing - If TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, newLeft, issueSpan) Then - ' replacement node might not be in it's simplest form, so add simplify annotation to it. - replacementNode = memberAccess.Update(memberAccess.Kind, newLeft, memberAccess.OperatorToken, memberAccess.Name).WithAdditionalAnnotations(Simplifier.Annotation) - - ' Ensure that replacement doesn't change semantics. - Return Not ReplacementChangesSemantics(memberAccess, replacementNode, semanticModel, cancellationToken) - End If - - Return False - End If - - Case SyntaxKind.QualifiedName - If True Then - Dim qualifiedName = DirectCast(expression, QualifiedNameSyntax) - Dim newLeft As ExpressionSyntax = Nothing - If TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, newLeft, issueSpan) Then - If Not TypeOf newLeft Is NameSyntax Then - Contract.Fail("QualifiedName Left = " + qualifiedName.Left.ToString() + " and QualifiedName Right = " + qualifiedName.Right.ToString() + " . Left is tried to be replaced with the PredefinedType " + replacementNode.ToString()) - End If - - ' replacement node might not be in it's simplest form, so add simplify annotation to it. - replacementNode = qualifiedName.Update(DirectCast(newLeft, NameSyntax), qualifiedName.DotToken, qualifiedName.Right).WithAdditionalAnnotations(Simplifier.Annotation) - - ' Ensure that replacement doesn't change semantics. - Return Not ReplacementChangesSemantics(qualifiedName, replacementNode, semanticModel, cancellationToken) - End If - - Return False - End If - End Select - - Return False - End Function - - Private Function ReplacementChangesSemantics( - originalExpression As ExpressionSyntax, - replacedExpression As ExpressionSyntax, - semanticModel As SemanticModel, - cancellationToken As CancellationToken) As Boolean - Dim speculationAnalyzer = New SpeculationAnalyzer(originalExpression, replacedExpression, semanticModel, cancellationToken) - Return speculationAnalyzer.ReplacementChangesSemantics() - End Function - - ' Note: The caller needs to verify that replacement doesn't change semantics of the original expression. - Private Function TrySimplifyMemberAccessOrQualifiedName( - left As ExpressionSyntax, - right As ExpressionSyntax, - semanticModel As SemanticModel, - ByRef replacementNode As ExpressionSyntax, - ByRef issueSpan As TextSpan - ) As Boolean - replacementNode = Nothing - issueSpan = Nothing - - If left IsNot Nothing AndAlso right IsNot Nothing Then - Dim leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left) - If leftSymbol IsNot Nothing AndAlso leftSymbol.Kind = SymbolKind.NamedType Then - Dim rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right) - If rightSymbol IsNot Nothing AndAlso (rightSymbol.IsStatic OrElse rightSymbol.Kind = SymbolKind.NamedType) Then - ' Static member access or nested type member access. - Dim containingType As INamedTypeSymbol = rightSymbol.ContainingType - Dim isInCref = left.Ancestors(ascendOutOfTrivia:=True).OfType(Of CrefReferenceSyntax)().Any() - - ' Crefs in VB , irrespective of the expression are parsed as QualifiedName (no MemberAccessExpression) - ' Hence the Left can never be a PredefinedType (or anything other than NameSyntax) - If isInCref AndAlso TypeOf rightSymbol Is IMethodSymbol AndAlso Not containingType.SpecialType = SpecialType.None Then - Return False - End If - - If containingType IsNot Nothing AndAlso Not containingType.Equals(leftSymbol) Then - - Dim namedType = TryCast(leftSymbol, INamedTypeSymbol) - If namedType IsNot Nothing AndAlso - containingType.TypeArguments.Length <> 0 Then - Return False - End If - - ' We have a static member access or a nested type member access using a more derived type. - ' Simplify syntax so as to use accessed member's most immediate containing type instead of the derived type. - replacementNode = containingType.GenerateTypeSyntax().WithLeadingTrivia(left.GetLeadingTrivia()).WithTrailingTrivia(left.GetTrailingTrivia()).WithAdditionalAnnotations(Simplifier.Annotation) - issueSpan = left.Span - Return True - End If - End If - End If - End If - - Return False - End Function - - - Private Function TryReplaceWithAlias( - node As ExpressionSyntax, - semanticModel As SemanticModel, - ByRef aliasReplacement As IAliasSymbol) As Boolean - aliasReplacement = Nothing - - If Not node.IsAliasReplaceableExpression() Then - Return False - End If - - Dim symbol = semanticModel.GetSymbolInfo(node).Symbol - - If (symbol.IsConstructor()) Then - symbol = symbol.ContainingType - End If - - ' The following condition checks if the user has used alias in the original code and - ' if so the expression is replaced with the Alias - If TypeOf node Is QualifiedNameSyntax Then - Dim qualifiedNameNode = DirectCast(node, QualifiedNameSyntax) - If qualifiedNameNode.Right.Identifier.HasAnnotations(AliasAnnotation.Kind) Then - Dim aliasAnnotationInfo = qualifiedNameNode.Right.Identifier.GetAnnotations(AliasAnnotation.Kind).Single() - - Dim aliasName = AliasAnnotation.GetAliasName(aliasAnnotationInfo) - Dim aliasIdentifier = SyntaxFactory.IdentifierName(aliasName) - - Dim aliasTypeInfo = semanticModel.GetSpeculativeAliasInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsTypeOrNamespace) - - If Not aliasTypeInfo Is Nothing Then - aliasReplacement = aliasTypeInfo - Return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol) - End If - End If - End If - - If node.Kind = SyntaxKind.IdentifierName AndAlso semanticModel.GetAliasInfo(DirectCast(node, IdentifierNameSyntax)) IsNot Nothing Then - Return False - End If - - ' an alias can only replace a type Or namespace - If symbol Is Nothing OrElse - (symbol.Kind <> SymbolKind.Namespace AndAlso symbol.Kind <> SymbolKind.NamedType) Then - - Return False - End If - - If symbol Is Nothing OrElse Not TypeOf (symbol) Is INamespaceOrTypeSymbol Then - Return False - End If - - Dim preferAliasToQualifiedName = True - - If TypeOf node Is QualifiedNameSyntax Then - Dim qualifiedName = DirectCast(node, QualifiedNameSyntax) - If Not qualifiedName.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation) Then - Dim type = semanticModel.GetTypeInfo(node).Type - If Not type Is Nothing Then - Dim keywordKind = GetPredefinedKeywordKind(type.SpecialType) - If keywordKind <> SyntaxKind.None Then - preferAliasToQualifiedName = False - End If - End If - End If - End If - - aliasReplacement = DirectCast(symbol, INamespaceOrTypeSymbol).GetAliasForSymbol(node, semanticModel) - If aliasReplacement IsNot Nothing And preferAliasToQualifiedName Then - Return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol) - End If - - Return False - End Function - - ' We must verify that the alias actually binds back to the thing it's aliasing. - ' It's possible there is another symbol with the same name as the alias that binds first - Private Function ValidateAliasForTarget(aliasReplacement As IAliasSymbol, semanticModel As SemanticModel, node As ExpressionSyntax, symbol As ISymbol) As Boolean - Dim aliasName = aliasReplacement.Name - - ' If we're the argument of a NameOf(X.Y) call, then we can't simplify to an - ' alias unless the alias has the same name as us (i.e. 'Y'). - If node.IsNameOfArgumentExpression() Then - Dim nameofValueOpt = semanticModel.GetConstantValue(node.Parent.Parent.Parent) - If Not nameofValueOpt.HasValue Then - Return False - End If - - Dim existingValue = TryCast(nameofValueOpt.Value, String) - If existingValue Is Nothing OrElse existingValue <> aliasName Then - Return False - End If - End If - - Dim boundSymbols = semanticModel.LookupNamespacesAndTypes(node.SpanStart, name:=aliasName) - If boundSymbols.Length = 1 Then - Dim boundAlias = TryCast(boundSymbols(0), IAliasSymbol) - If boundAlias IsNot Nothing And aliasReplacement.Target.Equals(symbol) Then - If symbol.IsAttribute Then - boundSymbols = semanticModel.LookupNamespacesAndTypes(node.Span.Start, name:=aliasName + "Attribute") - Return boundSymbols.IsEmpty - End If - - Return True - End If - End If - Return False - End Function - Public Function IsNameOfArgumentExpression(expression As ExpressionSyntax) As Boolean Return expression.IsParentKind(SyntaxKind.NameOfExpression) End Function - - Private Function CanReplaceWithReducedName( - name As NameSyntax, - replacementNode As ExpressionSyntax, - semanticModel As SemanticModel, - cancellationToken As CancellationToken - ) As Boolean - Dim speculationAnalyzer = New SpeculationAnalyzer(name, replacementNode, semanticModel, cancellationToken) - If speculationAnalyzer.ReplacementChangesSemantics() Then - Return False - End If - - Return name.CanReplaceWithReducedNameInContext(replacementNode) - End Function - - - Private Function CanReplaceWithReducedNameInContext(name As NameSyntax, replacementNode As ExpressionSyntax) As Boolean - - ' Special case. if this new minimal name parses out to a predefined type, then we - ' have to make sure that we're not in a using alias. That's the one place where the - ' language doesn't allow predefined types. You have to use the fully qualified name - ' instead. - Dim invalidTransformation1 = IsNonNameSyntaxInImportsDirective(name, replacementNode) - Dim invalidTransformation2 = IsReservedNameInAttribute(name, replacementNode) - - Dim invalidTransformation3 = IsNullableTypeSyntaxLeftOfDotInMemberAccess(name, replacementNode) - - Return Not (invalidTransformation1 OrElse invalidTransformation2 OrElse invalidTransformation3) - End Function - - Private Function IsMeOrNamedTypeOrNamespace(expression As ExpressionSyntax, semanticModel As SemanticModel) As Boolean - If expression.Kind = SyntaxKind.MeExpression Then - Return True - End If - - Dim expressionInfo = semanticModel.GetSymbolInfo(expression) - If SimplificationHelpers.IsValidSymbolInfo(expressionInfo.Symbol) Then - If TypeOf expressionInfo.Symbol Is INamespaceOrTypeSymbol Then - Return True - End If - - If expressionInfo.Symbol.IsThisParameter() Then - Return True - End If - End If - - Return False - End Function - - ''' - ''' Returns the predefined keyword kind for a given special type. - ''' - ''' The specialtype of this type. - ''' The keyword kind for a given special type, or SyntaxKind.None if the type name is not a predefined type. - Private Function GetPredefinedKeywordKind(type As SpecialType) As SyntaxKind - Select Case type - Case SpecialType.System_Boolean - Return SyntaxKind.BooleanKeyword - Case SpecialType.System_Byte - Return SyntaxKind.ByteKeyword - Case SpecialType.System_SByte - Return SyntaxKind.SByteKeyword - Case SpecialType.System_Int32 - Return SyntaxKind.IntegerKeyword - Case SpecialType.System_UInt32 - Return SyntaxKind.UIntegerKeyword - Case SpecialType.System_Int16 - Return SyntaxKind.ShortKeyword - Case SpecialType.System_UInt16 - Return SyntaxKind.UShortKeyword - Case SpecialType.System_Int64 - Return SyntaxKind.LongKeyword - Case SpecialType.System_UInt64 - Return SyntaxKind.ULongKeyword - Case SpecialType.System_Single - Return SyntaxKind.SingleKeyword - Case SpecialType.System_Double - Return SyntaxKind.DoubleKeyword - Case SpecialType.System_Decimal - Return SyntaxKind.DecimalKeyword - Case SpecialType.System_String - Return SyntaxKind.StringKeyword - Case SpecialType.System_Char - Return SyntaxKind.CharKeyword - Case SpecialType.System_Object - Return SyntaxKind.ObjectKeyword - Case SpecialType.System_DateTime - Return SyntaxKind.DateKeyword - Case Else - Return SyntaxKind.None - End Select - End Function - - Private Function IsNullableTypeSyntaxLeftOfDotInMemberAccess(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean - Return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso - simplifiedNode.Kind = SyntaxKind.NullableType - End Function - - Private Function IsNonNameSyntaxInImportsDirective(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean - Return TypeOf expression.Parent Is ImportsClauseSyntax AndAlso - Not TypeOf simplifiedNode Is NameSyntax - End Function - Public Function IsReservedNameInAttribute(originalName As NameSyntax, simplifiedNode As ExpressionSyntax) As Boolean Dim attribute = originalName.GetAncestorOrThis(Of AttributeSyntax)() If attribute Is Nothing Then diff --git a/src/Workspaces/VisualBasic/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.vb b/src/Workspaces/VisualBasic/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.vb index b6ac6c1fee936..2b351653f27cc 100644 --- a/src/Workspaces/VisualBasic/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.vb +++ b/src/Workspaces/VisualBasic/Portable/Extensions/MemberAccessExpressionSyntaxExtensions.vb @@ -10,6 +10,7 @@ Imports System.Text Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Simplification Imports Microsoft.CodeAnalysis.VisualBasic.Symbols Imports Microsoft.CodeAnalysis.VisualBasic.Syntax @@ -125,5 +126,36 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Return Nothing End Function + + + Public Function GetNameWithTriviaMoved(memberAccess As MemberAccessExpressionSyntax, + semanticModel As SemanticModel) As SimpleNameSyntax + Dim replacementNode = memberAccess.Name + replacementNode = DirectCast(replacementNode, SimpleNameSyntax) _ + .WithIdentifier(VisualBasicSimplificationService.TryEscapeIdentifierToken( + memberAccess.Name.Identifier, + semanticModel)) _ + .WithLeadingTrivia(GetLeadingTriviaForSimplifiedMemberAccess(memberAccess)) _ + .WithTrailingTrivia(memberAccess.GetTrailingTrivia()) + + Return replacementNode + End Function + + Private Function GetLeadingTriviaForSimplifiedMemberAccess(memberAccess As MemberAccessExpressionSyntax) As SyntaxTriviaList + ' We want to include any user-typed trivia that may be present between the 'Expression', 'OperatorToken' and 'Identifier' of the MemberAccessExpression. + ' However, we don't want to include any elastic trivia that may have been introduced by the expander in these locations. This is to avoid triggering + ' aggressive formatting. Otherwise, formatter will see this elastic trivia added by the expander And use that as a cue to introduce unnecessary blank lines + ' etc. around the user's original code. + Return SyntaxFactory.TriviaList(WithoutElasticTrivia( + memberAccess.GetLeadingTrivia(). + AddRange(memberAccess.Expression.GetTrailingTrivia()). + AddRange(memberAccess.OperatorToken.LeadingTrivia). + AddRange(memberAccess.OperatorToken.TrailingTrivia). + AddRange(memberAccess.Name.GetLeadingTrivia()))) + End Function + + Private Function WithoutElasticTrivia(list As IEnumerable(Of SyntaxTrivia)) As IEnumerable(Of SyntaxTrivia) + Return list.Where(Function(t) Not t.IsElastic()) + End Function End Module End Namespace diff --git a/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicNameReducer.vb b/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicNameReducer.vb index b1905e1d16ecb..2326cefe8b8a6 100644 --- a/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicNameReducer.vb +++ b/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicNameReducer.vb @@ -9,6 +9,7 @@ Imports Microsoft.CodeAnalysis.Options Imports Microsoft.CodeAnalysis.PooledObjects Imports Microsoft.CodeAnalysis.Simplification Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification @@ -33,11 +34,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification Dim replacementNode As ExpressionSyntax = Nothing Dim issueSpan As TextSpan - If Not node.TryReduceOrSimplifyExplicitName(semanticModel, - replacementNode, - issueSpan, - optionSet, - cancellationToken) Then + If Not ExpressionSimplifier.Instance.TrySimplify( + node, semanticModel, optionSet, + replacementNode, issueSpan, cancellationToken) Then Return node End If diff --git a/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicVariableDeclaratorReducer.vb b/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicVariableDeclaratorReducer.vb index 76822464da1fa..c367eba39c7d8 100644 --- a/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicVariableDeclaratorReducer.vb +++ b/src/Workspaces/VisualBasic/Portable/Simplification/Reducers/VisualBasicVariableDeclaratorReducer.vb @@ -2,6 +2,7 @@ ' 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.Runtime.InteropServices Imports System.Threading Imports Microsoft.CodeAnalysis.Formatting Imports Microsoft.CodeAnalysis.Options @@ -32,12 +33,134 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification Dim replacementNode As SyntaxNode = Nothing Dim issueSpan As TextSpan - If Not node.TryReduceVariableDeclaratorWithoutType(semanticModel, replacementNode, issueSpan) Then + If Not TryReduceVariableDeclaratorWithoutType( + node, semanticModel, replacementNode, issueSpan) Then Return node End If replacementNode = node.CopyAnnotationsTo(replacementNode).WithAdditionalAnnotations(Formatter.Annotation) Return replacementNode.WithoutAnnotations(Simplifier.Annotation) End Function + + Private Shared Function TryReduceVariableDeclaratorWithoutType( + variableDeclarator As VariableDeclaratorSyntax, + semanticModel As SemanticModel, + ByRef replacementNode As SyntaxNode, + ByRef issueSpan As TextSpan) As Boolean + + replacementNode = Nothing + issueSpan = Nothing + + ' Failfast Conditions + If variableDeclarator.AsClause Is Nothing OrElse + Not variableDeclarator.Parent.IsKind( + SyntaxKind.LocalDeclarationStatement, + SyntaxKind.UsingStatement, + SyntaxKind.ForStatement, + SyntaxKind.ForEachStatement, + SyntaxKind.FieldDeclaration) Then + Return False + End If + + If variableDeclarator.Names.Count <> 1 Then + Return False + End If + + Dim parent = variableDeclarator.Parent + Dim modifiedIdentifier = variableDeclarator.Names.Single() + + Dim simpleAsClause = TryCast(variableDeclarator.AsClause, SimpleAsClauseSyntax) + If simpleAsClause Is Nothing Then + Return False + End If + + If (parent.IsKind(SyntaxKind.LocalDeclarationStatement, SyntaxKind.UsingStatement, SyntaxKind.FieldDeclaration) AndAlso + variableDeclarator.Initializer IsNot Nothing) Then + + ' Type Check + + Dim declaredSymbolType As ITypeSymbol = Nothing + If Not HasValidDeclaredTypeSymbol(modifiedIdentifier, semanticModel, declaredSymbolType) Then + Return False + End If + + Dim initializerType As ITypeSymbol + + If declaredSymbolType.IsArrayType() AndAlso variableDeclarator.Initializer.Value.Kind() = SyntaxKind.CollectionInitializer Then + ' Get type of the array literal in context without the target type + initializerType = semanticModel.GetSpeculativeTypeInfo(variableDeclarator.Initializer.Value.SpanStart, variableDeclarator.Initializer.Value, SpeculativeBindingOption.BindAsExpression).ConvertedType + Else + initializerType = semanticModel.GetTypeInfo(variableDeclarator.Initializer.Value).Type + End If + + If Not declaredSymbolType.Equals(initializerType) Then + Return False + End If + + Dim newModifiedIdentifier = SyntaxFactory.ModifiedIdentifier(modifiedIdentifier.Identifier) ' LeadingTrivia is copied here + replacementNode = SyntaxFactory.VariableDeclarator(SyntaxFactory.SingletonSeparatedList(newModifiedIdentifier.WithTrailingTrivia(variableDeclarator.AsClause.GetTrailingTrivia())), + asClause:=Nothing, + initializer:=variableDeclarator.Initializer) 'TrailingTrivia is copied here + issueSpan = variableDeclarator.Span + Return True + End If + + If (parent.IsKind(SyntaxKind.ForEachStatement, SyntaxKind.ForStatement)) Then + ' Type Check for ForStatement + If parent.IsKind(SyntaxKind.ForStatement) Then + Dim declaredSymbolType As ITypeSymbol = Nothing + If Not HasValidDeclaredTypeSymbol(modifiedIdentifier, semanticModel, declaredSymbolType) Then + Return False + End If + + Dim valueType = semanticModel.GetTypeInfo(DirectCast(parent, ForStatementSyntax).ToValue).Type + + If Not valueType.Equals(declaredSymbolType) Then + Return False + End If + End If + + If parent.IsKind(SyntaxKind.ForEachStatement) Then + Dim forEachStatementInfo = semanticModel.GetForEachStatementInfo(DirectCast(parent, ForEachStatementSyntax)) + If Not forEachStatementInfo.ElementConversion.IsIdentity Then + Return False + End If + End If + + Dim newIdentifierName = SyntaxFactory.IdentifierName(modifiedIdentifier.Identifier) ' Leading Trivia is copied here + replacementNode = newIdentifierName.WithTrailingTrivia(variableDeclarator.AsClause.GetTrailingTrivia()) ' Trailing Trivia is copied here + issueSpan = variableDeclarator.Span + Return True + End If + + Return False + End Function + + Private Shared Function HasValidDeclaredTypeSymbol( + modifiedIdentifier As ModifiedIdentifierSyntax, + semanticModel As SemanticModel, + ByRef typeSymbol As ITypeSymbol) As Boolean + + Dim declaredSymbol = semanticModel.GetDeclaredSymbol(modifiedIdentifier) + If declaredSymbol Is Nothing OrElse + (Not TypeOf declaredSymbol Is ILocalSymbol AndAlso Not TypeOf declaredSymbol Is IFieldSymbol) Then + Return False + End If + + Dim localSymbol = TryCast(declaredSymbol, ILocalSymbol) + If localSymbol IsNot Nothing AndAlso TypeOf localSymbol IsNot IErrorTypeSymbol AndAlso TypeOf localSymbol.Type IsNot IErrorTypeSymbol Then + typeSymbol = localSymbol.Type + Return True + End If + + Dim fieldSymbol = TryCast(declaredSymbol, IFieldSymbol) + If fieldSymbol IsNot Nothing AndAlso TypeOf fieldSymbol IsNot IErrorTypeSymbol AndAlso TypeOf fieldSymbol.Type IsNot IErrorTypeSymbol Then + typeSymbol = fieldSymbol.Type + Return True + End If + + Return False + End Function + End Class End Namespace diff --git a/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/AbstractVisualBasicSimplifier.vb b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/AbstractVisualBasicSimplifier.vb new file mode 100644 index 0000000000000..e2cb9deeb1bbf --- /dev/null +++ b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/AbstractVisualBasicSimplifier.vb @@ -0,0 +1,202 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Runtime.InteropServices +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Options +Imports Microsoft.CodeAnalysis.Simplification +Imports Microsoft.CodeAnalysis.Simplification.Simplifiers +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers + Friend MustInherit Class AbstractVisualBasicSimplifier(Of TSyntax As SyntaxNode, TSimplifiedSyntax As SyntaxNode) + Inherits AbstractSimplifier(Of TSyntax, TSimplifiedSyntax) + + ''' + ''' Returns the predefined keyword kind for a given special type. + ''' + ''' The specialtype of this type. + ''' The keyword kind for a given special type, or SyntaxKind.None if the type name is not a predefined type. + Protected Shared Function GetPredefinedKeywordKind(type As SpecialType) As SyntaxKind + Select Case type + Case SpecialType.System_Boolean + Return SyntaxKind.BooleanKeyword + Case SpecialType.System_Byte + Return SyntaxKind.ByteKeyword + Case SpecialType.System_SByte + Return SyntaxKind.SByteKeyword + Case SpecialType.System_Int32 + Return SyntaxKind.IntegerKeyword + Case SpecialType.System_UInt32 + Return SyntaxKind.UIntegerKeyword + Case SpecialType.System_Int16 + Return SyntaxKind.ShortKeyword + Case SpecialType.System_UInt16 + Return SyntaxKind.UShortKeyword + Case SpecialType.System_Int64 + Return SyntaxKind.LongKeyword + Case SpecialType.System_UInt64 + Return SyntaxKind.ULongKeyword + Case SpecialType.System_Single + Return SyntaxKind.SingleKeyword + Case SpecialType.System_Double + Return SyntaxKind.DoubleKeyword + Case SpecialType.System_Decimal + Return SyntaxKind.DecimalKeyword + Case SpecialType.System_String + Return SyntaxKind.StringKeyword + Case SpecialType.System_Char + Return SyntaxKind.CharKeyword + Case SpecialType.System_Object + Return SyntaxKind.ObjectKeyword + Case SpecialType.System_DateTime + Return SyntaxKind.DateKeyword + Case Else + Return SyntaxKind.None + End Select + End Function + + Protected Shared Function TryReplaceWithAlias( + node As ExpressionSyntax, + semanticModel As SemanticModel, + ByRef aliasReplacement As IAliasSymbol) As Boolean + aliasReplacement = Nothing + + If Not IsAliasReplaceableExpression(node) Then + Return False + End If + + Dim symbol = semanticModel.GetSymbolInfo(node).Symbol + + If (symbol.IsConstructor()) Then + symbol = symbol.ContainingType + End If + + ' The following condition checks if the user has used alias in the original code and + ' if so the expression is replaced with the Alias + If TypeOf node Is QualifiedNameSyntax Then + Dim qualifiedNameNode = DirectCast(node, QualifiedNameSyntax) + If qualifiedNameNode.Right.Identifier.HasAnnotations(AliasAnnotation.Kind) Then + Dim aliasAnnotationInfo = qualifiedNameNode.Right.Identifier.GetAnnotations(AliasAnnotation.Kind).Single() + + Dim aliasName = AliasAnnotation.GetAliasName(aliasAnnotationInfo) + Dim aliasIdentifier = SyntaxFactory.IdentifierName(aliasName) + + Dim aliasTypeInfo = semanticModel.GetSpeculativeAliasInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsTypeOrNamespace) + + If Not aliasTypeInfo Is Nothing Then + aliasReplacement = aliasTypeInfo + Return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol) + End If + End If + End If + + If node.Kind = SyntaxKind.IdentifierName AndAlso semanticModel.GetAliasInfo(DirectCast(node, IdentifierNameSyntax)) IsNot Nothing Then + Return False + End If + + ' an alias can only replace a type Or namespace + If symbol Is Nothing OrElse + (symbol.Kind <> SymbolKind.Namespace AndAlso symbol.Kind <> SymbolKind.NamedType) Then + + Return False + End If + + If symbol Is Nothing OrElse Not TypeOf (symbol) Is INamespaceOrTypeSymbol Then + Return False + End If + + Dim preferAliasToQualifiedName = True + + If TypeOf node Is QualifiedNameSyntax Then + Dim qualifiedName = DirectCast(node, QualifiedNameSyntax) + If Not qualifiedName.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation) Then + Dim type = semanticModel.GetTypeInfo(node).Type + If Not type Is Nothing Then + Dim keywordKind = GetPredefinedKeywordKind(type.SpecialType) + If keywordKind <> SyntaxKind.None Then + preferAliasToQualifiedName = False + End If + End If + End If + End If + + aliasReplacement = DirectCast(symbol, INamespaceOrTypeSymbol).GetAliasForSymbol(node, semanticModel) + If aliasReplacement IsNot Nothing And preferAliasToQualifiedName Then + Return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol) + End If + + Return False + End Function + + Private Shared Function IsAliasReplaceableExpression(expression As ExpressionSyntax) As Boolean + If expression.Kind = SyntaxKind.IdentifierName OrElse + expression.Kind = SyntaxKind.QualifiedName Then + Return True + End If + + If expression.Kind = SyntaxKind.SimpleMemberAccessExpression Then + Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) + + Return memberAccess.Expression IsNot Nothing AndAlso IsAliasReplaceableExpression(memberAccess.Expression) + End If + + Return False + End Function + + ' We must verify that the alias actually binds back to the thing it's aliasing. + ' It's possible there is another symbol with the same name as the alias that binds first + Private Shared Function ValidateAliasForTarget(aliasReplacement As IAliasSymbol, semanticModel As SemanticModel, node As ExpressionSyntax, symbol As ISymbol) As Boolean + Dim aliasName = aliasReplacement.Name + + ' If we're the argument of a NameOf(X.Y) call, then we can't simplify to an + ' alias unless the alias has the same name as us (i.e. 'Y'). + If node.IsNameOfArgumentExpression() Then + Dim nameofValueOpt = semanticModel.GetConstantValue(node.Parent.Parent.Parent) + If Not nameofValueOpt.HasValue Then + Return False + End If + + Dim existingValue = TryCast(nameofValueOpt.Value, String) + If existingValue Is Nothing OrElse existingValue <> aliasName Then + Return False + End If + End If + + Dim boundSymbols = semanticModel.LookupNamespacesAndTypes(node.SpanStart, name:=aliasName) + If boundSymbols.Length = 1 Then + Dim boundAlias = TryCast(boundSymbols(0), IAliasSymbol) + If boundAlias IsNot Nothing And aliasReplacement.Target.Equals(symbol) Then + If symbol.IsAttribute Then + boundSymbols = semanticModel.LookupNamespacesAndTypes(node.Span.Start, name:=aliasName + "Attribute") + Return boundSymbols.IsEmpty + End If + + Return True + End If + End If + Return False + End Function + + Protected Shared Function PreferPredefinedTypeKeywordInMemberAccess(expression As ExpressionSyntax, optionSet As OptionSet) As Boolean + Return (IsDirectChildOfMemberAccessExpression(expression) OrElse IsInCrefReferenceForPredefinedTypeInMemberAccessContext(expression)) AndAlso + (Not InsideNameOfExpression(expression)) AndAlso + SimplificationHelpers.PreferPredefinedTypeKeywordInMemberAccess(optionSet, LanguageNames.VisualBasic) + End Function + + Protected Shared Function InsideNameOfExpression(expr As ExpressionSyntax) As Boolean + Dim nameOfExpression = expr.FirstAncestorOrSelf(Of NameOfExpressionSyntax)() + Return nameOfExpression IsNot Nothing + End Function + + ''' + ''' Note: This helper exists solely to work around Bug 1012713. Once it is fixed, this helper must be + ''' deleted in favor of . + ''' Context: Bug 1012713 makes it so that the compiler doesn't support PredefinedType.Member inside crefs + ''' (i.e. System.Int32.MaxValue is supported but Integer.MaxValue isn't). Until this bug is fixed, we don't + ''' support simplifying types names Like System.Int32.MaxValue to Integer.MaxValue. + ''' + Private Shared Function IsInCrefReferenceForPredefinedTypeInMemberAccessContext(expression As ExpressionSyntax) As Boolean + Return (InsideCrefReference(expression) AndAlso Not expression.IsLeftSideOfQualifiedName) + End Function + End Class +End Namespace diff --git a/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/ExpressionSimplifier.vb b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/ExpressionSimplifier.vb new file mode 100644 index 0000000000000..aa743a61f334d --- /dev/null +++ b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/ExpressionSimplifier.vb @@ -0,0 +1,341 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Runtime.InteropServices +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeStyle +Imports Microsoft.CodeAnalysis.Options +Imports Microsoft.CodeAnalysis.Simplification +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.VisualBasic.Utilities + +Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers + Friend Class ExpressionSimplifier + Inherits AbstractVisualBasicSimplifier(Of ExpressionSyntax, ExpressionSyntax) + + Public Shared ReadOnly Instance As New ExpressionSimplifier() + + Private Sub New() + End Sub + + Public Overrides Function TrySimplify(expression As ExpressionSyntax, + semanticModel As SemanticModel, + optionSet As OptionSet, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + cancellationToken As CancellationToken) As Boolean + If TryReduceExplicitName(expression, semanticModel, replacementNode, issueSpan, optionSet, cancellationToken) Then + Return True + End If + + Return TrySimplify(expression, semanticModel, replacementNode, issueSpan) + End Function + + Private Function TryReduceExplicitName( + expression As ExpressionSyntax, + semanticModel As SemanticModel, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + optionSet As OptionSet, + cancellationToken As CancellationToken + ) As Boolean + replacementNode = Nothing + issueSpan = Nothing + + If expression.Kind = SyntaxKind.SimpleMemberAccessExpression Then + Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) + Return TryReduce(memberAccess, semanticModel, + replacementNode, + issueSpan, + optionSet, + cancellationToken) + ElseIf TypeOf expression Is NameSyntax Then + Dim name = DirectCast(expression, NameSyntax) + Return NameSimplifier.Instance.TrySimplify( + name, semanticModel, optionSet, + replacementNode, issueSpan, cancellationToken) + End If + + Return False + End Function + + Private Function TryReduce( + memberAccess As MemberAccessExpressionSyntax, + semanticModel As SemanticModel, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + optionSet As OptionSet, + cancellationToken As CancellationToken + ) As Boolean + If memberAccess.Expression Is Nothing OrElse memberAccess.Name Is Nothing Then + Return False + End If + + If memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind) Then + replacementNode = SyntaxFactory.PredefinedType( + SyntaxFactory.Token( + GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First())))) _ + .WithLeadingTrivia(memberAccess.GetLeadingTrivia()) + + issueSpan = memberAccess.Span + Return True + End If + + ' See https//github.com/dotnet/roslyn/issues/40974 + ' + ' To be very safe, we only support simplifying code that bound to a symbol without any + ' sort of problems. We could potentially relax this in the future. However, we would + ' need to be very careful about the implications of us offering to fixup 'broken' code + ' in a manner that might end up making things worse Or confusing the user. + Dim symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess) + If symbol Is Nothing Then + Return False + End If + + If memberAccess.Expression.IsKind(SyntaxKind.MeExpression) AndAlso + Not SimplificationHelpers.ShouldSimplifyThisOrMeMemberAccessExpression(semanticModel, optionSet, symbol) Then + Return False + End If + + If Not memberAccess.IsRightSideOfDot() Then + Dim aliasReplacement As IAliasSymbol = Nothing + + If TryReplaceWithAlias(memberAccess, semanticModel, aliasReplacement) Then + Dim identifierToken = SyntaxFactory.Identifier( + memberAccess.GetLeadingTrivia(), + aliasReplacement.Name, + memberAccess.GetTrailingTrivia()) + + identifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken( + identifierToken, + semanticModel) + replacementNode = SyntaxFactory.IdentifierName(identifierToken) + + issueSpan = memberAccess.Span + + ' In case the alias name is the same as the last name of the alias target, we only include + ' the left part of the name in the unnecessary span to Not confuse uses. + If memberAccess.Name.Identifier.ValueText = identifierToken.ValueText Then + issueSpan = memberAccess.Expression.Span + End If + + Return True + End If + + If PreferPredefinedTypeKeywordInMemberAccess(memberAccess, optionSet) Then + If (symbol IsNot Nothing AndAlso symbol.IsKind(SymbolKind.NamedType)) Then + Dim keywordKind = GetPredefinedKeywordKind(DirectCast(symbol, INamedTypeSymbol).SpecialType) + If keywordKind <> SyntaxKind.None Then + replacementNode = SyntaxFactory.PredefinedType( + SyntaxFactory.Token( + memberAccess.GetLeadingTrivia(), + keywordKind, + memberAccess.GetTrailingTrivia())) + + replacementNode = replacementNode.WithAdditionalAnnotations( + New SyntaxAnnotation(NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))) + + issueSpan = memberAccess.Span + Return True + End If + End If + End If + End If + + ' a module name was inserted by the name expansion, so removing this should be tried first. + If memberAccess.HasAnnotation(SimplificationHelpers.SimplifyModuleNameAnnotation) Then + If TryOmitModuleName(memberAccess, semanticModel, symbol, replacementNode, issueSpan, cancellationToken) Then + Return True + End If + End If + + replacementNode = memberAccess.GetNameWithTriviaMoved(semanticModel) + issueSpan = memberAccess.Expression.Span + + If CanReplaceWithReducedName(memberAccess, replacementNode, semanticModel, symbol, cancellationToken) Then + Return True + End If + + If TryOmitModuleName(memberAccess, semanticModel, symbol, replacementNode, issueSpan, cancellationToken) Then + Return True + End If + + Return False + End Function + + Private Overloads Function TrySimplify( + expression As ExpressionSyntax, + semanticModel As SemanticModel, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan + ) As Boolean + replacementNode = Nothing + issueSpan = Nothing + + Select Case expression.Kind + Case SyntaxKind.SimpleMemberAccessExpression + If True Then + Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) + Dim newLeft As ExpressionSyntax = Nothing + If TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, newLeft, issueSpan) Then + ' replacement node might not be in it's simplest form, so add simplify annotation to it. + replacementNode = memberAccess.Update(memberAccess.Kind, newLeft, memberAccess.OperatorToken, memberAccess.Name).WithAdditionalAnnotations(Simplifier.Annotation) + + ' Ensure that replacement doesn't change semantics. + Return Not ReplacementChangesSemantics(memberAccess, replacementNode, semanticModel) + End If + + Return False + End If + + Case SyntaxKind.QualifiedName + If True Then + Dim qualifiedName = DirectCast(expression, QualifiedNameSyntax) + Dim newLeft As ExpressionSyntax = Nothing + If TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, newLeft, issueSpan) Then + If Not TypeOf newLeft Is NameSyntax Then + Contract.Fail("QualifiedName Left = " + qualifiedName.Left.ToString() + " and QualifiedName Right = " + qualifiedName.Right.ToString() + " . Left is tried to be replaced with the PredefinedType " + replacementNode.ToString()) + End If + + ' replacement node might not be in it's simplest form, so add simplify annotation to it. + replacementNode = qualifiedName.Update(DirectCast(newLeft, NameSyntax), qualifiedName.DotToken, qualifiedName.Right).WithAdditionalAnnotations(Simplifier.Annotation) + + ' Ensure that replacement doesn't change semantics. + Return Not ReplacementChangesSemantics(qualifiedName, replacementNode, semanticModel) + End If + + Return False + End If + End Select + + Return False + End Function + + Private Function ReplacementChangesSemantics(originalExpression As ExpressionSyntax, replacedExpression As ExpressionSyntax, semanticModel As SemanticModel) As Boolean + Dim speculationAnalyzer = New SpeculationAnalyzer(originalExpression, replacedExpression, semanticModel, CancellationToken.None) + Return speculationAnalyzer.ReplacementChangesSemantics() + End Function + + ' Note: The caller needs to verify that replacement doesn't change semantics of the original expression. + Private Function TrySimplifyMemberAccessOrQualifiedName( + left As ExpressionSyntax, + right As ExpressionSyntax, + semanticModel As SemanticModel, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan + ) As Boolean + replacementNode = Nothing + issueSpan = Nothing + + If left IsNot Nothing AndAlso right IsNot Nothing Then + Dim leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left) + If leftSymbol IsNot Nothing AndAlso leftSymbol.Kind = SymbolKind.NamedType Then + Dim rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right) + If rightSymbol IsNot Nothing AndAlso (rightSymbol.IsStatic OrElse rightSymbol.Kind = SymbolKind.NamedType) Then + ' Static member access or nested type member access. + Dim containingType As INamedTypeSymbol = rightSymbol.ContainingType + Dim isInCref = left.Ancestors(ascendOutOfTrivia:=True).OfType(Of CrefReferenceSyntax)().Any() + + ' Crefs in VB , irrespective of the expression are parsed as QualifiedName (no MemberAccessExpression) + ' Hence the Left can never be a PredefinedType (or anything other than NameSyntax) + If isInCref AndAlso TypeOf rightSymbol Is IMethodSymbol AndAlso Not containingType.SpecialType = SpecialType.None Then + Return False + End If + + If containingType IsNot Nothing AndAlso Not containingType.Equals(leftSymbol) Then + + Dim namedType = TryCast(leftSymbol, INamedTypeSymbol) + If namedType IsNot Nothing AndAlso + containingType.TypeArguments.Length <> 0 Then + Return False + End If + + ' We have a static member access or a nested type member access using a more derived type. + ' Simplify syntax so as to use accessed member's most immediate containing type instead of the derived type. + replacementNode = containingType.GenerateTypeSyntax().WithLeadingTrivia(left.GetLeadingTrivia()).WithTrailingTrivia(left.GetTrailingTrivia()).WithAdditionalAnnotations(Simplifier.Annotation) + issueSpan = left.Span + Return True + End If + End If + End If + End If + + Return False + End Function + + Private Function TryOmitModuleName(memberAccess As MemberAccessExpressionSyntax, + semanticModel As SemanticModel, + symbol As ISymbol, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + cancellationToken As CancellationToken) As Boolean + If memberAccess.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) Then + Dim symbolForMemberAccess = semanticModel.GetSymbolInfo(DirectCast(memberAccess.Parent, MemberAccessExpressionSyntax)).Symbol + If symbolForMemberAccess.IsModuleMember Then + replacementNode = memberAccess.Expression.WithLeadingTrivia(memberAccess.GetLeadingTrivia()) + issueSpan = memberAccess.Name.Span + + Dim parent = DirectCast(memberAccess.Parent, MemberAccessExpressionSyntax) + Dim parentReplacement = parent.ReplaceNode(parent.Expression, replacementNode) + + If CanReplaceWithReducedName(parent, parentReplacement, semanticModel, symbol, cancellationToken) Then + Return True + End If + End If + End If + + Return False + End Function + + Private Shared Function CanReplaceWithReducedName( + memberAccess As MemberAccessExpressionSyntax, + reducedNode As ExpressionSyntax, + semanticModel As SemanticModel, + symbol As ISymbol, + cancellationToken As CancellationToken + ) As Boolean + If Not IsMeOrNamedTypeOrNamespace(memberAccess.Expression, semanticModel) Then + Return False + End If + + ' See if we can simplify a member access expression of the form E.M or E.M() to M or M() + Dim speculationAnalyzer = New SpeculationAnalyzer(memberAccess, reducedNode, semanticModel, cancellationToken) + If Not speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() OrElse + speculationAnalyzer.ReplacementChangesSemantics() Then + Return False + End If + + If memberAccess.Expression.IsKind(SyntaxKind.MyBaseExpression) Then + Dim enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken) + If enclosingNamedType IsNot Nothing AndAlso + Not enclosingNamedType.IsSealed AndAlso + symbol IsNot Nothing AndAlso + symbol.IsOverridable() Then + Return False + End If + End If + + Return True + End Function + + Private Shared Function IsMeOrNamedTypeOrNamespace(expression As ExpressionSyntax, semanticModel As SemanticModel) As Boolean + If expression.Kind = SyntaxKind.MeExpression Then + Return True + End If + + Dim expressionInfo = semanticModel.GetSymbolInfo(expression) + If SimplificationHelpers.IsValidSymbolInfo(expressionInfo.Symbol) Then + If TypeOf expressionInfo.Symbol Is INamespaceOrTypeSymbol Then + Return True + End If + + If expressionInfo.Symbol.IsThisParameter() Then + Return True + End If + End If + + Return False + End Function + End Class +End Namespace diff --git a/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/NameSimplifier.vb b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/NameSimplifier.vb new file mode 100644 index 0000000000000..d4ca3b008b497 --- /dev/null +++ b/src/Workspaces/VisualBasic/Portable/Simplification/Simplifiers/NameSimplifier.vb @@ -0,0 +1,462 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Runtime.InteropServices +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeStyle +Imports Microsoft.CodeAnalysis.Options +Imports Microsoft.CodeAnalysis.Rename.ConflictEngine +Imports Microsoft.CodeAnalysis.Simplification +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Symbols +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.VisualBasic.Utilities + +Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers + Friend Class NameSimplifier + Inherits AbstractVisualBasicSimplifier(Of NameSyntax, ExpressionSyntax) + + Public Shared ReadOnly Instance As New NameSimplifier() + + Private Sub New() + End Sub + + Public Overrides Function TrySimplify( + name As NameSyntax, + semanticModel As SemanticModel, + optionSet As OptionSet, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + cancellationToken As CancellationToken) As Boolean + + ' do not simplify names of a namespace declaration + If IsPartOfNamespaceDeclarationName(name) Then + Return False + End If + + ' see whether binding the name binds to a symbol/type. if not, it is ambiguous and + ' nothing we can do here. + Dim symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, name) + If SimplificationHelpers.IsValidSymbolInfo(symbol) Then + If symbol.Kind = SymbolKind.Method AndAlso symbol.IsConstructor() Then + symbol = symbol.ContainingType + End If + + If symbol.Kind = SymbolKind.Method AndAlso name.Kind = SyntaxKind.GenericName Then + Dim genericName = DirectCast(name, GenericNameSyntax) + replacementNode = SyntaxFactory.IdentifierName(genericName.Identifier).WithLeadingTrivia(genericName.GetLeadingTrivia()).WithTrailingTrivia(genericName.GetTrailingTrivia()) + + issueSpan = genericName.TypeArgumentList.Span + Return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken) + End If + + If Not TypeOf symbol Is INamespaceOrTypeSymbol Then + Return False + End If + Else + Return False + End If + + If name.HasAnnotations(SpecialTypeAnnotation.Kind) Then + replacementNode = SyntaxFactory.PredefinedType( + SyntaxFactory.Token(name.GetLeadingTrivia(), + GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(name.GetAnnotations(SpecialTypeAnnotation.Kind).First())), + name.GetTrailingTrivia())) + + issueSpan = name.Span + + Return CanReplaceWithReducedNameInContext(name, replacementNode) + Else + + If Not name.IsRightSideOfDot() Then + + Dim aliasReplacement As IAliasSymbol = Nothing + If TryReplaceWithAlias(name, semanticModel, aliasReplacement) Then + Dim identifierToken = SyntaxFactory.Identifier( + name.GetLeadingTrivia(), + aliasReplacement.Name, + name.GetTrailingTrivia()) + + identifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken( + identifierToken, + semanticModel) + + replacementNode = SyntaxFactory.IdentifierName(identifierToken) + + Dim annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(RenameAnnotation.Kind) + For Each annotatedNodeOrToken In annotatedNodesOrTokens + If annotatedNodeOrToken.IsToken Then + identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken) + Else + replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode) + End If + Next + + annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(AliasAnnotation.Kind) + For Each annotatedNodeOrToken In annotatedNodesOrTokens + If annotatedNodeOrToken.IsToken Then + identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken) + Else + replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode) + End If + Next + + replacementNode = DirectCast(replacementNode, SimpleNameSyntax).WithIdentifier(identifierToken) + issueSpan = name.Span + + ' In case the alias name is the same as the last name of the alias target, we only include + ' the left part of the name in the unnecessary span to Not confuse uses. + If name.Kind = SyntaxKind.QualifiedName Then + Dim qualifiedName As QualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax) + + If qualifiedName.Right.Identifier.ValueText = identifierToken.ValueText Then + issueSpan = qualifiedName.Left.Span + End If + End If + + If CanReplaceWithReducedNameInContext(name, replacementNode) Then + + ' check if the alias name ends with an Attribute suffix that can be omitted. + Dim replacementNodeWithoutAttributeSuffix As ExpressionSyntax = Nothing + Dim issueSpanWithoutAttributeSuffix As TextSpan = Nothing + If TryReduceAttributeSuffix(name, identifierToken, semanticModel, aliasReplacement IsNot Nothing, replacementNodeWithoutAttributeSuffix, issueSpanWithoutAttributeSuffix, cancellationToken) Then + If CanReplaceWithReducedName(name, replacementNodeWithoutAttributeSuffix, semanticModel, cancellationToken) Then + replacementNode = replacementNode.CopyAnnotationsTo(replacementNodeWithoutAttributeSuffix) + issueSpan = issueSpanWithoutAttributeSuffix + End If + End If + + Return True + End If + + Return False + End If + + Dim nameHasNoAlias = False + + If TypeOf name Is SimpleNameSyntax Then + Dim simpleName = DirectCast(name, SimpleNameSyntax) + If Not simpleName.Identifier.HasAnnotations(AliasAnnotation.Kind) Then + nameHasNoAlias = True + End If + End If + + If TypeOf name Is QualifiedNameSyntax Then + Dim qualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax) + If Not qualifiedNameSyntax.Right.Identifier.HasAnnotations(AliasAnnotation.Kind) Then + nameHasNoAlias = True + End If + End If + + Dim aliasInfo = semanticModel.GetAliasInfo(name, cancellationToken) + + ' Don't simplify to predefined type if name is part of a QualifiedName. + ' QualifiedNames can't contain PredefinedTypeNames (although MemberAccessExpressions can). + ' In other words, the left side of a QualifiedName can't be a PredefinedTypeName. + If nameHasNoAlias AndAlso aliasInfo Is Nothing AndAlso Not name.Parent.IsKind(SyntaxKind.QualifiedName) Then + Dim type = semanticModel.GetTypeInfo(name).Type + If type IsNot Nothing Then + Dim keywordKind = GetPredefinedKeywordKind(type.SpecialType) + If keywordKind <> SyntaxKind.None Then + ' But do simplify to predefined type if not simplifying results in just the addition of escaping + ' brackets. E.g., even if specified otherwise, prefer `String` to `[String]`. + Dim token = SyntaxFactory.Token( + name.GetLeadingTrivia(), + keywordKind, + name.GetTrailingTrivia()) + Dim valueText = TryCast(name, IdentifierNameSyntax)?.Identifier.ValueText + Dim inDeclarationContext = PreferPredefinedTypeKeywordInDeclarations(name, optionSet) + Dim inMemberAccessContext = PreferPredefinedTypeKeywordInMemberAccess(name, optionSet) + If token.Text = valueText OrElse (inDeclarationContext OrElse inMemberAccessContext) Then + + Dim codeStyleOptionName As String = Nothing + If inDeclarationContext Then + codeStyleOptionName = NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInDeclaration) + ElseIf inMemberAccessContext Then + codeStyleOptionName = NameOf(CodeStyleOptions.PreferIntrinsicPredefinedTypeKeywordInMemberAccess) + End If + + replacementNode = SyntaxFactory.PredefinedType(token) + issueSpan = name.Span + + Dim canReplace = CanReplaceWithReducedNameInContext(name, replacementNode) + If canReplace Then + replacementNode = replacementNode.WithAdditionalAnnotations(New SyntaxAnnotation(codeStyleOptionName)) + End If + + Return canReplace + End If + End If + End If + End If + + ' Nullable rewrite: Nullable(Of Integer) -> Integer? + ' Don't rewrite in the case where Nullable(Of Integer) is part of some qualified name like Nullable(Of Integer).Something + If (symbol.Kind = SymbolKind.NamedType) AndAlso (Not name.IsLeftSideOfQualifiedName) Then + Dim type = DirectCast(symbol, INamedTypeSymbol) + If aliasInfo Is Nothing AndAlso CanSimplifyNullable(type, name) Then + Dim genericName As GenericNameSyntax + If name.Kind = SyntaxKind.QualifiedName Then + genericName = DirectCast(DirectCast(name, QualifiedNameSyntax).Right, GenericNameSyntax) + Else + genericName = DirectCast(name, GenericNameSyntax) + End If + + Dim oldType = genericName.TypeArgumentList.Arguments.First() + replacementNode = SyntaxFactory.NullableType(oldType).WithLeadingTrivia(name.GetLeadingTrivia()) + + issueSpan = name.Span + + If CanReplaceWithReducedNameInContext(name, replacementNode) Then + Return True + End If + End If + End If + End If + + Select Case name.Kind + Case SyntaxKind.QualifiedName + ' a module name was inserted by the name expansion, so removing this should be tried first. + Dim qualifiedName = DirectCast(name, QualifiedNameSyntax) + If qualifiedName.HasAnnotation(SimplificationHelpers.SimplifyModuleNameAnnotation) Then + If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then + Return True + End If + End If + + replacementNode = qualifiedName.Right.WithLeadingTrivia(name.GetLeadingTrivia()) + replacementNode = DirectCast(replacementNode, SimpleNameSyntax) _ + .WithIdentifier(VisualBasicSimplificationService.TryEscapeIdentifierToken( + DirectCast(replacementNode, SimpleNameSyntax).Identifier, + semanticModel)) + issueSpan = qualifiedName.Left.Span + + + If CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken) Then + Return True + End If + + If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then + Return True + End If + + Case SyntaxKind.IdentifierName + Dim identifier = DirectCast(name, IdentifierNameSyntax).Identifier + TryReduceAttributeSuffix(name, identifier, semanticModel, False, replacementNode, issueSpan, cancellationToken) + End Select + End If + + If replacementNode Is Nothing Then + Return False + End If + + Return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken) + End Function + + ''' + ''' Checks if the SyntaxNode is a name of a namespace declaration. To be a namespace name, the syntax + ''' must be parented by an namespace declaration and the node itself must be equal to the declaration's Name + ''' property. + ''' + Private Function IsPartOfNamespaceDeclarationName(node As SyntaxNode) As Boolean + + Dim nextNode As SyntaxNode = node + + Do While nextNode IsNot Nothing + + Select Case nextNode.Kind + + Case SyntaxKind.IdentifierName, SyntaxKind.QualifiedName + node = nextNode + nextNode = nextNode.Parent + + Case SyntaxKind.NamespaceStatement + Dim namespaceStatement = DirectCast(nextNode, NamespaceStatementSyntax) + Return namespaceStatement.Name Is node + + Case Else + Return False + + End Select + + Loop + + Return False + End Function + + Private Function CanReplaceWithReducedName( + name As NameSyntax, + replacementNode As ExpressionSyntax, + semanticModel As SemanticModel, + cancellationToken As CancellationToken + ) As Boolean + Dim speculationAnalyzer = New SpeculationAnalyzer(name, replacementNode, semanticModel, cancellationToken) + If speculationAnalyzer.ReplacementChangesSemantics() Then + Return False + End If + + Return CanReplaceWithReducedNameInContext(name, replacementNode) + End Function + + Private Function CanReplaceWithReducedNameInContext(name As NameSyntax, replacementNode As ExpressionSyntax) As Boolean + + ' Special case. if this new minimal name parses out to a predefined type, then we + ' have to make sure that we're not in a using alias. That's the one place where the + ' language doesn't allow predefined types. You have to use the fully qualified name + ' instead. + Dim invalidTransformation1 = IsNonNameSyntaxInImportsDirective(name, replacementNode) + Dim invalidTransformation2 = IsReservedNameInAttribute(name, replacementNode) + + Dim invalidTransformation3 = IsNullableTypeSyntaxLeftOfDotInMemberAccess(name, replacementNode) + + Return Not (invalidTransformation1 OrElse invalidTransformation2 OrElse invalidTransformation3) + End Function + + Private Function IsNonNameSyntaxInImportsDirective(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean + Return TypeOf expression.Parent Is ImportsClauseSyntax AndAlso + Not TypeOf simplifiedNode Is NameSyntax + End Function + + Private Function IsNullableTypeSyntaxLeftOfDotInMemberAccess(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean + Return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso + simplifiedNode.Kind = SyntaxKind.NullableType + End Function + + Private Function TryOmitModuleName(name As QualifiedNameSyntax, semanticModel As SemanticModel, ByRef replacementNode As ExpressionSyntax, ByRef issueSpan As TextSpan, cancellationToken As CancellationToken) As Boolean + If name.IsParentKind(SyntaxKind.QualifiedName) Then + Dim symbolForName = semanticModel.GetSymbolInfo(DirectCast(name.Parent, QualifiedNameSyntax)).Symbol + + ' in case this QN is used in a "New NSName.ModuleName.MemberName()" expression + ' the returned symbol is a constructor. Then we need to get the containing type. + If symbolForName.IsConstructor Then + symbolForName = symbolForName.ContainingType + End If + + If symbolForName.IsModuleMember Then + + replacementNode = name.Left.WithLeadingTrivia(name.GetLeadingTrivia()) + issueSpan = name.Right.Span + + Dim parent = DirectCast(name.Parent, QualifiedNameSyntax) + Dim parentReplacement = parent.ReplaceNode(parent.Left, replacementNode) + + If CanReplaceWithReducedName(parent, parentReplacement, semanticModel, cancellationToken) Then + Return True + End If + End If + End If + + Return False + End Function + + Private Shared Function TryReduceAttributeSuffix( + name As NameSyntax, + identifierToken As SyntaxToken, + semanticModel As SemanticModel, + isIdentifierNameFromAlias As Boolean, + ByRef replacementNode As ExpressionSyntax, + ByRef issueSpan As TextSpan, + cancellationToken As CancellationToken + ) As Boolean + If SyntaxFacts.IsAttributeName(name) AndAlso Not isIdentifierNameFromAlias Then + + ' When the replacement is an Alias we don't want the "Attribute" Suffix to be removed because this will result in symbol change + Dim aliasSymbol = semanticModel.GetAliasInfo(name, cancellationToken) + If aliasSymbol IsNot Nothing AndAlso + String.Compare(aliasSymbol.Name, identifierToken.ValueText, StringComparison.OrdinalIgnoreCase) = 0 Then + Return False + End If + + If name.Parent.Kind = SyntaxKind.Attribute OrElse name.IsRightSideOfDot() Then + Dim newIdentifierText = String.Empty + + ' an attribute that should keep it (unnecessary "Attribute" suffix should be annotated with a DontSimplifyAnnotation + If identifierToken.HasAnnotation(SimplificationHelpers.DontSimplifyAnnotation) Then + newIdentifierText = identifierToken.ValueText + "Attribute" + ElseIf identifierToken.ValueText.TryReduceAttributeSuffix(newIdentifierText) Then + issueSpan = New TextSpan(name.Span.End - 9, 9) + Else + Return False + End If + + ' escape it (VB allows escaping even for abbreviated identifiers, C# does not!) + Dim newIdentifierToken = identifierToken.CopyAnnotationsTo( + SyntaxFactory.Identifier( + identifierToken.LeadingTrivia, + newIdentifierText, + identifierToken.TrailingTrivia)) + newIdentifierToken = VisualBasicSimplificationService.TryEscapeIdentifierToken(newIdentifierToken, semanticModel) + replacementNode = SyntaxFactory.IdentifierName(newIdentifierToken).WithLeadingTrivia(name.GetLeadingTrivia()) + Return True + End If + End If + + Return False + End Function + + Private Function PreferPredefinedTypeKeywordInDeclarations(name As NameSyntax, optionSet As OptionSet) As Boolean + Return (Not IsDirectChildOfMemberAccessExpression(name)) AndAlso + (Not InsideCrefReference(name)) AndAlso + (Not InsideNameOfExpression(name)) AndAlso + SimplificationHelpers.PreferPredefinedTypeKeywordInDeclarations(optionSet, LanguageNames.VisualBasic) + End Function + + Private Shared Function CanSimplifyNullable(type As INamedTypeSymbol, name As NameSyntax) As Boolean + If Not type.IsNullable Then + Return False + End If + + If type.IsUnboundGenericType Then + ' Don't simplify unbound generic type "Nullable(Of )". + Return False + End If + + If InsideNameOfExpression(name) Then + ' Nullable(Of T) can't be simplified to T? in nameof expressions. + Return False + End If + + If Not InsideCrefReference(name) Then + ' Nullable(Of T) can always be simplified to T? outside crefs. + Return True + End If + + ' Inside crefs, if the T in this Nullable(Of T) is being declared right here + ' then this Nullable(Of T) is not a constructed generic type and we should + ' not offer to simplify this to T?. + ' + ' For example, we should not offer the simplification in the following cases where + ' T does not bind to an existing type / type parameter in the user's code. + ' - + ' - + ' + ' And we should offer the simplification in the following cases where SomeType and + ' SomeMethod bind to a type and method declared elsewhere in the users code. + ' - + + If name.IsKind(SyntaxKind.GenericName) Then + If (name.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="Nullable(Of T)" + (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso name.Parent?.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="System.Nullable(Of T)" + (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso (name.Parent?.IsParentKind(SyntaxKind.QualifiedName)).GetValueOrDefault() AndAlso name.Parent.Parent?.IsParentKind(SyntaxKind.CrefReference)) Then ' cref="System.Nullable(Of T).Value" + ' Unfortunately, unlike in corresponding C# case, we need syntax based checking to detect these cases because of bugs in the VB SemanticModel. + ' See https://github.com/dotnet/roslyn/issues/2196, https://github.com/dotnet/roslyn/issues/2197 + Return False + End If + End If + + Dim argument = type.TypeArguments.SingleOrDefault() + If argument Is Nothing OrElse argument.IsErrorType() Then + Return False + End If + + Dim argumentDecl = argument.DeclaringSyntaxReferences.FirstOrDefault() + If argumentDecl Is Nothing Then + ' The type argument is a type from metadata - so this is a constructed generic nullable type that can be simplified (e.g. Nullable(Of Integer)). + Return True + End If + + Return Not name.Span.Contains(argumentDecl.Span) + End Function + + End Class +End Namespace