diff --git a/package-lock.json b/package-lock.json index a37333c..e4171fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@intrinsicai/gbnfgen", "version": "0.0.0", "license": "MIT", + "dependencies": { + "ts-morph": "^20.0.0" + }, "devDependencies": { "ts-node": "^10.9.1", "typescript": "^5.1.6", @@ -415,12 +418,55 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@ts-morph/common": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.21.0.tgz", + "integrity": "sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==", + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -582,6 +628,30 @@ "node": "*" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -618,6 +688,11 @@ "node": "*" } }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -708,6 +783,40 @@ "@esbuild/win32-x64": "0.18.17" } }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -731,6 +840,44 @@ "node": "*" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -776,6 +923,54 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mlly": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", @@ -827,6 +1022,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/pathe": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", @@ -848,6 +1048,17 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -901,12 +1112,40 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "3.27.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.27.0.tgz", @@ -923,6 +1162,28 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -986,6 +1247,26 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-morph": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-20.0.0.tgz", + "integrity": "sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==", + "dependencies": { + "@ts-morph/common": "~0.21.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", diff --git a/package.json b/package.json index fbb51e9..0aa3105 100644 --- a/package.json +++ b/package.json @@ -16,5 +16,8 @@ "ts-node": "^10.9.1", "typescript": "^5.1.6", "vitest": "^0.34.0" + }, + "dependencies": { + "ts-morph": "^20.0.0" } } diff --git a/src/compiler.test.ts b/src/compiler.test.ts index 743662a..c08a21d 100644 --- a/src/compiler.test.ts +++ b/src/compiler.test.ts @@ -1,9 +1,9 @@ import { expect, test } from "vitest"; -import { compile } from "./compiler.js"; +import { compile, compileSync } from "./compiler.js"; import { serializeGrammar } from "./grammar.js"; test("Single interface generation", () => { - const postalAddressGrammar = compile( + const postalAddressGrammar = compileSync( `interface PostalAddress { streetNumber: number; street: string; @@ -29,7 +29,7 @@ numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws }); test("Single interface with enum generation", () => { - const postalAddressGrammar = compile( + const postalAddressGrammar = compileSync( `enum AddressType { Business = "business", Home = "home" }; interface PostalAddress { streetNumber: number; @@ -47,7 +47,7 @@ test("Single interface with enum generation", () => { root ::= PostalAddress PostalAddress ::= "{" ws "\"streetNumber\":" ws number "," ws "\"type\":" ws AddressType "," ws "\"street\":" ws string "," ws "\"city\":" ws string "," ws "\"state\":" ws string "," ws "\"postalCode\":" ws number "}" PostalAddresslist ::= "[]" | "[" ws PostalAddress ("," ws PostalAddress)* "]" -AddressType ::= "\"" "business" "\"" | "\"" "home" "\"" +AddressType ::= "\"business\"" | "\"home\"" string ::= "\"" ([^"]*) "\"" boolean ::= "true" | "false" ws ::= [ \t\n]* @@ -59,7 +59,7 @@ numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws }); test("Single multiple interface with references generation", () => { - const resumeGrammar = compile( + const resumeGrammar = compileSync( ` interface JobCandidate { name: string; @@ -92,7 +92,7 @@ numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws }); test("Single multiple interface and enum with references generation", () => { - const resumeGrammar = compile( + const resumeGrammar = compileSync( ` // Define an enum for product categories enum ProductCategory { @@ -134,10 +134,10 @@ test("Single multiple interface and enum with references generation", () => { root ::= Order Order ::= "{" ws "\"orderId\":" ws number "," ws "\"products\":" ws Productlist "," ws "\"status\":" ws OrderStatus "," ws "\"orderDate\":" ws string "}" Orderlist ::= "[]" | "[" ws Order ("," ws Order)* "]" -OrderStatus ::= "\"" "Pending" "\"" | "\"" "Shipped" "\"" | "\"" "Delivered" "\"" | "\"" "Canceled" "\"" +OrderStatus ::= "\"Pending\"" | "\"Shipped\"" | "\"Delivered\"" | "\"Canceled\"" Product ::= "{" ws "\"id\":" ws number "," ws "\"name\":" ws string "," ws "\"description\":" ws string "," ws "\"price\":" ws number "," ws "\"category\":" ws ProductCategory "}" Productlist ::= "[]" | "[" ws Product ("," ws Product)* "]" -ProductCategory ::= "\"" "Electronics" "\"" | "\"" "Clothing" "\"" | "\"" "Food" "\"" +ProductCategory ::= "\"Electronics\"" | "\"Clothing\"" | "\"Food\"" string ::= "\"" ([^"]*) "\"" boolean ::= "true" | "false" ws ::= [ \t\n]* @@ -149,13 +149,19 @@ numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws }); test("Jsonformer car example", () => { - const grammar = compile( + const grammar = compileSync( ` + // The car and owner interface CarAndOwner { + + /* + The car component + */ car: Car; owner: Owner; } + // The car interface Car { make: string; model: string; @@ -221,3 +227,149 @@ stringlist ::= "[" ws "]" | "[" ws string ("," ws string)* ws numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws "]"`.trim() ); }); + +test("compiler errors", () => { + expect(() => + compileSync( + ` + interface failure { + name: string; + name: number; + } + `, + "failure" + ) + ).toThrowError(`Duplicate identifier 'name'.`); + + expect(() => + compileSync( + ` + interface failure { + name1: string; + name2: myfaketype; + } + `, + "failure" + ) + ).toThrowError(`Compilation failed: Cannot find name 'myfaketype'.`); + + expect(() => + compileSync( + ` + const failure = {}; + `, + "failure" + ) + ).toThrowError( + `Invalid top-level declaration of kind VariableStatement: const failure = {};` + ); + + // TODO(aduffy): fix when TypeAliasDeclaration support has been added. + expect(() => + compileSync( + ` + type Person = string; + interface failure { + name: Person; + } + `, + "failure" + ) + ).toThrowError( + `Invalid top-level declaration of kind TypeAliasDeclaration: type Person = string;` + ); +}); + +test("async", async () => { + await expect( + compile( + ` + type Person = string; + interface failure { + name: Person; + } + `, + "failure" + ) + ).rejects.toThrow( + "Invalid top-level declaration of kind TypeAliasDeclaration: type Person = string;" + ); + + const grammar = await compile( + ` +// The car and owner +interface CarAndOwner { + + /* + The car component + */ + car: Car; + owner: Owner; +} + +// The car +interface Car { + make: string; + model: string; + year: number; + colors: string[]; + features: Features; +} + +interface Owner { + firstName: string; + lastName: string; + age: number; +} + +interface Features { + audio: AudioFeature; + safety: SafetyFeature; + performance: PerformanceFeature; +} + +interface AudioFeature { + brand: string; + speakers: number; + hasBluetooth: boolean; +} + +interface SafetyFeature { + airbags: number; + parkingSensors: number; + laneAssist: number; +} + +interface PerformanceFeature { + engine: string; + horsepower: number; + topSpeed: number; +}`, + "CarAndOwner" + ); + + expect(serializeGrammar(grammar).trimEnd()).toEqual( + String.raw` +root ::= CarAndOwner +PerformanceFeature ::= "{" ws "\"engine\":" ws string "," ws "\"horsepower\":" ws number "," ws "\"topSpeed\":" ws number "}" +PerformanceFeaturelist ::= "[]" | "[" ws PerformanceFeature ("," ws PerformanceFeature)* "]" +SafetyFeature ::= "{" ws "\"airbags\":" ws number "," ws "\"parkingSensors\":" ws number "," ws "\"laneAssist\":" ws number "}" +SafetyFeaturelist ::= "[]" | "[" ws SafetyFeature ("," ws SafetyFeature)* "]" +AudioFeature ::= "{" ws "\"brand\":" ws string "," ws "\"speakers\":" ws number "," ws "\"hasBluetooth\":" ws boolean "}" +AudioFeaturelist ::= "[]" | "[" ws AudioFeature ("," ws AudioFeature)* "]" +Features ::= "{" ws "\"audio\":" ws AudioFeature "," ws "\"safety\":" ws SafetyFeature "," ws "\"performance\":" ws PerformanceFeature "}" +Featureslist ::= "[]" | "[" ws Features ("," ws Features)* "]" +Owner ::= "{" ws "\"firstName\":" ws string "," ws "\"lastName\":" ws string "," ws "\"age\":" ws number "}" +Ownerlist ::= "[]" | "[" ws Owner ("," ws Owner)* "]" +Car ::= "{" ws "\"make\":" ws string "," ws "\"model\":" ws string "," ws "\"year\":" ws number "," ws "\"colors\":" ws stringlist "," ws "\"features\":" ws Features "}" +Carlist ::= "[]" | "[" ws Car ("," ws Car)* "]" +CarAndOwner ::= "{" ws "\"car\":" ws Car "," ws "\"owner\":" ws Owner "}" +CarAndOwnerlist ::= "[]" | "[" ws CarAndOwner ("," ws CarAndOwner)* "]" +string ::= "\"" ([^"]*) "\"" +boolean ::= "true" | "false" +ws ::= [ \t\n]* +number ::= [0-9]+ "."? [0-9]* +stringlist ::= "[" ws "]" | "[" ws string ("," ws string)* ws "]" +numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws "]"`.trim() + ); +}); diff --git a/src/compiler.ts b/src/compiler.ts index 3ae71c1..e2c368c 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,4 +1,4 @@ -import ts, { InterfaceDeclaration, EnumDeclaration } from "typescript"; +import { Project, ts, InterfaceDeclaration, EnumDeclaration } from "ts-morph"; import { Grammar, GrammarElement, @@ -87,67 +87,30 @@ export interface Interface { properties: Array; } -interface InMemoryCompilerHost extends ts.CompilerHost { - addSource(fileName: string, code: string): ts.SourceFile; -} - -class InMemoryCompilerHostImpl implements InMemoryCompilerHost { - private files: Map = new Map(); - - addSource(fileName: string, code: string) { - if (this.files.has(fileName)) { - throw new Error(`File already exists: ${fileName}`); - } - const srcFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext); - this.files.set(fileName, srcFile); - return srcFile; - } - - getSourceFile = (fileName: string): ts.SourceFile | undefined => { - return this.files.get(fileName); - }; - getDefaultLibFileName = () => "lib.d.ts"; - writeFile = () => { - // Do nothing. - }; - getCurrentDirectory = () => "."; - getCanonicalFileName = (fileName: string) => fileName; - useCaseSensitiveFileNames = () => true; - getNewLine = () => `\n`; - fileExists = (fileName: string) => this.files.has(fileName); - readFile = (fileName: string) => { - return this.files.get(fileName)?.getFullText(); - }; -} - -export function createInMemoryCompilerHost(): InMemoryCompilerHost { - return new InMemoryCompilerHostImpl(); -} - function handleEnum(enumNode: EnumDeclaration): GrammarElement { // Get all the choices of the enum const choices: GrammarRule[] = []; - if (enumNode && enumNode.members) { - for (const member of enumNode.members) { + if (enumNode && enumNode.getMembers().length > 0) { + for (const member of enumNode.getMembers()) { // NOTE(aduffy): support union type literals as well. - if (ts.isEnumMember(member) && ts.isIdentifier(member.name)) { + if (member.isKind(ts.SyntaxKind.EnumMember)) { // If initializer is String, we use the string value. Else, we assume a numeric value. - if (!member.initializer || !ts.isStringLiteral(member.initializer)) { + const initializer = member.getInitializer(); + if (!initializer || !initializer.isKind(ts.SyntaxKind.StringLiteral)) { throw new Error( "Only string enums are supported. Please check the String enums section of the TypeScript Handbook at https://www.typescriptlang.org/docs/handbook/enums.html" ); } - choices.push(literal(member.initializer.text, true)); + choices.push(literal(initializer.getText())); } } } - return { identifier: enumNode.name.text, alternatives: choices }; + return { identifier: enumNode.getName(), alternatives: choices }; } function handleInterface( iface: InterfaceDeclaration, - srcFile: ts.SourceFile, declaredTypes: Set, register: GrammarRegister ): Interface { @@ -157,21 +120,22 @@ function handleInterface( declaredArrayTypes.set(`${declType}[]`, declType); } - if (iface.typeParameters) { + if (iface.getTypeParameters().length > 0) { + console.log(iface.getFullText()); throw new Error( - `${iface.name.getText(srcFile)}: interfaces cannot have type parameters` + `${iface.getName()}: interfaces cannot have type parameters` ); } - const ifaceName = iface.name.getText(srcFile); + const ifaceName = iface.getName(); const props: Array = []; - for (const child of iface.members) { - if (!ts.isPropertySignature(child)) { + for (const child of iface.getMembers()) { + if (!child.isKind(ts.SyntaxKind.PropertySignature)) { throw new Error( `Invalid interface member: interfaces must only contain properties, contained ${child}` ); } - const propName = child.name.getText(srcFile); - const propType = child.type?.getText(srcFile) ?? "never"; + const propName = child.getName(); + const propType = child.getType().getText(); // Validate one of the accepted types let propTypeValidated: PropertyType; @@ -204,58 +168,70 @@ function handleInterface( } /** - * Main compilation function, targeting {@link Grammar} type from raw TypeScript interface source code. + * Async variant of main compilation function, targeting {@link Grammar} type from raw TypeScript interface source code. * @param source * @returns */ -export function compile(source: string, rootType: string): Grammar { - const host = createInMemoryCompilerHost(); +export async function compile( + source: string, + rootType: string +): Promise { + return new Promise((resolve, reject) => { + try { + const grammar = compileSync(source, rootType); + resolve(grammar); + } catch (e) { + reject(e); + } + }); +} - const srcFile = host.addSource("source.ts", source); - const program = ts.createProgram({ - rootNames: ["source.ts"], - options: { - ...ts.getDefaultCompilerOptions(), - // TODO(aduffy): Turn this back on to force failure on compile/type-checking errors. - // noEmitOnError: true, +/** + * Sync variant of main compilation function, targeting {@link Grammar} type from raw TypeScript interface source code. + * @param source + * @returns + */ +export function compileSync(source: string, rootType: string): Grammar { + const project = new Project({ + useInMemoryFileSystem: true, + compilerOptions: { + lib: ["lib.es5.d.ts"], }, - host, }); - // Get the default Grammar Register - const register = getDefaultGrammar(); + // Import the file from local node_modules and import at build time. + const srcFile = project.createSourceFile("source.ts", source); + + const emitResult = project.emitToMemory(); - // Run the compiler to ensure that the typescript source file is correct. - const emitResult = program.emit(); - if (emitResult.diagnostics.length > 0) { - const errors = emitResult.diagnostics - .filter((diag) => diag.category === ts.DiagnosticCategory.Error) - .map((err) => err.messageText) + // const emitResult = program.emit(); + const diagnostics = project + .getPreEmitDiagnostics() + .concat(emitResult.getDiagnostics()); + if (diagnostics.length > 0) { + const errors = diagnostics + .filter((diag) => diag.getCategory() === ts.DiagnosticCategory.Error) + .map((err) => err.getMessageText()) .join("\n"); - throw new Error( - `Compilation or provided TypeScript source failed: ${errors}` - ); + + throw new Error(`Compilation failed: ${errors}`); } // Find all the declared interfaces and enums. let declaredTypes: Set = new Set(); srcFile.forEachChild((child) => { - if (ts.isInterfaceDeclaration(child)) { - declaredTypes.add(child.name.getText(srcFile)); + if (child.isKind(ts.SyntaxKind.InterfaceDeclaration)) { + declaredTypes.add(child.getName()); } // Add the Enum to Grammar Register - if (ts.isEnumDeclaration(child)) { - declaredTypes.add(child.name.getText(srcFile)); + else if (child.isKind(ts.SyntaxKind.EnumDeclaration)) { + declaredTypes.add(child.getName()); } }); - // Reject when the root type is not found - if (!declaredTypes.has(rootType)) { - throw new Error( - `Root type ${rootType} is not one of the declared types ${declaredTypes}` - ); - } + // Get the default Grammar Register + const register = getDefaultGrammar(); // Import default grammar rules const grammar: Grammar = { @@ -263,16 +239,39 @@ export function compile(source: string, rootType: string): Grammar { }; srcFile.forEachChild((child) => { - if (ts.isInterfaceDeclaration(child)) { - const iface = handleInterface(child, srcFile, declaredTypes, register); - const ifaceGrammar = toGrammar(iface); - grammar.elements.unshift(...ifaceGrammar.elements); - } else if (ts.isEnumDeclaration(child)) { - const enumGrammar = handleEnum(child); - grammar.elements.unshift(enumGrammar); + switch (child.getKind()) { + case ts.SyntaxKind.InterfaceDeclaration: + const iface = handleInterface( + child as InterfaceDeclaration, + declaredTypes, + register + ); + const ifaceGrammar = toGrammar(iface); + grammar.elements.unshift(...ifaceGrammar.elements); + break; + case ts.SyntaxKind.EnumDeclaration: + const enumGrammar = handleEnum(child as EnumDeclaration); + grammar.elements.unshift(enumGrammar); + break; + case ts.SyntaxKind.EndOfFileToken: + case ts.SyntaxKind.EmptyStatement: + break; + default: + throw new Error( + `Invalid top-level declaration of kind ${child.getKindName()}: ${child.getText()}` + ); } }); + // Reject when the root type is not found + if (!declaredTypes.has(rootType)) { + throw new Error( + `Root type ${rootType} is not one of the declared types ${Array.from( + declaredTypes.values() + )}` + ); + } + grammar.elements.unshift({ identifier: "root", alternatives: [reference(toElementId(rootType))], diff --git a/src/grammar.ts b/src/grammar.ts index e9bca7c..f443f92 100644 --- a/src/grammar.ts +++ b/src/grammar.ts @@ -28,7 +28,6 @@ export interface RuleGroup { export interface RuleLiteral { type: "literal"; literal: string; - quote: boolean; } export interface RuleReference { @@ -91,9 +90,7 @@ function serializeGroup(rule: RuleGroup): string { } function serializeLiteralRule(rule: RuleLiteral): string { - return rule.quote - ? '"\\"" ' + JSON.stringify(rule.literal) + ' "\\""' - : JSON.stringify(rule.literal); + return JSON.stringify(rule.literal); } function serializeReference(rule: RuleReference): string { @@ -138,11 +135,10 @@ export function serializeGrammar(grammar: Grammar): string { return out; } -export function literal(value: string, quote: boolean = false): RuleLiteral { +export function literal(value: string): RuleLiteral { return { type: "literal", literal: value, - quote: quote, }; } diff --git a/src/index.ts b/src/index.ts index 49e70ad..06a72c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { compile } from "./compiler.js"; +import { compileSync } from "./compiler.js"; import { serializeGrammar, Grammar, @@ -23,7 +23,7 @@ import { } from "./grammar.js"; export { - compile, + compileSync as compile, serializeGrammar, Grammar, GrammarElement,