From 7f744ea6400044c8dc11ff13a56c04b35bf814aa Mon Sep 17 00:00:00 2001 From: Robert Nystrom Date: Thu, 17 Jul 2025 17:34:50 -0700 Subject: [PATCH 1/3] Enable language version 3.10 and format dot shorthands. This is using a workaround for https://github.com/dart-lang/sdk/issues/60840. That issue is fixed and a new version of analyzer is published, but unfortunately there is no corresponding version of test that works with it so I can't upgrade yet. The workaround is pretty harmless, so I'm fine with it. (The formatter uses similar logic to handle commas which also aren't represented in the AST nodes.) --- CHANGELOG.md | 4 +- lib/src/dart_formatter.dart | 2 +- lib/src/front_end/ast_node_visitor.dart | 34 +++++++++ test/tall/invocation/dot_shorthand.stmt | 69 +++++++++++++++++++ .../invocation/dot_shorthand_comment.stmt | 26 +++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 test/tall/invocation/dot_shorthand.stmt create mode 100644 test/tall/invocation/dot_shorthand_comment.stmt diff --git a/CHANGELOG.md b/CHANGELOG.md index f62fd816..e7d059d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 3.1.1-wip -* Update to latest analyzer and enable language version 3.9. +* Support dot shorthand syntax. +* Enable language version 3.10. +* Update to latest analyzer. ## 3.1.0 diff --git a/lib/src/dart_formatter.dart b/lib/src/dart_formatter.dart index eb4cb713..a55fe105 100644 --- a/lib/src/dart_formatter.dart +++ b/lib/src/dart_formatter.dart @@ -34,7 +34,7 @@ final RegExp _widthCommentPattern = RegExp(r'^// dart format width=(\d+)$'); final class DartFormatter { /// The latest Dart language version that can be parsed and formatted by this /// version of the formatter. - static final latestLanguageVersion = Version(3, 9, 0); + static final latestLanguageVersion = Version(3, 10, 0); /// The latest Dart language version that will be formatted using the older /// "short" style. diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart index 6f0d5ba8..85b83a5c 100644 --- a/lib/src/front_end/ast_node_visitor.dart +++ b/lib/src/front_end/ast_node_visitor.dart @@ -589,6 +589,29 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { pieces.token(node.semicolon); } + @override + void visitDotShorthandInvocation(DotShorthandInvocation node) { + // TODO(rnystrom): Get this directly from the AST once the formatter can + // use analyzer 8.0.0. + // See: https://github.com/dart-lang/sdk/issues/60840 + if (node.period.previous case var token? + when token.keyword == Keyword.CONST) { + pieces.token(token); + pieces.space(); + } + + pieces.token(node.period); + pieces.visit(node.memberName); + pieces.visit(node.typeArguments); + pieces.visit(node.argumentList); + } + + @override + void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) { + pieces.token(node.period); + pieces.visit(node.propertyName); + } + @override void visitDottedName(DottedName node) { writeDotted(node.components); @@ -2316,6 +2339,17 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { // Instead, we hoist the comment out of all of those and then have comment // precede them all so that they don't split. var firstToken = node.firstNonCommentToken; + + // TODO(rnystrom): The AST node for DotShorthandInvocation in analyzer + // before 8.0.0 doesn't include a leading `const` as part of the node. If + // we ignore that, then a comment before the `.` gets incorrectly hoisted + // before the `const`. Remove this when we can upgrade to 8.0.0. + // See: https://github.com/dart-lang/sdk/issues/60840 + if (node is DotShorthandInvocation && + firstToken.previous?.keyword == Keyword.CONST) { + firstToken = firstToken.previous!; + } + if (firstToken.precedingComments != null) { var comments = pieces.takeCommentsBefore(firstToken); var piece = pieces.build(() { diff --git a/test/tall/invocation/dot_shorthand.stmt b/test/tall/invocation/dot_shorthand.stmt new file mode 100644 index 00000000..6e990121 --- /dev/null +++ b/test/tall/invocation/dot_shorthand.stmt @@ -0,0 +1,69 @@ +40 columns | +(experiment dot-shorthands) +>>> Getter. +variable = . getter; +<<< 3.9 +variable = .getter; +>>> Method call with unsplit arguments. +variable = .method(1,x:2,3,y:4); +<<< 3.9 +variable = .method(1, x: 2, 3, y: 4); +>>> Method call with split arguments. +variable = .method(one, x: two, three, y: four); +<<< 3.9 +variable = .method( + one, + x: two, + three, + y: four, +); +>>> Generic method call. +variable = . method < int , String > ( ) ; +<<< 3.9 +variable = .method(); +>>> Constructor. +variable = .new(1); +<<< 3.9 +variable = .new(1); +>>> Const constructor. +variable = const . new ( ); +<<< 3.9 +variable = const .new(); +>>> Const named constructor. +variable = const . named ( ); +<<< 3.9 +variable = const .named(); +>>> Unsplit selector chain. +v = . property . method() . x . another(); +<<< 3.9 +v = .property.method().x.another(); +>>> Split selector chain on shorthand getter. +variable = .shorthand.method().another().third(); +<<< 3.9 +variable = .shorthand + .method() + .another() + .third(); +>>> Split selector chain on shorthand method. +variable = .shorthand().method().getter.another().third(); +<<< 3.9 +variable = .shorthand() + .method() + .getter + .another() + .third(); +>>> Split in shorthand method call argument list. +context(.shorthand(argument, anotherArgument, thirdArgument) +.chain().another().third().fourthOne()); +<<< 3.9 +context( + .shorthand( + argument, + anotherArgument, + thirdArgument, + ) + .chain() + .another() + .third() + .fourthOne(), +); diff --git a/test/tall/invocation/dot_shorthand_comment.stmt b/test/tall/invocation/dot_shorthand_comment.stmt new file mode 100644 index 00000000..d5cde8c4 --- /dev/null +++ b/test/tall/invocation/dot_shorthand_comment.stmt @@ -0,0 +1,26 @@ +40 columns | +(experiment dot-shorthands) +>>> Line comment after dot. +variable = . // Comment. +whoDoesThis(); +<<< 3.9 +variable = + . // Comment. + whoDoesThis(); +>>> Block comment after dot. +variable = ./* Comment. */whoDoesThis(); +<<< 3.9 +variable = + . /* Comment. */ whoDoesThis(); +>>> Line comment after `const`. +variable = const // Comment. +. whoDoesThis(); +<<< 3.9 +variable = + const // Comment. + .whoDoesThis(); +>>> Block comment after `const`. +variable = const/* Comment. */.whoDoesThis(); +<<< 3.9 +variable = + const /* Comment. */ .whoDoesThis(); From 9499bcd12cd8f14825f807a1744f4af8319f96ee Mon Sep 17 00:00:00 2001 From: Robert Nystrom Date: Fri, 18 Jul 2025 11:03:04 -0700 Subject: [PATCH 2/3] Add a more complex nested dot shorthand test. --- test/tall/invocation/dot_shorthand.stmt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/tall/invocation/dot_shorthand.stmt b/test/tall/invocation/dot_shorthand.stmt index 6e990121..0bafee2a 100644 --- a/test/tall/invocation/dot_shorthand.stmt +++ b/test/tall/invocation/dot_shorthand.stmt @@ -67,3 +67,11 @@ context( .third() .fourthOne(), ); +>>> Nested call. +.method(.getter,.method(.new(.new())),const.ctor()); +<<< +.method( + .getter, + .method(.new(.new())), + const .ctor(), +); From b6fe5750c133428821d56ee31735f5d78e5c009c Mon Sep 17 00:00:00 2001 From: Robert Nystrom Date: Tue, 22 Jul 2025 10:12:11 -0700 Subject: [PATCH 3/3] Update to analyzer 8.0.0 and remove workarounds. --- lib/src/dart_formatter.dart | 7 +++-- lib/src/exceptions.dart | 6 ++-- lib/src/front_end/ast_node_visitor.dart | 39 ++++++++++--------------- lib/src/short/source_visitor.dart | 8 ++--- pubspec.yaml | 2 +- 5 files changed, 26 insertions(+), 36 deletions(-) diff --git a/lib/src/dart_formatter.dart b/lib/src/dart_formatter.dart index a55fe105..4ddd74f1 100644 --- a/lib/src/dart_formatter.dart +++ b/lib/src/dart_formatter.dart @@ -7,6 +7,7 @@ import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:analyzer/error/error.dart'; // ignore: implementation_imports import 'package:analyzer/src/dart/scanner/scanner.dart'; @@ -176,7 +177,7 @@ final class DartFormatter { // Throw if there are syntactic errors. var syntacticErrors = parseResult.errors.where((error) { - return error.errorCode.type == ErrorType.SYNTACTIC_ERROR; + return error.diagnosticCode.type == DiagnosticType.SYNTACTIC_ERROR; }).toList(); if (syntacticErrors.isNotEmpty) { throw FormatterException(syntacticErrors); @@ -194,11 +195,11 @@ final class DartFormatter { var token = node.endToken.next!; if (token.type != TokenType.CLOSE_CURLY_BRACKET) { var stringSource = StringSource(text, source.uri); - var error = AnalysisError.tmp( + var error = Diagnostic.tmp( source: stringSource, offset: token.offset - inputOffset, length: math.max(token.length, 1), - errorCode: ParserErrorCode.UNEXPECTED_TOKEN, + diagnosticCode: ParserErrorCode.UNEXPECTED_TOKEN, arguments: [token.lexeme], ); throw FormatterException([error]); diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 58213080..7c859efb 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -2,14 +2,14 @@ // 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/error/error.dart'; +import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:source_span/source_span.dart'; /// Thrown when one or more errors occurs while parsing the code to be /// formatted. final class FormatterException implements Exception { - /// The [AnalysisError]s that occurred. - final List errors; + /// The [Diagnostic]s that occurred. + final List errors; /// Creates a new FormatterException with an optional error [message]. const FormatterException(this.errors); diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart index 85b83a5c..d33c0c72 100644 --- a/lib/src/front_end/ast_node_visitor.dart +++ b/lib/src/front_end/ast_node_visitor.dart @@ -531,7 +531,7 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { // The name of the type being constructed. var type = node.type; - pieces.token(type.name2); + pieces.token(type.name); pieces.visit(type.typeArguments); pieces.token(type.question); @@ -590,16 +590,18 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { } @override - void visitDotShorthandInvocation(DotShorthandInvocation node) { - // TODO(rnystrom): Get this directly from the AST once the formatter can - // use analyzer 8.0.0. - // See: https://github.com/dart-lang/sdk/issues/60840 - if (node.period.previous case var token? - when token.keyword == Keyword.CONST) { - pieces.token(token); - pieces.space(); - } + void visitDotShorthandConstructorInvocation( + DotShorthandConstructorInvocation node, + ) { + pieces.modifier(node.constKeyword); + pieces.token(node.period); + pieces.visit(node.constructorName); + pieces.visit(node.typeArguments); + pieces.visit(node.argumentList); + } + @override + void visitDotShorthandInvocation(DotShorthandInvocation node) { pieces.token(node.period); pieces.visit(node.memberName); pieces.visit(node.typeArguments); @@ -1252,7 +1254,7 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { // The type being constructed. var type = constructor.type; - pieces.token(type.name2); + pieces.token(type.name); pieces.visit(type.typeArguments); // If this is a named constructor call, the name. @@ -1357,7 +1359,7 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { void visitLibraryDirective(LibraryDirective node) { pieces.withMetadata(node.metadata, () { pieces.token(node.libraryKeyword); - pieces.visit(node.name2, spaceBefore: true); + pieces.visit(node.name, spaceBefore: true); pieces.token(node.semicolon); }); } @@ -1559,7 +1561,7 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { void visitNamedType(NamedType node) { pieces.token(node.importPrefix?.name); pieces.token(node.importPrefix?.period); - pieces.token(node.name2); + pieces.token(node.name); pieces.visit(node.typeArguments); pieces.token(node.question); } @@ -2339,17 +2341,6 @@ final class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { // Instead, we hoist the comment out of all of those and then have comment // precede them all so that they don't split. var firstToken = node.firstNonCommentToken; - - // TODO(rnystrom): The AST node for DotShorthandInvocation in analyzer - // before 8.0.0 doesn't include a leading `const` as part of the node. If - // we ignore that, then a comment before the `.` gets incorrectly hoisted - // before the `const`. Remove this when we can upgrade to 8.0.0. - // See: https://github.com/dart-lang/sdk/issues/60840 - if (node is DotShorthandInvocation && - firstToken.previous?.keyword == Keyword.CONST) { - firstToken = firstToken.previous!; - } - if (firstToken.precedingComments != null) { var comments = pieces.takeCommentsBefore(firstToken); var piece = pieces.build(() { diff --git a/lib/src/short/source_visitor.dart b/lib/src/short/source_visitor.dart index a235bb2e..3124d186 100644 --- a/lib/src/short/source_visitor.dart +++ b/lib/src/short/source_visitor.dart @@ -1970,9 +1970,7 @@ final class SourceVisitor extends ThrowingAstVisitor { _visitDirectiveMetadata(node); _simpleStatement(node, () { token(node.libraryKeyword); - if (node.name2 != null) { - visit(node.name2, before: space); - } + if (node.name case var name?) visit(name, before: space); }); } @@ -2184,10 +2182,10 @@ final class SourceVisitor extends ThrowingAstVisitor { token(importPrefix.name); soloZeroSplit(); token(importPrefix.period); - token(node.name2); + token(node.name); builder.endSpan(); } else { - token(node.name2); + token(node.name); } visit(node.typeArguments); diff --git a/pubspec.yaml b/pubspec.yaml index c73438c0..07c31c2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ environment: sdk: ^3.7.0 dependencies: - analyzer: ^7.5.2 + analyzer: ^8.0.0 args: ">=1.0.0 <3.0.0" collection: "^1.17.0" package_config: ^2.1.0