diff --git a/vitest.config.ts b/vitest.config.ts --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,6 +22,7 @@ { extends: true, test: { + globalSetup: ['./src/py-compiler/__tests__/globalTeardown.ts'], name: '@hey-api/openapi-python', root: 'packages/openapi-python', setupFiles: ['./vitest.setup.ts'], @@ -30,6 +31,7 @@ { extends: true, test: { + globalSetup: ['./src/ts-compiler/__tests__/globalTeardown.ts'], name: '@hey-api/openapi-ts', root: 'packages/openapi-ts', setupFiles: ['./vitest.setup.ts'], diff --git a/packages/openapi-python/src/py-compiler/index.ts b/packages/openapi-python/src/py-compiler/index.ts --- a/packages/openapi-python/src/py-compiler/index.ts +++ b/packages/openapi-python/src/py-compiler/index.ts @@ -24,7 +24,10 @@ import type { PyKeywordArgument as _PyKeywordArgument } from './nodes/expressions/keywordArg'; import type { PyLambdaExpression as _PyLambdaExpression } from './nodes/expressions/lambda'; import type { PyListExpression as _PyListExpression } from './nodes/expressions/list'; -import type { PyLiteral as _PyLiteral } from './nodes/expressions/literal'; +import type { + PyLiteral as _PyLiteral, + PyLiteralValue as _PyLiteralValue, +} from './nodes/expressions/literal'; import type { PyMemberExpression as _PyMemberExpression } from './nodes/expressions/member'; import type { PySetExpression as _PySetExpression } from './nodes/expressions/set'; import type { PySubscriptExpression as _PySubscriptExpression } from './nodes/expressions/subscript'; @@ -129,6 +132,9 @@ // Printer export type PrinterOptions = _PyPrinterOptions; + + // Miscellaneous + export type LiteralValue = _PyLiteralValue; } export const py = { diff --git a/packages/openapi-python/src/py-compiler/printer.ts b/packages/openapi-python/src/py-compiler/printer.ts --- a/packages/openapi-python/src/py-compiler/printer.ts +++ b/packages/openapi-python/src/py-compiler/printer.ts @@ -5,10 +5,11 @@ indentSize?: number; } +const DEFAULT_INDENT_SIZE = 4; const PARAMS_MULTILINE_THRESHOLD = 3; export function createPrinter(options?: PyPrinterOptions) { - const indentSize = options?.indentSize ?? 4; + const indentSize = options?.indentSize ?? DEFAULT_INDENT_SIZE; let indentLevel = 0; @@ -52,12 +53,12 @@ switch (node.kind) { case PyNodeKind.Assignment: { const target = printNode(node.target); - if (node.annotation) { - const annotation = printNode(node.annotation); + if (node.type) { + const type = printNode(node.type); if (node.value) { - parts.push(printLine(`${target}: ${annotation} = ${printNode(node.value)}`)); + parts.push(printLine(`${target}: ${type} = ${printNode(node.value)}`)); } else { - parts.push(printLine(`${target}: ${annotation}`)); + parts.push(printLine(`${target}: ${type}`)); } } else { parts.push(printLine(`${target} = ${printNode(node.value!)}`)); @@ -182,7 +183,7 @@ const defPrefix = modifiers ? `${modifiers} def` : 'def'; const formatParameter = (parameter: (typeof node.parameters)[number]): string => { const children: Array = [parameter.name]; - if (parameter.annotation) children.push(`: ${printNode(parameter.annotation)}`); + if (parameter.type) children.push(`: ${printNode(parameter.type)}`); if (parameter.defaultValue) children.push(` = ${printNode(parameter.defaultValue)}`); return children.join(''); }; @@ -272,7 +273,7 @@ case PyNodeKind.LambdaExpression: { const parameters = node.parameters.map((parameter) => { const children: Array = [parameter.name]; - if (parameter.annotation) children.push(`: ${printNode(parameter.annotation)}`); + if (parameter.type) children.push(`: ${printNode(parameter.type)}`); if (parameter.defaultValue) children.push(` = ${printNode(parameter.defaultValue)}`); return children.join(''); }); diff --git a/packages/openapi-ts/src/ts-compiler/index.ts b/packages/openapi-ts/src/ts-compiler/index.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/index.ts @@ -0,0 +1,56 @@ +import type { TsNode as _TsNode, TsNodeBase as _TsNodeBase } from './nodes/base'; +import type { TsExpression as _TsExpression } from './nodes/expression'; +import type { TsIdentifier as _TsIdentifier } from './nodes/expressions/identifier'; +import type { + TsLiteral as _TsLiteral, + TsLiteralValue as _TsLiteralValue, +} from './nodes/expressions/literal'; +import { factory } from './nodes/factory'; +import { TsNodeKind } from './nodes/kinds'; +import type { TsStatement as _TsStatement } from './nodes/statement'; +import type { TsAssignment as _TsAssignment } from './nodes/statements/assignment'; +import type { TsVariableStatement as _TsVariableStatement } from './nodes/statements/var'; +import type { TsSourceFile } from './nodes/structure/sourceFile'; +import type { TsType as _TsType } from './nodes/type'; +import type { TsPrinterOptions as _TsPrinterOptions } from './printer'; +import { createPrinter, printAst } from './printer'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace ts { + // Base / Core + export type Node = _TsNode; + export type NodeBase = _TsNodeBase; + export type NodeKind = TsNodeKind; + export type Expression = _TsExpression; + export type Statement = _TsStatement; + export type Type = _TsType; + + // Structure + export type SourceFile = TsSourceFile; + + // Declarations + // ... + + // Statements + export type Assignment = _TsAssignment; + export type VariableStatement = _TsVariableStatement; + + // Expressions + export type Identifier = _TsIdentifier; + export type Literal = _TsLiteral; + + // Printer + export type PrinterOptions = _TsPrinterOptions; + + // Miscellaneous + export type LiteralValue = _TsLiteralValue; +} + +export const ts = { + TsNodeKind, + createPrinter, + factory, + printAst, +} as const; + +export { factory }; diff --git a/packages/openapi-ts/src/ts-compiler/printer.ts b/packages/openapi-ts/src/ts-compiler/printer.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/printer.ts @@ -0,0 +1,130 @@ +import type { TsNode } from './nodes/base'; +import { TsNodeKind } from './nodes/kinds'; + +export interface TsPrinterOptions { + /** + * Number of spaces per indentation level. + * + * @default 2 + */ + indentSize?: number; + /** + * Whether to add trailing semicolons to statements. + * + * @default true + */ + semicolons?: boolean; +} + +const DEFAULT_INDENT_SIZE = 2; +const DEFAULT_SEMICOLONS = true; + +export function createPrinter(options?: TsPrinterOptions) { + const indentSize = options?.indentSize ?? DEFAULT_INDENT_SIZE; + const semicolons = options?.semicolons ?? DEFAULT_SEMICOLONS; + + let indentLevel = 0; + + function printComments( + parts: Array, + lines: ReadonlyArray, + indent?: boolean, + ): void { + if (indent) indentLevel += 1; + parts.push(...lines.map((line) => printLine(`// ${line}`))); + if (indent) indentLevel -= 1; + } + + function printLine(line: string): string { + if (line === '') return ''; + return ' '.repeat(indentLevel * indentSize) + line; + } + + function printNode(node: TsNode): string { + const parts: Array = []; + + if (node.leadingComments) { + printComments(parts, node.leadingComments); + } + + switch (node.kind) { + case TsNodeKind.Assignment: { + const target = printNode(node.target); + if (node.type) { + const type = printNode(node.type); + if (node.value) { + parts.push(printLine(`${target}: ${type} = ${printNode(node.value)}`)); + } else { + parts.push(printLine(`${target}: ${type}`)); + } + } else { + parts.push(printLine(`${target} = ${printNode(node.value!)}`)); + } + if (semicolons) { + const lastIndex = parts.length - 1; + parts[lastIndex] += ';'; + } + break; + } + + case TsNodeKind.Identifier: + parts.push(node.text); + break; + + case TsNodeKind.Literal: + if (typeof node.value === 'string') { + parts.push(`"${node.value}"`); + } else if (typeof node.value === 'boolean') { + parts.push(node.value ? 'true' : 'false'); + } else if (node.value === null) { + parts.push('null'); + } else { + parts.push(String(node.value)); + } + break; + + case TsNodeKind.SourceFile: + parts.push(...node.statements.map(printNode)); + break; + + case TsNodeKind.VariableStatement: { + const keyword = node.keyword; + const name = node.name; + let line = `${keyword} ${name}`; + if (node.typeAnnotation) { + line += `: ${printNode(node.typeAnnotation)}`; + } + if (node.initializer) { + line += ` = ${printNode(node.initializer)}`; + } + if (semicolons) { + line += ';'; + } + parts.push(printLine(line)); + break; + } + + default: + throw new Error(`Unsupported node kind: ${(node as { kind: string }).kind}`); + } + + if (node.trailingComments) { + printComments(parts, node.trailingComments); + } + + return parts.join('\n'); + } + + function printFile(node: TsNode): string { + const parts: Array = [printNode(node), '']; + return parts.join('\n'); + } + + return { + printFile, + }; +} + +export function printAst(node: TsNode): string { + return JSON.stringify(node, null, 2); +} diff --git a/packages/openapi-python/src/py-dsl/expr/literal.ts b/packages/openapi-python/src/py-dsl/expr/literal.ts --- a/packages/openapi-python/src/py-dsl/expr/literal.ts +++ b/packages/openapi-python/src/py-dsl/expr/literal.ts @@ -3,16 +3,14 @@ import { py } from '../../py-compiler'; import { PyDsl } from '../base'; -export type LiteralValue = string | number | boolean | null; - const Mixed = PyDsl; export class LiteralPyDsl extends Mixed { readonly '~dsl' = 'LiteralPyDsl'; - protected value: LiteralValue; + protected value: py.LiteralValue; - constructor(value: LiteralValue) { + constructor(value: py.LiteralValue) { super(); this.value = value; } diff --git a/packages/openapi-python/src/py-dsl/stmt/var.ts b/packages/openapi-python/src/py-dsl/stmt/var.ts --- a/packages/openapi-python/src/py-dsl/stmt/var.ts +++ b/packages/openapi-python/src/py-dsl/stmt/var.ts @@ -44,10 +44,10 @@ override toAst() { this.$validate(); const target = this.$node(this.name)!; - const annotation = this.$type(); + const type = this.$type(); const value = this.$value(); - return py.factory.createAssignment(target, annotation, value); + return py.factory.createAssignment(target, type, value); } $validate(): asserts this { diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/constants.ts b/packages/openapi-ts/src/ts-compiler/__tests__/constants.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/constants.ts @@ -0,0 +1,4 @@ +import path from 'node:path'; + +export const snapshotsDir = path.join(__dirname, '..', '__snapshots__'); +export const tmpDir = path.join(__dirname, '..', '.tmp'); diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/globalTeardown.ts b/packages/openapi-ts/src/ts-compiler/__tests__/globalTeardown.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/globalTeardown.ts @@ -0,0 +1,7 @@ +import fs from 'node:fs'; + +import { tmpDir } from './constants'; + +export function teardown() { + fs.rmSync(tmpDir, { force: true, recursive: true }); +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/base.ts b/packages/openapi-ts/src/ts-compiler/nodes/base.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/base.ts @@ -0,0 +1,14 @@ +import type { TsExpression } from './expression'; +import type { TsNodeKind } from './kinds'; +import type { TsStatement } from './statement'; +// import type { TsBlock } from './statements/block'; +import type { TsSourceFile } from './structure/sourceFile'; + +export interface TsNodeBase { + kind: TsNodeKind; + leadingComments?: ReadonlyArray; + trailingComments?: ReadonlyArray; +} + +// TsBlock | +export type TsNode = TsExpression | TsSourceFile | TsStatement; diff --git a/packages/openapi-ts/src/ts-compiler/nodes/expression.ts b/packages/openapi-ts/src/ts-compiler/nodes/expression.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/expression.ts @@ -0,0 +1,4 @@ +import type { TsIdentifier } from './expressions/identifier'; +import type { TsLiteral } from './expressions/literal'; + +export type TsExpression = TsIdentifier | TsLiteral; diff --git a/packages/openapi-ts/src/ts-compiler/nodes/factory.ts b/packages/openapi-ts/src/ts-compiler/nodes/factory.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/factory.ts @@ -0,0 +1,13 @@ +import { createIdentifier } from './expressions/identifier'; +import { createLiteral } from './expressions/literal'; +import { createAssignment } from './statements/assignment'; +import { createVariableStatement } from './statements/var'; +import { createSourceFile } from './structure/sourceFile'; + +export const factory = { + createAssignment, + createIdentifier, + createLiteral, + createSourceFile, + createVariableStatement, +}; diff --git a/packages/openapi-ts/src/ts-compiler/nodes/kinds.ts b/packages/openapi-ts/src/ts-compiler/nodes/kinds.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/kinds.ts @@ -0,0 +1,7 @@ +export enum TsNodeKind { + Assignment = 'Assignment', + Identifier = 'Identifier', + Literal = 'Literal', + SourceFile = 'SourceFile', + VariableStatement = 'VariableStatement', +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/statement.ts b/packages/openapi-ts/src/ts-compiler/nodes/statement.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/statement.ts @@ -0,0 +1,4 @@ +import type { TsAssignment } from './statements/assignment'; +import type { TsVariableStatement } from './statements/var'; + +export type TsStatement = TsAssignment | TsVariableStatement; diff --git a/packages/openapi-ts/src/ts-compiler/nodes/type.ts b/packages/openapi-ts/src/ts-compiler/nodes/type.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/type.ts @@ -0,0 +1,1 @@ +export type TsType = never; diff --git a/packages/openapi-ts/src/ts-dsl/expr/literal.ts b/packages/openapi-ts/src/ts-dsl/expr/literal.ts --- a/packages/openapi-ts/src/ts-dsl/expr/literal.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/literal.ts @@ -1,29 +1,28 @@ import type { AnalysisContext } from '@hey-api/codegen-core'; -import ts from 'typescript'; +import tsOld from 'typescript'; +import type { ts } from '../../ts-compiler'; import { TsDsl } from '../base'; import { PrefixTsDsl } from '../expr/prefix'; import { AsMixin } from '../mixins/as'; -export type LiteralValue = string | number | boolean | bigint | null; - const Mixed = AsMixin( TsDsl< - | ts.BigIntLiteral - | ts.BooleanLiteral - | ts.NullLiteral - | ts.NumericLiteral - | ts.PrefixUnaryExpression - | ts.StringLiteral + | tsOld.BigIntLiteral + | tsOld.BooleanLiteral + | tsOld.NullLiteral + | tsOld.NumericLiteral + | tsOld.PrefixUnaryExpression + | tsOld.StringLiteral >, ); export class LiteralTsDsl extends Mixed { readonly '~dsl' = 'LiteralTsDsl'; - protected value: LiteralValue; + protected value: ts.LiteralValue; - constructor(value: LiteralValue) { + constructor(value: ts.LiteralValue) { super(); this.value = value; } @@ -34,20 +33,20 @@ override toAst() { if (typeof this.value === 'boolean') { - return this.value ? ts.factory.createTrue() : ts.factory.createFalse(); + return this.value ? tsOld.factory.createTrue() : tsOld.factory.createFalse(); } if (typeof this.value === 'number') { - const expr = ts.factory.createNumericLiteral(Math.abs(this.value)); + const expr = tsOld.factory.createNumericLiteral(Math.abs(this.value)); return this.value < 0 ? this.$node(new PrefixTsDsl(expr).neg()) : expr; } if (typeof this.value === 'string') { - return ts.factory.createStringLiteral(this.value, true); + return tsOld.factory.createStringLiteral(this.value, true); } if (typeof this.value === 'bigint') { - return ts.factory.createBigIntLiteral(this.value.toString()); + return tsOld.factory.createBigIntLiteral(this.value.toString()); } if (this.value === null) { - return ts.factory.createNull(); + return tsOld.factory.createNull(); } throw new Error(`Unsupported literal: ${String(this.value)}`); } diff --git a/packages/openapi-ts/src/ts-dsl/type/literal.ts b/packages/openapi-ts/src/ts-dsl/type/literal.ts --- a/packages/openapi-ts/src/ts-dsl/type/literal.ts +++ b/packages/openapi-ts/src/ts-dsl/type/literal.ts @@ -1,18 +1,19 @@ import type { AnalysisContext, NodeScope } from '@hey-api/codegen-core'; -import ts from 'typescript'; +import tsOld from 'typescript'; +import type { ts } from '../../ts-compiler'; import { TsDsl } from '../base'; -import { LiteralTsDsl, type LiteralValue } from '../expr/literal'; +import { LiteralTsDsl } from '../expr/literal'; -const Mixed = TsDsl; +const Mixed = TsDsl; export class TypeLiteralTsDsl extends Mixed { readonly '~dsl' = 'TypeLiteralTsDsl'; override scope: NodeScope = 'type'; - protected value: LiteralValue; + protected value: ts.LiteralValue; - constructor(value: LiteralValue) { + constructor(value: ts.LiteralValue) { super(); this.value = value; } @@ -22,6 +23,6 @@ } override toAst() { - return ts.factory.createLiteralTypeNode(this.$node(new LiteralTsDsl(this.value))); + return tsOld.factory.createLiteralTypeNode(this.$node(new LiteralTsDsl(this.value))); } } diff --git a/packages/openapi-python/src/py-compiler/nodes/declarations/functionParameter.ts b/packages/openapi-python/src/py-compiler/nodes/declarations/functionParameter.ts --- a/packages/openapi-python/src/py-compiler/nodes/declarations/functionParameter.ts +++ b/packages/openapi-python/src/py-compiler/nodes/declarations/functionParameter.ts @@ -3,25 +3,25 @@ import { PyNodeKind } from '../kinds'; export interface PyFunctionParameter extends PyNodeBase { - annotation?: PyExpression; defaultValue?: PyExpression; kind: PyNodeKind.FunctionParameter; name: string; + type?: PyExpression; } export function createFunctionParameter( name: string, - annotation?: PyExpression, + type?: PyExpression, defaultValue?: PyExpression, leadingComments?: ReadonlyArray, trailingComments?: ReadonlyArray, ): PyFunctionParameter { return { - annotation, defaultValue, kind: PyNodeKind.FunctionParameter, leadingComments, name, trailingComments, + type, }; } diff --git a/packages/openapi-python/src/py-compiler/nodes/expressions/literal.ts b/packages/openapi-python/src/py-compiler/nodes/expressions/literal.ts --- a/packages/openapi-python/src/py-compiler/nodes/expressions/literal.ts +++ b/packages/openapi-python/src/py-compiler/nodes/expressions/literal.ts @@ -1,15 +1,15 @@ import type { PyNodeBase } from '../base'; import { PyNodeKind } from '../kinds'; -export type LiteralValue = string | number | boolean | null; +export type PyLiteralValue = string | number | boolean | null; export interface PyLiteral extends PyNodeBase { kind: PyNodeKind.Literal; - value: LiteralValue; + value: PyLiteralValue; } export function createLiteral( - value: LiteralValue, + value: PyLiteralValue, leadingComments?: ReadonlyArray, trailingComments?: ReadonlyArray, ): PyLiteral { diff --git a/packages/openapi-python/src/py-compiler/nodes/statements/assignment.ts b/packages/openapi-python/src/py-compiler/nodes/statements/assignment.ts --- a/packages/openapi-python/src/py-compiler/nodes/statements/assignment.ts +++ b/packages/openapi-python/src/py-compiler/nodes/statements/assignment.ts @@ -3,29 +3,29 @@ import { PyNodeKind } from '../kinds'; export interface PyAssignment extends PyNodeBase { - annotation?: PyExpression; kind: PyNodeKind.Assignment; target: PyExpression; + type?: PyExpression; value?: PyExpression; } export function createAssignment( target: PyExpression, - annotation?: PyExpression, + type?: PyExpression, value?: PyExpression, leadingComments?: ReadonlyArray, trailingComments?: ReadonlyArray, ): PyAssignment { - if (!annotation && !value) { - throw new Error('Assignment requires at least annotation or value'); + if (!type && !value) { + throw new Error('Assignment requires at least type or value'); } return { - annotation, kind: PyNodeKind.Assignment, leadingComments, target, trailingComments, + type, value, }; } diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/nodes/utils.ts b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/utils.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/utils.ts @@ -0,0 +1,43 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { ts } from '../../index'; +import { snapshotsDir, tmpDir } from '../constants'; + +function getCallerFile(): string { + const error = new Error(); + const stack = (error.stack ?? '').split('\n'); + const callerLine = stack.find((line) => line.includes('.test.ts')); + if (!callerLine) { + throw new Error('Could not find test file in stack trace'); + } + const match = callerLine.match(/\(([^)]+)\)/) || callerLine.match(/at (.+):\d+:\d+/); + if (!match?.[1]) { + throw new Error('Could not extract file path'); + } + return match[1]; +} + +export async function assertPrintedMatchesSnapshot( + file: ts.SourceFile, + filename: string, +): Promise { + const result = ts.createPrinter().printFile(file); + + const caller = getCallerFile(); + const relPath = path + .relative(path.join(process.cwd(), 'src', 'ts-compiler', '__tests__'), caller) + .replace(/\.test\.ts$/, ''); + const outputPath = path.join(tmpDir, relPath, filename); + const outputDir = path.dirname(outputPath); + + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(outputPath, result); + + const snapshotPath = path.join(snapshotsDir, relPath, filename); + + const snapshotDir = path.dirname(snapshotPath); + fs.mkdirSync(snapshotDir, { recursive: true }); + + await expect(result).toMatchFileSnapshot(snapshotPath); +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/expressions/identifier.ts b/packages/openapi-ts/src/ts-compiler/nodes/expressions/identifier.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/expressions/identifier.ts @@ -0,0 +1,20 @@ +import type { TsNodeBase } from '../base'; +import { TsNodeKind } from '../kinds'; + +export interface TsIdentifier extends TsNodeBase { + kind: TsNodeKind.Identifier; + text: string; +} + +export function createIdentifier( + text: string, + leadingComments?: ReadonlyArray, + trailingComments?: ReadonlyArray, +): TsIdentifier { + return { + kind: TsNodeKind.Identifier, + leadingComments, + text, + trailingComments, + }; +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/expressions/literal.ts b/packages/openapi-ts/src/ts-compiler/nodes/expressions/literal.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/expressions/literal.ts @@ -0,0 +1,22 @@ +import type { TsNodeBase } from '../base'; +import { TsNodeKind } from '../kinds'; + +export type TsLiteralValue = string | number | boolean | bigint | null; + +export interface TsLiteral extends TsNodeBase { + kind: TsNodeKind.Literal; + value: TsLiteralValue; +} + +export function createLiteral( + value: TsLiteralValue, + leadingComments?: ReadonlyArray, + trailingComments?: ReadonlyArray, +): TsLiteral { + return { + kind: TsNodeKind.Literal, + leadingComments, + trailingComments, + value, + }; +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/statements/assignment.ts b/packages/openapi-ts/src/ts-compiler/nodes/statements/assignment.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/statements/assignment.ts @@ -0,0 +1,31 @@ +import type { TsNodeBase } from '../base'; +import type { TsExpression } from '../expression'; +import { TsNodeKind } from '../kinds'; + +export interface TsAssignment extends TsNodeBase { + kind: TsNodeKind.Assignment; + target: TsExpression; + type?: TsExpression; + value?: TsExpression; +} + +export function createAssignment( + target: TsExpression, + type?: TsExpression, + value?: TsExpression, + leadingComments?: ReadonlyArray, + trailingComments?: ReadonlyArray, +): TsAssignment { + if (!type && !value) { + throw new Error('Assignment requires at least type or value'); + } + + return { + kind: TsNodeKind.Assignment, + leadingComments, + target, + trailingComments, + type, + value, + }; +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/statements/var.ts b/packages/openapi-ts/src/ts-compiler/nodes/statements/var.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/statements/var.ts @@ -0,0 +1,33 @@ +import type { TsNodeBase } from '../base'; +import type { TsExpression } from '../expression'; +import { TsNodeKind } from '../kinds'; +import type { TsType } from '../type'; + +export type TsVariableKeyword = 'var' | 'let' | 'const'; + +export interface TsVariableStatement extends TsNodeBase { + initializer?: TsExpression; + keyword: TsVariableKeyword; + kind: TsNodeKind.VariableStatement; + name: string; + typeAnnotation?: TsType; +} + +export function createVariableStatement( + keyword: TsVariableKeyword, + name: string, + initializer?: TsExpression, + typeAnnotation?: TsType, + leadingComments?: ReadonlyArray, + trailingComments?: ReadonlyArray, +): TsVariableStatement { + return { + initializer, + keyword, + kind: TsNodeKind.VariableStatement, + leadingComments, + name, + trailingComments, + typeAnnotation, + }; +} diff --git a/packages/openapi-ts/src/ts-compiler/nodes/structure/sourceFile.ts b/packages/openapi-ts/src/ts-compiler/nodes/structure/sourceFile.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/nodes/structure/sourceFile.ts @@ -0,0 +1,20 @@ +import type { TsNode, TsNodeBase } from '../base'; +import { TsNodeKind } from '../kinds'; + +export interface TsSourceFile extends TsNodeBase { + kind: TsNodeKind.SourceFile; + statements: ReadonlyArray; +} + +export function createSourceFile( + statements: ReadonlyArray, + leadingComments?: ReadonlyArray, + trailingComments?: ReadonlyArray, +): TsSourceFile { + return { + kind: TsNodeKind.SourceFile, + leadingComments, + statements, + trailingComments, + }; +} diff --git a/packages/openapi-python/src/py-compiler/__tests__/nodes/expressions/literal.test.ts b/packages/openapi-python/src/py-compiler/__tests__/nodes/expressions/literal.test.ts --- a/packages/openapi-python/src/py-compiler/__tests__/nodes/expressions/literal.test.ts +++ b/packages/openapi-python/src/py-compiler/__tests__/nodes/expressions/literal.test.ts @@ -20,6 +20,11 @@ py.factory.createLiteral(true), ), py.factory.createAssignment( + py.factory.createIdentifier('c'), + undefined, + py.factory.createLiteral(false), + ), + py.factory.createAssignment( py.factory.createIdentifier('none'), undefined, py.factory.createLiteral(null), diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/identifier.test.ts b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/identifier.test.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/identifier.test.ts @@ -0,0 +1,22 @@ +import { ts } from '../../../index'; +import { assertPrintedMatchesSnapshot } from '../utils'; + +describe('identifier expression', () => { + it('assignment', async () => { + const file = ts.factory.createSourceFile([ + ts.factory.createVariableStatement('let', 'x'), + ts.factory.createVariableStatement('let', 'y'), + ts.factory.createAssignment( + ts.factory.createIdentifier('y'), + undefined, + ts.factory.createLiteral(42), + ), + ts.factory.createAssignment( + ts.factory.createIdentifier('x'), + undefined, + ts.factory.createIdentifier('y'), + ), + ]); + await assertPrintedMatchesSnapshot(file, 'identifier.ts'); + }); +}); diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/literal.test.ts b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/literal.test.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/expressions/literal.test.ts @@ -0,0 +1,40 @@ +import { ts } from '../../../index'; +import { assertPrintedMatchesSnapshot } from '../utils'; + +describe('literal expression', () => { + it('primitive variables', async () => { + const file = ts.factory.createSourceFile([ + ts.factory.createVariableStatement('let', 's'), + ts.factory.createVariableStatement('let', 'n'), + ts.factory.createVariableStatement('let', 'b'), + ts.factory.createVariableStatement('let', 'c'), + ts.factory.createVariableStatement('let', 'none'), + ts.factory.createAssignment( + ts.factory.createIdentifier('s'), + undefined, + ts.factory.createLiteral('hello'), + ), + ts.factory.createAssignment( + ts.factory.createIdentifier('n'), + undefined, + ts.factory.createLiteral(123), + ), + ts.factory.createAssignment( + ts.factory.createIdentifier('b'), + undefined, + ts.factory.createLiteral(true), + ), + ts.factory.createAssignment( + ts.factory.createIdentifier('c'), + undefined, + ts.factory.createLiteral(false), + ), + ts.factory.createAssignment( + ts.factory.createIdentifier('none'), + undefined, + ts.factory.createLiteral(null), + ), + ]); + await assertPrintedMatchesSnapshot(file, 'primitive.ts'); + }); +}); diff --git a/packages/openapi-ts/src/ts-compiler/__tests__/nodes/statements/var.test.ts b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/statements/var.test.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__tests__/nodes/statements/var.test.ts @@ -0,0 +1,25 @@ +import { ts } from '../../../index'; +import { assertPrintedMatchesSnapshot } from '../utils'; + +describe('variable statement', () => { + it('const', async () => { + const file = ts.factory.createSourceFile([ + ts.factory.createVariableStatement('const', 'answer', ts.factory.createLiteral(42)), + ]); + await assertPrintedMatchesSnapshot(file, 'const.ts'); + }); + + it('let', async () => { + const file = ts.factory.createSourceFile([ + ts.factory.createVariableStatement('let', 'message', ts.factory.createLiteral('hello')), + ]); + await assertPrintedMatchesSnapshot(file, 'let.ts'); + }); + + it('var', async () => { + const file = ts.factory.createSourceFile([ + ts.factory.createVariableStatement('var', 'count', ts.factory.createLiteral(0)), + ]); + await assertPrintedMatchesSnapshot(file, 'var.ts'); + }); +}); diff --git a/packages/openapi-python/src/py-compiler/__snapshots__/nodes/expressions/literal/primitive.py b/packages/openapi-python/src/py-compiler/__snapshots__/nodes/expressions/literal/primitive.py --- a/packages/openapi-python/src/py-compiler/__snapshots__/nodes/expressions/literal/primitive.py +++ b/packages/openapi-python/src/py-compiler/__snapshots__/nodes/expressions/literal/primitive.py @@ -1,4 +1,5 @@ s = "hello" n = 123 b = True +c = False none = None diff --git a/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/identifier/identifier.ts b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/identifier/identifier.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/identifier/identifier.ts @@ -0,0 +1,4 @@ +let x; +let y; +y = 42; +x = y; diff --git a/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/literal/primitive.ts b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/literal/primitive.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/expressions/literal/primitive.ts @@ -0,0 +1,10 @@ +let s; +let n; +let b; +let c; +let none; +s = "hello"; +n = 123; +b = true; +c = false; +none = null; diff --git a/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/const.ts b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/const.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/const.ts @@ -0,0 +1,1 @@ +const answer = 42; diff --git a/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/let.ts b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/let.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/let.ts @@ -0,0 +1,1 @@ +let message = "hello"; diff --git a/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/var.ts b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/var.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/ts-compiler/__snapshots__/nodes/statements/var/var.ts @@ -0,0 +1,1 @@ +var count = 0;