| // 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 'package:kernel/core_types.dart'; |
| import 'package:kernel/kernel.dart'; |
| import 'package:kernel/target/targets.dart'; |
| import 'package:_fe_analyzer_shared/src/messages/codes.dart' |
| show |
| Message, |
| LocatedMessage, |
| messageJsInteropAnonymousFactoryPositionalParameters, |
| messageJsInteropEnclosingClassJSAnnotation, |
| messageJsInteropEnclosingClassJSAnnotationContext, |
| messageJsInteropExternalExtensionMemberOnTypeInvalid, |
| messageJsInteropExternalMemberNotJSAnnotated, |
| messageJsInteropNamedParameters, |
| messageJsInteropNonExternalConstructor, |
| messageJsInteropNonExternalMember, |
| messageJsInteropOperatorsNotSupported, |
| templateJsInteropDartClassExtendsJSClass, |
| templateJsInteropStaticInteropWithInstanceMembers, |
| templateJsInteropStaticInteropWithNonStaticSupertype, |
| templateJsInteropJSClassExtendsDartClass, |
| templateJsInteropNativeClassInAnnotation; |
| |
| import 'src/js_interop.dart'; |
| |
| class JsInteropChecks extends RecursiveVisitor { |
| final CoreTypes _coreTypes; |
| final DiagnosticReporter<Message, LocatedMessage> _diagnosticsReporter; |
| final Map<String, Class> _nativeClasses; |
| bool _classHasJSAnnotation = false; |
| bool _classHasAnonymousAnnotation = false; |
| bool _classHasStaticInteropAnnotation = false; |
| bool _libraryHasJSAnnotation = false; |
| Map<Reference, Extension>? _libraryExtensionsIndex; |
| |
| /// Libraries that use `external` to exclude from checks on external. |
| static final Iterable<String> _pathsWithAllowedDartExternalUsage = <String>[ |
| '_foreign_helper', // for foreign helpers |
| '_interceptors', // for ddc JS string |
| '_native_typed_data', |
| '_runtime', // for ddc types at runtime |
| 'async', |
| 'core', // for environment constructors |
| 'html', |
| 'html_common', |
| 'indexed_db', |
| 'js', |
| 'js_util', |
| 'svg', |
| 'web_audio', |
| 'web_gl', |
| 'web_sql' |
| ]; |
| |
| /// Native tests to exclude from checks on external. |
| // TODO(rileyporter): Use ExternalName from CFE to exclude native tests. |
| List<Pattern> _allowedNativeTestPatterns = [ |
| RegExp(r'(?<!generated_)tests/web/native'), |
| RegExp(r'(?<!generated_)tests/web/internal'), |
| 'generated_tests/web/native/native_test', |
| RegExp(r'(?<!generated_)tests/web_2/native'), |
| RegExp(r'(?<!generated_)tests/web_2/internal'), |
| 'generated_tests/web_2/native/native_test', |
| ]; |
| |
| bool _libraryIsGlobalNamespace = false; |
| |
| JsInteropChecks( |
| this._coreTypes, this._diagnosticsReporter, this._nativeClasses); |
| |
| /// Extract all native class names from the [component]. |
| /// |
| /// Returns a map from the name to the underlying Class node. This is a |
| /// static method so that the result can be cached in the corresponding |
| /// compiler target. |
| static Map<String, Class> getNativeClasses(Component component) { |
| Map<String, Class> nativeClasses = {}; |
| for (var library in component.libraries) { |
| for (var cls in library.classes) { |
| var nativeNames = getNativeNames(cls); |
| for (var nativeName in nativeNames) { |
| nativeClasses[nativeName] = cls; |
| } |
| } |
| } |
| return nativeClasses; |
| } |
| |
| @override |
| void defaultMember(Member member) { |
| _checkInstanceMemberJSAnnotation(member); |
| if (!_isJSInteropMember(member)) _checkDisallowedExternal(member); |
| // TODO(43530): Disallow having JS interop annotations on non-external |
| // members (class members or otherwise). Currently, they're being ignored. |
| super.defaultMember(member); |
| } |
| |
| @override |
| void visitClass(Class cls) { |
| _classHasJSAnnotation = hasJSInteropAnnotation(cls); |
| _classHasAnonymousAnnotation = hasAnonymousAnnotation(cls); |
| _classHasStaticInteropAnnotation = hasStaticInteropAnnotation(cls); |
| var superclass = cls.superclass; |
| if (superclass != null && superclass != _coreTypes.objectClass) { |
| var superHasJSAnnotation = hasJSInteropAnnotation(superclass); |
| if (_classHasJSAnnotation && !superHasJSAnnotation) { |
| _diagnosticsReporter.report( |
| templateJsInteropJSClassExtendsDartClass.withArguments( |
| cls.name, superclass.name), |
| cls.fileOffset, |
| cls.name.length, |
| cls.fileUri); |
| } else if (!_classHasJSAnnotation && superHasJSAnnotation) { |
| _diagnosticsReporter.report( |
| templateJsInteropDartClassExtendsJSClass.withArguments( |
| cls.name, superclass.name), |
| cls.fileOffset, |
| cls.name.length, |
| cls.fileUri); |
| } else if (_classHasStaticInteropAnnotation) { |
| if (!hasStaticInteropAnnotation(superclass)) { |
| _diagnosticsReporter.report( |
| templateJsInteropStaticInteropWithNonStaticSupertype |
| .withArguments(cls.name, superclass.name), |
| cls.fileOffset, |
| cls.name.length, |
| cls.fileUri); |
| } |
| } |
| } |
| // Validate that superinterfaces are all annotated as static as well. Note |
| // that mixins are already disallowed and therefore are not checked here. |
| if (_classHasStaticInteropAnnotation) { |
| for (var supertype in cls.implementedTypes) { |
| if (!hasStaticInteropAnnotation(supertype.classNode)) { |
| _diagnosticsReporter.report( |
| templateJsInteropStaticInteropWithNonStaticSupertype |
| .withArguments(cls.name, supertype.classNode.name), |
| cls.fileOffset, |
| cls.name.length, |
| cls.fileUri); |
| } |
| } |
| } |
| // Since this is a breaking check, it is language-versioned. |
| if (cls.enclosingLibrary.languageVersion >= Version(2, 13) && |
| _classHasJSAnnotation && |
| !_classHasStaticInteropAnnotation && |
| !_classHasAnonymousAnnotation && |
| _libraryIsGlobalNamespace) { |
| var jsClass = getJSName(cls); |
| if (jsClass.isEmpty) { |
| // No rename, take the name of the class directly. |
| jsClass = cls.name; |
| } else { |
| // Remove any global prefixes. Regex here is greedy and will only return |
| // a value for `className` that doesn't start with 'self.' or 'window.'. |
| var classRegexp = new RegExp(r'^((self|window)\.)*(?<className>.*)$'); |
| var matches = classRegexp.allMatches(jsClass); |
| jsClass = matches.first.namedGroup('className')!; |
| } |
| var nativeClass = _nativeClasses[jsClass]; |
| if (nativeClass != null) { |
| _diagnosticsReporter.report( |
| templateJsInteropNativeClassInAnnotation.withArguments( |
| cls.name, |
| nativeClass.name, |
| nativeClass.enclosingLibrary.importUri.toString()), |
| cls.fileOffset, |
| cls.name.length, |
| cls.fileUri); |
| } |
| } |
| super.visitClass(cls); |
| _classHasAnonymousAnnotation = false; |
| _classHasJSAnnotation = false; |
| } |
| |
| @override |
| void visitLibrary(Library lib) { |
| _libraryHasJSAnnotation = hasJSInteropAnnotation(lib); |
| _libraryIsGlobalNamespace = false; |
| if (_libraryHasJSAnnotation) { |
| var libraryAnnotation = getJSName(lib); |
| var globalRegexp = new RegExp(r'^(self|window)(\.(self|window))*$'); |
| if (libraryAnnotation.isEmpty || |
| globalRegexp.hasMatch(libraryAnnotation)) { |
| _libraryIsGlobalNamespace = true; |
| } |
| } else { |
| _libraryIsGlobalNamespace = true; |
| } |
| super.visitLibrary(lib); |
| _libraryIsGlobalNamespace = false; |
| _libraryHasJSAnnotation = false; |
| _libraryExtensionsIndex = null; |
| } |
| |
| @override |
| void visitProcedure(Procedure procedure) { |
| _checkInstanceMemberJSAnnotation(procedure); |
| if (_classHasJSAnnotation && !procedure.isExternal) { |
| // If not one of few exceptions, member is not allowed to exclude |
| // `external` inside of a JS interop class. |
| if (!(procedure.isAbstract || |
| procedure.isFactory || |
| procedure.isStatic)) { |
| _diagnosticsReporter.report( |
| messageJsInteropNonExternalMember, |
| procedure.fileOffset, |
| procedure.name.text.length, |
| procedure.fileUri); |
| } |
| } |
| |
| if (!_isJSInteropMember(procedure)) { |
| _checkDisallowedExternal(procedure); |
| } else { |
| // Check JS interop indexing. |
| if (!procedure.isStatic && procedure.kind == ProcedureKind.Operator) { |
| _diagnosticsReporter.report( |
| messageJsInteropOperatorsNotSupported, |
| procedure.fileOffset, |
| procedure.name.text.length, |
| procedure.fileUri); |
| } |
| |
| // Check JS Interop positional and named parameters. |
| var isAnonymousFactory = |
| _classHasAnonymousAnnotation && procedure.isFactory; |
| if (isAnonymousFactory) { |
| // ignore: unnecessary_null_comparison |
| if (procedure.function != null && |
| !procedure.function.positionalParameters.isEmpty) { |
| var firstPositionalParam = procedure.function.positionalParameters[0]; |
| _diagnosticsReporter.report( |
| messageJsInteropAnonymousFactoryPositionalParameters, |
| firstPositionalParam.fileOffset, |
| firstPositionalParam.name!.length, |
| firstPositionalParam.location!.file); |
| } |
| } else { |
| // Only factory constructors for anonymous classes are allowed to have |
| // named parameters. |
| _checkNoNamedParameters(procedure.function); |
| } |
| } |
| |
| if (_classHasStaticInteropAnnotation && |
| procedure.isInstanceMember && |
| !procedure.isFactory && |
| !procedure.isSynthetic) { |
| _diagnosticsReporter.report( |
| templateJsInteropStaticInteropWithInstanceMembers |
| .withArguments(procedure.enclosingClass!.name), |
| procedure.fileOffset, |
| procedure.name.text.length, |
| procedure.fileUri); |
| } |
| } |
| |
| @override |
| void visitField(Field field) { |
| if (_classHasStaticInteropAnnotation && field.isInstanceMember) { |
| _diagnosticsReporter.report( |
| templateJsInteropStaticInteropWithInstanceMembers |
| .withArguments(field.enclosingClass!.name), |
| field.fileOffset, |
| field.name.text.length, |
| field.fileUri); |
| } |
| super.visitField(field); |
| } |
| |
| @override |
| void visitConstructor(Constructor constructor) { |
| _checkInstanceMemberJSAnnotation(constructor); |
| if (_classHasJSAnnotation && |
| !constructor.isExternal && |
| !constructor.isSynthetic) { |
| // Non-synthetic constructors must be annotated with `external`. |
| _diagnosticsReporter.report( |
| messageJsInteropNonExternalConstructor, |
| constructor.fileOffset, |
| constructor.name.text.length, |
| constructor.fileUri); |
| } |
| |
| if (!_isJSInteropMember(constructor)) { |
| _checkDisallowedExternal(constructor); |
| } else { |
| _checkNoNamedParameters(constructor.function); |
| } |
| } |
| |
| /// Reports an error if [functionNode] has named parameters. |
| void _checkNoNamedParameters(FunctionNode functionNode) { |
| // ignore: unnecessary_null_comparison |
| if (functionNode != null && !functionNode.namedParameters.isEmpty) { |
| var firstNamedParam = functionNode.namedParameters[0]; |
| _diagnosticsReporter.report( |
| messageJsInteropNamedParameters, |
| firstNamedParam.fileOffset, |
| firstNamedParam.name!.length, |
| firstNamedParam.location!.file); |
| } |
| } |
| |
| /// Reports an error if given instance [member] is JS interop, but inside a |
| /// non JS interop class. |
| void _checkInstanceMemberJSAnnotation(Member member) { |
| var enclosingClass = member.enclosingClass; |
| |
| if (!_classHasJSAnnotation && |
| enclosingClass != null && |
| hasJSInteropAnnotation(member)) { |
| // If in a class that is not JS interop, this member is not allowed to be |
| // JS interop. |
| _diagnosticsReporter.report(messageJsInteropEnclosingClassJSAnnotation, |
| member.fileOffset, member.name.text.length, member.fileUri, |
| context: <LocatedMessage>[ |
| messageJsInteropEnclosingClassJSAnnotationContext.withLocation( |
| enclosingClass.fileUri, |
| enclosingClass.fileOffset, |
| enclosingClass.name.length) |
| ]); |
| } |
| } |
| |
| /// Assumes given [member] is not JS interop, and reports an error if |
| /// [member] is `external` and not an allowed `external` usage. |
| void _checkDisallowedExternal(Member member) { |
| if (member.isExternal) { |
| if (member.isExtensionMember) { |
| if (!_isNativeExtensionMember(member)) { |
| _diagnosticsReporter.report( |
| messageJsInteropExternalExtensionMemberOnTypeInvalid, |
| member.fileOffset, |
| member.name.text.length, |
| member.fileUri); |
| } |
| } else if (!hasJSInteropAnnotation(member) && |
| !_isAllowedExternalUsage(member)) { |
| // Member could be JS annotated and not considered a JS interop member |
| // if inside a non-JS interop class. Should not report an error in this |
| // case, since a different error will already be produced. |
| _diagnosticsReporter.report( |
| messageJsInteropExternalMemberNotJSAnnotated, |
| member.fileOffset, |
| member.name.text.length, |
| member.fileUri); |
| } |
| } |
| } |
| |
| /// Verifies given member is one of the allowed usages of external: |
| /// a dart low level library, a foreign helper, a native test, |
| /// or a from environment constructor. |
| bool _isAllowedExternalUsage(Member member) { |
| Uri uri = member.enclosingLibrary.importUri; |
| return uri.isScheme('dart') && |
| _pathsWithAllowedDartExternalUsage.contains(uri.path) || |
| _allowedNativeTestPatterns.any((pattern) => uri.path.contains(pattern)); |
| } |
| |
| /// Returns whether [member] is considered to be a JS interop member. |
| /// |
| /// A JS interop member is `external`, and is in a valid JS interop context, |
| /// which can be: |
| /// - inside a JS interop class |
| /// - inside an extension on a JS interop class |
| /// - a top level member that is JS interop annotated or in a JS interop |
| /// library |
| /// If a member belongs to a class, the class must be JS interop annotated. |
| bool _isJSInteropMember(Member member) { |
| if (member.isExternal) { |
| if (_classHasJSAnnotation) return true; |
| if (member.isExtensionMember) return _isJSExtensionMember(member); |
| if (member.enclosingClass == null) { |
| return hasJSInteropAnnotation(member) || _libraryHasJSAnnotation; |
| } |
| } |
| |
| // Otherwise, not JS interop. |
| return false; |
| } |
| |
| /// Returns whether given extension [member] is in an extension that is on a |
| /// JS interop class. |
| bool _isJSExtensionMember(Member member) { |
| return _checkExtensionMember(member, hasJSInteropAnnotation); |
| } |
| |
| /// Returns whether given extension [member] is in an extension on a Native |
| /// class. |
| bool _isNativeExtensionMember(Member member) { |
| return _checkExtensionMember(member, _nativeClasses.containsValue); |
| } |
| |
| /// Returns whether given extension [member] is on a class that passses the |
| /// given [validateExtensionClass]. |
| bool _checkExtensionMember(Member member, Function validateExtensionClass) { |
| assert(member.isExtensionMember); |
| if (_libraryExtensionsIndex == null) { |
| _libraryExtensionsIndex = {}; |
| member.enclosingLibrary.extensions.forEach((extension) => |
| extension.members.forEach((memberDescriptor) => |
| _libraryExtensionsIndex![memberDescriptor.member] = extension)); |
| } |
| |
| var onType = _libraryExtensionsIndex![member.reference]!.onType; |
| return onType is InterfaceType && validateExtensionClass(onType.classNode); |
| } |
| } |