| // 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; | |
| } | |
| } |