blob: c00c93ab1a44ee74ef038748b3db724b58dfb766 [file] [log] [blame]
// Copyright (c) 2018, 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';
// ignore: deprecated_member_use
import 'package:analyzer/analyzer.dart';
import 'package:build/build.dart';
import 'package:meta/meta.dart';
import 'platform.dart';
/// A Dart library within a module.
///
/// Modules can be computed based on library dependencies (imports and exports)
/// and parts.
class ModuleLibrary {
/// The AssetId of the original Dart source file.
final AssetId id;
/// Whether this library can be imported.
///
/// This will be false if the source file is a "part of", or imports code that
/// can't be used outside the SDK.
final bool isImportable;
/// Whether this library is an entrypoint.
///
/// True if the library is in `lib/` but not `lib/src`, or if it is outside of
/// `lib/` and contains a `main` method. Always false if this is not an
/// importable library.
final bool isEntryPoint;
/// Deps that are imported with a conditional import.
///
/// Keys are the stringified ast node for the conditional, and the default
/// import is under the magic `$default` key.
final List<Map<String, AssetId>> conditionalDeps;
/// The IDs of libraries that are imported or exported by this library.
///
/// Null if this is not an importable library.
final Set<AssetId> _deps;
/// The "part" files for this library.
///
/// Null if this is not an importable library.
final Set<AssetId> parts;
/// The `dart:` libraries that this library directly depends on.
final Set<String> sdkDeps;
/// Whether this library has a `main` function.
final bool hasMain;
ModuleLibrary._(this.id,
{@required this.isEntryPoint,
@required Set<AssetId> deps,
@required this.parts,
@required this.conditionalDeps,
@required this.sdkDeps,
@required this.hasMain})
: _deps = deps,
isImportable = true;
ModuleLibrary._nonImportable(this.id)
: isImportable = false,
isEntryPoint = false,
_deps = null,
parts = null,
conditionalDeps = null,
sdkDeps = null,
hasMain = false;
factory ModuleLibrary._fromCompilationUnit(
AssetId id, bool isEntryPoint, CompilationUnit parsed) {
var deps = <AssetId>{};
var parts = <AssetId>{};
var sdkDeps = <String>{};
var conditionalDeps = <Map<String, AssetId>>[];
for (var directive in parsed.directives) {
if (directive is! UriBasedDirective) continue;
var path = (directive as UriBasedDirective).uri.stringValue;
var uri = Uri.parse(path);
if (uri.isScheme('dart-ext')) {
// TODO: What should we do for native extensions?
continue;
}
if (uri.scheme == 'dart') {
sdkDeps.add(uri.path);
continue;
}
var linkedId = AssetId.resolve(path, from: id);
if (linkedId == null) continue;
if (directive is PartDirective) {
parts.add(linkedId);
continue;
}
List<Configuration> conditionalDirectiveConfigurations;
if (directive is ImportDirective && directive.configurations.isNotEmpty) {
conditionalDirectiveConfigurations = directive.configurations;
} else if (directive is ExportDirective &&
directive.configurations.isNotEmpty) {
conditionalDirectiveConfigurations = directive.configurations;
}
if (conditionalDirectiveConfigurations != null) {
var conditions = <String, AssetId>{r'$default': linkedId};
for (var condition in conditionalDirectiveConfigurations) {
if (Uri.parse(condition.uri.stringValue).scheme == 'dart') {
throw ArgumentError('Unsupported conditional import of '
'`${condition.uri.stringValue}` found in $id.');
}
conditions[condition.name.toSource()] =
AssetId.resolve(condition.uri.stringValue, from: id);
}
conditionalDeps.add(conditions);
} else {
deps.add(linkedId);
}
}
return ModuleLibrary._(id,
isEntryPoint: isEntryPoint,
deps: deps,
parts: parts,
sdkDeps: sdkDeps,
conditionalDeps: conditionalDeps,
hasMain: _hasMainMethod(parsed));
}
/// Parse the directives from [source] and compute the library information.
static ModuleLibrary fromSource(AssetId id, String source) {
final isLibDir = id.path.startsWith('lib/');
// ignore: deprecated_member_use
final parsed = parseCompilationUnit(source,
name: id.path, suppressErrors: true, parseFunctionBodies: false);
// Packages within the SDK but published might have libraries that can't be
// used outside the SDK.
if (parsed.directives.any((d) =>
d is UriBasedDirective &&
d.uri.stringValue.startsWith('dart:_') &&
id.package != 'dart_internal')) {
return ModuleLibrary._nonImportable(id);
}
if (_isPart(parsed)) {
return ModuleLibrary._nonImportable(id);
}
final isEntryPoint =
(isLibDir && !id.path.startsWith('lib/src/')) || _hasMainMethod(parsed);
return ModuleLibrary._fromCompilationUnit(id, isEntryPoint, parsed);
}
/// Parses the output of [serialize] back into a [ModuleLibrary].
///
/// Importable libraries can be round tripped to a String. Non-importable
/// libraries should not be printed or parsed.
factory ModuleLibrary.deserialize(AssetId id, String encoded) {
var json = jsonDecode(encoded);
return ModuleLibrary._(id,
isEntryPoint: json['isEntrypoint'] as bool,
deps: _deserializeAssetIds(json['deps'] as Iterable),
parts: _deserializeAssetIds(json['parts'] as Iterable),
sdkDeps: Set.of((json['sdkDeps'] as Iterable).cast<String>()),
conditionalDeps:
(json['conditionalDeps'] as Iterable).map((conditions) {
return Map.of((conditions as Map<String, dynamic>)
.map((k, v) => MapEntry(k, AssetId.parse(v as String))));
}).toList(),
hasMain: json['hasMain'] as bool);
}
String serialize() => jsonEncode({
'isEntrypoint': isEntryPoint,
'deps': _deps.map((id) => id.toString()).toList(),
'parts': parts.map((id) => id.toString()).toList(),
'conditionalDeps': conditionalDeps
.map((conditions) =>
conditions.map((k, v) => MapEntry(k, v.toString())))
.toList(),
'sdkDeps': sdkDeps.toList(),
'hasMain': hasMain,
});
List<AssetId> depsForPlatform(DartPlatform platform) {
return _deps.followedBy(conditionalDeps.map((conditions) {
var selectedImport = conditions[r'$default'];
assert(selectedImport != null);
for (var condition in conditions.keys) {
if (condition == r'$default') continue;
if (!condition.startsWith('dart.library.')) {
throw UnsupportedError(
'$condition not supported for config specific imports. Only the '
'dart.library.<name> constants are supported.');
}
var library = condition.substring('dart.library.'.length);
if (platform.supportsLibrary(library)) {
selectedImport = conditions[condition];
break;
}
}
return selectedImport;
})).toList();
}
}
Set<AssetId> _deserializeAssetIds(Iterable serlialized) =>
Set.from(serlialized.map((decoded) => AssetId.parse(decoded as String)));
bool _isPart(CompilationUnit dart) =>
dart.directives.any((directive) => directive is PartOfDirective);
/// Allows two or fewer arguments to `main` so that entrypoints intended for
/// use with `spawnUri` get counted.
//
// TODO: This misses the case where a Dart file doesn't contain main(),
// but has a part that does, or it exports a `main` from another library.
bool _hasMainMethod(CompilationUnit dart) => dart.declarations.any((node) =>
node is FunctionDeclaration &&
node.name.name == 'main' &&
node.functionExpression.parameters.parameters.length <= 2);