blob: 167d657bcade8a2d820bd567e8e648cb0a28ae82 [file] [log] [blame]
// Copyright (c) 2021, 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/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:pub_semver/pub_semver.dart';
import '../analyzer.dart';
const _desc =
r'Use trailing commas for all parameter lists and argument lists.';
class RequireTrailingCommas extends LintRule {
/// The version when tall-style was introduced in the formatter.
static final Version language37 = Version(3, 7, 0);
RequireTrailingCommas()
: super(name: LintNames.require_trailing_commas, description: _desc);
@override
DiagnosticCode get diagnosticCode => LinterLintCode.requireTrailingCommas;
@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
// Don't report if tall-style is enforced by the formatter.
var languageVersion = context.libraryElement?.languageVersion.effective;
if (languageVersion != null && languageVersion >= language37) return;
var visitor = _Visitor(this);
registry
..addArgumentList(this, visitor)
..addAssertInitializer(this, visitor)
..addAssertStatement(this, visitor)
..addCompilationUnit(this, visitor)
..addFormalParameterList(this, visitor)
..addListLiteral(this, visitor)
..addSetOrMapLiteral(this, visitor);
}
}
class _Visitor extends SimpleAstVisitor<void> {
final LintRule rule;
late LineInfo _lineInfo;
_Visitor(this.rule);
@override
void visitArgumentList(ArgumentList node) {
super.visitArgumentList(node);
if (node.arguments.isEmpty) return;
_checkTrailingComma(
openingToken: node.leftParenthesis,
closingToken: node.rightParenthesis,
lastNode: node.arguments.last,
);
}
@override
void visitAssertInitializer(AssertInitializer node) {
super.visitAssertInitializer(node);
_checkTrailingComma(
openingToken: node.leftParenthesis,
closingToken: node.rightParenthesis,
lastNode: node.message ?? node.condition,
);
}
@override
void visitAssertStatement(AssertStatement node) {
super.visitAssertStatement(node);
_checkTrailingComma(
openingToken: node.leftParenthesis,
closingToken: node.rightParenthesis,
lastNode: node.message ?? node.condition,
);
}
@override
void visitCompilationUnit(CompilationUnit node) => _lineInfo = node.lineInfo;
@override
void visitFormalParameterList(FormalParameterList node) {
super.visitFormalParameterList(node);
if (node.parameters.isEmpty) return;
_checkTrailingComma(
openingToken: node.leftParenthesis,
closingToken: node.rightParenthesis,
lastNode: node.parameters.last,
errorToken: node.rightDelimiter ?? node.rightParenthesis,
);
}
@override
void visitListLiteral(ListLiteral node) {
super.visitListLiteral(node);
if (node.elements.isNotEmpty) {
_checkTrailingComma(
openingToken: node.leftBracket,
closingToken: node.rightBracket,
lastNode: node.elements.last,
);
}
}
@override
void visitSetOrMapLiteral(SetOrMapLiteral node) {
super.visitSetOrMapLiteral(node);
if (node.elements.isNotEmpty) {
_checkTrailingComma(
openingToken: node.leftBracket,
closingToken: node.rightBracket,
lastNode: node.elements.last,
);
}
}
void _checkTrailingComma({
required Token openingToken,
required Token closingToken,
required AstNode lastNode,
Token? errorToken,
}) {
errorToken ??= closingToken;
// Early exit if trailing comma is present.
if (lastNode.endToken.next?.type == TokenType.COMMA) return;
// No trailing comma is needed if the function call or declaration, up to
// the closing parenthesis, fits on a single line. Ensuring the left and
// right parenthesis are on the same line is sufficient since `dart format`
// places the left parenthesis right after the identifier (on the same
// line).
if (_isSameLine(openingToken, closingToken)) return;
// Check the last parameter to determine if there are any exceptions.
if (_shouldAllowTrailingCommaException(lastNode)) return;
rule.reportAtToken(errorToken);
}
bool _isSameLine(Token token1, Token token2) =>
_lineInfo.getLocation(token1.offset).lineNumber ==
_lineInfo.getLocation(token2.end).lineNumber;
bool _shouldAllowTrailingCommaException(AstNode lastNode) {
// No exceptions are allowed if the last argument is named.
if (lastNode is FormalParameter && lastNode.isNamed) return false;
// No exceptions are allowed if the entire last argument fits on one line.
if (_isSameLine(lastNode.beginToken, lastNode.endToken)) return false;
// Exception is allowed if the last argument is a function literal.
if (lastNode is FunctionExpression && lastNode.body is BlockFunctionBody) {
return true;
}
// Exception is allowed if the last argument is a (multiline) string
// literal.
if (lastNode is StringLiteral) return true;
// Exception is allowed if the last argument is a anonymous function call.
// This case arises a lot in asserts.
if (lastNode is FunctionExpressionInvocation &&
lastNode.function is FunctionExpression &&
_isSameLine(
lastNode.argumentList.leftParenthesis,
lastNode.argumentList.rightParenthesis,
)) {
return true;
}
// Exception is allowed if the last argument is a set, map or list literal.
if (lastNode is SetOrMapLiteral || lastNode is ListLiteral) return true;
return false;
}
}