blob: 5620e4c165464ae30da0823622e5ece25611a2f5 [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 the internal
/// `dart:_js_annotations` or `dart:js_interop`.
bool hasJSInteropAnnotation(Annotatable a) =>
a.annotations.any(_isJSInteropAnnotation);
/// 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 the internal
/// `dart:_js_annotations`.
bool hasAnonymousAnnotation(Annotatable a) =>
a.annotations.any(_isAnonymousAnnotation);
/// Returns true iff the node has an `@staticInterop` annotation from the
/// internal `dart:_js_annotations`.
bool hasStaticInteropAnnotation(Annotatable a) =>
a.annotations.any(_isStaticInteropAnnotation);
/// Returns true iff the node has an `@trustTypes` annotation from the internal
/// `dart:_js_annotations`.
bool hasTrustTypesAnnotation(Annotatable a) =>
a.annotations.any(_isTrustTypesAnnotation);
/// Returns true iff the node has an `@JSExport(...)` annotation from the
/// internal `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 `@ObjectLiteral(...)` annotation from
/// `dart:js_interop`.
bool hasObjectLiteralAnnotation(Annotatable a) =>
a.annotations.any(_isObjectLiteralAnnotation);
/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
/// If there is none or the class does not have a `@JS()` annotation, returns
/// an empty String.
String getJSName(Annotatable a) {
String jsClass = '';
for (var annotation in a.annotations) {
if (_isJSInteropAnnotation(annotation)) {
var jsClasses = stringAnnotationValues(annotation);
if (jsClasses.isNotEmpty) {
jsClass = jsClasses[0];
}
}
}
return jsClass;
}
/// 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 _internalJs = 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 `dart:_js_annotations` or `dart:js_interop`.
///
/// If [dartJsInteropOnly] is true, we only check if it's the annotation from
/// `dart:js_interop`.
bool _isInteropAnnotation(Expression value, String annotationClassName,
{bool dartJsInteropOnly = false}) {
var c = annotationClass(value);
if (c == null || c.name != annotationClassName) return false;
var importUri = c.enclosingLibrary.importUri;
if (dartJsInteropOnly) return importUri == _jsInterop;
return importUri == _internalJs || importUri == _jsInterop;
}
bool _isJSInteropAnnotation(Expression value) =>
_isInteropAnnotation(value, 'JS');
bool _isDartJSInteropAnnotation(Expression value) =>
_isInteropAnnotation(value, 'JS', dartJsInteropOnly: true);
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 `ObjectLiteral` annotation from
/// `dart:js_interop`.
bool _isObjectLiteralAnnotation(Expression value) {
final c = annotationClass(value);
return c != null &&
c.name == 'ObjectLiteral' &&
c.enclosingLibrary.importUri == _jsInterop;
}
/// 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(','));
}
}
return values;
}
/// Returns the [Library] within [component] matching the specified
/// [interopUri] or [null].
Library? _findJsInteropLibrary(Component component, Uri interopUri) {
for (Library lib in component.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.
///
/// Returns null if the given js interop library is not imported.
/// NOTE: This function was based off of
/// `calculateTransitiveImportsOfDartFfiIfUsed` in
/// pkg/vm/lib/transformations/ffi/common.dart.
List<Library>? calculateTransitiveImportsOfJsInteropIfUsed(
Component component, 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(component, interopUri);
if (jsInteropLibrary == null) return null;
kernel_graph.LibraryGraph graph =
kernel_graph.LibraryGraph(component.libraries);
Set<Library> result =
kernel_graph.calculateTransitiveDependenciesOf(graph, {jsInteropLibrary});
return result.toList();
}