blob: d1b1478e56ee9513f462c2a827b2b596f7b0f78f [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/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';
import 'package:front_end/src/api_prototype/codes.dart'
show
Message,
LocatedMessage,
messageDartFfiLibraryInDart2Wasm,
messageJsInteropDartJsInteropAnnotationForStaticInteropOnly,
messageJsInteropEnclosingClassJSAnnotation,
messageJsInteropEnclosingClassJSAnnotationContext,
messageJsInteropExtensionTypeMemberNotInterop,
messageJsInteropExtensionTypeUsedWithWrongJsAnnotation,
messageJsInteropExternalExtensionMemberOnTypeInvalid,
messageJsInteropExternalExtensionMemberWithStaticDisallowed,
messageJsInteropExternalMemberNotJSAnnotated,
messageJsInteropFunctionToJSNamedParameters,
messageJsInteropFunctionToJSTypeParameters,
messageJsInteropInvalidStaticClassMemberName,
messageJsInteropNamedParameters,
messageJsInteropNonExternalConstructor,
messageJsInteropNonExternalMember,
messageJsInteropOperatorCannotBeRenamed,
messageJsInteropOperatorsNotSupported,
messageJsInteropStaticInteropGenerativeConstructor,
messageJsInteropStaticInteropParameterInitializersAreIgnored,
messageJsInteropStaticInteropSyntheticConstructor,
templateJsInteropDartClassExtendsJSClass,
templateJsInteropDisallowedInteropLibraryInDart2Wasm,
templateJsInteropJSClassExtendsDartClass,
templateJsInteropNonStaticWithStaticInteropSupertype,
templateJsInteropStaticInteropNoJSAnnotation,
templateJsInteropStaticInteropWithInstanceMembers,
templateJsInteropStaticInteropWithNonStaticSupertype,
templateJsInteropObjectLiteralConstructorPositionalParameters,
templateJsInteropNativeClassInAnnotation,
templateJsInteropStaticInteropTearOffsDisallowed,
templateJsInteropStaticInteropTrustTypesUsageNotAllowed,
templateJsInteropStaticInteropTrustTypesUsedWithoutStaticInterop,
templateJsInteropExtensionTypeNotInterop,
templateJsInteropFunctionToJSRequiresStaticType,
templateJsInteropStaticInteropExternalAccessorTypeViolation,
templateJsInteropStaticInteropExternalFunctionTypeViolation,
templateJsInteropStaticInteropToJSFunctionTypeViolation;
// Used for importing CFE utility functions for constructor tear-offs.
import 'package:front_end/src/api_prototype/lowering_predicates.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart' hide Pattern;
import 'package:kernel/src/printer.dart';
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;
final AstTextStrategy _textStrategy = const AstTextStrategy(
showNullableOnly: true,
useQualifiedTypeParameterNames: false,
);
bool _classHasJSAnnotation = false;
bool _classHasAnonymousAnnotation = false;
bool _classHasStaticInteropAnnotation = false;
bool _inTearoff = false;
bool _libraryHasDartJSInteropAnnotation = false;
bool _libraryHasJSAnnotation = false;
bool _libraryIsGlobalNamespace = false;
final ExportChecker exportChecker;
final bool isDart2Wasm;
final List<String> _disallowedInteropLibrariesInDart2Wasm;
/// 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'),
];
static final List<Pattern>
_allowedUseOfDart2WasmDisallowedInteropLibrariesTestPatterns = [
// Benchmarks.
RegExp(r'BigIntParsePrint/dart/native_version_javascript.dart'),
RegExp(r'JSInterop/dart/jsinterop_lib.dart'),
// Tests.
RegExp(r'(?<!generated_)tests/lib/js/export'),
// Negative lookahead to test the violation.
RegExp(
r'(?<!generated_)tests/lib/js/static_interop_test(?!/disallowed_interop_libraries_test.dart)',
),
RegExp(r'(?<!generated_)tests/web/wasm/(?!ffi/).*'),
// Flutter tests.
RegExp(r'flutter/lib/web_ui/test'),
];
// TODO(srujzs): Help migrate some of these away. Once we're done, we can
// remove `dart:*` interop libraries from the check as they can be moved out
// of `libraries.json`.
static const allowedInteropLibrariesInDart2WasmPackages = [
// Both these packages re-export other interop libraries
'js',
'js_util',
// Flutter/benchmarks.
'flutter',
'engine',
'ui',
// Non-SDK packages that have been migrated for the Wasm experiment but
// still have references to older interop libraries.
'package_info_plus',
'test',
'url_launcher_web',
];
/// Interop libraries that cannot be used in dart2wasm.
static const _disallowedInteropLibrariesInDart2WasmByDefault = [
'package:js/js.dart',
'package:js/js_util.dart',
'dart:js_util',
'dart:ffi',
];
/// 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,
bool enableExperimentalFfi = false,
}) : exportChecker = ExportChecker(_reporter, _coreTypes.objectClass),
_functionToJSTarget = _coreTypes.index.getTopLevelProcedure(
'dart:js_interop',
'FunctionToJSExportedDartFunction|get#toJS',
),
_staticTypeContext = StatefulStaticTypeContext.stacked(
TypeEnvironment(_coreTypes, hierarchy),
),
_disallowedInteropLibrariesInDart2Wasm = [
for (final entry in _disallowedInteropLibrariesInDart2WasmByDefault)
if (!(entry == 'dart:ffi' && enableExperimentalFfi)) entry,
] {
extensionIndex = ExtensionIndex(
_coreTypes,
_staticTypeContext.typeEnvironment,
);
}
/// 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,
),
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);
if (isDart2Wasm) _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,
);
_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 {
_checkJsInteropOperator(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);
final 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.
_reportExternalProcedureIfNotAllowedFunctionType(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);
}
super.visitConstructor(node);
}
@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
/// Check that [node] doesn't depend on any disallowed interop libraries in
/// dart2wasm.
///
/// We allowlist `dart:*` libraries, select packages, and test patterns.
void _checkDisallowedLibrariesForDart2Wasm(Library node) {
final uri = node.importUri;
for (final dependency in node.dependencies) {
final dependencyUriString = dependency.targetLibrary.importUri.toString();
if (_disallowedInteropLibrariesInDart2Wasm.contains(
dependencyUriString,
)) {
// TODO(srujzs): While we allow these imports for all `dart:*`
// libraries, we may want to restrict this further, as it may include
// `dart:ui`.
final allowedToImport =
uri.isScheme('dart') ||
(uri.isScheme('package') &&
allowedInteropLibrariesInDart2WasmPackages.any(
(pkg) => uri.pathSegments.first == pkg,
)) ||
_allowedUseOfDart2WasmDisallowedInteropLibrariesTestPatterns.any(
(pattern) => uri.path.contains(pattern),
);
if (allowedToImport) return;
final message =
dependencyUriString == 'dart:ffi'
? messageDartFfiLibraryInDart2Wasm
: templateJsInteropDisallowedInteropLibraryInDart2Wasm
.withArguments(dependencyUriString);
_reporter.report(
message,
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) {
final 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;
// TODO(srujzs): Delete the check for patched member once
// https://github.com/dart-lang/sdk/issues/53367 is resolved.
if (member.isExternal && !JsInteropChecks.isPatchedMember(member)) {
final String 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) {
void report(Message message) => _reporter.report(
message,
node.fileOffset,
node.name.text.length,
node.location?.file,
);
final argument = node.arguments.positional.single;
final functionType = argument.getStaticType(_staticTypeContext);
if (functionType is! FunctionType) {
report(
templateJsInteropFunctionToJSRequiresStaticType.withArguments(
functionType,
),
);
} else {
if (functionType.typeParameters.isNotEmpty) {
report(messageJsInteropFunctionToJSTypeParameters);
}
if (functionType.namedParameters.isNotEmpty) {
report(messageJsInteropFunctionToJSNamedParameters);
}
_reportFunctionToJSInvocationIfNotAllowedFunctionType(functionType, 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, on a non-static interop type, or renamed.
void _checkJsInteropOperator(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,
);
}
}
}
/// 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;
}
/// Return whether [type] can be used on a `dart:js_interop` external member
/// or in the signature of a function that is converted via `toJS`.
bool _isAllowedExternalType(DartType type) {
if (type is VoidType || type is NullType) return true;
if (type is TypeParameterType || type is StructuralParameterType) {
final bound = type.nonTypeParameterBound;
// If it can be used as a representation type of an interop extension
// type, it is okay to be used as a bound.
// TODO(srujzs): We may want to support type parameters with primitive
// bounds that are themselves allowed e.g. `num`. If so, we should handle
// that change in dart2wasm.
if (extensionIndex.isAllowedRepresentationType(bound)) return true;
}
// If it can be used as a representation type of an interop extension type,
// it is okay to be used on an external member.
if (extensionIndex.isAllowedRepresentationType(type)) return true;
// ExternalDartReference is allowed on interop members even though it's not
// an interop type.
if (extensionIndex.isExternalDartReferenceType(type)) return true;
if (type is InterfaceType) {
final cls = type.classNode;
// Primitive types are okay.
if (cls == _coreTypes.boolClass ||
cls == _coreTypes.numClass ||
cls == _coreTypes.doubleClass ||
cls == _coreTypes.intClass ||
cls == _coreTypes.stringClass) {
return true;
}
} else if (type is ExtensionType) {
// Extension types that wrap other allowed types are also okay. Interop
// extension types and ExternalDartReference are handled above, so this is
// essentially for extension types on primitives.
return _isAllowedExternalType(type.extensionTypeErasure);
}
return false;
}
bool _isAllowedExternalFunctionType(FunctionType type) =>
_isAllowedExternalType(type.returnType) &&
type.namedParameters.every((p) => _isAllowedExternalType(p.type)) &&
type.positionalParameters.every((p) => _isAllowedExternalType(p));
String _disallowedExternalFunctionTypeString(FunctionType functionType) {
String typeStringToErrorTypeString(String type) => '*$type*';
String typeToString(DartType type) {
final string = type.toText(_textStrategy);
return _isAllowedExternalType(type)
? string
: typeStringToErrorTypeString(string);
}
String namedTypeToString(NamedType type) {
final string = type.toText(_textStrategy);
return _isAllowedExternalType(type.type)
? string
: typeStringToErrorTypeString(string);
}
final positionalParameterTypeString = functionType.positionalParameters
.map(typeToString)
.join(', ');
final namedParameterTypeString = functionType.namedParameters
.map(namedTypeToString)
.join(', ');
String parameterTypeString;
if (positionalParameterTypeString.isNotEmpty &&
namedParameterTypeString.isNotEmpty) {
parameterTypeString =
'$positionalParameterTypeString, {$namedParameterTypeString}';
} else {
parameterTypeString =
namedParameterTypeString.isNotEmpty
? '{$namedParameterTypeString}'
: positionalParameterTypeString;
}
return '${typeToString(functionType.returnType)} '
'Function($parameterTypeString)';
}
void _reportExternalProcedureIfNotAllowedFunctionType(Procedure node) {
FunctionType functionType;
if (node.isExtensionMember || node.isExtensionTypeMember) {
functionType = extensionIndex.getFunctionType(node)!;
} else {
functionType =
node.signatureType ??
node.function.computeFunctionType(Nullability.nonNullable);
}
final isGetter = extensionIndex.isGetter(node);
final isSetter = extensionIndex.isSetter(node);
if (isGetter || isSetter) {
// There's only one type, so only report that one type instead of a
// function type. This also avoids duplication in reporting external
// fields, which are just a getter and a setter.
final accessorType =
isGetter
? functionType.returnType
: functionType.positionalParameters[0];
if (!_isAllowedExternalType(accessorType)) {
_reporter.report(
templateJsInteropStaticInteropExternalAccessorTypeViolation
.withArguments(accessorType),
node.fileOffset,
node.name.text.length,
node.location?.file,
);
}
} else {
// Methods, operators, constructors, factories.
if (!_isAllowedExternalFunctionType(functionType)) {
_reporter.report(
templateJsInteropStaticInteropExternalFunctionTypeViolation
.withArguments(
_disallowedExternalFunctionTypeString(functionType),
),
node.fileOffset,
node.name.text.length,
node.location?.file,
);
}
}
}
void _reportFunctionToJSInvocationIfNotAllowedFunctionType(
FunctionType functionType,
StaticInvocation invocation,
) {
if (!_isAllowedExternalFunctionType(functionType)) {
_reporter.report(
templateJsInteropStaticInteropToJSFunctionTypeViolation.withArguments(
_disallowedExternalFunctionTypeString(functionType),
),
invocation.fileOffset,
invocation.name.text.length,
invocation.location?.file,
);
}
}
}
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;
}
}