-
Notifications
You must be signed in to change notification settings - Fork 125
Allow configuring project-wide page width using a surrounding analysis_options.yaml file #1571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
76830e1
Add code for working with analysis_options.yaml files.
munificent 81d0b28
Enable "unnecessary_final" lint and fix violations.
munificent aea8bec
Split cli_test.dart into separate files.
munificent f56469b
Look up page width of surrounding analysis_options.yaml file.
munificent 09ec863
Handle "package:" URIs in analysis options includes.
munificent 23637d1
Apply review feedback.
munificent 2e37ca3
Merge branch 'main' into analysis-options
munificent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// 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 'package:yaml/yaml.dart'; | ||
|
||
import 'file_system.dart'; | ||
import 'merge_options.dart'; | ||
|
||
/// The analysis options configuration is a dynamically-typed JSON-like data | ||
/// structure. | ||
/// | ||
/// (It's JSON-*like* and not JSON because maps in it may have non-string keys.) | ||
typedef AnalysisOptions = Map<Object?, Object?>; | ||
|
||
/// Interface for taking a "package:" URI that may appear in an analysis | ||
/// options file's "include" key and resolving it to a file path which can be | ||
/// passed to [FileSystem.join()]. | ||
typedef ResolvePackageUri = Future<String?> Function(Uri packageUri); | ||
|
||
/// Reads an `analysis_options.yaml` file in [directory] or in the nearest | ||
/// surrounding folder that contains that file using [fileSystem]. | ||
/// | ||
/// Stops walking parent directories as soon as it finds one that contains an | ||
/// `analysis_options.yaml` file. If it reaches the root directory without | ||
/// finding one, returns an empty [YamlMap]. | ||
/// | ||
/// If an `analysis_options.yaml` file is found, reads it and parses it to a | ||
/// [YamlMap]. If the map contains an `include` key whose value is a list, then | ||
/// reads any of the other referenced YAML files and merges them into this one. | ||
/// Returns the resulting map with the `include` key removed. | ||
/// | ||
/// If there any "package:" includes, then they are resolved to file paths | ||
/// using [resolvePackageUri]. If [resolvePackageUri] is omitted, an exception | ||
/// is thrown if any "package:" includes are found. | ||
Future<AnalysisOptions> findAnalysisOptions( | ||
FileSystem fileSystem, FileSystemPath directory, | ||
{ResolvePackageUri? resolvePackageUri}) async { | ||
while (true) { | ||
var optionsPath = await fileSystem.join(directory, 'analysis_options.yaml'); | ||
if (await fileSystem.fileExists(optionsPath)) { | ||
return readAnalysisOptions(fileSystem, optionsPath, | ||
resolvePackageUri: resolvePackageUri); | ||
} | ||
|
||
var parent = await fileSystem.parentDirectory(directory); | ||
if (parent == null) break; | ||
directory = parent; | ||
} | ||
|
||
// If we get here, we didn't find an analysis_options.yaml. | ||
return const {}; | ||
} | ||
|
||
/// Uses [fileSystem] to read the analysis options file at [optionsPath]. | ||
/// | ||
/// If there any "package:" includes, then they are resolved to file paths | ||
/// using [resolvePackageUri]. If [resolvePackageUri] is omitted, an exception | ||
/// is thrown if any "package:" includes are found. | ||
Future<AnalysisOptions> readAnalysisOptions( | ||
FileSystem fileSystem, FileSystemPath optionsPath, | ||
{ResolvePackageUri? resolvePackageUri}) async { | ||
var yaml = loadYamlNode(await fileSystem.readFile(optionsPath)); | ||
|
||
// If for some reason the YAML isn't a map, consider it malformed and yield | ||
// a default empty map. | ||
if (yaml is! YamlMap) return const {}; | ||
|
||
// Lower the YAML to a regular map. | ||
var options = {...yaml}; | ||
|
||
// If there is an `include:` key, then load that and merge it with these | ||
// options. | ||
if (options['include'] case String include) { | ||
options.remove('include'); | ||
|
||
// If the include path is "package:", resolve it to a file path first. | ||
var includeUri = Uri.tryParse(include); | ||
if (includeUri != null && includeUri.scheme == 'package') { | ||
if (resolvePackageUri != null) { | ||
var filePath = await resolvePackageUri(includeUri); | ||
if (filePath != null) { | ||
include = filePath; | ||
} else { | ||
throw PackageResolutionException( | ||
'Failed to resolve package URI "$include" in include.'); | ||
} | ||
} else { | ||
throw PackageResolutionException( | ||
'Couldn\'t resolve package URI "$include" in include because ' | ||
'no package resolver was provided.'); | ||
} | ||
} | ||
|
||
// The include path may be relative to the directory containing the current | ||
// options file. | ||
var includePath = await fileSystem.join( | ||
(await fileSystem.parentDirectory(optionsPath))!, include); | ||
var includeFile = await readAnalysisOptions(fileSystem, includePath, | ||
resolvePackageUri: resolvePackageUri); | ||
options = merge(includeFile, options) as AnalysisOptions; | ||
} | ||
|
||
return options; | ||
} | ||
|
||
/// Exception thrown when an analysis options file contains a "package:" URI in | ||
/// an include and resolving the URI to a file path failed. | ||
final class PackageResolutionException implements Exception { | ||
final String _message; | ||
|
||
PackageResolutionException(this._message); | ||
|
||
@override | ||
String toString() => _message; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// 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. | ||
|
||
/// Abstraction over a file system. | ||
/// | ||
/// Implement this if you want to control how this package locates and reads | ||
/// files. | ||
abstract interface class FileSystem { | ||
/// Returns `true` if there is a file at [path]. | ||
Future<bool> fileExists(covariant FileSystemPath path); | ||
|
||
/// Joins [from] and [to] into a single path with appropriate path separators. | ||
/// | ||
/// Note that [to] may be an absolute path implementation of [join()] should | ||
/// be prepared to handle that by ignoring [from]. | ||
Future<FileSystemPath> join(covariant FileSystemPath from, String to); | ||
|
||
/// Returns a path for the directory containing [path]. | ||
/// | ||
/// If [path] is a root path, then returns `null`. | ||
Future<FileSystemPath?> parentDirectory(covariant FileSystemPath path); | ||
|
||
/// Returns the series of directories surrounding [path], from innermost out. | ||
/// | ||
/// If [path] is itself a directory, then it should be the first directory | ||
/// yielded by this. Otherwise, the stream should begin with the directory | ||
/// containing that file. | ||
// Stream<FileSystemPath> parentDirectories(FileSystemPath path); | ||
|
||
/// Reads the contents of the file as [path], which should exist and contain | ||
/// UTF-8 encoded text. | ||
Future<String> readFile(covariant FileSystemPath path); | ||
} | ||
|
||
/// Abstraction over a file or directory in a [FileSystem]. | ||
/// | ||
/// An implementation of [FileSystem] should have a corresponding implementation | ||
/// of this class. It can safely assume that any instances of this passed in to | ||
/// the class were either directly created as instances of the implementation | ||
/// class by the host application, or were returned by methods on that same | ||
/// [FileSystem] object. Thus it is safe for an implementation of [FileSystem] | ||
/// to downcast instances of this to its expected implementation type. | ||
abstract interface class FileSystemPath {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// 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:io'; | ||
|
||
import 'package:path/path.dart' as p; | ||
|
||
import 'file_system.dart'; | ||
|
||
/// An implementation of [FileSystem] using `dart:io`. | ||
final class IOFileSystem implements FileSystem { | ||
Future<IOFileSystemPath> makePath(String path) async => | ||
IOFileSystemPath._(path); | ||
|
||
@override | ||
Future<bool> fileExists(covariant IOFileSystemPath path) => | ||
File(path.path).exists(); | ||
|
||
@override | ||
Future<FileSystemPath> join(covariant IOFileSystemPath from, String to) => | ||
makePath(p.join(from.path, to)); | ||
|
||
@override | ||
Future<FileSystemPath?> parentDirectory( | ||
covariant IOFileSystemPath path) async { | ||
// Make [path] absolute (if not already) so that we can walk outside of the | ||
// literal path string passed. | ||
var result = p.dirname(p.absolute(path.path)); | ||
|
||
// If the parent directory is the same as [path], we must be at the root. | ||
if (result == path.path) return null; | ||
|
||
return makePath(result); | ||
} | ||
|
||
@override | ||
Future<String> readFile(covariant IOFileSystemPath path) => | ||
File(path.path).readAsString(); | ||
} | ||
|
||
/// An abstraction over a file path string, used by [IOFileSystem]. | ||
/// | ||
/// To create an instance of this, use [IOFileSystem.makePath()]. | ||
final class IOFileSystemPath implements FileSystemPath { | ||
/// The underlying physical file system path. | ||
final String path; | ||
|
||
IOFileSystemPath._(this.path); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// 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. | ||
|
||
/// Merges a [defaults] options set with an [overrides] options set using | ||
/// simple override semantics, suitable for merging two configurations where | ||
/// one defines default values that are added to (and possibly overridden) by an | ||
/// overriding one. | ||
/// | ||
/// The merge rules are: | ||
/// | ||
/// * Lists are concatenated without duplicates. | ||
/// * A list of strings is promoted to a map of strings to `true` when merged | ||
/// with another map of strings to booleans. For example `['opt1', 'opt2']` | ||
/// is promoted to `{'opt1': true, 'opt2': true}`. | ||
/// * Maps unioned. When both have the same key, the corresponding values are | ||
/// merged, recursively. | ||
/// * Otherwise, a non-`null` override replaces a default value. | ||
Object? merge(Object? defaults, Object? overrides) { | ||
return switch ((defaults, overrides)) { | ||
(List(isAllStrings: true) && var list, Map(isToBools: true)) => | ||
munificent marked this conversation as resolved.
Show resolved
Hide resolved
|
||
merge(_promoteList(list), overrides), | ||
(Map(isToBools: true), List(isAllStrings: true) && var list) => | ||
merge(defaults, _promoteList(list)), | ||
(Map defaultsMap, Map overridesMap) => _mergeMap(defaultsMap, overridesMap), | ||
(List defaultsList, List overridesList) => | ||
_mergeList(defaultsList, overridesList), | ||
(_, null) => | ||
// Default to override, unless the overriding value is `null`. | ||
defaults, | ||
_ => overrides, | ||
}; | ||
} | ||
|
||
/// Promote a list of strings to a map of those strings to `true`. | ||
Map<Object?, Object?> _promoteList(List<Object?> list) { | ||
return {for (var element in list) element: true}; | ||
} | ||
|
||
/// Merge lists, avoiding duplicates. | ||
List<Object?> _mergeList(List<Object?> defaults, List<Object?> overrides) { | ||
// Add them both to a set so that the overrides replace the defaults. | ||
return {...defaults, ...overrides}.toList(); | ||
} | ||
|
||
/// Merge maps (recursively). | ||
Map<Object?, Object?> _mergeMap( | ||
Map<Object?, Object?> defaults, Map<Object?, Object?> overrides) { | ||
var merged = {...defaults}; | ||
|
||
overrides.forEach((key, value) { | ||
merged.update(key, (defaultValue) => merge(defaultValue, value), | ||
ifAbsent: () => value); | ||
}); | ||
|
||
return merged; | ||
} | ||
|
||
extension<T> on List<T> { | ||
bool get isAllStrings => every((e) => e is String); | ||
} | ||
|
||
extension<K, V> on Map<K, V> { | ||
bool get isToBools => values.every((v) => v is bool); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.