| // Copyright (c) 2017, 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:collection'; | |
| import 'package:_fe_analyzer_shared/src/messages/codes.dart' | |
| show Message, LocatedMessage; | |
| import 'package:_js_interop_checks/js_interop_checks.dart'; | |
| import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart'; | |
| import 'package:_js_interop_checks/src/transformations/shared_interop_transformer.dart'; | |
| import 'package:front_end/src/api_prototype/deprecated_js_interop_libraries.dart' | |
| show deprecatedJsInteropLibraryNames; | |
| import 'package:kernel/class_hierarchy.dart'; | |
| import 'package:kernel/core_types.dart'; | |
| import 'package:kernel/kernel.dart' hide Pattern; | |
| import 'package:kernel/reference_from_index.dart'; | |
| import 'package:kernel/target/changed_structure_notifier.dart'; | |
| import 'package:kernel/target/targets.dart'; | |
| import 'package:kernel/transformations/track_widget_constructor_locations.dart'; | |
| import 'package:kernel/type_environment.dart'; | |
| import 'constants.dart' show DevCompilerConstantsBackend; | |
| import 'kernel_helpers.dart'; | |
| /// A kernel [Target] to configure the Dart Front End for dartdevc. | |
| class DevCompilerTarget extends Target { | |
| DevCompilerTarget(this.flags, {this.deprecatedJsInterop = true}); | |
| @override | |
| final TargetFlags flags; | |
| /// Whether the deprecated JS interop libraries may be used. | |
| /// | |
| /// When `false`, `dart.library.<name>` is considered `false` for each of the | |
| /// [deprecatedJsInteropLibraryNames] and importing any of them is an error. | |
| final bool deprecatedJsInterop; | |
| WidgetCreatorTracker? _widgetTracker; | |
| Map<String, Class>? _nativeClasses; | |
| DiagnosticReporter<Message, LocatedMessage>? _diagnosticReporter; | |
| @override | |
| int get enabledLateLowerings => LateLowering.all; | |
| @override | |
| bool get supportsLateLoweringSentinel => false; | |
| @override | |
| bool get useStaticFieldLowering => false; | |
| // TODO(johnniwinther,sigmund): Remove this when js-interop handles getter | |
| // calls encoded with an explicit property get or disallows getter calls. | |
| @override | |
| bool get supportsExplicitGetterCalls => false; | |
| @override | |
| int get enabledConstructorTearOffLowerings => ConstructorTearOffLowering.all; | |
| @override | |
| String get name => 'dartdevc'; | |
| @override | |
| List<String> get extraRequiredLibraries => [ | |
| 'dart:_ddc_only', | |
| 'dart:_runtime', | |
| 'dart:_async_status_codes', | |
| 'dart:_js_shared_embedded_names', | |
| 'dart:_recipe_syntax', | |
| 'dart:_rti', | |
| 'dart:_debugger', | |
| 'dart:_foreign_helper', | |
| 'dart:_interceptors', | |
| 'dart:_internal', | |
| 'dart:_isolate_helper', | |
| 'dart:_js_annotations', | |
| 'dart:_js_helper', | |
| 'dart:_js_names', | |
| 'dart:_js_primitives', | |
| 'dart:_js_types', | |
| 'dart:_metadata', | |
| 'dart:_native_typed_data', | |
| 'dart:async', | |
| 'dart:collection', | |
| 'dart:convert', | |
| 'dart:developer', | |
| if (flags.includeUnsupportedPlatformLibraryStubs) 'dart:ffi', | |
| 'dart:io', | |
| 'dart:isolate', | |
| 'dart:js', | |
| 'dart:js_interop', | |
| 'dart:js_interop_unsafe', | |
| 'dart:js_util', | |
| 'dart:math', | |
| 'dart:typed_data', | |
| 'dart:indexed_db', | |
| 'dart:html', | |
| 'dart:html_common', | |
| 'dart:svg', | |
| 'dart:web_audio', | |
| 'dart:web_gl', | |
| ]; | |
| @override | |
| List<String> get extraRequiredLibrariesPlatform => const ['dart:ffi']; | |
| // The libraries required to be indexed via CoreTypes. | |
| @override | |
| List<String> get extraIndexedLibraries => const [ | |
| 'dart:async', | |
| 'dart:collection', | |
| 'dart:html', | |
| 'dart:indexed_db', | |
| 'dart:js', | |
| 'dart:js_util', | |
| 'dart:js_interop', | |
| 'dart:js_interop_unsafe', | |
| 'dart:math', | |
| 'dart:svg', | |
| 'dart:typed_data', | |
| 'dart:web_audio', | |
| 'dart:web_gl', | |
| 'dart:_foreign_helper', | |
| 'dart:_interceptors', | |
| 'dart:_js_helper', | |
| 'dart:_js_types', | |
| 'dart:_native_typed_data', | |
| 'dart:_runtime', | |
| 'dart:_rti', | |
| ]; | |
| @override | |
| bool mayDefineRestrictedType(Uri uri) => | |
| uri.isScheme('dart') && | |
| (uri.path == 'core' || | |
| uri.path == 'typed_data' || | |
| uri.path == '_interceptors' || | |
| uri.path == '_js_helper' || | |
| uri.path == '_native_typed_data' || | |
| uri.path == '_runtime'); | |
| /// Returns [true] if [uri] represents a test script has been whitelisted to | |
| /// import private platform libraries. | |
| /// | |
| /// Unit tests for the dart:_runtime library have imports like this. It is | |
| /// only allowed from a specific SDK test directory or through the modular | |
| /// test framework. | |
| bool _allowedTestLibrary(Uri uri) { | |
| // Multi-root scheme used by modular test framework. | |
| if (uri.isScheme('dev-dart-app')) return true; | |
| // Test package used by expression evaluation tests. | |
| if (uri.isScheme('package') && uri.path == 'eval_test/test.dart') { | |
| return true; | |
| } | |
| return allowedNativeTest(uri); | |
| } | |
| bool _allowedDartLibrary(Uri uri) => uri.isScheme('dart'); | |
| @override | |
| bool enableNative(Uri uri) => | |
| _allowedTestLibrary(uri) || _allowedDartLibrary(uri); | |
| @override | |
| bool allowPlatformPrivateLibraryAccess(Uri importer, Uri imported) => | |
| super.allowPlatformPrivateLibraryAccess(importer, imported) || | |
| _allowedTestLibrary(importer) || | |
| (importer.isScheme('package') && | |
| (importer.path.startsWith('dart2js_runtime_metrics/') || | |
| importer.path == 'js/js.dart')); | |
| @override | |
| bool get errorOnUnexactWebIntLiterals => true; | |
| @override | |
| bool get enableNoSuchMethodForwarders => true; | |
| @override | |
| void performModularTransformationsOnLibraries( | |
| Component component, | |
| CoreTypes coreTypes, | |
| ClassHierarchy hierarchy, | |
| List<Library> libraries, | |
| Map<String, String>? environmentDefines, | |
| DiagnosticReporter diagnosticReporter, | |
| ReferenceFromIndex? referenceFromIndex, { | |
| void Function(String msg)? logger, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| }) { | |
| _nativeClasses ??= JsInteropChecks.getNativeClasses(component); | |
| _diagnosticReporter = | |
| diagnosticReporter as DiagnosticReporter<Message, LocatedMessage>; | |
| _performTransformations(coreTypes, hierarchy, libraries); | |
| } | |
| @override | |
| void performTransformationsOnProcedure( | |
| CoreTypes coreTypes, | |
| ClassHierarchy hierarchy, | |
| Procedure procedure, | |
| Map<String, String>? environmentDefines, { | |
| void Function(String)? logger, | |
| required DiagnosticReporter diagnosticReporter, | |
| }) { | |
| _diagnosticReporter = | |
| diagnosticReporter as DiagnosticReporter<Message, LocatedMessage>; | |
| _performTransformations(coreTypes, hierarchy, [procedure]); | |
| } | |
| void _performTransformations( | |
| CoreTypes coreTypes, | |
| ClassHierarchy hierarchy, | |
| List<TreeNode> nodes, | |
| ) { | |
| final jsInteropReporter = JsInteropDiagnosticReporter(_diagnosticReporter!); | |
| final jsInteropChecks = JsInteropChecks( | |
| coreTypes, | |
| hierarchy, | |
| jsInteropReporter, | |
| _nativeClasses!, | |
| ); | |
| for (var node in nodes) { | |
| // Process and validate first before doing anything with exports. | |
| node.accept(jsInteropChecks); | |
| } | |
| final sharedInteropTransformer = SharedInteropTransformer( | |
| TypeEnvironment(coreTypes, hierarchy), | |
| jsInteropReporter, | |
| jsInteropChecks.exportChecker, | |
| jsInteropChecks.extensionIndex, | |
| ); | |
| final jsUtilOptimizer = JsUtilOptimizer( | |
| coreTypes, | |
| hierarchy, | |
| jsInteropChecks.extensionIndex, | |
| isDart2JS: false, | |
| ); | |
| for (var node in nodes) { | |
| _CovarianceTransformer(node).transform(); | |
| // Shared interop transformer has static checks, so we still visit. | |
| node.accept(sharedInteropTransformer); | |
| if (!jsInteropReporter.hasJsInteropErrors) { | |
| // We can't guarantee calls are well-formed, so don't transform. | |
| node.accept(jsUtilOptimizer); | |
| } | |
| } | |
| } | |
| @override | |
| void performOutlineTransformations( | |
| Component component, { | |
| List<Library>? libraries, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| }) { | |
| super.performOutlineTransformations( | |
| component, | |
| libraries: libraries, | |
| changedStructureNotifier: changedStructureNotifier, | |
| ); | |
| // In addition to [performPreConstantEvaluationTransformations] this makes | |
| // sure summaries are also transformed. | |
| if (flags.trackCreationLocations) { | |
| (_widgetTracker ??= WidgetCreatorTracker()).transform( | |
| libraries ?? component.libraries, | |
| component.libraries, | |
| changedStructureNotifier, | |
| ); | |
| } | |
| } | |
| @override | |
| void performPreConstantEvaluationTransformations( | |
| Component component, | |
| CoreTypes coreTypes, | |
| List<Library> libraries, | |
| DiagnosticReporter diagnosticReporter, { | |
| void Function(String msg)? logger, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| }) { | |
| if (flags.trackCreationLocations) { | |
| (_widgetTracker ??= WidgetCreatorTracker()).transform( | |
| libraries, | |
| component.libraries, | |
| changedStructureNotifier, | |
| ); | |
| } | |
| } | |
| @override | |
| Expression instantiateInvocation( | |
| CoreTypes coreTypes, | |
| Expression receiver, | |
| String name, | |
| Arguments arguments, | |
| int offset, | |
| bool isSuper, | |
| ) { | |
| // TODO(jmesserly): preserve source information? | |
| // (These method are synthetic. Also unclear if the offset will correspond | |
| // to the file where the class resides, or the file where the method we're | |
| // mocking resides). | |
| Expression createInvocation(String name, ExpressionList positional) { | |
| // TODO(jmesserly): this uses the implementation _Invocation class, | |
| // because the CFE does not resolve the redirecting factory constructors | |
| // like it would for user code. Our code generator expects all redirecting | |
| // factories to be resolved to the real constructor. | |
| var ctor = coreTypes.index | |
| .getClass('dart:core', '_Invocation') | |
| .constructors | |
| .firstWhere((c) => c.name.text == name); | |
| return ConstructorInvocation(ctor, Arguments(positional)); | |
| } | |
| if (name.startsWith('get:')) { | |
| return createInvocation( | |
| 'getter', | |
| ExpressionList(SymbolLiteral(name.substring(4))), | |
| ); | |
| } | |
| if (name.startsWith('set:')) { | |
| return createInvocation( | |
| 'setter', | |
| ExpressionList( | |
| SymbolLiteral(name.substring(4)), | |
| arguments.positional.single, | |
| ), | |
| ); | |
| } | |
| var ctorArgs = ExpressionList( | |
| SymbolLiteral(name), | |
| arguments.types.isNotEmpty | |
| ? ListLiteral(ExpressionList.mapped(arguments.types, TypeLiteral.new)) | |
| : NullLiteral(), | |
| ListLiteral(arguments.positional), | |
| arguments.named.isNotEmpty | |
| ? MapLiteral([ | |
| for (var n in arguments.named) | |
| MapLiteralEntry(SymbolLiteral(n.name), n.value), | |
| ], keyType: coreTypes.symbolNonNullableRawType) | |
| : NullLiteral(), | |
| ); | |
| return createInvocation('method', ctorArgs); | |
| } | |
| @override | |
| ConstantsBackend get constantsBackend => const DevCompilerConstantsBackend(); | |
| @override | |
| DartLibrarySupport get dartLibrarySupport => | |
| DevCompilerDartLibrarySupport(deprecatedJsInterop: deprecatedJsInterop); | |
| // For correctness the DDC runtime needs to reevaluate libraries that contain | |
| // mixin applications when the mixin was edited. If the edit was only within | |
| // the body of a mixin member the experimental invalidation logic would only | |
| // invalidate that library. This enables a search for applications of the | |
| // mixin in other libraries adds them to the invalidated set. | |
| @override | |
| bool get incrementalCompilerIncludeMixinApplicationInvalidatedLibraries => | |
| true; | |
| } | |
| class DevCompilerDartLibrarySupport extends CustomizedDartLibrarySupport { | |
| // This is required so that `dart.library._ddc_only` can be used as an import | |
| // condition. Libraries with leading underscores are otherwise considered | |
| // unsupported regardless of the library specification. | |
| // | |
| // When [deprecatedJsInterop] is `false`, the deprecated JS interop libraries | |
| // are considered unsupported so that conditions on them evaluate to `false`. | |
| const DevCompilerDartLibrarySupport({bool deprecatedJsInterop = true}) | |
| : super( | |
| supported: const {'_ddc_only'}, | |
| unsupported: deprecatedJsInterop | |
| ? const {} | |
| : deprecatedJsInteropLibraryNames, | |
| ); | |
| } | |
| /// Analyzes a component to determine if any covariance checks in private | |
| /// members can be eliminated, and adjusts the flags to remove those checks. | |
| /// | |
| /// See [_CovarianceTransformer.transform]. | |
| class _CovarianceTransformer extends RecursiveVisitor { | |
| /// The set of private instance members in [_library] that (potentially) need | |
| /// covariance checks. | |
| /// | |
| /// Members need checks if they are accessed through a receiver whose type is | |
| /// not exactly known (i.e. the actual receiver could be a subtype of its | |
| /// static type). If the receiver expression is `this`, `super` or non-factory | |
| /// instance creation, it is known to have an exact type. | |
| final _checkedMembers = HashSet<Member>(); | |
| /// List of private instance procedures. | |
| /// | |
| /// [transform] uses this list to eliminate covariance flags for members that | |
| /// aren't in [_checkedMembers]. | |
| final _privateProcedures = <Procedure>[]; | |
| /// List of private instance fields. | |
| /// | |
| /// [transform] uses this list to eliminate covariance flags for members that | |
| /// aren't in [_checkedMembers]. | |
| final _privateFields = <Field>[]; | |
| late final Library _library; | |
| /// Create covariance transformer from a node. | |
| /// | |
| /// The [_node] is expected to be a [Library] in initial compilation | |
| /// and a [Procedure] in the interactive expression compilation. | |
| _CovarianceTransformer(TreeNode node) { | |
| assert( | |
| node is Library || node is Procedure, | |
| 'Unexpected node in _CovarianceTransformer', | |
| ); | |
| if (node is Library) _library = node; | |
| if (node is Procedure) _library = node.enclosingLibrary; | |
| } | |
| /// Transforms [_library], eliminating unnecessary checks for private members. | |
| /// | |
| /// Kernel will mark covariance checks on members, for example: | |
| /// - a field with [Field.isCovariantByClass] or | |
| /// [Field.isCovariantByDeclaration]. | |
| /// - a method/setter with parameter(s) or type parameter(s) that have | |
| /// `isCovariantByClass` or `isCovariantByDeclaration` set. | |
| /// | |
| /// If the check can be safely eliminated, those properties will be set to | |
| /// false so the JS compiler does not emit checks. | |
| /// | |
| /// Public members always need covariance checks (we cannot see all potential | |
| /// call sites), but in some cases we can eliminate these checks for private | |
| /// members. | |
| /// | |
| /// Private members only need covariance checks if they are accessed through a | |
| /// receiver whose type is not exactly known (i.e. the actual receiver could | |
| /// be a subtype of its static type). If the receiver expression is `this`, | |
| /// `super` or non-factory instance creation, it is known to have an exact | |
| /// type, so no callee check is necessary to ensure soundness (normal | |
| /// subtyping checks at the call site are sufficient). | |
| /// | |
| /// However to eliminate a check, we must know that all call sites are safe. | |
| /// So the first pass is to collect any potentially-unsafe call sites, this | |
| /// is done by [_checkTarget] and [_checkTearoff]. | |
| /// | |
| /// Method tearoffs must also be marked potentially-unsafe, regardless of | |
| /// whether the receiver type is known, because they could escape. Also their | |
| /// runtime type must store `Object` in for covariant parameters (this | |
| /// affects `is`, casts, and the `.runtimeType` property). | |
| /// | |
| /// Note 1: dynamic calls do not need to be considered here, because they | |
| /// will be checked based on runtime type information. | |
| /// | |
| /// Node 2: public members in private classes cannot be treated as private | |
| /// unless we know that the member is not exposed via some public interface | |
| /// (implemented by their class or a subclass) that needs a covariance check. | |
| /// That is somewhat complex to analyze, so for now we ignore it. | |
| void transform() { | |
| _library.visitChildren(this); | |
| // Update the tree based on the methods that need checks. | |
| for (var field in _privateFields) { | |
| if (!_checkedMembers.contains(field)) { | |
| field.isCovariantByDeclaration = false; | |
| field.isCovariantByClass = false; | |
| } | |
| } | |
| void clearCovariant(FunctionParameter parameter) { | |
| parameter.isCovariantByDeclaration = false; | |
| parameter.isCovariantByClass = false; | |
| } | |
| for (var member in _privateProcedures) { | |
| if (!_checkedMembers.contains(member)) { | |
| var function = member.function; | |
| function.positionalParameters.forEach(clearCovariant); | |
| function.namedParameters.forEach(clearCovariant); | |
| for (var t in function.typeParameters) { | |
| t.isCovariantByClass = false; | |
| } | |
| } | |
| } | |
| } | |
| /// Checks if [target] is a private member called through a [receiver] that | |
| /// will potentially need a covariance check. | |
| /// | |
| /// If the member needs a check it will be stored in [_checkedMembers]. | |
| /// | |
| /// See [transform] for more information. | |
| void _checkTarget(Expression receiver, Member? target) { | |
| if (target != null && | |
| target.name.isPrivate && | |
| target.isInstanceMember && | |
| receiver is! ThisExpression && | |
| receiver is! ConstructorInvocation) { | |
| assert( | |
| target.enclosingLibrary == _library, | |
| 'call to private member must be in same library', | |
| ); | |
| _checkedMembers.add(target); | |
| } | |
| } | |
| /// Checks if [target] is a tearoff of a private member. | |
| /// | |
| /// In this case we will need a covariance check, because the method could | |
| /// escape, and it also has a different runtime type. | |
| /// | |
| /// See [transform] for more information. | |
| void _checkTearoff(Member? target) { | |
| if (target != null && | |
| target.name.isPrivate && | |
| target.isInstanceMember && | |
| target is Procedure && | |
| !target.isAccessor) { | |
| assert( | |
| target.enclosingLibrary == _library, | |
| 'tearoff of private member must be in same library', | |
| ); | |
| _checkedMembers.add(target); | |
| } | |
| } | |
| @override | |
| void visitProcedure(Procedure node) { | |
| if (node.name.isPrivate && | |
| // The member must be private to this library. Member signatures, | |
| // forwarding stubs and noSuchMethod forwarders for private members in | |
| // other libraries can be injected. | |
| node.name.library == _library && | |
| node.isInstanceMember && | |
| // No need to check abstract methods. | |
| node.function.body != null) { | |
| _privateProcedures.add(node); | |
| } | |
| super.visitProcedure(node); | |
| } | |
| @override | |
| void visitField(Field node) { | |
| if (node.name.isPrivate && | |
| // The member must be private to this library. Member signatures, | |
| // forwarding stubs and noSuchMethod forwarders for private members in | |
| // other libraries can be injected. | |
| node.name.library == _library && | |
| isCovariantField(node)) { | |
| _privateFields.add(node); | |
| } | |
| super.visitField(node); | |
| } | |
| @override | |
| void visitInstanceGet(InstanceGet node) { | |
| _checkTearoff(node.interfaceTarget); | |
| super.visitInstanceGet(node); | |
| } | |
| @override | |
| void visitInstanceSet(InstanceSet node) { | |
| _checkTarget(node.receiver, node.interfaceTarget); | |
| super.visitInstanceSet(node); | |
| } | |
| @override | |
| void visitInstanceInvocation(InstanceInvocation node) { | |
| _checkTarget(node.receiver, node.interfaceTarget); | |
| super.visitInstanceInvocation(node); | |
| } | |
| @override | |
| void visitInstanceGetterInvocation(InstanceGetterInvocation node) { | |
| _checkTarget(node.receiver, node.interfaceTarget); | |
| super.visitInstanceGetterInvocation(node); | |
| } | |
| @override | |
| void visitInstanceTearOff(InstanceTearOff node) { | |
| _checkTearoff(node.interfaceTarget); | |
| super.visitInstanceTearOff(node); | |
| } | |
| @override | |
| void visitEqualsCall(EqualsCall node) { | |
| _checkTarget(node.left, node.interfaceTarget); | |
| super.visitEqualsCall(node); | |
| } | |
| } | |
| List<Pattern> _allowedNativeTestPatterns = [ | |
| 'tests/dartdevc', | |
| 'tests/web/native', | |
| 'tests/web/internal', | |
| ]; | |
| bool allowedNativeTest(Uri uri) { | |
| var path = uri.path; | |
| return _allowedNativeTestPatterns.any((pattern) => path.contains(pattern)); | |
| } |