| // Copyright (c) 2024, 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 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:dart_style/src/config_cache.dart'; |
| import 'package:dart_style/src/dart_formatter.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_descriptor/test_descriptor.dart' as d; |
| |
| import 'utils.dart'; |
| |
| void main() { |
| group('findLanguageVersion()', () { |
| test('no surrounding package config', () async { |
| // Note: In theory this test could fail if the machine it runs on happens |
| // to have a `.dart_tool` directory containing a package config in one of |
| // the parent directories of the system temporary directory. |
| |
| await d.dir('dir', [d.file('main.dart', 'f() {}')]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectNullVersion(cache, 'dir/main.dart'); |
| }); |
| |
| test('language version from package config', () async { |
| await d.dir('foo', [ |
| packageConfig('foo', version: '3.4'), |
| d.file('main.dart', 'f() {}'), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectVersion(cache, 'foo/main.dart', 3, 4); |
| }); |
| |
| test('multiple packages in directory', () async { |
| await d.dir('parent', [ |
| _makePackage('foo', '3.4'), |
| _makePackage('bar', '3.5'), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectVersion(cache, 'parent/foo/main.dart', 3, 4); |
| await _expectVersion(cache, 'parent/bar/main.dart', 3, 5); |
| }); |
| |
| test('multiple files in same package', () async { |
| await _makePackage('foo', '3.4', [ |
| d.file('main.dart', 'f() {}'), |
| d.dir('sub', [ |
| d.file('another.dart', 'f() {}'), |
| d.dir('further', [d.file('third.dart', 'f() {}')]), |
| ]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectVersion(cache, 'foo/main.dart', 3, 4); |
| await _expectVersion(cache, 'foo/sub/another.dart', 3, 4); |
| await _expectVersion(cache, 'foo/sub/further/third.dart', 3, 4); |
| }); |
| |
| test('some files in package, some not', () async { |
| await d.dir('parent', [ |
| _makePackage('foo', '3.4'), |
| d.file('outside.dart', 'f() {}'), |
| d.dir('sub', [d.file('another.dart', 'f() {}')]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectVersion(cache, 'parent/foo/main.dart', 3, 4); |
| await _expectNullVersion(cache, 'parent/outside.dart'); |
| await _expectNullVersion(cache, 'parent/sub/another.dart'); |
| }); |
| |
| test('non-existent file', () async { |
| await d.dir('dir', []).create(); |
| |
| var cache = ConfigCache(); |
| await _expectNullVersion(cache, 'dir/does_not_exist.dart'); |
| }); |
| |
| test('non-existent directory', () async { |
| await d.dir('dir', []).create(); |
| |
| var cache = ConfigCache(); |
| await _expectNullVersion(cache, 'dir/does/not/exist.dart'); |
| }); |
| |
| test('nested package', () async { |
| await _makePackage('outer', '3.4', [ |
| d.file('out_main.dart', 'f() {}'), |
| _makePackage('inner', '3.5', [d.file('in_main.dart', 'f() {}')]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| await _expectVersion(cache, 'outer/out_main.dart', 3, 4); |
| await _expectVersion(cache, 'outer/inner/in_main.dart', 3, 5); |
| }); |
| }); |
| |
| group('findPageWidth()', () { |
| test('null page width if no surrounding options', () async { |
| await d.dir('dir', [d.file('main.dart', 'main() {}')]).create(); |
| |
| var cache = ConfigCache(); |
| expect(await cache.findPageWidth(_expectedFile('dir/main.dart')), isNull); |
| }); |
| |
| test('use page width of surrounding options', () async { |
| await d.dir('dir', [ |
| analysisOptionsFile(pageWidth: 20), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: 20); |
| }); |
| |
| test('use page width of indirectly surrounding options', () async { |
| await d.dir('dir', [ |
| analysisOptionsFile(pageWidth: 30), |
| d.dir('some', [ |
| d.dir('sub', [ |
| d.dir('directory', [d.file('main.dart', 'f() {}')]), |
| ]), |
| ]), |
| ]).create(); |
| |
| await _expectWidth(file: 'dir/some/sub/directory/main.dart', width: 30); |
| }); |
| |
| test('null page width if no "formatter" key in options', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({'unrelated': 'stuff'}), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: null); |
| }); |
| |
| test('null page width if "formatter" is not a map', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({'formatter': 'not a map'}), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: null); |
| }); |
| |
| test('null page width if no "page_width" key in formatter', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({ |
| 'formatter': {'no': 'page_width'}, |
| }), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: null); |
| }); |
| |
| test('null page width if "page_width" not an int', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({ |
| 'formatter': {'page_width': 'not an int'}, |
| }), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: null); |
| }); |
| |
| test('take page width from included options file', () async { |
| await d.dir('dir', [ |
| analysisOptionsFile(include: 'other.yaml'), |
| analysisOptionsFile(name: 'other.yaml', include: 'sub/third.yaml'), |
| d.dir('sub', [analysisOptionsFile(name: 'third.yaml', pageWidth: 30)]), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectWidth(width: 30); |
| }); |
| |
| test('resolve "package:" includes', () async { |
| await d.dir('dir', [ |
| d.dir('foo', [ |
| packageConfig( |
| 'foo', |
| packages: {'bar': '../../bar', 'baz': '../../baz'}, |
| ), |
| analysisOptionsFile(include: 'package:bar/analysis_options.yaml'), |
| d.file('main.dart', 'main() {}'), |
| ]), |
| d.dir('bar', [ |
| d.dir('lib', [ |
| analysisOptionsFile(include: 'package:baz/analysis_options.yaml'), |
| ]), |
| ]), |
| d.dir('baz', [ |
| d.dir('lib', [analysisOptionsFile(pageWidth: 30)]), |
| ]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| expect(await cache.findPageWidth(_expectedFile('dir/foo/main.dart')), 30); |
| }); |
| |
| test('use the root file\'s config for transitive "package:" ' |
| 'includes', () async { |
| // This tests a tricky edge case. Consider: |
| // |
| // Package my_app has analysis_options.yaml: |
| // |
| // include: "package:foo/options.yaml" |
| // |
| // my_app also has a package config that resolves bar to `bar_1.0.0/`. |
| // |
| // Package foo has analysis_options.yaml: |
| // |
| // include: "package:bar/options.yaml" |
| // |
| // foo also has a package config that resolves bar to `bar_2.0.0/`. |
| // |
| // Package bar_1.0.0 has options.yaml with a page width of 40. |
| // Package bar_2.0.0 has options.yaml with a page width of 60. |
| // |
| // Which page width do files in my_app get? If we resolve every "package:" |
| // include using the package config surrounding the analysis options file |
| // containing that include, you will get 60. If we resolve every |
| // "package:" include using the package config surrounding the original |
| // source file that we're formatting, you'll get 40. |
| // |
| // The answer we want is 40. A file is being formatted in the context of |
| // some package and we want that package's own transitive dependency solve |
| // to be used for analysis options look up, not the dependency solves of |
| // those dependencies. |
| await d.dir('dir', [ |
| d.dir('foo', [ |
| packageConfig( |
| 'foo', |
| packages: {'bar': '../../bar', 'baz': '../../baz'}, |
| ), |
| analysisOptionsFile(include: 'package:bar/analysis_options.yaml'), |
| d.file('main.dart', 'main() {}'), |
| ]), |
| d.dir('bar', [ |
| packageConfig('foo', packages: {'baz': '../../evil_baz'}), |
| d.dir('lib', [ |
| analysisOptionsFile(include: 'package:baz/analysis_options.yaml'), |
| ]), |
| ]), |
| d.dir('baz', [ |
| d.dir('lib', [analysisOptionsFile(pageWidth: 30)]), |
| ]), |
| d.dir('evil_baz', [ |
| d.dir('lib', [analysisOptionsFile(pageWidth: 666)]), |
| ]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| expect(await cache.findPageWidth(_expectedFile('dir/foo/main.dart')), 30); |
| }); |
| |
| test('nested package', () async { |
| // Both packages have a package config for resolving "bar" but each |
| // resolves to a different "bar" directory. Test that when resolving a |
| // "package:bar" include, we use the nearest surrounding package config. |
| await d.dir('dir', [ |
| d.dir('outer', [ |
| packageConfig('outer', packages: {'bar': '../../outer_bar'}), |
| d.dir('inner', [ |
| packageConfig('inner', packages: {'bar': '../../../inner_bar'}), |
| analysisOptionsFile(include: 'package:bar/analysis_options.yaml'), |
| d.file('main.dart', 'f() {}'), |
| ]), |
| analysisOptionsFile(include: 'package:bar/analysis_options.yaml'), |
| d.file('main.dart', 'f() {}'), |
| ]), |
| d.dir('outer_bar', [ |
| d.dir('lib', [analysisOptionsFile(pageWidth: 20)]), |
| ]), |
| d.dir('inner_bar', [ |
| d.dir('lib', [analysisOptionsFile(pageWidth: 30)]), |
| ]), |
| ]).create(); |
| |
| var cache = ConfigCache(); |
| expect( |
| await cache.findPageWidth(_expectedFile('dir/outer/main.dart')), |
| 20, |
| ); |
| expect( |
| await cache.findPageWidth(_expectedFile('dir/outer/inner/main.dart')), |
| 30, |
| ); |
| }); |
| }); |
| |
| group('findTrailingCommas()', () { |
| test('null if no surrounding options', () async { |
| await d.dir('dir', [d.file('main.dart', 'main() {}')]).create(); |
| |
| var cache = ConfigCache(); |
| expect( |
| await cache.findTrailingCommas(_expectedFile('dir/main.dart')), |
| isNull, |
| ); |
| }); |
| |
| test('automate trailing commas if option is "automate"', () async { |
| await d.dir('dir', [ |
| analysisOptionsFile(trailingCommas: TrailingCommas.automate), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(TrailingCommas.automate); |
| }); |
| |
| test('preserve trailing commas if option is "preserve"', () async { |
| await d.dir('dir', [ |
| analysisOptionsFile(trailingCommas: TrailingCommas.preserve), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(TrailingCommas.preserve); |
| }); |
| |
| test('null if no "formatter" key in options', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({'unrelated': 'stuff'}), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(null); |
| }); |
| |
| test('null if "formatter" is not a map', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({'formatter': 'not a map'}), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(null); |
| }); |
| |
| test('null if no "trailing_commas" key in formatter', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({ |
| 'formatter': {'no': 'trailing_commas'}, |
| }), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(null); |
| }); |
| |
| test('null if "trailing_commas" not a string', () async { |
| await d.dir('dir', [ |
| d.FileDescriptor( |
| 'analysis_options.yaml', |
| jsonEncode({ |
| 'formatter': {'trailing_commas': 123}, |
| }), |
| ), |
| d.file('main.dart', 'main() {}'), |
| ]).create(); |
| |
| await _expectTrailingCommas(null); |
| }); |
| }); |
| } |
| |
| Future<void> _expectVersion( |
| ConfigCache cache, |
| String path, |
| int major, |
| int minor, |
| ) async { |
| expect( |
| await cache.findLanguageVersion(_expectedFile(path), path), |
| Version(major, minor, 0), |
| ); |
| } |
| |
| Future<void> _expectNullVersion(ConfigCache cache, String path) async { |
| expect(await cache.findLanguageVersion(_expectedFile(path), path), null); |
| } |
| |
| /// Test that a [file] with some some surrounding analysis_options.yaml is |
| /// interpreted as having the given page [width]. |
| Future<void> _expectWidth({ |
| String file = 'dir/main.dart', |
| required int? width, |
| }) async { |
| var cache = ConfigCache(); |
| expect(await cache.findPageWidth(_expectedFile(file)), width); |
| } |
| |
| /// Test that a [file] with some some surrounding "analysis_options.yaml" is |
| /// interpreted as having the given [trailingCommas] setting. |
| Future<void> _expectTrailingCommas( |
| TrailingCommas? trailingCommas, { |
| String file = 'dir/main.dart', |
| }) async { |
| var cache = ConfigCache(); |
| expect(await cache.findTrailingCommas(_expectedFile(file)), trailingCommas); |
| } |
| |
| /// Normalize path separators to the host OS separator since that's what the |
| /// cache uses. |
| File _expectedFile(String path) => |
| File(p.joinAll([d.sandbox, ...p.posix.split(path)])); |
| |
| /// Create a test package with [packageName] containing a package config with |
| /// language version [major].[minor]. |
| /// |
| /// If [files] is given, then the package contains those files, otherwise it |
| /// contains a default `main.dart` file. |
| d.DirectoryDescriptor _makePackage( |
| String packageName, |
| String version, [ |
| List<d.Descriptor>? files, |
| ]) { |
| files ??= [d.file('main.dart', 'f() {}')]; |
| return d.dir(packageName, [ |
| packageConfig(packageName, version: version), |
| ...files, |
| ]); |
| } |