blob: 4373f09223f7f0d85b649f6468093ed745bdfdad [file] [log] [blame]
// 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 'package:analyzer/analyzer.dart';
import 'package:path/path.dart' as p;
import 'package:collection/collection.dart';
import 'package:pub/src/dart.dart';
import 'package:pub/src/entrypoint.dart';
import 'package:pub/src/io.dart';
import 'package:pub/src/log.dart' as log;
import 'package:pub/src/utils.dart';
import 'package:pub/src/validator.dart';
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
/// Validates that Dart source files only import declared dependencies.
class StrictDependenciesValidator extends Validator {
StrictDependenciesValidator(Entrypoint entrypoint) : super(entrypoint);
/// Lazily returns all dependency uses in [files].
///
/// Files that do not parse and directives that don't import or export
/// `package:` URLs are ignored.
Iterable<_Usage> _findPackages(Iterable<String> files) sync* {
for (var file in files) {
List<UriBasedDirective> directives;
var contents = readTextFile(file);
try {
directives = parseImportsAndExports(contents, name: file);
} on AnalyzerErrorGroup catch (e, s) {
// Ignore files that do not parse.
log.fine(getErrorMessage(e));
log.fine(new Chain.forTrace(s).terse);
continue;
}
for (var directive in directives) {
Uri url;
try {
url = Uri.parse(directive.uri.stringValue);
} on FormatException catch (_) {
// Ignore a format exception. [url] will be null, and we'll emit an
// "Invalid URL" warning below.
}
// If the URL could not be parsed or it is a `package:` URL AND there
// are no segments OR any segment are empty, it's invalid.
if (url == null ||
(url.scheme == 'package' &&
(url.pathSegments.length < 2 ||
url.pathSegments.any((s) => s.isEmpty)))) {
warnings.add(
_Usage.errorMessage('Invalid URL.', file, contents, directive));
} else if (url.scheme == 'package') {
yield new _Usage(file, contents, directive, url);
}
}
}
}
Future validate() async {
var dependencies = entrypoint.root.dependencies.keys.toSet()
..add(entrypoint.root.name);
var devDependencies = new MapKeySet(entrypoint.root.devDependencies);
_validateLibBin(dependencies, devDependencies);
_validateBenchmarkExampleTestTool(dependencies, devDependencies);
}
/// Validates that no Dart files in `lib/` or `bin/` have dependencies that
/// aren't in [deps].
///
/// The [devDeps] are used to generate special warnings for files that import
/// dev dependencies.
void _validateLibBin(Set<String> deps, Set<String> devDeps) {
for (var usage in _usagesBeneath(['lib', 'bin'])) {
if (!deps.contains(usage.package)) {
if (devDeps.contains(usage.package)) {
warnings.add(usage.dependencyMisplaceMessage());
} else {
warnings.add(usage.dependencyMissingMessage());
}
}
}
}
/// Validates that no Dart files in `benchmark/`, `example/, `test/` or
/// `tool/` have dependencies that aren't in [deps] or [devDeps].
void _validateBenchmarkExampleTestTool(
Set<String> deps, Set<String> devDeps) {
for (var usage
in _usagesBeneath(['benchmark', 'example', 'test', 'tool'])) {
if (!deps.contains(usage.package) && !devDeps.contains(usage.package)) {
warnings.add(usage.dependencyMissingMessage());
}
}
}
Iterable<_Usage> _usagesBeneath(List<String> paths) => _findPackages(paths
.map((path) => entrypoint.root.listFiles(beneath: path))
.expand((files) => files)
.where((String file) => p.extension(file) == '.dart'));
}
/// A parsed import or export directive in a D source file.
class _Usage {
/// Returns a formatted error message highlighting [directive] in [file].
static String errorMessage(String message, String file, String contents,
UriBasedDirective directive) {
return new SourceFile.fromString(contents, url: file)
.span(directive.offset, directive.offset + directive.length)
.message(message);
}
/// The path to the file from which [_directive] was parsed.
final String _file;
/// The contents of [_file].
final String _contents;
/// The URI parsed from [_directive].
final Uri _url;
/// The directive that uses [_url].
final UriBasedDirective _directive;
_Usage(this._file, this._contents, this._directive, this._url);
/// The name of the package referred to by this usage..
String get package => _url.pathSegments.first;
/// Returns a message associated with [_directive].
///
/// We assume that normally all directives are valid and we won't see an error
/// message, so we create the SourceFile lazily to avoid parsing line endings
/// in the case of only valid directives.
String _toMessage(String message) =>
errorMessage(message, _file, _contents, _directive);
/// Returns an error message saying the package is not listed in dependencies.
String dependencyMissingMessage() =>
_toMessage("This package doesn't depend on $package.");
/// Returns an error message saying the package should be in `dependencies`.
String dependencyMisplaceMessage() {
var shortFile = p.split(p.relative(_file)).first;
return _toMessage(
'$package is a dev dependency. Packages used in $shortFile/ must be '
'declared as normal dependencies.');
}
}