[pkg:js] Add @staticInterop annotation to package:js

Adds @staticInterop annotation to allow users to declare static interop
classes. Also adds errors for erroneous usage of static interop
classes, like including instance members or using a non-static
supertype.

Change-Id: I21abafbf6ea6c2eb7cd0425f0a54c1ba35d6ec6c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/215010
Reviewed-by: Sigmund Cherem <sigmund@google.com>
This commit is contained in:
Srujan Gaddam
2021-10-06 19:04:02 +00:00
committed by commit-bot@chromium.org
parent d3b0aa2670
commit 67be663490
10 changed files with 186 additions and 36 deletions
@@ -6471,9 +6471,9 @@ const Template<
Message Function(
String name, String name2, String string3)>(
problemMessageTemplate:
r"""JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'.""",
r"""Non-static JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'.""",
correctionMessageTemplate:
r"""Try making the @JS class into an @anonymous class or use js_util on the JS object.""",
r"""Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '#name2'.""",
withArguments: _withArgumentsJsInteropNativeClassInAnnotation);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
@@ -6493,8 +6493,8 @@ Message _withArgumentsJsInteropNativeClassInAnnotation(
if (string3.isEmpty) throw 'No string provided';
return new Message(codeJsInteropNativeClassInAnnotation,
problemMessage:
"""JS interop class '${name}' conflicts with natively supported class '${name2}' in '${string3}'.""",
correctionMessage: """Try making the @JS class into an @anonymous class or use js_util on the JS object.""",
"""Non-static JS interop class '${name}' conflicts with natively supported class '${name2}' in '${string3}'.""",
correctionMessage: """Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '${name2}'.""",
arguments: {'name': name, 'name2': name2, 'string3': string3});
}
@@ -6520,6 +6520,66 @@ const MessageCode messageJsInteropNonExternalMember = const MessageCode(
r"""This JS interop member must be annotated with `external`. Only factories and static methods can be non-external.""",
correctionMessage: r"""Try annotating the member with `external`.""");
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(String name)>
templateJsInteropStaticInteropWithInstanceMembers =
const Template<Message Function(String name)>(
problemMessageTemplate:
r"""JS interop class '#name' with `@staticInterop` annotation cannot declare instance members.""",
correctionMessageTemplate:
r"""Try moving the instance member to a static extension.""",
withArguments: _withArgumentsJsInteropStaticInteropWithInstanceMembers);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name)>
codeJsInteropStaticInteropWithInstanceMembers =
const Code<Message Function(String name)>(
"JsInteropStaticInteropWithInstanceMembers",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropStaticInteropWithInstanceMembers(String name) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
return new Message(codeJsInteropStaticInteropWithInstanceMembers,
problemMessage:
"""JS interop class '${name}' with `@staticInterop` annotation cannot declare instance members.""",
correctionMessage: """Try moving the instance member to a static extension.""",
arguments: {'name': name});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(String name, String name2)>
templateJsInteropStaticInteropWithNonStaticSupertype =
const Template<Message Function(String name, String name2)>(
problemMessageTemplate:
r"""JS interop class '#name' has an `@staticInterop` annotation, but has supertype '#name2', which is non-static.""",
correctionMessageTemplate:
r"""Try marking the supertype as a static interop class using `@staticInterop`.""",
withArguments:
_withArgumentsJsInteropStaticInteropWithNonStaticSupertype);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Code<Message Function(String name, String name2)>
codeJsInteropStaticInteropWithNonStaticSupertype =
const Code<Message Function(String name, String name2)>(
"JsInteropStaticInteropWithNonStaticSupertype",
);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsJsInteropStaticInteropWithNonStaticSupertype(
String name, String name2) {
if (name.isEmpty) throw 'No name provided';
name = demangleMixinApplicationName(name);
if (name2.isEmpty) throw 'No name provided';
name2 = demangleMixinApplicationName(name2);
return new Message(codeJsInteropStaticInteropWithNonStaticSupertype,
problemMessage:
"""JS interop class '${name}' has an `@staticInterop` annotation, but has supertype '${name2}', which is non-static.""",
correctionMessage: """Try marking the supertype as a static interop class using `@staticInterop`.""",
arguments: {'name': name, 'name2': name2});
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(String name)> templateLabelNotFound = const Template<
@@ -19,6 +19,8 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
messageJsInteropNonExternalConstructor,
messageJsInteropNonExternalMember,
templateJsInteropDartClassExtendsJSClass,
templateJsInteropStaticInteropWithInstanceMembers,
templateJsInteropStaticInteropWithNonStaticSupertype,
templateJsInteropJSClassExtendsDartClass,
templateJsInteropNativeClassInAnnotation;
@@ -30,6 +32,7 @@ class JsInteropChecks extends RecursiveVisitor {
final Map<String, Class> _nativeClasses;
bool _classHasJSAnnotation = false;
bool _classHasAnonymousAnnotation = false;
bool _classHasStaticInteropAnnotation = false;
bool _libraryHasJSAnnotation = false;
Map<Reference, Extension>? _libraryExtensionsIndex;
@@ -99,6 +102,7 @@ class JsInteropChecks extends RecursiveVisitor {
void visitClass(Class cls) {
_classHasJSAnnotation = hasJSInteropAnnotation(cls);
_classHasAnonymousAnnotation = hasAnonymousAnnotation(cls);
_classHasStaticInteropAnnotation = hasStaticInteropAnnotation(cls);
var superclass = cls.superclass;
if (superclass != null && superclass != _coreTypes.objectClass) {
var superHasJSAnnotation = hasJSInteropAnnotation(superclass);
@@ -116,11 +120,35 @@ class JsInteropChecks extends RecursiveVisitor {
cls.fileOffset,
cls.name.length,
cls.fileUri);
} else if (_classHasStaticInteropAnnotation) {
if (!hasStaticInteropAnnotation(superclass)) {
_diagnosticsReporter.report(
templateJsInteropStaticInteropWithNonStaticSupertype
.withArguments(cls.name, superclass.name),
cls.fileOffset,
cls.name.length,
cls.fileUri);
}
}
}
// Validate that superinterfaces are all annotated as static as well. Note
// that mixins are already disallowed and therefore are not checked here.
if (_classHasStaticInteropAnnotation) {
for (var supertype in cls.implementedTypes) {
if (!hasStaticInteropAnnotation(supertype.classNode)) {
_diagnosticsReporter.report(
templateJsInteropStaticInteropWithNonStaticSupertype
.withArguments(cls.name, supertype.classNode.name),
cls.fileOffset,
cls.name.length,
cls.fileUri);
}
}
}
// Since this is a breaking check, it is language-versioned.
if (cls.enclosingLibrary.languageVersion >= Version(2, 13) &&
_classHasJSAnnotation &&
!_classHasStaticInteropAnnotation &&
!_classHasAnonymousAnnotation &&
_libraryIsGlobalNamespace) {
var jsClass = getJSName(cls);
@@ -221,6 +249,30 @@ class JsInteropChecks extends RecursiveVisitor {
_checkNoNamedParameters(procedure.function);
}
}
if (_classHasStaticInteropAnnotation &&
procedure.isInstanceMember &&
!procedure.isFactory) {
_diagnosticsReporter.report(
templateJsInteropStaticInteropWithInstanceMembers
.withArguments(procedure.enclosingClass!.name),
procedure.fileOffset,
procedure.name.text.length,
procedure.fileUri);
}
}
@override
void visitField(Field field) {
if (_classHasStaticInteropAnnotation && field.isInstanceMember) {
_diagnosticsReporter.report(
templateJsInteropStaticInteropWithInstanceMembers
.withArguments(field.enclosingClass!.name),
field.fileOffset,
field.name.text.length,
field.fileUri);
}
super.visitField(field);
}
@override
+21 -15
View File
@@ -9,11 +9,16 @@ import 'package:kernel/kernel.dart';
bool hasJSInteropAnnotation(Annotatable a) =>
a.annotations.any(_isPublicJSAnnotation);
/// Returns true iff the node has an `@anonymous(...)` annotation from
/// `package:js` or from the internal `dart:_js_annotations`.
/// Returns true iff the node has an `@anonymous` annotation from `package:js`
/// or from the internal `dart:_js_annotations`.
bool hasAnonymousAnnotation(Annotatable a) =>
a.annotations.any(_isAnonymousAnnotation);
/// Returns true iff the node has an `@staticInterop` annotation from
/// `package:js` or from the internal `dart:_js_annotations`.
bool hasStaticInteropAnnotation(Annotatable a) =>
a.annotations.any(_isStaticInteropAnnotation);
/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
@@ -52,26 +57,26 @@ final _packageJs = Uri.parse('package:js/js.dart');
final _internalJs = Uri.parse('dart:_js_annotations');
final _jsHelper = Uri.parse('dart:_js_helper');
/// Returns true if [value] is the `JS` annotation from `package:js` or from
/// `dart:_js_annotations`.
bool _isPublicJSAnnotation(Expression value) {
/// Returns true if [value] is the interop annotation whose class is
/// [annotationClassName] from `package:js` or from `dart:_js_annotations`.
bool _isInteropAnnotation(Expression value, String annotationClassName) {
var c = _annotationClass(value);
return c != null &&
c.name == 'JS' &&
c.name == annotationClassName &&
(c.enclosingLibrary.importUri == _packageJs ||
c.enclosingLibrary.importUri == _internalJs);
}
/// Returns true if [value] is the `anyonymous` annotation from `package:js` or
/// from `dart:_js_annotations`.
bool _isAnonymousAnnotation(Expression value) {
var c = _annotationClass(value);
return c != null &&
c.name == '_Anonymous' &&
(c.enclosingLibrary.importUri == _packageJs ||
c.enclosingLibrary.importUri == _internalJs);
}
bool _isPublicJSAnnotation(Expression value) =>
_isInteropAnnotation(value, 'JS');
bool _isAnonymousAnnotation(Expression value) =>
_isInteropAnnotation(value, '_Anonymous');
bool _isStaticInteropAnnotation(Expression value) =>
_isInteropAnnotation(value, '_StaticInterop');
/// Returns true if [value] is the `Native` annotation from `dart:_js_helper`.
bool _isNativeAnnotation(Expression value) {
var c = _annotationClass(value);
return c != null &&
@@ -85,6 +90,7 @@ bool _isNativeAnnotation(Expression value) {
///
/// - `@JS()` would return the "JS" class in "package:js".
/// - `@anonymous` would return the "_Anonymous" class in "package:js".
/// - `@staticInterop` would return the "_StaticInterop" class in "package:js".
/// - `@Native` would return the "Native" class in "dart:_js_helper".
///
/// This function works regardless of whether the CFE is evaluating constants,
+4
View File
@@ -523,6 +523,10 @@ JsInteropExternalMemberNotJSAnnotated/analyzerCode: Fail # Web compiler specific
JsInteropExternalMemberNotJSAnnotated/example: Fail # Web compiler specific
JsInteropIndexNotSupported/analyzerCode: Fail # Web compiler specific
JsInteropIndexNotSupported/example: Fail # Web compiler specific
JsInteropStaticInteropWithInstanceMembers/analyzerCode: Fail # Web compiler specific
JsInteropStaticInteropWithInstanceMembers/example: Fail # Web compiler specific
JsInteropStaticInteropWithNonStaticSupertype/analyzerCode: Fail # Web compiler specific
JsInteropStaticInteropWithNonStaticSupertype/example: Fail # Web compiler specific
JsInteropJSClassExtendsDartClass/analyzerCode: Fail # Web compiler specific
JsInteropJSClassExtendsDartClass/example: Fail # Web compiler specific
JsInteropNamedParameters/analyzerCode: Fail # Web compiler specific
+10 -2
View File
@@ -5050,6 +5050,14 @@ JsInteropIndexNotSupported:
problemMessage: "JS interop classes do not support [] and []= operator methods."
correctionMessage: "Try replacing with a normal method."
JsInteropStaticInteropWithInstanceMembers:
problemMessage: "JS interop class '#name' with `@staticInterop` annotation cannot declare instance members."
correctionMessage: "Try moving the instance member to a static extension."
JsInteropStaticInteropWithNonStaticSupertype:
problemMessage: "JS interop class '#name' has an `@staticInterop` annotation, but has supertype '#name2', which is non-static."
correctionMessage: "Try marking the supertype as a static interop class using `@staticInterop`."
JsInteropJSClassExtendsDartClass:
problemMessage: "JS interop class '#name' cannot extend Dart class '#name2'."
correctionMessage: "Try removing the JS interop annotation or adding it to the parent class."
@@ -5059,8 +5067,8 @@ JsInteropNamedParameters:
correctionMessage: "Try replacing them with normal or optional parameters."
JsInteropNativeClassInAnnotation:
problemMessage: "JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'."
correctionMessage: "Try making the @JS class into an @anonymous class or use js_util on the JS object."
problemMessage: "Non-static JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'."
correctionMessage: "Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '#name2'."
JsInteropNonExternalConstructor:
problemMessage: "JS interop classes do not support non-external constructors."
@@ -37,6 +37,7 @@ futureor
guides
h
https
interact
interop
intervening
js_util
@@ -65,6 +66,7 @@ re
sdksummary
solutions
stacktrace
staticinterop
stringokempty
struct<#name
structs
+12
View File
@@ -25,6 +25,10 @@ class _Anonymous {
const _Anonymous();
}
class _StaticInterop {
const _StaticInterop();
}
/// An annotation that indicates a [JS] annotated class is structural and does
/// not have a known JavaScript prototype.
///
@@ -33,3 +37,11 @@ class _Anonymous {
/// desugars to creating a JavaScript object literal with name-value pairs
/// corresponding to the parameter names and values.
const _Anonymous anonymous = _Anonymous();
/// [staticInterop] enables the [JS] annotated class to be treated as a "static"
/// interop class.
///
/// These classes allow interop with native types, like the ones in `dart:html`.
/// These classes should not contain any instance members, inherited or
/// otherwise, and should instead use static extension members.
const _StaticInterop staticInterop = _StaticInterop();
+7 -1
View File
@@ -18,4 +18,10 @@ class _Anonymous {
const _Anonymous();
}
const _Anonymous anonymous = const _Anonymous();
class _StaticInterop {
const _StaticInterop();
}
const _Anonymous anonymous = _Anonymous();
const _StaticInterop staticInterop = _StaticInterop();
@@ -13,29 +13,29 @@ import 'package:js/js.dart';
@JS()
class HTMLDocument {}
// ^
// [web] JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// [web] Non-static JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// Test same annotation name as a native class.
@JS('HTMLDocument')
class HtmlDocument {}
// ^
// [web] JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// [web] Non-static JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// Test annotation name with 'self' and 'window' prefixes.
@JS('self.Window')
class WindowWithSelf {}
// ^
// [web] JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('window.Window')
class WindowWithWindow {}
// ^
// [web] JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('self.window.self.window.self.Window')
class WindowWithMultipleSelfsAndWindows {}
// ^
// [web] JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
// Test annotation with native class name but with a prefix that isn't 'self' or
// 'window'.
@@ -47,14 +47,14 @@ class WindowWithDifferentPrefix {}
@JS()
class DOMWindow {}
// ^
// [web] JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// Test same annotation name as a native class with multiple annotation names
// dart:html.Window uses both "Window" and "DOMWindow".
@JS('DOMWindow')
class DomWindow {}
// ^
// [web] JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// Test different annotation name but with same class name as a @Native class.
@JS('Foo')
@@ -12,27 +12,27 @@ import 'package:js/js.dart';
@JS()
class HTMLDocument {}
// ^
// [web] JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// [web] Non-static JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
@JS('HTMLDocument')
class HtmlDocument {}
// ^
// [web] JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
// [web] Non-static JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
@JS('self.Window')
class WindowWithSelf {}
// ^
// [web] JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('window.Window')
class WindowWithWindow {}
// ^
// [web] JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('self.window.self.window.self.Window')
class WindowWithMultipleSelfsAndWindows {}
// ^
// [web] JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('foo.Window')
class WindowWithDifferentPrefix {}
@@ -40,12 +40,12 @@ class WindowWithDifferentPrefix {}
@JS()
class DOMWindow {}
// ^
// [web] JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('DOMWindow')
class DomWindow {}
// ^
// [web] JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
// [web] Non-static JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
@JS('Foo')
class Window {}