| // 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/analysis_rule.dart'; | |
| 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'; | |
| import '../diagnostic.dart' as diag; | |
| const _desc = | |
| r'Use trailing commas for all parameter lists and argument lists.'; | |
| class RequireTrailingCommas extends AnalysisRule { | |
| /// The version when tall-style was introduced in the formatter. | |
| static final Version language37 = Version(3, 7, 0); | |
| new() : super(name: LintNames.require_trailing_commas, description: _desc); | |
| @override | |
| DiagnosticCode get diagnosticCode => diag.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(final AnalysisRule rule) extends SimpleAstVisitor<void> { | |
| late LineInfo _lineInfo; | |
| @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.parent case PrimaryConstructorDeclaration primary) { | |
| if (primary.parent is ExtensionTypeDeclaration) { | |
| return; | |
| } | |
| } | |
| 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.onSameLine(token1.offset, token2.end); | |
| 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; | |
| } | |
| } |