diff --git a/pkg/analyzer/lib/dart/analysis/code_style_options.dart b/pkg/analyzer/lib/dart/analysis/code_style_options.dart index b0e7804961f..4aaaef10e0c 100644 --- a/pkg/analyzer/lib/dart/analysis/code_style_options.dart +++ b/pkg/analyzer/lib/dart/analysis/code_style_options.dart @@ -2,6 +2,8 @@ // 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/ast/ast.dart'; + /// A set of options related to coding style that apply to the code within a /// single analysis context. /// @@ -14,6 +16,10 @@ abstract class CodeStyleOptions { /// Return `true` if local variables should be `final` whenever possible. bool get makeLocalsFinal; + /// Return the preferred quote based on the enabled lints,otherwise a single + /// quote. + String get preferredQuoteForStrings; + /// Return `true` if constructors should be sorted first, before other /// class members. bool get sortConstructorsFirst; @@ -25,4 +31,8 @@ abstract class CodeStyleOptions { /// Return `true` if URIs should be "relative", meaning without a scheme, /// whenever possible. bool get useRelativeUris; + + /// Return the preferred quote based on the enabled lints, otherwise based + /// on the first directive, otherwise a single quote. + String preferredQuoteForUris(List directives); } diff --git a/pkg/analyzer/lib/src/analysis_options/code_style_options.dart b/pkg/analyzer/lib/src/analysis_options/code_style_options.dart index 283d13305d6..bc5ac9bf9b9 100644 --- a/pkg/analyzer/lib/src/analysis_options/code_style_options.dart +++ b/pkg/analyzer/lib/src/analysis_options/code_style_options.dart @@ -4,6 +4,8 @@ import 'package:analyzer/dart/analysis/analysis_options.dart'; import 'package:analyzer/dart/analysis/code_style_options.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:collection/collection.dart'; /// The concrete implementation of [CodeStyleOptions]. class CodeStyleOptionsImpl implements CodeStyleOptions { @@ -22,12 +24,34 @@ class CodeStyleOptionsImpl implements CodeStyleOptions { @override bool get makeLocalsFinal => _isLintEnabled('prefer_final_locals'); + @override + String get preferredQuoteForStrings => _lintQuote() ?? "'"; + @override bool get sortConstructorsFirst => _isLintEnabled('sort_constructors_first'); @override bool get useRelativeUris => _isLintEnabled('prefer_relative_imports'); + @override + String preferredQuoteForUris(List directives) { + var lintQuote = _lintQuote(); + if (lintQuote != null) { + return lintQuote; + } + var uri = directives.firstOrNull?.uri; + var doubleQuote = uri is SimpleStringLiteral && + uri.literal.value().toString().startsWith('"'); + return doubleQuote ? '"' : "'"; + } + /// Return `true` if the lint with the given [name] is enabled. bool _isLintEnabled(String name) => options.isLintEnabled(name); + + /// Return the preferred lint quote, otherwise `null`. + String? _lintQuote() => _isLintEnabled("prefer_single_quotes") + ? "'" + : _isLintEnabled("prefer_double_quotes") + ? '"' + : 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 6a0ff13fde2..286a214b513 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 @@ -1600,10 +1600,11 @@ class DartFileEditBuilderImpl extends FileEditBuilderImpl var importList = imports.toList(); importList.sort((a, b) => a.uriText.compareTo(b.uriText)); + var quote = codeStyleOptions.preferredQuoteForUris(importDirectives); void writeImport(EditBuilder builder, _LibraryToImport import) { - builder.write("import '"); + builder.write('import $quote'); builder.write(import.uriText); - builder.write("'"); + builder.write(quote); var prefix = import.prefix; if (prefix != null) { builder.write(' as '); diff --git a/pkg/analyzer_plugin/pubspec.yaml b/pkg/analyzer_plugin/pubspec.yaml index 29516fa14dd..0760eaab6fd 100644 --- a/pkg/analyzer_plugin/pubspec.yaml +++ b/pkg/analyzer_plugin/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: dev_dependencies: analyzer_utilities: any lints: any + linter: any meta: any path: any test_reflective_loader: any diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart index 000f4531146..4b4d287a4f3 100644 --- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart +++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart @@ -15,6 +15,7 @@ import 'package:analyzer/src/test_utilities/package_config_file_builder.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_dart.dart' show DartLinkedEditBuilderImpl; +import 'package:linter/src/rules.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -2088,6 +2089,43 @@ import 'package:foo/foo.dart'; ); } + Future test_default_quote() async { + await _assertImportLibrary( + initialCode: ''' +''', + uriList: ['dart:aaa'], + expectedCode: ''' +import 'dart:aaa'; +''', + ); + } + + Future test_directive_double_quote() async { + await _assertImportLibrary( + initialCode: ''' +import "dart:bbb"; +''', + uriList: ['dart:aaa'], + expectedCode: ''' +import "dart:aaa"; +import "dart:bbb"; +''', + ); + } + + Future test_directive_single_quote() async { + await _assertImportLibrary( + initialCode: ''' +import 'dart:bbb'; +''', + uriList: ['dart:aaa'], + expectedCode: ''' +import 'dart:aaa'; +import 'dart:bbb'; +''', + ); + } + Future test_multiple_dart_then_package() async { await _assertImportLibrary( initialCode: ''' @@ -2395,6 +2433,36 @@ import 'foo.dart'; ); } + Future test_prefer_double_quotes() async { + registerLintRules(); + writeTestPackageAnalysisOptionsFile(lints: ['prefer_double_quotes']); + await _assertImportLibrary( + initialCode: ''' +import 'dart:bbb'; +''', + uriList: ['dart:aaa'], + expectedCode: ''' +import "dart:aaa"; +import 'dart:bbb'; +''', + ); + } + + Future test_prefer_single_quotes() async { + registerLintRules(); + writeTestPackageAnalysisOptionsFile(lints: ['prefer_single_quotes']); + await _assertImportLibrary( + initialCode: ''' +import "dart:bbb"; +''', + uriList: ['dart:aaa'], + expectedCode: ''' +import 'dart:aaa'; +import "dart:bbb"; +''', + ); + } + Future test_relative_afterDart() async { await _assertImportLibrary( initialCode: ''' diff --git a/pkg/analyzer_plugin/test/support/abstract_context.dart b/pkg/analyzer_plugin/test/support/abstract_context.dart index 7663cde5aca..8b14c9b79f6 100644 --- a/pkg/analyzer_plugin/test/support/abstract_context.dart +++ b/pkg/analyzer_plugin/test/support/abstract_context.dart @@ -126,6 +126,25 @@ class AbstractContextTest with ResourceProviderMixin { newFile(path, config.toContent(toUriStr: toUriStr)); } + /// Write an analysis options file based on the given arguments. + /// TODO(asashour) Use AnalysisOptionsFileConfig + void writeTestPackageAnalysisOptionsFile({ + List? lints, + }) { + var buffer = StringBuffer(); + + if (lints != null) { + buffer.writeln('linter:'); + buffer.writeln(' rules:'); + for (var lint in lints) { + buffer.writeln(' - $lint'); + } + } + + print(buffer); + newFile('$testPackageRootPath/analysis_options.yaml', buffer.toString()); + } + void writeTestPackageConfig({ PackageConfigFileBuilder? config, String? languageVersion,