Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typescript): Implement type literal AST for TS #5057

Merged
merged 8 commits into from
Oct 31, 2024
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
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[];
}
Comment on lines +39 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the value and valueType should be a single interface so that it's impossible for the length to be inconsistent.


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
Loading