From d3ee00c233cd05d025a0035ce99d5a4e13152fd6 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sat, 24 Feb 2024 21:16:05 -0800 Subject: [PATCH 01/17] Add .tsbuildinfo to .gitignore --- .gitignore | 1 + packages/@glimmer-workspace/tsconfig.tsbuildinfo | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 packages/@glimmer-workspace/tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore index 7ea645bc14..529ce4972e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ **/node_modules/ **/.turbo +**/*.tsbuildinfo tmp/ /ts-dist/ **/ts-dist diff --git a/packages/@glimmer-workspace/tsconfig.tsbuildinfo b/packages/@glimmer-workspace/tsconfig.tsbuildinfo deleted file mode 100644 index b6e52e5a05..0000000000 --- a/packages/@glimmer-workspace/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.scripthost.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.full.d.ts","../../node_modules/.pnpm/rollup@3.21.5/node_modules/rollup/dist/rollup.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/globals.global.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/esbuild@0.17.18/node_modules/esbuild/lib/main.d.ts","../../node_modules/.pnpm/source-map-js@1.0.2/node_modules/source-map-js/source-map.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/comment.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/at-rule.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/rule.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/container.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/declaration.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/previous-map.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/input.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/warning.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/document.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/root.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/processor.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/node.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/list.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/postcss.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/postcss.d.mts","../../node_modules/.pnpm/vite@4.3.3_@types+node@20.1.0/node_modules/vite/dist/node/index.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/typescript.d.ts","./@glimmer/build/lib/config.d.ts","./@glimmer/build/lib/import-meta.d.ts","./@glimmer/build/index.d.ts","./@glimmer/build/plugins.d.ts","./@glimmer/build/dist/index.d.ts","./@glimmer/build/lib/inline.d.ts","./@glimmer/build/lib/replace.d.ts","./@glimmer/vm-babel-plugins/dist/index.d.cts","./@glimmer/vm-babel-plugins/dist/index.d.ts","../@types/qunit/index.d.ts","../../node_modules/.pnpm/@babel+types@7.21.5/node_modules/@babel/types/lib/index.d.ts","../../node_modules/.pnpm/@types+babel__generator@7.6.4/node_modules/@types/babel__generator/index.d.ts","../../node_modules/.pnpm/@babel+parser@7.21.8/node_modules/@babel/parser/typings/babel-parser.d.ts","../../node_modules/.pnpm/@types+babel__template@7.4.1/node_modules/@types/babel__template/index.d.ts","../../node_modules/.pnpm/@types+babel__traverse@7.18.5/node_modules/@types/babel__traverse/index.d.ts","../../node_modules/.pnpm/@types+babel__core@7.20.0/node_modules/@types/babel__core/index.d.ts","../../node_modules/.pnpm/@types+babel-plugin-macros@3.1.0/node_modules/@types/babel-plugin-macros/index.d.ts","../../node_modules/.pnpm/@types+prettier@2.7.2/node_modules/@types/prettier/index.d.ts","../../node_modules/.pnpm/@types+preval.macro@3.0.0/node_modules/@types/preval.macro/index.d.ts","../../node_modules/.pnpm/devtools-protocol@0.0.1120988/node_modules/devtools-protocol/types/protocol.d.ts","../../node_modules/.pnpm/devtools-protocol@0.0.1120988/node_modules/devtools-protocol/types/protocol-mapping.d.ts","../../node_modules/.pnpm/puppeteer@20.1.2_typescript@5.0.4/node_modules/puppeteer/lib/types.d.ts","../../node_modules/.pnpm/vite@4.3.5_@types+node@20.1.0/node_modules/vite/dist/node/index.d.ts"],"fileInfos":[{"version":"6a6b471e7e43e15ef6f8fe617a22ce4ecb0e34efa6c3dfcfe7cebd392bcca9d2","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"dc48272d7c333ccf58034c0026162576b7d50ea0e69c3b9292f803fc20720fd5","impliedFormat":1},{"version":"27147504487dc1159369da4f4da8a26406364624fa9bc3db632f7d94a5bae2c3","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"fcd3ecc9f764f06f4d5c467677f4f117f6abf49dee6716283aa204ff1162498b","affectsGlobalScope":true,"impliedFormat":1},{"version":"9a60b92bca4c1257db03b349d58e63e4868cfc0d1c8d0ba60c2dbc63f4e6c9f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5c5565225fce2ede835725a92a28ece149f83542aa4866cfb10290bff7b8996","affectsGlobalScope":true,"impliedFormat":1},{"version":"7d2dbc2a0250400af0809b0ad5f84686e84c73526de931f84560e483eb16b03c","affectsGlobalScope":true,"impliedFormat":1},{"version":"f296963760430fb65b4e5d91f0ed770a91c6e77455bacf8fa23a1501654ede0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5114a95689b63f96b957e00216bc04baf9e1a1782aa4d8ee7e5e9acbf768e301","affectsGlobalScope":true,"impliedFormat":1},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab22100fdd0d24cfc2cc59d0a00fc8cf449830d9c4030dc54390a46bd562e929","affectsGlobalScope":true,"impliedFormat":1},{"version":"f7bd636ae3a4623c503359ada74510c4005df5b36de7f23e1db8a5c543fd176b","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"0c20f4d2358eb679e4ae8a4432bdd96c857a2960fd6800b21ec4008ec59d60ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"36ae84ccc0633f7c0787bc6108386c8b773e95d3b052d9464a99cd9b8795fbec","affectsGlobalScope":true,"impliedFormat":1},{"version":"82d0d8e269b9eeac02c3bd1c9e884e85d483fcb2cd168bccd6bc54df663da031","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8deab98702588840be73d67f02412a2d45a417a3c097b2e96f7f3a42ac483d1","affectsGlobalScope":true,"impliedFormat":1},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"376d554d042fb409cb55b5cbaf0b2b4b7e669619493c5d18d5fa8bd67273f82a","affectsGlobalScope":true,"impliedFormat":1},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true,"impliedFormat":1},{"version":"c4138a3dd7cd6cf1f363ca0f905554e8d81b45844feea17786cdf1626cb8ea06","affectsGlobalScope":true,"impliedFormat":1},{"version":"6ff3e2452b055d8f0ec026511c6582b55d935675af67cdb67dd1dc671e8065df","affectsGlobalScope":true,"impliedFormat":1},{"version":"03de17b810f426a2f47396b0b99b53a82c1b60e9cba7a7edda47f9bb077882f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"8184c6ddf48f0c98429326b428478ecc6143c27f79b79e85740f17e6feb090f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"261c4d2cf86ac5a89ad3fb3fafed74cbb6f2f7c1d139b0540933df567d64a6ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"6af1425e9973f4924fca986636ac19a0cf9909a7e0d9d3009c349e6244e957b6","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"15a630d6817718a2ddd7088c4f83e4673fde19fa992d2eae2cf51132a302a5d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7e9f95a7387e3f66be0ed6db43600c49cec33a3900437ce2fd350d9b7cb16f2","affectsGlobalScope":true,"impliedFormat":1},{"version":"01e0ee7e1f661acedb08b51f8a9b7d7f959e9cdb6441360f06522cc3aea1bf2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac17a97f816d53d9dd79b0d235e1c0ed54a8cc6a0677e9a3d61efb480b2a3e4e","affectsGlobalScope":true,"impliedFormat":1},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true,"impliedFormat":1},{"version":"ec0104fee478075cb5171e5f4e3f23add8e02d845ae0165bfa3f1099241fa2aa","affectsGlobalScope":true,"impliedFormat":1},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"9cc66b0513ad41cb5f5372cca86ef83a0d37d1c1017580b7dace3ea5661836df","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"307c8b7ebbd7f23a92b73a4c6c0a697beca05b06b036c23a34553e5fe65e4fdc","affectsGlobalScope":true,"impliedFormat":1},{"version":"189c0703923150aa30673fa3de411346d727cc44a11c75d05d7cf9ef095daa22","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"322cc0ca9c311414642c0d7ef3b57beedbac198ca074e3e109a4be4c366dcb81","impliedFormat":1},{"version":"092d6ef5196e5c6487bcf68c86b8ce950cc6538e39d3d530ed05bc2b6d262562","impliedFormat":1},{"version":"66a6e7b6e32eea099615d627028ed0d681679e31237df03e68e1b912e3e61da0","impliedFormat":1},{"version":"a69c09dbea52352f479d3e7ac949fde3d17b195abe90b045d619f747b38d6d1a","impliedFormat":1},{"version":"f749812878fecfa53cfc13b36e5d35086fb6377983a9df44175da83ccc23af1f","affectsGlobalScope":true,"impliedFormat":1},{"version":"fd536fe411f46ae62c4729e342a3494adbdd54335e41039c238170c9781da9ff","impliedFormat":1},{"version":"772ff00e189d93488d1ca6f0f4dfba77bd090a99ed31e19a14bc16fad4078e48","affectsGlobalScope":true,"impliedFormat":1},{"version":"609fe91e93a4932286a0906c2afbd78f7d6fe5050d6ed5866c03f1c6da8df579","impliedFormat":1},{"version":"ac0c7cb0a4c1bec60be2c0460b3bda1e674eaf2c98312d7b6f16a0bb58718e2d","impliedFormat":1},{"version":"7e2181a6fc140b4525d5a45c204477c37fa78a635558e88552c68f76a4325403","affectsGlobalScope":true,"impliedFormat":1},{"version":"82408ed3e959ddc60d3e9904481b5a8dc16469928257af22a3f7d1a3bc7fd8c4","impliedFormat":1},{"version":"6a6bfa45d3ed96dea1ad653444fb26ddaa9245d2e2d6ddaab050cb9a2c0936d7","impliedFormat":1},{"version":"276b547eeb8eeeee9a446a3bfa6e07d1c0199269bdcf33813abab1281394a9cb","impliedFormat":1},{"version":"c999f7816955f75b447823e3e4085f699600e2a4a3327907df9af65b0a9c0df6","impliedFormat":1},{"version":"958df89fdcf5555cdfd57a14037e05505048dd29c59e8009bea279b65523b241","impliedFormat":1},{"version":"ffa8324a4c0611f9a50262fb36f097eeabe0fa0d71755aa67156da13cde1b932","impliedFormat":1},{"version":"9b814e0806922056145bedb096f11b73bdce70cc871f3ccfc4ce00b2cba75718","impliedFormat":1},{"version":"6b526a5ec4a401ca7c26cfe6a48e641d8f30af76673bad3b06a1b4504594a960","affectsGlobalScope":true,"impliedFormat":1},{"version":"be3d24f3fd8c85fedd91485b207d180f14ac2602805b52f6a33462ada14c27b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"e58cfbe7db26c2077027662ef6c87a3cf9d721717e77ecaa98c4d8d7911b3299","impliedFormat":1},{"version":"2ad6a251b6ef19fd1f8498f83bb7b265033bd52287e1f6569d09544f18806713","impliedFormat":1},{"version":"bfa08f2c30c475aef1c9451855ba6b2acfdc64f61950a38fae75806d66fb85c2","impliedFormat":1},{"version":"159807eb55a9439f9a675bd493788190a6203b5f36c315f8c3acbfcb875c7072","impliedFormat":1},{"version":"fe31b2b31ac5453fc7b8eef32b62017e55b214ceb884f0b177f442af92e84682","impliedFormat":1},{"version":"dd72576c8ea64d55af46a386068503d3cfcecce84ed7e1cbd4ff4081ba67fafc","impliedFormat":1},{"version":"125af9d85cb9d5e508353f10a8d52f01652d2d48b2cea54789a33e5b4d289c1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"70a7e8a7880d55396285e4b85ff5bdf3af3083176abe07f944967836f2a43188","impliedFormat":1},{"version":"3570df7c6f3a976109f55b596a2d88c2f87e0574cd1502272594ee5c4e56d0ef","impliedFormat":1},{"version":"850e95721334c2aa7697b08782f443ec4286274e5024169d4443933544f359d7","impliedFormat":1},{"version":"74e6cd21f7b5e29fab05060ea24e2b90aa254f16f3f62ccd7055bdb8fc7b2ff5","affectsGlobalScope":true,"impliedFormat":1},{"version":"5761c90b0cabdd6bd1f5fb1c3bf942088fdd39e18ed35dbe39b0c34bc733bf13","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eb6c5569d41e6021832d0a8a71f45fecbc13d03ad7d306da485999148b64955","impliedFormat":1},{"version":"c05ef0ecf06540ad3039515c10d3b27f9380639ced40f4093fd073e1b5ff21d9","impliedFormat":1},{"version":"c774096c5935712de41c763e3c08a04b7a788a85fb07a0c2df23fb5ace3bc610","impliedFormat":1},{"version":"2b847f2aba41dcd69677684d5d300c08aea4e306c305fd39bf548cef19f03cfe","impliedFormat":1},{"version":"f4c0cbc93eb51fd61e07a86111739770dd524f6297bd83266ff004aec553e085","impliedFormat":1},{"version":"9a134dbb29f0af914d90b23f609b39019d66ed53db7d492ab6b04c67114559da","impliedFormat":1},{"version":"1b952304137851e45bc009785de89ada562d9376177c97e37702e39e60c2f1ff","impliedFormat":1},{"version":"785e5be57d4f20f290a20e7b0c6263f6c57fd6e51283050756cef07d6d651c68","impliedFormat":1},{"version":"44b8b584a338b190a59f4f6929d072431950c7bd92ec2694821c11bce180c8a5","impliedFormat":1},{"version":"7b3781fbdfddbee8dba55ccee5aa74a7c8d6701ade11d49ab7d8cb1fcefe669e","impliedFormat":1},{"version":"c4aab2ec3a249f2a4caa9cbdb099752a80daf999b79d85aa3504cdfd6e559476","impliedFormat":1},{"version":"2cff47e15621f3055af1df1547426f833c9d0a571750c4f0659836f45c50fe0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"ad08154d9602429522cac965a715fde27d421d69b24756c5d291877dda75353e","impliedFormat":1},{"version":"c764a6cf523d13f2304a23216cd1084e28c041eebabd8aa9b2a9d99866c668c0","impliedFormat":1},{"version":"1272a5c2bd05961adc473e905332b7a422b00485c10b41c752f7fcf6835e3436","impliedFormat":1},{"version":"30ef92bf8135ce36ba1231fe41715276f2a40be72a478ddeb862bc16672e8680","impliedFormat":1},{"version":"4ace0a30a70fe5963442d75ea6e69f525671ae76f6e57ab7556c44839b4237e8","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f03dbf03c001fb3ac1c9bea6dde049dfff27ef8886cc4a886374aacf2e997d","affectsGlobalScope":true,"impliedFormat":1},{"version":"66bfb3de947abf4b117ee849c245425dbe494d6903e28f9ded566e91c9d05d77","impliedFormat":1},{"version":"c28d4f58131b93d60e087b86148d4e0c9d9b5c49c23ff1a9d1a9594fdedd5d08","impliedFormat":1},{"version":"c6b5d7f259544c91024ecf2b17138574a3f6ff2476468fafd7f957d2b68d6d98","impliedFormat":1},{"version":"1ec27c4b695590464113276d174f873e260e468ef226b7dc18f9193875fa559d","affectsGlobalScope":true,"impliedFormat":1},{"version":"33da4ee2ab9fdd9ef8b3fc526d871ce02ae8c825283f5695e7cad507c087b97c","impliedFormat":1},{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true,"impliedFormat":1},{"version":"71709584ed5ed7d236dc225441ec4634ffc6c718853e04b9c27b9ea121459044","impliedFormat":1},{"version":"99d951629f7096dcd79adbaa83a85e3be57613005533bd23029b3aba4ce9383e","impliedFormat":1},{"version":"858d0d831826c6eb563df02f7db71c90e26deadd0938652096bea3cc14899700","impliedFormat":1},{"version":"ae128dd11edfae24d9dfc6bc60390e488cec3792e4dd80115e422b5c2e1524cf","impliedFormat":1},{"version":"cef246ba183aff9bdb98831690ffcd1603655ea23ce38252e3db3ce3e06d262c","impliedFormat":1},{"version":"9d110b91e4c091ceee9d360ec425737580fd0d531bdb2d4f77e203c520d4da4c","impliedFormat":1},{"version":"96826ee13c04e92150ae68c6d89bf6ab7970abeca6e1a41cc59c5dcacefb4526","impliedFormat":1},{"version":"cb81640e895111bdf71201045f0f6664663cb0120e05a03f91e978b6a4ea2dd7","impliedFormat":1},{"version":"0d598aeda14bcdaa823b05a043eb7c18045c0a17fc3594d2387a11c09a5ff64b","impliedFormat":1},{"version":"be0db7e6060868cbabb02b3400fbaad4471d0815ce49d1b0f1e027296017b710","impliedFormat":1},{"version":"dbdec414a2de11b47a0bc8e2a0a93397235878ca7e6c02b62570e0a72faa878e","impliedFormat":1},{"version":"d9f79888a9ecee73c3cb33ae3eaaacb973dc96d0537599300d3dcb50ed1a1cf3","impliedFormat":1},{"version":"7ad14170a7f1f59dbe90ca3747699396f4a320921c7c560aa53f05022221df13","impliedFormat":1},{"version":"096c2a810f50f8a8b5bbb336ee32476d7882ffd93b2ee50ec5d4c1a9d5c209cf","impliedFormat":1},{"version":"6be9369a41da87241c164021631f20fe4706daf9f82a33c754974c6f4eade3cd","impliedFormat":1},{"version":"2eb26714666406e7c6c3ad3482da148781845b50a354a351d28bd5dba1a3f88a","impliedFormat":1},{"version":"022a6fbd269120763350030e66333f8c084a328ce7d6f6414d9cd49d834cbc77","impliedFormat":1},{"version":"4f85fea72dfeb24be05b650d680a21bda8324430bbfb437af9452630c9c740c1","impliedFormat":1},{"version":"179bfe3e835098360fdfa7f1c139c01b83c00410dccac214b3c71f16d949044b","impliedFormat":1},{"version":"18af117b1a77567a980c6ab6611be835e809300af6b511a2093208dc14ad2f7c","impliedFormat":1},{"version":"59345c3c2297677cb323f5f70df756776ae8eec2c97e6254e0739f711ab39131","impliedFormat":1},{"version":"77926a706478940016e826b162f95f8e4077b1ad3184b2592dc03bd8b33e0384","impliedFormat":99},{"version":"745dc480fe54b1c4f0207d9919020656218dfdab72b99ac5ef3b94666f1b8696","impliedFormat":99},{"version":"21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6","impliedFormat":1},{"version":"f57556e0e2930e617d5d9c441e40d3f32290a02c62ab1f1360ec4db52abdbf3a","impliedFormat":99},{"version":"b0e48ebeadb1bb0b4800ba7d4bcfb82135b3aeb088f7b20fe14d5ee9604b8656","impliedFormat":99},{"version":"8660efeb8b1f66e01a1a544d840fe8b58841c449fcdbb429687f20aee03509b9","impliedFormat":99},{"version":"4b6db46b7acd3e3e7c02288e2abbf62535814d776933bfc0764a46cb4e116951","impliedFormat":99},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":99},{"version":"0387301ca9e16c7b2b3e728f29e749b5415324b3700039a50758069a984d11a2","impliedFormat":99},{"version":"a6192dc1f77521c47fd57ffdb6ad5c6a920ab79641f0f61444720ac6d52227d6","impliedFormat":99},{"version":"649edc362664fa7aef2a923e62dd7a92d477cb486bdaff4577f33277259e9684","impliedFormat":1},{"version":"3835f8d92ad0699690cf572ad0da8aa3bfa5cc1c66fbd2609c52a02b9da828dc","impliedFormat":1},{"version":"314c601bedf7ae241feb4b274e519b9dd3f63fe22f7950c8a54f3dd81fda1ccf","affectsGlobalScope":true,"impliedFormat":1},{"version":"a20fc1fcd9cd7c2b79d5f00d14802c1d58c3848e09ee4f84b350591af88b7636","impliedFormat":1},{"version":"cc957354aa3c94c9961ebf46282cfde1e81d107fc5785a61f62c67f1dd3ac2eb","impliedFormat":1},{"version":"b4f76b34637d79cefad486127115fed843762c69512d7101b7096e1293699679","impliedFormat":1},{"version":"93de1c6dab503f053efe8d304cb522bb3a89feab8c98f307a674a4fae04773e9","impliedFormat":1},{"version":"dae3d1adc67ac3dbd1cd471889301339ec439837b5df565982345be20c8fca9a","impliedFormat":1},{"version":"b6ddf3a46ccfa4441d8be84d2e9bf3087573c48804196faedbd4a25b60631beb","impliedFormat":1},{"version":"7b7df5ade976f0049e32a75339dd1c0632da161c5168dda539b93d0f1c1911a3","impliedFormat":1},{"version":"bc88e4049153bc4dddb4503ed7e624eb141edfa9064b3659d6c86e900fe9e621","impliedFormat":1},{"version":"eee3f58de215609efb8e04b332b1df37519f204ee3fffaf1fa4bf0cd100eff18","impliedFormat":1},{"version":"757c84e91e2abbeec53b7884ebd7b524d32f1e93ceaf206f7ba4123bb57f3a11","impliedFormat":1},{"version":"2d03c4fb7c546057a09d18fc7cb0f09798978eb59ac7dde72cddf948d935fd91","impliedFormat":1},{"version":"9740fbc194086bf11aaee58266e503a5f79436d9490cbae2b93bc974aae4d320","impliedFormat":1}],"root":[[127,135]],"options":{"composite":true,"exactOptionalPropertyTypes":true,"module":199,"noImplicitOverride":true,"noPropertyAccessFromIndexSignature":true,"noUncheckedIndexedAccess":true,"skipLibCheck":true,"strict":true,"suppressImplicitAnyIndexErrors":false,"target":7,"useDefineForClassFields":false},"fileIdsList":[[96,137],[96],[96,142],[96,137,138,139,140,141],[96,137,139],[50,96],[53,96],[54,59,87,96],[55,66,67,74,84,95,96],[55,56,66,74,96],[57,96],[58,59,67,75,96],[59,84,92,96],[60,62,66,74,96],[61,96],[62,63,96],[66,96],[64,66,96],[66,67,68,84,95,96],[66,67,68,81,84,87,96],[96,100],[62,66,69,74,84,95,96],[66,67,69,70,74,84,92,95,96],[69,71,84,92,95,96],[50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],[66,72,96],[73,95,96],[62,66,74,84,96],[75,96],[76,96],[53,77,96],[78,94,96,100],[79,96],[80,96],[66,81,82,96],[81,83,96,98],[54,66,84,85,86,87,96],[54,84,86,96],[84,85,96],[87,96],[88,96],[84,96],[66,90,91,96],[90,91,96],[59,74,84,92,96],[93,96],[74,94,96],[54,69,80,95,96],[59,96],[84,96,97],[96,98],[96,99],[54,59,66,68,77,84,95,96,98,100],[84,96,101],[96,146],[96,109],[96,109,121],[96,106,107,108,110,121],[96,112],[96,109,116,120,123],[96,111,123],[96,114,116,119,120,123],[96,114,116,117,119,120,123],[96,106,107,108,109,110,112,113,114,115,116,120,123],[96,123],[96,105,106,107,108,109,110,112,113,114,115,116,117,119,120,121,122],[96,105,123],[96,116,117,118,120,123],[96,119,123],[96,109,115,120,123],[96,113,121],[55,84,96,103,146,147],[96,105],[49,66,67,69,71,74,84,92,95,96,101,103,104,124],[96,127,128],[49,96,125,126],[49,96],[49,96,126,149]],"referencedMap":[[139,1],[137,2],[143,3],[142,4],[138,1],[140,5],[141,1],[50,6],[51,6],[53,7],[54,8],[55,9],[56,10],[57,11],[58,12],[59,13],[60,14],[61,15],[62,16],[63,16],[65,17],[64,18],[66,17],[67,19],[68,20],[52,21],[102,2],[69,22],[70,23],[71,24],[103,25],[72,26],[73,27],[74,28],[75,29],[76,30],[77,31],[78,32],[79,33],[80,34],[81,35],[82,35],[83,36],[84,37],[86,38],[85,39],[87,40],[88,41],[89,42],[90,43],[91,44],[92,45],[93,46],[94,47],[95,48],[96,49],[97,50],[98,51],[99,52],[100,53],[101,54],[144,2],[145,2],[147,55],[146,2],[104,2],[107,56],[106,57],[109,58],[113,59],[110,57],[115,60],[112,61],[117,62],[122,2],[118,63],[121,64],[124,65],[123,66],[111,67],[119,68],[120,69],[116,70],[108,56],[114,71],[148,72],[49,2],[105,73],[46,2],[47,2],[8,2],[9,2],[13,2],[12,2],[2,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[3,2],[4,2],[25,2],[22,2],[23,2],[24,2],[26,2],[27,2],[28,2],[5,2],[29,2],[30,2],[31,2],[32,2],[6,2],[36,2],[33,2],[34,2],[35,2],[37,2],[7,2],[38,2],[48,2],[43,2],[44,2],[39,2],[40,2],[41,2],[42,2],[1,2],[45,2],[11,2],[10,2],[126,2],[125,74],[136,2],[131,2],[129,75],[127,76],[128,77],[132,77],[133,77],[130,2],[134,2],[135,2]],"exportedModulesMap":[[139,1],[137,2],[143,3],[142,4],[138,1],[140,5],[141,1],[50,6],[51,6],[53,7],[54,8],[55,9],[56,10],[57,11],[58,12],[59,13],[60,14],[61,15],[62,16],[63,16],[65,17],[64,18],[66,17],[67,19],[68,20],[52,21],[102,2],[69,22],[70,23],[71,24],[103,25],[72,26],[73,27],[74,28],[75,29],[76,30],[77,31],[78,32],[79,33],[80,34],[81,35],[82,35],[83,36],[84,37],[86,38],[85,39],[87,40],[88,41],[89,42],[90,43],[91,44],[92,45],[93,46],[94,47],[95,48],[96,49],[97,50],[98,51],[99,52],[100,53],[101,54],[144,2],[145,2],[147,55],[146,2],[104,2],[107,56],[106,57],[109,58],[113,59],[110,57],[115,60],[112,61],[117,62],[122,2],[118,63],[121,64],[124,65],[123,66],[111,67],[119,68],[120,69],[116,70],[108,56],[114,71],[148,72],[49,2],[105,73],[46,2],[47,2],[8,2],[9,2],[13,2],[12,2],[2,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[3,2],[4,2],[25,2],[22,2],[23,2],[24,2],[26,2],[27,2],[28,2],[5,2],[29,2],[30,2],[31,2],[32,2],[6,2],[36,2],[33,2],[34,2],[35,2],[37,2],[7,2],[38,2],[48,2],[43,2],[44,2],[39,2],[40,2],[41,2],[42,2],[1,2],[45,2],[11,2],[10,2],[126,2],[125,74],[136,2],[131,2],[129,75],[127,78],[128,77],[132,77],[133,77],[130,2],[134,2],[135,2]],"semanticDiagnosticsPerFile":[139,137,143,142,138,140,141,50,51,53,54,55,56,57,58,59,60,61,62,63,65,64,66,67,68,52,102,69,70,71,103,72,73,74,75,76,77,78,79,80,81,82,83,84,86,85,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,144,145,147,146,104,107,106,109,113,110,115,112,117,122,118,121,124,123,111,119,120,116,108,114,148,49,105,46,47,8,9,13,12,2,14,15,16,17,18,19,20,21,3,4,25,22,23,24,26,27,28,5,29,30,31,32,6,36,33,34,35,37,7,38,48,43,44,39,40,41,42,1,45,11,10,126,125,136,131,129,127,128,132,133,130,134,135],"latestChangedDtsFile":"../../ts-dist/build/@glimmer/vm-babel-plugins/test/fixtures/explicit-true/output.d.ts"},"version":"5.0.4"} \ No newline at end of file From 893b19461867ca936e71f11eb4a9051c65601907 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 15:42:48 -0800 Subject: [PATCH 02/17] Deprecate `Program` in v1 AST `{ type: "Program" }` isn't a real AST node you actually encounter, it was inherited from the Handlebars AST, but it has been split into AST.Template and AST.Block in Glimmer for a long time now. More throughly deprecate the `Program` visitor and builders. Also rename `blockParams` on `Template` to `locals`, and make both the array and the property immutable. We are only providing this information so that AST consumers can make decisions on what names are referring to outer local variables, but the compiler does not refer to it at all, and always use the original `options.locals` as the source of truth, so if any AST plugin tried to mutate this, it would not have worked. Keeping `blockParams` around as a deprecated alias for the time being. Consumers that need to support older versions should be able to feature detect this (such as with `"locals" in node`, or even just short-circuiting `node.locals ?? node.blockParams` to avoid triggering the deprecation warning. --- .../@glimmer/syntax/lib/generation/printer.ts | 11 +- packages/@glimmer/syntax/lib/parser.ts | 4 +- .../lib/parser/handlebars-node-visitors.ts | 1 - .../lib/parser/tokenizer-event-handlers.ts | 12 +-- .../@glimmer/syntax/lib/traversal/traverse.ts | 7 +- .../@glimmer/syntax/lib/traversal/visitor.ts | 10 ++ .../@glimmer/syntax/lib/traversal/walker.ts | 33 ++---- .../@glimmer/syntax/lib/v1/legacy-interop.ts | 40 +++++-- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 20 ++-- .../@glimmer/syntax/lib/v1/parser-builders.ts | 17 ++- .../@glimmer/syntax/lib/v1/public-builders.ts | 36 ++++--- .../@glimmer/syntax/lib/v1/visitor-keys.ts | 6 -- .../@glimmer/syntax/test/parser-node-test.ts | 102 +++++++++--------- .../@glimmer/syntax/test/plugin-node-test.ts | 66 ++++++++---- 14 files changed, 208 insertions(+), 157 deletions(-) diff --git a/packages/@glimmer/syntax/lib/generation/printer.ts b/packages/@glimmer/syntax/lib/generation/printer.ts index 535edad7ee..78a4960968 100644 --- a/packages/@glimmer/syntax/lib/generation/printer.ts +++ b/packages/@glimmer/syntax/lib/generation/printer.ts @@ -113,8 +113,6 @@ export default class Printer { case 'PathExpression': case 'SubExpression': return this.Expression(node); - case 'Program': - return this.Block(node); case 'ConcatStatement': // should have an AttrNode parent return this.ConcatStatement(node); @@ -174,15 +172,20 @@ export default class Printer { case 'ElementNode': return this.ElementNode(statement); case 'Block': - case 'Template': return this.Block(statement); + case 'Template': + return this.Template(statement); case 'AttrNode': // should have element return this.AttrNode(statement); } } - Block(block: ASTv1.Block | ASTv1.Program | ASTv1.Template): void { + Template(template: ASTv1.Template): void { + this.TopLevelStatements(template.body); + } + + Block(block: ASTv1.Block): void { /* When processing a template like: diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index a73410ad5b..0e7ffabac1 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -150,7 +150,9 @@ export abstract class Parser { } acceptTemplate(node: HBS.Program): ASTv1.Template { - return this[node.type as 'Program'](node) as ASTv1.Template; + let result = this.Program(node); + assert(result.type === 'Template', 'expected a template'); + return result; } acceptNode(node: HBS.Program): ASTv1.Block | ASTv1.Template; diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index c11b06a1b0..3c5d6cba1b 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -35,7 +35,6 @@ export abstract class HandlebarsNodeVisitors extends Parser { if (this.isTopLevel) { node = b.template({ body, - blockParams: program.blockParams, loc: this.source.spanFor(program.loc), }); } else { diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 6b0e39598a..7cf10390dc 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -461,21 +461,21 @@ export function preprocess( end: offsets.endPosition, }; - let program = new TokenizerEventHandlers(source, entityParser, mode).acceptTemplate(ast); + let template = new TokenizerEventHandlers(source, entityParser, mode).acceptTemplate(ast); - if (options.strictMode) { - program.blockParams = options.locals ?? []; + if (options.strictMode && options.locals?.length) { + template = b.template({ ...template, locals: options.locals }); } - if (options && options.plugins && options.plugins.ast) { + if (options?.plugins?.ast) { for (const transform of options.plugins.ast) { let env: ASTPluginEnvironment = assign({}, options, { syntax }, { plugins: undefined }); let pluginResult = transform(env); - traverse(program, pluginResult.visitor); + traverse(template, pluginResult.visitor); } } - return program; + return template; } diff --git a/packages/@glimmer/syntax/lib/traversal/traverse.ts b/packages/@glimmer/syntax/lib/traversal/traverse.ts index 81d0fc2128..80a86afc8f 100644 --- a/packages/@glimmer/syntax/lib/traversal/traverse.ts +++ b/packages/@glimmer/syntax/lib/traversal/traverse.ts @@ -67,8 +67,11 @@ function getNodeHandler( visitor: NodeVisitor, nodeType: N['type'] ): NodeTraversal | undefined { - if (nodeType === 'Template' || nodeType === 'Block') { - if (visitor.Program) { + if (visitor.Program) { + if ( + (nodeType === 'Template' && !visitor.Template) || + (nodeType === 'Block' && !visitor.Block) + ) { deprecate( `The 'Program' visitor node is deprecated. Use 'Template' or 'Block' instead (node was '${nodeType}') ` ); diff --git a/packages/@glimmer/syntax/lib/traversal/visitor.ts b/packages/@glimmer/syntax/lib/traversal/visitor.ts index 4e1069a969..3810a14061 100644 --- a/packages/@glimmer/syntax/lib/traversal/visitor.ts +++ b/packages/@glimmer/syntax/lib/traversal/visitor.ts @@ -13,6 +13,11 @@ export type NodeTraversal = FullNodeTraversal | NodeHan export type NodeVisitor = { [P in keyof ASTv1.Nodes]?: NodeTraversal } & { All?: NodeTraversal; + + /** + * @deprecated use Template or Block instead + */ + Program?: NodeTraversal; }; export interface FullKeyTraversal { @@ -27,4 +32,9 @@ export type KeyTraversal> = export type KeysVisitor = { [P in VisitorKey]?: KeyTraversal } & { All?: KeyTraversal>; + + /** + * @deprecated use Template or Block instead + */ + Program?: KeyTraversal; }; diff --git a/packages/@glimmer/syntax/lib/traversal/walker.ts b/packages/@glimmer/syntax/lib/traversal/walker.ts index 7be62d9fb8..232e616e37 100644 --- a/packages/@glimmer/syntax/lib/traversal/walker.ts +++ b/packages/@glimmer/syntax/lib/traversal/walker.ts @@ -33,40 +33,21 @@ export default class Walker { switch (node.type) { case 'Block': case 'Template': - return visitors.Program(this, node as unknown as ASTv1.Program, callback); + walkBody(this, node.body, callback); + return; case 'ElementNode': - return visitors.ElementNode(this, node, callback); + walkBody(this, node.children, callback); + return; case 'BlockStatement': - return visitors.BlockStatement(this, node, callback); + this.visit(node.program, callback); + this.visit(node.inverse || null, callback); + return; default: return; } } } -const visitors = { - Program(walker: Walker, node: ASTv1.Program, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - Template(walker: Walker, node: ASTv1.Template, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - Block(walker: Walker, node: ASTv1.Block, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - ElementNode(walker: Walker, node: ASTv1.ElementNode, callback: NodeCallback) { - walkBody(walker, node.children, callback); - }, - - BlockStatement(walker: Walker, node: ASTv1.BlockStatement, callback: NodeCallback) { - walker.visit(node.program, callback); - walker.visit(node.inverse || null, callback); - }, -} as const; - function walkBody( walker: Walker, body: ASTv1.Statement[], diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 686b22e9a1..6400698533 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -1,11 +1,39 @@ -import { asPresentArray, assertPresentArray, getFirst } from '@glimmer/util'; +import { asPresentArray, assertPresentArray, deprecate, getFirst } from '@glimmer/util'; import type { SourceSpan } from '../source/span'; -import type { PathExpression, PathHead } from './nodes-v1'; +import type * as ASTv1 from './nodes-v1'; import b from './public-builders'; -export class PathExpressionImplV1 implements PathExpression { +export type TemplateParams = Omit; + +export function buildLegacyTemplate({ body, locals, loc }: TemplateParams): ASTv1.Template { + const node = { + type: 'Template', + body, + loc, + }; + + Object.defineProperty(node, 'locals', { + enumerable: true, + writable: false, + value: Object.freeze([...locals]), + }); + + Object.defineProperty(node, 'blockParams', { + enumerable: false, + get(): readonly string[] { + deprecate( + `Template nodes can never have block params, for in-scope variables, use locals instead` + ); + return this.locals; + }, + }); + + return node as ASTv1.Template; +} + +export class PathExpressionImplV1 implements ASTv1.PathExpression { type = 'PathExpression' as const; public parts: string[]; public this = false; @@ -13,7 +41,7 @@ export class PathExpressionImplV1 implements PathExpression { constructor( public original: string, - head: PathHead, + head: ASTv1.PathHead, tail: string[], public loc: SourceSpan ) { @@ -32,9 +60,9 @@ export class PathExpressionImplV1 implements PathExpression { } // Cache for the head value. - _head?: PathHead = undefined; + _head?: ASTv1.PathHead = undefined; - get head(): PathHead { + get head(): ASTv1.PathHead { if (this._head) { return this._head; } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index e1bbc500d2..85b351a644 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -12,26 +12,31 @@ export interface BaseNode { export interface CommonProgram extends BaseNode { body: Statement[]; - blockParams: string[]; - chained?: boolean; -} - -export interface Program extends CommonProgram { - type: 'Program'; } export interface Block extends CommonProgram { type: 'Block'; + blockParams: string[]; blockParamNodes: BlockParam[]; + chained?: boolean; } export type EntityEncodingState = 'transformed' | 'raw'; export interface Template extends CommonProgram { type: 'Template'; + readonly locals: readonly string[]; + + /** + * @deprecated use locals instead + */ + readonly blockParams: readonly string[]; } -export type PossiblyDeprecatedBlock = Block | Template; +/** + * @deprecated use Template or Block instead + */ +export type Program = Template | Block; export interface CallParts { path: Expression; @@ -320,7 +325,6 @@ export type Nodes = SharedNodes & { ElementStartNode: ElementStartNode; ElementPartNode: ElementPartNode; ElementNameNode: ElementNameNode; - Program: Program; Template: Template; Block: Block; BlockStatement: BlockStatement; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 476a0f6b93..67e4c16f5a 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -6,7 +6,7 @@ import type { SourceLocation } from '../source/location'; import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; -import { PathExpressionImplV1 } from './legacy-interop'; +import { buildLegacyTemplate, PathExpressionImplV1 } from './legacy-interop'; const DEFAULT_STRIP = { close: false, @@ -51,20 +51,19 @@ class Builders { } template({ - body, - blockParams, + body = [], + locals = [], loc, }: { body?: ASTv1.Statement[]; - blockParams?: string[]; + locals?: string[]; loc: SourceSpan; }): ASTv1.Template { - return { - type: 'Template', - body: body || [], - blockParams: blockParams || [], + return buildLegacyTemplate({ + body, + locals, loc, - }; + }); } mustache({ diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 0a0e0cff8c..9a5e01d548 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -7,7 +7,7 @@ import type * as ASTv1 from './api'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; -import { PathExpressionImplV1 } from './legacy-interop'; +import { buildLegacyTemplate, PathExpressionImplV1 } from './legacy-interop'; let _SOURCE: Source | undefined; @@ -50,12 +50,14 @@ function buildMustache( }; } +type PossiblyDeprecatedBlock = ASTv1.Block | ASTv1.Template; + function buildBlock( path: BuilderHead, params: Nullable, hash: Nullable, - _defaultBlock: ASTv1.PossiblyDeprecatedBlock, - _elseBlock?: Nullable, + _defaultBlock: PossiblyDeprecatedBlock, + _elseBlock?: Nullable, loc?: SourceLocation, openStrip?: ASTv1.StripFlags, inverseStrip?: ASTv1.StripFlags, @@ -454,13 +456,14 @@ function buildProgram( body?: ASTv1.Statement[], blockParams?: string[], loc?: SourceLocation -): ASTv1.Template { - return { - type: 'Template', - body: body || [], - blockParams: blockParams || [], - loc: buildLoc(loc || null), - }; +): ASTv1.Template | ASTv1.Block { + deprecate(`b.program is deprecated. Use b.template or b.blockItself instead.`); + + if (blockParams && blockParams.length) { + return buildBlockItself(body, blockParams, false, loc); + } else { + return buildTemplate(body, [], loc); + } } function buildBlockItself( @@ -481,16 +484,15 @@ function buildBlockItself( } function buildTemplate( - body?: ASTv1.Statement[], - blockParams?: string[], + body: ASTv1.Statement[] = [], + locals: string[] = [], loc?: SourceLocation ): ASTv1.Template { - return { - type: 'Template', - body: body || [], - blockParams: blockParams || [], + return buildLegacyTemplate({ + body, + locals, loc: buildLoc(loc || null), - }; + }); } function buildPosition(line: number, column: number): SourcePosition { diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index 8f06a5986f..af7e8bdc12 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -3,7 +3,6 @@ import type * as ASTv1 from './api'; // ensure stays in sync with typing // ParentNode and ChildKey types are derived from VisitorKeysMap const visitorKeys = { - Program: ['body'], Template: ['body'], Block: ['body'], @@ -35,11 +34,6 @@ const visitorKeys = { Hash: ['pairs'], HashPair: ['value'], BlockParam: [], - - // v2 new nodes - NamedBlock: ['attributes', 'modifiers', 'children', 'comments'], - SimpleElement: ['attributes', 'modifiers', 'children', 'comments'], - Component: ['head', 'attributes', 'modifiers', 'children', 'comments'], } as const; type VisitorKeysMap = typeof visitorKeys; diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index f600fb873c..1f5c4d7186 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -11,17 +11,17 @@ QUnit.module('[glimmer-syntax] Parser - AST'); test('a simple piece of content', () => { let t = 'some content'; - astEqual(t, b.program([b.text('some content')])); + astEqual(t, b.template([b.text('some content')])); }); test('self-closed element', () => { let t = ''; - astEqual(t, b.program([element('g/')])); + astEqual(t, b.template([element('g/')])); }); test('elements can have empty attributes', () => { let t = ''; - astEqual(t, b.program([element('img', ['attrs', ['id', '']])])); + astEqual(t, b.template([element('img', ['attrs', ['id', '']])])); }); test('disallowed quote in element space is rejected', (assert) => { @@ -46,17 +46,17 @@ test('disallowed equals sign in element space is rejected', (assert) => { test('svg content', () => { let t = ''; - astEqual(t, b.program([element('svg')])); + astEqual(t, b.template([element('svg')])); }); test('html content with html content inline', () => { let t = '

