|  | // 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), | 
|  | // 3, 4, 5 are tombstones. Don't reuse! | 
|  | // isTestValueError(3), | 
|  | // isTestTypeError(4), | 
|  | // 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)); | 
|  | } | 
|  | } |