blob: 08dc94df132a619bf7b22ca81d8219e4579cf3dd [file]
// Copyright (c) 2026, 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.
/// Domain models for JavaScript interop annotations extracted from
/// `dart:js_interop` and `package:js`.
library;
import 'package:analyzer/dart/element/element.dart';
import 'api_facet.dart';
/// An [ApiFacet] capturing a `@JS([name])` annotation from `dart:js_interop`
/// or `package:js`.
final class JsBindingFacet implements ApiFacet {
@override
String get namespace => 'js';
/// The customized JavaScript symbol name, if provided.
final String? name;
const JsBindingFacet([this.name]);
/// Extracts a `@JS([name])` annotation from [element], or returns `null`
/// if not present.
static JsBindingFacet? fromElement(Element element) {
for (final metadata in _metadataFor(element)) {
for (final annotation in metadata.annotations) {
final constant = annotation.computeConstantValue();
if (constant == null) continue;
final type = constant.type;
if (type == null) continue;
final typeElement = type.element;
if (typeElement is! InterfaceElement) continue;
final uri = typeElement.library.uri.toString();
if (typeElement.name == 'JS' &&
(uri == 'dart:js_interop' || uri == 'package:js/js.dart')) {
final name = constant.getField('name')?.toStringValue();
return JsBindingFacet(name);
}
}
}
return null;
}
factory JsBindingFacet.fromJson(Map<String, dynamic> json) =>
JsBindingFacet(json['name'] as String?);
@override
Map<String, dynamic> toJson() => {
'namespace': namespace,
if (name != null) 'name': name,
};
@override
List<String> get parentheticalSegments => [
if (name != null) 'js: "$name"' else 'js',
];
@override
int compareTo(ApiFacet other) {
final diff = namespace.compareTo(other.namespace);
if (diff != 0) return diff;
if (other is JsBindingFacet) {
return (name ?? '').compareTo(other.name ?? '');
}
return 0;
}
@override
bool operator ==(Object other) =>
identical(this, other) || other is JsBindingFacet && name == other.name;
@override
int get hashCode => Object.hash(namespace, name);
}
/// An [ApiFacet] capturing a `@JSExport([name])` annotation from
/// `dart:js_interop`.
final class JsExportFacet implements ApiFacet {
@override
String get namespace => 'js_export';
/// The customized exported property name, if provided.
final String? name;
const JsExportFacet([this.name]);
/// Extracts a `@JSExport([name])` annotation from [element], or returns
/// `null` if not present.
static JsExportFacet? fromElement(Element element) {
for (final metadata in _metadataFor(element)) {
for (final annotation in metadata.annotations) {
final constant = annotation.computeConstantValue();
if (constant == null) continue;
final type = constant.type;
if (type == null) continue;
final typeElement = type.element;
if (typeElement is! InterfaceElement) continue;
final uri = typeElement.library.uri.toString();
if (typeElement.name == 'JSExport' &&
(uri == 'dart:js_interop' || uri.contains('interop'))) {
final name = constant.getField('name')?.toStringValue();
return JsExportFacet(name);
}
}
}
return null;
}
factory JsExportFacet.fromJson(Map<String, dynamic> json) =>
JsExportFacet(json['name'] as String?);
@override
Map<String, dynamic> toJson() => {
'namespace': namespace,
if (name != null) 'name': name,
};
@override
List<String> get parentheticalSegments => [
if (name != null) 'jsExport: "$name"' else 'jsExport',
];
@override
int compareTo(ApiFacet other) {
final diff = namespace.compareTo(other.namespace);
if (diff != 0) return diff;
if (other is JsExportFacet) {
return (name ?? '').compareTo(other.name ?? '');
}
return 0;
}
@override
bool operator ==(Object other) =>
identical(this, other) || other is JsExportFacet && name == other.name;
@override
int get hashCode => Object.hash(namespace, name);
}
Iterable<Metadata> _metadataFor(Element element) sync* {
yield element.nonSynthetic.metadata;
if (element is PropertyAccessorElement) {
yield element.variable.nonSynthetic.metadata;
}
}