'; - astEqual(t, b.program([element('div', ['body', element('p')])])); + astEqual(t, b.template([element('div', ['body', element('p')])])); }); test('html content with svg content inline', () => { let t = '
'; - astEqual(t, b.program([element('div', ['body', element('svg')])])); + astEqual(t, b.template([element('div', ['body', element('svg')])])); }); let integrationPoints = ['foreignObject', 'desc']; @@ -65,7 +65,7 @@ function buildIntegrationPointTest(integrationPoint: string) { let t = '<' + integrationPoint + '>
'; astEqual( t, - b.program([element('svg', ['body', element(integrationPoint, ['body', element('div')])])]) + b.template([element('svg', ['body', element(integrationPoint, ['body', element('div')])])]) ); }; } @@ -81,7 +81,7 @@ test('svg title with html content', () => { let t = '
'; astEqual( t, - b.program([element('svg', ['body', element('title', ['body', b.text('
')])])]) + b.template([element('svg', ['body', element('title', ['body', b.text('
')])])]) ); }); @@ -89,7 +89,7 @@ test('a piece of content with HTML', () => { let t = 'some
content
done'; astEqual( t, - b.program([b.text('some '), element('div', ['body', b.text('content')]), b.text(' done')]) + b.template([b.text('some '), element('div', ['body', b.text('content')]), b.text(' done')]) ); }); @@ -97,7 +97,7 @@ test('a piece of Handlebars with HTML', () => { let t = 'some
{{content}}
done'; astEqual( t, - b.program([ + b.template([ b.text('some '), element('div', ['body', b.mustache(b.path('content'))]), b.text(' done'), @@ -109,7 +109,7 @@ test('Handlebars embedded in an attribute (quoted)', () => { let t = 'some
content
done'; astEqual( t, - b.program([ + b.template([ b.text('some '), element( 'div', @@ -125,7 +125,7 @@ test('Handlebars embedded in an attribute (unquoted)', () => { let t = 'some
content
done'; astEqual( t, - b.program([ + b.template([ b.text('some '), element('div', ['attrs', ['class', b.mustache(b.path('foo'))]], ['body', b.text('content')]), b.text(' done'), @@ -137,14 +137,14 @@ test('Handlebars embedded in an attribute of a self-closing tag (unqouted)', () let t = ''; let el = element('input/', ['attrs', ['value', b.mustache(b.path('foo'))]]); - astEqual(t, b.program([el])); + astEqual(t, b.template([el])); }); test('Handlebars embedded in an attribute (sexprs)', () => { let t = 'some
content
done'; astEqual( t, - b.program([ + b.template([ b.text('some '), element( 'div', @@ -166,7 +166,7 @@ test('Handlebars embedded in an attribute with other content surrounding it', () let t = 'some content done'; astEqual( t, - b.program([ + b.template([ b.text('some '), element( 'a', @@ -185,7 +185,7 @@ test('A more complete embedding example', () => { " {{more 'embed'}}"; astEqual( t, - b.program([ + b.template([ b.mustache(b.path('embed')), b.text(' '), b.mustache(b.path('some'), [b.string('content')]), @@ -219,7 +219,7 @@ test('Simple embedded block helpers', () => { let t = '{{#if foo}}
{{content}}
{{/if}}'; astEqual( t, - b.program([ + b.template([ b.block( b.path('if'), [b.path('foo')], @@ -236,7 +236,7 @@ test('block params', (assert) => { blockParams: ['bar', 'baz', 'qux'], }); let mustache = b.block(b.path('Foo'), [], b.hash(), b.blockItself([], ['bar', 'baz', 'qux'])); - astEqual(t, b.program([element, mustache])); + astEqual(t, b.template([element, mustache])); assert.strictEqual(element.blockParamNodes.length, 3); assert.strictEqual(mustache.program.blockParamNodes.length, 3); assert.deepEqual( @@ -254,7 +254,7 @@ test('Involved block helper', () => { '

hi

content {{#testing shouldRender}}

Appears!

{{/testing}} more content here'; astEqual( t, - b.program([ + b.template([ element('p', ['body', b.text('hi')]), b.text(' content '), b.block( @@ -274,7 +274,7 @@ test('Element modifiers', () => { let t = "

Some content

"; astEqual( t, - b.program([ + b.template([ element( 'p', ['attrs', ['class', 'bar']], @@ -288,7 +288,7 @@ test('Element modifiers', () => { test('Element paths', (assert) => { let t = ""; const elem = element('bar.x.y', ['attrs', ['class', 'bar']]); - astEqual(t, b.program([elem])); + astEqual(t, b.template([elem])); assert.strictEqual(elem.parts.length, 3); assert.deepEqual( elem.parts.map((p) => p.value), @@ -298,34 +298,34 @@ test('Element paths', (assert) => { test('Tokenizer: MustacheStatement encountered in beforeAttributeName state', () => { let t = ''; - astEqual(t, b.program([element('input', ['modifiers', 'bar'])])); + astEqual(t, b.template([element('input', ['modifiers', 'bar'])])); }); test('Tokenizer: MustacheStatement encountered in attributeName state', () => { let t = ''; - astEqual(t, b.program([element('input', ['attrs', ['foo', '']], ['modifiers', ['bar']])])); + astEqual(t, b.template([element('input', ['attrs', ['foo', '']], ['modifiers', ['bar']])])); }); test('Tokenizer: MustacheStatement encountered in afterAttributeName state', () => { let t = ''; - astEqual(t, b.program([element('input', ['attrs', ['foo', '']], ['modifiers', 'bar'])])); + astEqual(t, b.template([element('input', ['attrs', ['foo', '']], ['modifiers', 'bar'])])); }); test('Tokenizer: MustacheStatement encountered in afterAttributeValue state', () => { let t = ''; - astEqual(t, b.program([element('input', ['attrs', ['foo', '1']], ['modifiers', ['bar']])])); + astEqual(t, b.template([element('input', ['attrs', ['foo', '1']], ['modifiers', ['bar']])])); }); test('Tokenizer: MustacheStatement encountered in afterAttributeValueQuoted state', () => { let t = ""; - astEqual(t, b.program([element('input', ['attrs', ['foo', '1']], ['modifiers', 'bar'])])); + astEqual(t, b.template([element('input', ['attrs', ['foo', '1']], ['modifiers', 'bar'])])); }); test('Stripping - mustaches', () => { let t = 'foo {{~content}} bar'; astEqual( t, - b.program([ + b.template([ b.text('foo'), b.mustache(b.path('content'), undefined, undefined, undefined, undefined, { open: true, @@ -338,7 +338,7 @@ test('Stripping - mustaches', () => { t = 'foo {{content~}} bar'; astEqual( t, - b.program([ + b.template([ b.text('foo '), b.mustache(b.path('content'), undefined, undefined, undefined, undefined, { open: false, @@ -353,7 +353,7 @@ test('Stripping - blocks', () => { let t = 'foo {{~#wat}}{{/wat}} bar'; astEqual( t, - b.program([ + b.template([ b.text('foo'), b.block(b.path('wat'), [], b.hash(), b.blockItself(), undefined, undefined, { open: true, @@ -366,7 +366,7 @@ test('Stripping - blocks', () => { t = 'foo {{#wat}}{{/wat~}} bar'; astEqual( t, - b.program([ + b.template([ b.text('foo '), b.block( b.path('wat'), @@ -388,7 +388,7 @@ test('Stripping - programs', () => { let t = '{{#wat~}} foo {{else}}{{/wat}}'; astEqual( t, - b.program([ + b.template([ b.block( b.path('wat'), [], @@ -404,7 +404,7 @@ test('Stripping - programs', () => { t = '{{#wat}} foo {{~else}}{{/wat}}'; astEqual( t, - b.program([ + b.template([ b.block( b.path('wat'), [], @@ -421,7 +421,7 @@ test('Stripping - programs', () => { t = '{{#wat}}{{else~}} foo {{/wat}}'; astEqual( t, - b.program([ + b.template([ b.block( b.path('wat'), [], @@ -438,7 +438,7 @@ test('Stripping - programs', () => { t = '{{#wat}}{{else}} foo {{~/wat}}'; astEqual( t, - b.program([ + b.template([ b.block( b.path('wat'), [], @@ -459,7 +459,7 @@ test('Stripping - removes unnecessary text nodes', () => { astEqual( t, - b.program([ + b.template([ b.block( b.path('each'), [], @@ -480,7 +480,7 @@ test('Whitespace control - linebreaks after blocks removed by default', () => { astEqual( t, - b.program([ + b.template([ b.block( b.path('each'), [], @@ -497,7 +497,7 @@ test('Whitespace control - preserve all whitespace if config is set', () => { astEqual( t, - b.program([ + b.template([ b.block( b.path('each'), [], @@ -518,13 +518,13 @@ skip('Awkward mustache in unquoted attribute value', () => { let t = '
'; astEqual( t, - b.program([element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo')])]])]) + b.template([element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo')])]])]) ); t = '
'; astEqual( t, - b.program([ + b.template([ element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo'), b.text('b')])]]), ]) ); @@ -532,20 +532,20 @@ skip('Awkward mustache in unquoted attribute value', () => { t = '
'; astEqual( t, - b.program([element('div', ['attrs', ['class', b.concat([b.mustache('foo'), b.text('b')])]])]) + b.template([element('div', ['attrs', ['class', b.concat([b.mustache('foo'), b.text('b')])]])]) ); }); test('an HTML comment', () => { let t = 'before after'; - astEqual(t, b.program([b.text('before '), b.comment(' some comment '), b.text(' after')])); + astEqual(t, b.template([b.text('before '), b.comment(' some comment '), b.text(' after')])); }); test('a Handlebars comment inside an HTML comment', () => { let t = 'before after'; astEqual( t, - b.program([ + b.template([ b.text('before '), b.comment(' some {{! nested thing }} comment '), b.text(' after'), @@ -557,7 +557,7 @@ test('a Handlebars comment', () => { let t = 'before {{! some comment }} after'; astEqual( t, - b.program([b.text('before '), b.mustacheComment(' some comment '), b.text(' after')]) + b.template([b.text('before '), b.mustacheComment(' some comment '), b.text(' after')]) ); }); @@ -565,7 +565,7 @@ test('a Handlebars comment in proper element space', () => { let t = 'before
after'; astEqual( t, - b.program([ + b.template([ b.text('before '), element( 'div', @@ -581,7 +581,7 @@ test('a Handlebars comment after a valueless attribute', () => { let t = ''; astEqual( t, - b.program([ + b.template([ element('input', ['attrs', ['foo', '']], ['comments', b.mustacheComment(' comment ')]), ]) ); @@ -637,25 +637,25 @@ test('a Handlebars comment in invalid element space', (assert) => { test('allow {{null}} to be passed as helper name', () => { let ast = parse('{{null}}'); - astEqual(ast, b.program([b.mustache(b.null())])); + astEqual(ast, b.template([b.mustache(b.null())])); }); test('allow {{null}} to be passed as a param', () => { let ast = parse('{{foo null}}'); - astEqual(ast, b.program([b.mustache(b.path('foo'), [b.null()])])); + astEqual(ast, b.template([b.mustache(b.path('foo'), [b.null()])])); }); test('allow {{undefined}} to be passed as helper name', () => { let ast = parse('{{undefined}}'); - astEqual(ast, b.program([b.mustache(b.undefined())])); + astEqual(ast, b.template([b.mustache(b.undefined())])); }); test('allow {{undefined}} to be passed as a param', () => { let ast = parse('{{foo undefined}}'); - astEqual(ast, b.program([b.mustache(b.path('foo'), [b.undefined()])])); + astEqual(ast, b.template([b.mustache(b.path('foo'), [b.undefined()])])); }); test('Handlebars partial should error', (assert) => { @@ -725,7 +725,7 @@ test('disallowed mustaches in the tagName space', (assert) => { test('mustache immediately followed by self closing tag does not error', () => { let ast = parse(''); let el = element('FooBar/', ['attrs', ['data-foo', b.mustache('blah')]]); - astEqual(ast, b.program([el])); + astEqual(ast, b.template([el])); }); QUnit.dump.maxDepth = 100; @@ -752,7 +752,7 @@ test('named blocks', () => { ['as', 'contents'] ), ]); - astEqual(ast, b.program([el])); + astEqual(ast, b.template([el])); }); test('path expression with "dangling dot" throws error', (assert) => { diff --git a/packages/@glimmer/syntax/test/plugin-node-test.ts b/packages/@glimmer/syntax/test/plugin-node-test.ts index 0e9e7cdc5f..8d725f157f 100644 --- a/packages/@glimmer/syntax/test/plugin-node-test.ts +++ b/packages/@glimmer/syntax/test/plugin-node-test.ts @@ -13,8 +13,8 @@ test('function based AST plugins can be provided to the compiler', (assert) => { () => ({ name: 'plugin-a', visitor: { - Program() { - assert.step('Program'); + Template() { + assert.step('Template'); assert.ok(true, 'transform was called!'); }, }, @@ -23,7 +23,7 @@ test('function based AST plugins can be provided to the compiler', (assert) => { }, }); - assert.verifySteps(['Program']); + assert.verifySteps(['Template']); }); test('plugins are provided the syntax package', (assert) => { @@ -43,6 +43,32 @@ test('plugins are provided the syntax package', (assert) => { assert.verifySteps(['syntax']); }); +test('deprecated program visitor', (assert) => { + let plugin = () => { + return { + name: 'plugin', + visitor: { + // eslint-disable-next-line deprecation/deprecation + Program(node: AST.Program) { + assert.step(node.type); + }, + + BlockStatement(node: AST.BlockStatement) { + assert.step(node.type); + }, + }, + }; + }; + + preprocess('Hello {{#inner}}world{{/inner}}', { + plugins: { + ast: [plugin], + }, + }); + + assert.verifySteps(['Template', 'BlockStatement', 'Block']); +}); + test('can support the legacy AST transform API via ASTPlugin', (assert) => { function ensurePlugin(FunctionOrPlugin: any): ASTPluginBuilder { if (FunctionOrPlugin.prototype && FunctionOrPlugin.prototype.transform) { @@ -51,7 +77,7 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => { name: 'plugin-a', visitor: { - Program(node: AST.Program) { + Template(node: AST.Template) { let plugin = new FunctionOrPlugin(env); plugin.syntax = env.syntax; @@ -69,9 +95,9 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => { class Plugin { declare syntax: Syntax; - transform(program: AST.Program): AST.Program { + transform(template: AST.Template): AST.Template { assert.ok(true, 'transform was called!'); - return program; + return template; } } @@ -82,18 +108,18 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => { }); }); -const FIRST_PLUGIN = new WeakMap(); -const SECOND_PLUGIN = new WeakMap(); -const THIRD_PLUGIN = new WeakMap(); +const FIRST_PLUGIN = new WeakMap(); +const SECOND_PLUGIN = new WeakMap(); +const THIRD_PLUGIN = new WeakMap(); test('AST plugins can be chained', (assert) => { let first = () => { return { name: 'first', visitor: { - Program(program: AST.Program | AST.Template | AST.Block) { - assert.step('Program first'); - FIRST_PLUGIN.set(program, true); + Template(template: AST.Template) { + assert.step('Template first'); + FIRST_PLUGIN.set(template, true); }, }, }; @@ -103,8 +129,8 @@ test('AST plugins can be chained', (assert) => { return { name: 'second', visitor: { - Program(node: AST.Program | AST.Block | AST.Template) { - assert.step('Program second'); + Template(node: AST.Template) { + assert.step('Template second'); assert.true(FIRST_PLUGIN.get(node), 'AST from first plugin is passed to second'); SECOND_PLUGIN.set(node, true); @@ -117,8 +143,8 @@ test('AST plugins can be chained', (assert) => { return { name: 'third', visitor: { - Program(node: AST.Program | AST.Block | AST.Template) { - assert.step('Program third'); + Template(node: AST.Template) { + assert.step('Template third'); assert.true(SECOND_PLUGIN.get(node), 'AST from second plugin is passed to third'); THIRD_PLUGIN.set(node, true); @@ -135,7 +161,7 @@ test('AST plugins can be chained', (assert) => { assert.true(THIRD_PLUGIN.get(ast), 'return value from last AST transform is used'); - assert.verifySteps(['Program first', 'Program second', 'Program third']); + assert.verifySteps(['Template first', 'Template second', 'Template third']); }); test('AST plugins can access meta from environment', (assert) => { @@ -143,8 +169,8 @@ test('AST plugins can access meta from environment', (assert) => { return { name: 'exposedMetaTemplateData', visitor: { - Program() { - assert.step('Program'); + Template() { + assert.step('Template'); const { meta } = env; const { moduleName } = expectPresent( meta as { moduleName: 'string' }, @@ -170,5 +196,5 @@ test('AST plugins can access meta from environment', (assert) => { }, }); - assert.verifySteps(['Program']); + assert.verifySteps(['Template']); }); From 02313376f21a606841b00a7789fd6128a1b292af Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 20:45:04 -0800 Subject: [PATCH 03/17] Deprecate escaped, parts, this, data on v1 AST nodes These were marked as `@deprecated` in the code for a long time, but we never actaully issued deprecation warnings at runtime. These features were available for a very long time, so consumers shouldn't have a problem switching to the recommended properties at this point. --- .../@glimmer/syntax/lib/generation/printer.ts | 4 +- .../lib/parser/handlebars-node-visitors.ts | 8 +- .../@glimmer/syntax/lib/v1/legacy-interop.ts | 141 +++++++++++------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 13 +- .../@glimmer/syntax/lib/v1/parser-builders.ts | 28 +--- .../@glimmer/syntax/lib/v1/public-builders.ts | 58 ++----- packages/@glimmer/syntax/lib/v2/normalize.ts | 6 +- .../test/traversal/manipulating-node-test.ts | 22 +-- 8 files changed, 141 insertions(+), 139 deletions(-) diff --git a/packages/@glimmer/syntax/lib/generation/printer.ts b/packages/@glimmer/syntax/lib/generation/printer.ts index 78a4960968..ae0df53bed 100644 --- a/packages/@glimmer/syntax/lib/generation/printer.ts +++ b/packages/@glimmer/syntax/lib/generation/printer.ts @@ -323,7 +323,7 @@ export default class Printer { return; } - this.buffer += mustache.escaped ? '{{' : '{{{'; + this.buffer += mustache.trusting ? '{{{' : '{{'; if (mustache.strip.open) { this.buffer += '~'; @@ -337,7 +337,7 @@ export default class Printer { this.buffer += '~'; } - this.buffer += mustache.escaped ? '}}' : '}}}'; + this.buffer += mustache.trusting ? '}}}' : '}}'; } BlockStatement(block: ASTv1.BlockStatement): void { diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 3c5d6cba1b..2a90f09ccc 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -10,7 +10,7 @@ import { Parser } from '../parser'; import { NON_EXISTENT_LOCATION } from '../source/location'; import { generateSyntaxError } from '../syntax-error'; import { appendChild, isHBSLiteral, parseProgramBlockParamsLocs, printLiteral } from '../utils'; -import { PathExpressionImplV1 } from '../v1/legacy-interop'; +import { buildLegacyPath } from '../v1/legacy-interop'; import b from '../v1/parser-builders'; const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState; @@ -382,7 +382,11 @@ export abstract class HandlebarsNodeVisitors extends Parser { }; } - return new PathExpressionImplV1(path.original, pathHead, parts, this.source.spanFor(path.loc)); + return buildLegacyPath({ + head: pathHead, + tail: parts, + loc: this.source.spanFor(path.loc), + }); } Hash(hash: HBS.Hash): ASTv1.Hash { diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 6400698533..30b6d7f370 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -1,6 +1,6 @@ -import { asPresentArray, assertPresentArray, deprecate, getFirst } from '@glimmer/util'; +import type { PresentArray } from '@glimmer/interfaces/index'; +import { asPresentArray, deprecate } from '@glimmer/util'; -import type { SourceSpan } from '../source/span'; import type * as ASTv1 from './nodes-v1'; import b from './public-builders'; @@ -33,59 +33,100 @@ export function buildLegacyTemplate({ body, locals, loc }: TemplateParams): ASTv return node as ASTv1.Template; } -export class PathExpressionImplV1 implements ASTv1.PathExpression { - type = 'PathExpression' as const; - public parts: string[]; - public this = false; - public data = false; - - constructor( - public original: string, - head: ASTv1.PathHead, - tail: string[], - public loc: SourceSpan - ) { - let parts = tail.slice(); - - if (head.type === 'ThisHead') { - this.this = true; - } else if (head.type === 'AtHead') { - this.data = true; - parts.unshift(head.name.slice(1)); - } else { - parts.unshift(head.name); - } - - this.parts = parts; - } - - // Cache for the head value. - _head?: ASTv1.PathHead = undefined; +export type MustacheStatementParams = Omit; - get head(): ASTv1.PathHead { - if (this._head) { - return this._head; - } +export function buildLegacyMustache({ + path, + params, + hash, + trusting, + strip, + loc, +}: MustacheStatementParams): ASTv1.MustacheStatement { + const node = { + type: 'MustacheStatement', + path, + params, + hash, + trusting, + strip, + loc, + }; - let firstPart: string; + Object.defineProperty(node, 'escaped', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The escaped property on mustache nodes is deprecated, use trusting instead`); + return !this.trusting; + }, + set(this: typeof node, value: boolean) { + deprecate(`The escaped property on mustache nodes is deprecated, use trusting instead`); + this.trusting = !value; + }, + }); - if (this.this) { - firstPart = 'this'; - } else if (this.data) { - firstPart = `@${getFirst(asPresentArray(this.parts))}`; - } else { - assertPresentArray(this.parts); - firstPart = getFirst(this.parts); - } + return node as ASTv1.MustacheStatement; +} - let firstPartLoc = this.loc.collapse('start').sliceStartChars({ - chars: firstPart.length, - }).loc; +export type PathExpressionParams = Omit; - return (this._head = b.head(firstPart, firstPartLoc)); +function original(head: ASTv1.PathHead, tail: readonly string[]): string { + switch (head.type) { + case 'ThisHead': + return ['this', ...tail].join('.'); + case 'AtHead': + case 'VarHead': + return [head.name, ...tail].join('.'); } +} - get tail(): string[] { - return this.this ? this.parts : this.parts.slice(1); - } +export function buildLegacyPath({ head, tail, loc }: PathExpressionParams): ASTv1.PathExpression { + const node = { + type: 'PathExpression', + head, + tail, + loc, + }; + + Object.defineProperty(node, 'original', { + enumerable: true, + get(this: typeof node): string { + return original(this.head, this.tail); + }, + set(this: typeof node, value: string) { + let [head, ...tail] = asPresentArray(value.split('.')); + this.head = b.head(head, this.head.loc); + this.tail = tail; + }, + }); + + Object.defineProperty(node, 'parts', { + enumerable: false, + get(this: { original: string }): readonly string[] { + deprecate(`The parts property on path nodes is deprecated, use trusting instead`); + return Object.freeze(this.original.split('.')); + }, + set(this: { original: string }, value: PresentArray) { + deprecate(`The parts property on mustache nodes is deprecated, use trusting instead`); + this.original = value.join('.'); + }, + }); + + Object.defineProperty(node, 'this', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The this property on path nodes is deprecated, use head.type instead`); + return this.head.type === 'ThisHead'; + }, + }); + + Object.defineProperty(node, 'data', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The data property on path nodes is deprecated, use head.type instead`); + return this.head.type === 'AtHead'; + }, + }); + + return node as ASTv1.PathExpression; } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 85b351a644..0541faa9e0 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -62,10 +62,13 @@ export interface MustacheStatement extends BaseNode { path: Expression; params: Expression[]; hash: Hash; - /** @deprecated */ - escaped: boolean; trusting: boolean; strip: StripFlags; + + /** + * @deprecated use trusting instead + */ + escaped: boolean; } export interface BlockStatement extends BaseNode { @@ -230,15 +233,15 @@ export interface PathExpression extends MinimalPathExpression { /** * @deprecated use `head` and `tail` instead */ - parts: string[]; + parts: readonly string[]; /** * @deprecated use `head.type` instead */ - this: boolean; + readonly this: boolean; /** * @deprecated use `head.type' instead */ - data: boolean; + readonly data: boolean; } export type LiteralName = diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 67e4c16f5a..e349762961 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -6,7 +6,7 @@ import type { SourceLocation } from '../source/location'; import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; -import { buildLegacyTemplate, PathExpressionImplV1 } from './legacy-interop'; +import { buildLegacyMustache, buildLegacyPath, buildLegacyTemplate } from './legacy-interop'; const DEFAULT_STRIP = { close: false, @@ -81,16 +81,14 @@ class Builders { loc: SourceSpan; strip: ASTv1.StripFlags; }): ASTv1.MustacheStatement { - return { - type: 'MustacheStatement', + return buildLegacyMustache({ path, params, hash, - escaped: !trusting, trusting, + strip, loc, - strip: strip || { open: false, close: false }, - }; + }); } block({ @@ -269,10 +267,7 @@ class Builders { tail: string[]; loc: SourceSpan; }): ASTv1.PathExpression { - let { original: originalHead } = headToString(head); - let original = [...originalHead, ...tail].join('.'); - - return new PathExpressionImplV1(original, head, tail, loc); + return buildLegacyPath({ head, tail, loc }); } head(head: string, loc: SourceSpan): ASTv1.PathHead { @@ -423,17 +418,4 @@ export interface BuildElementOptions { loc: SourceSpan; } -// Expressions - -function headToString(head: ASTv1.PathHead): { original: string; parts: string[] } { - switch (head.type) { - case 'AtHead': - return { original: head.name, parts: [head.name] }; - case 'ThisHead': - return { original: `this`, parts: [] }; - case 'VarHead': - return { original: head.name, parts: [head.name] }; - } -} - export default new Builders(); diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 9a5e01d548..2c5dd0263f 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -7,7 +7,7 @@ import type * as ASTv1 from './api'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; -import { buildLegacyTemplate, PathExpressionImplV1 } from './legacy-interop'; +import { buildLegacyMustache, buildLegacyPath, buildLegacyTemplate } from './legacy-interop'; let _SOURCE: Source | undefined; @@ -28,26 +28,20 @@ export type TagDescriptor = string | { name: string; selfClosing: boolean }; function buildMustache( path: BuilderHead | ASTv1.Literal, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, - raw?: boolean, + params: ASTv1.Expression[] = [], + hash: ASTv1.Hash = buildHash([]), + trusting = false, loc?: SourceLocation, - strip?: ASTv1.StripFlags + strip: ASTv1.StripFlags = { open: false, close: false } ): ASTv1.MustacheStatement { - if (typeof path === 'string') { - path = buildPath(path); - } - - return { - type: 'MustacheStatement', - path, - params: params || [], - hash: hash || buildHash([]), - escaped: !raw, - trusting: !!raw, + return buildLegacyMustache({ + path: buildPath(path), + params, + hash, + trusting, + strip, loc: buildLoc(loc || null), - strip: strip || { open: false, close: false }, - }; + }); } type PossiblyDeprecatedBlock = ASTv1.Block | ASTv1.Template; @@ -286,17 +280,6 @@ function buildSexpr( }; } -function headToString(head: ASTv1.PathHead): { original: string; parts: string[] } { - switch (head.type) { - case 'AtHead': - return { original: head.name, parts: [head.name] }; - case 'ThisHead': - return { original: `this`, parts: [] }; - case 'VarHead': - return { original: head.name, parts: [head.name] }; - } -} - function buildHead( original: string, loc: SourceLocation @@ -376,11 +359,7 @@ function buildCleanPath( tail: string[], loc: SourceLocation ): ASTv1.PathExpression { - let { original: originalHead, parts: headParts } = headToString(head); - let parts = [...headParts, ...tail]; - let original = [...originalHead, ...parts].join('.'); - - return new PathExpressionImplV1(original, head, tail, buildLoc(loc || null)); + return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); } function buildPath( @@ -404,20 +383,13 @@ function buildPath( `builder.path({ head, tail }) should not be called with a head with dots in it` ); - let { original: originalHead } = headToString(head); - - return new PathExpressionImplV1( - [originalHead, ...tail].join('.'), - head, - tail, - buildLoc(loc || null) - ); + return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); } } let { head, tail } = buildHead(path, SourceSpan.broken()); - return new PathExpressionImplV1(path, head, tail, buildLoc(loc || null)); + return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); } function buildLiteral( diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index aaf120fac6..9a174ed658 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -372,7 +372,7 @@ class StatementNormalizer { * Normalizes an ASTv1.MustacheStatement to an ASTv2.AppendStatement */ MustacheStatement(mustache: ASTv1.MustacheStatement): ASTv2.AppendContent { - let { escaped } = mustache; + let { trusting } = mustache; let loc = this.block.loc(mustache.loc); // Normalize the call parts in AppendSyntaxContext @@ -392,7 +392,7 @@ class StatementNormalizer { return this.block.builder.append( { table: this.block.table, - trusting: !escaped, + trusting, value, }, loc @@ -562,7 +562,7 @@ class ElementNormalizer { } { switch (part.type) { case 'MustacheStatement': - return { expr: this.mustacheAttr(part), trusting: !part.escaped }; + return { expr: this.mustacheAttr(part), trusting: part.trusting }; case 'TextNode': return { expr: this.ctx.builder.literal(part.chars, this.ctx.loc(part.loc)), diff --git a/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts b/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts index e22df27f47..8fe5eaa425 100644 --- a/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts +++ b/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts @@ -23,7 +23,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName]: (node: AST.MustacheStatement) => { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') { + if (node.path.type === 'PathExpression' && node.path.original === 'z') { return null; } return; @@ -45,7 +45,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') { + if (node.path.type === 'PathExpression' && node.path.original === 'z') { return []; } return; @@ -63,7 +63,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') { + if (node.path.type === 'PathExpression' && node.path.original === 'z') { return b.mustache('a'); } return; @@ -82,7 +82,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') { + if (node.path.type === 'PathExpression' && node.path.original === 'z') { return [b.mustache('a')]; } return; @@ -106,7 +106,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') { + if (node.path.type === 'PathExpression' && node.path.original === 'z') { return [b.mustache('a'), b.mustache('b'), b.mustache('c')]; } return; @@ -125,7 +125,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return null; } return; @@ -142,7 +142,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return []; } return; @@ -159,7 +159,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return b.mustache('a'); } return; @@ -178,7 +178,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return [b.mustache('a')]; } return; @@ -198,7 +198,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating'); traverse(ast, { MustacheStatement: { [eventName](node: AST.MustacheStatement) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return [b.mustache('a'), b.mustache('b'), b.mustache('c')]; } return; @@ -218,7 +218,7 @@ QUnit.test('Inside of a block', () => { traverse(ast, { MustacheStatement(node) { - if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') { + if (node.path.type === 'PathExpression' && node.path.original === 'y') { return [b.mustache('a'), b.mustache('b'), b.mustache('c')]; } return; From 5d523087a27b736efc83eeb08a55a5fd159d4092 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 22:26:25 -0800 Subject: [PATCH 04/17] Drop the defunct PartialStatementNode from v1 AST Note that this is referring to the Handlebars `{{> ... }}` syntax, not the (also defunct) Ember `{{partial ...}}` feature. This had never worked in any modern versions of Ember (maybe v1+) and the compiler doesn't do anything with it. --- .../@glimmer/syntax/lib/generation/printer.ts | 15 --------------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 11 ----------- .../@glimmer/syntax/lib/v1/public-builders.ts | 19 ------------------- .../@glimmer/syntax/lib/v1/visitor-keys.ts | 1 - packages/@glimmer/syntax/lib/v2/normalize.ts | 2 -- 5 files changed, 48 deletions(-) diff --git a/packages/@glimmer/syntax/lib/generation/printer.ts b/packages/@glimmer/syntax/lib/generation/printer.ts index ae0df53bed..c8327485b0 100644 --- a/packages/@glimmer/syntax/lib/generation/printer.ts +++ b/packages/@glimmer/syntax/lib/generation/printer.ts @@ -96,7 +96,6 @@ export default class Printer { switch (node.type) { case 'MustacheStatement': case 'BlockStatement': - case 'PartialStatement': case 'MustacheCommentStatement': case 'CommentStatement': case 'TextNode': @@ -161,8 +160,6 @@ export default class Printer { return this.MustacheStatement(statement); case 'BlockStatement': return this.BlockStatement(statement); - case 'PartialStatement': - return this.PartialStatement(statement); case 'MustacheCommentStatement': return this.MustacheCommentStatement(statement); case 'CommentStatement': @@ -388,18 +385,6 @@ export default class Printer { this.buffer += ` as |${blockParams.join(' ')}|`; } - PartialStatement(partial: ASTv1.PartialStatement): void { - if (this.handledByOverride(partial)) { - return; - } - - this.buffer += '{{>'; - this.Expression(partial.name); - this.Params(partial.params); - this.Hash(partial.hash); - this.buffer += '}}'; - } - ConcatStatement(concat: ASTv1.ConcatStatement): void { if (this.handledByOverride(concat)) { return; diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 0541faa9e0..66902562c8 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -93,15 +93,6 @@ export interface ElementModifierStatement extends BaseNode { hash: Hash; } -export interface PartialStatement extends BaseNode { - type: 'PartialStatement'; - name: PathExpression | SubExpression; - params: Expression[]; - hash: Hash; - indent: string; - strip: StripFlags; -} - export interface CommentStatement extends BaseNode { type: 'CommentStatement'; value: string; @@ -167,7 +158,6 @@ export type StatementName = | 'MustacheStatement' | 'CommentStatement' | 'BlockStatement' - | 'PartialStatement' | 'MustacheCommentStatement' | 'TextNode' | 'ElementNode'; @@ -318,7 +308,6 @@ export type SharedNodes = { UndefinedLiteral: UndefinedLiteral; MustacheStatement: MustacheStatement; ElementModifierStatement: ElementModifierStatement; - PartialStatement: PartialStatement; AttrNode: AttrNode; ConcatStatement: ConcatStatement; }; diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 2c5dd0263f..7d418602b8 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -105,24 +105,6 @@ function buildElementModifier( }; } -function buildPartial( - name: ASTv1.PathExpression, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, - indent?: string, - loc?: SourceLocation -): ASTv1.PartialStatement { - return { - type: 'PartialStatement', - name: name, - params: params || [], - hash: hash || buildHash([]), - indent: indent || '', - strip: { open: false, close: false }, - loc: buildLoc(loc || null), - }; -} - function buildComment(value: string, loc?: SourceLocation): ASTv1.CommentStatement { return { type: 'CommentStatement', @@ -512,7 +494,6 @@ function buildLoc(...args: any[]): SourceSpan { export default { mustache: buildMustache, block: buildBlock, - partial: buildPartial, comment: buildComment, mustacheComment: buildMustacheComment, element: buildElement, diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index af7e8bdc12..b319365333 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -9,7 +9,6 @@ const visitorKeys = { MustacheStatement: ['path', 'params', 'hash'], BlockStatement: ['path', 'params', 'hash', 'program', 'inverse'], ElementModifierStatement: ['path', 'params', 'hash'], - PartialStatement: ['name', 'params', 'hash'], CommentStatement: [], MustacheCommentStatement: [], ElementNode: ['attributes', 'modifiers', 'children', 'comments'], diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index 9a174ed658..5ffee1f6a9 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -323,8 +323,6 @@ class StatementNormalizer { normalize(node: ASTv1.Statement): ASTv2.ContentNode | ASTv2.NamedBlock { switch (node.type) { - case 'PartialStatement': - throw new Error(`Handlebars partial syntax ({{> ...}}) is not allowed in Glimmer`); case 'BlockStatement': return this.BlockStatement(node); case 'ElementNode': From db16ab9f0123b27706e8724887c2b83d985d71df Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 21:59:19 -0800 Subject: [PATCH 05/17] Revert "Merge pull request #1552 from patricklx/param-node" This reverts commit c7f954bb13410049a173742ace6fe592e3b2aa7e, reversing changes made to e934005154977862a0a73e789537e24f836fa097. --- .../lib/parser/handlebars-node-visitors.ts | 3 +- packages/@glimmer/syntax/lib/utils.ts | 53 ++----------------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 13 ----- .../@glimmer/syntax/lib/v1/parser-builders.ts | 7 +-- .../@glimmer/syntax/lib/v1/public-builders.ts | 4 -- .../@glimmer/syntax/lib/v1/visitor-keys.ts | 1 - .../@glimmer/syntax/test/loc-node-test.ts | 26 --------- .../@glimmer/syntax/test/parser-node-test.ts | 21 -------- 8 files changed, 7 insertions(+), 121 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 2a90f09ccc..bfc4ff59ef 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -9,7 +9,7 @@ import type * as HBS from '../v1/handlebars-ast'; import { Parser } from '../parser'; import { NON_EXISTENT_LOCATION } from '../source/location'; import { generateSyntaxError } from '../syntax-error'; -import { appendChild, isHBSLiteral, parseProgramBlockParamsLocs, printLiteral } from '../utils'; +import { appendChild, isHBSLiteral, printLiteral } from '../utils'; import { buildLegacyPath } from '../v1/legacy-interop'; import b from '../v1/parser-builders'; @@ -108,7 +108,6 @@ export abstract class HandlebarsNodeVisitors extends Parser { inverseStrip: block.inverseStrip, closeStrip: block.closeStrip, }); - parseProgramBlockParamsLocs(this.source, node); const parentProgram = this.currentElement(); diff --git a/packages/@glimmer/syntax/lib/utils.ts b/packages/@glimmer/syntax/lib/utils.ts index 9aeb3793cf..12decf566b 100644 --- a/packages/@glimmer/syntax/lib/utils.ts +++ b/packages/@glimmer/syntax/lib/utils.ts @@ -1,7 +1,7 @@ import type { Nullable } from '@glimmer/interfaces'; import { expect, unwrap } from '@glimmer/util'; -import type * as src from './source/api'; +import type { src } from '..'; import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; @@ -18,31 +18,7 @@ let ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u; export function parseElementBlockParams(element: ASTv1.ElementNode): void { let params = parseBlockParams(element); - if (params) { - element.blockParamNodes = params; - element.blockParams = params.map((p) => p.value); - } -} - -export function parseProgramBlockParamsLocs(code: src.Source, block: ASTv1.BlockStatement) { - const blockRange = [block.loc.getStart().offset!, block.loc.getEnd().offset!] as [number, number]; - let part = code.slice(...blockRange); - let start = blockRange[0]; - let idx = part.indexOf('|') + 1; - start += idx; - part = part.slice(idx, -1); - idx = part.indexOf('|'); - part = part.slice(0, idx); - for (const param of block.program.blockParamNodes) { - const regex = new RegExp(`\\b${param.value}\\b`); - const match = regex.exec(part)!; - const range = [start + match.index, 0] as [number, number]; - range[1] = range[0] + param.value.length; - param.loc = code.spanFor({ - start: code.hbsPosFor(range[0])!, - end: code.hbsPosFor(range[1])!, - }); - } + if (params) element.blockParams = params; } export function parseElementPartLocs(code: src.Source, element: ASTv1.ElementNode) { @@ -65,7 +41,7 @@ export function parseElementPartLocs(code: src.Source, element: ASTv1.ElementNod } } -function parseBlockParams(element: ASTv1.ElementNode): Nullable { +function parseBlockParams(element: ASTv1.ElementNode): Nullable { let l = element.attributes.length; let attrNames = []; @@ -99,7 +75,7 @@ function parseBlockParams(element: ASTv1.ElementNode): Nullable param.length) { - loc = loc.slice({ - skipEnd: loc.endPosition.column - loc.startPosition.column - param.length, - }); - } - - params.push({ - type: 'BlockParam', - value: param, - loc, - }); + params.push(param); } } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 66902562c8..4d0be5cd57 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -17,7 +17,6 @@ export interface CommonProgram extends BaseNode { export interface Block extends CommonProgram { type: 'Block'; blockParams: string[]; - blockParamNodes: BlockParam[]; chained?: boolean; } @@ -148,7 +147,6 @@ export interface ElementNode extends BaseNode { selfClosing: boolean; attributes: AttrNode[]; blockParams: string[]; - blockParamNodes: BlockParam[]; modifiers: ElementModifierStatement[]; comments: MustacheCommentStatement[]; children: Statement[]; @@ -282,16 +280,6 @@ export interface HashPair extends BaseNode { value: Expression; } -/** - * a param inside the pipes of elements or mustache blocks, - * ... bar is a BlockParam. - * {{#Foo as |bar|}}... bar is a BlockParam. - */ -export interface BlockParam extends BaseNode { - type: 'BlockParam'; - value: string; -} - export interface StripFlags { open: boolean; close: boolean; @@ -325,7 +313,6 @@ export type Nodes = SharedNodes & { PathExpression: PathExpression; Hash: Hash; HashPair: HashPair; - BlockParam: BlockParam; }; export type NodeType = keyof Nodes; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index e349762961..096bfb45de 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -42,9 +42,6 @@ class Builders { type: 'Block', body: body, blockParams: blockParams, - blockParamNodes: blockParams?.map( - (b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam - ), chained, loc, }; @@ -183,9 +180,7 @@ class Builders { .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode), selfClosing: selfClosing, attributes: attrs || [], - blockParams: blockParams, - blockParamNodes: - blockParams.map((x) => ({ type: 'BlockParam', value: x }) as ASTv1.BlockParam) || [], + blockParams: blockParams || [], modifiers: modifiers || [], comments: (comments as ASTv1.MustacheCommentStatement[]) || [], children: children || [], diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 7d418602b8..276d052919 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -215,8 +215,6 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS selfClosing: selfClosing, attributes: attrs || [], blockParams: blockParams || [], - blockParamNodes: - blockParams?.map((x) => ({ type: 'BlockParam', value: x }) as ASTv1.BlockParam) || [], modifiers: modifiers || [], comments: (comments as ASTv1.MustacheCommentStatement[]) || [], children: children || [], @@ -430,8 +428,6 @@ function buildBlockItself( type: 'Block', body: body || [], blockParams: blockParams || [], - blockParamNodes: - blockParams?.map((b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam) || [], chained, loc: buildLoc(loc || null), }; diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index b319365333..897f16ad60 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -32,7 +32,6 @@ const visitorKeys = { Hash: ['pairs'], HashPair: ['value'], - BlockParam: [], } as const; type VisitorKeysMap = typeof visitorKeys; diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index d148b717ac..62304b5aea 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -337,32 +337,6 @@ data-barf="herpy" } }); -test('element block params', () => { - let ast = parse(``); - - let [Foo] = ast.body; - if (assertNodeType(Foo, 'ElementNode')) { - let [ab, cd, efg] = guardArray({ blockParamNodes: Foo.blockParamNodes }, { min: 3 }); - locEqual(ab, 1, 9, 1, 11); - locEqual(cd, 1, 12, 1, 14); - locEqual(efg, 1, 15, 1, 18); - } -}); - -test('mustache block params', () => { - let ast = parse(`{{#Foo as |ab cd efg|}}{{/Foo}}`); - ` -{{#Foo as |ab cd efg|}}{{/Foo}}`; - - let [Foo] = ast.body; - if (assertNodeType(Foo, 'BlockStatement')) { - let [ab, cd, efg] = guardArray({ blockParamNodes: Foo.program.blockParamNodes }, { min: 3 }); - locEqual(ab, 1, 11, 1, 13); - locEqual(cd, 1, 14, 1, 16); - locEqual(efg, 1, 17, 1, 20); - } -}); - test('element dynamic attribute', () => { let ast = parse(``); diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index 1f5c4d7186..adc0efc97a 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -230,25 +230,6 @@ test('Simple embedded block helpers', () => { ); }); -test('block params', (assert) => { - let t = '{{#Foo as |bar baz qux|}}{{/Foo}}'; - let element = b.element('Foo', { - blockParams: ['bar', 'baz', 'qux'], - }); - let mustache = b.block(b.path('Foo'), [], b.hash(), b.blockItself([], ['bar', 'baz', 'qux'])); - astEqual(t, b.template([element, mustache])); - assert.strictEqual(element.blockParamNodes.length, 3); - assert.strictEqual(mustache.program.blockParamNodes.length, 3); - assert.deepEqual( - element.blockParamNodes.map((b) => b.value), - ['bar', 'baz', 'qux'] - ); - assert.deepEqual( - mustache.program.blockParamNodes.map((b) => b.value), - ['bar', 'baz', 'qux'] - ); -}); - test('Involved block helper', () => { let t = '

hi

content {{#testing shouldRender}}

Appears!

{{/testing}} more content here'; @@ -940,8 +921,6 @@ export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.E selfClosing: selfClosing, attributes: attrs || [], blockParams: blockParams || [], - blockParamNodes: - blockParams?.map((b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam) || [], modifiers: modifiers || [], comments: (comments as ASTv1.MustacheCommentStatement[]) || [], children: children || [], From 6f5b20fccb0a71a549fc55b3b418d1fc70ce7f3b Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 22:12:18 -0800 Subject: [PATCH 06/17] Revert "Merge pull request #1553 from patricklx/html-tag-split" This reverts commit e934005154977862a0a73e789537e24f836fa097, reversing changes made to 648ce182b20ca1c191fbfa1e9d2c96fba5ba5e4d. --- .../lib/parser/tokenizer-event-handlers.ts | 23 +-------- packages/@glimmer/syntax/lib/utils.ts | 21 -------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 48 +------------------ .../@glimmer/syntax/lib/v1/parser-builders.ts | 15 ------ .../@glimmer/syntax/lib/v1/public-builders.ts | 15 ------ .../@glimmer/syntax/lib/v1/visitor-keys.ts | 4 -- .../@glimmer/syntax/test/loc-node-test.ts | 23 --------- .../@glimmer/syntax/test/parser-node-test.ts | 28 +---------- 8 files changed, 3 insertions(+), 174 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 7cf10390dc..36515b99be 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -14,7 +14,7 @@ import * as src from '../source/api'; import { generateSyntaxError } from '../syntax-error'; import traverse from '../traversal/traverse'; import Walker from '../traversal/walker'; -import { appendChild, parseElementBlockParams, parseElementPartLocs } from '../utils'; +import { appendChild, parseElementBlockParams } from '../utils'; import b from '../v1/parser-builders'; import publicBuilder from '../v1/public-builders'; import { HandlebarsNodeVisitors } from './handlebars-node-visitors'; @@ -135,21 +135,6 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { blockParams: [], loc, }); - element.startTag = { - type: 'ElementStartNode', - value: name, - loc: loc, - }; - element.nameNode = { - type: 'ElementNameNode', - value: name, - loc: loc - .withStart(this.source.offsetFor(loc.startPosition.line, loc.startPosition.column + 1)) - .withEnd( - this.source.offsetFor(loc.startPosition.line, loc.startPosition.column + 1 + name.length) - ), - }; - parseElementPartLocs(this.source, element); this.elementStack.push(element); } @@ -158,12 +143,6 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { let element = this.elementStack.pop() as ASTv1.ElementNode; - element.endTag = { - type: 'ElementEndNode', - loc: tag.loc, - value: element.selfClosing ? '' : tag.name, - }; - this.validateEndTag(tag, element, isVoid); let parent = this.currentElement(); diff --git a/packages/@glimmer/syntax/lib/utils.ts b/packages/@glimmer/syntax/lib/utils.ts index 12decf566b..c0c2d1cb1d 100644 --- a/packages/@glimmer/syntax/lib/utils.ts +++ b/packages/@glimmer/syntax/lib/utils.ts @@ -1,7 +1,6 @@ import type { Nullable } from '@glimmer/interfaces'; import { expect, unwrap } from '@glimmer/util'; -import type { src } from '..'; import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; @@ -21,26 +20,6 @@ export function parseElementBlockParams(element: ASTv1.ElementNode): void { if (params) element.blockParams = params; } -export function parseElementPartLocs(code: src.Source, element: ASTv1.ElementNode) { - const elementRange = [element.loc.getStart().offset!, element.loc.getEnd().offset!] as [ - number, - number, - ]; - let start = elementRange[0]; - let codeSlice = code.slice(...elementRange); - for (const part of element.parts) { - const idx = codeSlice.indexOf(part.value); - const range = [start + idx, 0] as [number, number]; - range[1] = range[0] + part.value.length; - codeSlice = code.slice(range[1], elementRange[1]); - start = range[1]; - part.loc = code.spanFor({ - start: code.hbsPosFor(range[0])!, - end: code.hbsPosFor(range[1])!, - }); - } -} - function parseBlockParams(element: ASTv1.ElementNode): Nullable { let l = element.attributes.length; let attrNames = []; diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 4d0be5cd57..6b30a06313 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -102,48 +102,9 @@ export interface MustacheCommentStatement extends BaseNode { value: string; } -export interface ElementName { - type: 'ElementName'; - name: string; - loc: src.SourceLocation; -} - -export interface ElementStartNode extends BaseNode { - type: 'ElementStartNode'; - value: string; -} - -export interface ElementNameNode extends BaseNode { - type: 'ElementNameNode'; - value: string; -} - -export interface ElementEndNode extends BaseNode { - type: 'ElementEndNode'; - value: string; -} - -export interface ElementPartNode extends BaseNode { - type: 'ElementPartNode'; - value: string; -} - -/* - - ^-- ElementPartNode - ^-- ElementPartNode - ^- ElementPartNode - ^-------- ElementNameNode - ^------------------ ElementStartNode - ^----------- ElementEndNode - */ export interface ElementNode extends BaseNode { type: 'ElementNode'; tag: string; - nameNode: ElementNameNode; - startTag: ElementStartNode; - endTag: ElementEndNode; - parts: ElementPartNode[]; selfClosing: boolean; attributes: AttrNode[]; blockParams: string[]; @@ -285,7 +246,7 @@ export interface StripFlags { close: boolean; } -export type SharedNodes = { +export type Nodes = { CommentStatement: CommentStatement; MustacheCommentStatement: MustacheCommentStatement; TextNode: TextNode; @@ -298,13 +259,6 @@ export type SharedNodes = { ElementModifierStatement: ElementModifierStatement; AttrNode: AttrNode; ConcatStatement: ConcatStatement; -}; - -export type Nodes = SharedNodes & { - ElementEndNode: ElementEndNode; - ElementStartNode: ElementStartNode; - ElementPartNode: ElementPartNode; - ElementNameNode: ElementNameNode; Template: Template; Block: Block; BlockStatement: BlockStatement; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 096bfb45de..5fdf6684af 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -163,21 +163,6 @@ class Builders { return { type: 'ElementNode', tag, - nameNode: { - type: 'ElementNameNode', - value: tag, - } as ASTv1.ElementNameNode, - startTag: { - type: 'ElementStartNode', - value: tag, - } as ASTv1.ElementStartNode, - endTag: { - type: 'ElementEndNode', - value: selfClosing ? '' : tag, - } as ASTv1.ElementEndNode, - parts: tag - .split('.') - .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode), selfClosing: selfClosing, attributes: attrs || [], blockParams: blockParams || [], diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 276d052919..f7aa709f89 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -197,21 +197,6 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS return { type: 'ElementNode', tag: tagName, - nameNode: { - type: 'ElementNameNode', - value: tag, - } as ASTv1.ElementNameNode, - startTag: { - type: 'ElementStartNode', - value: tag, - } as ASTv1.ElementStartNode, - endTag: { - type: 'ElementEndNode', - value: selfClosing ? '' : tag, - } as ASTv1.ElementEndNode, - parts: tagName - .split('.') - .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode), selfClosing: selfClosing, attributes: attrs || [], blockParams: blockParams || [], diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index 897f16ad60..38e10263d8 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -12,10 +12,6 @@ const visitorKeys = { CommentStatement: [], MustacheCommentStatement: [], ElementNode: ['attributes', 'modifiers', 'children', 'comments'], - ElementStartNode: [], - ElementPartNode: [], - ElementEndNode: [], - ElementNameNode: [], AttrNode: ['value'], TextNode: [], diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index 62304b5aea..c41a431944 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -134,29 +134,6 @@ test('html elements', () => { } }); -test('html elements with paths', () => { - let ast = parse(` - - - - - `); - - let [, foo] = ast.body; - locEqual(foo, 2, 4, 5, 10, 'Foo element'); - if (assertNodeType(foo, 'ElementNode')) { - locEqual(foo.startTag, 2, 4, 2, 18, 'Foo start tag'); - locEqual(foo.nameNode, 2, 5, 2, 8, 'Foo name node'); - locEqual(foo.endTag, 5, 4, 5, 10, 'Foo end tag'); - let [, barSelfClosed] = foo.children; - if (assertNodeType(barSelfClosed, 'ElementNode')) { - locEqual(barSelfClosed.parts[0], 3, 7, 3, 10, 'bar.x.y bar part'); - locEqual(barSelfClosed.parts[1], 3, 11, 3, 12, 'bar.x.y x part'); - locEqual(barSelfClosed.parts[2], 3, 13, 3, 14, 'bar.x.y y part'); - } - } -}); - test('html elements with nested blocks', (assert) => { let ast = parse(`
diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index adc0efc97a..15df3a5242 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -266,17 +266,6 @@ test('Element modifiers', () => { ); }); -test('Element paths', (assert) => { - let t = ""; - const elem = element('bar.x.y', ['attrs', ['class', 'bar']]); - astEqual(t, b.template([elem])); - assert.strictEqual(elem.parts.length, 3); - assert.deepEqual( - elem.parts.map((p) => p.value), - ['bar', 'x', 'y'] - ); -}); - test('Tokenizer: MustacheStatement encountered in beforeAttributeName state', () => { let t = ''; astEqual(t, b.template([element('input', ['modifiers', 'bar'])])); @@ -902,22 +891,7 @@ export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.E return { type: 'ElementNode', - tag: tag, - nameNode: { - type: 'ElementNameNode', - value: tag, - } as ASTv1.ElementNameNode, - startTag: { - type: 'ElementStartNode', - value: tag, - } as ASTv1.ElementStartNode, - endTag: { - type: 'ElementEndNode', - value: selfClosing ? '' : tag, - } as ASTv1.ElementEndNode, - parts: tag - .split('.') - .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode), + tag: tag || '', selfClosing: selfClosing, attributes: attrs || [], blockParams: blockParams || [], From f01813760981132c26c80d4924f0f74ab907c9e8 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 23:06:34 -0800 Subject: [PATCH 07/17] Use SourceSpan consistently --- .../syntax/lib/parser/handlebars-node-visitors.ts | 12 ++++++------ packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index bfc4ff59ef..1001f3a350 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -338,10 +338,10 @@ export abstract class HandlebarsNodeVisitors extends Parser { if (thisHead) { pathHead = { type: 'ThisHead', - loc: { + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + 4 }, - }, + }), }; } else if (path.data) { const head = parts.shift(); @@ -356,10 +356,10 @@ export abstract class HandlebarsNodeVisitors extends Parser { pathHead = { type: 'AtHead', name: `@${head}`, - loc: { + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length + 1 }, - }, + }), }; } else { const head = parts.shift(); @@ -374,10 +374,10 @@ export abstract class HandlebarsNodeVisitors extends Parser { pathHead = { type: 'VarHead', name: head, - loc: { + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length }, - }, + }), }; } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 6b30a06313..ac5b306d5c 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -151,19 +151,19 @@ export interface SubExpression extends Call { export interface ThisHead { type: 'ThisHead'; - loc: src.SourceLocation; + loc: src.SourceSpan; } export interface AtHead { type: 'AtHead'; name: string; - loc: src.SourceLocation; + loc: src.SourceSpan; } export interface VarHead { type: 'VarHead'; name: string; - loc: src.SourceLocation; + loc: src.SourceSpan; } export type PathHead = ThisHead | AtHead | VarHead; From cd7171185c7c8bae901efeee1324694430eaa007 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 23 Feb 2024 23:22:48 -0800 Subject: [PATCH 08/17] Sync v1 AST nodes with visitor-keys `PathHead` is not a real AST node, its visitor will never be called --- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 24 +++++++++++-------- .../@glimmer/syntax/lib/v1/visitor-keys.ts | 1 - 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index ac5b306d5c..3cedb521bb 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -247,24 +247,28 @@ export interface StripFlags { } export type Nodes = { + Template: Template; + Block: Block; + + MustacheStatement: MustacheStatement; + BlockStatement: BlockStatement; + ElementModifierStatement: ElementModifierStatement; CommentStatement: CommentStatement; MustacheCommentStatement: MustacheCommentStatement; + ElementNode: ElementNode; + AttrNode: AttrNode; TextNode: TextNode; + + ConcatStatement: ConcatStatement; + SubExpression: SubExpression; + PathExpression: PathExpression; + StringLiteral: StringLiteral; BooleanLiteral: BooleanLiteral; NumberLiteral: NumberLiteral; NullLiteral: NullLiteral; UndefinedLiteral: UndefinedLiteral; - MustacheStatement: MustacheStatement; - ElementModifierStatement: ElementModifierStatement; - AttrNode: AttrNode; - ConcatStatement: ConcatStatement; - Template: Template; - Block: Block; - BlockStatement: BlockStatement; - ElementNode: ElementNode; - SubExpression: SubExpression; - PathExpression: PathExpression; + Hash: Hash; HashPair: HashPair; }; diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index 38e10263d8..c82dd0d3a9 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -18,7 +18,6 @@ const visitorKeys = { ConcatStatement: ['parts'], SubExpression: ['path', 'params', 'hash'], PathExpression: [], - PathHead: [], StringLiteral: [], BooleanLiteral: [], From 1d7652db07509f82f2d031febaad8f6d0dd09bbb Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sat, 24 Feb 2024 20:40:55 -0800 Subject: [PATCH 09/17] Fix types for ASTv1 "callable" nodes Other than mustache, none of the other "callable" nodes allow a literal in the "callee" (node.path) position: {{log ("hi")}} ~~~~ illegal
~~~~ illegal {{#"hi"}}...{{/"hi"}} ~~~~ illegal Further more, in mustache positions this is only allowed if there are no other arguments: {{"hi" 1 2 3}} ~~~~ illlegal Most of these syntaxes already generate syntax errors in the parser but is allowed in the v1 AST, so it's possible for a AST plugin to force these illegal conditions in a transform but should ultimately get caught and rejected elsewhere. This commit fixes the type to reflect that these conditions are illegal/impossible and make sure we catch all of these edge cases in the v2 normalization step. --- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 19 ++--- .../@glimmer/syntax/lib/v1/public-builders.ts | 7 +- packages/@glimmer/syntax/lib/v2/normalize.ts | 82 +++++++++++++++---- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 3cedb521bb..c410146044 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -37,15 +37,10 @@ export interface Template extends CommonProgram { */ export type Program = Template | Block; -export interface CallParts { - path: Expression; - params: Expression[]; - hash: Hash; -} +export type CallableExpression = SubExpression | PathExpression; -export interface Call extends BaseNode { - name?: Expression; - path: Expression; +export interface CallParts { + path: CallableExpression; params: Expression[]; hash: Hash; } @@ -72,7 +67,7 @@ export interface MustacheStatement extends BaseNode { export interface BlockStatement extends BaseNode { type: 'BlockStatement'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; program: Block; @@ -87,7 +82,7 @@ export interface BlockStatement extends BaseNode { export interface ElementModifierStatement extends BaseNode { type: 'ElementModifierStatement'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; } @@ -142,9 +137,9 @@ export interface ConcatStatement extends BaseNode { export type ExpressionName = 'SubExpression' | 'PathExpression' | LiteralName; -export interface SubExpression extends Call { +export interface SubExpression extends BaseNode { type: 'SubExpression'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; } diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index f7aa709f89..6e121a8593 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -23,7 +23,7 @@ function SOURCE(): Source { // Statements -export type BuilderHead = string | ASTv1.Expression; +export type BuilderHead = string | ASTv1.CallableExpression; export type TagDescriptor = string | { name: string; selfClosing: boolean }; function buildMustache( @@ -91,7 +91,7 @@ function buildBlock( } function buildElementModifier( - path: BuilderHead | ASTv1.Expression, + path: BuilderHead, params?: ASTv1.Expression[], hash?: ASTv1.Hash, loc?: Nullable @@ -331,8 +331,9 @@ function buildPath( path: ASTv1.PathExpression | string | { head: string; tail: string[] }, loc?: SourceLocation ): ASTv1.PathExpression; +function buildPath(path: BuilderHead, loc?: SourceLocation): ASTv1.CallableExpression; +function buildPath(path: BuilderHead | ASTv1.Literal, loc?: SourceLocation): ASTv1.Expression; function buildPath(path: ASTv1.Expression, loc?: SourceLocation): ASTv1.Expression; -function buildPath(path: BuilderHead | ASTv1.Expression, loc?: SourceLocation): ASTv1.Expression; function buildPath( path: BuilderHead | ASTv1.Expression | { head: string; tail: string[] }, loc?: SourceLocation diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index 5ffee1f6a9..0c198a5c3c 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -191,6 +191,11 @@ class ExpressionNormalizer { case 'PathExpression': return this.path(expr, resolution); case 'SubExpression': { + // expr.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(expr.path)) { + assertIllegalLiteral(expr.path, expr.loc); + } + let resolution = this.block.resolutionFor(expr, SexpSyntaxContext); if (resolution.result === 'error') { @@ -370,22 +375,30 @@ class StatementNormalizer { * Normalizes an ASTv1.MustacheStatement to an ASTv2.AppendStatement */ MustacheStatement(mustache: ASTv1.MustacheStatement): ASTv2.AppendContent { - let { trusting } = mustache; + let { path, params, hash, trusting } = mustache; let loc = this.block.loc(mustache.loc); + let context = AppendSyntaxContext(mustache); + let value: ASTv2.ExpressionNode; - // Normalize the call parts in AppendSyntaxContext - let callParts = this.expr.callParts( - { - path: mustache.path, - params: mustache.params, - hash: mustache.hash, - }, - AppendSyntaxContext(mustache) - ); + if (isLiteral(path)) { + if (params.length === 0 && hash.pairs.length === 0) { + value = this.expr.normalize(path, context); + } else { + assertIllegalLiteral(path, loc); + } + } else { + // Normalize the call parts in AppendSyntaxContext + let callParts = this.expr.callParts( + { + path, + params, + hash, + }, + AppendSyntaxContext(mustache) + ); - let value = callParts.args.isEmpty() - ? callParts.callee - : this.block.builder.sexp(callParts, loc); + value = callParts.args.isEmpty() ? callParts.callee : this.block.builder.sexp(callParts, loc); + } return this.block.builder.append( { @@ -404,6 +417,11 @@ class StatementNormalizer { let { program, inverse } = block; let loc = this.block.loc(block.loc); + // block.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(block.path)) { + assertIllegalLiteral(block.path, loc); + } + let resolution = this.block.resolutionFor(block, BlockSyntaxContext); if (resolution.result === 'error') { @@ -513,6 +531,11 @@ class ElementNormalizer { } private modifier(m: ASTv1.ElementModifierStatement): ASTv2.ElementModifier { + // modifier.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(m.path)) { + assertIllegalLiteral(m.path, m.loc); + } + let resolution = this.ctx.resolutionFor(m, ModifierSyntaxContext); if (resolution.result === 'error') { @@ -536,10 +559,21 @@ class ElementNormalizer { * ``` */ private mustacheAttr(mustache: ASTv1.MustacheStatement): ASTv2.ExpressionNode { + let { path, params, hash, loc } = mustache; + let context = AttrValueSyntaxContext(mustache); + + if (isLiteral(path)) { + if (params.length === 0 && hash.pairs.length === 0) { + return this.expr.normalize(path, context); + } else { + assertIllegalLiteral(path, loc); + } + } + // Normalize the call parts in AttrValueSyntaxContext let sexp = this.ctx.builder.sexp( - this.expr.callParts(mustache, AttrValueSyntaxContext(mustache)), - this.ctx.loc(mustache.loc) + this.expr.callParts(mustache as ASTv1.CallParts, AttrValueSyntaxContext(mustache)), + this.ctx.loc(loc) ); // If there are no params or hash, just return the function part as its own expression @@ -948,6 +982,24 @@ class ElementChildren extends Children { } } +function isLiteral(node: ASTv1.Expression): node is ASTv1.Literal { + switch (node.type) { + case 'StringLiteral': + case 'BooleanLiteral': + case 'NumberLiteral': + case 'UndefinedLiteral': + case 'NullLiteral': + return true; + default: + return false; + } +} + +function assertIllegalLiteral(node: ASTv1.Literal, loc: SourceSpan): never { + let value = node.type === 'StringLiteral' ? JSON.stringify(node.value) : String(node.value); + throw generateSyntaxError(`Unexpected literal \`${value}\``, loc); +} + function printPath(node: ASTv1.PathExpression | ASTv1.CallNode): string { if (node.type !== 'PathExpression' && node.path.type === 'PathExpression') { return printPath(node.path); From 198fa29d30682e29441d06197d08bab3d5be8c76 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sat, 24 Feb 2024 21:00:45 -0800 Subject: [PATCH 10/17] Deprecate AST v1 literal.original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These probably once existed to normalize across all the possible nodes that call go into a `mustache.path` position, such that you can always do `node.path.original` on a mustache. In my opinion, this is of dubious utility in the first place – the `original` property on these can have a variety of different types, it's unclear what you would do with the generic value you acquired in this manner. Code like `if (node.path.original === "component")` will likely misclassify `{{"component"}}` as `{{component}}`. Further more, with the introduction of the `{{(sexp)}}` syntax, the original assumption no longer holds anyway, as sub-expression nodes do not have an `original` property. Deprecating this property and turning it into a getter/setter for `value`, ensuring they stay in sync. In the future, we may want to reclaim this original property to store the _actual_ original syntax in the form of a string. For example, today, if you had `{{1.0}}` in the source code, it gets parsed into the JavaScript number/value `1` and the original "raw" format is irrecoverably lost, which is a problem for printer and codemods type use cases. --- .../lib/parser/handlebars-node-visitors.ts | 30 +++++++++++++--- .../@glimmer/syntax/lib/v1/legacy-interop.ts | 30 ++++++++++++++++ packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 20 +++++++++++ .../@glimmer/syntax/lib/v1/parser-builders.ts | 36 +++++-------------- .../@glimmer/syntax/lib/v1/public-builders.ts | 12 ++++--- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 1001f3a350..80675a2b17 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -401,23 +401,43 @@ export abstract class HandlebarsNodeVisitors extends Parser { } StringLiteral(string: HBS.StringLiteral): ASTv1.StringLiteral { - return b.literal({ type: 'StringLiteral', value: string.value, loc: string.loc }); + return b.literal({ + type: 'StringLiteral', + value: string.value, + loc: this.source.spanFor(string.loc), + }); } BooleanLiteral(boolean: HBS.BooleanLiteral): ASTv1.BooleanLiteral { - return b.literal({ type: 'BooleanLiteral', value: boolean.value, loc: boolean.loc }); + return b.literal({ + type: 'BooleanLiteral', + value: boolean.value, + loc: this.source.spanFor(boolean.loc), + }); } NumberLiteral(number: HBS.NumberLiteral): ASTv1.NumberLiteral { - return b.literal({ type: 'NumberLiteral', value: number.value, loc: number.loc }); + return b.literal({ + type: 'NumberLiteral', + value: number.value, + loc: this.source.spanFor(number.loc), + }); } UndefinedLiteral(undef: HBS.UndefinedLiteral): ASTv1.UndefinedLiteral { - return b.literal({ type: 'UndefinedLiteral', value: undefined, loc: undef.loc }); + return b.literal({ + type: 'UndefinedLiteral', + value: undefined, + loc: this.source.spanFor(undef.loc), + }); } NullLiteral(nul: HBS.NullLiteral): ASTv1.NullLiteral { - return b.literal({ type: 'NullLiteral', value: null, loc: nul.loc }); + return b.literal({ + type: 'NullLiteral', + value: null, + loc: this.source.spanFor(nul.loc), + }); } } diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 30b6d7f370..188910a9b5 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -130,3 +130,33 @@ export function buildLegacyPath({ head, tail, loc }: PathExpressionParams): ASTv return node as ASTv1.PathExpression; } + +export function buildLegacyLiteral({ + type, + value, + loc, +}: { + type: T['type']; + value: T['value']; + loc: T['loc']; +}): T { + const node = { + type, + value, + loc, + }; + + Object.defineProperty(node, 'original', { + enumerable: false, + get(this: typeof node): T['original'] { + deprecate(`The original property on literal nodes is deprecated, use value instead`); + return this.value; + }, + set(this: typeof node, value: T['original']) { + deprecate(`The original property on literal nodes is deprecated, use value instead`); + this.value = value; + }, + }); + + return node as T; +} diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index c410146044..6680c5f4c2 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -198,30 +198,50 @@ export type LiteralName = export interface StringLiteral extends BaseNode { type: 'StringLiteral'; value: string; + + /** + * @deprecated use value instead + */ original: string; } export interface BooleanLiteral extends BaseNode { type: 'BooleanLiteral'; value: boolean; + + /** + * @deprecated use value instead + */ original: boolean; } export interface NumberLiteral extends BaseNode { type: 'NumberLiteral'; value: number; + + /** + * @deprecated use value instead + */ original: number; } export interface UndefinedLiteral extends BaseNode { type: 'UndefinedLiteral'; value: undefined; + + /** + * @deprecated use value instead + */ original: undefined; } export interface NullLiteral extends BaseNode { type: 'NullLiteral'; value: null; + + /** + * @deprecated use value instead + */ original: null; } diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 5fdf6684af..49569104ad 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -6,7 +6,12 @@ import type { SourceLocation } from '../source/location'; import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; -import { buildLegacyMustache, buildLegacyPath, buildLegacyTemplate } from './legacy-interop'; +import { + buildLegacyLiteral, + buildLegacyMustache, + buildLegacyPath, + buildLegacyTemplate, +} from './legacy-interop'; const DEFAULT_STRIP = { close: false, @@ -324,34 +329,9 @@ class Builders { }: { type: T['type']; value: T['value']; - loc?: SourceLocation; + loc: SourceSpan; }): T { - return { - type, - value, - original: value, - loc, - } as T; - } - - undefined(): ASTv1.UndefinedLiteral { - return this.literal({ type: 'UndefinedLiteral', value: undefined }); - } - - null(): ASTv1.NullLiteral { - return this.literal({ type: 'NullLiteral', value: null }); - } - - string(value: string, loc: SourceSpan): ASTv1.StringLiteral { - return this.literal({ type: 'StringLiteral', value, loc }); - } - - boolean(value: boolean, loc: SourceSpan): ASTv1.BooleanLiteral { - return this.literal({ type: 'BooleanLiteral', value, loc }); - } - - number(value: number, loc: SourceSpan): ASTv1.NumberLiteral { - return this.literal({ type: 'NumberLiteral', value, loc }); + return buildLegacyLiteral({ type, value, loc }); } } diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 6e121a8593..641600b796 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -7,7 +7,12 @@ import type * as ASTv1 from './api'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; -import { buildLegacyMustache, buildLegacyPath, buildLegacyTemplate } from './legacy-interop'; +import { + buildLegacyLiteral, + buildLegacyMustache, + buildLegacyPath, + buildLegacyTemplate, +} from './legacy-interop'; let _SOURCE: Source | undefined; @@ -363,12 +368,11 @@ function buildLiteral( value: T['value'], loc?: SourceLocation ): T { - return { + return buildLegacyLiteral({ type, value, - original: value, loc: buildLoc(loc || null), - } as T; + }); } // Miscellaneous From d3c3ef52b994b6b85f7e0c65c732c7256225ebb8 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sat, 24 Feb 2024 23:59:56 -0800 Subject: [PATCH 11/17] Funnel all v1 AST node construction through parser builders The parser builders are the defacto constructors for the v1 AST nodes. As the nodes get increasingly more eloborate for backwards compatibility and such, it's important that we share the logic for constructing these nodes as much as possible so we don't miss any cases. Also standardize the signatures to use the object argument style, you should be able to clone a node by passing it back into the same builder method which it originated from. --- packages/@glimmer/syntax/lib/parser.ts | 8 +- .../lib/parser/handlebars-node-visitors.ts | 94 +++---- .../lib/parser/tokenizer-event-handlers.ts | 47 ++-- .../@glimmer/syntax/lib/v1/handlebars-ast.ts | 6 +- .../@glimmer/syntax/lib/v1/parser-builders.ts | 121 +++++---- .../@glimmer/syntax/lib/v1/public-builders.ts | 249 +++++++----------- packages/@glimmer/syntax/lib/v2/normalize.ts | 2 +- .../@glimmer/syntax/test/parser-node-test.ts | 65 +++-- 8 files changed, 286 insertions(+), 306 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index 0e7ffabac1..1463f967d9 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -11,7 +11,7 @@ import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; export type ParserNodeBuilder = Omit & { - loc: src.SourceOffset; + start: src.SourceOffset; }; export interface Tag { @@ -42,7 +42,7 @@ export abstract class Parser { public currentNode: Nullable< Readonly< | ParserNodeBuilder - | ASTv1.TextNode + | ParserNodeBuilder | ParserNodeBuilder> | ParserNodeBuilder> > @@ -70,7 +70,7 @@ export abstract class Parser { finish(node: ParserNodeBuilder): T { return assign({}, node, { - loc: node.loc.until(this.offset()), + loc: node.start.until(this.offset()), } as const) as unknown as T; // node.loc = node.loc.withEnd(end); @@ -143,7 +143,7 @@ export abstract class Parser { return node; } - get currentData(): ASTv1.TextNode { + get currentData(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'TextNode', 'expected a text node'); return node; diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 80675a2b17..8719cb116e 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -10,7 +10,6 @@ import { Parser } from '../parser'; import { NON_EXISTENT_LOCATION } from '../source/location'; import { generateSyntaxError } from '../syntax-error'; import { appendChild, isHBSLiteral, printLiteral } from '../utils'; -import { buildLegacyPath } from '../v1/legacy-interop'; import b from '../v1/parser-builders'; const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState; @@ -129,7 +128,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { mustache = b.mustache({ path: this.acceptNode(rawMustache.path), params: [], - hash: b.hash([], this.source.spanFor(rawMustache.path.loc).collapse('end')), + hash: b.hash({ pairs: [], loc: this.source.spanFor(rawMustache.path.loc).collapse('end') }), trusting: !escaped, loc: this.source.spanFor(loc), strip, @@ -229,7 +228,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { } const { value, loc } = rawComment; - const comment = b.mustacheComment(value, this.source.spanFor(loc)); + const comment = b.mustacheComment({ value, loc: this.source.spanFor(loc) }); switch (tokenizer.state) { case 'beforeAttributeName': @@ -336,13 +335,12 @@ export abstract class HandlebarsNodeVisitors extends Parser { let pathHead: ASTv1.PathHead; if (thisHead) { - pathHead = { - type: 'ThisHead', + pathHead = b.this({ loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + 4 }, }), - }; + }); } else if (path.data) { const head = parts.shift(); @@ -353,14 +351,13 @@ export abstract class HandlebarsNodeVisitors extends Parser { ); } - pathHead = { - type: 'AtHead', + pathHead = b.atName({ name: `@${head}`, loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length + 1 }, }), - }; + }); } else { const head = parts.shift(); @@ -371,17 +368,16 @@ export abstract class HandlebarsNodeVisitors extends Parser { ); } - pathHead = { - type: 'VarHead', + pathHead = b.var({ name: head, loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length }, }), - }; + }); } - return buildLegacyPath({ + return b.path({ head: pathHead, tail: parts, loc: this.source.spanFor(path.loc), @@ -397,7 +393,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { }) ); - return b.hash(pairs, this.source.spanFor(hash.loc)); + return b.hash({ pairs, loc: this.source.spanFor(hash.loc) }); } StringLiteral(string: HBS.StringLiteral): ASTv1.StringLiteral { @@ -502,38 +498,43 @@ function acceptCallNodes( params: ASTv1.Expression[]; hash: ASTv1.Hash; } { - if (node.path.type.endsWith('Literal')) { - const path = node.path as unknown as - | HBS.StringLiteral - | HBS.UndefinedLiteral - | HBS.NullLiteral - | HBS.NumberLiteral - | HBS.BooleanLiteral; - - let value = ''; - if (path.type === 'BooleanLiteral') { - value = path.original.toString(); - } else if (path.type === 'StringLiteral') { - value = `"${path.original}"`; - } else if (path.type === 'NullLiteral') { - value = 'null'; - } else if (path.type === 'NumberLiteral') { - value = path.value.toString(); - } else { - value = 'undefined'; + let path: ASTv1.PathExpression | ASTv1.SubExpression; + + switch (node.path.type) { + case 'PathExpression': + path = compiler.PathExpression(node.path); + break; + + case 'SubExpression': + path = compiler.SubExpression(node.path); + break; + + case 'StringLiteral': + case 'UndefinedLiteral': + case 'NullLiteral': + case 'NumberLiteral': + case 'BooleanLiteral': { + let value: string; + if (node.path.type === 'BooleanLiteral') { + value = node.path.original.toString(); + } else if (node.path.type === 'StringLiteral') { + value = `"${node.path.original}"`; + } else if (node.path.type === 'NullLiteral') { + value = 'null'; + } else if (node.path.type === 'NumberLiteral') { + value = node.path.value.toString(); + } else { + value = 'undefined'; + } + throw generateSyntaxError( + `${node.path.type} "${ + node.path.type === 'StringLiteral' ? node.path.original : value + }" cannot be called as a sub-expression, replace (${value}) with ${value}`, + compiler.source.spanFor(node.path.loc) + ); } - throw generateSyntaxError( - `${path.type} "${ - path.type === 'StringLiteral' ? path.original : value - }" cannot be called as a sub-expression, replace (${value}) with ${value}`, - compiler.source.spanFor(path.loc) - ); } - const path = - node.path.type === 'PathExpression' - ? compiler.PathExpression(node.path) - : compiler.SubExpression(node.path as unknown as HBS.SubExpression); const params = node.params ? node.params.map((e) => compiler.acceptNode(e)) : []; @@ -544,11 +545,10 @@ function acceptCallNodes( const hash = node.hash ? compiler.Hash(node.hash) - : ({ - type: 'Hash', - pairs: [] as ASTv1.HashPair[], + : b.hash({ + pairs: [], loc: compiler.source.spanFor(end).collapse('end'), - } as const); + }); return { path, params, hash }; } diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 36515b99be..85955bc70a 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -30,7 +30,11 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { // Comment beginComment(): void { - this.currentNode = b.comment('', this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn)); + this.currentNode = { + type: 'CommentStatement', + value: '', + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + }; } appendToCommentData(char: string): void { @@ -38,16 +42,17 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishComment(): void { - appendChild(this.currentElement(), this.finish(this.currentComment)); + appendChild(this.currentElement(), b.comment(this.finish(this.currentComment))); } // Data beginData(): void { - this.currentNode = b.text({ + this.currentNode = { + type: 'TextNode', chars: '', - loc: this.offset().collapsed(), - }); + start: this.offset(), + }; } appendToData(char: string): void { @@ -55,9 +60,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishData(): void { - this.currentData.loc = this.currentData.loc.withEnd(this.offset()); - - appendChild(this.currentElement(), this.currentData); + appendChild(this.currentElement(), b.text(this.finish(this.currentData))); } // Tags - basic @@ -75,7 +78,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { modifiers: [], comments: [], selfClosing: false, - loc: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; } @@ -87,7 +90,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { modifiers: [], comments: [], selfClosing: false, - loc: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; } @@ -101,7 +104,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { throw generateSyntaxError( 'Invalid named block named detected, you may have created a named block without a name, or you may have began your name with a number. Named blocks must have names that are at least one character long, and begin with a lower case letter', this.source.spanFor({ - start: this.currentTag.loc.toJSON(), + start: this.currentTag.start.toJSON(), end: this.offset().toJSON(), }) ); @@ -116,19 +119,14 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishStartTag(): void { - let { - name, - attributes: attrs, - modifiers, - comments, - selfClosing, - loc, - } = this.finish(this.currentStartTag); + let { name, attributes, modifiers, comments, selfClosing, loc } = this.finish( + this.currentStartTag + ); let element = b.element({ tag: name, selfClosing, - attrs, + attributes, modifiers, comments, children: [], @@ -148,7 +146,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { element.loc = element.loc.withEnd(this.offset()); parseElementBlockParams(element); - appendChild(parent, element); + appendChild(parent, b.element(element)); } markTagAsSelfClosing(): void { @@ -222,7 +220,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { if (tag.type === 'EndTag') { throw generateSyntaxError( `Invalid end tag: closing tag must not have attributes`, - this.source.spanFor({ start: tag.loc.toJSON(), end: tokenizerPos.toJSON() }) + this.source.spanFor({ start: tag.start.toJSON(), end: tokenizerPos.toJSON() }) ); } @@ -256,7 +254,10 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { let first = getFirst(parts); let last = getLast(parts); - return b.concat(parts, this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc))); + return b.concat({ + parts, + loc: this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc)), + }); } validateEndTag( diff --git a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts index 3099cb3e4e..9a70737fd7 100644 --- a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts +++ b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts @@ -86,9 +86,9 @@ export interface CommonBlock extends CommonNode { hash: Hash; program: Program; inverse: Program; - openStrip: StripFlags; - inverseStrip: StripFlags; - closeStrip: StripFlags; + openStrip?: StripFlags; + inverseStrip?: StripFlags; + closeStrip?: StripFlags; } export interface BlockStatement extends CommonBlock { diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 49569104ad..a616082d25 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -1,9 +1,8 @@ -import type { Dict, Nullable, PresentArray } from '@glimmer/interfaces'; +import type { Dict, Nullable, Optional, PresentArray } from '@glimmer/interfaces'; import { assert } from '@glimmer/util'; -import type { ParserNodeBuilder } from '../parser'; import type { SourceLocation } from '../source/location'; -import type { SourceOffset, SourceSpan } from '../source/span'; +import type { SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; import { @@ -25,7 +24,7 @@ const DEFAULT_STRIP = { * 2. Mandating source locations */ class Builders { - pos(line: number, column: number) { + pos({ line, column }: { line: number; column: number }) { return { line, column, @@ -81,7 +80,7 @@ class Builders { hash: ASTv1.Hash; trusting: boolean; loc: SourceSpan; - strip: ASTv1.StripFlags; + strip?: Optional; }): ASTv1.MustacheStatement { return buildLegacyMustache({ path, @@ -108,11 +107,11 @@ class Builders { params: ASTv1.Expression[]; hash: ASTv1.Hash; defaultBlock: ASTv1.Block; - elseBlock?: Nullable; + elseBlock: Nullable; loc: SourceSpan; - openStrip: ASTv1.StripFlags; - inverseStrip: ASTv1.StripFlags; - closeStrip: ASTv1.StripFlags; + openStrip?: Optional; + inverseStrip?: Optional; + closeStrip?: Optional; }): ASTv1.BlockStatement { return { type: 'BlockStatement', @@ -121,33 +120,42 @@ class Builders { hash, program: defaultBlock, inverse: elseBlock, - loc: loc, - openStrip: openStrip, - inverseStrip: inverseStrip, - closeStrip: closeStrip, + loc, + openStrip, + inverseStrip, + closeStrip, }; } - comment(value: string, loc: SourceOffset): ParserNodeBuilder { + comment({ value, loc }: { value: string; loc: SourceSpan }): ASTv1.CommentStatement { return { type: 'CommentStatement', - value: value, + value, loc, }; } - mustacheComment(value: string, loc: SourceSpan): ASTv1.MustacheCommentStatement { + mustacheComment({ + value, + loc, + }: { + value: string; + loc: SourceSpan; + }): ASTv1.MustacheCommentStatement { return { type: 'MustacheCommentStatement', - value: value, + value, loc, }; } - concat( - parts: PresentArray, - loc: SourceSpan - ): ASTv1.ConcatStatement { + concat({ + parts, + loc, + }: { + parts: PresentArray; + loc: SourceSpan; + }): ASTv1.ConcatStatement { return { type: 'ConcatStatement', parts, @@ -158,22 +166,31 @@ class Builders { element({ tag, selfClosing, - attrs, + attributes, blockParams, modifiers, comments, children, loc, - }: BuildElementOptions): ASTv1.ElementNode { + }: { + tag: string; + selfClosing: boolean; + attributes: ASTv1.AttrNode[]; + modifiers: ASTv1.ElementModifierStatement[]; + children: ASTv1.Statement[]; + comments: ASTv1.MustacheCommentStatement[]; + blockParams: string[]; + loc: SourceSpan; + }): ASTv1.ElementNode { return { type: 'ElementNode', tag, selfClosing: selfClosing, - attributes: attrs || [], - blockParams: blockParams || [], - modifiers: modifiers || [], - comments: (comments as ASTv1.MustacheCommentStatement[]) || [], - children: children || [], + attributes, + blockParams, + modifiers, + comments, + children, loc, }; } @@ -255,26 +272,31 @@ class Builders { return buildLegacyPath({ head, tail, loc }); } - head(head: string, loc: SourceSpan): ASTv1.PathHead { - if (head[0] === '@') { - return this.atName(head, loc); - } else if (head === 'this') { - return this.this(loc); + head({ original, loc }: { original: string; loc: SourceSpan }): ASTv1.PathHead { + if (original === 'this') { + return this.this({ loc }); + } + if (original[0] === '@') { + return this.atName({ name: original, loc }); } else { - return this.var(head, loc); + return this.var({ name: original, loc }); } } - this(loc: SourceSpan): ASTv1.PathHead { + this({ loc }: { loc: SourceSpan }): ASTv1.ThisHead { return { type: 'ThisHead', loc, }; } - atName(name: string, loc: SourceSpan): ASTv1.PathHead { + atName({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { // the `@` should be included so we have a complete source range assert(name[0] === '@', `call builders.at() with a string that starts with '@'`); + assert( + name.indexOf('.') === -1, + `builder.at() should not be called with a name with dots in it` + ); return { type: 'AtHead', @@ -283,12 +305,16 @@ class Builders { }; } - var(name: string, loc: SourceSpan): ASTv1.PathHead { + var({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { assert(name !== 'this', `You called builders.var() with 'this'. Call builders.this instead`); assert( name[0] !== '@', `You called builders.var() with '${name}'. Call builders.at('${name}') instead` ); + assert( + name.indexOf('.') === -1, + `builder.var() should not be called with a name with dots in it` + ); return { type: 'VarHead', @@ -297,10 +323,10 @@ class Builders { }; } - hash(pairs: ASTv1.HashPair[], loc: SourceSpan): ASTv1.Hash { + hash({ pairs, loc }: { pairs: ASTv1.HashPair[]; loc: SourceSpan }): ASTv1.Hash { return { type: 'Hash', - pairs: pairs || [], + pairs, loc, }; } @@ -316,7 +342,7 @@ class Builders { }): ASTv1.HashPair { return { type: 'HashPair', - key: key, + key, value, loc, }; @@ -341,7 +367,7 @@ export type ElementParts = | ['attrs', ...AttrSexp[]] | ['modifiers', ...ModifierSexp[]] | ['body', ...ASTv1.Statement[]] - | ['comments', ...ElementComment[]] + | ['comments', ...ASTv1.MustacheCommentStatement[]] | ['as', ...string[]] | ['loc', SourceLocation]; @@ -357,8 +383,6 @@ export type AttrSexp = [string, ASTv1.AttrNode['value'] | string, LocSexp?]; export type LocSexp = ['loc', SourceLocation]; -export type ElementComment = ASTv1.MustacheCommentStatement | SourceLocation | string; - export type SexpValue = | string | ASTv1.Expression[] @@ -367,15 +391,4 @@ export type SexpValue = | PathSexp | undefined; -export interface BuildElementOptions { - tag: string; - selfClosing: boolean; - attrs: ASTv1.AttrNode[]; - modifiers: ASTv1.ElementModifierStatement[]; - children: ASTv1.Statement[]; - comments: ElementComment[]; - blockParams: string[]; - loc: SourceSpan; -} - export default new Builders(); diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 641600b796..f8e04dc29a 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -1,5 +1,5 @@ import type { Dict, Nullable } from '@glimmer/interfaces'; -import { asPresentArray, assert, assign, deprecate, isPresentArray } from '@glimmer/util'; +import { asPresentArray, assert, deprecate, isPresentArray } from '@glimmer/util'; import type { SourceLocation, SourcePosition } from '../source/location'; import type * as ASTv1 from './api'; @@ -7,12 +7,7 @@ import type * as ASTv1 from './api'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; -import { - buildLegacyLiteral, - buildLegacyMustache, - buildLegacyPath, - buildLegacyTemplate, -} from './legacy-interop'; +import b from './parser-builders'; let _SOURCE: Source | undefined; @@ -37,9 +32,9 @@ function buildMustache( hash: ASTv1.Hash = buildHash([]), trusting = false, loc?: SourceLocation, - strip: ASTv1.StripFlags = { open: false, close: false } + strip?: ASTv1.StripFlags ): ASTv1.MustacheStatement { - return buildLegacyMustache({ + return b.mustache({ path: buildPath(path), params, hash, @@ -56,19 +51,22 @@ function buildBlock( params: Nullable, hash: Nullable, _defaultBlock: PossiblyDeprecatedBlock, - _elseBlock?: Nullable, + _elseBlock: Nullable = null, loc?: SourceLocation, openStrip?: ASTv1.StripFlags, inverseStrip?: ASTv1.StripFlags, closeStrip?: ASTv1.StripFlags ): ASTv1.BlockStatement { let defaultBlock: ASTv1.Block; - let elseBlock: Nullable | undefined; + let elseBlock: Nullable = null; if (_defaultBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); - - defaultBlock = assign({}, _defaultBlock, { type: 'Block' }) as unknown as ASTv1.Block; + defaultBlock = b.blockItself({ + blockParams: [..._defaultBlock.locals], + body: _defaultBlock.body, + loc: _defaultBlock.loc, + }); } else { defaultBlock = _defaultBlock; } @@ -76,23 +74,26 @@ function buildBlock( if (_elseBlock !== undefined && _elseBlock !== null && _elseBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); - elseBlock = assign({}, _elseBlock, { type: 'Block' }) as unknown as ASTv1.Block; + elseBlock = b.blockItself({ + blockParams: [..._elseBlock.locals], + body: _elseBlock.body, + loc: _elseBlock.loc, + }); } else { elseBlock = _elseBlock; } - return { - type: 'BlockStatement', + return b.block({ path: buildPath(path), params: params || [], hash: hash || buildHash([]), - program: defaultBlock || null, - inverse: elseBlock || null, + defaultBlock, + elseBlock, loc: buildLoc(loc || null), - openStrip: openStrip || { open: false, close: false }, - inverseStrip: inverseStrip || { open: false, close: false }, - closeStrip: closeStrip || { open: false, close: false }, - }; + openStrip, + inverseStrip, + closeStrip, + }); } function buildElementModifier( @@ -101,29 +102,26 @@ function buildElementModifier( hash?: ASTv1.Hash, loc?: Nullable ): ASTv1.ElementModifierStatement { - return { - type: 'ElementModifierStatement', + return b.elementModifier({ path: buildPath(path), params: params || [], hash: hash || buildHash([]), loc: buildLoc(loc || null), - }; + }); } function buildComment(value: string, loc?: SourceLocation): ASTv1.CommentStatement { - return { - type: 'CommentStatement', + return b.comment({ value: value, loc: buildLoc(loc || null), - }; + }); } function buildMustacheComment(value: string, loc?: SourceLocation): ASTv1.MustacheCommentStatement { - return { - type: 'MustacheCommentStatement', + return b.mustacheComment({ value: value, loc: buildLoc(loc || null), - }; + }); } function buildConcat( @@ -134,11 +132,10 @@ function buildConcat( throw new Error(`b.concat requires at least one part`); } - return { - type: 'ConcatStatement', - parts: parts || [], + return b.concat({ + parts, loc: buildLoc(loc || null), - }; + }); } // Nodes @@ -177,7 +174,7 @@ export interface BuildElementOptions { attrs?: ASTv1.AttrNode[]; modifiers?: ASTv1.ElementModifierStatement[]; children?: ASTv1.Statement[]; - comments?: ElementComment[]; + comments?: ASTv1.MustacheCommentStatement[]; blockParams?: string[]; loc?: SourceSpan; } @@ -199,129 +196,69 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS tagName = tag; } - return { - type: 'ElementNode', + return b.element({ tag: tagName, - selfClosing: selfClosing, + selfClosing, attributes: attrs || [], blockParams: blockParams || [], modifiers: modifiers || [], - comments: (comments as ASTv1.MustacheCommentStatement[]) || [], + comments: comments || [], children: children || [], loc: buildLoc(loc || null), - }; + }); } -function buildAttr( - name: string, - value: ASTv1.AttrNode['value'], - loc?: SourceLocation -): ASTv1.AttrNode { - return { - type: 'AttrNode', +function buildAttr(name: string, value: ASTv1.AttrValue, loc?: SourceLocation): ASTv1.AttrNode { + return b.attr({ name: name, value: value, loc: buildLoc(loc || null), - }; + }); } -function buildText(chars?: string, loc?: SourceLocation): ASTv1.TextNode { - return { - type: 'TextNode', - chars: chars || '', +function buildText(chars = '', loc?: SourceLocation): ASTv1.TextNode { + return b.text({ + chars, loc: buildLoc(loc || null), - }; + }); } // Expressions function buildSexpr( path: BuilderHead, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, + params: ASTv1.Expression[] = [], + hash: ASTv1.Hash = buildHash([]), loc?: SourceLocation ): ASTv1.SubExpression { - return { - type: 'SubExpression', + return b.sexpr({ path: buildPath(path), - params: params || [], - hash: hash || buildHash([]), + params, + hash, loc: buildLoc(loc || null), - }; + }); } -function buildHead( - original: string, - loc: SourceLocation -): { head: ASTv1.PathHead; tail: string[] } { +function buildHead(original: string, loc: SourceLocation): ASTv1.PathExpression { let [head, ...tail] = asPresentArray(original.split('.')); - let headNode: ASTv1.PathHead; - - if (head === 'this') { - headNode = { - type: 'ThisHead', - loc: buildLoc(loc || null), - }; - } else if (head[0] === '@') { - headNode = { - type: 'AtHead', - name: head, - loc: buildLoc(loc || null), - }; - } else { - headNode = { - type: 'VarHead', - name: head, - loc: buildLoc(loc || null), - }; - } - - return { - head: headNode, - tail, - }; + let headNode = b.head({ original: head, loc: buildLoc(loc || null) }); + return b.path({ head: headNode, tail, loc: buildLoc(loc || null) }); } function buildThis(loc: SourceLocation): ASTv1.PathHead { - return { - type: 'ThisHead', - loc: buildLoc(loc || null), - }; + return b.this({ loc: buildLoc(loc || null) }); } function buildAtName(name: string, loc: SourceLocation): ASTv1.PathHead { - // the `@` should be included so we have a complete source range - assert(name[0] === '@', `call builders.at() with a string that starts with '@'`); - - return { - type: 'AtHead', - name, - loc: buildLoc(loc || null), - }; + return b.atName({ name, loc: buildLoc(loc || null) }); } function buildVar(name: string, loc: SourceLocation): ASTv1.PathHead { - assert(name !== 'this', `You called builders.var() with 'this'. Call builders.this instead`); - assert( - name[0] !== '@', - `You called builders.var() with '${name}'. Call builders.at('${name}') instead` - ); - - return { - type: 'VarHead', - name, - loc: buildLoc(loc || null), - }; + return b.var({ name, loc: buildLoc(loc || null) }); } -function buildHeadFromString(head: string, loc: SourceLocation): ASTv1.PathHead { - if (head[0] === '@') { - return buildAtName(head, loc); - } else if (head === 'this') { - return buildThis(loc); - } else { - return buildVar(head, loc); - } +function buildHeadFromString(original: string, loc: SourceLocation): ASTv1.PathHead { + return b.head({ original, loc: buildLoc(loc || null) }); } function buildCleanPath( @@ -329,7 +266,7 @@ function buildCleanPath( tail: string[], loc: SourceLocation ): ASTv1.PathExpression { - return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); + return b.path({ head, tail, loc: buildLoc(loc || null) }); } function buildPath( @@ -347,20 +284,24 @@ function buildPath( if ('type' in path) { return path; } else { - let { head, tail } = buildHead(path.head, SourceSpan.broken()); - assert( - tail.length === 0, + path.head.indexOf('.') === -1, `builder.path({ head, tail }) should not be called with a head with dots in it` ); - return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); + let { head, tail } = path; + + return b.path({ + head: b.head({ original: head, loc: SourceSpan.broken() }), + tail, + loc: buildLoc(loc || null), + }); } } let { head, tail } = buildHead(path, SourceSpan.broken()); - return buildLegacyPath({ head, tail, loc: buildLoc(loc || null) }); + return b.path({ head, tail, loc: buildLoc(loc || null) }); } function buildLiteral( @@ -368,7 +309,7 @@ function buildLiteral( value: T['value'], loc?: SourceLocation ): T { - return buildLegacyLiteral({ + return b.literal({ type, value, loc: buildLoc(loc || null), @@ -377,21 +318,19 @@ function buildLiteral( // Miscellaneous -function buildHash(pairs?: ASTv1.HashPair[], loc?: SourceLocation): ASTv1.Hash { - return { - type: 'Hash', - pairs: pairs || [], +function buildHash(pairs: ASTv1.HashPair[] = [], loc?: SourceLocation): ASTv1.Hash { + return b.hash({ + pairs, loc: buildLoc(loc || null), - }; + }); } function buildPair(key: string, value: ASTv1.Expression, loc?: SourceLocation): ASTv1.HashPair { - return { - type: 'HashPair', - key: key, + return b.pair({ + key, value, loc: buildLoc(loc || null), - }; + }); } function buildProgram( @@ -409,18 +348,17 @@ function buildProgram( } function buildBlockItself( - body?: ASTv1.Statement[], - blockParams?: string[], + body: ASTv1.Statement[] = [], + blockParams: string[] = [], chained = false, loc?: SourceLocation ): ASTv1.Block { - return { - type: 'Block', - body: body || [], - blockParams: blockParams || [], + return b.blockItself({ + body, + blockParams, chained, loc: buildLoc(loc || null), - }; + }); } function buildTemplate( @@ -428,7 +366,7 @@ function buildTemplate( locals: string[] = [], loc?: SourceLocation ): ASTv1.Template { - return buildLegacyTemplate({ + return b.template({ body, locals, loc: buildLoc(loc || null), @@ -436,10 +374,10 @@ function buildTemplate( } function buildPosition(line: number, column: number): SourcePosition { - return { + return b.pos({ line, column, - }; + }); } function buildLoc(loc: Nullable): SourceSpan; @@ -450,8 +388,17 @@ function buildLoc( endColumn?: number, source?: string ): SourceSpan; - -function buildLoc(...args: any[]): SourceSpan { +function buildLoc( + ...args: + | [Nullable] + | [ + startLine: number, + startColumn: number, + endLine?: number | undefined, + endColumn?: number | undefined, + source?: string | undefined, + ] +): SourceSpan { if (args.length === 1) { let loc = args[0]; @@ -470,8 +417,8 @@ function buildLoc(...args: any[]): SourceSpan { column: startColumn, }, end: { - line: endLine, - column: endColumn, + line: endLine || startLine, + column: endColumn || startColumn, }, }); } diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index 0c198a5c3c..2a2add36bc 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -753,7 +753,7 @@ class ElementNormalizer { if (isComponent) { let path = b.path({ - head: b.head(variable, variableLoc), + head: b.head({ original: variable, loc: variableLoc }), tail, loc: pathLoc, }); diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index 15df3a5242..1269e570fa 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -5,7 +5,7 @@ import { syntaxErrorFor } from '@glimmer-workspace/test-utils'; import { astEqual } from './support'; -const { test, skip } = QUnit; +const { test } = QUnit; QUnit.module('[glimmer-syntax] Parser - AST'); @@ -484,25 +484,50 @@ test('Whitespace control - preserve all whitespace if config is set', () => { }); // TODO: Make these throw an error. -skip('Awkward mustache in unquoted attribute value', () => { - let t = '
'; - astEqual( - t, - b.template([element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo')])]])]) +test('Awkward mustache in unquoted attribute value', (assert) => { + assert.throws( + () => { + parse('
', { + meta: { moduleName: 'test-module' }, + }); + }, + syntaxErrorFor( + `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`, + 'class=a{{foo}}', + 'test-module', + 1, + 5 + ) ); - t = '
'; - astEqual( - t, - b.template([ - element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo'), b.text('b')])]]), - ]) + assert.throws( + () => { + parse('
', { + meta: { moduleName: 'test-module' }, + }); + }, + syntaxErrorFor( + `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`, + 'class=a{{foo}}b', + 'test-module', + 1, + 5 + ) ); - t = '
'; - astEqual( - t, - b.template([element('div', ['attrs', ['class', b.concat([b.mustache('foo'), b.text('b')])]])]) + assert.throws( + () => { + parse('
', { + meta: { moduleName: 'test-module' }, + }); + }, + syntaxErrorFor( + `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`, + 'class={{foo}}b', + 'test-module', + 1, + 5 + ) ); }); @@ -990,13 +1015,7 @@ export function normalizeModifier(sexp: ModifierSexp): ASTv1.ElementModifierStat loc = next[1]; } - return { - type: 'ElementModifierStatement', - path, - params: params || [], - hash: hash || b.hash([]), - loc: b.loc(loc || null), - }; + return b.elementModifier(path as ASTv1.CallableExpression, params, hash, b.loc(loc || null)); } export function normalizeHead(path: PathSexp): ASTv1.Expression { From 6070862e385efe282f92932c92af68c6b3f4f871 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sun, 25 Feb 2024 00:19:20 -0800 Subject: [PATCH 12/17] Add v1 AST PathHead.original This makes printing-esq tasks easier when working with `path.head` and restores feature parity with `path.original` which is a very common way to work with PathExpression nodes --- .../@glimmer/syntax/lib/v1/legacy-interop.ts | 24 ++---- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 11 +++ .../@glimmer/syntax/lib/v1/parser-builders.ts | 85 ++++++++++++++----- packages/@glimmer/syntax/lib/v2/normalize.ts | 8 +- 4 files changed, 79 insertions(+), 49 deletions(-) diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 188910a9b5..0f86391373 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -70,35 +70,21 @@ export function buildLegacyMustache({ export type PathExpressionParams = Omit; -function original(head: ASTv1.PathHead, tail: readonly string[]): string { - switch (head.type) { - case 'ThisHead': - return ['this', ...tail].join('.'); - case 'AtHead': - case 'VarHead': - return [head.name, ...tail].join('.'); - } -} - export function buildLegacyPath({ head, tail, loc }: PathExpressionParams): ASTv1.PathExpression { const node = { type: 'PathExpression', head, tail, - loc, - }; - - Object.defineProperty(node, 'original', { - enumerable: true, - get(this: typeof node): string { - return original(this.head, this.tail); + get original() { + return [this.head.original, ...this.tail].join('.'); }, - set(this: typeof node, value: string) { + set original(value: string) { let [head, ...tail] = asPresentArray(value.split('.')); this.head = b.head(head, this.head.loc); this.tail = tail; }, - }); + loc, + }; Object.defineProperty(node, 'parts', { enumerable: false, diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 6680c5f4c2..9763381375 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -146,6 +146,7 @@ export interface SubExpression extends BaseNode { export interface ThisHead { type: 'ThisHead'; + original: 'this'; loc: src.SourceSpan; } @@ -153,12 +154,22 @@ export interface AtHead { type: 'AtHead'; name: string; loc: src.SourceSpan; + + /** + * alias for name + */ + original: string; } export interface VarHead { type: 'VarHead'; name: string; loc: src.SourceSpan; + + /** + * alias for name + */ + original: string; } export type PathHead = ThisHead | AtHead | VarHead; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index a616082d25..d37392002e 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -286,41 +286,80 @@ class Builders { this({ loc }: { loc: SourceSpan }): ASTv1.ThisHead { return { type: 'ThisHead', + get original() { + return 'this' as const; + }, loc, }; } atName({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { - // the `@` should be included so we have a complete source range - assert(name[0] === '@', `call builders.at() with a string that starts with '@'`); - assert( - name.indexOf('.') === -1, - `builder.at() should not be called with a name with dots in it` - ); - - return { - type: 'AtHead', - name, + let _name = ''; + + const node = { + type: 'AtHead' as const, + get name() { + return _name; + }, + set name(value) { + assert(value[0] === '@', `call builders.at() with a string that starts with '@'`); + assert( + value.indexOf('.') === -1, + `builder.at() should not be called with a name with dots in it` + ); + _name = value; + }, + get original() { + return this.name; + }, + set original(value) { + this.name = value; + }, loc, }; + + // trigger the assertions + node.name = name; + + return node; } var({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { - assert(name !== 'this', `You called builders.var() with 'this'. Call builders.this instead`); - assert( - name[0] !== '@', - `You called builders.var() with '${name}'. Call builders.at('${name}') instead` - ); - assert( - name.indexOf('.') === -1, - `builder.var() should not be called with a name with dots in it` - ); - - return { - type: 'VarHead', - name, + let _name = ''; + + const node = { + type: 'VarHead' as const, + get name() { + return _name; + }, + set name(value) { + assert( + value !== 'this', + `You called builders.var() with 'this'. Call builders.this instead` + ); + assert( + value[0] !== '@', + `You called builders.var() with '${name}'. Call builders.at('${name}') instead` + ); + assert( + value.indexOf('.') === -1, + `builder.var() should not be called with a name with dots in it` + ); + _name = value; + }, + get original() { + return this.name; + }, + set original(value) { + this.name = value; + }, loc, }; + + // trigger the assertions + node.name = name; + + return node; } hash({ pairs, loc }: { pairs: ASTv1.HashPair[]; loc: SourceSpan }): ASTv1.Hash { diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index 2a2add36bc..6530c15525 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -1010,13 +1010,7 @@ function printPath(node: ASTv1.PathExpression | ASTv1.CallNode): string { function printHead(node: ASTv1.PathExpression | ASTv1.CallNode): string { if (node.type === 'PathExpression') { - switch (node.head.type) { - case 'AtHead': - case 'VarHead': - return node.head.name; - case 'ThisHead': - return 'this'; - } + return node.head.original; } else if (node.path.type === 'PathExpression') { return printHead(node.path); } else { From 3104daf14212d2c30956974a79ae9ed49a455317 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sun, 25 Feb 2024 12:04:52 -0800 Subject: [PATCH 13/17] Second attempt at landing #1552 (part 1/2) The original commit has a few issues: - Type safety - Added a new AST node, which is unnecessary since we already have `VarHead`, but is also a breaking change and is kind of breakage we have been careful to avoid in this area thus far - The parsing is robust against a few classes of edge cases This rework the same feature (on the `Block`/`BlockStatement` side) and adds more test coverage for the feature. Also renamed the field to `block.params`. --- packages/@glimmer/syntax/lib/parser.ts | 10 +- .../lib/parser/handlebars-node-visitors.ts | 152 +++++++++++++----- .../lib/parser/tokenizer-event-handlers.ts | 5 +- .../@glimmer/syntax/lib/source/loc/span.ts | 2 +- .../@glimmer/syntax/lib/v1/handlebars-ast.ts | 4 +- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 35 +++- .../@glimmer/syntax/lib/v1/parser-builders.ts | 71 +++----- .../@glimmer/syntax/lib/v1/public-builders.ts | 23 ++- .../@glimmer/syntax/test/loc-node-test.ts | 136 ++++++++++++++-- .../@glimmer/syntax/test/parser-node-test.ts | 77 +++++++++ 10 files changed, 400 insertions(+), 115 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index 1463f967d9..2724b9fbcf 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -76,6 +76,8 @@ export abstract class Parser { // node.loc = node.loc.withEnd(end); } + abstract parse(node: HBS.Program, locals: string[]): ASTv1.Template; + abstract Program(node: HBS.Program): HBS.Output<'Program'>; abstract MustacheStatement(node: HBS.MustacheStatement): HBS.Output<'MustacheStatement'>; abstract Decorator(node: HBS.Decorator): HBS.Output<'Decorator'>; @@ -149,14 +151,6 @@ export abstract class Parser { return node; } - acceptTemplate(node: HBS.Program): ASTv1.Template { - let result = this.Program(node); - assert(result.type === 'Template', 'expected a template'); - return result; - } - - acceptNode(node: HBS.Program): ASTv1.Block | ASTv1.Template; - acceptNode(node: HBS.Node): U; acceptNode(node: HBS.Node): HBS.Output { return (this[node.type as T] as (node: HBS.Node) => HBS.Output)(node); } diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 8719cb116e..b23dceeb07 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -1,8 +1,9 @@ import type { Nullable, Recast } from '@glimmer/interfaces'; import type { TokenizerState } from 'simple-html-tokenizer'; -import { getLast, isPresentArray, unwrap } from '@glimmer/util'; +import { assert, getLast, isPresentArray, unwrap } from '@glimmer/util'; import type { ParserNodeBuilder, Tag } from '../parser'; +import type { SourceSpan } from '../source/span'; import type * as ASTv1 from '../v1/api'; import type * as HBS from '../v1/handlebars-ast'; @@ -24,46 +25,62 @@ export abstract class HandlebarsNodeVisitors extends Parser { return this.elementStack.length === 0; } - Program(program: HBS.Program): ASTv1.Block; - Program(program: HBS.Program): ASTv1.Template; - Program(program: HBS.Program): ASTv1.Template | ASTv1.Block; - Program(program: HBS.Program): ASTv1.Block | ASTv1.Template { - const body: ASTv1.Statement[] = []; - let node; - - if (this.isTopLevel) { - node = b.template({ - body, - loc: this.source.spanFor(program.loc), - }); - } else { - node = b.blockItself({ - body, - blockParams: program.blockParams, - chained: program.chained, - loc: this.source.spanFor(program.loc), - }); - } + parse(program: HBS.Program, locals: string[]): ASTv1.Template { + let node = b.template({ + body: [], + locals, + loc: this.source.spanFor(program.loc), + }); - let i, - l = program.body.length; + return this.parseProgram(node, program); + } - this.elementStack.push(node); + Program(program: HBS.Program, blockParams?: ASTv1.VarHead[]): ASTv1.Block { + // The abstract signature doesn't have the blockParams argument, but in + // practice we can only come from this.BlockStatement() which adds the + // extra argument for us + assert( + Array.isArray(blockParams), + '[BUG] Program in parser unexpectedly called without block params' + ); - if (l === 0) { - return this.elementStack.pop() as ASTv1.Block | ASTv1.Template; + let node = b.blockItself({ + body: [], + params: blockParams, + chained: program.chained, + loc: this.source.spanFor(program.loc), + }); + + return this.parseProgram(node, program); + } + + private parseProgram(node: T, program: HBS.Program): T { + if (program.body.length === 0) { + return node; } - for (i = 0; i < l; i++) { - this.acceptNode(unwrap(program.body[i])); + let poppedNode; + + try { + this.elementStack.push(node); + + for (let child of program.body) { + this.acceptNode(child); + } + } finally { + poppedNode = this.elementStack.pop(); } // Ensure that that the element stack is balanced properly. - const poppedNode = this.elementStack.pop(); - if (poppedNode !== node) { - const elementNode = poppedNode as ASTv1.ElementNode; - - throw generateSyntaxError(`Unclosed element \`${elementNode.tag}\``, elementNode.loc); + if (node !== poppedNode) { + if (poppedNode?.type === 'ElementNode') { + throw generateSyntaxError(`Unclosed element \`${poppedNode.tag}\``, poppedNode.loc); + } else { + // If the stack is not balanced, then it is likely our own bug, because + // any unclosed Handlebars blocks should already been caught by now + assert(poppedNode !== undefined, '[BUG] empty parser elementStack'); + assert(false, `[BUG] mismatched parser elementStack node: ${node.type}`); + } } return node; @@ -83,6 +100,65 @@ export abstract class HandlebarsNodeVisitors extends Parser { } const { path, params, hash } = acceptCallNodes(this, block); + const loc = this.source.spanFor(block.loc); + + // Backfill block params loc for the default block + let blockParams: ASTv1.VarHead[] = []; + + if (block.program.blockParams?.length) { + // Start from right after the hash + let span = hash.loc.collapse('end'); + + // Extend till the beginning of the block + if (block.program.loc) { + span = span.withEnd(this.source.spanFor(block.program.loc).getStart()); + } else if (block.program.body[0]) { + span = span.withEnd(this.source.spanFor(block.program.body[0].loc).getStart()); + } else { + // ...or if all else fail, use the end of the block statement + // this can only happen if the block statement is empty anyway + span = span.withEnd(loc.getEnd()); + } + + // Now we have a span for something like this: + // + // {{#foo bar baz=bat as |wow wat|}} + // ~~~~~~~~~~~~~~~ + // + // Or, if we are unlucky: + // + // {{#foo bar baz=bat as |wow wat|}}{{/foo}} + // ~~~~~~~~~~~~~~~~~~~~~~~ + // + // Either way, within this span, there should be exactly two pipes + // fencing our block params, neatly whitespace separated and with + // legal identifiers only + const content = span.asString(); + let skipStart = content.indexOf('|') + 1; + const limit = content.indexOf('|', skipStart); + + for (const name of block.program.blockParams) { + let nameStart: number; + let loc: SourceSpan; + + if (skipStart >= limit) { + nameStart = -1; + } else { + nameStart = content.indexOf(name, skipStart); + } + + if (nameStart === -1 || nameStart + name.length > limit) { + skipStart = limit; + loc = this.source.spanFor(NON_EXISTENT_LOCATION); + } else { + skipStart = nameStart; + loc = span.sliceStartChars({ skipStart, chars: name.length }); + skipStart += name.length; + } + + blockParams.push(b.var({ name, loc })); + } + } // These are bugs in Handlebars upstream if (!block.program.loc) { @@ -93,8 +169,8 @@ export abstract class HandlebarsNodeVisitors extends Parser { block.inverse.loc = NON_EXISTENT_LOCATION; } - const program = this.Program(block.program); - const inverse = block.inverse ? this.Program(block.inverse) : null; + const program = this.Program(block.program, blockParams); + const inverse = block.inverse ? this.Program(block.inverse, []) : null; const node = b.block({ path, @@ -126,7 +202,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { if (isHBSLiteral(rawMustache.path)) { mustache = b.mustache({ - path: this.acceptNode(rawMustache.path), + path: this.acceptNode<(typeof rawMustache.path)['type']>(rawMustache.path), params: [], hash: b.hash({ pairs: [], loc: this.source.spanFor(rawMustache.path.loc).collapse('end') }), trusting: !escaped, @@ -388,7 +464,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { const pairs = hash.pairs.map((pair) => b.pair({ key: pair.key, - value: this.acceptNode(pair.value), + value: this.acceptNode(pair.value), loc: this.source.spanFor(pair.loc), }) ); @@ -536,7 +612,7 @@ function acceptCallNodes( } const params = node.params - ? node.params.map((e) => compiler.acceptNode(e)) + ? node.params.map((e) => compiler.acceptNode(e)) : []; // if there is no hash, position it as a collapsed node immediately after the last param (or the diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 85955bc70a..660c8dc7a0 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -441,7 +441,10 @@ export function preprocess( end: offsets.endPosition, }; - let template = new TokenizerEventHandlers(source, entityParser, mode).acceptTemplate(ast); + let template = new TokenizerEventHandlers(source, entityParser, mode).parse( + ast, + options.locals ?? [] + ); if (options.strictMode && options.locals?.length) { template = b.template({ ...template, locals: options.locals }); diff --git a/packages/@glimmer/syntax/lib/source/loc/span.ts b/packages/@glimmer/syntax/lib/source/loc/span.ts index 73c50a0db2..627de87a77 100644 --- a/packages/@glimmer/syntax/lib/source/loc/span.ts +++ b/packages/@glimmer/syntax/lib/source/loc/span.ts @@ -190,7 +190,7 @@ export class SourceSpan implements SourceLocation { /** * Create a new span with the current span's beginning and a new ending. */ - withEnd(this: SourceSpan, other: SourceOffset): SourceSpan { + withEnd(other: SourceOffset): SourceSpan { return span(this.data.getStart(), other.data); } diff --git a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts index 9a70737fd7..371503f7b9 100644 --- a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts +++ b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts @@ -13,7 +13,7 @@ export interface CommonNode { } export interface NodeMap { - Program: { input: Program; output: ASTv1.Template | ASTv1.Block }; + Program: { input: Program; output: ASTv1.Block }; MustacheStatement: { input: MustacheStatement; output: ASTv1.MustacheStatement | void }; Decorator: { input: Decorator; output: never }; BlockStatement: { input: BlockStatement; output: ASTv1.BlockStatement | void }; @@ -50,7 +50,7 @@ export interface Position { export interface Program extends CommonNode { type: 'Program'; body: Statement[]; - blockParams: string[]; + blockParams?: string[]; chained?: boolean; } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 9763381375..2d6650679a 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -16,8 +16,13 @@ export interface CommonProgram extends BaseNode { export interface Block extends CommonProgram { type: 'Block'; - blockParams: string[]; + params: VarHead[]; chained?: boolean; + + /** + * string accessor for params.name + */ + blockParams: string[]; } export type EntityEncodingState = 'transformed' | 'raw'; @@ -302,6 +307,34 @@ export type Nodes = { export type NodeType = keyof Nodes; export type Node = Nodes[NodeType]; +// These "sub-node" cannot appear standalone, they are only used inside another +// "real" AST node to provide richer information. The distinction mostly exists +// for backwards compatibility reason. These nodes are not traversed and do not +// have visitor keys for them, so it won't break existing AST consumers (e.g. +// those that implemented an `All` visitor may not be expecting these new types +// of nodes). +// +// Conceptually, the idea of "sub-node" does make sense, and you can say source +// locations are another kind of these things. However, in these cases, they +// actually fully implement the `BaseNode` interface, and only not extending +// `BaseNode` because the `type` field is not `keyof Nodes` (which is circular +// reasoning). If these are not "real" nodes because they can only appear in +// very limited context, then the same reasoning probably applies for, say, +// HashPair. +// +// If we do eventually make some kind of breaking change here, perhaps with +// some kind of opt-in, then we can consider upgrading these into "real" nodes, +// but for now, this is where they go, and it isn't a huge problem in practice +// because there are little utility in traversing these kind of nodes anyway. +export type SubNodes = { + ThisHead: ThisHead; + AtHead: AtHead; + VarHead: VarHead; +}; + +export type SubNodeType = keyof SubNodes; +export type SubNode = SubNodes[SubNodeType]; + export type Statement = Nodes[StatementName]; export type Statements = Pick; export type Literal = Nodes[LiteralName]; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index d37392002e..73e9b4f961 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -1,10 +1,9 @@ -import type { Dict, Nullable, Optional, PresentArray } from '@glimmer/interfaces'; +import type { Nullable, Optional, PresentArray } from '@glimmer/interfaces'; import { assert } from '@glimmer/util'; -import type { SourceLocation } from '../source/location'; -import type { SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; +import { SourceSpan } from '../source/span'; import { buildLegacyLiteral, buildLegacyMustache, @@ -32,32 +31,40 @@ class Builders { } blockItself({ - body = [], - blockParams = [], + body, + params, chained = false, loc, }: { - body?: ASTv1.Statement[] | undefined; - blockParams?: string[] | undefined; - chained?: boolean | undefined; + body: ASTv1.Statement[]; + params: ASTv1.VarHead[]; + chained?: Optional; loc: SourceSpan; }): ASTv1.Block { return { type: 'Block', - body: body, - blockParams: blockParams, + body, + params, + get blockParams() { + return this.params.map((p) => p.name); + }, + set blockParams(params: string[]) { + this.params = params.map((name) => { + return b.var({ name, loc: SourceSpan.synthetic(name) }); + }); + }, chained, loc, }; } template({ - body = [], - locals = [], + body, + locals, loc, }: { - body?: ASTv1.Statement[]; - locals?: string[]; + body: ASTv1.Statement[]; + locals: string[]; loc: SourceSpan; }): ASTv1.Template { return buildLegacyTemplate({ @@ -293,7 +300,7 @@ class Builders { }; } - atName({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { + atName({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.AtHead { let _name = ''; const node = { @@ -324,7 +331,7 @@ class Builders { return node; } - var({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.PathHead { + var({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.VarHead { let _name = ''; const node = { @@ -400,34 +407,6 @@ class Builders { } } -// Nodes - -export type ElementParts = - | ['attrs', ...AttrSexp[]] - | ['modifiers', ...ModifierSexp[]] - | ['body', ...ASTv1.Statement[]] - | ['comments', ...ASTv1.MustacheCommentStatement[]] - | ['as', ...string[]] - | ['loc', SourceLocation]; - -export type PathSexp = string | ['path', string, LocSexp?]; - -export type ModifierSexp = - | string - | [PathSexp, LocSexp?] - | [PathSexp, ASTv1.Expression[], LocSexp?] - | [PathSexp, ASTv1.Expression[], Dict, LocSexp?]; - -export type AttrSexp = [string, ASTv1.AttrNode['value'] | string, LocSexp?]; - -export type LocSexp = ['loc', SourceLocation]; - -export type SexpValue = - | string - | ASTv1.Expression[] - | Dict - | LocSexp - | PathSexp - | undefined; +const b = new Builders(); -export default new Builders(); +export default b; diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index f8e04dc29a..ec201ab92b 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -63,7 +63,7 @@ function buildBlock( if (_defaultBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); defaultBlock = b.blockItself({ - blockParams: [..._defaultBlock.locals], + params: buildBlockParams(_defaultBlock.locals), body: _defaultBlock.body, loc: _defaultBlock.loc, }); @@ -73,9 +73,10 @@ function buildBlock( if (_elseBlock !== undefined && _elseBlock !== null && _elseBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); + assert(_elseBlock.locals.length === 0, '{{else}} block cannot have block params'); elseBlock = b.blockItself({ - blockParams: [..._elseBlock.locals], + params: [], body: _elseBlock.body, loc: _elseBlock.loc, }); @@ -280,6 +281,8 @@ function buildPath( path: BuilderHead | ASTv1.Expression | { head: string; tail: string[] }, loc?: SourceLocation ): ASTv1.Expression { + let span = buildLoc(loc || null); + if (typeof path !== 'string') { if ('type' in path) { return path; @@ -292,16 +295,16 @@ function buildPath( let { head, tail } = path; return b.path({ - head: b.head({ original: head, loc: SourceSpan.broken() }), + head: b.head({ original: head, loc: span.sliceStartChars({ chars: head.length }) }), tail, loc: buildLoc(loc || null), }); } } - let { head, tail } = buildHead(path, SourceSpan.broken()); + let { head, tail } = buildHead(path, span); - return b.path({ head, tail, loc: buildLoc(loc || null) }); + return b.path({ head, tail, loc: span }); } function buildLiteral( @@ -347,15 +350,21 @@ function buildProgram( } } +function buildBlockParams(params: ReadonlyArray): ASTv1.VarHead[] { + return params.map((p) => + typeof p === 'string' ? b.var({ name: p, loc: SourceSpan.synthetic(p) }) : p + ); +} + function buildBlockItself( body: ASTv1.Statement[] = [], - blockParams: string[] = [], + params: Array = [], chained = false, loc?: SourceLocation ): ASTv1.Block { return b.blockItself({ body, - blockParams, + params: buildBlockParams(params), chained, loc: buildLoc(loc || null), }); diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index c41a431944..d3eec8896c 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -4,24 +4,28 @@ import { guardArray } from '@glimmer-workspace/test-utils'; QUnit.module('[glimmer-syntax] Parser - Location Info'); -function assertNodeType( - node: AST.Node | null | undefined, +function assertNodeType(node: unknown, type: T): node is AST.Nodes[T]; +function assertNodeType( + node: unknown, type: T -): node is AST.Nodes[T] { - let nodeType = node && node.type; - QUnit.assert.pushResult({ - result: nodeType === type, - actual: nodeType, - expected: type, - message: `expected node type to be ${type} but was ${String(nodeType)}`, - }); +): node is AST.SubNodes[T]; +function assertNodeType(node: unknown, type: string): boolean { + let nodeType: unknown = undefined; + + try { + nodeType = (node as { type?: unknown } | null | undefined)?.type; + } catch { + // no-op + } + + QUnit.assert.strictEqual(nodeType, type, `expected node type to be ${type}`); return nodeType === type; } const { test } = QUnit; function locEqual( - node: AST.Node | null | undefined, + node: AST.Node | AST.SubNode | null | undefined, startLine: number, startColumn: number, endLine: number, @@ -201,6 +205,116 @@ test('mustache + newline + element ', () => { locEqual(p, 3, 4, 3, 14, 'p element'); }); +test('block with block params', () => { + let ast = parse(` + {{#foo as |bar bat baz|}} + {{bar}} {{bat}} {{baz}} + {{/foo}} + `); + + let statement = ast.body[1]; + if (assertNodeType(statement, 'BlockStatement')) { + let block = statement.program; + + if (assertNodeType(block.params[0], 'VarHead')) { + locEqual(block.params[0], 2, 15, 2, 18, 'bar'); + } + + if (assertNodeType(block.params[1], 'VarHead')) { + locEqual(block.params[1], 2, 19, 2, 22, 'bat'); + } + + if (assertNodeType(block.params[2], 'VarHead')) { + locEqual(block.params[2], 2, 23, 2, 26, 'baz'); + } + } +}); + +test('block with block params edge case: multiline', () => { + let ast = parse(` + {{#foo as +|bar bat + b +a + z|}} + {{bar}} {{bat}} {{baz}} + {{/foo}} + `); + + let statement = ast.body[1]; + if (assertNodeType(statement, 'BlockStatement')) { + let block = statement.program; + + if (assertNodeType(block.params[0], 'VarHead')) { + locEqual(block.params[0], 3, 1, 3, 4, 'bar'); + } + + if (assertNodeType(block.params[1], 'VarHead')) { + locEqual(block.params[1], 3, 5, 3, 8, 'bat'); + } + + if (assertNodeType(block.params[2], 'VarHead')) { + locEqual(block.params[2], 4, 6, 4, 7, 'b'); + } + + if (assertNodeType(block.params[3], 'VarHead')) { + locEqual(block.params[3], 5, 0, 5, 1, 'a'); + } + + if (assertNodeType(block.params[4], 'VarHead')) { + locEqual(block.params[4], 6, 6, 6, 7, 'z'); + } + } +}); + +test('block with block params edge case: block-params like params', () => { + let ast = parse(` + {{#foo "as |bar bat baz|" as |bar bat baz|}} + {{bar}} {{bat}} {{baz}} + {{/foo}} + `); + + let statement = ast.body[1]; + if (assertNodeType(statement, 'BlockStatement')) { + let block = statement.program; + + if (assertNodeType(block.params[0], 'VarHead')) { + locEqual(block.params[0], 2, 34, 2, 37, 'bar'); + } + + if (assertNodeType(block.params[1], 'VarHead')) { + locEqual(block.params[1], 2, 38, 2, 41, 'bat'); + } + + if (assertNodeType(block.params[2], 'VarHead')) { + locEqual(block.params[2], 2, 42, 2, 45, 'baz'); + } + } +}); + +test('block with block params edge case: block-params like content', () => { + let ast = parse(` + {{#foo as |bar bat baz|}}as |bar bat baz|{{/foo}} + `); + + let statement = ast.body[1]; + if (assertNodeType(statement, 'BlockStatement')) { + let block = statement.program; + + if (assertNodeType(block.params[0], 'VarHead')) { + locEqual(block.params[0], 2, 15, 2, 18, 'bar'); + } + + if (assertNodeType(block.params[1], 'VarHead')) { + locEqual(block.params[1], 2, 19, 2, 22, 'bat'); + } + + if (assertNodeType(block.params[2], 'VarHead')) { + locEqual(block.params[2], 2, 23, 2, 26, 'baz'); + } + } +}); + test('blocks with nested html elements', () => { let ast = parse(` {{#foo-bar}}
Foo
{{/foo-bar}}

Hi!

diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index 1269e570fa..1b1644a337 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -251,6 +251,83 @@ test('Involved block helper', () => { ); }); +test('block with block params', () => { + let t = `{{#foo as |bar bat baz|}}{{bar}} {{bat}} {{baz}}{{/foo}}`; + + astEqual( + t, + b.template([ + b.block( + b.path('foo'), + null, + null, + b.blockItself( + [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')], + ['bar', 'bat', 'baz'] + ) + ), + ]) + ); +}); + +test('block with block params edge case: multiline', () => { + let t = `{{#foo as +|bar bat + b +a + z|}}{{bar}} {{bat}} {{baz}}{{/foo}}`; + + astEqual( + t, + b.template([ + b.block( + b.path('foo'), + null, + null, + b.blockItself( + [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')], + ['bar', 'bat', 'b', 'a', 'z'] + ) + ), + ]) + ); +}); + +test('block with block params edge case: block-params like params', () => { + let t = `{{#foo "as |a b c|" as |bar bat baz|}}{{bar}} {{bat}} {{baz}}{{/foo}}`; + + astEqual( + t, + b.template([ + b.block( + b.path('foo'), + [b.string('as |a b c|')], + null, + b.blockItself( + [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')], + ['bar', 'bat', 'baz'] + ) + ), + ]) + ); +}); + +test('block with block params edge case: block-params like content', () => { + let t = `{{#foo as |bar bat baz|}}as |a b c|{{/foo}}`; + + astEqual( + t, + b.template([ + b.block( + b.path('foo'), + null, + null, + b.blockItself([b.text('as |a b c|')], ['bar', 'bat', 'baz']) + ), + ]) + ); +}); + test('Element modifiers', () => { let t = "

Some content

"; astEqual( From 13aeaa093634000502a894c19f5f0784d241f29f Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Mon, 26 Feb 2024 16:49:34 -0800 Subject: [PATCH 14/17] Second attempt at landing #1552 (part 2/2) The previous/existing "parsing" code tries to piggy back on letting the HTML tokenizer/parser parse out the block params syntax into attribute nodes and tries to recover the information after the fact This is fundamentally broken and suffers from some wild edge cases like: ```
foo', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Cannot use zero block parameters', - 'foo', + 'Invalid block parameters syntax: empty parameters list, expecting at least one identifier', + 'as ||', 'test-module', 1, - 0 + 7 ) ); @@ -71,11 +87,108 @@ class SyntaxErrors extends RenderTest { preprocess('foo', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Cannot use zero block parameters', - 'foo', + 'Invalid block parameters syntax: empty parameters list, expecting at least one identifier', + 'as | |', 'test-module', 1, - 0 + 7 + ) + ); + } + + @test + 'Block params in HTML syntax - invalid mustaches in block params list'() { + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{foo}}', + 'test-module', + 1, + 11 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{bar}}', + 'test-module', + 1, + 14 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{bar}}', + 'test-module', + 1, + 15 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: modifiers cannot follow parameters list', + '{{bar}}', + 'test-module', + 1, + 16 + ) + ); + } + + @test + 'Block params in HTML syntax - EOF in block params list'() { + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |', + 'test-module', + 1, + 7 + ) + ); + + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |foo', + 'test-module', + 1, + 7 + ) + ); + + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |foo|', + 'test-module', + 1, + 7 ) ); } @@ -87,24 +200,26 @@ class SyntaxErrors extends RenderTest { preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x y'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting "|" but the tag was closed prematurely', + 'as |x y>', 'test-module', 1, - 0 + 7 ) ); this.assert.throws( () => { - preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); + preprocess('{{x}},{{y}}', { + meta: { moduleName: 'test-module' }, + }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x| y'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + 'wat', 'test-module', 1, - 0 + 14 ) ); @@ -113,11 +228,11 @@ class SyntaxErrors extends RenderTest { preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x| y|'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + 'y|', 'test-module', 1, - 0 + 14 ) ); } @@ -129,11 +244,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid identifier for block parameters, 'foo.bar'", - '', + 'Invalid block parameters syntax: invalid identifier name `foo.bar`', + 'foo.bar', 'test-module', 1, - 0 + 13 ) ); @@ -141,7 +256,13 @@ class SyntaxErrors extends RenderTest { () => { preprocess('', { meta: { moduleName: 'test-module' } }); }, - syntaxErrorFor('" is not a valid character within attribute names', '', 'test-module', 1, 17) + syntaxErrorFor( + 'Invalid block parameters syntax: invalid identifier name `"foo"`', + '"foo"', + 'test-module', + 1, + 13 + ) ); this.assert.throws( @@ -149,11 +270,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid identifier for block parameters, 'foo[bar]'", - '', + 'Invalid block parameters syntax: invalid identifier name `foo[bar]`', + 'foo[bar]', 'test-module', 1, - 0 + 11 ) ); } @@ -165,11 +286,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - '', + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + '|x|', 'test-module', 1, - 0 + 7 ) ); @@ -180,11 +301,11 @@ class SyntaxErrors extends RenderTest { }); }, syntaxErrorFor( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - '<:baz |x|>', + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + '|x|', 'test-module', 1, - 7 + 13 ) ); } diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index 2724b9fbcf..cbf1c07277 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -14,16 +14,23 @@ export type ParserNodeBuilder = Omit { - readonly type: T; +export interface StartTag { + readonly type: 'StartTag'; name: string; readonly attributes: ASTv1.AttrNode[]; readonly modifiers: ASTv1.ElementModifierStatement[]; readonly comments: ASTv1.MustacheCommentStatement[]; + readonly params: ASTv1.VarHead[]; selfClosing: boolean; readonly loc: src.SourceSpan; } +export interface EndTag { + readonly type: 'EndTag'; + name: string; + readonly loc: src.SourceSpan; +} + export interface Attribute { name: string; currentPart: ASTv1.TextNode | null; @@ -43,8 +50,8 @@ export abstract class Parser { Readonly< | ParserNodeBuilder | ParserNodeBuilder - | ParserNodeBuilder> - | ParserNodeBuilder> + | ParserNodeBuilder + | ParserNodeBuilder > > = null; public tokenizer: EventedTokenizer; @@ -121,19 +128,19 @@ export abstract class Parser { return expect(this.currentAttribute, 'expected attribute'); } - get currentTag(): ParserNodeBuilder> { + get currentTag(): ParserNodeBuilder | ParserNodeBuilder { let node = this.currentNode; assert(node && (node.type === 'StartTag' || node.type === 'EndTag'), 'expected tag'); return node; } - get currentStartTag(): ParserNodeBuilder> { + get currentStartTag(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'StartTag', 'expected start tag'); return node; } - get currentEndTag(): ParserNodeBuilder> { + get currentEndTag(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'EndTag', 'expected end tag'); return node; diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index b23dceeb07..fefb739986 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -2,8 +2,8 @@ import type { Nullable, Recast } from '@glimmer/interfaces'; import type { TokenizerState } from 'simple-html-tokenizer'; import { assert, getLast, isPresentArray, unwrap } from '@glimmer/util'; -import type { ParserNodeBuilder, Tag } from '../parser'; -import type { SourceSpan } from '../source/span'; +import type { ParserNodeBuilder, StartTag } from '../parser'; +import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from '../v1/api'; import type * as HBS from '../v1/handlebars-ast'; @@ -13,18 +13,28 @@ import { generateSyntaxError } from '../syntax-error'; import { appendChild, isHBSLiteral, printLiteral } from '../utils'; import b from '../v1/parser-builders'; -const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState; -const ATTRIBUTE_VALUE_UNQUOTED = 'attributeValueUnquoted' as TokenizerState; +const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState.beforeAttributeName; +const ATTRIBUTE_VALUE_UNQUOTED = 'attributeValueUnquoted' as TokenizerState.attributeValueUnquoted; + +export interface PendingError { + mustache(span: SourceSpan): never; + eof(offset: SourceOffset): never; +} export abstract class HandlebarsNodeVisitors extends Parser { + // Because we interleave the HTML and HBS parsing, sometimes the HTML + // tokenizer can run out of tokens when we switch into {{...}} or reached + // EOF. There are positions where neither of these are expected, and it would + // like to generate an error, but there is no span to attach the error to. + // This allows the HTML tokenization to stash an error message and the next + // mustache visitor will attach the message to the appropriate span and throw + // the error. + protected pendingError: Nullable = null; + abstract override appendToCommentData(s: string): void; abstract override beginAttributeValue(quoted: boolean): void; abstract override finishAttributeValue(): void; - private get isTopLevel() { - return this.elementStack.length === 0; - } - parse(program: HBS.Program, locals: string[]): ASTv1.Template { let node = b.template({ body: [], @@ -32,7 +42,15 @@ export abstract class HandlebarsNodeVisitors extends Parser { loc: this.source.spanFor(program.loc), }); - return this.parseProgram(node, program); + let template = this.parseProgram(node, program); + + // TODO: we really need to verify that the tokenizer is in an acceptable + // state when we are "done" parsing. For example, right now, `>, + element: ParserNodeBuilder, mustache: ASTv1.MustacheStatement ) { const { path, params, hash, loc } = mustache; diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 660c8dc7a0..2a3d75c61a 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -1,9 +1,17 @@ import type { Nullable } from '@glimmer/interfaces'; -import { assertPresentArray, assign, getFirst, getLast, isPresentArray } from '@glimmer/util'; +import type { TokenizerState } from 'simple-html-tokenizer'; +import { + assert, + assertPresentArray, + assign, + getFirst, + getLast, + isPresentArray, +} from '@glimmer/util'; import { parse, parseWithoutProcessing } from '@handlebars/parser'; import { EntityParser } from 'simple-html-tokenizer'; -import type { Tag } from '../parser'; +import type { EndTag, StartTag } from '../parser'; import type { NodeVisitor } from '../traversal/visitor'; import type * as ASTv1 from '../v1/api'; import type * as HBS from '../v1/handlebars-ast'; @@ -14,11 +22,16 @@ import * as src from '../source/api'; import { generateSyntaxError } from '../syntax-error'; import traverse from '../traversal/traverse'; import Walker from '../traversal/walker'; -import { appendChild, parseElementBlockParams } from '../utils'; +import { appendChild } from '../utils'; import b from '../v1/parser-builders'; import publicBuilder from '../v1/public-builders'; import { HandlebarsNodeVisitors } from './handlebars-node-visitors'; +// vendored from simple-html-tokenizer because it's unexported +function isSpace(char: string): boolean { + return /[\t\n\f ]/u.test(char); +} + export class TokenizerEventHandlers extends HandlebarsNodeVisitors { private tagOpenLine = 0; private tagOpenColumn = 0; @@ -77,6 +90,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { attributes: [], modifiers: [], comments: [], + params: [], selfClosing: false, start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; @@ -86,16 +100,12 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentNode = { type: 'EndTag', name: '', - attributes: [], - modifiers: [], - comments: [], - selfClosing: false, start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; } finishTag(): void { - let tag = this.finish(this.currentTag); + let tag = this.finish(this.currentTag); if (tag.type === 'StartTag') { this.finishStartTag(); @@ -119,7 +129,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishStartTag(): void { - let { name, attributes, modifiers, comments, selfClosing, loc } = this.finish( + let { name, attributes, modifiers, comments, params, selfClosing, loc } = this.finish( this.currentStartTag ); @@ -129,15 +139,15 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { attributes, modifiers, comments, + params, children: [], - blockParams: [], loc, }); this.elementStack.push(element); } finishEndTag(isVoid: boolean): void { - let tag = this.finish(this.currentTag); + let tag = this.finish(this.currentTag); let element = this.elementStack.pop() as ASTv1.ElementNode; @@ -145,12 +155,20 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { let parent = this.currentElement(); element.loc = element.loc.withEnd(this.offset()); - parseElementBlockParams(element); appendChild(parent, b.element(element)); } markTagAsSelfClosing(): void { - this.currentTag.selfClosing = true; + let tag = this.currentTag; + + if (tag.type === 'StartTag') { + tag.selfClosing = true; + } else { + throw generateSyntaxError( + `Invalid end tag: closing tag must not be self-closing`, + this.source.spanFor({ start: tag.start.toJSON(), end: this.offset().toJSON() }) + ); + } } // Tags - name @@ -177,6 +195,13 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { appendToAttributeName(char: string): void { this.currentAttr.name += char; + + // The block params parsing code can actually handle peek=non-space just + // fine, but this check was added as an optimization, as there is a little + // bit of setup overhead for the parsing logic just to immediately bail + if (this.currentAttr.name === 'as') { + this.parsePossibleBlockParams(); + } } beginAttributeValue(isQuoted: boolean): void { @@ -225,6 +250,15 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } let { name, parts, start, isQuoted, isDynamic, valueSpan } = this.currentAttr; + + // Just trying to be helpful with `` rather than letting it through as an attribute + if (name.startsWith('|') && parts.length === 0 && !isQuoted && !isDynamic) { + throw generateSyntaxError( + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + start.until(start.move(name.length)) + ); + } + let value = this.assembleAttributeValue(parts, isQuoted, isDynamic, start.until(tokenizerPos)); value.loc = valueSpan.withEnd(tokenizerPos); @@ -233,6 +267,248 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentStartTag.attributes.push(attribute); } + private parsePossibleBlockParams() { + // const enums that we can't use directly + const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState.beforeAttributeName; + const ATTRIBUTE_NAME = 'attributeName' as TokenizerState.attributeName; + const AFTER_ATTRIBUTE_NAME = 'afterAttributeName' as TokenizerState.afterAttributeName; + + // Regex to validate the identifier for block parameters. + // Based on the ID validation regex in Handlebars. + const ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u; + + type States = { + PossibleAs: { state: 'PossibleAs' }; + BeforeStartPipe: { state: 'BeforeStartPipe' }; + BeforeBlockParamName: { state: 'BeforeBlockParamName' }; + BlockParamName: { + state: 'BlockParamName'; + name: string; + start: src.SourceOffset; + }; + AfterEndPipe: { state: 'AfterEndPipe' }; + Error: { + state: 'Error'; + message: string; + start: src.SourceOffset; + }; + Done: { state: 'Done' }; + }; + + type State = States[keyof States]; + + type Handler = (next: string) => void; + + assert(this.tokenizer.state === ATTRIBUTE_NAME, 'must be in TokenizerState.attributeName'); + + const element = this.currentStartTag; + const as = this.currentAttr; + + let state = { state: 'PossibleAs' } as State; + + const handlers = { + PossibleAs: (next: string) => { + assert(state.state === 'PossibleAs', 'bug in block params parser'); + + if (isSpace(next)) { + // " as ..." + state = { state: 'BeforeStartPipe' }; + this.tokenizer.transitionTo(AFTER_ATTRIBUTE_NAME); + this.tokenizer.consume(); + } else if (next === '|') { + // " as|..." + // Following Handlebars and require a space between "as" and the pipe + throw generateSyntaxError( + `Invalid block parameters syntax: expecting at least one space character between "as" and "|"`, + as.start.until(this.offset().move(1)) + ); + } else { + // " as{{...", " async...", " as=...", " as>...", " as/>..." + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } + }, + + BeforeStartPipe: (next: string) => { + assert(state.state === 'BeforeStartPipe', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '|') { + state = { state: 'BeforeBlockParamName' }; + this.tokenizer.transitionTo(BEFORE_ATTRIBUTE_NAME); + this.tokenizer.consume(); + } else { + // " as {{...", " as bs...", " as =...", " as ...", " as/>..." + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } + }, + + BeforeBlockParamName: (next: string) => { + assert(state.state === 'BeforeBlockParamName', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: mustaches cannot be used inside parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '|') { + if (element.params.length === 0) { + // Following Handlebars and treat empty block params a syntax error + throw generateSyntaxError( + `Invalid block parameters syntax: empty parameters list, expecting at least one identifier`, + as.start.until(this.offset().move(1)) + ); + } else { + state = { state: 'AfterEndPipe' }; + } + } else if (next === '>' || next === '/') { + throw generateSyntaxError( + `Invalid block parameters syntax: incomplete parameters list, expecting "|" but the tag was closed prematurely`, + as.start.until(this.offset().move(1)) + ); + } else { + // slurp up anything else into the name, validate later + state = { + state: 'BlockParamName', + name: next, + start: this.offset(), + }; + this.tokenizer.consume(); + } + }, + + BlockParamName: (next: string) => { + assert(state.state === 'BlockParamName', 'bug in block params parser'); + + if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or , HBS side will attach the error + // to the next span + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: mustaches cannot be used inside parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '|' || isSpace(next)) { + let loc = state.start.until(this.offset()); + + if (state.name === 'this' || ID_INVERSE_PATTERN.test(state.name)) { + throw generateSyntaxError( + `Invalid block parameters syntax: invalid identifier name \`${state.name}\``, + loc + ); + } + + element.params.push(b.var({ name: state.name, loc })); + + state = next === '|' ? { state: 'AfterEndPipe' } : { state: 'BeforeBlockParamName' }; + this.tokenizer.consume(); + } else if (next === '>' || next === '/') { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting "|" but the tag was closed prematurely`, + as.start.until(this.offset().move(1)) + ); + } else { + // slurp up anything else into the name, validate later + state.name += next; + this.tokenizer.consume(); + } + }, + + AfterEndPipe: (next: string) => { + assert(state.state === 'AfterEndPipe', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or , HBS side will attach the error + // to the next span + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: modifiers cannot follow parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '>' || next === '/') { + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } else { + // Slurp up the next "token" for the error span + state = { + state: 'Error', + message: + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + start: this.offset(), + }; + this.tokenizer.consume(); + } + }, + + Error: (next: string) => { + assert(state.state === 'Error', 'bug in block params parser'); + + if (next === '' || next === '/' || next === '>' || isSpace(next)) { + throw generateSyntaxError(state.message, state.start.until(this.offset())); + } else { + // Slurp up the next "token" for the error span + this.tokenizer.consume(); + } + }, + + Done: () => { + assert(false, 'This should never be called'); + }, + } as const satisfies { + [S in keyof States]: Handler; + }; + + let next: string; + + do { + next = this.tokenizer.peek(); + handlers[state.state](next); + } while (state.state !== 'Done' && next !== ''); + + assert(state.state === 'Done', 'bug in block params parser'); + } + reportSyntaxError(message: string): void { throw generateSyntaxError(message, this.offset().collapsed()); } @@ -260,11 +536,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { }); } - validateEndTag( - tag: Tag<'StartTag' | 'EndTag'>, - element: ASTv1.ElementNode, - selfClosing: boolean - ): void { + validateEndTag(tag: StartTag | EndTag, element: ASTv1.ElementNode, selfClosing: boolean): void { if (voidMap.has(tag.name) && !selfClosing) { // EngTag is also called by StartTag for void and self-closing tags (i.e. // or
, so we need to check for that here. Otherwise, we would diff --git a/packages/@glimmer/syntax/lib/utils.ts b/packages/@glimmer/syntax/lib/utils.ts index c0c2d1cb1d..ec6f6ed06b 100644 --- a/packages/@glimmer/syntax/lib/utils.ts +++ b/packages/@glimmer/syntax/lib/utils.ts @@ -1,84 +1,6 @@ -import type { Nullable } from '@glimmer/interfaces'; -import { expect, unwrap } from '@glimmer/util'; - import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; -import { generateSyntaxError } from './syntax-error'; - -// Regex to validate the identifier for block parameters. -// Based on the ID validation regex in Handlebars. - -let ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u; - -// Checks the element's attributes to see if it uses block params. -// If it does, registers the block params with the program and -// removes the corresponding attributes from the element. - -export function parseElementBlockParams(element: ASTv1.ElementNode): void { - let params = parseBlockParams(element); - if (params) element.blockParams = params; -} - -function parseBlockParams(element: ASTv1.ElementNode): Nullable { - let l = element.attributes.length; - let attrNames = []; - - for (let i = 0; i < l; i++) { - attrNames.push(unwrap(element.attributes[i]).name); - } - - let asIndex = attrNames.indexOf('as'); - - if ( - asIndex === -1 && - attrNames.length > 0 && - unwrap(attrNames[attrNames.length - 1]).charAt(0) === '|' - ) { - throw generateSyntaxError( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - element.loc - ); - } - - if (asIndex !== -1 && l > asIndex && unwrap(attrNames[asIndex + 1]).charAt(0) === '|') { - // Some basic validation, since we're doing the parsing ourselves - let paramsString = attrNames.slice(asIndex).join(' '); - if ( - paramsString.charAt(paramsString.length - 1) !== '|' || - expect(paramsString.match(/\|/gu), `block params must exist here`).length !== 2 - ) { - throw generateSyntaxError( - "Invalid block parameters syntax, '" + paramsString + "'", - element.loc - ); - } - - let params = []; - for (let i = asIndex + 1; i < l; i++) { - let param = unwrap(attrNames[i]).replace(/\|/gu, ''); - if (param !== '') { - if (ID_INVERSE_PATTERN.test(param)) { - throw generateSyntaxError( - "Invalid identifier for block parameters, '" + param + "'", - element.loc - ); - } - params.push(param); - } - } - - if (params.length === 0) { - throw generateSyntaxError('Cannot use zero block parameters', element.loc); - } - - element.attributes = element.attributes.slice(0, asIndex); - return params; - } - - return null; -} - export function childrenFor( node: ASTv1.Block | ASTv1.Template | ASTv1.ElementNode ): ASTv1.TopLevelStatement[] { diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 2d6650679a..a0e9c790c3 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -107,10 +107,15 @@ export interface ElementNode extends BaseNode { tag: string; selfClosing: boolean; attributes: AttrNode[]; - blockParams: string[]; + params: VarHead[]; modifiers: ElementModifierStatement[]; comments: MustacheCommentStatement[]; children: Statement[]; + + /** + * string accessor for params.name + */ + blockParams: string[]; } export type StatementName = diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 73e9b4f961..a702a6d4ca 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -174,8 +174,8 @@ class Builders { tag, selfClosing, attributes, - blockParams, modifiers, + params, comments, children, loc, @@ -184,9 +184,9 @@ class Builders { selfClosing: boolean; attributes: ASTv1.AttrNode[]; modifiers: ASTv1.ElementModifierStatement[]; + params: ASTv1.VarHead[]; children: ASTv1.Statement[]; comments: ASTv1.MustacheCommentStatement[]; - blockParams: string[]; loc: SourceSpan; }): ASTv1.ElementNode { return { @@ -194,11 +194,19 @@ class Builders { tag, selfClosing: selfClosing, attributes, - blockParams, modifiers, + params, comments, children, loc, + get blockParams() { + return this.params.map((p) => p.name); + }, + set blockParams(params: string[]) { + this.params = params.map((name) => { + return b.var({ name, loc: SourceSpan.synthetic(name) }); + }); + }, }; } diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index ec201ab92b..796a849e52 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -176,16 +176,15 @@ export interface BuildElementOptions { modifiers?: ASTv1.ElementModifierStatement[]; children?: ASTv1.Statement[]; comments?: ASTv1.MustacheCommentStatement[]; - blockParams?: string[]; - loc?: SourceSpan; + blockParams?: ASTv1.VarHead[] | string[]; + loc?: SourceLocation; } function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): ASTv1.ElementNode { let { attrs, blockParams, modifiers, comments, children, loc } = options; - let tagName: string; - // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST + let tagName: string; let selfClosing = false; if (typeof tag === 'object') { selfClosing = tag.selfClosing; @@ -197,11 +196,19 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS tagName = tag; } + let params = blockParams?.map((param) => { + if (typeof param === 'string') { + return buildVar(param); + } else { + return param; + } + }); + return b.element({ tag: tagName, selfClosing, attributes: attrs || [], - blockParams: blockParams || [], + params: params || [], modifiers: modifiers || [], comments: comments || [], children: children || [], @@ -240,25 +247,25 @@ function buildSexpr( }); } -function buildHead(original: string, loc: SourceLocation): ASTv1.PathExpression { +function buildHead(original: string, loc?: SourceLocation): ASTv1.PathExpression { let [head, ...tail] = asPresentArray(original.split('.')); let headNode = b.head({ original: head, loc: buildLoc(loc || null) }); return b.path({ head: headNode, tail, loc: buildLoc(loc || null) }); } -function buildThis(loc: SourceLocation): ASTv1.PathHead { +function buildThis(loc: SourceLocation): ASTv1.ThisHead { return b.this({ loc: buildLoc(loc || null) }); } -function buildAtName(name: string, loc: SourceLocation): ASTv1.PathHead { +function buildAtName(name: string, loc?: SourceLocation): ASTv1.AtHead { return b.atName({ name, loc: buildLoc(loc || null) }); } -function buildVar(name: string, loc: SourceLocation): ASTv1.PathHead { +function buildVar(name: string, loc?: SourceLocation): ASTv1.VarHead { return b.var({ name, loc: buildLoc(loc || null) }); } -function buildHeadFromString(original: string, loc: SourceLocation): ASTv1.PathHead { +function buildHeadFromString(original: string, loc?: SourceLocation): ASTv1.PathHead { return b.head({ original, loc: buildLoc(loc || null) }); } diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index d3eec8896c..20b38e7c9f 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -315,6 +315,131 @@ test('block with block params edge case: block-params like content', () => { } }); +test('element with block params', () => { + let ast = parse(` + + {{bar}} {{bat}} {{baz}} + + `); + + let element = ast.body[1]; + if (assertNodeType(element, 'ElementNode')) { + if (assertNodeType(element.params[0], 'VarHead')) { + locEqual(element.params[0], 2, 13, 2, 16, 'bar'); + } + + if (assertNodeType(element.params[1], 'VarHead')) { + locEqual(element.params[1], 2, 17, 2, 20, 'bat'); + } + + if (assertNodeType(element.params[2], 'VarHead')) { + locEqual(element.params[2], 2, 21, 2, 24, 'baz'); + } + } +}); + +test('element with block params edge case: multiline', () => { + let ast = parse(` + + {{bar}} {{bat}} {{baz}} + + `); + + let element = ast.body[1]; + if (assertNodeType(element, 'ElementNode')) { + if (assertNodeType(element.params[0], 'VarHead')) { + locEqual(element.params[0], 3, 1, 3, 4, 'bar'); + } + + if (assertNodeType(element.params[1], 'VarHead')) { + locEqual(element.params[1], 3, 5, 3, 8, 'bat'); + } + + if (assertNodeType(element.params[2], 'VarHead')) { + locEqual(element.params[2], 4, 6, 4, 7, 'b'); + } + + if (assertNodeType(element.params[3], 'VarHead')) { + locEqual(element.params[3], 5, 0, 5, 1, 'a'); + } + + if (assertNodeType(element.params[4], 'VarHead')) { + locEqual(element.params[4], 6, 6, 6, 7, 'z'); + } + } +}); + +test('elment with block params edge case: block-params like attribute names', () => { + let ast = parse(` + + {{bar}} {{bat}} {{baz}} + + `); + + let element = ast.body[1]; + if (assertNodeType(element, 'ElementNode')) { + if (assertNodeType(element.params[0], 'VarHead')) { + locEqual(element.params[0], 2, 30, 2, 33, 'bar'); + } + + if (assertNodeType(element.params[1], 'VarHead')) { + locEqual(element.params[1], 2, 34, 2, 37, 'bat'); + } + + if (assertNodeType(element.params[2], 'VarHead')) { + locEqual(element.params[2], 2, 38, 2, 41, 'baz'); + } + } +}); + +test('elment with block params edge case: block-params like attribute values', () => { + let ast = parse(` + + {{bar}} {{bat}} {{baz}} + + `); + + let element = ast.body[1]; + if (assertNodeType(element, 'ElementNode')) { + if (assertNodeType(element.params[0], 'VarHead')) { + locEqual(element.params[0], 2, 36, 2, 39, 'bar'); + } + + if (assertNodeType(element.params[1], 'VarHead')) { + locEqual(element.params[1], 2, 40, 2, 43, 'bat'); + } + + if (assertNodeType(element.params[2], 'VarHead')) { + locEqual(element.params[2], 2, 44, 2, 47, 'baz'); + } + } +}); + +test('element with block params edge case: block-params like content', () => { + let ast = parse(` + as |bar bat baz| + `); + + let element = ast.body[1]; + if (assertNodeType(element, 'ElementNode')) { + if (assertNodeType(element.params[0], 'VarHead')) { + locEqual(element.params[0], 2, 13, 2, 16, 'bar'); + } + + if (assertNodeType(element.params[1], 'VarHead')) { + locEqual(element.params[1], 2, 17, 2, 20, 'bat'); + } + + if (assertNodeType(element.params[2], 'VarHead')) { + locEqual(element.params[2], 2, 21, 2, 24, 'baz'); + } + } +}); + test('blocks with nested html elements', () => { let ast = parse(` {{#foo-bar}}
Foo
{{/foo-bar}}

Hi!

diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index 1b1644a337..59201ec21b 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -328,6 +328,87 @@ test('block with block params edge case: block-params like content', () => { ); }); +test('element with block params', () => { + let t = `{{bar}} {{bat}} {{baz}}`; + + astEqual( + t, + b.template([ + element( + 'Foo', + ['as', b.var('bar'), b.var('bat'), b.var('baz')], + ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')] + ), + ]) + ); +}); + +test('element with block params edge case: multiline', () => { + let t = `{{bar}} {{bat}} {{baz}}`; + + astEqual( + t, + b.template([ + element( + 'Foo', + ['as', b.var('bar'), b.var('bat'), b.var('b'), b.var('a'), b.var('z')], + ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')] + ), + ]) + ); +}); + +test('element with block params edge case: block-params like attribute names', () => { + let t = `as |a b c|`; + + astEqual( + t, + b.template([ + element( + 'Foo', + ['attrs', ['as', 'a'], ['async', 'b']], + ['as', b.var('bar'), b.var('bat'), b.var('baz')], + ['body', b.text('as |a b c|')] + ), + ]) + ); +}); + +test('element with block params edge case: block-params like attribute values', () => { + let t = `{{bar}} {{bat}} {{baz}}`; + + astEqual( + t, + b.template([ + element( + 'Foo', + ['attrs', ['foo', 'as |a b c|']], + ['as', b.var('bar'), b.var('bat'), b.var('baz')], + ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')] + ), + ]) + ); +}); + +test('element with block params edge case: block-params like content', () => { + let t = `as |a b c|`; + + astEqual( + t, + b.template([ + element( + 'Foo', + ['as', b.var('bar'), b.var('bat'), b.var('baz')], + ['body', b.text('as |a b c|')] + ), + ]) + ); +}); + test('Element modifiers', () => { let t = "

Some content

"; astEqual( @@ -821,7 +902,7 @@ test('named blocks', () => { element( ':body', ['body', element('div', ['body', b.mustache('contents')])], - ['as', 'contents'] + ['as', b.var('contents')] ), ]); astEqual(ast, b.template([el])); @@ -932,8 +1013,8 @@ export type ElementParts = | ['attrs', ...AttrSexp[]] | ['modifiers', ...ModifierSexp[]] | ['body', ...ASTv1.Statement[]] - | ['comments', ...ElementComment[]] - | ['as', ...string[]] + | ['comments', ...ASTv1.MustacheCommentStatement[]] + | ['as', ...ASTv1.VarHead[]] | ['loc', ASTv1.SourceLocation]; export type PathSexp = string | ['path', string, LocSexp?]; @@ -948,8 +1029,6 @@ export type AttrSexp = [string, ASTv1.AttrNode['value'] | string, LocSexp?]; export type LocSexp = ['loc', ASTv1.SourceLocation]; -export type ElementComment = ASTv1.MustacheCommentStatement | ASTv1.SourceLocation | string; - export type SexpValue = | string | ASTv1.Expression[] @@ -958,16 +1037,9 @@ export type SexpValue = | PathSexp | undefined; -export interface BuildElementOptions { - attrs?: ASTv1.AttrNode[]; - modifiers?: ASTv1.ElementModifierStatement[]; - children?: ASTv1.Statement[]; - comments?: ElementComment[]; - blockParams?: string[]; - loc?: ASTv1.SourceLocation; -} - -export type TagDescriptor = string | { name: string; selfClosing: boolean }; +export type BuildElementParams = Parameters; +export type TagDescriptor = BuildElementParams[0]; +export type BuildElementOptions = NonNullable; export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.ElementNode { let normalized: BuildElementOptions; @@ -977,31 +1049,7 @@ export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.E normalized = options || {}; } - let { attrs, blockParams, modifiers, comments, children, loc } = normalized; - - // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST - let selfClosing = false; - if (typeof tag === 'object') { - selfClosing = tag.selfClosing; - tag = tag.name; - } else { - if (tag.slice(-1) === '/') { - tag = tag.slice(0, -1); - selfClosing = true; - } - } - - return { - type: 'ElementNode', - tag: tag || '', - selfClosing: selfClosing, - attributes: attrs || [], - blockParams: blockParams || [], - modifiers: modifiers || [], - comments: (comments as ASTv1.MustacheCommentStatement[]) || [], - children: children || [], - loc: b.loc(loc || null), - }; + return b.element(tag, normalized); } export function normalizeElementParts(...args: ElementParts[]): BuildElementOptions { From 68509ace73a77d1b36eab9c958006c9f1c98ec06 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Tue, 27 Feb 2024 01:58:12 -0800 Subject: [PATCH 15/17] Second attempt at landing #1553 Provide source spans for the open and close tags without adding new AST nodes, also parse the element's tag name into a path which has the required source span. --- packages/@glimmer/syntax/lib/parser.ts | 2 + .../lib/parser/tokenizer-event-handlers.ts | 50 ++++++++- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 17 ++- .../@glimmer/syntax/lib/v1/parser-builders.ts | 33 +++++- .../@glimmer/syntax/lib/v1/public-builders.ts | 75 ++++++++++--- .../@glimmer/syntax/test/loc-node-test.ts | 102 ++++++++++++++++-- .../@glimmer/syntax/test/parser-node-test.ts | 16 +++ packages/@glimmer/syntax/test/support.ts | 8 +- 8 files changed, 265 insertions(+), 38 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index cbf1c07277..449b25e4c9 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -17,6 +17,8 @@ export type ParserNodeBuilder = Omit; + nameEnd: Nullable; readonly attributes: ASTv1.AttrNode[]; readonly modifiers: ASTv1.ElementModifierStatement[]; readonly comments: ASTv1.MustacheCommentStatement[]; diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 2a3d75c61a..652036bdb1 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -1,6 +1,7 @@ import type { Nullable } from '@glimmer/interfaces'; import type { TokenizerState } from 'simple-html-tokenizer'; import { + asPresentArray, assert, assertPresentArray, assign, @@ -87,6 +88,8 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentNode = { type: 'StartTag', name: '', + nameStart: null, + nameEnd: null, attributes: [], modifiers: [], comments: [], @@ -129,24 +132,42 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishStartTag(): void { - let { name, attributes, modifiers, comments, params, selfClosing, loc } = this.finish( + let { name, nameStart, nameEnd } = this.currentStartTag; + + // <> should probably be a syntax error, but s-h-t is currently broken for that case + assert(name !== '', 'tag name cannot be empty'); + assert(nameStart !== null, 'nameStart unexpectedly null'); + assert(nameEnd !== null, 'nameEnd unexpectedly null'); + + let nameLoc = nameStart.until(nameEnd); + let [head, ...tail] = asPresentArray(name.split('.')); + let path = b.path({ + head: b.head({ original: head, loc: nameLoc.sliceStartChars({ chars: head.length }) }), + tail, + loc: nameLoc, + }); + + let { attributes, modifiers, comments, params, selfClosing, loc } = this.finish( this.currentStartTag ); let element = b.element({ - tag: name, + path, selfClosing, attributes, modifiers, comments, params, children: [], + openTag: loc, + closeTag: selfClosing ? null : src.SourceSpan.broken(), loc, }); this.elementStack.push(element); } finishEndTag(isVoid: boolean): void { + let { start: closeTagStart } = this.currentTag; let tag = this.finish(this.currentTag); let element = this.elementStack.pop() as ASTv1.ElementNode; @@ -154,7 +175,16 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.validateEndTag(tag, element, isVoid); let parent = this.currentElement(); + if (isVoid) { + element.closeTag = null; + } else if (element.selfClosing) { + assert(element.closeTag === null, 'element.closeTag unexpectedly present'); + } else { + element.closeTag = closeTagStart.until(this.offset()); + } + element.loc = element.loc.withEnd(this.offset()); + appendChild(parent, b.element(element)); } @@ -174,7 +204,21 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { // Tags - name appendToTagName(char: string): void { - this.currentTag.name += char; + let tag = this.currentTag; + tag.name += char; + + if (tag.type === 'StartTag') { + let offset = this.offset(); + + if (tag.nameStart === null) { + assert(tag.nameEnd === null, 'nameStart and nameEnd must both be null'); + + // Note that the tokenizer already consumed the token here + tag.nameStart = offset.move(-1); + } + + tag.nameEnd = offset; + } } // Tags - attributes diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index a0e9c790c3..06a54d8150 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -104,7 +104,7 @@ export interface MustacheCommentStatement extends BaseNode { export interface ElementNode extends BaseNode { type: 'ElementNode'; - tag: string; + path: PathExpression; selfClosing: boolean; attributes: AttrNode[]; params: VarHead[]; @@ -112,6 +112,21 @@ export interface ElementNode extends BaseNode { comments: MustacheCommentStatement[]; children: Statement[]; + /** + * span for the open tag + */ + openTag: src.SourceSpan; + + /** + * span for the close tag, null for void or self-closing tags + */ + closeTag: Nullable; + + /** + * string accessor for path.original + */ + tag: string; + /** * string accessor for params.name */ diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index a702a6d4ca..3d7046809d 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -171,34 +171,47 @@ class Builders { } element({ - tag, + path, selfClosing, attributes, modifiers, params, comments, children, + openTag, + closeTag, loc, }: { - tag: string; + path: ASTv1.PathExpression; selfClosing: boolean; attributes: ASTv1.AttrNode[]; modifiers: ASTv1.ElementModifierStatement[]; params: ASTv1.VarHead[]; children: ASTv1.Statement[]; comments: ASTv1.MustacheCommentStatement[]; + openTag: SourceSpan; + closeTag: Nullable; loc: SourceSpan; }): ASTv1.ElementNode { + let _selfClosing = selfClosing; + return { type: 'ElementNode', - tag, - selfClosing: selfClosing, + path, attributes, modifiers, params, comments, children, + openTag, + closeTag, loc, + get tag() { + return this.path.original; + }, + set tag(name: string) { + this.path.original = name; + }, get blockParams() { return this.params.map((p) => p.name); }, @@ -207,6 +220,18 @@ class Builders { return b.var({ name, loc: SourceSpan.synthetic(name) }); }); }, + get selfClosing() { + return _selfClosing; + }, + set selfClosing(selfClosing: boolean) { + _selfClosing = selfClosing; + + if (selfClosing) { + this.closeTag = null; + } else { + this.closeTag = SourceSpan.synthetic(``); + } + }, }; } diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 796a849e52..1b9a0224f9 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -4,6 +4,7 @@ import { asPresentArray, assert, deprecate, isPresentArray } from '@glimmer/util import type { SourceLocation, SourcePosition } from '../source/location'; import type * as ASTv1 from './api'; +import { isVoidTag } from '../generation/printer'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; @@ -24,7 +25,11 @@ function SOURCE(): Source { // Statements export type BuilderHead = string | ASTv1.CallableExpression; -export type TagDescriptor = string | { name: string; selfClosing: boolean }; +export type TagDescriptor = + | string + | ASTv1.PathExpression + | { path: ASTv1.PathExpression; selfClosing?: boolean } + | { name: string; selfClosing?: boolean }; function buildMustache( path: BuilderHead | ASTv1.Literal, @@ -71,7 +76,7 @@ function buildBlock( defaultBlock = _defaultBlock; } - if (_elseBlock !== undefined && _elseBlock !== null && _elseBlock.type === 'Template') { + if (_elseBlock?.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); assert(_elseBlock.locals.length === 0, '{{else}} block cannot have block params'); @@ -177,23 +182,51 @@ export interface BuildElementOptions { children?: ASTv1.Statement[]; comments?: ASTv1.MustacheCommentStatement[]; blockParams?: ASTv1.VarHead[] | string[]; + openTag?: SourceLocation; + closeTag?: Nullable; loc?: SourceLocation; } function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): ASTv1.ElementNode { - let { attrs, blockParams, modifiers, comments, children, loc } = options; + let { + attrs, + blockParams, + modifiers, + comments, + children, + openTag, + closeTag: _closeTag, + loc, + } = options; // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST - let tagName: string; - let selfClosing = false; - if (typeof tag === 'object') { + let path: ASTv1.PathExpression; + let selfClosing: boolean | undefined; + + if (typeof tag === 'string') { + if (tag.endsWith('/')) { + path = buildPath(tag.slice(0, -1)); + selfClosing = true; + } else { + path = buildPath(tag); + } + } else if ('type' in tag) { + assert(tag.type === 'PathExpression', `Invalid tag type ${tag.type}`); + path = tag; + } else if ('path' in tag) { + assert(tag.path.type === 'PathExpression', `Invalid tag type ${tag.path.type}`); + path = tag.path; selfClosing = tag.selfClosing; - tagName = tag.name; - } else if (tag.slice(-1) === '/') { - tagName = tag.slice(0, -1); - selfClosing = true; } else { - tagName = tag; + path = buildPath(tag.name); + selfClosing = tag.selfClosing; + } + + if (selfClosing) { + assert( + _closeTag === null || _closeTag === undefined, + 'Cannot build a self-closing tag with a closeTag source location' + ); } let params = blockParams?.map((param) => { @@ -204,14 +237,24 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS } }); + let closeTag: Nullable = null; + + if (_closeTag) { + closeTag = buildLoc(_closeTag || null); + } else if (_closeTag === undefined) { + closeTag = selfClosing || isVoidTag(path.original) ? null : buildLoc(null); + } + return b.element({ - tag: tagName, - selfClosing, + path, + selfClosing: selfClosing || false, attributes: attrs || [], params: params || [], modifiers: modifiers || [], comments: comments || [], children: children || [], + openTag: buildLoc(openTag || null), + closeTag, loc: buildLoc(loc || null), }); } @@ -253,7 +296,7 @@ function buildHead(original: string, loc?: SourceLocation): ASTv1.PathExpression return b.path({ head: headNode, tail, loc: buildLoc(loc || null) }); } -function buildThis(loc: SourceLocation): ASTv1.ThisHead { +function buildThis(loc?: SourceLocation): ASTv1.ThisHead { return b.this({ loc: buildLoc(loc || null) }); } @@ -271,8 +314,8 @@ function buildHeadFromString(original: string, loc?: SourceLocation): ASTv1.Path function buildCleanPath( head: ASTv1.PathHead, - tail: string[], - loc: SourceLocation + tail: string[] = [], + loc?: SourceLocation ): ASTv1.PathExpression { return b.path({ head, tail, loc: buildLoc(loc || null) }); } diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index 20b38e7c9f..b7ce04eca5 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -25,7 +25,7 @@ function assertNodeType(node: unknown, type: string): boolean { const { test } = QUnit; function locEqual( - node: AST.Node | AST.SubNode | null | undefined, + node: AST.Node | AST.SubNode | src.SourceSpan | null | undefined, startLine: number, startColumn: number, endLine: number, @@ -37,11 +37,15 @@ function locEqual( end: { line: endLine, column: endColumn }, }; - QUnit.assert.deepEqual( - node && { start: node.loc.startPosition, end: node.loc.endPosition }, - expected, - message - ); + let actual: src.SourceLocation | null | undefined; + + if (node && 'type' in node) { + actual = node.loc.toJSON(); + } else if (node && 'toJSON' in node) { + actual = node.toJSON(); + } + + QUnit.assert.deepEqual(actual, expected, message); } test('programs', () => { @@ -114,7 +118,7 @@ test('element modifier', () => { } }); -test('html elements', () => { +test('html elements', (assert) => { let ast = parse(`

@@ -125,19 +129,95 @@ test('html elements', () => { `); let [, section] = ast.body; - locEqual(section, 2, 4, 7, 14, 'section element'); + if (assertNodeType(section, 'ElementNode')) { + locEqual(section, 2, 4, 7, 14, 'section element'); + locEqual(section.path, 2, 5, 2, 12); + locEqual(section.path.head, 2, 5, 2, 12); + locEqual(section.openTag, 2, 4, 2, 13, '
'); + locEqual(section.closeTag, 7, 4, 7, 14, '
'); + let [, br, , div] = section.children; - locEqual(br, 3, 6, 3, 10, 'br element'); - locEqual(div, 4, 6, 6, 12, 'div element'); + + if (assertNodeType(br, 'ElementNode')) { + locEqual(br, 3, 6, 3, 10, 'br element'); + locEqual(br.path, 3, 7, 3, 9); + locEqual(br.path.head, 3, 7, 3, 9); + locEqual(br.openTag, 3, 6, 3, 10, '
'); + assert.strictEqual(br.closeTag, null); + } + if (assertNodeType(div, 'ElementNode')) { + locEqual(div, 4, 6, 6, 12, 'div element'); + + locEqual(div, 4, 6, 6, 12, 'div element'); + locEqual(div.path, 4, 7, 4, 10); + locEqual(div.path.head, 4, 7, 4, 10); + locEqual(div.openTag, 4, 6, 4, 11, '
'); + locEqual(div.closeTag, 6, 6, 6, 12, '
'); + let [, hr] = div.children; - locEqual(hr, 5, 8, 5, 14, 'hr element'); + if (assertNodeType(hr, 'ElementNode')) { + locEqual(hr, 5, 8, 5, 14, 'hr element'); + locEqual(hr.path, 5, 9, 5, 11); + locEqual(hr.path.head, 5, 9, 5, 11); + locEqual(hr.openTag, 5, 8, 5, 14, '
'); + assert.strictEqual(hr.closeTag, null); + } } } }); +test('various html element paths', () => { + let ast = parse(` + + + + + <@Foo /> + <@Foo.bar.baz /> + <:foo /> + `); + + let [, Foo, , FooDotBar, , This, , ThisDotFoo, , AtFoo, , AtFooDotBar, , NamedBlock] = ast.body; + + if (assertNodeType(Foo, 'ElementNode')) { + locEqual(Foo.path, 2, 5, 2, 8); + locEqual(Foo.path.head, 2, 5, 2, 8); + } + + if (assertNodeType(FooDotBar, 'ElementNode')) { + locEqual(FooDotBar.path, 3, 5, 3, 16); + locEqual(FooDotBar.path.head, 3, 5, 3, 8); + } + + if (assertNodeType(This, 'ElementNode')) { + locEqual(This.path, 4, 5, 4, 9); + locEqual(This.path.head, 4, 5, 4, 9); + } + + if (assertNodeType(ThisDotFoo, 'ElementNode')) { + locEqual(ThisDotFoo.path, 5, 5, 5, 17); + locEqual(ThisDotFoo.path.head, 5, 5, 5, 9); + } + + if (assertNodeType(AtFoo, 'ElementNode')) { + locEqual(AtFoo.path, 6, 5, 6, 9); + locEqual(AtFoo.path.head, 6, 5, 6, 9); + } + + if (assertNodeType(AtFooDotBar, 'ElementNode')) { + locEqual(AtFooDotBar.path, 7, 5, 7, 17); + locEqual(AtFooDotBar.path.head, 7, 5, 7, 9); + } + + if (assertNodeType(NamedBlock, 'ElementNode')) { + locEqual(NamedBlock.path, 8, 5, 8, 9); + locEqual(NamedBlock.path.head, 8, 5, 8, 9); + } +}); + test('html elements with nested blocks', (assert) => { let ast = parse(`
diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index 59201ec21b..dc8d3e150e 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -19,6 +19,22 @@ test('self-closed element', () => { astEqual(t, b.template([element('g/')])); }); +test('various html element paths', () => { + const cases = [ + [``, b.fullPath(b.var('Foo'))], + [``, b.fullPath(b.var('Foo'), ['bar', 'baz'])], + [``, b.fullPath(b.this())], + [``, b.fullPath(b.this(), ['foo', 'bar'])], + [`<@Foo />`, b.fullPath(b.at('@Foo'))], + [`<@Foo.bar.baz />`, b.fullPath(b.at('@Foo'), ['bar', 'baz'])], + [`<:foo />`, b.fullPath(b.var(':foo'))], + ] satisfies Array<[string, ASTv1.PathExpression]>; + + for (const [t, path] of cases) { + astEqual(t, b.template([b.element({ path, selfClosing: true })])); + } +}); + test('elements can have empty attributes', () => { let t = ''; astEqual(t, b.template([element('img', ['attrs', ['id', '']])])); diff --git a/packages/@glimmer/syntax/test/support.ts b/packages/@glimmer/syntax/test/support.ts index 234e39e740..8dbe6d0c86 100644 --- a/packages/@glimmer/syntax/test/support.ts +++ b/packages/@glimmer/syntax/test/support.ts @@ -6,15 +6,17 @@ function normalizeNode(obj: AST.Node | Array): AST.Node | Array(obj: T): T { if (obj && typeof obj === 'object') { if (Array.isArray(obj)) { return obj.map(normalizeValue) as T; } else { return fromEntries( - entries(obj).flatMap(([key, value]) => - key === 'loc' ? [] : [[key, normalizeValue(value)]] - ) + entries(obj).flatMap(([key, value]) => (isLoc(key) ? [] : [[key, normalizeValue(value)]])) ) as T; } } else { From 31cc61278fb51ab230695b43a41fe2127b5bb944 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Mon, 4 Mar 2024 20:16:28 -0800 Subject: [PATCH 16/17] Revert making `Template.locals` readonly It turns out to be important that AST plugins can mutate the list of local variables in the root `Template` node. Instead, use *that* list of local variables as the ultimate source of truth after AST plugin ran. --- .../syntax/lib/parser/tokenizer-event-handlers.ts | 4 ---- packages/@glimmer/syntax/lib/v1/legacy-interop.ts | 15 ++++++++------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 4 ++-- packages/@glimmer/syntax/lib/v2/normalize.ts | 2 +- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 652036bdb1..69bc492a16 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -762,10 +762,6 @@ export function preprocess( options.locals ?? [] ); - if (options.strictMode && options.locals?.length) { - template = b.template({ ...template, locals: options.locals }); - } - if (options?.plugins?.ast) { for (const transform of options.plugins.ast) { let env: ASTPluginEnvironment = assign({}, options, { syntax }, { plugins: undefined }); diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 0f86391373..79e977f5eb 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -11,23 +11,24 @@ export function buildLegacyTemplate({ body, locals, loc }: TemplateParams): ASTv const node = { type: 'Template', body, + locals, loc, }; - Object.defineProperty(node, 'locals', { - enumerable: true, - writable: false, - value: Object.freeze([...locals]), - }); - Object.defineProperty(node, 'blockParams', { enumerable: false, - get(): readonly string[] { + get(): string[] { deprecate( `Template nodes can never have block params, for in-scope variables, use locals instead` ); return this.locals; }, + set(value: string[]) { + deprecate( + `Template nodes can never have block params, for in-scope variables, use locals instead` + ); + this.locals = value; + }, }); return node as ASTv1.Template; diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 06a54d8150..96d2af5d06 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -29,12 +29,12 @@ export type EntityEncodingState = 'transformed' | 'raw'; export interface Template extends CommonProgram { type: 'Template'; - readonly locals: readonly string[]; + locals: string[]; /** * @deprecated use locals instead */ - readonly blockParams: readonly string[]; + blockParams: string[]; } /** diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index 6530c15525..b70ef34a2e 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -40,8 +40,8 @@ export function normalize( let normalizeOptions = { strictMode: false, - locals: [], ...options, + locals: ast.locals, }; let top = SymbolTable.top( From e63f159f023900eed2bd482897ffe7a33c4febd0 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Mon, 4 Mar 2024 20:24:42 -0800 Subject: [PATCH 17/17] Revert `Template.blockParams` -> `Template.locals` change for now The rename is motivated by: 1. It's confusing for the root node to have "block params" 2. All other AST nodes that has `blockParams` also has a `params` field that exposes the source location as `VarHead`s There is some uncertainty in: 1. Whether there are a lot of existing AST consumers that uses it (shouldn't be a big deal in itself, as it's just a deprecation) 2. Whether the direction of using `Template.locals` as the source of truth for outside local variables will ultimately prevail When we have more certainty on those and if this rename is still desired, it should be possible to revert this commit --- .../lib/parser/handlebars-node-visitors.ts | 4 +-- .../@glimmer/syntax/lib/v1/legacy-interop.ts | 29 ------------------- packages/@glimmer/syntax/lib/v1/nodes-v1.ts | 5 ---- .../@glimmer/syntax/lib/v1/parser-builders.ts | 18 +++++------- .../@glimmer/syntax/lib/v1/public-builders.ts | 8 ++--- packages/@glimmer/syntax/lib/v2/normalize.ts | 2 +- 6 files changed, 14 insertions(+), 52 deletions(-) diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index fefb739986..6f7a620bb6 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -35,10 +35,10 @@ export abstract class HandlebarsNodeVisitors extends Parser { abstract override beginAttributeValue(quoted: boolean): void; abstract override finishAttributeValue(): void; - parse(program: HBS.Program, locals: string[]): ASTv1.Template { + parse(program: HBS.Program, blockParams: string[]): ASTv1.Template { let node = b.template({ body: [], - locals, + blockParams, loc: this.source.spanFor(program.loc), }); diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 79e977f5eb..7687664615 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -5,35 +5,6 @@ import type * as ASTv1 from './nodes-v1'; import b from './public-builders'; -export type TemplateParams = Omit; - -export function buildLegacyTemplate({ body, locals, loc }: TemplateParams): ASTv1.Template { - const node = { - type: 'Template', - body, - locals, - loc, - }; - - Object.defineProperty(node, 'blockParams', { - enumerable: false, - get(): string[] { - deprecate( - `Template nodes can never have block params, for in-scope variables, use locals instead` - ); - return this.locals; - }, - set(value: string[]) { - deprecate( - `Template nodes can never have block params, for in-scope variables, use locals instead` - ); - this.locals = value; - }, - }); - - return node as ASTv1.Template; -} - export type MustacheStatementParams = Omit; export function buildLegacyMustache({ diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index 96d2af5d06..9c34badb89 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -29,11 +29,6 @@ export type EntityEncodingState = 'transformed' | 'raw'; export interface Template extends CommonProgram { type: 'Template'; - locals: string[]; - - /** - * @deprecated use locals instead - */ blockParams: string[]; } diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 3d7046809d..aeb8dd899b 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -4,12 +4,7 @@ import { assert } from '@glimmer/util'; import type * as ASTv1 from './api'; import { SourceSpan } from '../source/span'; -import { - buildLegacyLiteral, - buildLegacyMustache, - buildLegacyPath, - buildLegacyTemplate, -} from './legacy-interop'; +import { buildLegacyLiteral, buildLegacyMustache, buildLegacyPath } from './legacy-interop'; const DEFAULT_STRIP = { close: false, @@ -60,18 +55,19 @@ class Builders { template({ body, - locals, + blockParams, loc, }: { body: ASTv1.Statement[]; - locals: string[]; + blockParams: string[]; loc: SourceSpan; }): ASTv1.Template { - return buildLegacyTemplate({ + return { + type: 'Template', body, - locals, + blockParams, loc, - }); + }; } mustache({ diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 1b9a0224f9..556960647c 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -68,7 +68,7 @@ function buildBlock( if (_defaultBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); defaultBlock = b.blockItself({ - params: buildBlockParams(_defaultBlock.locals), + params: buildBlockParams(_defaultBlock.blockParams), body: _defaultBlock.body, loc: _defaultBlock.loc, }); @@ -78,7 +78,7 @@ function buildBlock( if (_elseBlock?.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); - assert(_elseBlock.locals.length === 0, '{{else}} block cannot have block params'); + assert(_elseBlock.blockParams.length === 0, '{{else}} block cannot have block params'); elseBlock = b.blockItself({ params: [], @@ -422,12 +422,12 @@ function buildBlockItself( function buildTemplate( body: ASTv1.Statement[] = [], - locals: string[] = [], + blockParams: string[] = [], loc?: SourceLocation ): ASTv1.Template { return b.template({ body, - locals, + blockParams, loc: buildLoc(loc || null), }); } diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index b70ef34a2e..df1fea03cc 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -41,7 +41,7 @@ export function normalize( let normalizeOptions = { strictMode: false, ...options, - locals: ast.locals, + locals: ast.blockParams, }; let top = SymbolTable.top(