blob: 98bae2455e7a9130a1df345bc736efef20bffd3c [file] [log] [blame]
// Copyright (c) 2025, 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 'dart:collection';
import 'package:code_builder/code_builder.dart';
import 'package:collection/collection.dart';
import '../interop_gen/namer.dart';
import '../js/typescript.types.dart';
import 'base.dart';
import 'builtin.dart';
import 'documentation.dart';
import 'helpers.dart';
import 'types.dart';
abstract class NestableDeclaration extends NamedDeclaration
implements DocumentedDeclaration {
NestableDeclaration? get parent;
String get qualifiedName =>
parent != null ? '${parent!.qualifiedName}.$name' : name;
String get completedDartName => parent != null
? '${parent!.completedDartName}_${dartName ?? name}'
: (dartName ?? name);
}
abstract class ParentDeclaration {
Set<TSNode> get nodes;
}
/// A declaration that defines a type (class or interface)
/// which contains declarations
sealed class TypeDeclaration extends NestableDeclaration
implements ExportableDeclaration {
@override
String name;
@override
String? dartName;
final ID _id;
@override
ID get id => ID(type: _id.type, name: qualifiedName, index: _id.index);
@override
final bool exported;
@override
NestableDeclaration? parent;
final Set<GenericType> typeParameters;
final List<MethodDeclaration> methods;
final List<PropertyDeclaration> properties;
final List<OperatorDeclaration> operators;
final List<ConstructorDeclaration> constructors;
@override
Documentation? documentation;
TypeDeclaration({
required this.name,
this.dartName,
required this.exported,
this.typeParameters = const {},
this.methods = const [],
this.properties = const [],
this.operators = const [],
this.constructors = const [],
this.parent,
this.documentation,
required ID id,
}) : _id = id;
/// [useFirstExtendeeAsRepType] is used to assert that the extension type
/// generated has a representation type of the first member of [extendees]
/// if any.
ExtensionType _emit(
covariant DeclarationOptions? options, {
bool abstract = false,
List<Type> extendees = const [],
List<Type> implementees = const [],
bool useFirstExtendeeAsRepType = false,
bool objectLiteralConstructor = false,
List<Method> extraMethods = const [],
}) {
options ??= DeclarationOptions();
final hierarchy = getMemberHierarchy(this);
final (doc, annotations) = generateFromDocumentation(documentation);
final fieldDecs = <Field>[];
final methodDecs = <Method>[];
bool isOverride(String name) =>
hierarchy.contains(name) && GlobalOptions.redeclareOverrides;
for (final prop in properties.where((p) => p.scope == DeclScope.public)) {
final spec = prop.emit(
options..override = isOverride(prop.dartName ?? prop.name),
);
if (spec is Method) {
methodDecs.add(spec);
} else {
fieldDecs.add(spec as Field);
}
}
methodDecs.addAll(
methods
.where((p) => p.scope == DeclScope.public)
.map(
(m) =>
m.emit(options!..override = isOverride(m.dartName ?? m.name)),
),
);
methodDecs.addAll(
operators
.where((p) => p.scope == DeclScope.public)
.map(
(m) =>
m.emit(options!..override = isOverride(m.dartName ?? m.name)),
),
);
final repType = useFirstExtendeeAsRepType || this is ClassDeclaration
? getRepresentationType(this)
: BuiltinType.primitiveType(PrimitiveType.object, isNullable: false);
return ExtensionType(
(e) => e
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..name = completedDartName
..annotations.addAll([
if (parent != null)
generateJSAnnotation(qualifiedName)
else if (dartName != null && dartName != name)
generateJSAnnotation(name),
])
..primaryConstructorName = '_'
..representationDeclaration = RepresentationDeclaration(
(r) => r
..declaredRepresentationType = repType.emit(
options?.toTypeOptions(),
)
..name = '_',
)
..implements.addAll([
if (extendees.isEmpty && implementees.isEmpty)
refer('JSObject', 'dart:js_interop')
else ...[
...extendees.map((e) => e.emit(options?.toTypeOptions())),
...implementees.map((i) => i.emit(options?.toTypeOptions())),
],
])
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
..constructors.addAll([
if (objectLiteralConstructor)
Constructor(
(c) => c
..external = true
..optionalParameters.addAll(
properties
.where((p) => p.scope == DeclScope.public)
.map(
(p) => Parameter(
(param) => param
..named = true
..name = p.name
..type = p.type.emit(options?.toTypeOptions()),
),
),
),
),
if (!abstract)
if (constructors.isEmpty && this is ClassDeclaration)
ConstructorDeclaration.defaultFor(this).emit(options)
else
...constructors.map((c) => c.emit(options)),
])
..fields.addAll(fieldDecs)
..methods.addAll(methodDecs)
..methods.addAll(extraMethods),
);
}
}
abstract class MemberDeclaration implements DocumentedDeclaration {
late final TypeDeclaration parent;
abstract final DeclScope scope;
String? get name;
}
class VariableDeclaration extends FieldDeclaration
implements ExportableDeclaration, DocumentedDeclaration {
/// The variable modifier, as represented in TypeScript
VariableModifier modifier;
@override
String name;
@override
Type type;
@override
bool exported;
@override
Documentation? documentation;
VariableDeclaration({
required this.name,
required this.type,
required this.modifier,
required this.exported,
this.documentation,
});
@override
ID get id => ID(type: 'var', name: name);
@override
String? dartName;
@override
Spec emit([DeclarationOptions? options]) {
final (doc, annotations) = generateFromDocumentation(documentation);
if (modifier == VariableModifier.$const) {
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([
...annotations,
if (_checkIfDiscardable(type))
refer('doNotStore', 'package:meta/meta.dart'),
])
..name = name
..type = MethodType.getter
..annotations.add(generateJSAnnotation())
..external = true
..static = options?.static ?? false
..returns = type.emit(options?.toTypeOptions()),
);
} else {
// getter and setter -> single variable
return Field(
(f) => f
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..external = true
..static = options?.static ?? false
..name = name
..type = type.emit(options?.toTypeOptions())
..annotations.add(generateJSAnnotation()),
);
}
}
@override
ReferredType<VariableDeclaration> asReferredType([
Iterable<Type>? typeArgs,
bool? isNullable,
String? url,
]) {
return ReferredType<VariableDeclaration>.fromType(
type,
this,
typeParams: typeArgs?.toList() ?? [],
url: url,
isNullable: isNullable ?? false,
);
}
PropertyDeclaration cloneAsProperty({
bool static = false,
DeclScope scope = DeclScope.public,
}) {
return PropertyDeclaration(
name: name,
id: id,
dartName: dartName,
documentation: documentation,
type: type,
static: static,
readonly: modifier == VariableModifier.$const,
scope: scope,
);
}
}
bool _checkIfDiscardable(Type type) {
if (type case BuiltinType(
discardable: final typeIsDiscardable,
) when typeIsDiscardable) {
return true;
}
return false;
}
enum VariableModifier { let, $const, $var }
class FunctionDeclaration extends CallableDeclaration
implements ExportableDeclaration, DocumentedDeclaration {
@override
String name;
@override
String? dartName;
@override
final List<ParameterDeclaration> parameters;
@override
final List<GenericType> typeParameters;
@override
final Type returnType;
@override
bool exported;
@override
ID id;
@override
Documentation? documentation;
FunctionDeclaration({
required this.name,
required this.id,
this.dartName,
this.parameters = const [],
this.typeParameters = const [],
required this.exported,
required this.returnType,
this.documentation,
});
@override
Method emit([DeclarationOptions? options]) {
options ??= DeclarationOptions();
final (doc, annotations) = generateFromDocumentation(documentation);
final (requiredParams, optionalParams) = emitParameters(
parameters,
options,
);
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([
...annotations,
if (_checkIfDiscardable(returnType))
refer('doNotStore', 'package:meta/meta.dart'),
])
..external = true
..name = dartName ?? name
..annotations.add(
generateJSAnnotation(
dartName == null || dartName == name ? null : name,
),
)
..static = options?.static ?? false
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
..returns = returnType.emit(options?.toTypeOptions())
..requiredParameters.addAll(requiredParams)
..optionalParameters.addAll(optionalParams),
);
}
@override
ReferredType<FunctionDeclaration> asReferredType([
IterableBase<Type>? typeArgs,
bool? isNullable,
String? url,
]) {
// TODO: We could do better here and make the function type typed
return ReferredType<FunctionDeclaration>.fromType(
BuiltinType.referred('Function')!,
this,
typeParams: typeArgs?.toList() ?? [],
url: url,
isNullable: isNullable ?? false,
);
}
MethodDeclaration cloneAsMethod({
bool static = false,
DeclScope scope = DeclScope.public,
}) {
return MethodDeclaration(
name: name,
id: id,
dartName: dartName,
static: static,
scope: scope,
documentation: documentation,
parameters: parameters,
typeParameters: typeParameters,
returnType: returnType,
);
}
}
class EnumDeclaration extends NestableDeclaration
implements ExportableDeclaration, DocumentedDeclaration {
@override
String name;
@override
final bool exported;
/// The underlying type of the enum (usually a number)
Type baseType;
final List<EnumMember> members;
@override
String? dartName;
@override
NestableDeclaration? parent;
@override
Documentation? documentation;
EnumDeclaration({
required this.name,
required this.baseType,
required this.members,
required this.exported,
this.dartName,
this.documentation,
});
@override
Spec emit([DeclarationOptions? options]) {
final (doc, annotations) = generateFromDocumentation(documentation);
final baseTypeIsJSType = getJSTypeAlternative(baseType) == baseType;
final externalMember = members.any((m) => m.isExternal);
final shouldUseJSRepType = externalMember || baseTypeIsJSType;
return ExtensionType(
(e) => e
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..annotations.addAll([
if (externalMember)
if (parent != null)
generateJSAnnotation(qualifiedName)
else if (dartName != null && dartName != name)
generateJSAnnotation(name),
])
..constant = !shouldUseJSRepType
..name = completedDartName
..primaryConstructorName = '_'
..representationDeclaration = RepresentationDeclaration(
(r) => r
..declaredRepresentationType =
// if any member doesn't have a value, we have to use external
// so such type should be the JS rep type
(shouldUseJSRepType ? getJSTypeAlternative(baseType) : baseType)
.emit(options?.toTypeOptions())
..name = '_',
)
..fields.addAll(
members.map((member) => member.emit(shouldUseJSRepType)),
),
);
}
@override
ID get id => ID(type: 'enum', name: qualifiedName);
PropertyDeclaration cloneAsProperty({
bool static = false,
DeclScope scope = DeclScope.public,
}) {
return PropertyDeclaration(
name: name,
id: id,
type: asReferredType(),
static: static,
scope: scope,
documentation: documentation,
);
}
}
class EnumMember {
final String name;
final Type? type;
final Object? value;
String parent;
bool get isExternal => value == null;
Documentation? documentation;
EnumMember(
this.name,
this.value, {
this.type,
required this.parent,
this.dartName,
this.documentation,
});
Field emit([bool? shouldUseJSRepType]) {
final jsRep = shouldUseJSRepType ?? (value == null);
final (doc, annotations) = generateFromDocumentation(documentation);
return Field((f) {
f
..docs.addAll([...doc])
..annotations.addAll([...annotations]);
// TODO(nikeokoronkwo): This does not render correctly on `code_builder`.
// Until the update is made, we will omit examples concerning this
// Luckily, not many real-world instances of enums use this anyways, https://github.com/dart-lang/tools/issues/2118
if (!isExternal) {
f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$);
}
if (dartName != null && name != dartName && isExternal) {
f.annotations.add(generateJSAnnotation(name));
}
f
..name = dartName ?? name
..type = refer(parent)
..external = value == null
..static = true
..assignment = value == null
? null
: refer(parent).property('_').call([
jsRep ? literal(value).property('toJS') : literal(value),
]).code;
});
}
String? dartName;
}
class TypeAliasDeclaration extends NestableDeclaration
implements ExportableDeclaration, DocumentedDeclaration {
@override
String name;
final List<GenericType> typeParameters;
final Type type;
@override
String? dartName;
@override
bool exported;
@override
ID get id => ID(type: 'typealias', name: name);
@override
Documentation? documentation;
TypeAliasDeclaration({
required this.name,
this.typeParameters = const [],
required this.type,
required this.exported,
this.documentation,
this.parent,
}) : dartName = null;
@override
TypeDef emit([DeclarationOptions? options]) {
options ??= DeclarationOptions();
final (doc, annotations) = generateFromDocumentation(documentation);
return TypeDef(
(t) => t
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..name = completedDartName
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
..definition = type.emit(options?.toTypeOptions()),
);
}
@override
NestableDeclaration? parent;
}
/// The declaration node for a TypeScript Namespace
// TODO: Refactor into shared class when supporting modules
class NamespaceDeclaration extends NestableDeclaration
implements ExportableDeclaration, ParentDeclaration {
@override
String name;
@override
String? dartName;
final ID _id;
@override
ID get id => ID(type: _id.type, name: qualifiedName, index: _id.index);
@override
bool exported;
@override
NamespaceDeclaration? parent;
final Set<NamespaceDeclaration> namespaceDeclarations;
final Set<Declaration> topLevelDeclarations;
final Set<NestableDeclaration> nestableDeclarations;
@override
Set<TSNode> nodes = {};
@override
Documentation? documentation;
NamespaceDeclaration({
required this.name,
this.exported = true,
required ID id,
this.dartName,
this.topLevelDeclarations = const {},
this.namespaceDeclarations = const {},
this.nestableDeclarations = const {},
this.documentation,
}) : _id = id;
@override
ExtensionType emit([covariant DeclarationOptions? options]) {
options?.static = true;
final (doc, annotations) = generateFromDocumentation(documentation);
// static props and vars
final methods = <Method>[];
final fields = <Field>[];
for (final decl in topLevelDeclarations) {
if (decl case final VariableDeclaration variable) {
if (variable.modifier == VariableModifier.$const) {
methods.add(
variable.emit(options ?? DeclarationOptions(static: true))
as Method,
);
} else {
fields.add(
variable.emit(options ?? DeclarationOptions(static: true)) as Field,
);
}
} else if (decl case final FunctionDeclaration fn) {
methods.add(fn.emit(options ?? DeclarationOptions(static: true)));
}
}
// namespace refs
for (final NamespaceDeclaration(
name: namespaceName,
dartName: namespaceDartName,
)
in namespaceDeclarations) {
methods.add(
Method(
(m) => m
..name = namespaceDartName ?? namespaceName
..annotations.addAll([
generateJSAnnotation('$qualifiedName.$namespaceName'),
])
..type = MethodType.getter
..returns = refer(
'${completedDartName}_${namespaceDartName ?? namespaceName}',
)
..external = true
..static = true,
),
);
}
// class refs
// TODO(nikeokoronkwo): Enum support
for (final nestable in nestableDeclarations) {
switch (nestable) {
case final ClassDeclaration cl:
final constr = _extractConstrFromClass(
cl,
options: options,
parent: this,
);
methods.addAll([if (constr != null) constr]);
break;
default:
break;
}
}
// put them together...
return ExtensionType(
(eType) => eType
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..name = completedDartName
..annotations.addAll([
if (parent != null)
generateJSAnnotation(qualifiedName)
else if (dartName != null && dartName != name)
generateJSAnnotation(name),
])
..implements.add(refer('JSObject', 'dart:js_interop'))
..primaryConstructorName = '_'
..representationDeclaration = RepresentationDeclaration(
(rep) => rep
..name = '_'
..declaredRepresentationType = refer('JSObject', 'dart:js_interop'),
)
..fields.addAll(fields)
..methods.addAll(methods),
);
}
CompositeDeclaration get asComposite =>
CompositeDeclaration.fromNamespace(this);
}
/// A composite declaration is formed from merging declarations together,
/// and is used to represent a JS Object derived from either a namespace or
/// interface, but is no longer strictly one.
///
/// It is NOT derived from the TS AST, and should not be used in such AST cases
class CompositeDeclaration extends TypeDeclaration {
final List<Type> extendedTypes;
final List<Type> implementedTypes;
/// This asserts that the extension type generated produces a rep type
/// other than its default, which is denoted by the first member of
/// [extendedTypes] if any.
final bool useFirstExtendeeAsRepType;
/// This asserts generating a constructor for creating the given interface
/// as an object literal via an object literal constructor
final bool objectLiteralConstructor;
final List<Method> rawMethods;
CompositeDeclaration._({
required super.name,
super.dartName,
required super.id,
super.typeParameters,
super.constructors,
super.methods,
super.properties,
super.operators,
super.documentation,
super.parent,
this.extendedTypes = const [],
this.implementedTypes = const [],
this.useFirstExtendeeAsRepType = false,
this.rawMethods = const [],
}) : objectLiteralConstructor = false,
super(exported: true);
CompositeDeclaration.fromInterface(
InterfaceDeclaration interface, [
this.rawMethods = const [],
]) : extendedTypes = interface.extendedTypes,
implementedTypes = [],
useFirstExtendeeAsRepType = interface.useFirstExtendeeAsRepType,
objectLiteralConstructor = interface.objectLiteralConstructor,
super(
name: interface.name,
dartName: interface.dartName,
exported: true,
id: interface.id,
parent: interface.parent,
typeParameters: interface.typeParameters,
methods: interface.methods,
properties: interface.properties,
operators: interface.operators,
constructors: interface.constructors,
documentation: interface.documentation,
);
factory CompositeDeclaration.fromNamespace(NamespaceDeclaration namespace) {
final methodDeclarations = <MethodDeclaration>[];
final propertyDeclarations = <PropertyDeclaration>[];
// TODO: Enum Support
final methods = <Method>[];
for (final decl in namespace.topLevelDeclarations) {
switch (decl) {
case final FunctionDeclaration fun:
methodDeclarations.add(fun.cloneAsMethod(static: true));
case final VariableDeclaration variable:
propertyDeclarations.add(variable.cloneAsProperty(static: true));
default:
break;
}
}
for (final decl in namespace.nestableDeclarations) {
switch (decl) {
case final EnumDeclaration enumeration:
propertyDeclarations.add(enumeration.cloneAsProperty());
case final ClassDeclaration cl:
final constr = _extractConstrFromClass(cl, parent: namespace);
methods.addAll([if (constr != null) constr]);
default:
break;
}
}
for (final ns in namespace.namespaceDeclarations) {
final NamespaceDeclaration(
name: namespaceName,
id: namespaceId,
dartName: namespaceDartName,
documentation: namespaceDoc,
) = ns;
propertyDeclarations.add(
PropertyDeclaration(
name: namespaceName,
id: namespaceId,
readonly: true,
type: ns.asReferredType(),
static: true,
documentation: namespaceDoc,
),
);
}
return CompositeDeclaration._(
name: namespace.name,
dartName: namespace.dartName,
parent: namespace.parent,
id: namespace.id,
typeParameters: {},
methods: methodDeclarations,
properties: propertyDeclarations,
operators: [],
constructors: [],
documentation: namespace.documentation,
extendedTypes: [],
implementedTypes: [],
useFirstExtendeeAsRepType: true,
rawMethods: methods,
);
}
InterfaceDeclaration get asInterface => InterfaceDeclaration(
name: name,
exported: exported,
id: id,
dartName: dartName,
documentation: documentation,
typeParameters: typeParameters,
properties: properties,
methods: methods,
operators: operators,
constructors: constructors,
extendedTypes: extendedTypes,
useFirstExtendeeAsRepType: useFirstExtendeeAsRepType,
objectLiteralConstructor: objectLiteralConstructor,
);
@override
Spec emit([covariant DeclarationOptions? options]) {
return super._emit(
options,
extendees: extendedTypes,
implementees: implementedTypes,
useFirstExtendeeAsRepType: useFirstExtendeeAsRepType,
objectLiteralConstructor: objectLiteralConstructor,
extraMethods: rawMethods,
);
}
}
/// The declaration node for a TypeScript/JavaScript Class
///
/// ```ts
/// class A {}
/// ```
class ClassDeclaration extends TypeDeclaration {
final bool abstract;
final Type? extendedType;
final List<Type> implementedTypes;
ClassDeclaration({
required super.name,
super.dartName,
this.abstract = false,
required super.exported,
super.typeParameters,
this.extendedType,
this.implementedTypes = const [],
super.constructors,
required super.methods,
required super.properties,
super.operators,
super.documentation,
}) : super(
id: ID(type: 'class', name: name),
);
@override
ExtensionType emit([covariant DeclarationOptions? options]) {
return super._emit(
options,
abstract: abstract,
extendees: [if (extendedType case final extendee?) extendee],
implementees: implementedTypes,
);
}
}
/// The declaration node for a TypeScript [Interface]()
///
/// ```ts
/// interface Movable {
///
/// }
/// ```
class InterfaceDeclaration extends TypeDeclaration {
final List<Type> extendedTypes;
/// This asserts that the extension type generated produces a rep type
/// other than its default, which is denoted by the first member of
/// [extendedTypes] if any.
final bool useFirstExtendeeAsRepType;
/// This asserts generating a constructor for creating the given interface
/// as an object literal via an object literal constructor
final bool objectLiteralConstructor;
InterfaceDeclaration({
required super.name,
required super.exported,
required super.id,
super.dartName,
super.typeParameters,
this.extendedTypes = const [],
super.methods,
super.properties,
super.operators,
super.constructors,
this.useFirstExtendeeAsRepType = false,
this.objectLiteralConstructor = false,
super.documentation,
});
@override
ExtensionType emit([covariant DeclarationOptions? options]) {
return super._emit(
options,
extendees: extendedTypes,
useFirstExtendeeAsRepType: useFirstExtendeeAsRepType,
objectLiteralConstructor: objectLiteralConstructor,
);
}
}
/// The declaration node for a field/property on a [TypeDeclaration]
class PropertyDeclaration extends FieldDeclaration
implements MemberDeclaration {
@override
String name;
@override
final ID id;
@override
String? dartName;
@override
late final TypeDeclaration parent;
@override
final DeclScope scope;
final bool isNullable;
final bool readonly;
final bool static;
@override
Type type;
@override
Documentation? documentation;
PropertyDeclaration({
required this.name,
this.dartName,
required this.id,
required this.type,
this.scope = DeclScope.public,
this.readonly = false,
required this.static,
this.isNullable = false,
this.documentation,
});
@override
Spec emit([covariant DeclarationOptions? options]) {
options ??= DeclarationOptions();
assert(scope == DeclScope.public, 'Only public members can be emitted');
final (doc, annotations) = generateFromDocumentation(documentation);
if (readonly) {
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([
...annotations,
if (_checkIfDiscardable(type))
refer('doNotStore', 'package:meta/meta.dart'),
])
..external = true
..static = static
..name = dartName ?? name
..type = MethodType.getter
..annotations.addAll([
if (dartName != null && dartName != name)
generateJSAnnotation(name),
if (options?.override ?? false) _redeclareExpression,
])
..returns = type.emit(options?.toTypeOptions(nullable: isNullable)),
);
} else {
return Field(
(f) => f
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..external = true
..static = static
..name = dartName ?? name
..annotations.addAll([
if (dartName != null && dartName != name)
generateJSAnnotation(name),
])
..type = type.emit(options?.toTypeOptions(nullable: isNullable)),
);
}
}
}
/// The declaration node for a method on a [TypeDeclaration]
class MethodDeclaration extends CallableDeclaration
implements MemberDeclaration {
@override
String name;
@override
String? dartName;
@override
ID id;
MethodKind? kind;
@override
List<ParameterDeclaration> parameters;
@override
Type returnType;
@override
List<GenericType> typeParameters;
@override
late final TypeDeclaration parent;
@override
final DeclScope scope;
final bool static;
final bool isNullable;
@override
Documentation? documentation;
MethodDeclaration({
required this.name,
this.dartName,
required this.id,
this.kind = MethodKind.none,
this.parameters = const [],
this.typeParameters = const [],
required this.returnType,
this.static = false,
this.scope = DeclScope.public,
this.isNullable = false,
this.documentation,
});
@override
Method emit([covariant DeclarationOptions? options]) {
options ??= DeclarationOptions();
final (doc, annotations) = generateFromDocumentation(documentation);
final (requiredParams, optionalParams) = emitParameters(
parameters,
options,
);
assert(scope == DeclScope.public, 'Only public members can be emitted');
if (isNullable) {
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([
...annotations,
if (_checkIfDiscardable(returnType))
refer('doNotStore', 'package:meta/meta.dart'),
])
..external = true
..name = dartName ?? name
..type = MethodType.getter
..static = static
..annotations.addAll([
if (dartName != null && dartName != name)
generateJSAnnotation(name),
if (options?.override ?? false) _redeclareExpression,
])
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
// TODO(nikeokoronkwo): We can make this function more typed in the future, https://github.com/dart-lang/sdk/issues/54557
..returns = TypeReference(
(t) => t
..symbol = 'JSFunction'
..isNullable = true
..url = 'dart:js_interop',
),
);
}
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..external = true
..name = dartName ?? name
..type = switch (kind) {
MethodKind.getter => MethodType.getter,
MethodKind.setter => MethodType.setter,
_ => null,
}
..static = static
..annotations.addAll([
if (dartName != null && dartName != name) generateJSAnnotation(name),
if (options?.override ?? false) _redeclareExpression,
])
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
..returns = kind == MethodKind.setter
? null
: returnType.emit(options?.toTypeOptions())
..requiredParameters.addAll(requiredParams)
..optionalParameters.addAll(optionalParams),
);
}
}
enum MethodKind { getter, setter, none }
/// The declaration node for a constructor on a [ClassDeclaration]
///
/// ```ts
/// class A {
/// num: number;
///
/// constructor(num: number) {
/// this.num = num;
/// }
/// }
/// ```
// TODO: Suggesting a config option for adding custom constructors (factories)
class ConstructorDeclaration implements MemberDeclaration {
@override
late final TypeDeclaration parent;
@override
final DeclScope scope;
final List<ParameterDeclaration> parameters;
@override
final String? name;
final ID id;
String? dartName;
@override
Documentation? documentation;
ConstructorDeclaration({
this.parameters = const [],
this.name,
String? dartName,
required this.id,
this.scope = DeclScope.public,
this.documentation,
}) : dartName = dartName == 'unnamed' ? null : dartName;
static ConstructorDeclaration defaultFor(TypeDeclaration decl) {
return ConstructorDeclaration(
id: ID(type: 'constructor', name: ''),
)..parent = decl;
}
Constructor emit([covariant DeclarationOptions? options]) {
options ??= DeclarationOptions();
final (doc, annotations) = generateFromDocumentation(documentation);
final (requiredParams, optionalParams) = emitParameters(
parameters,
options,
);
final isFactory = dartName != null && dartName != name;
final constructorName = (dartName ?? name)?.trim();
return Constructor(
(c) => c
..docs.addAll([...doc])
..annotations.addAll([...annotations])
..external = true
..name = constructorName == null || constructorName.isEmpty
? null
: constructorName
..annotations.addAll([
if (name != null && isFactory) generateJSAnnotation(name),
])
..factory = isFactory
..requiredParameters.addAll(requiredParams)
..optionalParameters.addAll(optionalParams),
);
}
}
/// The declaration node for an operator member on a class or interface,
/// usually an indexed accessor for classes, and could be what represents
/// callable/indexable interfaces
class OperatorDeclaration extends CallableDeclaration
implements MemberDeclaration {
@override
String get name => kind.expression;
@override
set name(String? name) {}
OperatorKind kind;
@override
String? dartName;
@override
List<ParameterDeclaration> parameters;
@override
Type returnType;
@override
List<GenericType> typeParameters;
@override
late final TypeDeclaration parent;
@override
final DeclScope scope;
final bool static;
@override
Documentation? documentation;
OperatorDeclaration({
required this.kind,
this.dartName,
this.parameters = const [],
required this.returnType,
this.typeParameters = const [],
this.scope = DeclScope.public,
this.static = false,
this.documentation,
});
@override
Method emit([covariant DeclarationOptions? options]) {
options ??= DeclarationOptions();
final (doc, annotations) = generateFromDocumentation(documentation);
final requiredParams = <Parameter>[];
final optionalParams = <Parameter>[];
for (final p in parameters) {
if (p.variadic) {
throw UnsupportedError(
'Variadic parameters are not supported for '
'operators.',
);
} else if (p.optional) {
optionalParams.add(p.emit(options));
} else {
requiredParams.add(p.emit(options));
}
}
return Method(
(m) => m
..docs.addAll([...doc])
..annotations.addAll([
...annotations,
if (_checkIfDiscardable(returnType))
refer('doNotStore', 'package:meta/meta.dart'),
])
..external = true
..name = 'operator $name'
..types.addAll(
typeParameters.map((t) => t.emit(options?.toTypeOptions())),
)
..returns = returnType.emit(options?.toTypeOptions())
..requiredParameters.addAll(requiredParams)
..optionalParameters.addAll(optionalParams),
);
}
@override
ID get id => ID(type: 'op', name: name);
}
enum OperatorKind {
squareBracket('[]'),
squareBracketSet('[]=');
const OperatorKind(this.expression);
final String expression;
}
Expression get _redeclareExpression =>
refer('redeclare', 'package:meta/meta.dart');
Method? _extractConstrFromClass(
ClassDeclaration classDecl, {
DeclarationOptions? options,
NamespaceDeclaration? parent,
}) {
final qualifiedName = parent?.qualifiedName ?? '';
final completedDartName = parent?.completedDartName ?? '';
final ClassDeclaration(
name: className,
dartName: classDartName,
constructors: constructors,
typeParameters: typeParams,
abstract: abstract,
) = classDecl;
var constr = constructors.firstWhereOrNull(
(c) => c.name == null || c.name == 'unnamed',
);
if (constructors.isEmpty && !abstract) {
constr = ConstructorDeclaration.defaultFor(classDecl);
}
// static call to class constructor
if (constr != null) {
options ??= DeclarationOptions();
final (requiredParams, optionalParams) = emitParameters(
constr.parameters,
options,
);
return Method(
(m) => m
..name = classDartName ?? className
..annotations.addAll([
generateJSAnnotation('$qualifiedName.$className'),
])
..types.addAll(typeParams.map((t) => t.emit(options?.toTypeOptions())))
..requiredParameters.addAll(requiredParams)
..optionalParameters.addAll(optionalParams)
..returns = refer('${completedDartName}_${classDartName ?? className}')
..lambda = true
..static = true
..body = refer(classDecl.completedDartName)
.call(
[
...requiredParams.map((p) => refer(p.name)),
if (optionalParams.isNotEmpty)
...optionalParams.map((p) => refer(p.name)),
],
{},
typeParams.map((t) => t.emit(options?.toTypeOptions())).toList(),
)
.code,
);
}
return null;
}