blob: 881c55c67eb66829cbe57a2aaeb5b0a70b8c513a [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/dart/ast/ast.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import '../dart.dart';
import '../entrypoint.dart';
import '../io.dart';
import '../log.dart' as log;
import '../utils.dart';
import '../validator.dart';
/// Validates that Dart source files only import declared dependencies.
class StrictDependenciesValidator extends Validator {
final AnalysisContextManager analysisContextManager =
AnalysisContextManager();
StrictDependenciesValidator(Entrypoint entrypoint) : super(entrypoint) {
var packagePath = p.normalize(p.absolute(entrypoint.root.dir));
analysisContextManager.createContextsForDirectory(packagePath);
}
/// 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 = analysisContextManager.parseImportsAndExports(file);
} on AnalyzerErrorGroup catch (e, s) {
// Ignore files that do not parse.
log.fine(getErrorMessage(e));
log.fine(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)))) {
errors.add(
_Usage.errorMessage('Invalid URL.', file, contents, directive));
} else if (url.scheme == 'package') {
yield _Usage(file, contents, directive, url);
}
}
}
}
@override
Future validate() async {
var dependencies = entrypoint.root.dependencies.keys.toSet()
..add(entrypoint.root.name);
var devDependencies = MapKeySet(entrypoint.root.devDependencies);
_validateLibBin(dependencies, devDependencies);
_validateBenchmarkTestTool(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)) {
errors.add(usage.dependencyMisplaceMessage());
} else {
errors.add(usage.dependencyMissingMessage());
}
}
}
}
/// Validates that no Dart files in `benchmark/`, `test/` or
/// `tool/` have dependencies that aren't in [deps] or [devDeps].
void _validateBenchmarkTestTool(Set<String> deps, Set<String> devDeps) {
var directories = ['benchmark', 'test', 'tool'];
for (var usage in _usagesBeneath(directories)) {
if (!deps.contains(usage.package) && !devDeps.contains(usage.package)) {
warnings.add(usage.dependenciesMissingMessage());
}
}
}
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 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 does not have $package in the `dependencies` '
'section of `pubspec.yaml`.');
/// Returns an error message saying the package is not listed in `dependencies`
/// or `dev_dependencies`.
String dependenciesMissingMessage() =>
_toMessage('This package does not have $package in the `dependencies` '
'or `dev_dependencies` section of `pubspec.yaml`.');
/// 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 in the `dev_dependencies` section of `pubspec.yaml`. '
'Packages used in $shortFile/ must be declared in the `dependencies` '
'section.');
}
}