Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import org.jacodb.ets.model.EtsNegExpr
import org.jacodb.ets.model.EtsNeverType
import org.jacodb.ets.model.EtsNewArrayExpr
import org.jacodb.ets.model.EtsNewExpr
import org.jacodb.ets.model.EtsNewClassExpr
import org.jacodb.ets.model.EtsSpreadExpansionExpr
import org.jacodb.ets.model.EtsNopStmt
import org.jacodb.ets.model.EtsNotEqExpr
Expand Down Expand Up @@ -371,6 +372,8 @@ class EtsMethodBuilder(
type = type.toEtsType(),
)

is NewClassExprDto -> EtsNewClassExpr(signature.toEtsClassSignature())

is NewArrayExprDto -> EtsNewArrayExpr(
elementType = elementType.toEtsType(),
size = size.toEtsEntity(),
Expand Down
9 changes: 9 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ data class PropertyRefDto(
override val type: TypeDto,
) : RefDto

@Serializable
@SerialName("NewClassExpr")
data class NewClassExprDto(
val signature: ClassSignatureDto,
) : ExprDto {
override val type: TypeDto
get() = ClassValueTypeDto(signature)
}

@Serializable
@SerialName("NewArrayExpr")
data class NewArrayExprDto(
Expand Down
16 changes: 16 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import org.jacodb.ets.toArrayType
interface EtsExpr : EtsEntity {
interface Visitor<out R> {
fun visit(expr: EtsNewExpr): R
fun visit(expr: EtsNewClassExpr): R {
if (this is Default) return defaultVisit(expr)
error("Cannot handle ${expr::class.java.simpleName}: $expr")
}
fun visit(expr: EtsSpreadExpansionExpr): R {
if (this is Default) return defaultVisit(expr)
error("Cannot handle ${expr::class.java.simpleName}: $expr")
Expand Down Expand Up @@ -89,6 +93,7 @@ interface EtsExpr : EtsEntity {

interface Default<out R> : Visitor<R> {
override fun visit(expr: EtsNewExpr): R = defaultVisit(expr)
override fun visit(expr: EtsNewClassExpr): R = defaultVisit(expr)
override fun visit(expr: EtsSpreadExpansionExpr): R = defaultVisit(expr)
override fun visit(expr: EtsToPropertyKeyExpr): R = defaultVisit(expr)
override fun visit(expr: EtsNewArrayExpr): R = defaultVisit(expr)
Expand Down Expand Up @@ -198,6 +203,17 @@ data class EtsToPropertyKeyExpr(val arg: EtsValue) : EtsExpr {
override fun <R> accept(visitor: EtsExpr.Visitor<R>): R = visitor.visit(this)
}

/** Evaluation of a class expression creates a fresh constructor for [signature]. */
data class EtsNewClassExpr(
val signature: EtsClassSignature,
) : EtsExpr {
override val type: EtsType get() = EtsClassValueType(signature)

override fun toString(): String = "class ${signature.name}"

override fun <R> accept(visitor: EtsExpr.Visitor<R>): R = visitor.visit(this)
}

data class EtsNewArrayExpr(
val elementType: EtsType,
val size: EtsEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import org.jacodb.ets.model.EtsMulExpr
import org.jacodb.ets.model.EtsNegExpr
import org.jacodb.ets.model.EtsNewArrayExpr
import org.jacodb.ets.model.EtsNewExpr
import org.jacodb.ets.model.EtsNewClassExpr
import org.jacodb.ets.model.EtsSpreadExpansionExpr
import org.jacodb.ets.model.EtsNopStmt
import org.jacodb.ets.model.EtsNotEqExpr
Expand Down Expand Up @@ -319,4 +320,7 @@ private object EntityGetOperands : EtsEntity.Visitor<Sequence<EtsEntity>> {

override fun visit(expr: EtsSpreadExpansionExpr): Sequence<EtsEntity> =
sequenceOf(expr.iterable)

override fun visit(expr: EtsNewClassExpr): Sequence<EtsEntity> =
emptySequence()
}
5 changes: 5 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Handler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import org.jacodb.ets.model.EtsMulExpr
import org.jacodb.ets.model.EtsNegExpr
import org.jacodb.ets.model.EtsNewArrayExpr
import org.jacodb.ets.model.EtsNewExpr
import org.jacodb.ets.model.EtsNewClassExpr
import org.jacodb.ets.model.EtsSpreadExpansionExpr
import org.jacodb.ets.model.EtsNopStmt
import org.jacodb.ets.model.EtsNotEqExpr
Expand Down Expand Up @@ -494,4 +495,8 @@ abstract class AbstractHandler : EtsEntity.Visitor<Unit>, EtsStmt.Visitor<Unit>
expr.iterable.accept(this)
}

final override fun visit(expr: EtsNewClassExpr) {
handle(expr)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
*/

package org.jacodb.ets.test

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jacodb.ets.dto.AssignStmtDto
import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.dto.NewClassExprDto
import org.jacodb.ets.dto.NewExprDto
import org.jacodb.ets.dto.RawValueDto
import org.jacodb.ets.dto.dtoModule
import org.jacodb.ets.dto.toEtsFile
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsNewClassExpr
import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME
import org.jacodb.ets.utils.EtsIrProvider
import org.jacodb.ets.utils.generateEtsIR
import org.jacodb.ets.utils.getOperands
import org.junit.jupiter.api.Test
import kotlin.io.path.createTempDirectory
import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.test.assertEquals
import kotlin.test.assertTrue


class EtsTsClassExpressionTest {
private fun frontendRoundTrip(source: String): EtsFileDto {
val input = createTempDirectory("ts-calls-test").resolve("test.ts")
input.writeText(source)

val output = generateEtsIR(
input,
isProject = false,
timeout = null,
provider = EtsIrProvider.TS_FRONTEND,
)
val dto = EtsFileDto.loadFromJson(output.readText())
return EtsFileDto.loadFromJson(Json { serializersModule = dtoModule }.encodeToString(dto))
}

private fun assignments(dto: EtsFileDto, method: String): List<AssignStmtDto> =
dto.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
.methods.single { it.signature.name == method }.body!!.cfg.blocks
.flatMap { it.stmts }.filterIsInstance<AssignStmtDto>()

@Test
fun `class expressions retain constructor identity field initialization and visitor support`() {
val dto = frontendRoundTrip(
"""
export function value(): number {
const Local = class { x = 1; };
return new Local().x;
}
""".trimIndent(),
)
val stmts = assignments(dto, "value")
val creation = stmts.single { it.right is NewClassExprDto }.right as NewClassExprDto
val clazz = dto.classes.single { it.signature == creation.signature }
val allocation = stmts.single { it.right is NewExprDto }.right as NewExprDto

assertEquals(creation.signature, (allocation.classType as org.jacodb.ets.dto.ClassTypeDto).signature)
assertTrue(clazz.fields.any { it.signature.name == "x" })
assertTrue(clazz.methods.any { it.signature.name == "%instInit" && it.body != null })
assertTrue(clazz.methods.any { it.signature.name == "constructor" && it.body != null })
assertTrue(stmts.none { it.right is RawValueDto })

val model = dto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
.methods.single { it.name == "value" }.cfg.stmts.filterIsInstance<EtsAssignStmt>()
val newClass = model.single { it.rhv is EtsNewClassExpr }.rhv as EtsNewClassExpr
assertTrue(newClass.getOperands().none())
assertEquals(creation.signature.name, newClass.signature.name)
}

}
6 changes: 6 additions & 0 deletions jacodb-ets/ts-frontend/src/dto/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type ImmediateDto = LocalDto | ConstantDto | ClassValueRefDto;

export type ExprDto =
| NewExprDto
| NewClassExprDto
| SpreadExpansionExprDto
| ToPropertyKeyExprDto
| NewArrayExprDto
Expand Down Expand Up @@ -114,6 +115,11 @@ export interface SpreadExpansionExprDto {
expectedCount: number;
}

export interface NewClassExprDto {
readonly _: "NewClassExpr";
signature: ClassSignatureDto;
}

export interface NewArrayExprDto {
readonly _: "NewArrayExpr";
elementType: TypeDto;
Expand Down
8 changes: 4 additions & 4 deletions jacodb-ets/ts-frontend/src/lowering/classBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ClassBuilder {
// Classes
// ------------------------------------------------------------------

buildClass(decl: ts.ClassDeclaration, nameOverride?: string): ClassDto {
buildClass(decl: ts.ClassLikeDeclaration, nameOverride?: string): ClassDto {
const convertedSignature = this.ctx.converter.classSignatureOf(decl);
const signature = nameOverride === undefined
? convertedSignature
Expand Down Expand Up @@ -458,7 +458,7 @@ export class ClassBuilder {
}

private superClassInfo(
decl: ts.ClassDeclaration,
decl: ts.ClassLikeDeclaration,
): { signature: ClassSignatureDto; constructorDecl?: ts.ConstructorDeclaration } | undefined {
const extended = decl.heritageClauses
?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
Expand Down Expand Up @@ -627,7 +627,7 @@ function hasParameterPropertyModifier(p: ts.ParameterDeclaration): boolean {
);
}

function superClassNameOf(decl: ts.ClassDeclaration): string | undefined {
function superClassNameOf(decl: ts.ClassLikeDeclaration): string | undefined {
for (const clause of decl.heritageClauses ?? []) {
if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) {
return clause.types[0].expression.getText();
Expand All @@ -636,7 +636,7 @@ function superClassNameOf(decl: ts.ClassDeclaration): string | undefined {
return undefined;
}

function implementedInterfacesOf(decl: ts.ClassDeclaration): string[] {
function implementedInterfacesOf(decl: ts.ClassLikeDeclaration): string[] {
for (const clause of decl.heritageClauses ?? []) {
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
return clause.types.map((t) => t.expression.getText());
Expand Down
54 changes: 54 additions & 0 deletions jacodb-ets/ts-frontend/src/lowering/exprLowering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ export class ExprLowerer {
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
return this.lowerClosure(node);
}
if (ts.isClassExpression(node)) {
return this.lowerClassExpression(node);
}
if (ts.isObjectLiteralExpression(node)) {
return this.lowerObjectLiteral(node);
}
Expand Down Expand Up @@ -1346,6 +1349,57 @@ export class ExprLowerer {
// Closures / object literals
// ------------------------------------------------------------------

private lowerClassExpression(node: ts.ClassExpression): ValueDto {
if (node.heritageClauses?.length !== undefined && node.heritageClauses.length > 0) {
throw new LoweringError("class expression heritage is not represented in EtsIR");
}

if (node.name !== undefined || node.members.some((member) =>
ts.isClassStaticBlockDeclaration(member)
|| (ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0
|| (member.name !== undefined && ts.isComputedPropertyName(member.name)))) {
throw new LoweringError("named, static, or computed class expression members are not represented in EtsIR");
}

let capturesLexicalValue = false;
const inspectCapture = (current: ts.Node): void => {
if (ts.isIdentifier(current)) {
const declarations = this.m.converter.symbolOf(current)?.declarations ?? [];
for (const declaration of declarations) {
if (!ts.isVariableDeclaration(declaration) && !ts.isParameter(declaration)
&& !ts.isBindingElement(declaration) && !ts.isFunctionDeclaration(declaration)
&& !ts.isClassDeclaration(declaration)) continue;
let ancestor: ts.Node | undefined = declaration;
let insideClass = false;
let methodLocal = false;
while (ancestor !== undefined) {
if (ancestor === node) insideClass = true;
if (ts.isFunctionLike(ancestor)) methodLocal = true;
ancestor = ancestor.parent;
}
if (!insideClass && methodLocal) capturesLexicalValue = true;
}
}
ts.forEachChild(current, inspectCapture);
};
inspectCapture(node);
if (capturesLexicalValue) {
throw new LoweringError("class expression lexical environment is not represented in EtsIR");
}

const buildClass = this.m.ctx.buildClassExpression;
if (buildClass === undefined) {
throw new LoweringError("class expression in a context without a class builder");
}

const registry = this.m.ctx.anonymous;
const signature = this.m.converter.classSignatureOf(node);
if (!registry.classes.some((clazz) => clazz.signature.name === signature.name)) {
registry.classes.push(buildClass(node, signature.name));
}
return { _: "NewClassExpr", signature };
}

/**
* Arrow function / function expression -> anonymous `%AM<n>$<method>` method
* on the enclosing class; the use-site value is a Local of that name typed
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/ts-frontend/src/lowering/fileBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class FileBuilder {
private readonly fileSignature: FileSignatureDto,
) {
this.classBuilder = new ClassBuilder(ctx);
this.ctx.buildClassExpression = (node, name) => this.classBuilder.buildClass(node, name);
}

build(sourceFile: ts.SourceFile): EtsFileDto {
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/ts-frontend/src/lowering/methodBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface LoweringContext {
isDefaultLibrarySourceFile(sf: ts.SourceFile): boolean;
diagnostics: Diagnostics;
anonymous: AnonymousRegistry;
buildClassExpression?: (node: ts.ClassExpression, name: string) => ClassDto;
/** File/namespace variables represented as static fields of the owning %dflt class. */
moduleFields: Map<ts.Symbol, FieldSignatureDto>;
}
Expand Down
22 changes: 18 additions & 4 deletions jacodb-ets/ts-frontend/src/types/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { decoratorsOf, memberName, modifiersOf, resolvedSymbolOf } from "../lowe
const MAX_DEPTH = 8;

export class TypeConverter {
private readonly classExpressionSignatures = new WeakMap<ts.ClassExpression, ClassSignatureDto>();
readonly structuralClasses: ClassDto[] = [];
private readonly structuralClassByNode = new Map<ts.TypeNode, ClassDto>();
private readonly structuralTypeParametersByNode = new Map<
Expand All @@ -79,6 +80,18 @@ export class TypeConverter {

/** Class-like signature (class / interface / enum / struct) with its namespace chain. */
classSignatureOf(decl: ts.Declaration & { name?: ts.DeclarationName }): ClassSignatureDto {
if (ts.isClassExpression(decl)) {
const registered = this.classExpressionSignatures.get(decl);
if (registered !== undefined) return registered;

const signature: ClassSignatureDto = {
name: `%ACExpr${decl.getStart()}`,
declaringFile: this.fileSignatureFor(decl.getSourceFile()),
};
this.classExpressionSignatures.set(decl, signature);
return signature;
}

const name = decl.name !== undefined && ts.isIdentifier(decl.name)
? decl.name.text
: ts.isClassDeclaration(decl) && (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Default) !== 0
Expand Down Expand Up @@ -599,7 +612,8 @@ export class TypeConverter {
// Instances of project classes/interfaces/enums -> ClassType.
const classDecl = symbol !== undefined ? findClassLikeDeclaration(symbol) : undefined;
if (classDecl !== undefined && isProjectDeclaration(classDecl)) {
if (ts.isClassDeclaration(classDecl) && type.getConstructSignatures().length > 0) {
if ((ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl))
&& type.getConstructSignatures().length > 0) {
return { _: "ClassValueType", signature: this.classSignatureOf(classDecl) };
}
const typeArgs =
Expand Down Expand Up @@ -740,10 +754,10 @@ function isWithin(node: ts.Node, ancestor: ts.Node): boolean {
/** Class-like declaration of a symbol (class / interface / enum). */
function findClassLikeDeclaration(
symbol: ts.Symbol,
): (ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration) | undefined {
): (ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.EnumDeclaration) | undefined {
return symbol.declarations?.find(
(d): d is ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration =>
ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d) || ts.isEnumDeclaration(d),
(d): d is ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.EnumDeclaration =>
ts.isClassDeclaration(d) || ts.isClassExpression(d) || ts.isInterfaceDeclaration(d) || ts.isEnumDeclaration(d),
);
}

Expand Down
Loading
Loading