Skip to content

Commit

Permalink
feat(typescript): Implement type literal AST for TS (#5057)
Browse files Browse the repository at this point in the history
* 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
ajgateno authored Oct 31, 2024
1 parent ee0e082 commit 4c82998
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 4 deletions.
8 changes: 8 additions & 0 deletions generators/commons/src/ast/AbstractWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export class AbstractWriter {
this.writeInternal(indentedText);
}

/**
* Writes arbitrary text without indentation
* @param text
*/
public writeNoIndent(text: string): void {
this.writeInternal(text);
}

/**
* Writes a node
* @param node
Expand Down
3 changes: 2 additions & 1 deletion generators/typescript/codegen/src/ast/CodeBlock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CodeBlock as CommonCodeBlock } from "@fern-api/generator-commons";
import { AstNode, Writer } from "../typescript";
import { AstNode } from "./core/AstNode";
import { Writer } from "./core/Writer";

export declare namespace CodeBlock {
/* Write arbitrary code */
Expand Down
169 changes: 169 additions & 0 deletions generators/typescript/codegen/src/ast/TypeLiteral.ts
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 generators/typescript/codegen/src/ast/__test__/TypeLiteral.test.ts
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();
});
});
});
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;
"
`;
Loading

0 comments on commit 4c82998

Please sign in to comment.