/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Cursor, rootCursor} from "../tree";
import {mapAsync, updateIfChanged} from "../util";
import {Type} from "./type";
export class TypeVisitor
{
protected cursor: Cursor = rootCursor();
async visitList(types: T[] | undefined, p: P): Promise {
if (!types) {
return undefined;
}
return mapAsync(types, t => this.visit(t, p) as Promise);
}
async preVisit(type: Type, p: P): Promise {
return type;
}
async postVisit(type: Type, p: P): Promise {
return type;
}
/**
* By calling this method, you are asserting that you know that the outcome will be non-null
* when the compiler couldn't otherwise prove this to be the case. This method is a shortcut
* for having to assert the non-nullability of the returned type.
*
* @param type A non-null type.
* @param p A state object that passes through the visitor.
* @return A non-null type.
*/
async visitNonNull(type: Type, p: P): Promise {
const t = await this.visit(type, p);
if (!t) {
throw new Error("Expected non-null type");
}
return t;
}
async visit(type: T | undefined, p: P): Promise {
if (!type) {
return undefined;
}
this.cursor = new Cursor(type, this.cursor);
let result = await this.preVisit(type, p);
if (!result) {
this.cursor = this.cursor.parent!;
return undefined;
}
switch (result.kind) {
case Type.Kind.Array:
result = await this.visitArray(result as Type.Array, p);
break;
case Type.Kind.Annotation:
result = await this.visitAnnotation(result as Type.Annotation, p);
break;
case Type.Kind.Class:
result = await this.visitClass(result as Type.Class, p);
break;
case Type.Kind.GenericTypeVariable:
result = await this.visitGenericTypeVariable(result as Type.GenericTypeVariable, p);
break;
case Type.Kind.Intersection:
result = await this.visitIntersection(result as Type.Intersection, p);
break;
case Type.Kind.Union:
result = await this.visitUnion(result as Type.Union, p);
break;
case Type.Kind.Parameterized:
result = await this.visitParameterized(result as Type.Parameterized, p);
break;
case Type.Kind.Primitive:
result = await this.visitPrimitive(result as Type.Primitive, p);
break;
case Type.Kind.Method:
result = await this.visitMethod(result as Type.Method, p);
break;
case Type.Kind.Variable:
result = await this.visitVariable(result as Type.Variable, p);
break;
case Type.Kind.ShallowClass:
result = await this.visitClass(result as Type.ShallowClass, p);
break;
case Type.Kind.Unknown:
result = await this.visitUnknown(result, p);
break;
}
if (result) {
result = await this.postVisit(result, p);
}
this.cursor = this.cursor.parent!;
return result as T;
}
protected async visitUnion(union: Type.Union, p: P): Promise {
return updateIfChanged(union, {
bounds: await mapAsync(union.bounds || [], b => this.visit(b, p)) as Type[],
});
}
protected async visitAnnotation(annotation: Type.Annotation, p: P): Promise {
const newType = await this.visit(annotation.type, p) as Type.FullyQualified;
// Note: values contain element values which themselves contain Type references
const newValues = await mapAsync(annotation.values || [], async v => {
const newElement = await this.visit(v.element, p);
if (v.kind === Type.Kind.SingleElementValue) {
const single = v as Type.Annotation.SingleElementValue;
const newReferenceValue = single.referenceValue
? await this.visit(single.referenceValue, p)
: undefined;
return updateIfChanged(v, {
element: newElement,
referenceValue: newReferenceValue,
});
} else if (v.kind === Type.Kind.ArrayElementValue) {
const array = v as Type.Annotation.ArrayElementValue;
const newReferenceValues = array.referenceValues
? await mapAsync(array.referenceValues, rv => this.visit(rv, p)) as Type[]
: undefined;
return updateIfChanged(v, {
element: newElement,
referenceValues: newReferenceValues,
});
}
return updateIfChanged(v, {element: newElement});
});
return updateIfChanged(annotation, {
type: newType,
values: newValues,
});
}
protected async visitArray(array: Type.Array, p: P): Promise {
return updateIfChanged(array, {
elemType: await this.visit(array.elemType, p),
annotations: await this.visitList(array.annotations, p) || [],
});
}
protected async visitClass(aClass: Type.Class, p: P): Promise {
return updateIfChanged(aClass, {
supertype: await this.visit(aClass.supertype, p),
owningClass: await this.visit(aClass.owningClass, p),
annotations: await mapAsync(aClass.annotations || [], a => this.visit(a, p)),
interfaces: await mapAsync(aClass.interfaces || [], i => this.visit(i, p)),
members: await mapAsync(aClass.members || [], m => this.visit(m, p)),
methods: await mapAsync(aClass.methods || [], m => this.visit(m, p)),
typeParameters: await this.visitList(aClass.typeParameters, p) || [],
});
}
protected async visitGenericTypeVariable(generic: Type.GenericTypeVariable, p: P): Promise {
return updateIfChanged(generic, {
bounds: await mapAsync(generic.bounds || [], b => this.visit(b, p)),
});
}
protected async visitIntersection(intersection: Type.Intersection, p: P): Promise {
return updateIfChanged(intersection, {
bounds: await mapAsync(intersection.bounds || [], b => this.visit(b, p)),
});
}
/**
* This does not visit the declaring type to avoid a visitor cycle.
*
* @param method The method to visit
* @param p Visit context
* @return A method
*/
protected async visitMethod(method: Type.Method, p: P): Promise {
return updateIfChanged(method, {
declaringType: await this.visit(method.declaringType, p),
returnType: await this.visit(method.returnType, p),
parameterTypes: await mapAsync(method.parameterTypes || [], pt => this.visit(pt, p)),
thrownExceptions: await mapAsync(method.thrownExceptions || [], t => this.visit(t, p)),
annotations: await mapAsync(method.annotations || [], a => this.visit(a, p)),
});
}
protected async visitParameterized(parameterized: Type.Parameterized, p: P): Promise {
return updateIfChanged(parameterized, {
type: await this.visit(parameterized.type, p),
typeParameters: await mapAsync(parameterized.typeParameters || [], t => this.visit(t, p)),
});
}
protected async visitPrimitive(primitive: Type.Primitive, p: P): Promise {
// Primitives are immutable singletons, just return as-is
return primitive;
}
/**
* This does not visit the owner to avoid a visitor cycle.
*
* @param variable The variable to visit
* @param p Visit context
* @return A variable
*/
protected async visitVariable(variable: Type.Variable, p: P): Promise {
return updateIfChanged(variable, {
owner: await this.visit(variable.owner, p),
type: await this.visit(variable.type, p),
annotations: await mapAsync(variable.annotations || [], a => this.visit(a, p)),
});
}
protected async visitUnknown(unknown: Type, p: P): Promise {
// Unknown types have no properties to visit
return unknown;
}
}