| // Copyright (c) 2017, 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:async'; |
| import 'dart:io'; |
| |
| import 'package:front_end/src/api_prototype/compiler_options.dart'; |
| import 'package:front_end/src/api_prototype/dependency_grapher.dart'; |
| import 'package:front_end/src/testing/package_root.dart' as package_root; |
| import 'package:path/path.dart' as pathos; |
| |
| main() async { |
| exit(await new _SubpackageRelationshipsTest().run()); |
| } |
| |
| /// List of packages that front_end is allowed to directly depend on. |
| /// |
| /// Note that this script only checks files in pkg/front_end/lib, so this list |
| /// excludes dev dependencies. |
| final allowedPackageDependencies = [ |
| 'charcode', |
| 'convert', |
| 'crypto', |
| 'kernel', |
| 'meta', |
| 'package_config', |
| 'path', |
| 'source_span', |
| 'testing', |
| ]; |
| |
| /// Map from subpackage name to the rules for what the subpackage is allowed to |
| /// depend directly on. |
| /// |
| /// Each listed directory is considered a subpackage. Each package contains all |
| /// of its descendant files that are not in a more deeply nested subpackage. |
| final subpackageRules = { |
| 'lib/src': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src/base', |
| 'lib/src/fasta', |
| "lib/src/fasta/dill", |
| "lib/src/fasta/kernel", |
| 'lib/src/fasta/source', |
| ]), |
| 'lib/src/api_prototype': new SubpackageRules(allowedDependencies: [ |
| 'lib/src', |
| 'lib/src/base', |
| 'lib/src/byte_store', |
| 'lib/src/fasta', |
| ]), |
| 'lib/src/api_unstable': new SubpackageRules(allowedDependencies: [ |
| 'lib/src', |
| 'lib/src/api_prototype', |
| 'lib/src/base', |
| 'lib/src/fasta', |
| ]), |
| 'lib/src/base': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src/fasta', |
| 'lib/src/fasta/util', |
| ]), |
| // Note: byte_store should never depend on other parts of front_end, since we |
| // may want to move it to its own package someday. |
| 'lib/src/byte_store': new SubpackageRules(allowedDependencies: []), |
| 'lib/src/codegen': new SubpackageRules(), |
| 'lib/src/fasta': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src/base', |
| 'lib/src/fasta/builder', |
| 'lib/src/fasta/dill', |
| 'lib/src/fasta/kernel', |
| 'lib/src/fasta/parser', |
| 'lib/src/fasta/scanner', |
| 'lib/src/fasta/source', |
| 'lib/src/fasta/util', |
| 'lib/src/scanner', |
| ]), |
| 'lib/src/fasta/builder': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/base', |
| 'lib/src/fasta', |
| 'lib/src/fasta/parser', |
| 'lib/src/fasta/source', |
| ]), |
| 'lib/src/fasta/dill': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/fasta', |
| 'lib/src/fasta/kernel', |
| ]), |
| 'lib/src/fasta/kernel': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src/fasta', |
| 'lib/src/base', |
| 'lib/src/fasta/builder', |
| 'lib/src/fasta/dill', |
| 'lib/src/fasta/parser', |
| 'lib/src/fasta/scanner', |
| 'lib/src/fasta/source', |
| 'lib/src/fasta/type_inference', |
| 'lib/src/scanner', |
| ]), |
| 'lib/src/fasta/parser': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/fasta', |
| 'lib/src/fasta/scanner', |
| 'lib/src/fasta/util', |
| 'lib/src/scanner', |
| ]), |
| 'lib/src/fasta/scanner': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/fasta', |
| // fasta scanner produces analyzer scanner tokens |
| 'lib/src/scanner', |
| 'lib/src/fasta/util', |
| ]), |
| 'lib/src/fasta/source': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src/fasta', |
| 'lib/src/base', |
| 'lib/src/fasta/builder', |
| 'lib/src/fasta/dill', |
| 'lib/src/fasta/kernel', |
| 'lib/src/fasta/parser', |
| 'lib/src/fasta/type_inference', |
| 'lib/src/scanner', |
| ]), |
| 'lib/src/fasta/testing': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src', |
| 'lib/src/base', |
| 'lib/src/fasta', |
| 'lib/src/fasta/kernel', |
| 'lib/src/fasta/scanner', |
| 'lib/src/scanner', |
| ]), |
| 'lib/src/fasta/type_inference': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/base', |
| 'lib/src/fasta/builder', |
| 'lib/src/fasta', |
| 'lib/src/fasta/kernel', |
| 'lib/src/fasta/source', |
| ]), |
| 'lib/src/fasta/util': new SubpackageRules(), |
| 'lib/src/incremental': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/api_prototype', |
| 'lib/src', |
| 'lib/src/base', |
| 'lib/src/fasta', |
| 'lib/src/fasta/dill', |
| 'lib/src/fasta/kernel', |
| 'lib/src/fasta/parser', |
| 'lib/src/fasta/scanner', |
| 'lib/src/fasta/source', |
| ]), |
| 'lib/src/scanner': new SubpackageRules(allowedDependencies: [ |
| 'lib/src/base', |
| // For error codes. |
| 'lib/src/fasta', |
| // fasta scanner produces analyzer scanner tokens |
| 'lib/src/fasta/scanner', |
| ]), |
| 'lib/src/testing': new SubpackageRules(allowedDependencies: [ |
| 'lib/src', |
| 'lib/src/api_prototype', |
| ]), |
| }; |
| |
| /// Rules for what a subpackage may depend directly on. |
| class SubpackageRules { |
| /// Indicates whether dart files may exist in subdirectories of this |
| /// subpackage. |
| /// |
| /// If `false`, any subdirectory of this subpackage must be a separate |
| /// subpackage. |
| final bool allowSubdirs; |
| |
| /// Indicates which other subpackages a given subpackage may directly depend |
| /// on. |
| final List<String> allowedDependencies; |
| |
| var actuallyContainsFiles = false; |
| |
| var actuallyHasSubdirs = false; |
| |
| var actualDependencies = new Set<String>(); |
| |
| SubpackageRules( |
| {this.allowSubdirs: false, this.allowedDependencies: const []}); |
| } |
| |
| class _SubpackageRelationshipsTest { |
| /// File uri of the front_end package's "lib" directory. |
| final frontEndLibUri = |
| new Uri.file(package_root.packageRoot).resolve('front_end/lib/'); |
| |
| /// Indicates whether any problems have been reported yet. |
| bool problemsReported = false; |
| |
| /// Package dependencies that were actually discovered |
| final actualPackageDependencies = <String>[]; |
| |
| /// Check for problems resulting from URI [src] having a direct dependency on |
| /// URI [dst]. |
| void checkDependency(Uri src, Uri dst) { |
| var srcSubpackage = subpackageForUri(src); |
| if (srcSubpackage == null) return; |
| var srcSubpackageRules = subpackageRules[srcSubpackage]; |
| if (srcSubpackageRules == null) { |
| problem('$src is in subpackage "$srcSubpackage", which is not found in ' |
| 'subpackageRules'); |
| return; |
| } |
| srcSubpackageRules.actuallyContainsFiles = true; |
| if (dst.scheme == 'dart') return; |
| if (dst.scheme != 'package') { |
| problem('$src depends on $dst, which is neither a package: or dart: URI'); |
| return; |
| } |
| if (src.scheme == 'package' && |
| src.pathSegments[0] == 'front_end' && |
| dst.scheme == 'package' && |
| dst.pathSegments[0] != 'front_end') { |
| if (allowedPackageDependencies.contains(dst.pathSegments[0])) { |
| actualPackageDependencies.add(dst.pathSegments[0]); |
| } else { |
| problem('$src depends on package "${dst.pathSegments[0]}", which is ' |
| 'not found in allowedPackageDependencies'); |
| } |
| } |
| var dstSubPackage = subpackageForUri(dst); |
| if (dstSubPackage == null) return; |
| if (dstSubPackage == srcSubpackage) return; |
| if (srcSubpackageRules.allowedDependencies.contains(dstSubPackage)) { |
| srcSubpackageRules.actualDependencies.add(dstSubPackage); |
| } else { |
| problem('$src depends on $dst, but subpackage "$srcSubpackage" is not ' |
| 'allowed to depend on subpackage "$dstSubPackage"'); |
| } |
| } |
| |
| /// Finds all files in the front_end's "lib" directory and returns their Uris |
| /// (as "package:" URIs). |
| List<Uri> findFrontEndUris() { |
| var frontEndUris = <Uri>[]; |
| var frontEndLibPath = pathos.fromUri(frontEndLibUri); |
| for (var entity in new Directory(frontEndLibPath) |
| .listSync(recursive: true, followLinks: false)) { |
| if (entity.path.endsWith('.dart')) { |
| var posixRelativePath = pathos.url.joinAll( |
| pathos.split(pathos.relative(entity.path, from: frontEndLibPath))); |
| frontEndUris.add(Uri.parse('package:front_end/$posixRelativePath')); |
| } |
| } |
| return frontEndUris; |
| } |
| |
| /// Reports a single problem. |
| void problem(String description) { |
| print(description); |
| problemsReported = true; |
| } |
| |
| /// Tests all subpackage relationships in the front end, and returns an |
| /// appropriate exit code. |
| Future<int> run() async { |
| var frontEndUris = await findFrontEndUris(); |
| var packagesFileUri = Platform.packageConfig != null |
| ? Uri.parse(Platform.packageConfig) |
| : frontEndLibUri.resolve('../../../.packages'); |
| var graph = await graphForProgram( |
| frontEndUris, |
| new CompilerOptions() |
| ..packagesFileUri = packagesFileUri |
| ..chaseDependencies = true); |
| for (var i = 0; i < graph.topologicallySortedCycles.length; i++) { |
| for (var library in graph.topologicallySortedCycles[i].libraries.values) { |
| for (var dependency in library.dependencies) { |
| checkDependency(library.uri, dependency.uri); |
| } |
| } |
| } |
| for (var package in allowedPackageDependencies) { |
| if (!actualPackageDependencies.contains(package)) { |
| problem('$package is listed in allowedPackageDependencies, ' |
| 'but is not used'); |
| } |
| } |
| subpackageRules.forEach((subpackage, rule) { |
| if (!rule.actuallyContainsFiles) { |
| problem("$subpackage contains no files"); |
| } |
| if (rule.allowSubdirs && !rule.actuallyHasSubdirs) { |
| problem("$subpackage is allowed to have subdirectories, but doesn't"); |
| } |
| for (var dep in rule.allowedDependencies |
| .toSet() |
| .difference(rule.actualDependencies)) { |
| problem("$subpackage lists $dep as a dependency, but doesn't use it"); |
| } |
| }); |
| return problemsReported ? 1 : 0; |
| } |
| |
| /// Determines which subpackage [src] is in. |
| /// |
| /// If [src] is not part of the front end, `null` is returned. |
| String subpackageForUri(Uri src) { |
| if (src.scheme != 'package') return null; |
| if (src.pathSegments[0] != 'front_end') return null; |
| var pathWithLib = 'lib/${src.pathSegments.skip(1).join('/')}'; |
| String subpackage; |
| String pathWithinSubpackage; |
| for (var subpackagePath in subpackageRules.keys) { |
| var subpackagePathWithSlash = '$subpackagePath/'; |
| if (pathWithLib.startsWith(subpackagePathWithSlash) && |
| (subpackage == null || subpackage.length < subpackagePath.length)) { |
| subpackage = subpackagePath; |
| pathWithinSubpackage = |
| pathWithLib.substring(subpackagePathWithSlash.length); |
| } |
| } |
| if (subpackage == null) { |
| problem('Uri $src is inside package:front_end but is not in any known ' |
| 'subpackage'); |
| } else if (pathWithinSubpackage.contains('/')) { |
| if (subpackageRules[subpackage].allowSubdirs) { |
| subpackageRules[subpackage].actuallyHasSubdirs = true; |
| } else { |
| problem('Uri $src is in a subfolder of $subpackage, but that ' |
| 'subpackage does not allow dart files in subdirectories.'); |
| } |
| } |
| return subpackage; |
| } |
| } |