blob: b305e65bc6bf93f10c9b948b627ae97713ef4947 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. 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:io';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart' as analyzer;
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:dart_strict_deps_lib/exceptions.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:quiver/check.dart';
import 'package:quiver/core.dart';
import 'package:tools.dart-strict-deps.dart_strict_deps_proto/protos/models.pb.dart';
p.Context context = p.Context(style: p.Style.posix);
// Collects list of source files from direct dependencies.
Set<String> collectKnownLibsFromDirectDeps({BuildInfo buildInfo}) =>
Set.from(buildInfo.buildTargets.expand((target) => target.rebasedSources));
// Collects list of known packages from direct dependencies
Set<String> collectKnownPackagesFromDirectDeps({BuildInfo buildInfo}) =>
Set.from(buildInfo.buildTargets.map((target) => target.packageName));
/// Returns compilation unit read with dart analyzer from filepath.
Optional<CompilationUnit> parseCompilationUnit(String filePath) {
try {
return Optional.of(analyzer
.parseFile(
path: filePath, featureSet: FeatureSet.latestLanguageVersion())
.unit);
} on FileSystemException {
return Optional.absent();
}
}
/// Returns true if the [uri] scheme is considered a Dart SDK import.
bool isDartSDKImport({@required Uri uri}) =>
(uri?.scheme == 'dart' || uri?.scheme == 'dart-ext');
/// Resolves import paths (package imports and relative imports) to absolute file path.
///
/// Returns an absent Optional if the import does not have a valid scheme.
Optional<String> resolveImportPath(
{@required Uri uri,
@required ResolverContext resolverContext,
@required ImportResult_Type type}) {
if (type == ImportResult_Type.PACKAGE) {
Uri resolved = resolverContext.resolver.resolve(uri);
if (resolved == null) return Optional.absent();
return Optional.of(resolved.path);
} else if (type == ImportResult_Type.RELATIVE) {
//local path
return Optional.of(context.normalize(
context.join(resolverContext.currentFileDirectory, uri.path)));
}
return Optional.absent();
}
/// Returns true if the [path] exists on file system.
bool pathExists(String path) =>
FileSystemEntity.typeSync(path) != FileSystemEntityType.notFound;
/// Returns the type of import [uri] represents.
ImportResult_Type getImportType(Uri uri) {
if (uri == null) {
return ImportResult_Type.ERROR;
}
if (isDartSDKImport(uri: uri)) {
return ImportResult_Type.DART_SDK;
} else if (uri.hasScheme) {
// package is only valid scheme after dart and dart_ext
return uri.isScheme('package')
? ImportResult_Type.PACKAGE
: ImportResult_Type.ERROR;
}
return ImportResult_Type.RELATIVE;
}
// Returns a ImportResult with context info filled out, specifically lineInfo and importURI.
ImportResult getBaseImportResultFromDirective(ImportDirective directive,
{LineInfo lineInfo, Uri uri}) {
final lineNumber =
lineInfo.getLocation(directive.offset).lineNumber.toString();
return ImportResult()
..lineInfo = lineNumber
..importUri = uri.toString();
}
/// Wrapper class for path resolution helpers.
class ResolverContext {
final PackageConfig resolver;
Set<String> knownLibFiles;
Set<String> knownPackages;
String currentFileDirectory;
String currentPackage;
ResolverContext(this.resolver, this.knownLibFiles, this.knownPackages,
{this.currentFileDirectory});
}
/// Returns dart package being imported
///
/// Returns Optional.absent if uri scheme could not be found.
Optional<String> getDartPackage(Uri uri, {ResolverContext resolverContext}) {
if (uri.isScheme('package') && uri.pathSegments.isNotEmpty) {
return Optional.of(uri.pathSegments[0]);
} else if (!uri.hasScheme) {
return Optional.fromNullable(resolverContext?.currentPackage);
}
return Optional.absent();
}
/// Returns a ImportResult representing the import defined by [directive] after resolution.
ImportResult directiveToImportResult(
{LineInfo lineInfo,
ImportDirective directive,
ResolverContext resolverContext}) {
final uri = Uri.parse(Uri.encodeFull(directive.uri.stringValue));
final importResult =
getBaseImportResultFromDirective(directive, lineInfo: lineInfo, uri: uri)
..type = getImportType(uri);
if (importResult.type == ImportResult_Type.DART_SDK) {
importResult.state = ImportResult_State.FOUND;
return importResult;
}
final path = resolveImportPath(
uri: uri, type: importResult.type, resolverContext: resolverContext);
final dartpkg = getDartPackage(uri, resolverContext: resolverContext);
if (dartpkg.isPresent) {
importResult
..dartPackage = dartpkg.value
..isDartPackageImported =
resolverContext.knownPackages.contains(dartpkg.value);
}
if (importResult.isDartPackageImported) {
importResult.state = ImportResult_State.FOUND;
} else {
importResult.state = ImportResult_State.NOT_FOUND;
}
if (path.isPresent) {
importResult..resolvedLocation = path.value;
}
return importResult;
}
/// Goes through compilationUnit and handles import directives.
FileCheckResult checkCompilationUnit(CompilationUnit compUnit,
{@required String file, @required ResolverContext resolverContext}) {
checkArgument(compUnit != null);
final lineInfo = compUnit.lineInfo;
final result = FileCheckResult()..filePath = file;
resolverContext.currentFileDirectory = context.dirname(file);
result.imports.addAll(compUnit.directives
.whereType<ImportDirective>()
.map((directive) => directiveToImportResult(
lineInfo: lineInfo,
directive: directive,
resolverContext: resolverContext))
.where((importResult) => importResult != null)
.toList());
return result;
}
/// Processes and analyzes specific target from GN.
///
/// parseCompilationUnitFunc is needed for unit testing
TargetCheckResult checkTarget(
{@required BuildTarget target,
@required ResolverContext resolverContext,
Optional<CompilationUnit> Function(String) parseCompilationUnitFunc =
parseCompilationUnit}) {
TargetCheckResult targetCheckResult = TargetCheckResult();
for (var file in target.rebasedSources) {
final compUnit = parseCompilationUnitFunc(file);
if (compUnit.isNotPresent) continue;
FileCheckResult parsedFile = checkCompilationUnit(compUnit.value,
file: file, resolverContext: resolverContext);
targetCheckResult.files.add(parsedFile);
}
return targetCheckResult;
}
/// Returns result of strict dependency check run against the target the metadata collection was run on.
///
/// (metadata collection is run against a specific target)
/// parseCompilationUnitFunc is needed for unit testing
TargetCheckResult checkBuildInfo(
{@required BuildInfo buildInfo,
@required PackageConfig resolver,
Optional<CompilationUnit> Function(String) parseCompilationUnitFunc =
parseCompilationUnit}) {
final knownLibs = collectKnownLibsFromDirectDeps(buildInfo: buildInfo);
final knownPackages =
collectKnownPackagesFromDirectDeps(buildInfo: buildInfo);
// find the target the metadata collection was run on
final target = Optional.fromNullable(buildInfo.buildTargets
.singleWhere((i) => i.isCurrentTarget, orElse: () => null));
if (target.isNotPresent) {
throw StrictDepsFatalException(
'Could not find current target in buildinfo');
}
ResolverContext resolverContext =
ResolverContext(resolver, knownLibs, knownPackages)
..currentPackage = target.value.packageName;
return checkTarget(
target: target.value,
resolverContext: resolverContext,
parseCompilationUnitFunc: parseCompilationUnitFunc);
}