diff --git a/pkg/analysis_server/lib/src/handler/legacy/edit_format.dart b/pkg/analysis_server/lib/src/handler/legacy/edit_format.dart index 7f7d30b31c3..a2da8d5cae9 100644 --- a/pkg/analysis_server/lib/src/handler/legacy/edit_format.dart +++ b/pkg/analysis_server/lib/src/handler/legacy/edit_format.dart @@ -7,9 +7,9 @@ import 'dart:async'; import 'package:analysis_server/protocol/protocol.dart'; import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analysis_server/src/handler/legacy/legacy_handler.dart'; -import 'package:analysis_server/src/utilities/extensions/formatter_options.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/src/utilities/formatter.dart'; import 'package:dart_style/dart_style.dart' hide TrailingCommas; /// The handler for the `edit.format` request. @@ -47,25 +47,17 @@ class EditFormatHandler extends LegacyHandler { length = null; } + var formatter = createFormatter(unit, defaultPageWidth: params.lineLength); var unformattedCode = unit.content; - var code = SourceCode( - unformattedCode, - selectionStart: start, - selectionLength: length, - ); - - var formatterOptions = unit.analysisOptions.formatterOptions; - var effectivePageWidth = formatterOptions.pageWidth ?? params.lineLength; - var effectiveTrailingCommas = formatterOptions.dartStyleTrailingCommas; - var effectiveLanguageVersion = unit.unit.languageVersion.effective; - var formatter = DartFormatter( - pageWidth: effectivePageWidth, - trailingCommas: effectiveTrailingCommas, - languageVersion: effectiveLanguageVersion, - ); SourceCode formattedResult; try { - formattedResult = formatter.formatSource(code); + formattedResult = formatter.formatSource( + SourceCode( + unformattedCode, + selectionStart: start, + selectionLength: length, + ), + ); } on FormatterException { sendResponse(Response.formatWithErrors(request)); return; diff --git a/pkg/analysis_server/lib/src/handler/legacy/edit_format_if_enabled.dart b/pkg/analysis_server/lib/src/handler/legacy/edit_format_if_enabled.dart index acbfc4a4121..17ba7d20b55 100644 --- a/pkg/analysis_server/lib/src/handler/legacy/edit_format_if_enabled.dart +++ b/pkg/analysis_server/lib/src/handler/legacy/edit_format_if_enabled.dart @@ -36,6 +36,9 @@ class EditFormatIfEnabledHandler extends LegacyHandler { var originalContent = file.readAsStringSync(); var code = SourceCode(originalContent); + // TODO(dantup): Consider using createFormatter() which takes a result and + // correctly applies settings like page_width, trailing_commas, and enabled + // experiments. var formatter = DartFormatter( languageVersion: languageVersion ?? DartFormatter.latestLanguageVersion, ); diff --git a/pkg/analysis_server/lib/src/lsp/source_edits.dart b/pkg/analysis_server/lib/src/lsp/source_edits.dart index e07a1a8cda9..3adf3f8bac4 100644 --- a/pkg/analysis_server/lib/src/lsp/source_edits.dart +++ b/pkg/analysis_server/lib/src/lsp/source_edits.dart @@ -8,7 +8,6 @@ import 'package:analysis_server/src/lsp/mapping.dart'; import 'package:analysis_server/src/protocol_server.dart' as server show SourceEdit; -import 'package:analysis_server/src/utilities/extensions/formatter_options.dart'; import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/token.dart'; @@ -17,6 +16,7 @@ import 'package:analyzer/source/line_info.dart'; import 'package:analyzer/source/source.dart'; import 'package:analyzer/src/dart/scanner/scanner.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; +import 'package:analyzer_plugin/src/utilities/formatter.dart'; import 'package:dart_style/dart_style.dart' hide TrailingCommas; /// Checks whether a string contains only characters that are allowed to differ @@ -130,25 +130,10 @@ ErrorOr?> generateEditsForFormatting( }) { var unformattedSource = result.content; - var formatterOptions = result.analysisOptions.formatterOptions; - // The analysis options page width always takes priority over the default from - // the LSP configuration. - var effectivePageWidth = formatterOptions.pageWidth ?? defaultPageWidth; - var effectiveTrailingCommas = formatterOptions.dartStyleTrailingCommas; - var effectiveLanguageVersion = result.unit.languageVersion.effective; - - var code = SourceCode(unformattedSource); - SourceCode formattedResult; + var formatter = createFormatter(result, defaultPageWidth: defaultPageWidth); + String formattedSource; try { - // Create a new formatter on every request because it may contain state that - // affects repeated formats. - // https://github.com/dart-lang/dart_style/issues/1337 - var formatter = DartFormatter( - pageWidth: effectivePageWidth, - trailingCommas: effectiveTrailingCommas, - languageVersion: effectiveLanguageVersion, - ); - formattedResult = formatter.formatSource(code); + formattedSource = formatter.format(unformattedSource); } on FormatterException { // If the document fails to parse, just return no edits to avoid the // use seeing edits on every save with invalid code (if LSP gains the @@ -156,7 +141,6 @@ ErrorOr?> generateEditsForFormatting( // we may wish to change this to return an error for that case). return success(null); } - var formattedSource = formattedResult.text; if (formattedSource == unformattedSource) { return success(null); diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart index 69448afb9a1..57d348d0ab9 100644 --- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart +++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart @@ -23,6 +23,7 @@ import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core import 'package:analyzer_plugin/src/utilities/charcodes.dart'; import 'package:analyzer_plugin/src/utilities/directive_sort.dart'; import 'package:analyzer_plugin/src/utilities/extensions/resolved_unit_result.dart'; +import 'package:analyzer_plugin/src/utilities/formatter.dart'; import 'package:analyzer_plugin/src/utilities/library.dart'; import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; @@ -1895,16 +1896,14 @@ class DartFileEditBuilderImpl extends FileEditBuilderImpl } } - var languageVersion = resolvedUnit.libraryElement.languageVersion.effective; - var formattedResult = DartFormatter(languageVersion: languageVersion) - .formatSource( - SourceCode( - newContent, - isCompilationUnit: true, - selectionStart: newRangeOffset, - selectionLength: newRangeLength, - ), - ); + var formatter = createFormatter(resolvedUnit); + var formattedResult = formatter.formatSource( + SourceCode( + newContent, + selectionStart: newRangeOffset, + selectionLength: newRangeLength, + ), + ); replaceEdits( range, diff --git a/pkg/analysis_server/lib/src/utilities/extensions/formatter_options.dart b/pkg/analyzer_plugin/lib/src/utilities/extensions/formatter_options.dart similarity index 100% rename from pkg/analysis_server/lib/src/utilities/extensions/formatter_options.dart rename to pkg/analyzer_plugin/lib/src/utilities/extensions/formatter_options.dart diff --git a/pkg/analyzer_plugin/lib/src/utilities/formatter.dart b/pkg/analyzer_plugin/lib/src/utilities/formatter.dart new file mode 100644 index 00000000000..152983cc97a --- /dev/null +++ b/pkg/analyzer_plugin/lib/src/utilities/formatter.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2026, 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/analysis/features.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/src/dart/analysis/experiments.dart' + show ExperimentStatus; +import 'package:analyzer_plugin/src/utilities/extensions/formatter_options.dart'; +import 'package:dart_style/dart_style.dart'; + +/// A list of all features that are currently enabled by an experiment flag. +final _allowedExperiments = ExperimentStatus.knownFeatures.values + .where((feature) => feature.status == FeatureStatus.future) + .toList(); + +/// Creates a formatter with the appropriate settings for [result]. +DartFormatter createFormatter( + ParsedUnitResult result, { + int? defaultPageWidth, +}) { + var featureSet = result.unit.featureSet; + var formatterOptions = result.analysisOptions.formatterOptions; + var effectivePageWidth = formatterOptions.pageWidth ?? defaultPageWidth; + var effectiveTrailingCommas = formatterOptions.dartStyleTrailingCommas; + var effectiveLanguageVersion = result.unit.languageVersion.effective; + return DartFormatter( + pageWidth: effectivePageWidth, + trailingCommas: effectiveTrailingCommas, + languageVersion: effectiveLanguageVersion, + experimentFlags: _getExperiments(featureSet), + ); +} + +/// Gets the list of experiment strings enabled by [featureSet] that are +/// required for future features. +List _getExperiments(FeatureSet featureSet) { + return _allowedExperiments + .where(featureSet.isEnabled) + .map((feature) => feature.enableString) + .toList(); +} diff --git a/pkg/analyzer_plugin/test/src/utilities/formatter_test.dart b/pkg/analyzer_plugin/test/src/utilities/formatter_test.dart new file mode 100644 index 00000000000..2c6cd462ebf --- /dev/null +++ b/pkg/analyzer_plugin/test/src/utilities/formatter_test.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2026, 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_plugin/src/utilities/formatter.dart'; +import 'package:analyzer_testing/experiments/experiments.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../support/abstract_single_unit.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FormatterTest); + }); +} + +@reflectiveTest +class FormatterTest extends AbstractSingleUnitTest { + Future test_experiments() async { + await resolveTestCode(''); + var formatter = createFormatter(result); + expect(formatter.experimentFlags, experimentsForTests); + } + + Future test_languageVersion_default() async { + await resolveTestCode(''); + var formatter = createFormatter(result); + expect(formatter.languageVersion, DartFormatter.latestLanguageVersion); + } + + Future test_languageVersion_override() async { + await resolveTestCode('// @dart=2.12'); + var formatter = createFormatter(result); + expect(formatter.languageVersion, Version(2, 12, 0)); + } + + Future test_pageWidth() async { + newFile(convertPath('$testPackageRootPath/analysis_options.yaml'), ''' +formatter: + page_width: 123 +'''); + await resolveTestCode(''); + var formatter = createFormatter(result); + expect(formatter.pageWidth, 123); + } + + Future test_trailingCommas() async { + newFile(convertPath('$testPackageRootPath/analysis_options.yaml'), ''' +formatter: + trailing_commas: preserve +'''); + await resolveTestCode(''); + var formatter = createFormatter(result); + expect(formatter.trailingCommas, TrailingCommas.preserve); + } +} diff --git a/pkg/analyzer_plugin/test/src/utilities/test_all.dart b/pkg/analyzer_plugin/test/src/utilities/test_all.dart index fb88ce8ae72..49bf1b47733 100644 --- a/pkg/analyzer_plugin/test/src/utilities/test_all.dart +++ b/pkg/analyzer_plugin/test/src/utilities/test_all.dart @@ -7,6 +7,7 @@ import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'change_builder/test_all.dart' as change_builder; import 'client_uri_converter_test.dart' as client_uri_converter; import 'completion/test_all.dart' as completion; +import 'formatter_test.dart' as formatter; import 'navigation/test_all.dart' as navigation; import 'string_utilities_test.dart' as string_utilities; import 'visitors/test_all.dart' as visitors; @@ -16,6 +17,7 @@ void main() { change_builder.main(); client_uri_converter.main(); completion.main(); + formatter.main(); navigation.main(); string_utilities.main(); visitors.main();