blob: 83b5fbbaf691a154112e30e0569568d4bf3c8d4d [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.
import 'package:kernel/kernel.dart';
import 'package:kernel/util/graph.dart' as kernel_graph;
/// Returns true iff the node has an `@JS(...)` annotation from `package:js`,
/// `dart:_js_annotations`, or `dart:_js_interop`. Note that while `package:js`
/// has no annotations any more, it used to, so we still need to support those
/// versions.
bool hasJSInteropAnnotation(Annotatable a) =>
a.annotations.any(_isJSInteropAnnotation);
/// Returns true iff the node has an `@JS(...)` annotation from `package:js` or
/// `dart:_js_annotations`.
bool hasPackageJSAnnotation(Annotatable a) =>
a.annotations.any(_isPackageJSAnnotation);
/// Returns true iff the node has an `@JS(...)` annotation from
/// `dart:js_interop`.
bool hasDartJSInteropAnnotation(Annotatable a) =>
a.annotations.any(_isDartJSInteropAnnotation);
/// Returns true iff the node has an `@anonymous` annotation from `package:js`
/// or `dart:_js_annotations`.
bool hasAnonymousAnnotation(Annotatable a) =>
a.annotations.any(_isAnonymousAnnotation);
/// Returns true iff the node has an `@staticInterop` annotation from
/// `package:js` or `dart:_js_annotations`.
bool hasStaticInteropAnnotation(Annotatable a) =>
a.annotations.any(_isStaticInteropAnnotation);
/// Returns true iff the node has an `@trustTypes` annotation from `package:js`
/// or `dart:_js_annotations`.
bool hasTrustTypesAnnotation(Annotatable a) =>
a.annotations.any(_isTrustTypesAnnotation);
/// Returns true iff the node has an `@JSExport(...)` annotation from
/// `package:js` or `dart:_js_annotations`.
bool hasJSExportAnnotation(Annotatable a) =>
a.annotations.any(_isJSExportAnnotation);
/// Returns true iff the node has an `@Native(...)` annotation from the internal
/// `dart:_js_helper`.
bool hasNativeAnnotation(Annotatable a) =>
a.annotations.any(_isNativeAnnotation);
/// Returns true iff the node has an `@patch` annotation from `dart:_internal`.
bool hasPatchAnnotation(Annotatable a) => a.annotations.any(_isPatchAnnotation);
/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
/// This function only considers `JS` annotations defined in [interopLibraries]
/// or, when that argument is not set, from both `package:js` and
/// `dart:js_interop`.
///
/// If there is none or the class does not have a `@JS()` annotation, returns
/// an empty String.
String getJSName(Annotatable a, {Set<Uri>? interopLibraries}) {
String jsClass = '';
for (var annotation in a.annotations) {
if (_isJSInteropAnnotation(
annotation,
interopLibraries: interopLibraries,
)) {
var jsClasses = stringAnnotationValues(annotation);
if (jsClasses.isNotEmpty) {
jsClass = jsClasses[0];
}
}
}
return jsClass;
}
/// If [a] has a `JS('...')` annotation from `dart:js_interop`, returns the
/// value inside the parentheses.
///
/// If no such annotation exists, returns an empty string.
String getDartJSInteropJSName(Annotatable a) {
return getJSName(a, interopLibraries: {_jsInterop});
}
/// If [a] has a `@Native('...')` annotation, returns the values inside the
/// parentheses.
///
/// If there are none or the class does not have a `@Native()` annotation,
/// returns an empty list. Unlike `@JS()`, the string within `@Native()` is
/// allowed to contain several classes separated by a `,`.
List<String> getNativeNames(Annotatable a) {
List<String> nativeClasses = [];
for (var annotation in a.annotations) {
if (_isNativeAnnotation(annotation)) {
nativeClasses.addAll(stringAnnotationValues(annotation));
}
}
return nativeClasses;
}
/// If [a] has a `@JSExport('...')` annotation, returns the value inside the
/// parentheses.
///
/// If there is no value or the class does not have a `@JSExport()` annotation,
/// returns an empty String.
String getJSExportName(Annotatable a) {
String jsExportValue = '';
for (var annotation in a.annotations) {
if (_isJSExportAnnotation(annotation)) {
var jsExportValues = stringAnnotationValues(annotation);
// TODO(srujzs): Theoretically, this should never be empty as there is a
// default empty value. However, in the modular tests, dart2js modular
// analysis does not see the default value, and reports this as empty in
// some cases. We should investigate why and fix it, but for now, we just
// manually provide the default value.
if (jsExportValues.isNotEmpty) {
jsExportValue = jsExportValues[0];
}
}
}
return jsExportValue;
}
final _packageJs = Uri.parse('package:js/js.dart');
final _internal = Uri.parse('dart:_internal');
final _jsAnnotations = Uri.parse('dart:_js_annotations');
final _jsHelper = Uri.parse('dart:_js_helper');
final _jsInterop = Uri.parse('dart:js_interop');
/// Returns true if [value] is the interop annotation whose class is
/// [annotationClassName] from [interopLibraries].
///
/// If [interopLibraries] is null, we check `package:js`,
/// `dart:_js_annotations`, and `dart:js_interop`.
bool _isInteropAnnotation(
Expression value,
String annotationClassName, {
Set<Uri>? interopLibraries,
}) {
interopLibraries ??= {_packageJs, _jsAnnotations, _jsInterop};
var c = annotationClass(value);
if (c == null || c.name != annotationClassName) return false;
var importUri = c.enclosingLibrary.importUri;
return interopLibraries.contains(importUri);
}
bool _isJSInteropAnnotation(Expression value, {Set<Uri>? interopLibraries}) =>
_isInteropAnnotation(value, 'JS', interopLibraries: interopLibraries);
bool _isPackageJSAnnotation(Expression value) => _isInteropAnnotation(
value,
'JS',
interopLibraries: {_packageJs, _jsAnnotations},
);
bool _isDartJSInteropAnnotation(Expression value) =>
_isInteropAnnotation(value, 'JS', interopLibraries: {_jsInterop});
bool _isAnonymousAnnotation(Expression value) =>
_isInteropAnnotation(value, '_Anonymous');
bool _isStaticInteropAnnotation(Expression value) =>
_isInteropAnnotation(value, '_StaticInterop');
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);
return c != null &&
c.name == 'Native' &&
c.enclosingLibrary.importUri == _jsHelper;
}
/// Returns true if [value] is the `patch` annotation from `dart:_internal`.
bool _isPatchAnnotation(Expression value) {
var c = annotationClass(value);
return c != null &&
c.name == '_Patch' &&
c.enclosingLibrary.importUri == _internal;
}
/// Returns the class of the instance referred to by metadata annotation [node].
///
/// For example:
///
/// - `@JS()` would return the "JS" class in "dart:_js_annotations".
/// - `@anonymous` would return the "_Anonymous" class in
/// "dart:_js_annotations".
/// - `@staticInterop` would return the "_StaticInterop" class in
/// "dart:_js_annotations".
/// - `@Native` would return the "Native" class in "dart:_js_helper".
///
/// This function works regardless of whether the CFE is evaluating constants,
/// or whether the constant is a field reference (such as "anonymous" above).
Class? annotationClass(Expression node) {
if (node is ConstantExpression) {
var constant = node.constant;
if (constant is InstanceConstant) return constant.classNode;
} else if (node is ConstructorInvocation) {
return node.target.enclosingClass;
} else if (node is StaticGet) {
var type = node.target.getterType;
if (type is InterfaceType) return type.classNode;
}
return null;
}
/// Returns the string values inside of a metadata annotation [node].
///
/// For example:
/// - `@JS('Foo')` would return ['Foo'].
/// - `@Native('Foo,Bar')` would return ['Foo', 'Bar'].
///
/// [node] is expected to be an annotation with either StringConstants or
/// StringLiterals that can be made up of multiple values. If there are none,
/// this method returns an empty list. This method throws an assertion if there
/// are multiple arguments or a named arg in the annotation.
List<String> stringAnnotationValues(Expression node) {
List<String> values = [];
if (node is ConstantExpression) {
var constant = node.constant;
if (constant is InstanceConstant) {
var argLength = constant.fieldValues.values.length;
if (argLength == 1) {
var value = constant.fieldValues.values.elementAt(0);
if (value is StringConstant) values.addAll(value.value.split(','));
} else if (argLength > 1) {
throw ArgumentError(
'Method expects annotation with at most one positional argument: '
'$node.',
);
}
}
} else if (node is ConstructorInvocation) {
var argLength = node.arguments.positional.length;
if (argLength > 1 || node.arguments.named.isNotEmpty) {
throw ArgumentError(
'Method expects annotation with at most one positional argument: '
'$node.',
);
} else if (argLength == 1) {
var value = node.arguments.positional[0];
if (value is StringLiteral) {
values.addAll(value.value.split(','));
} else if (value is StaticGet) {
// Sometimes the CFE will translate the following to a StaticGet of a
// const field:
//
// const String fieldName = 'field';
// @JS(fieldName)
//
// In this case we derive the name from the intializer of the referenced
// field.
var target = value.target;
if (target is Field && target.isConst) {
final value = target.initializer;
if (value is StringLiteral) {
values.addAll(value.value.split(','));
}
}
}
}
}
return values;
}
/// Returns the [Library] within [libraries] matching the specified
/// [interopUri] or [null].
Library? _findJsInteropLibrary(List<Library> libraries, Uri interopUri) {
for (Library lib in libraries) {
for (LibraryDependency dependency in lib.dependencies) {
Library targetLibrary = dependency.targetLibrary;
if (targetLibrary.importUri == interopUri) {
return targetLibrary;
}
}
}
return null;
}
/// Calculates the libraries in [component] that transitively import a given js
/// interop library.
///
/// NOTE: This function was based off of
/// `calculateTransitiveImportsOfDartFfiIfUsed` in
/// pkg/vm/lib/transformations/ffi/common.dart.
Set<Library> calculateTransitiveImportsOfJsInteropIfUsed(
List<Library> libraries,
Uri interopUri,
) {
// Check for the presence of [jsInteropLibrary] as a dependency of any of the
// libraries in [component]. We use this to bypass the expensive
// [calculateTransitiveDependenciesOf] call for cases where js interop is
// not used, otherwise we could just use the index of the library instead.
Library? jsInteropLibrary = _findJsInteropLibrary(libraries, interopUri);
if (jsInteropLibrary == null) return const <Library>{};
kernel_graph.LibraryGraph graph = kernel_graph.LibraryGraph(libraries);
Set<Library> result = kernel_graph.calculateTransitiveDependenciesOf(graph, {
jsInteropLibrary,
});
return result;
}