diff --git a/package.json b/package.json index 19d051c..c3e0877 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,10 @@ "test": "vitest run" }, "dependencies": { - "@babel/eslint-parser": "^7.28.6", "@glimmer/syntax": ">= 0.92.0", "@typescript-eslint/tsconfig-utils": "^8.38.0", "content-tag": "^4.1.0", + "ember-estree": "^0.4.1", "eslint-scope": "^9.1.1", "html-tags": "^5.1.0", "mathml-tag-names": "^4.0.0", @@ -64,7 +64,6 @@ "vitest": "^1.2.2" }, "peerDependencies": { - "@babel/core": "^7.23.6", "@typescript-eslint/parser": "*" }, "peerDependenciesMeta": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2819648..a98b346 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: .: dependencies: - '@babel/eslint-parser': - specifier: ^7.28.6 - version: 7.28.6(@babel/core@7.26.0)(eslint@8.57.1) '@glimmer/syntax': specifier: '>= 0.92.0' version: 0.92.3 @@ -23,6 +20,9 @@ importers: content-tag: specifier: ^4.1.0 version: 4.1.0 + ember-estree: + specifier: ^0.4.1 + version: 0.4.1 eslint-scope: specifier: ^9.1.1 version: 9.1.1 @@ -193,6 +193,12 @@ importers: test-projects/gjs-experimental-worker: devDependencies: + '@babel/core': + specifier: ^7.23.6 + version: 7.26.0 + '@babel/eslint-parser': + specifier: ^7.28.6 + version: 7.28.6(@babel/core@7.26.0)(eslint@8.57.1) '@babel/plugin-transform-runtime': specifier: ^7.28.5 version: 7.28.5(@babel/core@7.26.0) @@ -292,6 +298,12 @@ importers: test-projects/gts-experimental-worker: devDependencies: + '@babel/core': + specifier: ^7.23.6 + version: 7.26.0 + '@babel/eslint-parser': + specifier: ^7.28.6 + version: 7.28.6(@babel/core@7.26.0)(eslint@8.57.1) '@babel/plugin-transform-runtime': specifier: ^7.28.5 version: 7.28.5(@babel/core@7.26.0) @@ -1031,6 +1043,15 @@ packages: resolution: {integrity: sha512-/SusdG+zgosc3t+9sPFVKSFOYyiSgLfXOT6lYNWoG1YtnhWDxlK4S8leZ0jhcVjemdaHln5rTyxCnq8oFLxqpQ==} engines: {node: 12.* || 14.* || >= 16} + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -1415,6 +1436,9 @@ packages: resolution: {integrity: sha512-SkAyKAByB9l93Slyg8AUHGuM2kjvWioUTCckT/03J09jYnfEzMO/wSXmEhnKGYs6qx9De8TH4yJCl0Y9lRgnyQ==} engines: {node: '>=14.18.0'} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -1508,6 +1532,128 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@oxc-parser/binding-android-arm-eabi@0.119.0': + resolution: {integrity: sha512-e0ii/Tqwk5pAHZRY+ZyXOdKHNRNmE+dvTGQZ7xQ5XPH2Am59aktD30QvfcfwItGhNTLCj/5TYGH5RHvmvqaILQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.119.0': + resolution: {integrity: sha512-ha0xQpiStuoBv7HGazNKQWa6IRxri2+PpeojdAyBGnHGzfioA1GcStNGEGOyXvF+OxDfWvPuw5QiRYRUMtmgbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.119.0': + resolution: {integrity: sha512-h/AIi5jfQz9WQUJJkkkHeXNYMhPtR72qnYZt0ZpM/LvlH/wpI5QkCPi7MWjjyY+m0JDorIXJyfOfccn8SbNSxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.119.0': + resolution: {integrity: sha512-15RwS/AawrgognvWsonI2eLKI5BqO0FzrpYXnzROysSR0x5RYsCc3UMFBwB1ph0UFFQzJy3ZbHHxfxp8RGr5Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.119.0': + resolution: {integrity: sha512-iKaayTIDqEj0yyNPL+0t/spNAxMv7O32uY4eu/ir8BvFNgavoRmN8uqxRj8sxQDle89N/1Iw0dgRjS3tiWrqlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.119.0': + resolution: {integrity: sha512-PDoOaOx8YWoxy19WNeMs6kOE0uFSb5EtA64Ye0wSp6sQpe+l8Gd+yjX2L+yNwd5MpDOvOy8KToa2bqCV4pf6iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.119.0': + resolution: {integrity: sha512-AVxZ5Eo5squsUhpjnkCYuH20t5FCGV3HAP9UOKLxJQkmZW3kJvBGbfpH75ABxRrE2kGqmJW5FmS980u8v9Cepw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.119.0': + resolution: {integrity: sha512-9vfdyT9gczSeSwsEkBHVjigI8SWo3iB9zxEzL+YMBUrN0ftCUkKQr27DaDZK4/cQ80t6KRB+g9sUmT2T2AGnOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.119.0': + resolution: {integrity: sha512-7BOq/tjSrtnp/ihw615uGcxMY3iya2qvVtwm15h2NvBZ6Jje+PC1GSUBOLfqGKJbUr9riSVV//a4iNhHI48Qdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.119.0': + resolution: {integrity: sha512-PQIrLJwoAaNyNSWBF+2SSgv44Jp+xpKVUA+8+PuoMhyBQ6lFSbQdaxewdn11i3heTFMYd2xF339HWax4S6MPVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.119.0': + resolution: {integrity: sha512-spNh4YhT9K+Ya5hr6NmI1MazKSKORD8u5/06hFbzTslLnmmxiGaLqJXhNKIYUH39ne/JD5rkoRkUcOB2/LpC3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.119.0': + resolution: {integrity: sha512-hFoCTRxSJAcrNBYVlgNDDQq6LyJLYyhnJDlPtK/mWrPYS3x5/fUR9jc6wo5VyxKIL/0dDJBBWp19v81q9heU/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.119.0': + resolution: {integrity: sha512-bl7jHJZq3W5tYEvKG3yWZTUKTNb0/BtyYSnfMIrQ7t8hajCH4i1g0q+14s0KmQQl1UHxIX/Gx/Ps6e92qJQJmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.119.0': + resolution: {integrity: sha512-m+DE7NhJIEGp4efSJnNfRf3swT25rbZ1FTIihV+pOLTI+k5yNguxvqT338mNu61OVSx0BfpV8QlO2nz43GR/Zg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.119.0': + resolution: {integrity: sha512-pHhnXZHUfd5pYzFLQfvx1DH2HY+L8DPZeh6SsQrsmoaODm1+j8VPeWLwGSrXQSz5f1kfT/mnzm1iNLVOGIeuaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.119.0': + resolution: {integrity: sha512-8Fthv9nOec0hQLX16yjYyYIU+u8ZFuQojdQ3vNgXN+PcqI/bDohGgCATrxO69gLf0IzkyOmKmurXOQCYK8BYpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.119.0': + resolution: {integrity: sha512-KpOU6fLqevFDP6ndkgE4BPoceELM4bOsEsAXjpe+FKYuUyEzHssYPBmxouGpXDQeAeWTBIjosw5yElLMRPuccw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.119.0': + resolution: {integrity: sha512-omtTgAKIl6GQ40nG+wAWN8xMJLNtfmTdd0+wMIcrw1shX9y5TntAVIuiay3Du0wvUK9sgMpL07HYNphgHeZS0A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.119.0': + resolution: {integrity: sha512-+0kqoCfv4WFP3e4BqcVEtf1moUuG9Zv5lo1aKcw1JakqJo008TGG+C2LnVM4QucGSZVQ/Ii/H5XCvrRbkeLQfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.119.0': + resolution: {integrity: sha512-5kaKmBHD+OQjZzGAQQ9n8jWNvCRxu3MjElAjkCqsS3i2wiN3hqHlOPKwGDydYiB1gKdeYGlTjRYtuF4gBLDSxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.119.0': + resolution: {integrity: sha512-9SCGhodOxEicD2kblitu34fGHcpmqgI3beYw/E22ehVLHzccHRFH91NmKt0MhZEaAwLpei6OOA9aB6Vuks9qAg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1649,6 +1795,9 @@ packages: '@tsconfig/ember@3.0.8': resolution: {integrity: sha512-OVnIsZIt/8q0VEtcdz3rRryNrm6gdJTxXlxefkGIrkZnME0wqslmwHlUEZ7mvh377df9FqBhNKrYNarhCW8zJA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -2646,6 +2795,9 @@ packages: resolution: {integrity: sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA==} engines: {node: 10.* || >= 12.*} + ember-estree@0.4.1: + resolution: {integrity: sha512-q+7tr+NRRUWOC/UVGF9Sh3H9F6K/UlYlhqXKIx/ZMQzIvFPu7hw3Pzug5Uf00nUlxQnx4Af/UrOMFR8b7vd1wQ==} + ember-rfc176-data@0.3.18: resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} @@ -4141,6 +4293,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.119.0: + resolution: {integrity: sha512-fNiKvO0ZHSUmINQlVY2It+vGbHxCvhpqJi0rZYFFOESoOy3fs5E4erKYGZtB/J1aULkjtY06aWNil4JxMsKXGg==} + engines: {node: ^20.19.0 || >=22.12.0} + p-finally@2.0.1: resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} engines: {node: '>=8'} @@ -5373,6 +5529,9 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -6277,6 +6436,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.9.0': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -6728,6 +6903,13 @@ snapshots: jju: 1.4.0 read-yaml-file: 1.1.0 + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -6847,6 +7029,70 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@oxc-parser/binding-android-arm-eabi@0.119.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.119.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.119.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.119.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.119.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.119.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.119.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.119.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.119.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.119.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.119.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.119.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.119.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.119.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.119.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.119.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.119.0': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.119.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.119.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.119.0': + optional: true + + '@oxc-project/types@0.119.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -6939,6 +7185,11 @@ snapshots: '@tsconfig/ember@3.0.8': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.6': {} @@ -8422,6 +8673,14 @@ snapshots: - '@babel/core' - supports-color + ember-estree@0.4.1: + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/syntax': 0.95.0 + content-tag: 4.1.0 + oxc-parser: 0.119.0 + zimmerframe: 1.1.4 + ember-rfc176-data@0.3.18: {} ember-router-generator@2.0.0: @@ -10157,7 +10416,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.6.2 + tslib: 2.8.1 lru-cache@10.4.3: {} @@ -10482,6 +10741,31 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.119.0: + dependencies: + '@oxc-project/types': 0.119.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.119.0 + '@oxc-parser/binding-android-arm64': 0.119.0 + '@oxc-parser/binding-darwin-arm64': 0.119.0 + '@oxc-parser/binding-darwin-x64': 0.119.0 + '@oxc-parser/binding-freebsd-x64': 0.119.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.119.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.119.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.119.0 + '@oxc-parser/binding-linux-arm64-musl': 0.119.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.119.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.119.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.119.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.119.0 + '@oxc-parser/binding-linux-x64-gnu': 0.119.0 + '@oxc-parser/binding-linux-x64-musl': 0.119.0 + '@oxc-parser/binding-openharmony-arm64': 0.119.0 + '@oxc-parser/binding-wasm32-wasi': 0.119.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.119.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.119.0 + '@oxc-parser/binding-win32-x64-msvc': 0.119.0 + p-finally@2.0.1: {} p-limit@1.3.0: @@ -11892,3 +12176,5 @@ snapshots: yocto-queue@1.1.1: {} yoctocolors@2.1.1: {} + + zimmerframe@1.1.4: {} diff --git a/scripts/eslint-plugin-ember-test.mjs b/scripts/eslint-plugin-ember-test.mjs index b73672c..9916fb4 100644 --- a/scripts/eslint-plugin-ember-test.mjs +++ b/scripts/eslint-plugin-ember-test.mjs @@ -1,7 +1,11 @@ import { execaCommand } from 'execa'; import fse from 'fs-extra'; -const REPO = `https://github.com/ember-cli/eslint-plugin-ember.git`; +// Use the fork branch that has the GlimmerBlockParam nodeType fix +// until https://github.com/ember-cli/eslint-plugin-ember/pull/2577 merges, +// then switch back to ember-cli/eslint-plugin-ember. +const REPO = `https://github.com/NullVoxPopuli-ai-agent/eslint-plugin-ember.git`; +const BRANCH = 'update-blockparam-nodetype'; const FOLDERS = { here: process.cwd(), testRoot: '/tmp/eslint-plugin-ember-test/', @@ -12,7 +16,10 @@ await fse.remove(FOLDERS.testRoot); await fse.ensureDir(FOLDERS.testRoot); // Using pnpm instead of yarn, because pnpm is way faster -await execaCommand(`git clone ${REPO}`, { cwd: FOLDERS.testRoot, stdio: 'inherit' }); +await execaCommand(`git clone --branch ${BRANCH} ${REPO}`, { + cwd: FOLDERS.testRoot, + stdio: 'inherit', +}); await execaCommand(`pnpm install`, { cwd: FOLDERS.repo, stdio: 'inherit' }); await execaCommand(`pnpm add ${FOLDERS.here}`, { cwd: FOLDERS.repo, stdio: 'inherit' }); await execaCommand(`pnpm run test`, { cwd: FOLDERS.repo, stdio: 'inherit' }); diff --git a/src/parser/gjs-gts-parser.js b/src/parser/gjs-gts-parser.js index 8c753f5..b5e16e3 100644 --- a/src/parser/gjs-gts-parser.js +++ b/src/parser/gjs-gts-parser.js @@ -1,18 +1,22 @@ import { createRequire } from 'node:module'; import tsconfigUtils from '@typescript-eslint/tsconfig-utils'; -import babelParser from '@babel/eslint-parser/experimental-worker'; import { registerParsedFile } from '../preprocessor/noop.js'; import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js'; -import { transformForLint, preprocessGlimmerTemplates, convertAst } from './transforms.js'; +import { buildGlimmerVisitors } from './transforms.js'; +import { toTree } from 'ember-estree'; +import * as eslintScope from 'eslint-scope'; const require = createRequire(import.meta.url); /** * implements https://eslint.org/docs/latest/extend/custom-parsers - * 1. transforms gts/gjs files into parseable ts/js without changing the offsets and locations around it - * 2. parses the transformed code and generates the AST for TS ot JS - * 3. preprocesses the templates info and prepares the Glimmer AST - * 4. converts the js/ts AST so that it includes the Glimmer AST at the right locations, replacing the original + * + * Uses ember-estree's toTree with a custom parser option to handle + * the full pipeline: template extraction, placeholder replacement, + * JS/TS parsing, Glimmer AST processing, and AST splicing. + * + * Scope registration happens via toTree's visitors API, eliminating + * a second AST traversal. */ /** @@ -77,12 +81,10 @@ function getAllowJsFromPrograms(programs) { function getProjectServiceTsconfigPath(projectService) { if (!projectService) return null; - // If projectService is true, use default behavior (nearest tsconfig.json, allowJs from config) if (projectService === true) { return 'tsconfig.json'; } - // If projectService is an object, handle ProjectServiceOptions if (typeof projectService === 'object') { if (typeof projectService.allowDefaultProject !== 'undefined') { // eslint-disable-next-line no-console @@ -143,9 +145,6 @@ export function parseForESLint(code, options) { ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs })); } registerParsedFile(options.filePath); - let jsCode = code; - const info = transformForLint(code, options.filePath); - jsCode = info.output; const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts'); let useTypescript = true; @@ -154,41 +153,55 @@ export function parseForESLint(code, options) { useTypescript = false; } - let result = null; - const filePath = options.filePath; - if (options.project || options.projectService) { - jsCode = replaceExtensions(jsCode); - } - if (isTypescript && !typescriptParser) { throw new Error('Please install typescript to process gts'); } + const filePath = options.filePath; + const useTS = isTypescript || useTypescript; + + // Both paths create scopeManager inside the parser callback so it's + // available when toTree invokes visitors during splice — no second pass. + let scopeManager = null; + try { - result = - isTypescript || useTypescript - ? typescriptParser.parseForESLint(jsCode, { - ...options, - ranges: true, - extraFileExtensions: ['.gts', '.gjs'], - filePath, - }) - : babelParser.parseForESLint(jsCode, { - ...options, - requireConfigFile: false, - ranges: true, - }); - if (!info.templateInfos?.length) { - return result; - } - const preprocessedResult = preprocessGlimmerTemplates(info, code); - preprocessedResult.code = code; - const { templateVisitorKeys } = preprocessedResult; - const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys }; - result.isTypescript = isTypescript || useTypescript; - convertAst(result, preprocessedResult, visitorKeys); + const result = toTree(code, { + filePath, + parser: useTS + ? (placeholderJS) => { + let parseCode = placeholderJS; + if (options.project || options.projectService) { + parseCode = replaceExtensions(parseCode); + } + const tsResult = typescriptParser.parseForESLint(parseCode, { + ...options, + ranges: true, + extraFileExtensions: ['.gts', '.gjs'], + filePath, + }); + scopeManager = tsResult.scopeManager; + return tsResult; + } + : (placeholderJS) => { + // JS path: parse with oxc, create scope manager from placeholder AST + const { parseSync } = require('oxc-parser'); + const oxcResult = parseSync(filePath || 'input.js', placeholderJS); + const program = oxcResult.program; + program.tokens = oxcResult.tokens || []; + program.comments = oxcResult.comments || []; + scopeManager = eslintScope.analyze(program, { + ecmaVersion: 2022, + sourceType: 'module', + range: true, + }); + return { ast: program, scopeManager }; + }, + visitors: buildGlimmerVisitors(() => scopeManager, useTS), + }); + + if (!result.scopeManager) result.scopeManager = scopeManager; + if (result.services?.program) { - // Compare allowJs with the actual program's compiler options const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs; if ( !allowGjsWasSet && @@ -204,9 +217,27 @@ export function parseForESLint(code, options) { } syncMtsGtsSourceFiles(result.services.program); } - return { ...result, visitorKeys }; + + delete result.templateInfos; + delete result.isTypescript; + + return result; } catch (e) { + // eslint-disable-next-line no-console console.error(e); + // Convert content-tag parse errors to ESLint-compatible format + if (e.message?.includes('Parse Error at')) { + const [line, column] = e.message + .split(':') + .slice(-2) + .map((x) => parseInt(x)); + const err = new Error(e.source_code || e.message); + err.lineNumber = line; + err.column = column; + err.fileName = filePath; + err.index = undefined; + throw err; + } throw e; } } diff --git a/src/parser/hbs-parser.js b/src/parser/hbs-parser.js index 9722a74..00fb8f6 100644 --- a/src/parser/hbs-parser.js +++ b/src/parser/hbs-parser.js @@ -1,9 +1,8 @@ import * as eslintScope from 'eslint-scope'; -import DocumentLines from '../utils/document.js'; -import { processGlimmerTemplate, buildGlimmerVisitorKeys } from './transforms.js'; +import { toTree, glimmerVisitorKeys, DocumentLines } from 'ember-estree'; // Constant: Program + all Glimmer node types. Computed once at module load. -const hbsVisitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() }; +const hbsVisitorKeys = { Program: ['body'], ...glimmerVisitorKeys }; /** * implements https://eslint.org/docs/latest/extend/custom-parsers @@ -24,19 +23,15 @@ export const meta = { export function parseForESLint(code, options) { const filePath = (options && options.filePath) || ''; - const codeLines = new DocumentLines(code); let result; try { - result = processGlimmerTemplate({ - templateContent: code, - codeLines, - templateRange: [0, code.length], - }); + result = toTree(code, { templateOnly: true }); } catch (e) { // Transform glimmer parse error to ESLint-compatible error const loc = e.location || (e.hash && e.hash.loc); if (loc && loc.start) { + const codeLines = new DocumentLines(code); const err = Object.assign(new SyntaxError(e.message), { lineNumber: loc.start.line, column: loc.start.column, @@ -50,6 +45,10 @@ export function parseForESLint(code, options) { const { ast: templateNode, comments } = result; + // Use the Template node's loc.end for the Program's end position + // (avoids creating a duplicate DocumentLines just for this) + const endLoc = templateNode.loc?.end || { line: 1, column: code.length }; + // Wrap in a synthetic Program node (required by ESLint) const program = { type: 'Program', @@ -61,7 +60,7 @@ export function parseForESLint(code, options) { end: code.length, loc: { start: { line: 1, column: 0 }, - end: codeLines.offsetToPosition(code.length), + end: endLoc, }, }; diff --git a/src/parser/transforms.js b/src/parser/transforms.js index b2b3509..e9e0c60 100644 --- a/src/parser/transforms.js +++ b/src/parser/transforms.js @@ -1,12 +1,7 @@ import { createRequire } from 'node:module'; -import ContentTag from 'content-tag'; -import { - visitorKeys as glimmerVisitorKeys, - traverse as glimmerTraverse, - preprocess as glimmerPreprocess, - isKeyword as glimmerIsKeyword, -} from '@glimmer/syntax'; -import DocumentLines from '../utils/document.js'; +import { Preprocessor } from 'content-tag'; +import { isKeyword as glimmerIsKeyword } from '@glimmer/syntax'; +import { glimmerVisitorKeys } from 'ember-estree'; import { Reference, Scope, Variable, Definition } from 'eslint-scope'; import htmlTags from 'html-tags'; import svgTags from 'svg-tags'; @@ -30,12 +25,8 @@ try { // not available } -/** - * finds the nearest node scope - * @param scopeManager - * @param nodePath - * @return {*|null} - */ +// ── Scope helpers ───────────────────────────────────────────────────── + function findParentScope(scopeManager, nodePath) { let scope = null; let path = nodePath; @@ -49,18 +40,6 @@ function findParentScope(scopeManager, nodePath) { return null; } -/** - * tries to find the variable names {name} in any parent scope - * if the variable is not found it just returns the nearest scope, - * so that it's usage can be registered. - * - * Also returns the nearest scope (equivalent to findParentScope) in one pass, - * avoiding the redundant second traversal that findParentScope would perform. - * @param scopeManager - * @param nodePath - * @param name - * @return {{scope: null, variable: *}|{scope: (*|null)}} - */ function findVarInParentScopes(scopeManager, nodePath, name) { let defScope = null; let currentScope = null; @@ -82,19 +61,12 @@ function findVarInParentScopes(scopeManager, nodePath, name) { return { scope: currentScope, variable: defScope.set.get(name) }; } -/** - * registers a node variable usage in the scope. - * @param node - * @param scope - * @param variable - */ function registerNodeInScope(node, scope, variable) { const ref = new Reference(node, scope, Reference.READ); if (variable) { variable.references.push(ref); ref.resolved = variable; } else { - // register missing variable in most upper scope. let s = scope; while (s.upper) { s = s.upper; @@ -104,36 +76,106 @@ function registerNodeInScope(node, scope, variable) { scope.references.push(ref); } -/** - * Builds the complete Glimmer visitor keys map with "Glimmer" prefix and - * additional keys needed for traversal (blockParamNodes, parts, etc). - * Result is cached since glimmerVisitorKeys is a constant. - * @return {object} - */ -let _cachedGlimmerVisitorKeys = null; -function buildGlimmerVisitorKeys() { - if (_cachedGlimmerVisitorKeys) return _cachedGlimmerVisitorKeys; - const keys = {}; - for (const [k, v] of Object.entries(glimmerVisitorKeys)) { - keys[`Glimmer${k}`] = [...v]; +function isUpperCase(char) { + return char.toUpperCase() === char; +} + +function registerBlockParams(node, path, scopeManager, isTypescript) { + const blockParamNodes = node.blockParamNodes || []; + const upperScope = findParentScope(scopeManager, path); + const scope = isTypescript + ? new TypescriptScope.BlockScope(scopeManager, upperScope, node) + : new Scope(scopeManager, 'block', upperScope, node); + const declaredVariables = scopeManager.declaredVariables || scopeManager.__declaredVariables; + const vars = []; + declaredVariables.set(node, vars); + const virtualJSParentNode = { + type: 'FunctionDeclaration', + params: blockParamNodes, + range: node.range, + loc: node.loc, + parent: path.parent, + }; + for (const [i, b] of blockParamNodes.entries()) { + const v = new Variable(b.name, scope); + v.identifiers.push(b); + scope.variables.push(v); + scope.set.set(b.name, v); + vars.push(v); + + const virtualJSNode = { + type: 'Identifier', + name: b.name, + range: b.range, + loc: b.loc, + parent: virtualJSParentNode, + }; + v.defs.push(new Definition('Parameter', virtualJSNode, node, node, i, 'Block Param')); + v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param')); } - if (!keys.GlimmerElementNode.includes('blockParamNodes')) { - keys.GlimmerElementNode.push('blockParamNodes', 'parts'); +} + +function registerPathExpression(node, path, scopeManager) { + if (node.head.type !== 'VarHead') return; + const name = node.head.name; + if (glimmerIsKeyword(name)) return; + const { scope, variable } = findVarInParentScopes(scopeManager, path, name) || {}; + if (scope) { + node.head.parent = node; + registerNodeInScope(node.head, scope, variable); } - keys.GlimmerProgram = ['body', 'blockParamNodes']; - keys.GlimmerTemplate = ['body']; - _cachedGlimmerVisitorKeys = keys; - return keys; } +function registerElementNode(node, path, scopeManager) { + const n = node.parts[0]; + const { scope, variable } = findVarInParentScopes(scopeManager, path, n.name) || {}; + const ignore = + n.name === 'this' || + n.name.startsWith(':') || + n.name.startsWith('@') || + !scope || + n.name.includes('-'); + + const registerUndef = + isUpperCase(n.name[0]) || + node.name.includes('.') || + (!htmlTagsSet.has(node.name) && !svgTagsSet.has(node.name) && !mathMLTagsSet.has(node.name)); + + if (!ignore && (variable || registerUndef)) { + registerNodeInScope(n, scope, variable); + } +} + +// ── Visitor builders ────────────────────────────────────────────────── + /** - * traverses all nodes using the {visitorKeys} calling the callback function, visitor - * @param visitorKeys - * @param node - * @param visitor + * Build Glimmer visitors for toTree that register scopes during traversal. + * Uses a getter for scopeManager so it's available after the parser callback runs. + * @param {function} getScopeManager - returns the scopeManager (may be null initially) + * @param {boolean} isTypescript + * @returns {object} visitors for toTree */ +export function buildGlimmerVisitors(getScopeManager, isTypescript) { + return { + GlimmerPathExpression(node, path) { + const sm = getScopeManager(); + if (sm) registerPathExpression(node, path, sm); + }, + GlimmerElementNode(node, path) { + const sm = getScopeManager(); + if (sm) registerElementNode(node, path, sm); + }, + GlimmerBlockParams(node, path) { + const sm = getScopeManager(); + if (sm) registerBlockParams(node, path, sm, isTypescript); + }, + }; +} + +// ── registerGlimmerScopes (fallback for JS/oxc path) ────────────────── + function traverse(visitorKeys, node, visitor) { - const allVisitorKeys = { ...visitorKeys, ...buildGlimmerVisitorKeys() }; + const allVisitorKeys = { ...visitorKeys, ...glimmerVisitorKeys }; const queue = []; queue.push({ @@ -146,19 +188,12 @@ function traverse(visitorKeys, node, visitor) { while (queue.length > 0) { const currentPath = queue.pop(); - visitor(currentPath); - if (!currentPath.node) continue; - - const visitorKeys = allVisitorKeys[currentPath.node.type]; - if (!visitorKeys) { - continue; - } - - for (const visitorKey of visitorKeys) { + const keys = allVisitorKeys[currentPath.node.type]; + if (!keys) continue; + for (const visitorKey of keys) { const child = currentPath.node[visitorKey]; - if (!child) { continue; } else if (Array.isArray(child)) { @@ -184,475 +219,34 @@ function traverse(visitorKeys, node, visitor) { } } -function isUpperCase(char) { - return char.toUpperCase() === char; -} - -function isAlphaNumeric(code) { - return !( - !(code > 47 && code < 58) && // numeric (0-9) - !(code > 64 && code < 91) && // upper alpha (A-Z) - !(code > 96 && code < 123) - ); -} - -function isWhiteSpaceCode(code) { - return ( - code === 32 /* space */ || - code === 9 /* tab */ || - code === 13 /* carriageReturn */ || - code === 10 /* lineFeed */ || - code === 11 /* verticalTab */ - ); -} - -/** - * simple tokenizer for templates, just splits it up into words and punctuators - * @param template {string} - * @param startOffset {number} - * @param doc {DocumentLines} - * @return {Token[]} - */ -function tokenize(template, doc, startOffset) { - const tokens = []; - let wordStart = -1; - function pushToken(value, type, range) { - const t = { - type, - value, - range, - start: range[0], - end: range[1], - loc: { - start: { ...doc.offsetToPosition(range[0]), index: range[0] }, - end: { ...doc.offsetToPosition(range[1]), index: range[1] }, - }, - }; - tokens.push(t); - } - for (let i = 0; i < template.length; i++) { - const code = template.charCodeAt(i); - if (isAlphaNumeric(code)) { - if (wordStart < 0) { - wordStart = i; - } - } else { - if (wordStart >= 0) { - pushToken(template.slice(wordStart, i), 'word', [startOffset + wordStart, startOffset + i]); - wordStart = -1; - } - if (!isWhiteSpaceCode(code)) { - pushToken(template[i], 'Punctuator', [startOffset + i, startOffset + i + 1]); - } - } - } - if (wordStart >= 0) { - pushToken(template.slice(wordStart), 'word', [ - startOffset + wordStart, - startOffset + template.length, - ]); - } - return tokens; -} - -/** - * Traverses a Glimmer AST, sets parent references, and categorizes nodes. - * @param {object} ast - * @return {{ allNodes: object[], comments: object[], textNodes: object[], emptyTextNodes: object[] }} - */ -function collectNodes(ast) { - const allNodes = []; - const comments = []; - const textNodes = []; - const emptyTextNodes = []; - - glimmerTraverse(ast, { - All(node, path) { - node.parent = path.parentNode; - allNodes.push(node); - if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') { - comments.push(node); - } - if (node.type === 'TextNode') { - node.value = node.chars; - if (node.value.trim().length !== 0 || (node.parent && node.parent.type === 'AttrNode')) { - textNodes.push(node); - } else { - emptyTextNodes.push(node); - } - } - }, - }); - - return { allNodes, comments, textNodes, emptyTextNodes }; -} - -/** - * Removes nodes from their parent's children/body/parts arrays. - * @param {object[]} nodes - */ -function removeFromParent(nodes) { - for (const node of nodes) { - const children = - (node.parent && (node.parent.children || node.parent.body || node.parent.parts)) || []; - const idx = children.indexOf(node); - if (idx >= 0) { - children.splice(idx, 1); - } - } -} - -/** - * Builds the final token stream by filtering out tokens covered by comments - * or text nodes, then merging text nodes back in sorted order. - * @param {object[]} rawTokens - * @param {object[]} comments - * @param {object[]} textNodes - * @return {object[]} - */ -function buildTokenStream(rawTokens, comments, textNodes) { - // Build sorted interval arrays for O(log n) exclusion checks - const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]); - const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]); - - /** - * Binary-search: is the token's range fully covered by any interval in `intervals`? - * Intervals must be sorted by start offset. - * @param {number[]} tokenRange - * @param {number[][]} intervals - */ - function isCovered(tokenRange, intervals) { - let lo = 0; - let hi = intervals.length - 1; - while (lo <= hi) { - const mid = (lo + hi) >> 1; - const iv = intervals[mid]; - if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) { - return true; - } - if (iv[0] > tokenRange[0]) { - hi = mid - 1; - } else { - lo = mid + 1; - } - } - return false; - } - - // Single-pass filter: drop tokens covered by a comment or text node - const filteredTokens = rawTokens.filter( - (t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals) - ); - - // Merge text nodes (already sorted by position from the AST) into filteredTokens - // using a single linear merge pass instead of repeated splice calls. - const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]); - const result = []; - let ti = 0; - for (const token of filteredTokens) { - while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) { - result.push(sortedTextNodes[ti++]); - } - result.push(token); - } - while (ti < sortedTextNodes.length) { - result.push(sortedTextNodes[ti++]); - } - - return result; -} - /** - * Parses a Glimmer template and produces a processed AST ready for ESLint. - * Shared between hbs-parser (standalone .hbs files) and gjs/gts parser (embedded templates). - * - * @param {object} options - * @param {string} options.templateContent - The template string to parse with glimmer - * @param {DocumentLines} options.codeLines - DocumentLines for the full source file - * @param {[number, number]} options.templateRange - Range [start, end] for the Template root node - * @param {string} [options.tokenSource] - String to tokenize (defaults to templateContent) - * @return {{ ast: object, comments: object[] }} + * Full AST traversal for scope registration — used as fallback for JS/oxc path. + * For the TS path, toTree's visitor API handles this during splicing. */ -function processGlimmerTemplate({ templateContent, codeLines, templateRange, tokenSource }) { - const offset = templateRange[0]; - const docLines = new DocumentLines(templateContent); - - /** Convert a Glimmer loc to a file-level [start, end] range */ - const toFileRange = (loc) => [ - offset + docLines.positionToOffset(loc.start), - offset + docLines.positionToOffset(loc.end), - ]; - /** Convert a file-level range to a file-level loc */ - const toFileLoc = (range) => ({ - start: codeLines.offsetToPosition(range[0]), - end: codeLines.offsetToPosition(range[1]), - }); - - const ast = glimmerPreprocess(templateContent, { mode: 'codemod' }); - const { allNodes, comments, textNodes, emptyTextNodes } = collectNodes(ast); - - // Fix ranges, locs, and prefix types with "Glimmer" - for (const n of allNodes) { - if (n.type === 'PathExpression') { - n.head.range = toFileRange(n.head.loc); - n.head.loc = toFileLoc(n.head.range); - } - - n.range = n.type === 'Template' ? [...templateRange] : toFileRange(n.loc); - n.start = n.range[0]; - n.end = n.range[1]; - n.loc = toFileLoc(n.range); - - if (n.type === 'ElementNode') { - n.name = n.tag; - n.parts = [n.path.head].map((p) => { - const range = toFileRange(p.loc); - return { - ...p, - name: p.original, - parent: n, - type: 'GlimmerElementNodePart', - range, - loc: toFileLoc(range), - }; - }); - } - - if ('blockParams' in n) { - n.params = (n.params || []).map((p) => { - const range = toFileRange(p.loc); - return { - ...p, - type: 'BlockParam', - name: p.original, - parent: n, - range, - loc: toFileLoc(range), - }; - }); - } - - // Nullify empty hashes before the type is renamed - if ( - (n.type === 'MustacheStatement' || - n.type === 'BlockStatement' || - n.type === 'SubExpression') && - n.hash && - n.hash.pairs && - n.hash.pairs.length === 0 - ) { - n.hash = null; - } - - n.type = `Glimmer${n.type}`; - } - - // Clean up AST structure - removeFromParent(emptyTextNodes); - removeFromParent(comments); - for (const comment of comments) { - comment.type = 'Block'; - } - - // Build final token stream - ast.tokens = buildTokenStream( - tokenize(tokenSource || templateContent, codeLines, offset), - comments, - textNodes - ); - ast.contents = templateContent; - - return { ast, comments }; -} - -/** - * Preprocesses the template info, parsing the template content to Glimmer AST, - * fixing the offsets and locations of all nodes - * also calculates the block params locations & ranges - * and adding it to the info - * @param info - * @param code - * @return {{templateVisitorKeys: {}, comments: *[], templateInfos: {templateRange: *, range: *, replacedRange: *}[]}} - */ -export function preprocessGlimmerTemplates(info, code) { - const templateInfos = info.templateInfos.map((r) => ({ - utf16Range: [r.range.startUtf16Codepoint, r.range.endUtf16Codepoint], - })); - const codeLines = new DocumentLines(code); - const allComments = []; - - for (const tpl of templateInfos) { - const template = code.slice(...tpl.utf16Range); - - const { ast, comments } = processGlimmerTemplate({ - templateContent: template, - codeLines, - templateRange: [...tpl.utf16Range], - }); - - ast.content = template; - allComments.push(...comments); - tpl.ast = ast; - } - - return { - templateVisitorKeys: buildGlimmerVisitorKeys(), - templateInfos, - comments: allComments, - }; -} - -/** - * traverses the AST and replaces the transformed template parts with the Glimmer - * AST. - * This also creates the scopes for the Glimmer Blocks and registers the block params - * in the scope, and also any usages of variables in path expressions - * this allows the basic eslint rules no-undef and no-unsused to work also for the - * templates without needing any custom rules - * @param result - * @param preprocessedResult - * @param visitorKeys - */ -export function convertAst(result, preprocessedResult, visitorKeys) { - const templateInfos = preprocessedResult.templateInfos; - let counter = 0; - result.ast.comments.push(...preprocessedResult.comments); - - for (const ti of templateInfos) { - const firstIdx = result.ast.tokens.findIndex((t) => t.range[0] === ti.utf16Range[0]); - const lastIdx = result.ast.tokens.findIndex((t) => t.range[1] === ti.utf16Range[1]); - result.ast.tokens.splice(firstIdx, lastIdx - firstIdx + 1, ...ti.ast.tokens); - } - - // Build a Map keyed by range start for O(1) lookup during traversal - const templateInfoByStart = new Map(templateInfos.map((t) => [t.utf16Range[0], t])); - +export function registerGlimmerScopes(result) { // eslint-disable-next-line complexity - traverse(visitorKeys, result.ast, (path) => { + traverse(result.visitorKeys, result.ast, (path) => { const node = path.node; - if (!node) return null; - - if ( - node.type === 'ExpressionStatement' || - node.type === 'StaticBlock' || - node.type === 'TemplateLiteral' || - node.type === 'ExportDefaultDeclaration' - ) { - let range = node.range; - if (node.type === 'ExportDefaultDeclaration') { - range = [node.declaration.range[0], node.declaration.range[1]]; - } - - const template = templateInfoByStart.get(range[0]); - if ( - !template || - (template.utf16Range[1] !== range[1] && template.utf16Range[1] !== range[1] + 1) - ) { - return null; - } - counter++; - const ast = template.ast; - Object.assign(node, ast); - } - - if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') { - const name = node.head.name; - if (glimmerIsKeyword(name)) { - return null; - } - const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {}; - if (scope) { - node.head.parent = node; - registerNodeInScope(node.head, scope, variable); - } + if (!node) return; + if (node.type === 'GlimmerPathExpression') { + registerPathExpression(node, path, result.scopeManager); } if (node.type === 'GlimmerElementNode') { - // always reference first part of tag name, this also has the advantage - // that errors regarding this tag will only mark the tag name instead of - // the whole tag + children - const n = node.parts[0]; - const { scope, variable } = findVarInParentScopes(result.scopeManager, path, n.name) || {}; - /* - register a node in scope if we found a variable - we ignore named-blocks and args as we know that it doesn't reference anything in current scope - we also ignore `this` - if we do not find a variable we register it with a missing variable if - * it starts with upper case, it should be a component with a reference - * it includes a dot, it's a path which should have a reference - * it's NOT a standard html, svg or mathml tag, it should have a referenced variable - */ - const ignore = - // Local instance access - n.name === 'this' || - // named block - n.name.startsWith(':') || - // argument - n.name.startsWith('@') || - // defined locally - !scope || - // custom-elements are allowed to be used even if they don't exist - // and are undefined - n.name.includes('-'); - - const registerUndef = - isUpperCase(n.name[0]) || - node.name.includes('.') || - (!htmlTagsSet.has(node.name) && - !svgTagsSet.has(node.name) && - !mathMLTagsSet.has(node.name)); - - if (!ignore && (variable || registerUndef)) { - registerNodeInScope(n, scope, variable); - } + registerElementNode(node, path, result.scopeManager); } - - if ('blockParams' in node) { - const upperScope = findParentScope(result.scopeManager, path); - const scope = result.isTypescript - ? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node) - : new Scope(result.scopeManager, 'block', upperScope, node); - const declaredVariables = - result.scopeManager.declaredVariables || result.scopeManager.__declaredVariables; - const vars = []; - declaredVariables.set(node, vars); - const virtualJSParentNode = { - type: 'FunctionDeclaration', - params: node.params, - range: node.range, - loc: node.loc, - parent: path.parent, - }; - for (const [i, b] of node.params.entries()) { - const v = new Variable(b.name, scope); - v.identifiers.push(b); - scope.variables.push(v); - scope.set.set(b.name, v); - vars.push(v); - - const virtualJSNode = { - type: 'Identifier', - name: b.name, - range: b.range, - loc: b.loc, - parent: virtualJSParentNode, - }; - v.defs.push(new Definition('Parameter', virtualJSNode, node, node, i, 'Block Param')); - v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param')); - } + if ('blockParams' in node && node.type?.startsWith('Glimmer')) { + registerBlockParams(node, path, result.scopeManager, result.isTypescript); } - return null; }); - - if (counter !== templateInfos.length) { - throw new Error('failed to process all templates'); - } } +// ── transformForLint (used by ts-patch.js) ──────────────────────────── + export const replaceRange = function replaceRange(s, start, end, substitute) { return s.slice(0, start) + substitute + s.slice(end); }; -const processor = new ContentTag.Preprocessor(); +const processor = new Preprocessor(); class EmberParserError extends Error { constructor(message, fileName, location) { @@ -666,17 +260,14 @@ class EmberParserError extends Error { }); } - // For old version of ESLint https://github.com/typescript-eslint/typescript-eslint/pull/6556#discussion_r1123237311 get index() { return this.location.start.offset; } - // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L853 get lineNumber() { return this.location.start.line; } - // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L854 get column() { return this.location.start.column; } @@ -686,94 +277,44 @@ function createError(code, message, fileName, start, end = start) { return new EmberParserError(message, fileName, { end, start }); } +/** + * Transform code for TypeScript virtual file system. + * Replaces