blob: 3005f22be27099b6b4a5bc52b87ecbf0acc71e8e [file] [log] [blame]
// 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.
// ignore_for_file: implementation_imports
import 'package:_fe_analyzer_shared/src/messages/codes.dart'
show
Message,
LocatedMessage,
messageJsInteropDartJsInteropAnnotationForStaticInteropOnly,
messageJsInteropEnclosingClassJSAnnotation,
messageJsInteropEnclosingClassJSAnnotationContext,
messageJsInteropExtensionTypeMemberNotInterop,
messageJsInteropExtensionTypeUsedWithWrongJsAnnotation,
messageJsInteropExternalExtensionMemberOnTypeInvalid,
messageJsInteropExternalExtensionMemberWithStaticDisallowed,
messageJsInteropExternalMemberNotJSAnnotated,
messageJsInteropInvalidStaticClassMemberName,
messageJsInteropNamedParameters,
messageJsInteropNonExternalConstructor,
messageJsInteropNonExternalMember,
messageJsInteropOperatorCannotBeRenamed,
messageJsInteropOperatorsNotSupported,
messageJsInteropStaticInteropExternalMemberWithInvalidTypeParameters,
messageJsInteropStaticInteropGenerativeConstructor,
messageJsInteropStaticInteropParameterInitializersAreIgnored,
messageJsInteropStaticInteropSyntheticConstructor,
templateJsInteropDartClassExtendsJSClass,
templateJsInteropJSClassExtendsDartClass,
templateJsInteropNonStaticWithStaticInteropSupertype,
templateJsInteropStaticInteropNoJSAnnotation,
templateJsInteropStaticInteropWithInstanceMembers,
templateJsInteropStaticInteropWithNonStaticSupertype,
templateJsInteropObjectLiteralConstructorPositionalParameters,
templateJsInteropNativeClassInAnnotation,
templateJsInteropStaticInteropTearOffsDisallowed,
templateJsInteropStaticInteropTrustTypesUsageNotAllowed,
templateJsInteropStaticInteropTrustTypesUsedWithoutStaticInterop,
templateJsInteropStrictModeForbiddenLibrary;
import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity;
import 'package:_js_interop_checks/src/transformations/export_checker.dart';
import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart';
// Used for importing CFE utility functions for constructor tear-offs.
import 'package:front_end/src/api_prototype/lowering_predicates.dart';
import 'package:front_end/src/fasta/fasta_codes.dart'
show
templateJsInteropExtensionTypeNotInterop,
templateJsInteropFunctionToJSRequiresStaticType,
templateJsInteropStaticInteropExternalTypeViolation;
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart' hide Pattern;
import 'package:kernel/target/targets.dart';
import 'package:kernel/type_environment.dart';
import 'src/js_interop.dart';
class JsInteropChecks extends RecursiveVisitor {
final Set<Constant> _constantCache = {};
final CoreTypes _coreTypes;
late final ExtensionIndex _extensionIndex;
final Procedure _functionToJSTarget;
// Errors on constants need source information, so we use the surrounding
// `ConstantExpression` as the source.
ConstantExpression? _lastConstantExpression;
final Map<String, Class> _nativeClasses;
final JsInteropDiagnosticReporter _reporter;
final StatefulStaticTypeContext _staticTypeContext;
late _TypeParameterBoundChecker _typeParameterBoundChecker;
bool _classHasJSAnnotation = false;
bool _classHasAnonymousAnnotation = false;
bool _classHasStaticInteropAnnotation = false;
final _checkDisallowedInterop = false;
bool _inTearoff = false;
bool _libraryHasDartJSInteropAnnotation = false;
bool _libraryHasJSAnnotation = false;
bool _libraryIsGlobalNamespace = false;
final ExportChecker exportChecker;
final bool isDart2Wasm;
/// Native tests to exclude from checks on external.
// TODO(rileyporter): Use ExternalName from CFE to exclude native tests.
static final List<Pattern> _allowedNativeTestPatterns = [
RegExp(r'(?<!generated_)tests/web/native'),
RegExp(r'(?<!generated_)tests/web/internal'),
'generated_tests/web/native/native_test',
];
static final List<Pattern> _allowedTrustTypesTestPatterns = [
RegExp(r'(?<!generated_)tests/lib/js'),
];
/// Libraries that cannot be used when [_enforceStrictMode] is true.
static const _disallowedLibrariesInStrictMode = [
'package:js/js.dart',
'package:js/js_util.dart',
'dart:html',
'dart:js_util',
'dart:js'
];
/// Libraries that use `external` to exclude from checks on external.
static const Iterable<String> _pathsWithAllowedDartExternalUsage = <String>[
'_foreign_helper', // for foreign helpers
'_late_helper', // for dart2js late variable utilities
'_interceptors', // for ddc JS string
'_native_typed_data',
'_runtime', // for ddc types at runtime
'_js_helper', // for ddc inlined helper methods
'async',
'core', // for environment constructors
'html',
'html_common',
'indexed_db',
'js',
'js_interop',
'js_util',
'svg',
'web_audio',
'web_gl',
'web_sql'
];
JsInteropChecks(this._coreTypes, ClassHierarchy hierarchy, this._reporter,
this._nativeClasses,
{this.isDart2Wasm = false})
: exportChecker = ExportChecker(_reporter, _coreTypes.objectClass),
_functionToJSTarget = _coreTypes.index.getTopLevelProcedure(
'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'),
_staticTypeContext = StatefulStaticTypeContext.stacked(
TypeEnvironment(_coreTypes, hierarchy)) {
_extensionIndex =
ExtensionIndex(_coreTypes, _staticTypeContext.typeEnvironment);
_typeParameterBoundChecker = _TypeParameterBoundChecker(_extensionIndex);
}
/// Determines if given [member] is an external extension member that needs to
/// be patched instead of lowered.
static bool isPatchedMember(Member member) =>
member.isExternal && hasPatchAnnotation(member);
/// 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) {
final nativeClasses = <String, Class>{};
for (final library in component.libraries) {
for (final cls in library.classes) {
final nativeNames = getNativeNames(cls);
for (final nativeName in nativeNames) {
nativeClasses[nativeName] = cls;
}
}
}
return nativeClasses;
}
@override
void defaultMember(Member node) {
_staticTypeContext.enterMember(node);
_checkInstanceMemberJSAnnotation(node);
if (!_isJSInteropMember(node)) _checkDisallowedExternal(node);
// TODO(43530): Disallow having JS interop annotations on non-external
// members (class members or otherwise). Currently, they're being ignored.
exportChecker.visitMember(node);
super.defaultMember(node);
_staticTypeContext.leaveMember(node);
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
if (hasPackageJSAnnotation(node)) {
_reporter.report(messageJsInteropExtensionTypeUsedWithWrongJsAnnotation,
node.fileOffset, node.name.length, node.fileUri);
}
if (hasDartJSInteropAnnotation(node) &&
!_extensionIndex.isInteropExtensionType(node)) {
_reporter.report(
templateJsInteropExtensionTypeNotInterop.withArguments(
node.name, node.declaredRepresentationType, true),
node.fileOffset,
node.name.length,
node.fileUri);
}
super.visitExtensionTypeDeclaration(node);
}
@override
void visitClass(Class node) {
_classHasJSAnnotation = hasJSInteropAnnotation(node);
_classHasAnonymousAnnotation = hasAnonymousAnnotation(node);
_classHasStaticInteropAnnotation = hasStaticInteropAnnotation(node);
void report(Message message) => _reporter.report(
message, node.fileOffset, node.name.length, node.fileUri);
// @JS checks.
var superclass = node.superclass;
// Ignore the superclass if it is trivial.
if (superclass == _coreTypes.objectClass) superclass = null;
if (_classHasJSAnnotation) {
if (!_classHasAnonymousAnnotation &&
!_classHasStaticInteropAnnotation &&
_libraryIsGlobalNamespace) {
_checkJsInteropClassNotUsingNativeClass(node);
}
if (superclass != null && !hasJSInteropAnnotation(superclass)) {
report(templateJsInteropJSClassExtendsDartClass.withArguments(
node.name, superclass.name));
}
} else {
if (superclass != null && hasJSInteropAnnotation(superclass)) {
report(templateJsInteropDartClassExtendsJSClass.withArguments(
node.name, superclass.name));
}
}
// @staticInterop checks
if (_classHasStaticInteropAnnotation) {
if (!_classHasJSAnnotation) {
report(templateJsInteropStaticInteropNoJSAnnotation
.withArguments(node.name));
}
if (superclass != null && !hasStaticInteropAnnotation(superclass)) {
report(templateJsInteropStaticInteropWithNonStaticSupertype
.withArguments(node.name, superclass.name));
}
// Validate that superinterfaces are all valid supertypes as well. Note
// that mixins are already disallowed and therefore are not checked here.
for (final supertype in node.implementedTypes) {
if (!hasStaticInteropAnnotation(supertype.classNode)) {
report(templateJsInteropStaticInteropWithNonStaticSupertype
.withArguments(node.name, supertype.classNode.name));
}
}
} else {
// For classes, `dart:js_interop`'s `@JS` can only be used with
// `@staticInterop`.
if (hasDartJSInteropAnnotation(node)) {
report(messageJsInteropDartJsInteropAnnotationForStaticInteropOnly);
}
if (superclass != null && hasStaticInteropAnnotation(superclass)) {
report(templateJsInteropNonStaticWithStaticInteropSupertype
.withArguments(node.name, superclass.name));
}
// The converse of the above. If the class is not marked as static, it
// should not implement a class that is.
for (final supertype in node.implementedTypes) {
if (hasStaticInteropAnnotation(supertype.classNode)) {
report(templateJsInteropNonStaticWithStaticInteropSupertype
.withArguments(node.name, supertype.classNode.name));
}
}
}
// @trustTypes checks.
if (hasTrustTypesAnnotation(node)) {
if (!_isAllowedTrustTypesUsage(node)) {
report(templateJsInteropStaticInteropTrustTypesUsageNotAllowed
.withArguments(node.name));
}
if (!_classHasStaticInteropAnnotation) {
report(templateJsInteropStaticInteropTrustTypesUsedWithoutStaticInterop
.withArguments(node.name));
}
}
super.visitClass(node);
// Validate `@JSExport` usage after so we know if the members have the
// annotation.
exportChecker.visitClass(node);
_classHasStaticInteropAnnotation = false;
_classHasAnonymousAnnotation = false;
_classHasJSAnnotation = false;
}
@override
void visitLibrary(Library node) {
_staticTypeContext.enterLibrary(node);
_libraryHasDartJSInteropAnnotation = hasDartJSInteropAnnotation(node);
_libraryHasJSAnnotation =
_libraryHasDartJSInteropAnnotation || hasJSInteropAnnotation(node);
_libraryIsGlobalNamespace = _isLibraryGlobalNamespace(node);
// TODO(srujzs): Should we still keep around this check? Currently, it's
// unused since we allow the old interop on dart2wasm, but we should
// disallow them eventually.
if (_checkDisallowedInterop) _checkDisallowedLibrariesForDart2Wasm(node);
super.visitLibrary(node);
exportChecker.visitLibrary(node);
_staticTypeContext.leaveLibrary(node);
}
@override
void visitProcedure(Procedure node) {
_staticTypeContext.enterMember(node);
_inTearoff = isTearOffLowering(node);
void report(Message message) => _reporter.report(
message, node.fileOffset, node.name.text.length, node.fileUri);
// TODO(joshualitt): Add a check that only supported operators are allowed
// in external extension members and extension types.
_checkInstanceMemberJSAnnotation(node);
if (_classHasJSAnnotation &&
!node.isExternal &&
!node.isAbstract &&
!node.isFactory &&
!node.isStatic) {
// If not one of few exceptions, member is not allowed to exclude
// `external` inside of a JS interop class.
report(messageJsInteropNonExternalMember);
}
if (!_isJSInteropMember(node)) {
_checkDisallowedExternal(node);
} else {
_checkJsInteropMemberNotOperator(node);
// Check JS Interop positional and named parameters. Literal constructors
// can only have named parameters, and every other interop member can only
// have positional parameters.
final isObjectLiteralConstructor = node.isExtensionTypeMember &&
(_extensionIndex.getExtensionTypeDescriptor(node)!.kind ==
ExtensionTypeMemberKind.Constructor ||
_extensionIndex.getExtensionTypeDescriptor(node)!.kind ==
ExtensionTypeMemberKind.Factory) &&
node.function.namedParameters.isNotEmpty;
final isAnonymousFactory = _classHasAnonymousAnnotation && node.isFactory;
if (isObjectLiteralConstructor || isAnonymousFactory) {
_checkLiteralConstructorHasNoPositionalParams(node,
isAnonymousFactory: isAnonymousFactory);
} else {
_checkNoNamedParameters(node.function);
}
// JS static methods cannot use a JS name with dots.
if (node.isStatic &&
node.enclosingClass != null &&
getJSName(node).contains('.')) {
report(messageJsInteropInvalidStaticClassMemberName);
}
if (_classHasStaticInteropAnnotation ||
node.isExtensionTypeMember ||
node.isExtensionMember ||
node.enclosingClass == null &&
(hasDartJSInteropAnnotation(node) ||
_libraryHasDartJSInteropAnnotation)) {
_checkNoParamInitializersForStaticInterop(node.function);
late Annotatable? annotatable;
if (node.isExtensionTypeMember) {
annotatable = _extensionIndex.getExtensionType(node);
} else if (node.isExtensionMember) {
annotatable = _extensionIndex.getExtensionAnnotatable(node);
if (annotatable != null) {
// We do not support external extension members with the 'static'
// keyword currently.
if (_extensionIndex.getExtensionDescriptor(node)!.isStatic) {
report(
messageJsInteropExternalExtensionMemberWithStaticDisallowed);
}
}
} else {
annotatable = node.enclosingClass;
}
if (!isPatchedMember(node)) {
if (annotatable == null ||
((hasDartJSInteropAnnotation(annotatable) ||
annotatable is ExtensionTypeDeclaration))) {
// Checks for dart:js_interop APIs only.
_checkStaticInteropMemberUsesValidTypeParameters(node);
final function = node.function;
_reportProcedureIfNotAllowedType(function.returnType, node);
for (final parameter in function.positionalParameters) {
_reportProcedureIfNotAllowedType(parameter.type, node);
}
for (final parameter in function.namedParameters) {
_reportProcedureIfNotAllowedType(parameter.type, node);
}
}
}
}
}
if (_classHasStaticInteropAnnotation &&
node.isInstanceMember &&
!node.isFactory &&
!node.isSynthetic) {
report(templateJsInteropStaticInteropWithInstanceMembers
.withArguments(node.enclosingClass!.name));
}
super.visitProcedure(node);
_inTearoff = false;
_staticTypeContext.leaveMember(node);
}
@override
void visitStaticInvocation(StaticInvocation node) {
final target = node.target;
if (target == _functionToJSTarget) {
_checkFunctionToJSCall(node);
} else {
// Only check generated tear-offs in StaticInvocations.
final tornOff = _getTornOffFromGeneratedTearOff(target);
if (tornOff != null) _checkDisallowedTearoff(tornOff, node);
}
super.visitStaticInvocation(node);
}
@override
void visitField(Field node) {
if (_classHasStaticInteropAnnotation && node.isInstanceMember) {
_reporter.report(
templateJsInteropStaticInteropWithInstanceMembers
.withArguments(node.enclosingClass!.name),
node.fileOffset,
node.name.text.length,
node.fileUri);
}
super.visitField(node);
}
@override
void visitConstructor(Constructor node) {
void report(Message message) => _reporter.report(
message, node.fileOffset, node.name.text.length, node.fileUri);
_checkInstanceMemberJSAnnotation(node);
if (!node.isSynthetic) {
if (_classHasJSAnnotation && !node.isExternal) {
// Non-synthetic constructors must be annotated with `external`.
report(messageJsInteropNonExternalConstructor);
}
if (_classHasStaticInteropAnnotation) {
// Can only have factory constructors on @staticInterop classes.
report(messageJsInteropStaticInteropGenerativeConstructor);
}
}
if (!_isJSInteropMember(node)) {
_checkDisallowedExternal(node);
} else {
_checkNoNamedParameters(node.function);
}
}
@override
void visitConstructorInvocation(ConstructorInvocation node) {
final constructor = node.target;
if (constructor.isSynthetic &&
// Synthetic tear-offs are created for synthetic constructors by
// invoking them, so they need to be excluded here.
!_inTearoff &&
hasStaticInteropAnnotation(constructor.enclosingClass)) {
_reporter.report(messageJsInteropStaticInteropSyntheticConstructor,
node.fileOffset, node.name.text.length, node.location?.file);
}
super.visitConstructorInvocation(node);
}
@override
void visitConstantExpression(ConstantExpression node) {
_lastConstantExpression = node;
node.constant.acceptReference(this);
_lastConstantExpression = null;
}
@override
void visitStaticTearOff(StaticTearOff node) {
_checkDisallowedTearoff(
_getTornOffFromGeneratedTearOff(node.target) ?? node.target, node);
}
@override
void defaultConstantReference(Constant node) {
if (_constantCache.add(node)) {
node.visitChildren(this);
}
}
@override
void visitStaticTearOffConstantReference(StaticTearOffConstant node) {
if (_constantCache.contains(node)) return;
if (_checkDisallowedTearoff(
_getTornOffFromGeneratedTearOff(node.target) ?? node.target,
_lastConstantExpression)) {
return;
}
// Only add to the cache if we don't find an error. This is to make sure
// that multiple usages of the same constant can be caught if it's
// disallowed.
_constantCache.add(node);
}
// TODO(srujzs): Helper functions are organized according to node types, but
// it would be nice to get nominal separation instead of just comments.
// Extensions on the node types don't work well because there is a lot of
// state that these helpers use. Mixins probably won't work well for a similar
// reason. It's possible that named extensions on the visitor itself would
// work.
// JS interop library checks
void _checkDisallowedLibrariesForDart2Wasm(Library node) {
for (final dependency in node.dependencies) {
final dependencyUriString = dependency.targetLibrary.importUri.toString();
if (_disallowedLibrariesInStrictMode.contains(dependencyUriString)) {
_reporter.report(
templateJsInteropStrictModeForbiddenLibrary
.withArguments(dependencyUriString),
dependency.fileOffset,
dependencyUriString.length,
node.fileUri);
}
}
}
/// Compute whether top-level nodes under [node] would be using the global
/// JS namespace.
bool _isLibraryGlobalNamespace(Library node) {
if (_libraryHasJSAnnotation) {
final libraryAnnotation = getJSName(node);
final globalRegexp = RegExp(r'^(self|window)(\.(self|window))*$');
if (libraryAnnotation.isEmpty ||
globalRegexp.hasMatch(libraryAnnotation)) {
return true;
}
} else {
return true;
}
return false;
}
// JS interop class checks
/// Verifies that use of `@trustTypes` is allowed on [cls].
bool _isAllowedTrustTypesUsage(Class cls) {
final uri = cls.enclosingLibrary.importUri;
return uri.isScheme('dart') && uri.path == 'ui' ||
_allowedTrustTypesTestPatterns
.any((pattern) => uri.path.contains(pattern));
}
/// Check that JS interop class [node], that only has an @JS annotation, is
/// not bound to a type that is reserved by a @Native type.
///
/// Trying to interop those types without @staticInterop results in errors.
void _checkJsInteropClassNotUsingNativeClass(Class node) {
// Since this is a breaking check, it is language-versioned.
if (node.enclosingLibrary.languageVersion >= Version(2, 13)) {
var jsClass = getJSName(node);
if (jsClass.isEmpty) {
// No rename, take the name of the class directly.
jsClass = node.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.'.
final classRegexp = RegExp(r'^((self|window)\.)*(?<className>.*)$');
final matches = classRegexp.allMatches(jsClass);
jsClass = matches.first.namedGroup('className')!;
}
final nativeClass = _nativeClasses[jsClass];
if (nativeClass != null) {
_reporter.report(
templateJsInteropNativeClassInAnnotation.withArguments(
node.name,
nativeClass.name,
nativeClass.enclosingLibrary.importUri.toString()),
node.fileOffset,
node.name.length,
node.fileUri);
}
}
}
// JS interop member checks
/// 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));
}
/// 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) {
// TODO(joshualitt): These checks add value for our users, but unfortunately
// some backends support multiple native APIs. We should really make a
// neutral 'ExternalUsageVerifier` class, but until then we just disable
// this check on Dart2Wasm.
if (isDart2Wasm) return;
if (member.isExternal) {
if (_isAllowedExternalUsage(member)) return;
if (member.isExtensionMember) {
final annotatable = _extensionIndex.getExtensionAnnotatable(member);
if (annotatable == null) {
_reporter.report(messageJsInteropExternalExtensionMemberOnTypeInvalid,
member.fileOffset, member.name.text.length, member.fileUri);
}
} else if (member.isExtensionTypeMember) {
final extensionType = _extensionIndex.getExtensionType(member);
if (extensionType == null) {
_reporter.report(messageJsInteropExtensionTypeMemberNotInterop,
member.fileOffset, member.name.text.length, member.fileUri);
}
} else if (!hasJSInteropAnnotation(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.
_reporter.report(messageJsInteropExternalMemberNotJSAnnotated,
member.fileOffset, member.name.text.length, member.fileUri);
}
}
}
/// Given a [member] and the [context] in which the use of it occurs,
/// determines whether a tear-off of [member] can be used.
///
/// Tear-offs of the following are disallowed when using dart:js_interop:
///
/// - External extension type constructors and factories
/// - External factories of @staticInterop classes
/// - External interop extension type methods
/// - External interop extension methods on @staticInterop or extension types
/// - Synthetic generative @staticInterop constructors
/// - External top-level methods
///
/// Returns whether an error was triggered.
bool _checkDisallowedTearoff(Member member, TreeNode? context) {
if (context == null || context.location == null) return false;
if (member.isExternal) {
var memberKind = '';
var memberName = '';
if (member.isExtensionTypeMember) {
// Extension type interop members can not be torn off.
if (_extensionIndex.getExtensionType(member) == null) {
return false;
}
memberKind = 'extension type interop member';
memberName =
_extensionIndex.getExtensionTypeDescriptor(member)!.name.text;
if (memberName.isEmpty) memberName = 'new';
} else if (member.isExtensionMember) {
// JS interop members can not be torn off.
if (_extensionIndex.getExtensionAnnotatable(member) == null) {
return false;
}
memberKind = 'extension interop member';
memberName = _extensionIndex.getExtensionDescriptor(member)!.name.text;
} else if (member.enclosingClass != null) {
// @staticInterop members can not be torn off.
final enclosingClass = member.enclosingClass!;
if (!hasStaticInteropAnnotation(enclosingClass)) return false;
memberKind = '@staticInterop member';
memberName = member.name.text;
if (memberName.isEmpty) memberName = 'new';
} else {
// Top-levels with dart:js_interop can not be torn off.
if (!hasDartJSInteropAnnotation(member) &&
!hasDartJSInteropAnnotation(member.enclosingLibrary)) {
return false;
}
memberKind = 'top-level member';
memberName = member.name.text;
}
_reporter.report(
templateJsInteropStaticInteropTearOffsDisallowed.withArguments(
memberKind, memberName),
context.fileOffset,
1,
context.location!.file);
return true;
} else if (member is Constructor &&
member.isSynthetic &&
hasStaticInteropAnnotation(member.enclosingClass)) {
// Use of a synthetic generative constructor on @staticInterop class is
// disallowed.
_reporter.report(messageJsInteropStaticInteropSyntheticConstructor,
context.fileOffset, 1, context.location!.file);
return true;
}
return false;
}
/// Checks that [node], which is a call to 'Function.toJS', is called with a
/// valid function type.
void _checkFunctionToJSCall(StaticInvocation node) {
final argument = node.arguments.positional.single;
final functionType = argument.getStaticType(_staticTypeContext);
if (functionType is! FunctionType) {
_reporter.report(
templateJsInteropFunctionToJSRequiresStaticType.withArguments(
functionType, true),
node.fileOffset,
node.name.text.length,
node.location?.file);
} else {
_reportStaticInvocationIfNotAllowedType(functionType.returnType, node);
for (final parameter in functionType.positionalParameters) {
_reportStaticInvocationIfNotAllowedType(parameter, node);
}
}
}
/// Reports an error if given instance [member] is JS interop, but inside a
/// non JS interop class.
void _checkInstanceMemberJSAnnotation(Member member) {
final 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.
_reporter.report(messageJsInteropEnclosingClassJSAnnotation,
member.fileOffset, member.name.text.length, member.fileUri,
context: <LocatedMessage>[
messageJsInteropEnclosingClassJSAnnotationContext.withLocation(
enclosingClass.fileUri,
enclosingClass.fileOffset,
enclosingClass.name.length)
]);
}
}
/// Given JS interop member [node], checks that it is not an operator that is
/// disallowed.
///
/// Also checks that no renaming is done on interop operators.
void _checkJsInteropMemberNotOperator(Procedure node) {
var isInvalidOperator = false;
var operatorHasRenaming = false;
if ((node.isExtensionTypeMember &&
_extensionIndex.getExtensionTypeDescriptor(node)?.kind ==
ExtensionTypeMemberKind.Operator) ||
(node.isExtensionMember &&
_extensionIndex.getExtensionDescriptor(node)?.kind ==
ExtensionMemberKind.Operator)) {
final operator =
_extensionIndex.getExtensionTypeDescriptor(node)?.name.text ??
_extensionIndex.getExtensionDescriptor(node)?.name.text;
isInvalidOperator = operator != '[]' && operator != '[]=';
operatorHasRenaming = getJSName(node).isNotEmpty;
} else if (!node.isStatic && node.kind == ProcedureKind.Operator) {
isInvalidOperator = true;
operatorHasRenaming = getJSName(node).isNotEmpty;
}
if (isInvalidOperator) {
_reporter.report(messageJsInteropOperatorsNotSupported, node.fileOffset,
node.name.text.length, node.fileUri);
}
if (operatorHasRenaming) {
_reporter.report(messageJsInteropOperatorCannotBeRenamed, node.fileOffset,
node.name.text.length, node.fileUri);
}
}
void _checkLiteralConstructorHasNoPositionalParams(Procedure node,
{required bool isAnonymousFactory}) {
final positionalParams = node.function.positionalParameters;
if (positionalParams.isNotEmpty) {
final firstPositionalParam = positionalParams[0];
_reporter.report(
templateJsInteropObjectLiteralConstructorPositionalParameters
.withArguments(isAnonymousFactory
? '@anonymous factories'
: 'Object literal constructors'),
firstPositionalParam.fileOffset,
firstPositionalParam.name!.length,
firstPositionalParam.location!.file);
}
}
/// Reports an error if [functionNode] has named parameters.
void _checkNoNamedParameters(FunctionNode functionNode) {
if (functionNode.namedParameters.isNotEmpty) {
final firstNamedParam = functionNode.namedParameters[0];
_reporter.report(
messageJsInteropNamedParameters,
firstNamedParam.fileOffset,
firstNamedParam.name!.length,
firstNamedParam.location!.file);
}
}
/// Reports a warning if static interop function [node] has any parameters
/// that have a declared initializer.
void _checkNoParamInitializersForStaticInterop(FunctionNode node) {
for (final param in [
...node.positionalParameters,
...node.namedParameters
]) {
if (param.hasDeclaredInitializer) {
_reporter.report(
messageJsInteropStaticInteropParameterInitializersAreIgnored,
param.fileOffset,
param.name!.length,
param.location!.file);
}
}
}
void _checkStaticInteropMemberUsesValidTypeParameters(Procedure node) {
if (_typeParameterBoundChecker.containsInvalidTypeBound(node)) {
_reporter.report(
messageJsInteropStaticInteropExternalMemberWithInvalidTypeParameters,
node.fileOffset,
node.name.text.length,
node.fileUri);
}
}
/// If [procedure] is a generated procedure that represents a relevant
/// tear-off, return the torn-off member.
///
/// Otherwise, return null.
Member? _getTornOffFromGeneratedTearOff(Procedure procedure) {
final tornOff =
_extensionIndex.getExtensionTypeMemberForTearOff(procedure) ??
_extensionIndex.getExtensionMemberForTearOff(procedure);
if (tornOff != null) return tornOff.asMember;
final name = extractConstructorNameFromTearOff(procedure.name);
if (name == null) return null;
final enclosingClass = procedure.enclosingClass;
// To avoid processing every class' constructors again, we only check for
// constructor tear-offs on relevant classes a.k.a. @staticInterop classes.
if (enclosingClass == null || !hasStaticInteropAnnotation(enclosingClass)) {
return null;
}
for (final constructor in enclosingClass.constructors) {
if (constructor.name.text == name) {
return constructor;
}
}
for (final procedure in enclosingClass.procedures) {
if (procedure.isFactory && procedure.name.text == name) {
return procedure;
}
}
return null;
}
/// 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 or @Native annotatable
/// - inside a JS interop extension type
/// - a top level member that is JS interop annotated or in a package:js JS
/// interop library
bool _isJSInteropMember(Member member) {
if (member.isExternal) {
if (_classHasJSAnnotation) return true;
if (member.isExtensionMember) {
return _extensionIndex.getExtensionAnnotatable(member) != null;
}
if (member.isExtensionTypeMember) {
return _extensionIndex.getExtensionType(member) != null;
}
if (member.enclosingClass == null) {
// dart:js_interop requires top-levels to be @JS-annotated. package:js
// historically does not have this restriction. We add this restriction
// to refuse confusion on what an external member does now that we can
// have dart:ffi and dart:js_interop in the same code via dart2wasm.
final libraryHasPkgJSAnnotation =
_libraryHasJSAnnotation && !_libraryHasDartJSInteropAnnotation;
return hasJSInteropAnnotation(member) || libraryHasPkgJSAnnotation;
}
}
// Otherwise, not JS interop.
return false;
}
bool _isAllowedExternalType(DartType type) {
// TODO(joshualitt): We allow only JS types on external JS interop APIs with
// two exceptions: `void` and `Null`. Both of these exceptions exist largely
// to support passing Dart functions to JS as callbacks. Furthermore, both
// of these types mean no actual values needs to be returned to JS. That
// said, for completeness, we may restrict these two types someday, and
// provide JS types equivalents, but likely only if we have implicit
// conversions between Dart types and JS types.
// Type parameter types are checked elsewhere.
if (type is VoidType || type is NullType || type is TypeParameterType) {
return true;
}
if (type is InterfaceType) {
final cls = type.classNode;
if (cls == _coreTypes.boolClass ||
cls == _coreTypes.numClass ||
cls == _coreTypes.doubleClass ||
cls == _coreTypes.intClass ||
cls == _coreTypes.stringClass) {
return true;
}
if (hasStaticInteropAnnotation(cls)) return true;
}
if (type is ExtensionType) {
if (_extensionIndex
.isInteropExtensionType(type.extensionTypeDeclaration)) {
return true;
}
return _isAllowedExternalType(type.extensionTypeErasure);
}
return false;
}
void _reportIfNotAllowedExternalType(
DartType type, TreeNode node, Name name, Uri? fileUri) {
if (!_isAllowedExternalType(type)) {
_reporter.report(
templateJsInteropStaticInteropExternalTypeViolation.withArguments(
type, true),
node.fileOffset,
name.text.length,
fileUri);
}
}
void _reportProcedureIfNotAllowedType(DartType type, Procedure node) =>
_reportIfNotAllowedExternalType(type, node, node.name, node.fileUri);
void _reportStaticInvocationIfNotAllowedType(
DartType type, StaticInvocation node) =>
_reportIfNotAllowedExternalType(
type, node, node.name, node.location?.file);
}
/// Visitor used to check that all usages of type parameter types of an external
/// static interop member is a valid static interop type.
class _TypeParameterBoundChecker extends RecursiveVisitor {
final ExtensionIndex _extensionIndex;
_TypeParameterBoundChecker(this._extensionIndex);
bool _containsInvalidTypeBound = false;
bool containsInvalidTypeBound(Procedure node) {
_containsInvalidTypeBound = false;
final function = node.function;
for (final param in function.positionalParameters) {
param.accept(this);
}
function.returnType.accept(this);
return _containsInvalidTypeBound;
}
@override
void visitInterfaceType(InterfaceType node) {
final cls = node.classNode;
if (hasStaticInteropAnnotation(cls)) return;
super.visitInterfaceType(node);
}
@override
void visitExtensionType(ExtensionType node) {
if (_extensionIndex.isInteropExtensionType(node.extensionTypeDeclaration)) {
return;
}
super.visitExtensionType(node);
}
@override
void visitTypeParameterType(TypeParameterType node) {
final bound = node.bound;
if (bound is ExtensionType &&
!_extensionIndex
.isInteropExtensionType(bound.extensionTypeDeclaration)) {
_containsInvalidTypeBound = true;
}
if (bound is InterfaceType &&
!hasStaticInteropAnnotation(bound.classNode)) {
_containsInvalidTypeBound = true;
}
}
}
class JsInteropDiagnosticReporter {
bool hasJsInteropErrors = false;
final DiagnosticReporter<Message, LocatedMessage> _reporter;
JsInteropDiagnosticReporter(this._reporter);
void report(Message message, int charOffset, int length, Uri? fileUri,
{List<LocatedMessage>? context}) {
if (context == null) {
_reporter.report(message, charOffset, length, fileUri);
} else {
_reporter.report(message, charOffset, length, fileUri, context: context);
}
if (message.code.severity == Severity.error) hasJsInteropErrors = true;
}
}