| // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element2.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| |
| import '../analyzer.dart'; |
| import '../extensions.dart'; |
| |
| const _desc = r'Unnecessary parentheses can be removed.'; |
| |
| class UnnecessaryParenthesis extends LintRule { |
| UnnecessaryParenthesis() |
| : super( |
| name: LintNames.unnecessary_parenthesis, |
| description: _desc, |
| ); |
| |
| @override |
| LintCode get lintCode => LinterLintCode.unnecessary_parenthesis; |
| |
| @override |
| void registerNodeProcessors( |
| NodeLintRegistry registry, LinterContext context) { |
| var visitor = _Visitor(this, context.typeSystem); |
| registry.addParenthesizedExpression(this, visitor); |
| } |
| } |
| |
| class _ContainsFunctionExpressionVisitor extends UnifyingAstVisitor<void> { |
| bool hasFunctionExpression = false; |
| |
| @override |
| void visitFunctionExpression(FunctionExpression node) { |
| hasFunctionExpression = true; |
| } |
| |
| @override |
| void visitNode(AstNode node) { |
| if (!hasFunctionExpression) { |
| node.visitChildren(this); |
| } |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| final LintRule rule; |
| final TypeSystem typeSystem; |
| |
| _Visitor(this.rule, this.typeSystem); |
| |
| @override |
| void visitParenthesizedExpression(ParenthesizedExpression node) { |
| var parent = node.parent; |
| // `case const (a + b):` is OK. |
| if (parent is ConstantPattern) return; |
| |
| // `[...(p as List)]` is OK. |
| if (parent is SpreadElement) return; |
| |
| var expression = node.expression; |
| |
| // Don't over-report on records missing trailing commas. |
| // `(int,) r = (3);` is OK. |
| if (parent is VariableDeclaration && |
| parent.declaredElement2?.type is RecordType) { |
| if (expression is! RecordLiteral) return; |
| } |
| // `g((3)); => g((int,) i) { }` is OK. |
| if (parent is ArgumentList) { |
| var element = node.correspondingParameter; |
| if (element?.type is RecordType && node.expression is! RecordLiteral) { |
| return; |
| } |
| } |
| // `g(i: (3)); => g({required (int,) i}) { }` is OK. |
| if (parent is NamedExpression && |
| parent.correspondingParameter?.type is RecordType) { |
| if (expression is! RecordLiteral) return; |
| } |
| |
| // Directly wrapped into parentheses already - always report. |
| if (parent is ParenthesizedExpression || |
| parent is InterpolationExpression || |
| (parent is ArgumentList && parent.arguments.length == 1) || |
| (parent is IfStatement && node == parent.expression) || |
| (parent is IfElement && node == parent.expression) || |
| (parent is WhileStatement && node == parent.condition) || |
| (parent is DoStatement && node == parent.condition) || |
| (parent is SwitchStatement && node == parent.expression) || |
| (parent is SwitchExpression && node == parent.expression)) { |
| rule.reportLint(node); |
| return; |
| } |
| |
| // `(foo ? bar : baz)` is OK. |
| if (expression is ConditionalExpression) return; |
| |
| // `(List<int>).toString()` is OK. |
| if (expression is TypeLiteral) return; |
| |
| if (expression.isOneToken || |
| expression.containsNullAwareInvocationInChain) { |
| if (parent is PropertyAccess) { |
| var name = parent.propertyName.name; |
| if (name == 'hashCode' || name == 'runtimeType') { |
| // `(String).hashCode` is OK. |
| return; |
| } |
| |
| // Parentheses are required to stop null-aware shorting, which then |
| // allows an extension getter, which extends a nullable type, to be |
| // called on a `null` value. |
| var target = parent.propertyName.element?.enclosingElement2; |
| if (target is ExtensionElement2 && |
| typeSystem.isNullable(target.extendedType)) { |
| return; |
| } |
| } else if (parent is MethodInvocation) { |
| var name = parent.methodName.name; |
| if (name == 'noSuchMethod' || name == 'toString') { |
| // `(String).noSuchMethod()` is OK. |
| return; |
| } |
| |
| // Parentheses are required to stop null-aware shorting, which then |
| // allows an extension method, which extends a nullable type, to be |
| // called on a `null` value. |
| var target = parent.methodName.element?.enclosingElement2; |
| if (target is ExtensionElement2 && |
| typeSystem.isNullable(target.extendedType)) { |
| return; |
| } |
| } else if (parent is PostfixExpression && |
| parent.operator.type == TokenType.BANG) { |
| return; |
| } else if (expression is IndexExpression && expression.isNullAware) { |
| if (parent is ConditionalExpression && |
| identical(parent.thenExpression, node)) { |
| // In `a ? (b?[c]) : d`, the parentheses are necessary to prevent the |
| // second `?` from being interpreted as the start of a nested |
| // conditional expression (see |
| // https://github.com/dart-lang/linter/issues/4812). |
| return; |
| } else if (parent is MapLiteralEntry && identical(parent.key, node)) { |
| // In `{(a?[b]): c}`, the parentheses are necessary to prevent the |
| // second `?` from being interpreted as the start of a nested |
| // conditional expression (see |
| // https://github.com/dart-lang/linter/issues/4812). |
| return; |
| } |
| } |
| rule.reportLint(node); |
| return; |
| } |
| |
| // https://github.com/dart-lang/linter/issues/2944 |
| if (expression is FunctionExpression) { |
| if (parent is MethodInvocation || |
| parent is PropertyAccess || |
| parent is BinaryExpression || |
| parent is IndexExpression) { |
| return; |
| } |
| } |
| |
| if (expression is ConstructorReference) { |
| if (parent is! FunctionExpressionInvocation || |
| parent.typeArguments == null) { |
| rule.reportLint(node); |
| return; |
| } |
| } |
| |
| // `a..b = (c..d)` is OK. |
| if (expression is CascadeExpression || |
| node.thisOrAncestorMatching( |
| (n) => n is Statement || n is CascadeExpression) |
| is CascadeExpression) { |
| return; |
| } |
| |
| // Constructor field initializers are rather unguarded by delimiting |
| // tokens, which can get confused with a function expression. See test |
| // cases for issues #1395 and #1473. |
| // |
| // We cannot just look at the immediate `parent`. Take this example of a |
| // constructor: |
| // |
| // ```dart |
| // C(bool Function()? e) : e = e ??= (() => true); |
| // ``` |
| // |
| // The parentheses in question are not an immediate child of a constructor |
| // field initializer; they are the right side of `e ??= ...`, which is an |
| // immediate child of a constructor field initializer. There can be any |
| // number of expressions like this in between. The important principle is |
| // that `=>` must not be "bare", such that it can be interpreted as the |
| // delimiter for the constructor body. |
| if (node.isBareInConstructorFieldInitializer && |
| node.containsFunctionExpression) { |
| return; |
| } |
| |
| // `foo = (a == b)` is OK, `return (count != 0)` is OK. |
| if (expression is BinaryExpression && |
| (expression.operator.type == TokenType.EQ_EQ || |
| expression.operator.type == TokenType.BANG_EQ)) { |
| if (parent is AssignmentExpression || |
| parent is VariableDeclaration || |
| parent is ReturnStatement || |
| parent is YieldStatement || |
| parent is ConstructorFieldInitializer) { |
| return; |
| } |
| } |
| |
| // `switch` at the beginning of a statement will be parsed as a switch |
| // statement, the parenthesis are required to parse as a switch expression |
| // instead. |
| if (parent is ExpressionStatement && expression is SwitchExpression) { |
| return; |
| } |
| |
| if (expression.directlyContainsWhitespace) { |
| // An expression with internal whitespace can be made more readable when |
| // wrapped in parentheses in many cases. But when the parentheses are |
| // inside one of the following nodes, the readability is not affected. |
| if (parent is! AssignmentExpression && |
| parent is! ConstructorFieldInitializer && |
| parent is! ExpressionFunctionBody && |
| parent is! RecordLiteral && |
| parent is! ReturnStatement && |
| parent is! VariableDeclaration && |
| parent is! YieldStatement && |
| !node.isArgument) { |
| return; |
| } |
| } |
| |
| if (parent is Expression) { |
| if (parent is BinaryExpression) return; |
| if (parent is ConditionalExpression) return; |
| if (parent is CascadeExpression) return; |
| if (parent is FunctionExpressionInvocation && |
| expression is! PrefixedIdentifier) { |
| return; |
| } |
| if (parent is AsExpression) return; |
| if (parent is IsExpression) return; |
| |
| if (parent |
| case MethodInvocation(:var target) || PropertyAccess(:var target)) { |
| // Another case of the above exception, something like |
| // `!(const [7]).contains(5);`, where the _parent's_ parent is the |
| // PrefixExpression. |
| if (parent.parent is PrefixExpression && |
| target == node && |
| expression.directlyContainsWhitespace) { |
| return; |
| } |
| |
| // `(p++).toString()` is OK. `(++p).toString()` is OK. |
| if (expression is PostfixExpression && target == node) return; |
| if (expression is PrefixExpression && target == node) return; |
| } |
| |
| // Something like `({1, 2, 3}).forEach(print);`. |
| // The parens cannot be removed because then the curly brackets are not |
| // interpreted as a set-or-map literal. |
| if (node.wouldBeParsedAsStatementBlock) return; |
| } |
| rule.reportLint(node); |
| } |
| } |
| |
| extension on ParenthesizedExpression { |
| bool get containsFunctionExpression { |
| var visitor = _ContainsFunctionExpressionVisitor(); |
| accept(visitor); |
| return visitor.hasFunctionExpression; |
| } |
| |
| bool get isBareInConstructorFieldInitializer { |
| var ancestor = parent; |
| while (ancestor != null) { |
| if (ancestor is ConstructorFieldInitializer) return true; |
| if (ancestor is FunctionBody || ancestor is MethodInvocation) { |
| // The delimiters (e.g. parentheses) in such an ancestor mean that |
| // `this` is not a "bare" expression within the constructor field |
| // initializer. |
| return false; |
| } |
| ancestor = ancestor.parent; |
| } |
| return false; |
| } |
| |
| /// Returns whether a parser would attempt to parse `this` as a statement |
| /// block if the parentheses were removed. |
| /// |
| /// The two components that make this true are: |
| /// * the parenthesized expression is a [SetOrMapLiteral] (starting with `{`), |
| /// * the open parenthesis of this expression is the first token of an |
| /// [ExpressionStatement]. |
| bool get wouldBeParsedAsStatementBlock { |
| if (expression is! SetOrMapLiteral) { |
| return false; |
| } |
| var exprStatementAncestor = thisOrAncestorOfType<ExpressionStatement>(); |
| if (exprStatementAncestor == null) { |
| return false; |
| } |
| return exprStatementAncestor.beginToken == leftParenthesis; |
| } |
| } |
| |
| extension on Expression? { |
| /// Returns whether this directly contains whitespace. |
| bool get directlyContainsWhitespace { |
| var self = this; |
| return self is AsExpression || |
| self is AssignmentExpression || |
| self is AwaitExpression || |
| self is BinaryExpression || |
| self is IsExpression || |
| // As in, `!(new Foo())`. |
| (self is InstanceCreationExpression && self.keyword != null) || |
| // No TypedLiteral (ListLiteral, MapLiteral, SetLiteral) accepts `-` |
| // or `!` as a prefix operator, but this method can be called |
| // recursively, so this catches things like |
| // `!(const [].contains(42))`. |
| (self is TypedLiteral && self.constKeyword != null) || |
| // As in, `!(const List(3).contains(7))`, and chains like |
| // `-(new List(3).skip(1).take(3).skip(1).length)`. |
| (self is MethodInvocation && self.target.directlyContainsWhitespace) || |
| // As in, `-(new List(3).length)`, and chains like |
| // `-(new List(3).length.bitLength.bitLength)`. |
| (self is PropertyAccess && self.target.directlyContainsWhitespace); |
| } |
| } |
| |
| extension on Expression { |
| /// Whether this expression is directly inside an argument list or the |
| /// expression of a named argument. |
| bool get isArgument => |
| parent is ArgumentList || |
| (parent is NamedExpression && parent?.parent is ArgumentList); |
| |
| /// Whether this expression is a sigle token. |
| /// |
| /// This excludes type literals because they often need to be parenthesized. |
| bool get isOneToken => |
| this is SimpleIdentifier || |
| this is StringLiteral || |
| this is IntegerLiteral || |
| this is DoubleLiteral || |
| this is NullLiteral || |
| this is BooleanLiteral; |
| } |