-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(typescript): Implement type literal AST for TS (#5057)
* Implement type literal AST for TS * start writing test * Add tests * Always assume multiline on collections and remove braces from defns * simplify iterable * write string with backticks to helper * clean up type definitions and iterable impl * delete generics and make the world a better place
- Loading branch information
Showing
8 changed files
with
385 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { assertNever } from "@fern-api/core-utils"; | ||
import { AstNode, Writer } from "./core"; | ||
import { Type } from "./Type"; | ||
|
||
type InternalTypeLiteral = Array_ | Boolean_ | Number_ | Object_ | String_ | Tuple; | ||
|
||
interface Array_ { | ||
type: "array"; | ||
valueType: Type; | ||
values: TypeLiteral[]; | ||
} | ||
|
||
interface Boolean_ { | ||
type: "boolean"; | ||
value: boolean; | ||
} | ||
|
||
interface Number_ { | ||
type: "number"; | ||
value: number; | ||
} | ||
|
||
interface Object_ { | ||
type: "object"; | ||
fields: ObjectField[]; | ||
} | ||
|
||
interface ObjectField { | ||
name: string; | ||
valueType: Type; | ||
value: TypeLiteral; | ||
} | ||
|
||
interface String_ { | ||
type: "string"; | ||
value: string; | ||
} | ||
|
||
interface Tuple { | ||
type: "tuple"; | ||
// TODO: In theory this should be a tuple type, not an array of types | ||
valueTypes: Type[]; | ||
values: TypeLiteral[]; | ||
} | ||
|
||
export class TypeLiteral extends AstNode { | ||
private constructor(public readonly internalType: InternalTypeLiteral) { | ||
super(); | ||
} | ||
|
||
public write(writer: Writer): void { | ||
switch (this.internalType.type) { | ||
case "array": { | ||
this.writeIterable({ writer, iterable: this.internalType }); | ||
break; | ||
} | ||
case "boolean": { | ||
writer.write(this.internalType.value.toString()); | ||
break; | ||
} | ||
case "number": { | ||
// N.B. Defaults to decimal; further work needed to support alternatives like hex, binary, octal, etc. | ||
writer.write(this.internalType.value.toString()); | ||
break; | ||
} | ||
case "object": { | ||
this.writeObject({ writer, object: this.internalType }); | ||
break; | ||
} | ||
case "string": { | ||
if (this.internalType.value.includes("\n")) { | ||
this.writeStringWithBackticks({ writer, value: this.internalType.value }); | ||
} else { | ||
writer.write(`"${this.internalType.value.replaceAll('"', '\\"')}"`); | ||
} | ||
break; | ||
} | ||
case "tuple": { | ||
this.writeIterable({ writer, iterable: this.internalType }); | ||
break; | ||
} | ||
default: { | ||
assertNever(this.internalType); | ||
} | ||
} | ||
} | ||
|
||
private writeStringWithBackticks({ writer, value }: { writer: Writer; value: string }): void { | ||
writer.write("`"); | ||
const parts = value.split("\n"); | ||
const head = parts[0] + "\n"; | ||
const tail = parts.slice(1).join("\n"); | ||
writer.write(head.replaceAll("`", "\\`")); | ||
writer.writeNoIndent(tail.replaceAll("`", "\\`")); | ||
writer.write("`"); | ||
} | ||
|
||
private writeIterable({ writer, iterable }: { writer: Writer; iterable: Array_ | Tuple }): void { | ||
if (iterable.values.length === 0) { | ||
// Don't allow "multiline" empty iterables. | ||
writer.write("[]"); | ||
} else { | ||
writer.writeLine("["); | ||
writer.indent(); | ||
for (const value of iterable.values) { | ||
value.write(writer); | ||
writer.writeLine(","); | ||
} | ||
writer.dedent(); | ||
writer.write("]"); | ||
} | ||
} | ||
|
||
private writeObject({ writer, object }: { writer: Writer; object: Object_ }): void { | ||
if (object.fields.length === 0) { | ||
// Don't allow "multiline" empty objects. | ||
writer.write("{}"); | ||
} else { | ||
writer.writeLine("{"); | ||
writer.indent(); | ||
for (const field of object.fields) { | ||
writer.write(`${field.name}: `); | ||
field.value.write(writer); | ||
writer.writeLine(","); | ||
} | ||
writer.dedent(); | ||
writer.write("}"); | ||
} | ||
} | ||
|
||
/* Static factory methods for creating a TypeLiteral */ | ||
public static array({ valueType, values }: { valueType: Type; values: TypeLiteral[] }): TypeLiteral { | ||
return new this({ | ||
type: "array", | ||
valueType, | ||
values | ||
}); | ||
} | ||
|
||
public static boolean(value: boolean): TypeLiteral { | ||
return new this({ type: "boolean", value }); | ||
} | ||
|
||
public static number(value: number): TypeLiteral { | ||
return new this({ type: "number", value }); | ||
} | ||
|
||
public static object(fields: ObjectField[]): TypeLiteral { | ||
return new this({ | ||
type: "object", | ||
fields | ||
}); | ||
} | ||
|
||
public static string(value: string): TypeLiteral { | ||
return new this({ | ||
type: "string", | ||
value | ||
}); | ||
} | ||
|
||
public static tuple({ valueTypes, values }: { valueTypes: Type[]; values: TypeLiteral[] }): TypeLiteral { | ||
return new this({ | ||
type: "tuple", | ||
valueTypes, | ||
values | ||
}); | ||
} | ||
} |
117 changes: 117 additions & 0 deletions
117
generators/typescript/codegen/src/ast/__test__/TypeLiteral.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { ts } from "../.."; | ||
|
||
describe("TypeLiteral", () => { | ||
describe("emptyArrayToString", () => { | ||
it("Should generate an empty array", () => { | ||
const literal = ts.TypeLiteral.array({ | ||
valueType: ts.Type.string(), | ||
values: [] | ||
}); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("arrayOfStringsToString", () => { | ||
it("Should generate an array of strings", () => { | ||
const literal = ts.TypeLiteral.array({ | ||
valueType: ts.Type.string(), | ||
values: [ts.TypeLiteral.string("Hello, World!"), ts.TypeLiteral.string("Goodbye, World!")] | ||
}); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
// N.B. If the array is too short prettier is going to print it on a single line | ||
describe("longArrayOfStringsToString", () => { | ||
it("Should generate a multiline array of strings", () => { | ||
const literal = ts.TypeLiteral.array({ | ||
valueType: ts.Type.string(), | ||
values: [ | ||
ts.TypeLiteral.string("Hello, World!"), | ||
ts.TypeLiteral.string("Goodbye, World!"), | ||
ts.TypeLiteral.string("Hello, World!"), | ||
ts.TypeLiteral.string("Goodbye, World!"), | ||
ts.TypeLiteral.string("Hello, World!"), | ||
ts.TypeLiteral.string("Goodbye, World!"), | ||
ts.TypeLiteral.string("Hello, World!"), | ||
ts.TypeLiteral.string("Goodbye, World!") | ||
] | ||
}); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("trueBooleanToString", () => { | ||
it("Should generate a true boolean", () => { | ||
const literal = ts.TypeLiteral.boolean(true); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("falseBooleanToString", () => { | ||
it("Should generate a true boolean", () => { | ||
const literal = ts.TypeLiteral.boolean(false); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("numberToString", () => { | ||
it("Should generate a simple number", () => { | ||
const literal = ts.TypeLiteral.number(7); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("stringToString", () => { | ||
it("Should generate a simple string literal", () => { | ||
const literal = ts.TypeLiteral.string("Hello, World!"); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("stringWithDoubleQuotesToString", () => { | ||
it("Should generate a simple string literal with escaped double quotes", () => { | ||
const literal = ts.TypeLiteral.string('"Hello, World!"'); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("manyLinesMultilineStringToString", () => { | ||
it("Should generate a multiline string with backticks", () => { | ||
const literal = ts.TypeLiteral.string(`Hello, | ||
World!`); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("manyLinesMultilineStringWithBackticksToString", () => { | ||
it("Should generate a multiline string with escaped backticks", () => { | ||
const literal = ts.TypeLiteral.string(`\`Hello, | ||
World!\``); | ||
expect(literal.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe("simpleObjectToString", () => { | ||
it("Should generate a simple object", () => { | ||
const actual = ts.codeblock((writer) => { | ||
writer.write("let myObj = "); | ||
writer.writeNode( | ||
ts.TypeLiteral.object([ | ||
{ | ||
name: "name", | ||
valueType: ts.Type.string(), | ||
value: ts.TypeLiteral.string("John Smith") | ||
}, | ||
{ | ||
name: "hometown", | ||
valueType: ts.Type.string(), | ||
value: ts.TypeLiteral.string("New York, New York") | ||
} | ||
]) | ||
); | ||
}); | ||
expect(actual.toStringFormatted()).toMatchSnapshot(); | ||
}); | ||
}); | ||
}); |
70 changes: 70 additions & 0 deletions
70
generators/typescript/codegen/src/ast/__test__/__snapshots__/TypeLiteral.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
||
exports[`TypeLiteral > arrayOfStringsToString > Should generate an array of strings 1`] = ` | ||
"["Hello, World!", "Goodbye, World!"]; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > emptyArrayToString > Should generate an empty array 1`] = ` | ||
"[]; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > falseBooleanToString > Should generate a true boolean 1`] = ` | ||
"false; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > longArrayOfStringsToString > Should generate a multiline array of strings 1`] = ` | ||
"[ | ||
"Hello, World!", | ||
"Goodbye, World!", | ||
"Hello, World!", | ||
"Goodbye, World!", | ||
"Hello, World!", | ||
"Goodbye, World!", | ||
"Hello, World!", | ||
"Goodbye, World!", | ||
]; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > manyLinesMultilineStringToString > Should generate a multiline string with backticks 1`] = ` | ||
"\`Hello, | ||
World!\`; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > manyLinesMultilineStringWithBackticksToString > Should generate a multiline string with escaped backticks 1`] = ` | ||
"\`\\\`Hello, | ||
World!\\\`\`; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > numberToString > Should generate a simple number 1`] = ` | ||
"7; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > simpleObjectToString > Should generate a simple object 1`] = ` | ||
"let myObj = { | ||
name: "John Smith", | ||
hometown: "New York, New York", | ||
}; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > stringToString > Should generate a simple string literal 1`] = ` | ||
""Hello, World!"; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > stringWithDoubleQuotesToString > Should generate a simple string literal with escaped double quotes 1`] = ` | ||
""\\"Hello, World!\\""; | ||
" | ||
`; | ||
|
||
exports[`TypeLiteral > trueBooleanToString > Should generate a true boolean 1`] = ` | ||
"true; | ||
" | ||
`; |
Oops, something went wrong.