blob: ec1745508cda91c0276e223e7717beb3ab32bdc5 [file] [log] [blame] [edit]
// 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};
Future<Map<Object?, Object?>> optionsFromInclude(String include) async {
// 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,
);
return await readAnalysisOptions(
fileSystem,
includePath,
resolvePackageUri: resolvePackageUri,
);
}
// If there is an `include:` key with a String value, then load that and merge
// it with these options. If there is an `include:` key with a List value,
// then load each value, merging successive included options, overriding
// previous results with each set of included options, finally merging with
// these options.
switch (options['include']) {
case String include:
options.remove('include');
var includeOptions = await optionsFromInclude(include);
options = merge(includeOptions, options) as AnalysisOptions;
case List<Object?> includeList:
options.remove('include');
var mergedIncludeOptions = AnalysisOptions();
for (var include in includeList) {
if (include is! String) {
throw PackageResolutionException(
'Unsupported "include" value in analysis options include list: '
'"$include".',
);
}
var includeOptions = await optionsFromInclude(include);
mergedIncludeOptions =
merge(mergedIncludeOptions, includeOptions) as AnalysisOptions;
}
options = merge(mergedIncludeOptions, options) as AnalysisOptions;
case null:
break;
case Object include:
throw PackageResolutionException(
'Unsupported "include" value in analysis options: "$include".',
);
}
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;
}