linter: Fix unnecessary_parenthesis with list literals
Fixes https://github.com/dart-lang/linter/issues/3046
The diff looks bigger than it is. The gist is:
* There is a check for if the ParenthesizedExpression is immediately
wrapped with parens. Move that check higher.
* There are broad exceptions for ConditionalExpressions and
TypeLiterals being wrapped with parens. Also move those higher.
* Add allowances for wrapped expressions which "directly" contain
whitespace, except for a few cases where the parens are still
considered unnecessary, such as an expression that is an argument.
* Add allowances for PrefixExpressions and PostfixExpressions which
are the "targets" of property access or method invocations.
Cq-Include-Trybots: luci.dart.try:flutter-analyze-try,analyzer-win-release-try,pkg-win-release-try
Change-Id: I808586df874b20b064d4f4a0bd7775b2ec28442a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/366891
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/tool/wolf/generate.dart b/pkg/analyzer/tool/wolf/generate.dart
index 79c0c0e..c84d2c9 100644
--- a/pkg/analyzer/tool/wolf/generate.dart
+++ b/pkg/analyzer/tool/wolf/generate.dart
@@ -106,7 +106,7 @@
_addInstruction('yield_', []);
// Invocations and tearoffs
_addInstruction('call',
- [(callDescriptor('callDescriptor')), (argumentNames('argumentNames'))]);
+ [callDescriptor('callDescriptor'), argumentNames('argumentNames')]);
}
_NontrivialEncoding encoding(String type,
diff --git a/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart b/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart
index 669e006..122fab5 100644
--- a/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart
+++ b/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart
@@ -97,11 +97,13 @@
// `[...(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.declaredElement?.type is RecordType) {
- if (node.expression is! RecordLiteral) return;
+ if (expression is! RecordLiteral) return;
}
// `g((3)); => g((int,) i) { }` is OK.
if (parent is ArgumentList) {
@@ -113,10 +115,29 @@
// `g(i: (3)); => g({required (int,) i}) { }` is OK.
if (parent is NamedExpression &&
parent.staticParameterElement?.type is RecordType) {
- if (node.expression is! RecordLiteral) return;
+ if (expression is! RecordLiteral) return;
}
- var expression = node.expression;
+ // 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 is SimpleIdentifier ||
expression.containsNullAwareInvocationInChain()) {
if (parent is PropertyAccess) {
@@ -190,25 +211,6 @@
}
}
- // 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;
- }
-
// `a..b = (c..d)` is OK.
if (expression is CascadeExpression ||
node.thisOrAncestorMatching(
@@ -252,11 +254,24 @@
}
}
- // `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 (node.expression is SwitchExpression) {
- if (parent is ExpressionStatement) {
+ // `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! VariableDeclaration &&
+ parent is! ExpressionFunctionBody &&
+ parent is! ReturnStatement &&
+ parent is! YieldStatement &&
+ !node.isArgument) {
return;
}
}
@@ -265,53 +280,35 @@
if (parent is BinaryExpression) return;
if (parent is ConditionalExpression) return;
if (parent is CascadeExpression) return;
- if (parent is FunctionExpressionInvocation) {
- if (expression is PrefixedIdentifier) {
- rule.reportLint(node);
- }
+ if (parent is FunctionExpressionInvocation &&
+ expression is! PrefixedIdentifier) {
return;
}
if (parent is AsExpression) return;
if (parent is IsExpression) return;
- // A prefix expression (! or -) can have an argument wrapped in
- // "unnecessary" parens if that argument has potentially confusing
- // whitespace after its first token.
- if (parent is PrefixExpression && node.expression.startsWithWhitespace) {
- return;
- }
-
- // Another case of the above exception, something like
- // `!(const [7]).contains(5);`, where the _parent's_ parent is the
- // PrefixExpression.
- if (parent is MethodInvocation) {
- var target = parent.target;
+ 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 &&
- node.expression.startsWithWhitespace) return;
+ 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;
- }
-
- // TODO(asashour): an API to the AST for better usage
- // Precedence isn't sufficient (e.g. PostfixExpression requires parenthesis)
- if (expression is PropertyAccess ||
- expression is ConstructorReference ||
- expression is PrefixedIdentifier ||
- expression is MethodInvocation ||
- expression is IndexExpression ||
- expression is Literal ||
- parent.precedence < expression.precedence) {
- rule.reportLint(node);
- }
- } else {
- rule.reportLint(node);
+ if (node.wouldBeParsedAsStatementBlock) return;
}
+ rule.reportLint(node);
}
}
@@ -357,26 +354,34 @@
}
extension on Expression? {
- /// Returns whether this "starts" with whitespace.
- ///
- /// That is, is there definitely whitespace after the first token?
- bool get startsWithWhitespace {
+ /// Returns whether this directly contains whitespace.
+ bool get directlyContainsWhitespace {
var self = this;
- return
- // As in, `!(await foo)`.
+ return self is AsExpression ||
+ self is AssignmentExpression ||
self is AwaitExpression ||
- // 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.startsWithWhitespace) ||
- // As in, `-(new List(3).length)`, and chains like
- // `-(new List(3).length.bitLength.bitLength)`.
- (self is PropertyAccess && self.target.startsWithWhitespace);
+ 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);
+}
diff --git a/pkg/linter/test/rules/unnecessary_parenthesis_test.dart b/pkg/linter/test/rules/unnecessary_parenthesis_test.dart
index ce79949..63536bb 100644
--- a/pkg/linter/test/rules/unnecessary_parenthesis_test.dart
+++ b/pkg/linter/test/rules/unnecessary_parenthesis_test.dart
@@ -17,6 +17,116 @@
@override
String get lintRule => 'unnecessary_parenthesis';
+ test_asExpressionInside_targetOfIndexAssignmentExpression() async {
+ await assertNoDiagnostics(r'''
+void f(Object o) {
+ (o as List)[7] = 7;
+}
+''');
+ }
+
+ test_asExpressionInside_targetOfIndexExpression() async {
+ await assertNoDiagnostics(r'''
+void f(Object o) {
+ (o as List)[7];
+}
+''');
+ }
+
+ test_asExpressionInside_targetOfMethodInvocation() async {
+ await assertNoDiagnostics(r'''
+void f() {
+ (2 as num).toString();
+}
+''');
+ }
+
+ test_asExpressionInside_targetOfPrefixExpression() async {
+ await assertNoDiagnostics(r'''
+void f(Object o) {
+ !(o as bool);
+}
+''');
+ }
+
+ test_assignmentInside_await() async {
+ await assertNoDiagnostics(r'''
+void f(Future<void> f, Future<void>? g) async {
+ await (g ??= f);
+}
+''');
+ }
+
+ test_binaryExpressionInside_namedArgument() async {
+ await assertDiagnostics(r'''
+void f({required int p}) {
+ f(p: (1 + 3));
+}
+''', [
+ lint(34, 7),
+ ]);
+ }
+
+ test_binaryExpressionInside_positionalArgument() async {
+ await assertDiagnostics(r'''
+void f(int p) {
+ f((1 + 3));
+}
+''', [
+ lint(20, 7),
+ ]);
+ }
+
+ test_binaryExpressionInside_prefixExpression() async {
+ await assertNoDiagnostics(r'''
+var x = ~(1 | 2);
+''');
+ }
+
+ test_binaryExpressionInside_returnExpression() async {
+ await assertDiagnostics(r'''
+bool f() {
+ return (1 > 1);
+}
+''', [
+ lint(20, 7),
+ ]);
+ }
+
+ test_conditionalExpressionInside_argument() async {
+ await assertDiagnostics(r'''
+void f(int p) {
+ print((1 == 1 ? 2 : 3));
+}
+''', [
+ lint(24, 16),
+ ]);
+ }
+
+ test_conditionalExpressionInside_listLiteral() async {
+ await assertNoDiagnostics(r'''
+void f() {
+ [(1 == 1 ? 2 : 3)];
+}
+''');
+ }
+
+ test_conditionalExpressionInside_stringInterpolation() async {
+ await assertDiagnostics(r'''
+void f() {
+ '${(1 == 1 ? 2 : 3)}';
+}
+''', [
+ lint(16, 16),
+ ]);
+ }
+
+ test_conditionalInside_expressionBody() async {
+ await assertNoDiagnostics(r'''
+int f() => (1 == 1 ? 2 : 3);
+''');
+ }
+
/// https://github.com/dart-lang/linter/issues/4060
test_constantPattern() async {
await assertNoDiagnostics(r'''
@@ -105,6 +215,38 @@
''');
}
+ test_equalityInside_expressionBody() async {
+ await assertDiagnostics(r'''
+bool f() => (1 == 1);
+''', [
+ lint(12, 8),
+ ]);
+ }
+
+ test_inListLiteral() async {
+ await assertDiagnostics(r'''
+final items = [1, (DateTime.now())];
+''', [
+ lint(18, 16),
+ ]);
+ }
+
+ test_isExpressionInside_targetOfMethodInvocation() async {
+ await assertNoDiagnostics(r'''
+void f(Object o) {
+ (o is num).toString();
+}
+''');
+ }
+
+ test_notEqualInside_returnExpression() async {
+ await assertNoDiagnostics(r'''
+bool f() {
+ return (1 != 1);
+}
+''');
+ }
+
/// https://github.com/dart-lang/linter/issues/4041
test_nullAware_cascadeAssignment() async {
await assertNoDiagnostics(r'''
@@ -125,6 +267,32 @@
''');
}
+ test_positionalArgument() async {
+ await assertDiagnostics(r'''
+void f() {
+ print((1 + 2));
+}
+''', [
+ lint(19, 7),
+ ]);
+ }
+
+ test_postfixExpressionInside_targetOfMethodInvocation() async {
+ await assertNoDiagnostics(r'''
+void f(int i) {
+ (i++).toString();
+}
+''');
+ }
+
+ test_prefixExpressionInside_targetOfMethodInvocation() async {
+ await assertNoDiagnostics(r'''
+void f(bool b) {
+ (!b).toString();
+}
+''');
+ }
+
test_record_assignment() async {
await assertDiagnostics(r'''
void f() {
@@ -141,7 +309,7 @@
g(i: ((3,)));
}
-void g({required (int,) i}) { }
+void g({required (int,) i}) {}
''', [
lint(18, 6),
]);
@@ -153,7 +321,7 @@
g(((3,)));
}
-void g((int,) i) { }
+void g((int,) i) {}
''', [
lint(15, 6),
]);
@@ -178,7 +346,7 @@
g(i: (3));
}
-void g({required (int,) i}) { }
+void g({required (int,) i}) {}
''', [
error(
CompileTimeErrorCode.RECORD_LITERAL_ONE_POSITIONAL_NO_TRAILING_COMMA,
@@ -195,7 +363,7 @@
g((3));
}
-void g((int,) i) { }
+void g((int,) i) {}
''', [
error(
CompileTimeErrorCode.RECORD_LITERAL_ONE_POSITIONAL_NO_TRAILING_COMMA,
@@ -280,4 +448,28 @@
}
''');
}
+
+ test_typeLiteralInside() async {
+ await assertNoDiagnostics(r'''
+void f() {
+ (List<int>).toString();
+}
+''');
+ }
+
+ test_unaryExpressionInside_targetOfMethodInvocation() async {
+ await assertNoDiagnostics(r'''
+void f(int p) {
+ (p++).toString();
+}
+''');
+ }
+
+ test_unaryExpressionInside_targetOfPropertyAccess() async {
+ await assertNoDiagnostics(r'''
+void f(int p) {
+ (p++).hashCode;
+}
+''');
+ }
}
diff --git a/pkg/linter/test_data/rules/unnecessary_parenthesis.dart b/pkg/linter/test_data/rules/unnecessary_parenthesis.dart
index 528d31e..70bb595 100644
--- a/pkg/linter/test_data/rules/unnecessary_parenthesis.dart
+++ b/pkg/linter/test_data/rules/unnecessary_parenthesis.dart
@@ -39,7 +39,6 @@
(List<int>.filled)(3, 0); // LINT
(List.filled)<int>(3, 0); // OK
var tearoff = (List<int>.filled); // LINT
- (List<int>).toString(); //OK
}
var a, b, c, d;
@@ -53,8 +52,6 @@
// OK because it may be hard to know all of the precedence rules.
if ((a && b) || c && d) true; // OK
(await new Future.value(1)).toString(); // OK
- ('' as String).toString(); // OK
- !(true as bool); // OK
(b - a) as num; // OK
(b - a) is num; // OK
a = (a); // LINT
@@ -67,8 +64,6 @@
(a ?? true) ? true : true; // OK
true ? [] : []
..add(''); // OK
- m(p: (1 + 3)); // LINT
- (a++).toString(); // OK
// OK because it is unobvious where cascades fall in precedence.
a..b = (c..d); // OK
@@ -134,13 +129,8 @@
print(!({"a": "b"}["a"]!.isEmpty)); // LINT
- print((1 + 2)); // LINT
-
- print((1 == 1 ? 2 : 3)); // LINT
print('a'.substring((1 == 1 ? 2 : 3), 4)); // OK
var a1 = (1 == 1 ? 2 : 3); // OK
- print('${(1 == 1 ? 2 : 3)}'); // LINT
- print([(1 == 1 ? 2 : 3)]); // OK
var a2 = (1 == 1); // OK
a2 = (1 == 1); // OK
@@ -152,15 +142,15 @@
var a5 = ((a4.isEmpty), 2); // LINT
var a6 = (1, (2)); // LINT
- withManyArgs((''), false, 1); // LINT
- withManyArgs('', (a4.isEmpty), 1); // LINT
- withManyArgs('', (''.isEmpty), 1); // LINT
- withManyArgs('', false, (1)); // LINT
+ /*withManyArgs((''), false, 1); // LIxNT
+ withManyArgs('', (a4.isEmpty), 1); // LIxNT
+ withManyArgs('', (''.isEmpty), 1); // LIxNT
+ withManyArgs('', false, (1)); // LIxNT
- var a7 = (double.infinity).toString(); // LINT
+ var a7 = (double.infinity).toString(); // LIxNT
var list2 = ["a", null];
- var a8 = (list2.first)!.length; // LINT
+ var a8 = (list2.first)!.length; // LIxNT*/
// Null-aware index expression before `:` needs to be parenthesized to avoid
// being interpreted as a conditional expression.
@@ -170,13 +160,13 @@
void withManyArgs(String a, bool b, int c) {}
-bool testTernaryAndEquality() {
+void testTernaryAndEquality() {
if ((1 == 1 ? true : false)) // LINT
{
- return (1 != 1); // OK
+ //
} else if ((1 == 1 ? true : false)) // LINT
{
- return (1 > 1); // LINT
+ //
}
while ((1 == 1)) // LINT
{
@@ -185,19 +175,14 @@
switch ((5 == 6)) // LINT
{
case true:
- return false;
+ return;
default:
- return true;
+ return;
}
}
-int test2() => (1 == 1 ? 2 : 3); // OK
-bool test3() => (1 == 1); // LINT
-
Invocation? invocation() => null;
-m({p}) => null;
-
bool Function(dynamic) get fn => (x) => x is bool ? x : false;
extension<T> on Set<T> {