// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Diagnostics; using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Precedence; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; internal abstract class AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer< TLanguageKindEnum, TParenthesizedExpressionSyntax> : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer where TLanguageKindEnum : struct where TParenthesizedExpressionSyntax : SyntaxNode { protected AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer() : base(IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, EnforceOnBuildValues.RemoveUnnecessaryParentheses, options: ParenthesesDiagnosticAnalyzersHelper.Options, new LocalizableResourceString(nameof(AnalyzersResources.Remove_unnecessary_parentheses), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_can_be_removed), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { } protected abstract TLanguageKindEnum GetSyntaxKind(); protected abstract ISyntaxFacts GetSyntaxFacts(); public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; protected sealed override void InitializeWorker(AnalysisContext context) => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxKind()); protected abstract bool CanRemoveParentheses( TParenthesizedExpressionSyntax parenthesizedExpression, SemanticModel semanticModel, CancellationToken cancellationToken, out PrecedenceKind precedence, out bool clarifiesPrecedence, out bool innerExpressionHasPrimaryPrecedence); private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) { var cancellationToken = context.CancellationToken; var parenthesizedExpression = (TParenthesizedExpressionSyntax)context.Node; if (!CanRemoveParentheses(parenthesizedExpression, context.SemanticModel, cancellationToken, out var precedence, out var clarifiesPrecedence, out var innerExpressionHasPrimaryPrecedence)) { return; } // Do not remove parentheses from these expressions when there are different kinds // between the parent and child of the parenthesized expr.. This is because removing // these parens can significantly decrease readability and can confuse many people // (including several people quizzed on Roslyn). For example, most people see // "1 + 2 << 3" as "1 + (2 << 3)", when it's actually "(1 + 2) << 3". To avoid // making code bases more confusing, we just do not touch parens for these constructs // unless both the child and parent have the same kinds. // // However, if the inner expression has primary precedence (like a literal, identifier, // member access, or invocation), we allow removal regardless of kind mismatch, as there's // no precedence confusion. For example: "1 | (2)" can safely become "1 | 2". switch (precedence) { case PrecedenceKind.Shift: case PrecedenceKind.Bitwise: case PrecedenceKind.Coalesce: var syntaxFacts = GetSyntaxFacts(); var child = syntaxFacts.GetExpressionOfParenthesizedExpression(parenthesizedExpression); var parentKind = parenthesizedExpression.Parent?.RawKind; var childKind = child.RawKind; if (parentKind != childKind && !innerExpressionHasPrimaryPrecedence) { return; } // Ok to remove if it was the exact same kind. i.e. ```(a | b) | c``` // not ok to remove if kinds changed. i.e. ```(a + b) << c``` // Ok to remove if inner expression has primary precedence. i.e. ```1 | (2)``` break; } var options = context.GetAnalyzerOptions(); var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, precedence); if (ShouldSkipAnalysis(context, preference.Notification)) { // User doesn't care about these parens. So nothing for us to do. return; } if (preference.Value == ParenthesesPreference.AlwaysForClarity && clarifiesPrecedence) { // User wants these parens if they clarify precedence, and these parens // clarify precedence. So keep these around. return; } // either they don't want unnecessary parentheses, or they want them only for // clarification purposes and this does not make things clear. Debug.Assert(preference.Value == ParenthesesPreference.NeverIfUnnecessary || !clarifiesPrecedence); var additionalLocations = ImmutableArray.Create( parenthesizedExpression.GetLocation()); var additionalUnnecessaryLocations = ImmutableArray.Create( parenthesizedExpression.GetFirstToken().GetLocation(), parenthesizedExpression.GetLastToken().GetLocation()); context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( Descriptor, AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer<TLanguageKindEnum, TParenthesizedExpressionSyntax>.GetDiagnosticSquiggleLocation(parenthesizedExpression, cancellationToken), preference.Notification, context.Options, additionalLocations, additionalUnnecessaryLocations)); } /// <summary> /// Gets the span of text to squiggle underline. /// If the expression is contained within a single line, the entire expression span is returned. /// Otherwise it will return the span from the expression start to the end of the same line. /// </summary> private static Location GetDiagnosticSquiggleLocation(TParenthesizedExpressionSyntax parenthesizedExpression, CancellationToken cancellationToken) { var parenthesizedExpressionLocation = parenthesizedExpression.GetLocation(); var lines = parenthesizedExpression.SyntaxTree.GetText(cancellationToken).Lines; var expressionFirstLine = lines.GetLineFromPosition(parenthesizedExpressionLocation.SourceSpan.Start); var textSpanEndPosition = Math.Min(parenthesizedExpressionLocation.SourceSpan.End, expressionFirstLine.Span.End); return Location.Create(parenthesizedExpression.SyntaxTree, TextSpan.FromBounds(parenthesizedExpressionLocation.SourceSpan.Start, textSpanEndPosition)); } }