blob: 3b3d9a757ac51fa48d3e646916021ec040d241df [file] [edit]
// Copyright (c) 2023, 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 '../config/config.dart';
import '../elements/elements.dart';
import '../logging/logging.dart';
import 'visitor.dart';
typedef _Resolver = ClassDecl Function(String? binaryName);
/// A [Visitor] that adds the correct [ClassDecl] references from the
/// string binary names.
///
/// It adds the following references:
/// * Links [ClassDecl] objects from imported dependencies.
/// * Adds references from child elements back to their parent elements.
/// * Resolves Kotlin specific `asyncReturnType` for methods.
class Linker extends Visitor<Classes, Future<void>> with TopLevelVisitor {
@override
final GenerationStage stage = GenerationStage.linker;
Linker(this.config);
final Config config;
@override
Future<void> visit(Classes node) async {
// Specify paths for this package's classes.
final root = config.outputConfig.dartConfig.path;
if (config.outputConfig.dartConfig.structure ==
OutputStructure.singleFile) {
// Connect all to the root if the output is in single file mode.
final path = root.toFilePath();
for (final decl in node.decls.values) {
decl.path = path;
}
} else {
for (final decl in node.decls.values) {
final dollarSign = decl.binaryName.indexOf('\$');
final className = dollarSign != -1
? decl.binaryName.substring(0, dollarSign)
: decl.binaryName;
final path = className.replaceAll('.', '/');
decl.path = root.resolve(path).toFilePath();
}
}
// Find all the imported classes.
await config.importClasses();
if (config.importedClasses.keys
.toSet()
.intersection(node.decls.keys.toSet())
.isNotEmpty) {
log.fatal(
'Trying to re-import the generated classes.\n'
'Try hiding the class(es) in import.',
);
}
for (final className in config.importedClasses.keys) {
log.finest('Imported $className successfully.');
}
ClassDecl resolve(String? binaryName) {
return config.importedClasses[binaryName] ??
node.decls[binaryName] ??
resolve(DeclaredType.object.name);
}
DeclaredType.object.classDecl = resolve(DeclaredType.object.name);
final classLinker = _ClassLinker(
config,
resolve,
);
for (final classDecl in node.decls.values) {
classDecl.accept(classLinker);
}
}
}
class _ClassLinker extends Visitor<ClassDecl, void> {
final Config config;
final _Resolver resolve;
final Set<ClassDecl> linked;
/// Keeps track of the [TypeParam]s that introduced each type variable.
final typeVarOrigin = <String, TypeParam>{};
_ClassLinker(
this.config,
this.resolve,
) : linked = {...config.importedClasses.values};
@override
void visit(ClassDecl node) {
if (linked.contains(node)) return;
log.finest('Linking ${node.binaryName}.');
linked.add(node);
node.outerClass = node.outerClassBinaryName == null
? null
: resolve(node.outerClassBinaryName);
node.outerClass?.accept(this);
// Add type params of outer classes to the nested classes.
final allTypeParams = <TypeParam>[];
if (!node.isStatic) {
allTypeParams.addAll(node.outerClass?.allTypeParams ?? []);
}
allTypeParams.addAll(node.typeParams);
node.allTypeParams = allTypeParams;
for (final typeParam in node.allTypeParams) {
typeVarOrigin[typeParam.name] = typeParam;
}
final typeLinker = _TypeLinker(resolve, typeVarOrigin);
node.superclass ??= DeclaredType.object;
node.superclass!.accept(typeLinker);
final superclass = (node.superclass! as DeclaredType).classDecl;
superclass.accept(this);
final methodSignatures = <String>{};
final methodLinker = _MethodLinker(config, resolve, {...typeVarOrigin});
for (final method in node.methods) {
method.classDecl = node;
method.accept(methodLinker);
methodSignatures.add(method.javaSig);
}
// Add all methods from the superinterfaces of this class.
if (node.methods.isEmpty) {
// Make the list modifiable.
node.methods = [];
}
for (final interface in node.interfaces) {
interface.accept(typeLinker);
if (interface case final DeclaredType interfaceType) {
interfaceType.classDecl.accept(this);
for (final interfaceMethod in interfaceType.classDecl.methods) {
final clonedMethod =
interfaceMethod.clone(until: GenerationStage.linker);
clonedMethod.accept(_MethodMover(
typeVarOrigin: {...typeVarOrigin},
fromType: interfaceType,
toClass: node,
));
if (!methodSignatures.contains(clonedMethod.javaSig)) {
clonedMethod.accept(methodLinker);
methodSignatures.add(clonedMethod.javaSig);
node.methods.add(clonedMethod);
}
}
}
}
final fieldLinker = _FieldLinker(typeLinker);
for (final field in node.fields) {
field.classDecl = node;
field.accept(fieldLinker);
}
for (final typeParam in node.typeParams) {
typeParam.accept(_TypeParamLinker(typeLinker));
typeParam.parent = node;
}
}
}
class _MethodLinker extends Visitor<Method, void> {
_MethodLinker(this.config, this.resolve, this.typeVarOrigin)
: typeLinker = _TypeLinker(resolve, typeVarOrigin);
final Config config;
final _Resolver resolve;
final Map<String, TypeParam> typeVarOrigin;
final _TypeLinker typeLinker;
@override
void visit(Method node) {
final hasOuterClassArg = !node.classDecl.isStatic &&
node.classDecl.isNested &&
(node.isConstructor || node.isStatic);
if (hasOuterClassArg) {
final outerClassTypeParamCount = node.classDecl.allTypeParams.length -
node.classDecl.typeParams.length;
final outerClassTypeParams = [
for (final typeParam
in node.classDecl.allTypeParams.take(outerClassTypeParamCount)) ...[
TypeVar(name: typeParam.name)
..annotations = [if (typeParam.hasNonNull) Annotation.nonNull],
]
];
final outerClassType = DeclaredType(
binaryName: node.classDecl.outerClass!.binaryName,
params: outerClassTypeParams,
// `$outerClass` parameter must not be null.
annotations: [Annotation.nonNull],
);
final param = Param(name: '\$outerClass', type: outerClassType);
final usedDoclet = node.descriptor == null;
// For now the nullity of [node.descriptor] identifies if the doclet
// backend was used and the method would potentially need "unnesting".
// Static methods and constructors of non-static nested classes take an
// instance of their outer class as the first parameter.
//
// This is not accounted for by the **doclet** summarizer, so we
// manually add it as the first parameter.
if (usedDoclet) {
// Make the list modifiable.
if (node.params.isEmpty) node.params = [];
node.params.insert(0, param);
} else {
node.params.first = param;
}
}
for (final typeParam in node.typeParams) {
typeVarOrigin[typeParam.name] = typeParam;
}
node.descriptor = node.accept(_MethodDescriptor(typeVarOrigin));
node.returnType.accept(typeLinker);
// If the type itself does not have nullability annotations, use the
// parent's nullability annotations. Some annotations such as
// `androidx.annotation.NonNull` only get applied to elements but not types.
if (!node.returnType.hasNullabilityAnnotations &&
node.hasNullabilityAnnotations) {
node.returnType.annotations = [
...?node.returnType.annotations,
...?node.annotations,
];
}
final typeParamLinker = _TypeParamLinker(typeLinker);
final paramLinker = _ParamLinker(typeLinker);
for (final typeParam in node.typeParams) {
typeParam.accept(typeParamLinker);
typeParam.parent = node;
}
for (final param in node.params) {
param.accept(paramLinker);
param.method = node;
}
node.asyncReturnType?.accept(typeLinker);
// Fill out operator overloadings.
if (node.kotlinFunction?.isOperator ?? false) {
if (Operator.values.asNameMap()[node.kotlinFunction!.name]
case final operatorKind? when operatorKind.isCompatibleWith(node)) {
node.classDecl.operators[operatorKind] ??= node;
}
}
// Fill out compareTo method of the class used for comparison operators.
if (node.name == 'compareTo' && node.params.length == 1) {
final returnType = node.returnType;
final parameterType = node.params.single.type;
if (parameterType is DeclaredType &&
parameterType.binaryName == node.classDecl.binaryName &&
returnType is PrimitiveType &&
returnType.dartType == 'int') {
node.classDecl.compareTo = node;
}
}
}
}
class _TypeLinker extends TypeVisitor<void> {
const _TypeLinker(this.resolve, this.typeVarOrigin);
final _Resolver resolve;
final Map<String, TypeParam> typeVarOrigin;
@override
void visitDeclaredType(DeclaredType node) {
node.classDecl = resolve(node.binaryName);
for (final param in node.params) {
param.accept(this);
}
}
@override
void visitWildcard(Wildcard node) {
node.superBound?.accept(this);
node.extendsBound?.accept(this);
}
@override
void visitArrayType(ArrayType node) {
node.elementType.accept(this);
}
@override
void visitTypeVar(TypeVar node) {
node.origin = typeVarOrigin[node.name]!;
}
@override
void visitPrimitiveType(PrimitiveType node) {
// Do nothing.
}
@override
void visitNonPrimitiveType(ReferredType node) {
// Do nothing.
}
}
class _FieldLinker extends Visitor<Field, void> {
_FieldLinker(this.typeLinker);
final _TypeLinker typeLinker;
@override
void visit(Field node) {
// If the type itself does not have nullability annotations, use the
// parent's nullability annotations. Some annotations such as
// `androidx.annotation.NonNull` only get applied to elements but not types.
if (!node.type.hasNullabilityAnnotations &&
node.hasNullabilityAnnotations) {
node.type.annotations = [
...?node.type.annotations,
...?node.annotations,
];
}
node.type.accept(typeLinker);
node.type.descriptor =
node.type.accept(_TypeDescriptor(typeLinker.typeVarOrigin));
}
}
class _TypeParamLinker extends Visitor<TypeParam, void> {
_TypeParamLinker(this.typeLinker);
final _TypeLinker typeLinker;
@override
void visit(TypeParam node) {
for (final bound in node.bounds) {
bound.accept(typeLinker);
}
}
}
class _ParamLinker extends Visitor<Param, void> {
_ParamLinker(this.typeLinker);
final _TypeLinker typeLinker;
@override
void visit(Param node) {
// If the type itself does not have nullability annotations, use the
// parent's nullability annotations. Some annotations such as
// `androidx.annotation.NonNull` only get applied to elements but not types.
if (!node.type.hasNullabilityAnnotations &&
node.hasNullabilityAnnotations) {
node.type.annotations = [
...?node.type.annotations,
...?node.annotations,
];
}
node.type.accept(typeLinker);
}
}
/// Once a [Method] is cloned and added to another [ClassDecl], the original
/// type parameters can be replaced with other ones and its `classDecl` will be
/// different. This visitor fixes these issues after the cloning is done.
class _MethodMover extends Visitor<Method, void> {
final Map<String, TypeParam> typeVarOrigin;
final DeclaredType fromType;
final ClassDecl toClass;
_MethodMover({
required this.typeVarOrigin,
required this.fromType,
required this.toClass,
});
@override
void visit(Method node) {
node.classDecl = toClass;
final typeMover = _TypeMover(fromType: fromType);
if (node.returnType is TypeVar) {
node.returnType = typeMover.replaceTypeVar(node.returnType as TypeVar);
} else {
node.returnType.accept(typeMover);
}
for (final param in node.params) {
if (param.type is TypeVar) {
param.type = typeMover.replaceTypeVar(param.type as TypeVar);
} else {
param.type.accept(typeMover);
}
}
for (final typeParam in node.typeParams) {
for (final (i, bound) in typeParam.bounds.indexed) {
if (bound is TypeVar) {
typeParam.bounds[i] = typeMover.replaceTypeVar(bound);
} else {
bound.accept(typeMover);
}
}
}
// Since the types can be changed, the descriptor can be changed as well.
for (final typeParam in node.typeParams) {
typeVarOrigin[typeParam.name] = typeParam;
}
node.descriptor = node.accept(_MethodDescriptor(typeVarOrigin));
}
}
class _TypeMover extends TypeVisitor<void> {
final DeclaredType fromType;
_TypeMover({required this.fromType});
@override
void visitNonPrimitiveType(ReferredType node) {
// Do nothing.
}
ReferredType replaceTypeVar(TypeVar typeVar) {
if (typeVar.origin.parent == fromType.classDecl) {
final index = fromType.classDecl.allTypeParams
.indexWhere((typeParam) => typeParam.name == typeVar.name);
if (index != -1) {
if (index >= fromType.params.length) {
return DeclaredType.object.clone();
}
return fromType.params[index].clone(until: GenerationStage.linker);
}
}
return typeVar;
}
@override
void visitDeclaredType(DeclaredType node) {
for (final (i, typeParam) in node.params.indexed) {
if (typeParam is TypeVar) {
node.params[i] = replaceTypeVar(typeParam);
} else {
typeParam.accept(this);
}
}
}
@override
void visitArrayType(ArrayType node) {
if (node.elementType is TypeVar) {
node.elementType = replaceTypeVar(node.elementType as TypeVar);
} else {
node.elementType.accept(this);
}
}
@override
void visitPrimitiveType(PrimitiveType node) {
// Do nothing.
}
}
/// Generates JNI Method descriptor.
///
/// https://docs.oracle.com/en/java/javase/18/docs/specs/jni/types.html#type-signatures
/// Also see: [_TypeDescriptor]
class _MethodDescriptor extends Visitor<Method, String> {
final Map<String, TypeParam> typeVarOrigin;
_MethodDescriptor(this.typeVarOrigin);
@override
String visit(Method node) {
final s = StringBuffer();
final typeDescriptor = _TypeDescriptor(typeVarOrigin);
s.write('(');
for (final param in node.params) {
final desc = param.type.accept(typeDescriptor);
param.type.descriptor = desc;
s.write(desc);
}
s.write(')');
final returnTypeDesc =
node.returnType.accept(_TypeDescriptor(typeVarOrigin));
node.returnType.descriptor = returnTypeDesc;
s.write(returnTypeDesc);
return s.toString();
}
}
/// JVM representation of type signatures.
///
/// https://docs.oracle.com/en/java/javase/18/docs/specs/jni/types.html#type-signatures
class _TypeDescriptor extends TypeVisitor<String> {
final Map<String, TypeParam> typeVarOrigin;
_TypeDescriptor(this.typeVarOrigin);
@override
String visitArrayType(ArrayType node) {
final inner = node.elementType.accept(this);
return '[$inner';
}
@override
String visitDeclaredType(DeclaredType node) {
final internalName = node.binaryName.replaceAll('.', '/');
return 'L$internalName;';
}
@override
String visitPrimitiveType(PrimitiveType node) {
return node.signature;
}
@override
String visitTypeVar(TypeVar node) {
final typeParam = typeVarOrigin[node.name]!;
return typeParam.bounds.isEmpty
? super.visitTypeVar(node)
: typeParam.bounds.first.accept(this);
}
@override
String visitWildcard(Wildcard node) {
final extendsBound = node.extendsBound?.accept(this);
return extendsBound ?? super.visitWildcard(node);
}
@override
String visitNonPrimitiveType(ReferredType node) {
return 'Ljava/lang/Object;';
}
}