blob: 5b882a8a3c144acfb1dbf426e75268966e20341a [file] [log] [blame]
// Copyright (c) 2020, 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/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:cli_util/cli_util.dart';
import 'package:path/path.dart' as path;
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
import 'io.dart';
import 'language_version.dart';
import 'package.dart';
import 'package_name.dart';
import 'pubspec.dart';
import 'solver.dart';
import 'source.dart';
import 'source/cached.dart';
import 'source/path.dart';
import 'system_cache.dart';
enum NullSafetyCompliance {
/// This package and all dependencies opted into null safety.
compliant,
/// This package opted into null safety, but some file or dependency is not
/// opted in.
mixed,
/// This package did not opt-in to null safety yet.
notCompliant,
/// The resolution failed. Or some dart file in a dependency
/// doesn't parse.
analysisFailed,
}
class NullSafetyAnalysis {
static const String guideUrl = 'https://dart.dev/null-safety/migration-guide';
final SystemCache _systemCache;
/// A cache of the analysis done for a single package-version, not taking
/// dependencies into account. (Only the sdk constraint and no-files-opt-out).
///
/// This allows us to reuse the analysis of the same package-version when
/// used as a dependency from different packages.
///
/// Furthermore by awaiting the Future stored here, we avoid race-conditions
/// from downloading the same package-version into [_systemCache]
/// simultaneously when doing concurrent analyses.
final Map<PackageId, Future<NullSafetyAnalysisResult>>
_packageInternallyGoodCache = {};
NullSafetyAnalysis(SystemCache systemCache) : _systemCache = systemCache;
/// Decides if package version [packageId] and all its non-dev
/// dependencies (transitively) have a language version opting in to
/// null-safety and no files in lib/ of these packages opt out to a
/// pre-null-safety language version.
///
/// This will do a full resolution of that package's import graph, and also
/// download the package and all dependencies into [cache].
///
/// To avoid race conditions on downloading to the cache, only one instance
/// should be computing nullSafetyCompliance simultaneously with the same
/// cache.
///
/// If [packageId] is a relative path dependency [containingPath] must be
/// provided with an absolute path to resolve it against.
Future<NullSafetyAnalysisResult> nullSafetyCompliance(PackageId packageId,
{String? containingPath}) async {
// A space in the name prevents clashes with other package names.
final fakeRootName = '${packageId.name} importer';
final fakeRoot = Package.inMemory(Pubspec(fakeRootName,
fields: {
'dependencies': {
packageId.name: {
packageId.source!.name: packageId.source is PathSource
? (packageId.description['relative']
? path.join(
containingPath!, packageId.description['path'])
: packageId.description['path'])
: packageId.description,
'version': packageId.version.toString(),
}
}
},
sources: _systemCache.sources));
final rootPubspec =
await packageId.source!.bind(_systemCache).describe(packageId);
final rootLanguageVersion = rootPubspec.languageVersion;
if (!rootLanguageVersion.supportsNullSafety) {
final span =
_tryGetSpanFromYamlMap(rootPubspec.fields['environment'], 'sdk');
final where = span == null
? 'in the sdk constraint in the enviroment key in pubspec.yaml.'
: 'in pubspec.yaml: \n${span.highlight()}';
return NullSafetyAnalysisResult(
NullSafetyCompliance.notCompliant,
'Is not opting in to null safety $where',
);
}
SolveResult result;
try {
result = await resolveVersions(
SolveType.get,
_systemCache,
fakeRoot,
);
} on SolveFailure catch (e) {
return NullSafetyAnalysisResult(NullSafetyCompliance.analysisFailed,
'Could not resolve constraints: $e');
}
return nullSafetyComplianceOfPackages(
result.packages.where((id) => id.name != fakeRootName),
Package(rootPubspec,
packageId.source!.bind(_systemCache).getDirectory(packageId)),
);
}
/// Decides if all dependendencies (transitively) have a language version
/// opting in to null safety, and no files in lib/ of these packages, nor the
/// root package opt out to a pre-null-safety language version.
///
/// [rootPubspec] is the pubspec of the root package.
// TODO(sigurdm): make a source for the root package. Then we should not need
// to pass this.
///
/// This will download all dependencies into [cache].
///
/// Assumes the root package is opted in.
Future<NullSafetyAnalysisResult> nullSafetyComplianceOfPackages(
Iterable<PackageId> packages,
Package rootPackage,
) async {
NullSafetyAnalysisResult? firstBadPackage;
for (final dependencyId in packages) {
final packageInternalAnalysis =
await _packageInternallyGoodCache.putIfAbsent(dependencyId, () async {
Pubspec pubspec;
BoundSource? boundSource;
String packageDir;
if (dependencyId.source == null) {
pubspec = rootPackage.pubspec;
packageDir = rootPackage.dir;
} else {
boundSource = _systemCache.source(dependencyId.source);
pubspec = await boundSource.describe(dependencyId);
packageDir = boundSource.getDirectory(dependencyId);
}
if (!pubspec.languageVersion.supportsNullSafety) {
final span =
_tryGetSpanFromYamlMap(pubspec.fields['environment'], 'sdk');
final where = span == null
? 'in the sdk constraint in the environment key in its pubspec.yaml.'
: 'in its pubspec.yaml:\n${span.highlight()}';
return NullSafetyAnalysisResult(
NullSafetyCompliance.notCompliant,
'package:${dependencyId.name} is not opted into null safety $where',
);
}
if (boundSource is CachedSource) {
// TODO(sigurdm): Consider using withDependencyType here.
await boundSource.downloadToSystemCache(dependencyId);
}
final libDir =
path.absolute(path.normalize(path.join(packageDir, 'lib')));
if (dirExists(libDir)) {
var contextCollection = AnalysisContextCollection(
includedPaths: [path.normalize(packageDir)],
resourceProvider: PhysicalResourceProvider.INSTANCE,
sdkPath: getSdkPath(),
);
var analysisContext = contextCollection.contexts.first;
var analysisSession = analysisContext.currentSession;
for (final file in listDir(libDir,
recursive: true, includeDirs: false, includeHidden: true)) {
if (file.endsWith('.dart')) {
final fileUrl =
'package:${dependencyId.name}/${path.relative(file, from: libDir)}';
final someUnitResult =
analysisSession.getParsedUnit(path.normalize(file));
ParsedUnitResult unitResult;
if (someUnitResult is ParsedUnitResult) {
unitResult = someUnitResult;
} else {
return NullSafetyAnalysisResult(
NullSafetyCompliance.analysisFailed,
'Could not analyze $fileUrl.');
}
if (unitResult.errors.isNotEmpty) {
return NullSafetyAnalysisResult(
NullSafetyCompliance.analysisFailed,
'Could not analyze $fileUrl.');
}
if (unitResult.isPart) continue;
final languageVersionToken = unitResult.unit.languageVersionToken;
if (languageVersionToken == null) continue;
final languageVersion = LanguageVersion.fromLanguageVersionToken(
languageVersionToken);
if (!languageVersion.supportsNullSafety) {
final sourceFile =
SourceFile.fromString(readTextFile(file), url: fileUrl);
final span = sourceFile.span(languageVersionToken.offset,
languageVersionToken.offset + languageVersionToken.length);
return NullSafetyAnalysisResult(
NullSafetyCompliance.notCompliant,
'$fileUrl is opting out of null safety:\n${span.highlight()}');
}
}
}
}
return NullSafetyAnalysisResult(NullSafetyCompliance.compliant, null);
});
if (packageInternalAnalysis.compliance ==
NullSafetyCompliance.analysisFailed) {
return packageInternalAnalysis;
}
if (packageInternalAnalysis.compliance ==
NullSafetyCompliance.notCompliant) {
firstBadPackage ??= packageInternalAnalysis;
}
}
if (firstBadPackage == null) {
return NullSafetyAnalysisResult(NullSafetyCompliance.compliant, null);
}
if (firstBadPackage.compliance == NullSafetyCompliance.analysisFailed) {
return firstBadPackage;
}
return NullSafetyAnalysisResult(
NullSafetyCompliance.mixed, firstBadPackage.reason);
}
}
class NullSafetyAnalysisResult {
final NullSafetyCompliance compliance;
/// `null` if compliance == [NullSafetyCompliance.compliant].
final String? reason;
NullSafetyAnalysisResult(this.compliance, this.reason);
}
SourceSpan? _tryGetSpanFromYamlMap(Object? map, String key) {
if (map is YamlMap) {
return map.nodes[key]?.span;
}
return null;
}