[pkg:js] Add validation for @JSExport and createDartExport

Creates an external createDartExport function in js_util.

Adds a number of checks for the annotation:
- Classes with the annotation should not have value in the annotation
- Classes with the annotation should have at least one instance member
somewhere in the hierarchy
- There are no export name collisions that are unresolvable accounting
for overrides
- Members with this annotation are instance members with a body only

Also adds checks to createDartExport:
- Checks that the type is a Dart class
- Checks that the type is marked as exportable

Change-Id: I52f27275966e9603e88921ce7897b7615178c4d2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/259511
Reviewed-by: Riley Porter <rileyporter@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
This commit is contained in:
Srujan Gaddam 2022-10-24 17:37:09 +00:00 committed by Commit Queue
parent 877713882c
commit 7f93985005
12 changed files with 648 additions and 57 deletions

View file

@ -6973,6 +6973,155 @@ const MessageCode messageJsInteropEnclosingClassJSAnnotationContext =
severity: Severity.context,
problemMessage: r"""This is the enclosing class.""");
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(String name)>
templateJsInteropExportClassNotMarkedExportable =
const Template<Message Function(String name)>(
problemMessageTemplate:
r"""Class '#name' does not have a `@JSExport` on it or any of its members.""",
correctionMessageTemplate:
r"""Use the `@JSExport` annotation on this class.""",
withArguments: _withArgumentsJsInteropExportClassNotMarkedExportable);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name)>
codeJsInteropExportClassNotMarkedExportable =
const Code<Message Function(String name)>(
"JsInteropExportClassNotMarkedExportable",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportClassNotMarkedExportable(String name) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
return new Message(codeJsInteropExportClassNotMarkedExportable,
problemMessage:
"""Class '${name}' does not have a `@JSExport` on it or any of its members.""",
correctionMessage: """Use the `@JSExport` annotation on this class.""",
arguments: {'name': name});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(String name)>
templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue =
const Template<Message Function(String name)>(
problemMessageTemplate:
r"""The value in the `@JSExport` annotation on the class or mixin '#name' will be ignored.""",
correctionMessageTemplate: r"""Remove the value in the annotation.""",
withArguments:
_withArgumentsJsInteropExportDartInterfaceHasNonEmptyJSExportValue);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name)>
codeJsInteropExportDartInterfaceHasNonEmptyJSExportValue =
const Code<Message Function(String name)>(
"JsInteropExportDartInterfaceHasNonEmptyJSExportValue",
severity: Severity.warning);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportDartInterfaceHasNonEmptyJSExportValue(
String name) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
return new Message(codeJsInteropExportDartInterfaceHasNonEmptyJSExportValue,
problemMessage:
"""The value in the `@JSExport` annotation on the class or mixin '${name}' will be ignored.""",
correctionMessage: """Remove the value in the annotation.""",
arguments: {'name': name});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(
String
name)> templateJsInteropExportDisallowedMember = const Template<
Message Function(String name)>(
problemMessageTemplate:
r"""Member '#name' is not a concrete instance member, and therefore can't be exported.""",
correctionMessageTemplate:
r"""Remove the `@JSExport` annotation from the member, and use an instance member to call this member instead.""",
withArguments: _withArgumentsJsInteropExportDisallowedMember);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name)> codeJsInteropExportDisallowedMember =
const Code<Message Function(String name)>(
"JsInteropExportDisallowedMember",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportDisallowedMember(String name) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
return new Message(codeJsInteropExportDisallowedMember,
problemMessage:
"""Member '${name}' is not a concrete instance member, and therefore can't be exported.""",
correctionMessage: """Remove the `@JSExport` annotation from the member, and use an instance member to call this member instead.""",
arguments: {'name': name});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(
String name,
String
string)> templateJsInteropExportMemberCollision = const Template<
Message Function(String name, String string)>(
problemMessageTemplate:
r"""The following class members collide with the same export '#name': #string.""",
correctionMessageTemplate:
r"""Either remove the conflicting members or use a different export name.""",
withArguments: _withArgumentsJsInteropExportMemberCollision);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name, String string)>
codeJsInteropExportMemberCollision =
const Code<Message Function(String name, String string)>(
"JsInteropExportMemberCollision",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportMemberCollision(
String name, String string) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
if (string.isEmpty) throw 'No string provided';
return new Message(codeJsInteropExportMemberCollision,
problemMessage:
"""The following class members collide with the same export '${name}': ${string}.""",
correctionMessage: """Either remove the conflicting members or use a different export name.""",
arguments: {'name': name, 'string': string});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(
String
name)> templateJsInteropExportNoExportableMembers = const Template<
Message Function(String name)>(
problemMessageTemplate:
r"""Class '#name' has no exportable members in the class or the inheritance chain.""",
correctionMessageTemplate:
r"""Using `@JSExport`, annotate at least one instance member with a body or annotate a class that has such a member in the inheritance chain.""",
withArguments: _withArgumentsJsInteropExportNoExportableMembers);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name)>
codeJsInteropExportNoExportableMembers =
const Code<Message Function(String name)>(
"JsInteropExportNoExportableMembers",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportNoExportableMembers(String name) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
return new Message(codeJsInteropExportNoExportableMembers,
problemMessage:
"""Class '${name}' has no exportable members in the class or the inheritance chain.""",
correctionMessage: """Using `@JSExport`, annotate at least one instance member with a body or annotate a class that has such a member in the inheritance chain.""",
arguments: {'name': name});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Null> codeJsInteropExternalExtensionMemberOnTypeInvalid =
messageJsInteropExternalExtensionMemberOnTypeInvalid;

View file

@ -28,12 +28,14 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
templateJsInteropNativeClassInAnnotation,
templateJsInteropStaticInteropTrustTypesUsageNotAllowed,
templateJsInteropStaticInteropTrustTypesUsedWithoutStaticInterop;
import 'package:_js_interop_checks/src/transformations/static_interop_mock_creator.dart';
import 'src/js_interop.dart';
class JsInteropChecks extends RecursiveVisitor {
final CoreTypes _coreTypes;
final DiagnosticReporter<Message, LocatedMessage> _diagnosticsReporter;
final ExportChecker exportChecker;
final Map<String, Class> _nativeClasses;
final _TypeParameterVisitor _typeParameterVisitor = _TypeParameterVisitor();
bool _classHasJSAnnotation = false;
@ -81,7 +83,9 @@ class JsInteropChecks extends RecursiveVisitor {
bool _libraryIsGlobalNamespace = false;
JsInteropChecks(
this._coreTypes, this._diagnosticsReporter, this._nativeClasses);
this._coreTypes, this._diagnosticsReporter, this._nativeClasses)
: exportChecker =
ExportChecker(_diagnosticsReporter, _coreTypes.objectClass);
/// Extract all native class names from the [component].
///
@ -107,6 +111,7 @@ class JsInteropChecks extends RecursiveVisitor {
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.
exportChecker.visitMember(member);
super.defaultMember(member);
}
@ -206,6 +211,9 @@ class JsInteropChecks extends RecursiveVisitor {
}
}
super.visitClass(cls);
// Validate `@JSExport` usage after so we know if the members have the
// annotation.
exportChecker.visitClass(cls);
_classHasAnonymousAnnotation = false;
_classHasJSAnnotation = false;
}
@ -323,6 +331,7 @@ class JsInteropChecks extends RecursiveVisitor {
procedure.fileUri);
}
}
super.visitProcedure(procedure);
}
@override
@ -366,6 +375,12 @@ class JsInteropChecks extends RecursiveVisitor {
}
}
@override
void visitExtension(Extension extension) {
exportChecker.visitExtension(extension);
super.visitExtension(extension);
}
/// Reports an error if [functionNode] has named parameters.
void _checkNoNamedParameters(FunctionNode functionNode) {
// ignore: unnecessary_null_comparison

View file

@ -25,6 +25,11 @@ bool hasStaticInteropAnnotation(Annotatable a) =>
bool hasTrustTypesAnnotation(Annotatable a) =>
a.annotations.any(_isTrustTypesAnnotation);
/// Returns true iff the node has an `@JSExport(...)` annotation from
/// `package:js` or from the internal `dart:_js_annotations`.
bool hasJSExportAnnotation(Annotatable a) =>
a.annotations.any(_isJSExportAnnotation);
/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
@ -59,6 +64,21 @@ List<String> getNativeNames(Annotatable a) {
return nativeClasses;
}
/// If [a] has a `@JSExport('...')` annotation, returns the value inside the
/// parentheses.
///
/// If the class does not have a `@JSExport()` annotation, returns an empty
/// String. Note that a value is guaranteed to exist.
String getJSExportName(Annotatable a) {
String jsExportValue = '';
for (var annotation in a.annotations) {
if (_isJSExportAnnotation(annotation)) {
return _stringAnnotationValues(annotation)[0];
}
}
return jsExportValue;
}
final _packageJs = Uri.parse('package:js/js.dart');
final _internalJs = Uri.parse('dart:_js_annotations');
final _jsHelper = Uri.parse('dart:_js_helper');
@ -86,6 +106,9 @@ bool _isStaticInteropAnnotation(Expression value) =>
bool _isTrustTypesAnnotation(Expression value) =>
_isInteropAnnotation(value, '_TrustTypes');
bool _isJSExportAnnotation(Expression value) =>
_isInteropAnnotation(value, 'JSExport');
/// Returns true if [value] is the `Native` annotation from `dart:_js_helper`.
bool _isNativeAnnotation(Expression value) {
var c = _annotationClass(value);

View file

@ -4,6 +4,8 @@
import 'package:front_end/src/fasta/fasta_codes.dart'
show
templateJsInteropExportInvalidInteropTypeArgument,
templateJsInteropExportInvalidTypeArgument,
templateJsInteropStaticInteropMockMemberNotSubtype,
templateJsInteropStaticInteropMockNotDartInterfaceType,
templateJsInteropStaticInteropMockNotStaticInteropType;
@ -14,16 +16,190 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
show
Message,
LocatedMessage,
templateJsInteropExportClassNotMarkedExportable,
templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue,
templateJsInteropExportDisallowedMember,
templateJsInteropExportMemberCollision,
templateJsInteropExportNoExportableMembers,
templateJsInteropStaticInteropMockMissingOverride,
templateJsInteropStaticInteropMockExternalExtensionMemberConflict;
import 'package:_js_interop_checks/src/js_interop.dart' as js_interop;
class _ExtensionVisitor extends RecursiveVisitor {
final Map<Reference, Extension> staticInteropClassesWithExtensions;
enum _ExportStatus {
EXPORT_ERROR,
NON_EXPORTABLE,
EXPORTABLE,
}
_ExtensionVisitor(this.staticInteropClassesWithExtensions);
class ExportChecker {
final DiagnosticReporter<Message, LocatedMessage> _diagnosticReporter;
final Map<Class, Map<String, Set<Member>>> exportClassToMemberMap = {};
final Map<Class, _ExportStatus> exportStatus = {};
final Class _objectClass;
final Map<Class, Map<String, Member>> _overrideMap = {};
final Map<Reference, Extension> staticInteropClassesWithExtensions = {};
@override
ExportChecker(this._diagnosticReporter, this._objectClass);
/// Calculates the overrides, including inheritance, for [cls].
///
/// Note that we use a map from the unique name (with setter renaming) to
/// avoid duplicate checks on classes, and to store the overrides.
void _collectOverrides(Class cls) {
if (_overrideMap.containsKey(cls)) return;
Map<String, Member> memberMap;
var superclass = cls.superclass;
if (superclass != null && superclass != _objectClass) {
_collectOverrides(superclass);
memberMap = Map.from(_overrideMap[superclass]!);
} else {
memberMap = {};
}
// If this is a mixin application, fetch the members from the mixin.
var demangledCls = cls.isMixinApplication ? cls.mixin : cls;
for (var member in [
...demangledCls.procedures.where((proc) => proc.exportable),
...demangledCls.fields.where((field) => field.exportable)
]) {
var memberName = member.name.text;
if (member is Procedure && member.isSetter) {
memberMap[memberName + '='] = member;
} else {
if (member is Field && !member.isFinal) {
memberMap[memberName + '='] = member;
}
memberMap[memberName] = member;
}
}
_overrideMap[cls] = memberMap;
}
/// Determine if [cls] is exportable, and if so, compute the export members.
///
///
/// Check the following:
/// - If the class has a `@JSExport` annotation, the value should be empty.
/// - If the class has the annotation, it should have at least one exportable
/// member in the class or in any superclass (ignoring `Object`).
/// - Accounting for Dart overrides, the export member map of the class or
/// any of its superclasses do not contain unresolvable name collisions. An
/// explanation of the resolvable collisions is below.
void visitClass(Class cls) {
var classHasJSExport = js_interop.hasJSExportAnnotation(cls);
// If the class doesn't have the annotation or if the class wasn't marked
// when we visited the members and checked their annotations, there's
// nothing to do for this class.
if (!classHasJSExport && exportStatus[cls] != _ExportStatus.EXPORTABLE) {
exportStatus[cls] = _ExportStatus.NON_EXPORTABLE;
return;
}
if (classHasJSExport && js_interop.getJSExportName(cls).isNotEmpty) {
_diagnosticReporter.report(
templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue
.withArguments(cls.name),
cls.fileOffset,
cls.name.length,
cls.location?.file);
exportStatus[cls] = _ExportStatus.EXPORT_ERROR;
}
_collectOverrides(cls);
var allExportableMembers = _overrideMap[cls]!.values.where((member) =>
// Only members that qualify are those that are exportable, and either
// their class has the annotation or they have it themselves.
member.exportable &&
(js_interop.hasJSExportAnnotation(member) ||
js_interop.hasJSExportAnnotation(member.enclosingClass!)));
var exports = <String, Set<Member>>{};
// Store the exportable members.
for (var member in allExportableMembers) {
var exportName = member.exportPropertyName;
exports.putIfAbsent(exportName, () => {}).add(member);
}
// Walk through the export map and determine if there are any unresolvable
// conflicts.
for (var exportName in exports.keys) {
var existingMembers = exports[exportName]!;
if (existingMembers.length == 1) continue;
if (existingMembers.length == 2) {
// There are two instances where you can resolve collisions:
// 1. One of the members is a non-final field, and the other one is
// either a strict getter or a strict setter that overrides part of
// that field.
// 2. One of the members is a strict getter, and the other one is a
// strict setter or vice versa.
// Any other case is an error to have more than 1 member per name.
bool isCollisionOkay(Member m1, Member m2) {
if (m1.isNonFinalField &&
(m2.isStrictGetter || m2.isStrictSetter) &&
// Is an override if the same name and across different classes.
(m1.name.text == m2.name.text &&
m1.enclosingClass != m2.enclosingClass)) {
return true;
} else if (m1.isStrictGetter && m2.isStrictSetter) {
return true;
}
return false;
}
var first = existingMembers.elementAt(0);
var second = existingMembers.elementAt(1);
if (isCollisionOkay(first, second) || isCollisionOkay(second, first)) {
continue;
}
}
// Sort to get deterministic order.
var sortedExistingMembers =
existingMembers.map((member) => member.toString()).toList()..sort();
_diagnosticReporter.report(
templateJsInteropExportMemberCollision.withArguments(
exportName, sortedExistingMembers.join(', ')),
cls.fileOffset,
cls.name.length,
cls.location?.file);
exportStatus[cls] = _ExportStatus.EXPORT_ERROR;
}
if (exports.isEmpty) {
_diagnosticReporter.report(
templateJsInteropExportNoExportableMembers.withArguments(cls.name),
cls.fileOffset,
cls.name.length,
cls.location?.file);
exportStatus[cls] = _ExportStatus.EXPORT_ERROR;
}
exportClassToMemberMap[cls] = exports;
exportStatus[cls] ??= _ExportStatus.EXPORTABLE;
}
/// Check that the [member] can be exportable if it has an annotation, and if
/// so, mark the enclosing class as exportable.
void visitMember(Member member) {
var memberHasJSExportAnnotation = js_interop.hasJSExportAnnotation(member);
var cls = member.enclosingClass;
if (memberHasJSExportAnnotation) {
if (!member.exportable) {
_diagnosticReporter.report(
templateJsInteropExportDisallowedMember
.withArguments(member.name.text),
member.fileOffset,
member.name.text.length,
member.location?.file);
if (cls != null) exportStatus[cls] = _ExportStatus.EXPORT_ERROR;
} else {
// Mark as exportable so we know that the class has an exportable member
// when we process the class later.
if (cls != null) exportStatus[cls] = _ExportStatus.EXPORTABLE;
}
}
}
/// Store the [extension] if the on-type is a `@staticInterop` class.
void visitExtension(Extension extension) {
// TODO(srujzs): This code was written with the assumption there would be
// one single extension per `@staticInterop` class. This is no longer true
@ -35,29 +211,34 @@ class _ExtensionVisitor extends RecursiveVisitor {
staticInteropClassesWithExtensions[onType.className] = extension;
}
}
super.visitExtension(extension);
}
}
// TODO(srujzs): Rename this class and file to focus on exports. Separate out
// the export creation, export validation, and mock validation into three
// separate files to make this cleaner.
class StaticInteropMockCreator extends Transformer {
final Procedure _allowInterop;
final Procedure _callMethod;
final Procedure _createDartExport;
final Procedure _createStaticInteropMock;
final DiagnosticReporter<Message, LocatedMessage> _diagnosticReporter;
late final _ExtensionVisitor _extensionVisitor;
final ExportChecker _exportChecker;
final InterfaceType _functionType;
final Procedure _getProperty;
final Procedure _globalThis;
final InterfaceType _objectType;
final Procedure _setProperty;
final Map<Reference, Extension> _staticInteropClassesWithExtensions = {};
final TypeEnvironment _typeEnvironment;
StaticInteropMockCreator(this._typeEnvironment, this._diagnosticReporter)
StaticInteropMockCreator(
this._typeEnvironment, this._diagnosticReporter, this._exportChecker)
: _allowInterop = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js', 'allowInterop'),
_callMethod = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_util', 'callMethod'),
_createDartExport = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_util', 'createDartExport'),
_createStaticInteropMock = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_util', 'createStaticInteropMock'),
_functionType = _typeEnvironment.coreTypes.functionNonNullableRawType,
@ -67,16 +248,19 @@ class StaticInteropMockCreator extends Transformer {
.getTopLevelProcedure('dart:js_util', 'get:globalThis'),
_objectType = _typeEnvironment.coreTypes.objectNonNullableRawType,
_setProperty = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_util', 'setProperty') {
_extensionVisitor = _ExtensionVisitor(_staticInteropClassesWithExtensions);
}
void processExtensions(Library library) =>
_extensionVisitor.visitLibrary(library);
.getTopLevelProcedure('dart:js_util', 'setProperty');
@override
TreeNode visitStaticInvocation(StaticInvocation node) {
if (node.target == _createDartExport) {
if (_verifyExportable(node)) {
// TODO(srujzs): Create the export by refactoring `_createMock`. For
// now, don't do anything.
}
return node;
}
if (node.target != _createStaticInteropMock) return node;
var typeArguments = node.arguments.types;
assert(typeArguments.length == 2);
var staticInteropType = typeArguments[0];
@ -114,21 +298,12 @@ class StaticInteropMockCreator extends Transformer {
var dartMemberMap = <String, Member>{};
for (var procedure in dartClass.allInstanceProcedures) {
// We only care about concrete instance getters, setters, and methods.
if (procedure.isAbstract ||
procedure.isStatic ||
procedure.isExtensionMember ||
procedure.isFactory) {
continue;
}
var name = procedure.name.text;
// Add a suffix to differentiate getters and setters.
if (procedure.isSetter) name += '=';
dartMemberMap[name] = procedure;
}
for (var field in dartClass.allInstanceFields) {
// We only care about concrete instance fields.
if (field.isAbstract || field.isStatic) continue;
var name = field.name.text;
dartMemberMap[name] = field;
if (!field.isFinal) {
@ -144,7 +319,7 @@ class StaticInteropMockCreator extends Transformer {
staticInteropClass.computeAllNonStaticExternalExtensionMembers(
nameToDescriptors,
descriptorToClass,
_staticInteropClassesWithExtensions,
_exportChecker.staticInteropClassesWithExtensions,
_typeEnvironment);
for (var descriptorName in nameToDescriptors.keys) {
var descriptors = nameToDescriptors[descriptorName]!;
@ -163,7 +338,8 @@ class StaticInteropMockCreator extends Transformer {
var violations = <String>[];
for (var descriptor in descriptorConflicts) {
var cls = descriptorToClass[descriptor]!;
var extension = _staticInteropClassesWithExtensions[cls.reference]!;
var extension =
_exportChecker.staticInteropClassesWithExtensions[cls.reference]!;
var extensionName =
extension.isUnnamedExtension ? 'unnamed' : extension.name;
violations.add("'${cls.name}.$extensionName'");
@ -308,6 +484,52 @@ class StaticInteropMockCreator extends Transformer {
node, nameToDescriptors, descriptorToClass, dartMemberMap);
}
/// Validate that the class provided via `createDartExport` can be exported
/// safely.
///
/// Checks that:
/// - Type argument is a valid Dart interface type.
/// - Type argument is not a JS interop type.
/// - Type argument was not marked as non-exportable.
///
/// If there were no errors with processing the class, returns true.
/// Otherwise, returns false.
bool _verifyExportable(StaticInvocation node) {
var dartType = node.arguments.types[0];
if (dartType is! InterfaceType) {
_diagnosticReporter.report(
templateJsInteropExportInvalidTypeArgument.withArguments(
dartType, true),
node.fileOffset,
node.name.text.length,
node.location?.file);
return false;
}
var dartClass = dartType.classNode;
if (js_interop.hasJSInteropAnnotation(dartClass) ||
js_interop.hasStaticInteropAnnotation(dartClass) ||
js_interop.hasAnonymousAnnotation(dartClass)) {
_diagnosticReporter.report(
templateJsInteropExportInvalidInteropTypeArgument.withArguments(
dartType, true),
node.fileOffset,
node.name.text.length,
node.location?.file);
return false;
}
var exportStatus = _exportChecker.exportStatus[dartClass]!;
if (exportStatus == _ExportStatus.NON_EXPORTABLE) {
_diagnosticReporter.report(
templateJsInteropExportClassNotMarkedExportable
.withArguments(dartClass.name),
node.fileOffset,
node.name.text.length,
node.location?.file);
return false;
}
return exportStatus == _ExportStatus.EXPORTABLE;
}
TreeNode _createMock(
StaticInvocation node,
Map<String, List<ExtensionMemberDescriptor>> nameToDescriptors,
@ -528,22 +750,18 @@ class StaticInteropMockCreator extends Transformer {
}
}
// TODO(srujzs): Remove once we refactor the mock conformance. For now, the
// logic here is semi-duplicated above for exporting.
extension _DartClassExtension on Class {
List<Procedure> get allInstanceProcedures {
var allProcs = <Procedure>[];
Class? cls = this;
// We only care about instance procedures that have a body.
bool isInstanceProcedure(Procedure proc) =>
!proc.isAbstract &&
!proc.isStatic &&
!proc.isExtensionMember &&
!proc.isFactory;
while (cls != null) {
allProcs.addAll(cls.procedures.where(isInstanceProcedure));
allProcs.addAll(cls.procedures.where((proc) => proc.exportable));
// Mixin members override the given superclass' members, but are
// overridden by the class' instance members, so they are inserted next.
if (cls.isMixinApplication) {
allProcs.addAll(cls.mixin.procedures.where(isInstanceProcedure));
allProcs.addAll(cls.mixin.procedures.where((proc) => proc.exportable));
}
cls = cls.superclass;
}
@ -555,11 +773,10 @@ extension _DartClassExtension on Class {
List<Field> get allInstanceFields {
var allFields = <Field>[];
Class? cls = this;
bool isInstanceField(Field field) => !field.isAbstract && !field.isStatic;
while (cls != null) {
allFields.addAll(cls.fields.where(isInstanceField));
allFields.addAll(cls.fields.where((field) => field.exportable));
if (cls.isMixinApplication) {
allFields.addAll(cls.mixin.fields.where(isInstanceField));
allFields.addAll(cls.mixin.fields.where((field) => field.exportable));
}
cls = cls.superclass;
}
@ -675,3 +892,41 @@ extension ExtensionMemberDescriptorExtension on ExtensionMemberDescriptor {
bool get isExternal => (this.member.asProcedure).isExternal;
}
extension ProcedureExtension on Procedure {
// We only care about concrete instance procedures.
bool get exportable =>
!this.isAbstract &&
!this.isStatic &&
!this.isExtensionMember &&
!this.isFactory &&
!this.isExternal &&
this.kind != ProcedureKind.Operator;
}
extension FieldExtension on Field {
// We only care about concrete instance fields.
bool get exportable => !this.isAbstract && !this.isStatic && !this.isExternal;
}
extension MemberExtension on Member {
// Get the property name that this member will be exported as.
String get exportPropertyName {
var rename = js_interop.getJSExportName(this);
return rename.isEmpty ? this.name.text : rename;
}
bool get exportable =>
(this is Procedure && (this as Procedure).exportable) ||
(this is Field && (this as Field).exportable);
// Only a getter and not a setter.
bool get isStrictGetter =>
(this is Procedure && (this as Procedure).isGetter) ||
(this is Field && (this as Field).isFinal);
// Only a setter and not a getter.
bool get isStrictSetter => this is Procedure && (this as Procedure).isSetter;
bool get isNonFinalField => this is Field && !(this as Field).isFinal;
}

View file

@ -156,15 +156,16 @@ class Dart2jsTarget extends Target {
coreTypes,
diagnosticReporter as DiagnosticReporter<Message, LocatedMessage>,
_nativeClasses!);
var staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy), diagnosticReporter);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
// Cache extensions for entire component before creating mock.
for (var library in libraries) {
staticInteropMockCreator.processExtensions(library);
}
// Process and validate first before doing anything with exports.
for (var library in libraries) {
jsInteropChecks.visitLibrary(library);
}
var staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy),
diagnosticReporter,
jsInteropChecks.exportChecker);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
for (var library in libraries) {
staticInteropMockCreator.visitLibrary(library);
// TODO (rileyporter): Merge js_util optimizations with other lowerings
// in the single pass in `transformations/lowering.dart`.

View file

@ -105,15 +105,16 @@ class WasmTarget extends Target {
coreTypes,
diagnosticReporter as DiagnosticReporter<Message, LocatedMessage>,
_nativeClasses!);
final staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy), diagnosticReporter);
final jsUtilOptimizer = JsUtilWasmOptimizer(coreTypes, hierarchy);
// Cache extensions for entire component before creating mock.
for (Library library in interopDependentLibraries) {
staticInteropMockCreator.processExtensions(library);
}
// Process and validate first before doing anything with exports.
for (Library library in interopDependentLibraries) {
jsInteropChecks.visitLibrary(library);
}
final staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy),
diagnosticReporter,
jsInteropChecks.exportChecker);
final jsUtilOptimizer = JsUtilWasmOptimizer(coreTypes, hierarchy);
for (Library library in interopDependentLibraries) {
staticInteropMockCreator.visitLibrary(library);
jsUtilOptimizer.visitLibrary(library);
}

View file

@ -170,16 +170,17 @@ class DevCompilerTarget extends Target {
coreTypes,
diagnosticReporter as DiagnosticReporter<Message, LocatedMessage>,
_nativeClasses!);
var staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy), diagnosticReporter);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
// Cache extensions for entire component before creating mock.
// Process and validate first before doing anything with exports.
for (var library in libraries) {
staticInteropMockCreator.processExtensions(library);
jsInteropChecks.visitLibrary(library);
}
var staticInteropMockCreator = StaticInteropMockCreator(
TypeEnvironment(coreTypes, hierarchy),
diagnosticReporter,
jsInteropChecks.exportChecker);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
for (var library in libraries) {
_CovarianceTransformer(library).transform();
jsInteropChecks.visitLibrary(library);
staticInteropMockCreator.visitLibrary(library);
jsUtilOptimizer.visitLibrary(library);
}

View file

@ -3658,6 +3658,70 @@ Message _withArgumentsInvalidReturnPartNullability(
});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(DartType _type, bool isNonNullableByDefault)>
templateJsInteropExportInvalidInteropTypeArgument = const Template<
Message Function(DartType _type, bool isNonNullableByDefault)>(
problemMessageTemplate:
r"""Type argument '#type' needs to be a non-JS interop type.""",
correctionMessageTemplate:
r"""Use a non-JS interop class that uses `@JSExport` instead.""",
withArguments: _withArgumentsJsInteropExportInvalidInteropTypeArgument);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(DartType _type, bool isNonNullableByDefault)>
codeJsInteropExportInvalidInteropTypeArgument =
const Code<Message Function(DartType _type, bool isNonNullableByDefault)>(
"JsInteropExportInvalidInteropTypeArgument",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportInvalidInteropTypeArgument(
DartType _type, bool isNonNullableByDefault) {
TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault);
List<Object> typeParts = labeler.labelType(_type);
String type = typeParts.join();
return new Message(codeJsInteropExportInvalidInteropTypeArgument,
problemMessage:
"""Type argument '${type}' needs to be a non-JS interop type.""" +
labeler.originMessages,
correctionMessage:
"""Use a non-JS interop class that uses `@JSExport` instead.""",
arguments: {'type': _type});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(DartType _type, bool isNonNullableByDefault)>
templateJsInteropExportInvalidTypeArgument = const Template<
Message Function(DartType _type, bool isNonNullableByDefault)>(
problemMessageTemplate:
r"""Type argument '#type' needs to be an interface type.""",
correctionMessageTemplate:
r"""Use a non-JS interop class that uses `@JSExport` instead.""",
withArguments: _withArgumentsJsInteropExportInvalidTypeArgument);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(DartType _type, bool isNonNullableByDefault)>
codeJsInteropExportInvalidTypeArgument =
const Code<Message Function(DartType _type, bool isNonNullableByDefault)>(
"JsInteropExportInvalidTypeArgument",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropExportInvalidTypeArgument(
DartType _type, bool isNonNullableByDefault) {
TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault);
List<Object> typeParts = labeler.labelType(_type);
String type = typeParts.join();
return new Message(codeJsInteropExportInvalidTypeArgument,
problemMessage:
"""Type argument '${type}' needs to be an interface type.""" +
labeler.originMessages,
correctionMessage:
"""Use a non-JS interop class that uses `@JSExport` instead.""",
arguments: {'type': _type});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(

View file

@ -561,6 +561,20 @@ JsInteropDartClassExtendsJSClass/analyzerCode: Fail # Web compiler specific
JsInteropDartClassExtendsJSClass/example: Fail # Web compiler specific
JsInteropEnclosingClassJSAnnotation/analyzerCode: Fail # Web compiler specific
JsInteropEnclosingClassJSAnnotation/example: Fail # Web compiler specific
JsInteropExportClassNotMarkedExportable/analyzerCode: Fail # Web compiler specific
JsInteropExportClassNotMarkedExportable/example: Fail # Web compiler specific
JsInteropExportDartInterfaceHasNonEmptyJSExportValue/analyzerCode: Fail # Web compiler specific
JsInteropExportDartInterfaceHasNonEmptyJSExportValue/example: Fail # Web compiler specific
JsInteropExportDisallowedMember/analyzerCode: Fail # Web compiler specific
JsInteropExportDisallowedMember/example: Fail # Web compiler specific
JsInteropExportInvalidInteropTypeArgument/analyzerCode: Fail # Web compiler specific
JsInteropExportInvalidInteropTypeArgument/example: Fail # Web compiler specific
JsInteropExportInvalidTypeArgument/analyzerCode: Fail # Web compiler specific
JsInteropExportInvalidTypeArgument/example: Fail # Web compiler specific
JsInteropExportMemberCollision/analyzerCode: Fail # Web compiler specific
JsInteropExportMemberCollision/example: Fail # Web compiler specific
JsInteropExportNoExportableMembers/analyzerCode: Fail # Web compiler specific
JsInteropExportNoExportableMembers/example: Fail # Web compiler specific
JsInteropExternalExtensionMemberOnTypeInvalid/analyzerCode: Fail # Web compiler specific
JsInteropExternalExtensionMemberOnTypeInvalid/example: Fail # Web compiler specific
JsInteropExternalMemberNotJSAnnotated/analyzerCode: Fail # Web compiler specific

View file

@ -5207,6 +5207,35 @@ JsInteropEnclosingClassJSAnnotationContext:
problemMessage: "This is the enclosing class."
severity: CONTEXT
JsInteropExportClassNotMarkedExportable:
problemMessage: "Class '#name' does not have a `@JSExport` on it or any of its members."
correctionMessage: "Use the `@JSExport` annotation on this class."
JsInteropExportDartInterfaceHasNonEmptyJSExportValue:
problemMessage: "The value in the `@JSExport` annotation on the class or mixin '#name' will be ignored."
correctionMessage: "Remove the value in the annotation."
severity: WARNING
JsInteropExportDisallowedMember:
problemMessage: "Member '#name' is not a concrete instance member, and therefore can't be exported."
correctionMessage: "Remove the `@JSExport` annotation from the member, and use an instance member to call this member instead."
JsInteropExportInvalidInteropTypeArgument:
problemMessage: "Type argument '#type' needs to be a non-JS interop type."
correctionMessage: "Use a non-JS interop class that uses `@JSExport` instead."
JsInteropExportInvalidTypeArgument:
problemMessage: "Type argument '#type' needs to be an interface type."
correctionMessage: "Use a non-JS interop class that uses `@JSExport` instead."
JsInteropExportMemberCollision:
problemMessage: "The following class members collide with the same export '#name': #string."
correctionMessage: "Either remove the conflicting members or use a different export name."
JsInteropExportNoExportableMembers:
problemMessage: "Class '#name' has no exportable members in the class or the inheritance chain."
correctionMessage: "Using `@JSExport`, annotate at least one instance member with a body or annotate a class that has such a member in the inheritance chain."
JsInteropExternalExtensionMemberOnTypeInvalid:
problemMessage: "JS interop or Native class required for 'external' extension members."
correctionMessage: "Try adding a JS interop annotation to the on type class of the extension."

View file

@ -23,6 +23,7 @@ augmentations
augmented
b
c
collide
compilercontext.runincontext
compilesdk
constructor(s)
@ -48,6 +49,7 @@ interact
interop
intervening
js_util
jsexport
libraries.json
list.filled
loadlibrary

View file

@ -199,3 +199,40 @@ external Object? dartify(Object? o);
/// implemented, as well as how conflicts are resolved (if they are resolvable).
/// The semantics here tries to conform to the view type specification.
external T createStaticInteropMock<T, U>(U dartMock, [Object? proto = null]);
/// DO NOT USE - THIS IS UNIMPLEMENTED.
///
/// Given a Dart object that is marked exportable, creates a JS object literal
/// that forwards to that Dart class. Look at the `@JSExport` annotation to
/// determine what constitutes "exportable" for a Dart class. The object literal
/// will be a map of export names (which are either the written instance member
/// names or their rename) to their respective Dart instance members.
///
/// For example:
///
/// ```
/// @JSExport()
/// class ExportCounter {
/// int value = 0;
/// String stringify() => value.toString();
/// }
///
/// @JS()
/// @staticInterop
/// class Counter {}
///
/// extension on Counter {
/// external int get value;
/// external set value(int val);
/// external String stringify();
/// }
///
/// ...
///
/// var export = ExportCounter();
/// var counter = createDartExport(export) as Counter;
/// export.value = 1;
/// Expect.isTrue(counter.value, export.value);
/// Expect.isTrue(counter.stringify(), export.stringify());
/// ```
external Object createDartExport<T extends Object>(T dartObject);