blob: 91977e5a976363afc03ff4172e582cfc6d163156 [file] [log] [blame] [edit]
// Copyright (c) 2025, 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:io' show Platform;
import 'package:_js_interop_checks/js_interop_checks.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:collection/collection.dart';
import 'package:front_end/src/api_prototype/codes.dart'
show Message, LocatedMessage;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/target/targets.dart' show DiagnosticReporter;
import 'package:linter/src/lint_codes.dart';
import 'package:linter/src/rules/avoid_double_and_int_checks.dart';
import 'package:linter/src/rules/invalid_runtime_check_with_js_interop_types.dart';
import 'package:path/path.dart' as p;
/// Used to record the type of error in Flutter telemetry.
///
/// DO NOT alter the numeric values or change the meaning of these entries.
///
/// It is MUCH better to add new values and deprecate old values.
/// Consider commenting out the old value line as a tombstone.
///
/// Please notify folks in charge of Flutter Web analytics before making any
/// changes.
enum _DryRunErrorCode {
noDartHtml(0),
noDartJs(1),
interopChecksError(2),
// Deprecated.
// ignore: unused_field
isTestValueError(3),
// Deprecated.
// ignore: unused_field
isTestTypeError(4),
// Deprecated.
// ignore: unused_field
isTestGenericTypeError(5),
invalidRuntimeCheckWithJsInteropTypesDartAsJs(6),
invalidRuntimeCheckWithJsInteropTypesDartIsJs(7),
invalidRuntimeCheckWithJsInteropTypesJsAsDart(8),
invalidRuntimeCheckWithJsInteropTypesJsIsDart(9),
invalidRuntimeCheckWithJsInteropTypesJsAsIncompatibleJs(10),
invalidRuntimeCheckWithJsInteropTypesJsIsInconsistentJs(11),
invalidRuntimeCheckWithJsInteropTypesJsIsUnrelatedJs(12),
avoidDoubleAndIntChecks(13);
const _DryRunErrorCode(this.code);
final int code;
}
class _DryRunError {
final _DryRunErrorCode code;
final String problemMessage;
final Uri? errorSourceUri;
final Location? errorLocation;
_DryRunError(this.code, this.problemMessage,
{this.errorSourceUri, this.errorLocation});
static String _locationToString(Uri? sourceUri, Location? location) {
if (sourceUri == null && location == null) return 'unknown location';
final uri = sourceUri ?? location?.file;
final lineCol =
location != null ? ' ${location.line}:${location.column}' : '';
return '$uri$lineCol';
}
@override
String toString() => '${_locationToString(errorSourceUri, errorLocation)} '
'- $problemMessage (${code.code})';
}
/// Runs several passes over the provided kernel to find any code in the
/// sources that could block a migration to WASM from JS.
///
/// At the end of execution, it emits a summary to stdout that looks like:
/// ```
/// Found incompatibilities with WebAssembly.
///
/// package:dryrun/test.dart 6:7 - JS interop class 'B' cannot extend Dart class 'A'. (2)
/// ````
class DryRunSummarizer {
final _avoidDoubleAndIntChecks = AvoidDoubleAndIntChecks();
final _invalidRuntimeCheckWithJSInteropTypes =
InvalidRuntimeCheckWithJSInteropTypes();
final Component component;
late final CoreTypes coreTypes;
late final ClassHierarchy classHierarchy;
DryRunSummarizer(this.component) {
coreTypes = CoreTypes(component);
classHierarchy = ClassHierarchy(component, coreTypes);
}
static const Map<String, _DryRunErrorCode> _disallowedDartUris = {
'dart:html': _DryRunErrorCode.noDartHtml,
'dart:js': _DryRunErrorCode.noDartJs,
// 'dart:ffi' is handled by interop checks.
};
List<_DryRunError> _analyzeImports() {
final errors = <_DryRunError>[];
for (final library in component.libraries) {
if (library.importUri.scheme == 'dart') continue;
for (final dep in library.dependencies) {
final depLib = dep.importedLibraryReference.asLibrary;
final code = _disallowedDartUris[depLib.importUri.toString()];
if (code != null) {
errors.add(_DryRunError(code, '${depLib.importUri} unsupported',
errorSourceUri: library.importUri, errorLocation: dep.location));
}
}
}
return errors;
}
List<_DryRunError> _interopChecks() {
final collector = _CollectingDiagnosticReporter(component);
final reporter = JsInteropDiagnosticReporter(collector);
// These checks will already have been done by the CFE but the message
// format the CFE provides for those errors makes it hard to identify them
// as interop-specific errors. Instead we rerun and collect any errors here.
component.accept(JsInteropChecks(coreTypes, classHierarchy, reporter,
JsInteropChecks.getNativeClasses(component),
isDart2Wasm: true));
return collector.errors;
}
Future<List<_DryRunError>> _lintChecks() async {
final errors = <_DryRunError>[];
final pathUriMap = <String, Uri>{};
for (final library in component.libraries) {
if (library.importUri.scheme == 'dart') continue;
final uri = library.fileUri;
pathUriMap[p.normalize(
p.absolute(uri.toFilePath(windows: Platform.isWindows)))] = uri;
}
if (pathUriMap.isEmpty) return [];
final collection =
AnalysisContextCollection(includedPaths: pathUriMap.keys.toList());
for (var context in collection.contexts) {
var allOptions =
(context as DriverBasedAnalysisContext).allAnalysisOptions;
for (var options in allOptions) {
options as AnalysisOptionsImpl;
options.lintRules = [
_avoidDoubleAndIntChecks,
_invalidRuntimeCheckWithJSInteropTypes
];
options.lint = true;
}
for (var filePath in context.contextRoot.analyzedFiles()) {
final uri = pathUriMap[filePath]!;
var result = await context.currentSession.getErrors(filePath);
if (result is ErrorsResult) {
for (final diagnostic in result.diagnostics) {
final errorCode = _getDryRunErrorCodeFromDiagnostic(diagnostic);
if (errorCode != null) {
errors.add(_DryRunError(
errorCode,
'${diagnostic.diagnosticCode.name} lint violation: '
'${diagnostic.message}',
errorSourceUri: uri,
errorLocation: component.getLocation(
uri, diagnostic.problemMessage.offset)));
}
}
}
}
}
await collection.dispose();
return errors;
}
_DryRunErrorCode? _getDryRunErrorCodeFromDiagnostic(Diagnostic diagnostic) =>
switch (diagnostic.diagnosticCode) {
LinterLintCode.invalidRuntimeCheckWithJsInteropTypesDartAsJs =>
_DryRunErrorCode.invalidRuntimeCheckWithJsInteropTypesDartAsJs,
LinterLintCode.invalidRuntimeCheckWithJsInteropTypesDartIsJs =>
_DryRunErrorCode.invalidRuntimeCheckWithJsInteropTypesDartIsJs,
LinterLintCode.invalidRuntimeCheckWithJsInteropTypesJsAsDart =>
_DryRunErrorCode.invalidRuntimeCheckWithJsInteropTypesJsAsDart,
LinterLintCode.invalidRuntimeCheckWithJsInteropTypesJsIsDart =>
_DryRunErrorCode.invalidRuntimeCheckWithJsInteropTypesJsIsDart,
LinterLintCode
.invalidRuntimeCheckWithJsInteropTypesJsAsIncompatibleJs =>
_DryRunErrorCode
.invalidRuntimeCheckWithJsInteropTypesJsAsIncompatibleJs,
LinterLintCode
.invalidRuntimeCheckWithJsInteropTypesJsIsInconsistentJs =>
_DryRunErrorCode
.invalidRuntimeCheckWithJsInteropTypesJsIsInconsistentJs,
LinterLintCode.invalidRuntimeCheckWithJsInteropTypesJsIsUnrelatedJs =>
_DryRunErrorCode.invalidRuntimeCheckWithJsInteropTypesJsIsUnrelatedJs,
LinterLintCode.avoidDoubleAndIntChecks =>
_DryRunErrorCode.avoidDoubleAndIntChecks,
_ => null,
};
Future<bool> summarize() async {
final errors = [
..._analyzeImports(),
..._interopChecks(),
...(await _lintChecks()),
// TODO(srujzs): Add additional number semantics incompatibility checks
// here.
];
if (errors.isNotEmpty) {
print('Found incompatibilities with WebAssembly.\n');
print(errors.join('\n'));
return true;
}
return false;
}
}
class _CollectingDiagnosticReporter
extends DiagnosticReporter<Message, LocatedMessage> {
final Component component;
final List<_DryRunError> errors = <_DryRunError>[];
_CollectingDiagnosticReporter(this.component);
@override
void report(Message message, int charOffset, int length, Uri? fileUri,
{List<LocatedMessage>? context}) {
final libraryUri = fileUri != null
? component.libraries
.firstWhereOrNull((e) => e.fileUri == fileUri)
?.importUri
: null;
final location =
fileUri != null ? component.getLocation(fileUri, charOffset) : null;
errors.add(_DryRunError(
_DryRunErrorCode.interopChecksError, message.problemMessage,
errorSourceUri: libraryUri ?? fileUri, errorLocation: location));
}
}