blob: fba626f4ae1b79f109cbb805fb3c9c8fd1461aca [file] [log] [blame]
// Copyright (c) 2016, 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:collection';
import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/builder.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_general.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:package_config/packages.dart';
import 'package:yaml/yaml.dart';
/**
* Traverses the package structure to determine the transitive dependencies for
* a given package.
*/
class DependencyFinder {
/**
* The name of the pubspec.yaml file.
*/
static const String pubspecName = 'pubspec.yaml';
/**
* The resource provider used to access the file system.
*/
final ResourceProvider resourceProvider;
/**
* A table mapping the absolute paths of packages to a list of the names of
* the packages on which those packages depend.
*/
final Map<String, List<String>> dependencyMap =
new HashMap<String, List<String>>();
/**
* Initialize a newly created dependency finder to use the given
* [resourceProvider] to access the file system.
*/
DependencyFinder(this.resourceProvider);
/**
* Return a sorted list of the directories containing all of the packages on
* which the package at the given [packagePath] depends. The [packageMap]
* maps the names of packages to the directories in which they are contained.
*
* Throws an [AnalysisException] if any of the packages are missing their
* 'pubspec.yaml' file.
*/
List<String> transitiveDependenciesFor(
Map<String, List<Folder>> packageMap, String packagePath) {
Set<String> processedPackages = new HashSet<String>();
Set<String> processedPaths = new HashSet<String>();
void process(String packageName) {
if (processedPackages.add(packageName)) {
List<Folder> folderList = packageMap[packageName];
if (folderList == null || folderList.isEmpty) {
throw new StateError('No mapping for package "$packageName"');
}
String packagePath = folderList[0].path;
processedPaths.add(packagePath);
List<String> dependencies = _dependenciesFor(packagePath);
for (String dependency in dependencies) {
process(dependency);
}
}
}
List<String> dependencies = _dependenciesFor(packagePath);
dependencies.forEach(process);
processedPaths.remove(packagePath);
List<String> transitiveDependencies = processedPaths.toList();
transitiveDependencies.sort();
return transitiveDependencies;
}
/**
* Add to the given set of [dependecies] all of the package names used as keys
* in the given [yamlMap].
*/
void _collectDependencies(HashSet<String> dependencies, YamlMap yamlMap) {
if (yamlMap is Map) {
for (var key in yamlMap.keys) {
if (key is String) {
dependencies.add(key);
}
}
}
}
/**
* Return a list of the names of the packages on which the package at the
* [packagePath] depends.
*/
List<String> _dependenciesFor(String packagePath) {
return dependencyMap.putIfAbsent(packagePath, () {
Set<String> dependencies = new HashSet<String>();
YamlNode yamlNode = _readPubspec(packagePath);
if (yamlNode is YamlMap) {
_collectDependencies(dependencies, yamlNode['dependencies']);
}
return dependencies.toList();
});
}
/**
* Read the content of the pubspec file in the directory at the given
* [directoryPath]. Return `null` if the file does not exist, cannot be read,
* or has content that is not valid YAML.
*/
YamlNode _readPubspec(String directoryPath) {
try {
File yamlFile = resourceProvider
.getFolder(directoryPath)
.getChildAssumingFile(pubspecName);
String yamlContent = yamlFile.readAsStringSync();
return loadYamlNode(yamlContent);
} catch (exception, stackTrace) {
throw new AnalysisException('Missing $pubspecName in $directoryPath',
new CaughtException(exception, stackTrace));
}
}
}
/**
* A description of the context in which a package will be analyzed.
*/
class PackageDescription {
/**
* The id of the package being described. The id encodes the actual locations
* of the package itself and all of the packages on which it depends.
*/
final String id;
/**
* The SDK against which the package will be analyzed.
*/
final DartSdk sdk;
/**
* The analysis options that will be used when analyzing the package.
*/
final AnalysisOptions options;
/**
* Initialize a newly create package description to describe the package with
* the given [id] that is being analyzed against the given [sdk] using the
* given [options].
*/
PackageDescription(this.id, this.sdk, this.options);
@override
int get hashCode {
int hashCode = 0;
for (int value in options.signature) {
hashCode = JenkinsSmiHash.combine(hashCode, value);
}
hashCode = JenkinsSmiHash.combine(hashCode, id.hashCode);
hashCode = JenkinsSmiHash.combine(hashCode, sdk.hashCode);
return JenkinsSmiHash.finish(hashCode);
}
@override
bool operator ==(Object other) {
return other is PackageDescription &&
other.sdk == sdk &&
AnalysisOptions.signaturesEqual(
other.options.signature, options.signature) &&
other.id == id;
}
}
/**
* Manages the contexts in which each package is analyzed.
*/
class PackageManager {
/**
* The resource provider used to access the file system.
*/
final ResourceProvider resourceProvider;
/**
* A table mapping the id's of packages to the context in which the package is
* analyzed.
*/
final Map<PackageDescription, AnalysisContext> contextMap =
new HashMap<PackageDescription, AnalysisContext>();
/**
* Initialize a newly created package manager.
*/
PackageManager(this.resourceProvider);
/**
* Return the context in which the package at the given [packagePath] should
* be analyzed when the given [packages] object is used to resolve package
* names, the given [resolver] will be used to resolve 'dart:' URI's, and the
* given [options] will control the analysis.
*/
AnalysisContext getContext(String packagePath, Packages packages,
DartUriResolver resolver, AnalysisOptions options) {
DartSdk sdk = resolver.dartSdk;
Map<String, List<Folder>> packageMap =
new ContextBuilder(resourceProvider, null, null)
.convertPackagesToMap(packages);
PackageDescription description =
new PackageDescription(_buildId(packagePath, packageMap), sdk, options);
return contextMap.putIfAbsent(description, () {
AnalysisContext context = AnalysisEngine.instance.createAnalysisContext();
context.sourceFactory = new SourceFactory(<UriResolver>[
resolver,
new PackageMapUriResolver(resourceProvider, packageMap),
new ResourceUriResolver(resourceProvider)
], packages, resourceProvider);
context.analysisOptions = options;
return context;
});
}
/**
* Return the id associated with the package at the given [packagePath] when
* the given [packageMap] is used to resolve package names.
*/
String _buildId(String packagePath, Map<String, List<Folder>> packageMap) {
DependencyFinder finder = new DependencyFinder(resourceProvider);
List<String> dependencies =
finder.transitiveDependenciesFor(packageMap, packagePath);
StringBuffer buffer = new StringBuffer();
buffer.write(packagePath);
for (String dependency in dependencies) {
buffer.write(';');
buffer.write(dependency);
}
return buffer.toString();
}
}