blob: f15fd596bf04a84723686f714bd989525249253a [file] [log] [blame]
// Copyright (c) 2015, 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
/// Returns a non-null name for the provided [type].
///
/// In newer versions of the Dart analyzer, a `typedef` does not keep the
/// existing `name`, because it is used an alias:
/// ```
/// // Used to return `VoidFunc` for name, is now `null`.
/// typedef VoidFunc = void Function();
/// ```
///
/// This function will return `'VoidFunc'`, unlike [DartType.element.name].
String typeNameOf(DartType type) {
final aliasElement = type.alias?.element;
if (aliasElement != null) {
return aliasElement.name;
}
if (type is DynamicType) {
return 'dynamic';
}
if (type is InterfaceType) {
return type.element.name;
}
if (type is TypeParameterType) {
return type.element.name;
}
throw UnimplementedError('(${type.runtimeType}) $type');
}
bool hasExpectedPartDirective(CompilationUnit unit, String part) =>
unit.directives
.whereType<PartDirective>()
.any((e) => e.uri.stringValue == part);
/// Returns a uri suitable for `part of "..."` when pointing to [element].
String uriOfPartial(LibraryElement element, AssetId source, AssetId output) {
assert(source.package == output.package);
return p.url.relative(source.path, from: p.url.dirname(output.path));
}
/// Returns what 'part "..."' URL is needed to import [output] from [input].
///
/// For example, will return `test_lib.g.dart` for `test_lib.dart`.
String computePartUrl(AssetId input, AssetId output) => p.url.joinAll(
p.url.split(p.url.relative(output.path, from: input.path)).skip(1),
);
/// Returns a URL representing [element].
String urlOfElement(Element element) => element.kind == ElementKind.DYNAMIC
? 'dart:core#dynamic'
// using librarySource.uri – in case the element is in a part
: normalizeUrl(element.librarySource!.uri)
.replace(fragment: element.name)
.toString();
Uri normalizeUrl(Uri url) {
switch (url.scheme) {
case 'dart':
return normalizeDartUrl(url);
case 'package':
return packageToAssetUrl(url);
case 'file':
return fileToAssetUrl(url);
default:
return url;
}
}
/// Make `dart:`-type URLs look like a user-knowable path.
///
/// Some internal dart: URLs are something like `dart:core/map.dart`.
///
/// This isn't a user-knowable path, so we strip out extra path segments
/// and only expose `dart:core`.
Uri normalizeDartUrl(Uri url) => url.pathSegments.isNotEmpty
? url.replace(pathSegments: url.pathSegments.take(1))
: url;
Uri fileToAssetUrl(Uri url) {
if (!p.isWithin(p.url.current, url.path)) return url;
return Uri(
scheme: 'asset',
path: p.join(rootPackageName, p.relative(url.path)),
);
}
/// Returns a `package:` URL converted to a `asset:` URL.
///
/// This makes internal comparison logic much easier, but still allows users
/// to define assets in terms of `package:`, which is something that makes more
/// sense to most.
///
/// For example, this transforms `package:source_gen/source_gen.dart` into:
/// `asset:source_gen/lib/source_gen.dart`.
Uri packageToAssetUrl(Uri url) => url.scheme == 'package'
? url.replace(
scheme: 'asset',
pathSegments: <String>[
url.pathSegments.first,
'lib',
...url.pathSegments.skip(1),
],
)
: url;
/// Returns a `asset:` URL converted to a `package:` URL.
///
/// For example, this transformers `asset:source_gen/lib/source_gen.dart' into:
/// `package:source_gen/source_gen.dart`. Asset URLs that aren't pointing to a
/// file in the 'lib' folder are not modified.
///
/// Asset URLs come from `package:build`, as they are able to describe URLs that
/// are not describable using `package:...`, such as files in the `bin`, `tool`,
/// `web`, or even root directory of a package - `asset:some_lib/web/main.dart`.
Uri assetToPackageUrl(Uri url) => url.scheme == 'asset' &&
url.pathSegments.isNotEmpty &&
url.pathSegments[1] == 'lib'
? url.replace(
scheme: 'package',
pathSegments: [
url.pathSegments.first,
...url.pathSegments.skip(2),
],
)
: url;
final String rootPackageName = () {
final name =
(loadYaml(File('pubspec.yaml').readAsStringSync()) as Map)['name'];
if (name is! String) {
throw StateError(
'Your pubspec.yaml file is missing a `name` field or it isn\'t '
'a String.',
);
}
return name;
}();
/// Returns a valid buildExtensions map created from [optionsMap] or
/// returns [defaultExtensions] if no 'build_extensions' key exists.
///
/// Modifies [optionsMap] by removing the `build_extensions` key from it, if
/// present.
Map<String, List<String>> validatedBuildExtensionsFrom(
Map<String, dynamic>? optionsMap,
Map<String, List<String>> defaultExtensions,
) {
final extensionsOption = optionsMap?.remove('build_extensions');
if (extensionsOption == null) {
// defaultExtensions are provided by the builder author, not the end user.
// It should be safe to skip validation.
return defaultExtensions;
}
if (extensionsOption is! Map) {
throw ArgumentError(
'Configured build_extensions should be a map from inputs to outputs.',
);
}
final result = <String, List<String>>{};
for (final entry in extensionsOption.entries) {
final input = entry.key;
if (input is! String || !input.endsWith('.dart')) {
throw ArgumentError(
'Invalid key in build_extensions option: `$input` '
'should be a string ending with `.dart`',
);
}
final output = (entry.value is List) ? entry.value as List : [entry.value];
for (var i = 0; i < output.length; i++) {
final o = output[i];
if (o is! String || (i == 0 && !o.endsWith('.dart'))) {
throw ArgumentError(
'Invalid output extension `${entry.value}`. It should be a string '
'or a list of strings with the first ending with `.dart`',
);
}
}
result[input] = output.cast<String>().toList();
}
if (result.isEmpty) {
throw ArgumentError('Configured build_extensions must not be empty.');
}
return result;
}