| // Copyright (c) 2022, 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 'dart:typed_data'; | |
| // ignore: implementation_imports | |
| import 'package:_js_interop_checks/src/deprecated_js_interop_imports.dart' | |
| show | |
| DeprecatedJsInteropImportPaths, | |
| findDeprecatedJsInteropImportPaths, | |
| formatImportTree, | |
| hasDeprecatedJsInteropImports; | |
| // ignore: implementation_imports | |
| import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'; | |
| import 'package:collection/collection.dart'; | |
| // ignore: implementation_imports | |
| import 'package:front_end/src/api_unstable/dart2js.dart' as fe; | |
| import 'package:kernel/ast.dart' as ir; | |
| import 'package:kernel/binary/ast_from_binary.dart' show BinaryBuilder; | |
| import 'package:kernel/class_hierarchy.dart' as ir; | |
| import 'package:kernel/core_types.dart' as ir; | |
| import 'package:kernel/kernel.dart' hide LibraryDependency, Combinator; | |
| import 'package:kernel/target/targets.dart' hide DiagnosticReporter; | |
| import 'package:kernel/type_environment.dart' as ir; | |
| import 'package:kernel/verifier.dart'; | |
| import '../../compiler_api.dart' as api; | |
| import '../common.dart'; | |
| import '../diagnostics/diagnostic_listener.dart'; | |
| import '../environment.dart'; | |
| import '../ir/annotations.dart'; | |
| import '../ir/constants.dart'; | |
| import '../kernel/dart2js_target.dart' | |
| show | |
| Dart2jsConstantsBackend, | |
| Dart2jsDartLibrarySupport, | |
| Dart2jsTarget, | |
| implicitlyUsedLibraries; | |
| import '../kernel/front_end_adapter.dart'; | |
| import '../kernel/transformations/global/transform.dart' as global_transforms; | |
| import '../options.dart'; | |
| class Input { | |
| final CompilerOptions options; | |
| final api.CompilerInput compilerInput; | |
| final DiagnosticReporter reporter; | |
| /// Shared state between compilations. Only used when loading from source. | |
| final fe.InitializedCompilerState? initializedCompilerState; | |
| // TODO(johnniwinther): Remove this when #34942 is fixed. | |
| /// Force in-memory serialization/deserialization of the loaded component. | |
| /// | |
| /// This is used for testing. | |
| final bool forceSerialization; | |
| Input( | |
| this.options, | |
| this.compilerInput, | |
| this.reporter, | |
| this.initializedCompilerState, | |
| this.forceSerialization, | |
| ); | |
| } | |
| /// Result of invoking the CFE to produce the kernel IR. | |
| class Output { | |
| final ir.Component component; | |
| /// The [Uri] of the root library containing main. | |
| /// Note: rootLibraryUri will be null for some modules, for example in the | |
| /// case of dependent libraries processed modularly. | |
| final Uri? rootLibraryUri; | |
| /// Returns the [Uri]s of all libraries that have been loaded that are | |
| /// reachable from the [rootLibraryUri]. | |
| /// | |
| /// Note that [component] may contain some libraries that are excluded here. | |
| final List<Uri>? libraries; | |
| final fe.InitializedCompilerState? initializedCompilerState; | |
| Output withNewComponent(ir.Component component) => | |
| Output(component, rootLibraryUri, libraries, initializedCompilerState); | |
| Output( | |
| this.component, | |
| this.rootLibraryUri, | |
| this.libraries, | |
| this.initializedCompilerState, | |
| ); | |
| } | |
| Library _findEntryLibrary(Component component, Uri entryUri) { | |
| final entryLibrary = component.libraries.firstWhereOrNull( | |
| (l) => l.fileUri == entryUri, | |
| ); | |
| if (entryLibrary == null) { | |
| throw ArgumentError('Entry uri $entryUri not found in dill.'); | |
| } | |
| return entryLibrary; | |
| } | |
| ir.Reference _findMainMethod(Library entryLibrary) { | |
| var mainMethod = entryLibrary.procedures.firstWhereOrNull( | |
| (p) => p.name.text == 'main', | |
| ); | |
| // In some cases, a main method is defined in another file, and then | |
| // exported. In these cases, we search for the main method in | |
| // [additionalExports]. | |
| ir.Reference? mainMethodReference; | |
| if (mainMethod == null) { | |
| mainMethodReference = entryLibrary.additionalExports.firstWhereOrNull( | |
| (p) => p.canonicalName?.name == 'main', | |
| ); | |
| } else { | |
| mainMethodReference = mainMethod.reference; | |
| } | |
| if (mainMethodReference == null) { | |
| throw ArgumentError( | |
| 'Entry uri ${entryLibrary.fileUri} has no main method.', | |
| ); | |
| } | |
| return mainMethodReference; | |
| } | |
| String _getPlatformFilename(String targetName) => "${targetName}_platform.dill"; | |
| class _LoadFromKernelResult { | |
| final ir.Component? component; | |
| final Library? entryLibrary; | |
| _LoadFromKernelResult(this.component, this.entryLibrary); | |
| } | |
| void _simplifyConstConditionals( | |
| ir.Component component, | |
| CompilerOptions options, | |
| ir.ClassHierarchy classHierarchy, | |
| DiagnosticReporter reporter, | |
| ) { | |
| void reportMessage( | |
| fe.LocatedMessage message, | |
| List<fe.LocatedMessage>? context, | |
| ) { | |
| reportLocatedMessage(reporter, message, context); | |
| } | |
| bool shouldNotInline(ir.TreeNode node) { | |
| if (node is! ir.Annotatable) { | |
| return false; | |
| } | |
| return computePragmaAnnotationDataFromIr(node).any( | |
| (PragmaAnnotationData pragma) => | |
| pragma == const PragmaAnnotationData('noInline') || | |
| pragma == const PragmaAnnotationData('never-inline'), | |
| ); | |
| } | |
| fe.ConstConditionalSimplifier( | |
| Dart2jsDartLibrarySupport(deprecatedJsInterop: options.deprecatedJsInterop), | |
| const Dart2jsConstantsBackend(supportsUnevaluatedConstants: false), | |
| component, | |
| reportMessage, | |
| environmentDefines: options.environment, | |
| classHierarchy: classHierarchy, | |
| shouldNotInline: shouldNotInline, | |
| removeAsserts: !options.enableUserAssertions, | |
| ).run(); | |
| } | |
| // Perform any backend-specific transforms here that can be done on both | |
| // serialized components and components from source. | |
| void _doTransformsOnKernelLoad( | |
| Component component, | |
| CompilerOptions options, | |
| DiagnosticReporter reporter, | |
| ) { | |
| if (options.stage.shouldRunGlobalTransforms) { | |
| ir.CoreTypes coreTypes = ir.CoreTypes(component); | |
| // Ignore ambiguous supertypes. | |
| final classHierarchy = ir.ClassHierarchy( | |
| component, | |
| coreTypes, | |
| onAmbiguousSupertypes: (_, _, _) {}, | |
| ); | |
| ir.TypeEnvironment typeEnvironment = ir.TypeEnvironment( | |
| coreTypes, | |
| classHierarchy, | |
| ); | |
| final constantsEvaluator = Dart2jsConstantEvaluator( | |
| component, | |
| typeEnvironment, | |
| (fe.LocatedMessage message, List<fe.LocatedMessage>? context) => | |
| reportLocatedMessage(reporter, message, context), | |
| environment: Environment(options.environment), | |
| deprecatedJsInterop: options.deprecatedJsInterop, | |
| ); | |
| StaticInteropClassEraser(coreTypes).visitComponent(component); | |
| global_transforms.transformLibraries( | |
| component.libraries, | |
| constantsEvaluator, | |
| coreTypes, | |
| options, | |
| ); | |
| _simplifyConstConditionals(component, options, classHierarchy, reporter); | |
| } | |
| } | |
| /// Reports how each deprecated JS interop import is reachable from | |
| /// [entryLibrary]. | |
| /// | |
| /// When compiling from source, the CFE already reports an error at each such | |
| /// import when deprecated JS interop is disabled. This adds a single message | |
| /// after those errors showing the shortest import paths from the entrypoint to | |
| /// the deprecated JS interop libraries as a tree. Libraries in packages other | |
| /// than the entrypoint's own package are grouped by package, and repeated | |
| /// subtrees are only shown once. | |
| /// | |
| /// The message is an info if an error was already reported. Otherwise, such | |
| /// as when the imports are in libraries loaded from a `.dill` file, it is an | |
| /// error that also explains how to fix the imports, so that compilation fails | |
| /// instead of silently producing no output. | |
| /// | |
| /// Does nothing unless deprecated JS interop is disabled. The import graph is | |
| /// only built if some library in [component] imports a deprecated JS interop | |
| /// library. Returns `true` if any deprecated JS interop imports reachable from | |
| /// [entryLibrary] were found. | |
| bool _reportDeprecatedJsInteropImports( | |
| CompilerOptions options, | |
| Component component, | |
| Library? entryLibrary, | |
| DiagnosticReporter reporter, | |
| ) { | |
| if (options.deprecatedJsInterop || entryLibrary == null) return false; | |
| if (!hasDeprecatedJsInteropImports(component.libraries)) return false; | |
| final importPaths = findDeprecatedJsInteropImportPaths(entryLibrary); | |
| if (importPaths.isEmpty) return false; | |
| if (reporter.hasReportedError) { | |
| reporter.reportInfoMessage(noLocationSpannable, MessageKind.generic, { | |
| 'text': _importPathsText(importPaths), | |
| }); | |
| } else { | |
| reporter.reportErrorMessage(noLocationSpannable, MessageKind.generic, { | |
| 'text': _unreportedImportsErrorText(importPaths), | |
| }); | |
| } | |
| return true; | |
| } | |
| /// Explains how to fix imports of deprecated JS interop libraries, as in the | |
| /// error that the CFE reports at each such import. | |
| const String _deprecatedJsInteropMigrationHint = | |
| "Migrate to 'package:web' and 'dart:js_interop' (see " | |
| 'https://dart.dev/interop/js-interop/past-js-interop), or temporarily ' | |
| "enable the 'deprecated-js-interop' option."; | |
| /// Returns the text of the error listing [importPaths] when the CFE hasn't | |
| /// reported the imports, which also explains why they are an error and how to | |
| /// fix them. | |
| String _unreportedImportsErrorText( | |
| DeprecatedJsInteropImportPaths importPaths, | |
| ) => [ | |
| 'Imports of deprecated JS interop libraries are not allowed.', | |
| _importPathsText(importPaths), | |
| _deprecatedJsInteropMigrationHint, | |
| ].join('\n'); | |
| /// Returns the text of the info message listing [importPaths] from the main | |
| /// library. | |
| String _importPathsText(DeprecatedJsInteropImportPaths importPaths) { | |
| final tree = formatImportTree(importPaths, rootLabel: 'main library'); | |
| return [ | |
| 'Deprecated JS interop libraries are imported through:', | |
| for (final line in tree.split('\n')) ' $line', | |
| ].join('\n'); | |
| } | |
| /// Returns the library compiled from source for [entryUri], falling back to | |
| /// the library that declares `main`. | |
| Library? _findSourceEntryLibrary(Component component, Uri entryUri) => | |
| component.libraries.firstWhereOrNull( | |
| (library) => library.importUri == entryUri || library.fileUri == entryUri, | |
| ) ?? | |
| component.mainMethod?.enclosingLibrary; | |
| /// Returns [state] if it can be reused for a compilation with [options], or | |
| /// `null` otherwise. | |
| /// | |
| /// The CFE reuses compiler state, including its target, without comparing | |
| /// targets. State created for a target with a different | |
| /// [CompilerOptions.deprecatedJsInterop] value would resolve `dart.library.*` | |
| /// conditions incorrectly, so it must be discarded. | |
| fe.InitializedCompilerState? _reusableCompilerState( | |
| fe.InitializedCompilerState? state, | |
| CompilerOptions options, | |
| ) { | |
| final oldTarget = state?.options.target; | |
| if (oldTarget is Dart2jsTarget && | |
| oldTarget.deprecatedJsInterop != options.deprecatedJsInterop) { | |
| return null; | |
| } | |
| return state; | |
| } | |
| Future<_LoadFromKernelResult> _loadFromKernel( | |
| CompilerOptions options, | |
| api.CompilerInput compilerInput, | |
| String targetName, | |
| DiagnosticReporter reporter, | |
| ) async { | |
| Library? entryLibrary; | |
| var resolvedUri = options.compilationTarget; | |
| ir.Component component = ir.Component(); | |
| Future<void> read(Uri uri) async { | |
| api.Input<Uint8List> input = await compilerInput.readFromUri( | |
| uri, | |
| inputKind: api.InputKind.binary, | |
| ); | |
| BinaryBuilder(input.data).readComponent(component); | |
| } | |
| await read(resolvedUri); | |
| if (options.platformBinaries != null && | |
| options.stage.shouldReadPlatformBinaries) { | |
| var platformUri = options.platformBinaries?.resolve( | |
| _getPlatformFilename(targetName), | |
| ); | |
| // TODO(joshualitt): Change how we detect this case so it is less | |
| // brittle. | |
| if (platformUri != resolvedUri) await read(platformUri!); | |
| } | |
| // Concatenate dills. | |
| if (options.dillDependencies != null) { | |
| for (Uri dependency in options.dillDependencies!) { | |
| await read(dependency); | |
| } | |
| } | |
| if (options.entryUri != null) { | |
| entryLibrary = _findEntryLibrary(component, options.entryUri!); | |
| var mainMethod = _findMainMethod(entryLibrary); | |
| component.setMainMethodAndMode(mainMethod, true); | |
| } | |
| if (_reportDeprecatedJsInteropImports( | |
| options, | |
| component, | |
| entryLibrary ?? component.mainMethod?.enclosingLibrary, | |
| reporter, | |
| )) { | |
| return _LoadFromKernelResult(null, entryLibrary); | |
| } | |
| _doTransformsOnKernelLoad(component, options, reporter); | |
| registerSources(component, compilerInput); | |
| return _LoadFromKernelResult(component, entryLibrary); | |
| } | |
| class _LoadFromSourceResult { | |
| final ir.Component? component; | |
| final fe.InitializedCompilerState initializedCompilerState; | |
| _LoadFromSourceResult(this.component, this.initializedCompilerState); | |
| } | |
| Future<_LoadFromSourceResult> _loadFromSource( | |
| CompilerOptions options, | |
| api.CompilerInput compilerInput, | |
| DiagnosticReporter reporter, | |
| fe.InitializedCompilerState? initializedCompilerState, | |
| String targetName, | |
| ) async { | |
| bool verbose = false; | |
| bool cfeConstants = options.features.cfeConstants.isEnabled; | |
| Map<String, String>? environment = cfeConstants ? options.environment : null; | |
| Target target = Dart2jsTarget( | |
| targetName, | |
| TargetFlags(), | |
| options: options, | |
| supportsUnevaluatedConstants: !cfeConstants, | |
| ); | |
| fe.FileSystem fileSystem = CompilerFileSystem(compilerInput); | |
| fe.Verbosity verbosity = options.verbosity; | |
| void onDiagnostic(fe.CfeDiagnosticMessage message) { | |
| if (fe.Verbosity.shouldPrint(verbosity, message)) { | |
| reportFrontEndMessage(reporter, message); | |
| } | |
| } | |
| List<Uri> sources = [options.compilationTarget]; | |
| List<Uri> dependencies = []; | |
| if (options.platformBinaries != null) { | |
| dependencies.add( | |
| options.platformBinaries!.resolve(_getPlatformFilename(targetName)), | |
| ); | |
| } | |
| if (options.dillDependencies != null) { | |
| dependencies.addAll(options.dillDependencies!); | |
| } | |
| initializedCompilerState = fe.initializeCompiler( | |
| _reusableCompilerState(initializedCompilerState, options), | |
| target, | |
| options.librariesSpecificationUri, | |
| dependencies, | |
| options.packageConfig, | |
| explicitExperimentalFlags: options.explicitExperimentalFlags, | |
| environmentDefines: environment, | |
| invocationModes: options.cfeInvocationModes, | |
| verbosity: verbosity, | |
| ); | |
| ir.Component? component = await fe.compile( | |
| initializedCompilerState, | |
| verbose, | |
| fileSystem, | |
| onDiagnostic, | |
| sources, | |
| ); | |
| if (component != null) { | |
| assert(() { | |
| verifyComponent( | |
| target, | |
| VerificationStage.afterModularTransformations, | |
| component, | |
| ); | |
| return true; | |
| }()); | |
| if (_reportDeprecatedJsInteropImports( | |
| options, | |
| component, | |
| _findSourceEntryLibrary(component, options.compilationTarget), | |
| reporter, | |
| )) { | |
| return _LoadFromSourceResult(null, initializedCompilerState); | |
| } | |
| _doTransformsOnKernelLoad(component, options, reporter); | |
| registerSources(component, compilerInput); | |
| } | |
| return _LoadFromSourceResult(component, initializedCompilerState); | |
| } | |
| Output _createOutput( | |
| CompilerOptions options, | |
| DiagnosticReporter reporter, | |
| Library? entryLibrary, | |
| ir.Component component, | |
| fe.InitializedCompilerState? initializedCompilerState, | |
| ) { | |
| Uri? rootLibraryUri; | |
| Iterable<ir.Library> libraries = component.libraries; | |
| if (component.mainMethod == null) { | |
| // TODO(sigmund): move this so that we use the same error template | |
| // from the CFE. | |
| reporter.reportError( | |
| reporter.createMessage(noLocationSpannable, MessageKind.generic, { | |
| 'text': "No 'main' method found.", | |
| }), | |
| ); | |
| } | |
| // If we are building from dill and are passed an [entryUri], then we use | |
| // that to find the appropriate [entryLibrary]. Otherwise, we fallback to | |
| // the [enclosingLibrary] of the [mainMethod]. | |
| // NOTE: Under some circumstances, the [entryLibrary] exports the | |
| // [mainMethod] from another library, and thus the [enclosingLibrary] of | |
| // the [mainMethod] may not be the same as the [entryLibrary]. | |
| var root = entryLibrary ?? component.mainMethod!.enclosingLibrary; | |
| rootLibraryUri = root.importUri; | |
| // Filter unreachable libraries: [Component] was built by linking in the | |
| // entire SDK libraries, not all of them are used. We include anything | |
| // that is reachable from `main`. Note that all internal libraries that | |
| // the compiler relies on are reachable from `dart:core`. | |
| var seen = <Library>{}; | |
| void search(ir.Library current) { | |
| if (!seen.add(current)) return; | |
| for (ir.LibraryDependency dep in current.dependencies) { | |
| search(dep.targetLibrary); | |
| } | |
| } | |
| search(root); | |
| // Libraries dependencies do not show implicit imports to certain internal | |
| // libraries. | |
| const Set<String> alwaysInclude = { | |
| 'dart:_internal', | |
| 'dart:core', | |
| 'dart:async', | |
| ...implicitlyUsedLibraries, | |
| }; | |
| for (String uri in alwaysInclude) { | |
| Library library = component.libraries.firstWhere((lib) { | |
| return '${lib.importUri}' == uri; | |
| }); | |
| search(library); | |
| } | |
| libraries = libraries.where(seen.contains); | |
| return Output( | |
| component, | |
| rootLibraryUri, | |
| libraries.map((lib) => lib.importUri).toList(), | |
| initializedCompilerState, | |
| ); | |
| } | |
| /// Loads an entire Kernel [Component] from a file on disk. | |
| Future<Output?> run(Input input) async { | |
| CompilerOptions options = input.options; | |
| api.CompilerInput compilerInput = input.compilerInput; | |
| DiagnosticReporter reporter = input.reporter; | |
| String targetName = options.compileForServer ? "dart2js_server" : "dart2js"; | |
| Library? entryLibrary; | |
| ir.Component? component; | |
| fe.InitializedCompilerState? initializedCompilerState = | |
| input.initializedCompilerState; | |
| if (options.shouldLoadFromDill) { | |
| _LoadFromKernelResult result = await _loadFromKernel( | |
| options, | |
| compilerInput, | |
| targetName, | |
| reporter, | |
| ); | |
| component = result.component; | |
| entryLibrary = result.entryLibrary; | |
| } else { | |
| _LoadFromSourceResult result = await _loadFromSource( | |
| options, | |
| compilerInput, | |
| reporter, | |
| input.initializedCompilerState, | |
| targetName, | |
| ); | |
| component = result.component; | |
| initializedCompilerState = result.initializedCompilerState; | |
| } | |
| if (component == null) return null; | |
| if (input.forceSerialization) { | |
| // TODO(johnniwinther): Remove this when #34942 is fixed. | |
| Uint8List data = fe.serializeComponent(component); | |
| component = ir.Component(); | |
| BinaryBuilder(data).readComponent(component); | |
| // Ensure we use the new deserialized entry point library. | |
| entryLibrary = _findEntryLibrary(component, options.entryUri!); | |
| } | |
| return _createOutput( | |
| options, | |
| reporter, | |
| entryLibrary, | |
| component, | |
| initializedCompilerState, | |
| ); | |
| } | |
| /// Registers with the dart2js compiler all sources embedded in a kernel | |
| /// component. This may include sources that were read from disk directly as | |
| /// files, but also sources that were embedded in binary `.dill` files (like the | |
| /// platform kernel file). | |
| /// | |
| /// This registration improves how locations are presented when errors | |
| /// or crashes are reported by the dart2js compiler. | |
| void registerSources(ir.Component component, api.CompilerInput compilerInput) { | |
| component.uriToSource.forEach((uri, source) { | |
| compilerInput.registerUtf8ContentsForDiagnostics(uri, source.source); | |
| }); | |
| } |