diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c5eb534..d1c1644f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,3 +89,21 @@ jobs: - uses: crate-ci/typos@85f62a8a84f939ae994ab3763f01a0296d61a7ee # v1.36.2 with: files: . + + typecheck-rulegen: + name: TypeScript Type Check (rulegen) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: oxc-project/setup-node@fdbf0dfd334c4e6d56ceeb77d91c76339c2a0885 # v1.0.4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: tasks/rulegen + + - name: Run TypeScript type check + run: pnpm run typecheck + working-directory: tasks/rulegen diff --git a/justfile b/justfile index 691e3a24..5af8fc76 100644 --- a/justfile +++ b/justfile @@ -46,3 +46,6 @@ pull: pushd typescript-go && git reset --hard origin/main git pull just init + +new-rule name: + cd tasks/rulegen && pnpm run rulegen {{name}} && cd ../.. \ No newline at end of file diff --git a/tasks/rulegen/.gitignore b/tasks/rulegen/.gitignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/tasks/rulegen/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/tasks/rulegen/package.json b/tasks/rulegen/package.json new file mode 100644 index 00000000..4ad959ff --- /dev/null +++ b/tasks/rulegen/package.json @@ -0,0 +1,16 @@ +{ + "name": "@tsgolint/rulegen", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "rulegen": "node --import @oxc-node/core/register src/index.ts", + "typecheck": "tsc" + }, + "devDependencies": { + "@oxc-node/core": "^0.0.32", + "@types/node": "^24.5.2", + "oxc-parser": "^0.89.0", + "typescript": "^5.9.2" + } +} diff --git a/tasks/rulegen/pnpm-lock.yaml b/tasks/rulegen/pnpm-lock.yaml new file mode 100644 index 00000000..63e3a9e0 --- /dev/null +++ b/tasks/rulegen/pnpm-lock.yaml @@ -0,0 +1,428 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@oxc-node/core': + specifier: ^0.0.32 + version: 0.0.32 + '@types/node': + specifier: ^24.5.2 + version: 24.5.2 + oxc-parser: + specifier: ^0.89.0 + version: 0.89.0 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + +packages: + + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@napi-rs/wasm-runtime@1.0.5': + resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} + + '@oxc-node/core-android-arm-eabi@0.0.32': + resolution: {integrity: sha512-Ykkz+xYJ1Hd+vjVSXljyIFRW/ZRMp2tQfPy9T2jj9F2pm5CAchqJHSXEbB3FbhdUXusUqhWLmu70JdVhS4SBcQ==} + cpu: [arm] + os: [android] + + '@oxc-node/core-android-arm64@0.0.32': + resolution: {integrity: sha512-csPEZPGTlPtkoMnDeG+hrYX2AJIh8dPKW7TW5E0MdNMIkJ8aFIymBH2QF+xSMsFl9Gpbl5ZxYLkRIrth+Mhzhg==} + cpu: [arm64] + os: [android] + + '@oxc-node/core-darwin-arm64@0.0.32': + resolution: {integrity: sha512-wlURG+gBge3ovdBtMQ/KvyFsLVXnQ4h5vlIxBYMCTkwCzwvfXbcSOLLZMxypOeXPlH/cdVqOigvh9XBbc3KAfg==} + cpu: [arm64] + os: [darwin] + + '@oxc-node/core-darwin-x64@0.0.32': + resolution: {integrity: sha512-tFlUz54cemNp6N7+suP+uwCXErzol7ibMslxOHbXtGWwRVckV3Bb1rQyCajFSQYCr/du1ZdDCVsTfuPmXQlejg==} + cpu: [x64] + os: [darwin] + + '@oxc-node/core-freebsd-x64@0.0.32': + resolution: {integrity: sha512-yqikvHq0VbnPasRwOkDiMe5dhLZexvZSBzNroBVGbMWNaHBvkP+GP4EFZ3Y5pcNPC3og3xP4J6+N8bbbEZk3vg==} + cpu: [x64] + os: [freebsd] + + '@oxc-node/core-linux-arm-gnueabihf@0.0.32': + resolution: {integrity: sha512-Qbv937NEH4gNNC1W3Lx6G4Mh5mETMYSf2jbTxCzcfng74BVorpQd0pKB72bvEA0RQtIkXoEfCW24VeyR3yA84Q==} + cpu: [arm] + os: [linux] + + '@oxc-node/core-linux-arm64-gnu@0.0.32': + resolution: {integrity: sha512-w+6SmMWEaVap5RC9axCvg1ZOFdQsNLlLYrYrKX5siZvcVs6jPNyg3O9rbO85tBFPvuAvSLr66mBJwW70MQJv0w==} + cpu: [arm64] + os: [linux] + + '@oxc-node/core-linux-arm64-musl@0.0.32': + resolution: {integrity: sha512-AZeVCYaECJbQaj6bxCZK77ApbAQUKNOq4wWoRIkVZnn9/ay5wqHLmkvWdOku0aw2WkGiteLp5nCXSDZjw1g0/A==} + cpu: [arm64] + os: [linux] + + '@oxc-node/core-linux-ppc64-gnu@0.0.32': + resolution: {integrity: sha512-sz8r3BPLWZC5vZ/5UCb2MhcEFT8bRIeo3IFnksh6K9umAyjgByxnBZP/EyL4twcjQqbrDC64Ox0Pm5vtTIiFWw==} + cpu: [ppc64] + os: [linux] + + '@oxc-node/core-linux-s390x-gnu@0.0.32': + resolution: {integrity: sha512-YLpDVEcsvj03z6SqqcxA1J+ocTR+pP6dv0oIrGXaATwQtx0C+nt6WrVESaXjJpQDAc/5I+LkVDlimeAJb4iLNQ==} + cpu: [s390x] + os: [linux] + + '@oxc-node/core-linux-x64-gnu@0.0.32': + resolution: {integrity: sha512-FCBaQq4Y08AOLvl83AVzGeRAp/7RI49sDb44sE3/XzxdAVHYyhp0wA84h/NBi8Cj8XhSJdr6KosskHl1cVB0Qg==} + cpu: [x64] + os: [linux] + + '@oxc-node/core-linux-x64-musl@0.0.32': + resolution: {integrity: sha512-QKvj6d7VGKK3udW8TXfB0NYNjCloKxZznl58eaoFMoO+bOwuOWJX/8tN37FjkghH4Sb38vRK24+BdqebAHjBdA==} + cpu: [x64] + os: [linux] + + '@oxc-node/core-openharmony-arm64@0.0.32': + resolution: {integrity: sha512-k8wqQZ04J4l44v6iB7VFVNc+tz0BBJV2nxSwhLTHpciAFZnOG4BJUqw+2OsObUreR3z5zarebkVFyEJ3QaUH5g==} + cpu: [arm64] + os: [openharmony] + + '@oxc-node/core-wasm32-wasi@0.0.32': + resolution: {integrity: sha512-O8Bj67iC0lm0xSlpmHseU+tQytrqdhDFUc0MzYmPSvs0i60u3CNGiw/BWuO6xDpBfhp/IQkWSGBrecZZt1hMFg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-node/core-win32-arm64-msvc@0.0.32': + resolution: {integrity: sha512-mGFu9B3E/jBqvy6FzTZHjUZUOOst1dnN/LIX1Bi8wbSrB+1TdOBykOcNJ3F+BppdaTCWqKb6ieLU6BvvnxboaA==} + cpu: [arm64] + os: [win32] + + '@oxc-node/core-win32-ia32-msvc@0.0.32': + resolution: {integrity: sha512-JEk4P/c/x6T4MHPBtty7IdCx/hxqhdhLrrJ0pIQ8Ue1Kkx74Aq6NDPg6TWtHAxmn+4cw6c1TLzGPXW+1dwbFcA==} + cpu: [ia32] + os: [win32] + + '@oxc-node/core-win32-x64-msvc@0.0.32': + resolution: {integrity: sha512-6G0YmfxjD3Ec8G619VXfJ3luryJb1Fq649H8ZcfLO8810WLwtzZiAXZCsCLs8lPsLTEpTeFR+AWuc8v4IRHekg==} + cpu: [x64] + os: [win32] + + '@oxc-node/core@0.0.32': + resolution: {integrity: sha512-2lbEquSd7qU5SZwbu2ngxs/vaa5sgRB5FE6TSPIPp6wfy7+11M0OrzD3g9TxH5CcsawecF2pC8nNpRVjBgBhOg==} + + '@oxc-parser/binding-android-arm64@0.89.0': + resolution: {integrity: sha512-Tz0FoCIU/MGteWnCLk8UjN6EgNCnFO/HPrf6/Slg2snFYBaEv4kQVSfe5uN6DWlhuX0/Hk891bJif7kvQkXskg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.89.0': + resolution: {integrity: sha512-kypXATIbctk9djDWXuC7XGbfOgiNJgYpIHIN7lyIhEqYvvrpmSiRaufn2zIjzDxmJFyok4A7wHJH7LsNwrOZhw==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.89.0': + resolution: {integrity: sha512-0mBLPC7LH6FCTfpq8vjZ12ZevOPN8XvrxV6zIyUlW2Xv/s4AJ5myn1Vqy3g0kCJYVoyTNWOLHzkTp2uDx0VQOw==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.89.0': + resolution: {integrity: sha512-UWQai6zd3+w+Z7iRHkw8ZD5dwrlAWkbsBmK7nfNHwOa9gew/J10iS7aatX0YQ8MzpUVa5wIofGmB4kOoG/IzFg==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.89.0': + resolution: {integrity: sha512-vfi2bef+VMp0bcPomQugQcpEe4GpYKhT2HFCgUplEMneQNuPj5z/rF6u1Ix0y13MisO8cbNhxP0oB1tvqmPA6Q==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.89.0': + resolution: {integrity: sha512-EvLiLQSYkBTtp8c91bjBCxlf0rMIq3jfcYopEx5jz9lv/nhJGrxkzVvj0kAN3f2iufbkk0PygKkSvQx0Kpy9zQ==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.89.0': + resolution: {integrity: sha512-jeI7qoHvvUIqLCN4FAPmTLpcQrlL3cuteH0HuYBmeVdBcJyPLAWAht7EaDlPjYOFvp4e9Tp3IqrpdqCztCWdAw==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.89.0': + resolution: {integrity: sha512-Pxe89cKhKNvHm6eii1zWr711sEyxFzJxINYAP5SM7PgajQb9dZq8Le0e68NYADlsoXVc/zcYlo+aC2yhjxNMVA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.89.0': + resolution: {integrity: sha512-g9R9Uph7gi9SBYKop5SdNK3c9WCBZSeYkE3IU3dF5DZnR/xqBkVRKnc+LAuboijEhhqLw4XwbQ49Wd6cBDjSTg==} + engines: {node: '>=20.0.0'} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.89.0': + resolution: {integrity: sha512-RFalRHelX4EFJcp3Byy4z5ZnYoEFSlRmgZECYfr/7yPSmo79DKL2VEHeVzIAKFbfTOPDIm0yw9BbUEBDritWDQ==} + engines: {node: '>=20.0.0'} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.89.0': + resolution: {integrity: sha512-BjU07UxFMSm0/7c79KBBCC2Meef45CQ7dg068xLK6voZgEY24NNq2F0xfl7FxBrymR/viluSP5gopQYjHXqc4A==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.89.0': + resolution: {integrity: sha512-RLecF47V/uy3voAt2BFywfh1RUtHlgdz/oG9oMG0E+A/3ZQPTEB1WuJyAnDsC0anxQS5W2J+/az3l5sL4/n54w==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-wasm32-wasi@0.89.0': + resolution: {integrity: sha512-h9uyojS96as3+KnvQ6MojTBHeHTpN88ejzoNxEcUbhgupIYTCp59G+5BQh7E+w4lrCuGmnjphFkca07RvWQpbQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.89.0': + resolution: {integrity: sha512-OGa7mETyx7Mg0ecLQraawzRrVQDinNP7WQQ7CjDD/Y1YgKM/8oN9xa5TaVe5dh66pbKOz49NJ4LccaUs58UaMQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.89.0': + resolution: {integrity: sha512-uNRWb1VNW/OiJf26rc6XY63kRoHmIXMpsKFjECTGIfaxwNDKx/mtJSvLQaFDZxoWuY9VsNrtOqNidgkYeW3tfQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.89.0': + resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + + oxc-parser@0.89.0: + resolution: {integrity: sha512-BO/RxfooJxuoofjC1USPmZ9a32pzjwXFAM1MHO2zPSij9fkgngHMzAu4t8XY1zrXMmoLBv13fh6LUZ906Rjx+g==} + engines: {node: '>=20.0.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + +snapshots: + + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.0.5': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-node/core-android-arm-eabi@0.0.32': + optional: true + + '@oxc-node/core-android-arm64@0.0.32': + optional: true + + '@oxc-node/core-darwin-arm64@0.0.32': + optional: true + + '@oxc-node/core-darwin-x64@0.0.32': + optional: true + + '@oxc-node/core-freebsd-x64@0.0.32': + optional: true + + '@oxc-node/core-linux-arm-gnueabihf@0.0.32': + optional: true + + '@oxc-node/core-linux-arm64-gnu@0.0.32': + optional: true + + '@oxc-node/core-linux-arm64-musl@0.0.32': + optional: true + + '@oxc-node/core-linux-ppc64-gnu@0.0.32': + optional: true + + '@oxc-node/core-linux-s390x-gnu@0.0.32': + optional: true + + '@oxc-node/core-linux-x64-gnu@0.0.32': + optional: true + + '@oxc-node/core-linux-x64-musl@0.0.32': + optional: true + + '@oxc-node/core-openharmony-arm64@0.0.32': + optional: true + + '@oxc-node/core-wasm32-wasi@0.0.32': + dependencies: + '@napi-rs/wasm-runtime': 1.0.5 + optional: true + + '@oxc-node/core-win32-arm64-msvc@0.0.32': + optional: true + + '@oxc-node/core-win32-ia32-msvc@0.0.32': + optional: true + + '@oxc-node/core-win32-x64-msvc@0.0.32': + optional: true + + '@oxc-node/core@0.0.32': + dependencies: + pirates: 4.0.7 + optionalDependencies: + '@oxc-node/core-android-arm-eabi': 0.0.32 + '@oxc-node/core-android-arm64': 0.0.32 + '@oxc-node/core-darwin-arm64': 0.0.32 + '@oxc-node/core-darwin-x64': 0.0.32 + '@oxc-node/core-freebsd-x64': 0.0.32 + '@oxc-node/core-linux-arm-gnueabihf': 0.0.32 + '@oxc-node/core-linux-arm64-gnu': 0.0.32 + '@oxc-node/core-linux-arm64-musl': 0.0.32 + '@oxc-node/core-linux-ppc64-gnu': 0.0.32 + '@oxc-node/core-linux-s390x-gnu': 0.0.32 + '@oxc-node/core-linux-x64-gnu': 0.0.32 + '@oxc-node/core-linux-x64-musl': 0.0.32 + '@oxc-node/core-openharmony-arm64': 0.0.32 + '@oxc-node/core-wasm32-wasi': 0.0.32 + '@oxc-node/core-win32-arm64-msvc': 0.0.32 + '@oxc-node/core-win32-ia32-msvc': 0.0.32 + '@oxc-node/core-win32-x64-msvc': 0.0.32 + + '@oxc-parser/binding-android-arm64@0.89.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.89.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.89.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.89.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.89.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.89.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.89.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.89.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.89.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.89.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.89.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.89.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.89.0': + dependencies: + '@napi-rs/wasm-runtime': 1.0.5 + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.89.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.89.0': + optional: true + + '@oxc-project/types@0.89.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@24.5.2': + dependencies: + undici-types: 7.12.0 + + oxc-parser@0.89.0: + dependencies: + '@oxc-project/types': 0.89.0 + optionalDependencies: + '@oxc-parser/binding-android-arm64': 0.89.0 + '@oxc-parser/binding-darwin-arm64': 0.89.0 + '@oxc-parser/binding-darwin-x64': 0.89.0 + '@oxc-parser/binding-freebsd-x64': 0.89.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.89.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.89.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.89.0 + '@oxc-parser/binding-linux-arm64-musl': 0.89.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.89.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.89.0 + '@oxc-parser/binding-linux-x64-gnu': 0.89.0 + '@oxc-parser/binding-linux-x64-musl': 0.89.0 + '@oxc-parser/binding-wasm32-wasi': 0.89.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.89.0 + '@oxc-parser/binding-win32-x64-msvc': 0.89.0 + + pirates@4.0.7: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.2: {} + + undici-types@7.12.0: {} diff --git a/tasks/rulegen/src/index.ts b/tasks/rulegen/src/index.ts new file mode 100644 index 00000000..e26ead73 --- /dev/null +++ b/tasks/rulegen/src/index.ts @@ -0,0 +1,864 @@ +#!/usr/bin/env node +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Expression, ParseResult, parseSync, Program } from 'oxc-parser'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +interface TestCase { + code: string; + errors?: { + messageId?: string; + message?: string; + line?: number; + column?: number; + endLine?: number; + endColumn?: number; + suggestions?: { + messageId?: string; + message?: string; + output: string; + }[]; + }[]; + output?: string; + options?: any[]; +} + +interface RuleTester { + valid: TestCase[]; + invalid: TestCase[]; +} + +function kebabToPascal(str: string): string { + return str + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); +} + +function camelToPascal(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function extractOptionsFromAST(node: any): any { + if (!node) return null; + + if (node.type === 'ObjectExpression') { + const result: any = {}; + for (const prop of node.properties || []) { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key?.name || prop.key?.value; + if (key) { + result[key] = extractOptionsFromAST(prop.value); + } + } + } + return result; + } else if (node.type === 'ArrayExpression') { + return (node.elements || []).map((el: any) => extractOptionsFromAST(el)); + } else if (node.type === 'StringLiteral' || node.type === 'Literal') { + if (typeof node.value === 'string') { + return node.value; + } + return node.value; + } else if (node.type === 'NumericLiteral') { + return node.value; + } else if (node.type === 'BooleanLiteral') { + return node.value; + } else if (node.type === 'NullLiteral') { + return null; + } else if (node.type === 'Identifier') { + // Handle special identifiers like true, false, null, undefined + if (node.name === 'true') return true; + if (node.name === 'false') return false; + if (node.name === 'null') return null; + if (node.name === 'undefined') return undefined; + return node.name; + } + + return null; +} + +function convertOptionsToGoCode(options: any[], ruleName: string): string { + if (!options || options.length === 0) { + return ''; + } + + // For most rules, options is an array with a single object + if (options.length === 1 && typeof options[0] === 'object' && !Array.isArray(options[0])) { + const ruleNamePascal = kebabToPascal(ruleName); + const optionsTypeName = `${ruleNamePascal}Options`; + const opt = options[0]; + + let goCode = `Options: ${optionsTypeName}{`; + const fields: string[] = []; + + for (const [key, value] of Object.entries(opt)) { + const fieldName = camelToPascal(key); + + if (Array.isArray(value)) { + // Handle arrays (e.g., IgnoredTypeNames: []string{"RegExp"}) + if (value.every(v => typeof v === 'string')) { + const values = value.map(v => `"${v}"`).join(', '); + fields.push(`${fieldName}: []string{${values}}`); + } else { + // Handle other array types + fields.push(`${fieldName}: /* TODO: handle array type */`); + } + } else if (typeof value === 'boolean') { + fields.push(`${fieldName}: ${value}`); + } else if (typeof value === 'string') { + fields.push(`${fieldName}: "${value}"`); + } else if (typeof value === 'number') { + fields.push(`${fieldName}: ${value}`); + } else { + fields.push(`${fieldName}: /* TODO: handle complex type */`); + } + } + + goCode += fields.join(', ') + '}'; + return goCode; + } + + // For other cases, we'll need manual handling + return `Options: /* TODO: handle options: ${JSON.stringify(options)} */`; +} + +function extractTestCases(parseResult: ParseResult): RuleTester { + const valid: TestCase[] = []; + const invalid: TestCase[] = []; + + // Find the RuleTester.run call + function traverse(node: any): void { + if (!node || typeof node !== 'object') return; + + if ( + node.type === 'CallExpression' && + (node.callee?.type === 'MemberExpression' || node.callee?.type === 'StaticMemberExpression') && + node.callee?.object?.name === 'ruleTester' && + node.callee?.property?.name === 'run' + ) { + // The third argument should be the test cases object + const testCasesArg = node.arguments?.[2]; + if (testCasesArg?.type === 'ObjectExpression') { + for (const prop of testCasesArg.properties || []) { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key?.name || prop.key?.value; + if (key === 'valid' && prop.value?.type === 'ArrayExpression') { + for (let i = 0; i < (prop.value.elements?.length || 0); i++) { + const element = prop.value.elements[i]; + if (element) { + // Skip spread elements or other non-parseable nodes + if (element.type === 'SpreadElement') { + console.warn(`Skipping spread element in valid test case at index ${i}`); + continue; + } + try { + const testCase = parseTestCase(element); + if (!testCase.code) { + console.warn(`Skipping valid test case at index ${i}: missing code property`); + continue; + } + valid.push(testCase); + } catch (e) { + console.warn( + `Skipping valid test case at index ${i}: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + } + } else if (key === 'invalid' && prop.value?.type === 'ArrayExpression') { + for (let i = 0; i < (prop.value.elements?.length || 0); i++) { + const element = prop.value.elements[i]; + if (element) { + // Skip spread elements or other non-parseable nodes + if (element.type === 'SpreadElement') { + console.warn(`Skipping spread element in invalid test case at index ${i}`); + continue; + } + try { + const testCase = parseTestCase(element); + if (!testCase.code) { + console.warn(`Skipping invalid test case at index ${i}: missing code property`); + continue; + } + if (!testCase.errors || testCase.errors.length === 0) { + console.warn(`Warning: Invalid test case at index ${i} has no errors specified`); + } + invalid.push(testCase); + } catch (e) { + console.warn( + `Skipping invalid test case at index ${i}: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + } + } + } + } + } + } + + // Recursively traverse the AST + for (const key in node) { + if (key === 'parent') continue; // Skip parent references to avoid cycles + const value = node[key]; + if (value && typeof value === 'object') { + if (Array.isArray(value)) { + for (const child of value) { + if (child && typeof child === 'object') { + traverse(child); + } + } + } else { + traverse(value); + } + } + } + } + + function parseTestCase(node: any): TestCase { + const testCase: TestCase = { code: '' }; + + if (!node) { + throw new Error('Test case node is null or undefined'); + } + + // Handle string literals (different AST parsers use different names) + if (node.type === 'StringLiteral' || node.type === 'Literal' || node.type === 'TemplateLiteral') { + testCase.code = getStringValue(node); + if (!testCase.code && testCase.code !== '') { + throw new Error('Failed to extract code from string literal'); + } + } else if (node.type === 'TaggedTemplateExpression') { + // Handle tagged template expressions (e.g., noFormat`...`) + // For now, try to extract the template literal part + if (node.quasi?.type === 'TemplateLiteral') { + testCase.code = getStringValue(node.quasi); + } else { + // Fall back to empty string or throw error + console.warn(`Warning: Could not extract code from TaggedTemplateExpression`); + testCase.code = '// Tagged template expression - could not extract'; + } + } else if (node.type === 'ObjectExpression') { + for (const prop of node.properties || []) { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key?.name || prop.key?.value; + + switch (key) { + case 'code': + testCase.code = getStringValue(prop.value); + // Special handling for TaggedTemplateExpression that might have whitespace-only content + if (prop.value?.type === 'TaggedTemplateExpression' && !testCase.code?.trim()) { + // For tagged templates like noFormat`...`, even empty/whitespace code might be intentional + // Don't throw error, but log a warning + console.warn('Warning: Tagged template expression resulted in empty/whitespace-only code'); + } else if (!testCase.code) { + throw new Error('Test case code property is empty'); + } + break; + case 'errors': + if (prop.value?.type === 'ArrayExpression') { + testCase.errors = []; + for (const errorNode of prop.value.elements || []) { + if (errorNode) { + const error = parseError(errorNode); + testCase.errors.push(error); + } + } + } + break; + case 'output': + testCase.output = getStringValue(prop.value); + break; + case 'options': + if (prop.value?.type === 'ArrayExpression') { + testCase.options = []; + for (const optNode of prop.value.elements || []) { + if (optNode) { + const extractedOption = extractOptionsFromAST(optNode); + if (extractedOption !== null) { + testCase.options.push(extractedOption); + } + } + } + } + break; + } + } + } + } else { + throw new Error(`Unexpected test case node type: ${node.type}`); + } + + return testCase; + } + + function parseError(node: any): any { + const error: any = {}; + + if (!node) { + throw new Error('Error node is null or undefined'); + } + + if (node.type === 'ObjectExpression') { + for (const prop of node.properties || []) { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key?.name || prop.key?.value; + + switch (key) { + case 'messageId': + case 'message': + error[key] = getStringValue(prop.value); + break; + case 'line': + case 'column': + case 'endLine': + case 'endColumn': + const numValue = getNumberValue(prop.value); + if (numValue !== undefined) { + error[key] = numValue; + } + break; + case 'suggestions': + if (prop.value?.type === 'ArrayExpression') { + error.suggestions = []; + for (const suggestionNode of prop.value.elements || []) { + if (suggestionNode) { + const suggestion = parseSuggestion(suggestionNode); + error.suggestions.push(suggestion); + } + } + } + break; + } + } + } + } else { + throw new Error(`Unexpected error node type: ${node.type}`); + } + + return error; + } + + function parseSuggestion(node: any): any { + const suggestion: any = {}; + + if (!node) { + throw new Error('Suggestion node is null or undefined'); + } + + if (node.type === 'ObjectExpression') { + for (const prop of node.properties || []) { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key?.name || prop.key?.value; + + switch (key) { + case 'messageId': + case 'message': + case 'output': + suggestion[key] = getStringValue(prop.value); + break; + } + } + } + } else { + throw new Error(`Unexpected suggestion node type: ${node.type}`); + } + + return suggestion; + } + + function getStringValue(node: any): string { + if (!node) { + return ''; + } + + if (node.type === 'StringLiteral' || node.type === 'Literal') { + // Handle both StringLiteral and Literal (some parsers use different names) + if (typeof node.value === 'string') { + return node.value; + } + return node.value?.toString() || ''; + } else if (node.type === 'TemplateLiteral') { + // For template literals, concatenate the raw text parts + let result = ''; + for (let i = 0; i < (node.quasis?.length || 0); i++) { + result += node.quasis[i]?.value?.raw || ''; + if (i < (node.expressions?.length || 0)) { + // For now, we don't evaluate expressions in templates + // This could be improved to handle simple expressions + result += '${...}'; + } + } + return result; + } else if (node.type === 'TaggedTemplateExpression') { + // Handle tagged template expressions like noFormat`...` + // Extract the template literal part + if (node.quasi?.type === 'TemplateLiteral') { + return getStringValue(node.quasi); + } else if (node.quasi) { + // Try to extract from quasi if it's a different structure + let result = ''; + const quasi = node.quasi; + if (quasi.quasis && Array.isArray(quasi.quasis)) { + for (let i = 0; i < quasi.quasis.length; i++) { + result += quasi.quasis[i]?.value?.raw || ''; + if (i < (quasi.expressions?.length || 0)) { + result += '${...}'; + } + } + } + return result; + } + } + return ''; + } + + function getNumberValue(node: any): number | undefined { + if (!node) { + return undefined; + } + + if (node.type === 'NumericLiteral') { + return node.value; + } + return undefined; + } + + traverse(parseResult.program); + + return { valid, invalid }; +} + +function escapeGoString(str: string): string { + // Escape backticks in Go raw string literals + // If the string contains backticks, we need to use a different approach + if (str.includes('`')) { + // For strings with backticks, use regular string literals with escaping + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + + '"'; + } + // For strings without backticks, use raw string literals + return '`' + str + '`'; +} + +function generateGoRuleImplementation(ruleName: string, testCases: RuleTester): string { + const ruleNamePascal = kebabToPascal(ruleName); + const packageName = ruleName.replace(/-/g, '_'); + + // Extract unique messageIds from test cases + const messageIds = new Set(); + for (const testCase of testCases.invalid) { + if (testCase.errors) { + for (const error of testCase.errors) { + if (error.messageId) { + messageIds.add(error.messageId); + } + if (error.suggestions) { + for (const suggestion of error.suggestions) { + if (suggestion.messageId) { + messageIds.add(suggestion.messageId); + } + } + } + } + } + } + + // Extract options structure from test cases + const optionsFields = new Map>(); + for (const testCase of [...testCases.valid, ...testCases.invalid]) { + if (testCase.options && testCase.options.length > 0) { + for (const option of testCase.options) { + if (typeof option === 'object' && !Array.isArray(option)) { + for (const [key, value] of Object.entries(option)) { + if (!optionsFields.has(key)) { + optionsFields.set(key, new Set()); + } + // Track the type of this field + if (Array.isArray(value)) { + optionsFields.get(key)!.add('array'); + } else if (typeof value === 'boolean') { + optionsFields.get(key)!.add('bool'); + } else if (typeof value === 'string') { + optionsFields.get(key)!.add('string'); + } else if (typeof value === 'number') { + optionsFields.get(key)!.add('number'); + } + } + } + } + } + } + + let output = `package ${packageName} + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/typescript-eslint/tsgolint/internal/rule" +) + +`; + + // Generate Options struct if there are options + if (optionsFields.size > 0) { + output += `type ${ruleNamePascal}Options struct { +`; + for (const [key, types] of optionsFields.entries()) { + const fieldName = camelToPascal(key); + let goType = ''; + + // Determine Go type based on observed types + if (types.has('array')) { + // For now, assume string array (most common case) + goType = '[]string'; + } else if (types.has('bool')) { + goType = 'bool'; + } else if (types.has('string')) { + goType = 'string'; + } else if (types.has('number')) { + goType = 'int'; + } else { + goType = 'interface{}'; + } + + output += `\t${fieldName} ${goType} \`json:"${key}"\` +`; + } + output += `} + +`; + } + + // Generate message functions for each unique messageId + for (const messageId of messageIds) { + const funcName = `build${messageId.charAt(0).toUpperCase() + messageId.slice(1)}Message`; + output += `func ${funcName}() rule.RuleMessage { + return rule.RuleMessage{ + Id: "${messageId}", + Description: "TODO: Add description for ${messageId}", + } +} + +`; + } + + // Generate the main rule structure + output += `var ${ruleNamePascal}Rule = rule.Rule{ + Name: "${ruleName}", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + return rule.RuleListeners{ + // TODO: Implement the rule logic here + // This is a stub implementation that needs to be filled in + // based on the TypeScript ESLint rule implementation + + // Example listener for expressions: + // ast.KindCallExpression: func(node *ast.Node) { + // // Check the node and report if necessary + // ctx.ReportNode(node, buildMessageFunction()) + // }, + } + }, +} +`; + + return output; +} + +function generateGoTest(ruleName: string, testCases: RuleTester): string { + const ruleNamePascal = kebabToPascal(ruleName); + const packageName = ruleName.replace(/-/g, '_'); + + let output = `package ${packageName} + +import ( +\t"testing" + +\t"github.com/typescript-eslint/tsgolint/internal/rule_tester" +\t"github.com/typescript-eslint/tsgolint/internal/rules/fixtures" +) + +func Test${ruleNamePascal}Rule(t *testing.T) { +\trule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &${ruleNamePascal}Rule, []rule_tester.ValidTestCase{`; + + // Add valid test cases + for (const testCase of testCases.valid) { + const escapedCode = escapeGoString(testCase.code); + output += ` +\t\t{`; + + // Add Code field + output += `Code: ${escapedCode}`; + + // Add Options field if present + if (testCase.options && testCase.options.length > 0) { + const optionsCode = convertOptionsToGoCode(testCase.options, ruleName); + if (optionsCode) { + output += `, ${optionsCode}`; + } + } + + output += `},`; + } + + output += ` +\t}, []rule_tester.InvalidTestCase{`; + + // Add invalid test cases + for (const testCase of testCases.invalid) { + const escapedCode = escapeGoString(testCase.code); + output += ` +\t\t{ +\t\t\tCode: ${escapedCode},`; + + // Add Options field if present + if (testCase.options && testCase.options.length > 0) { + const optionsCode = convertOptionsToGoCode(testCase.options, ruleName); + if (optionsCode) { + output += ` +\t\t\t${optionsCode},`; + } + } + + if (testCase.errors && testCase.errors.length > 0) { + output += ` +\t\t\tErrors: []rule_tester.InvalidTestCaseError{`; + + for (const error of testCase.errors) { + output += ` +\t\t\t\t{`; + + if (error.messageId) { + output += ` +\t\t\t\t\tMessageId: "${error.messageId}",`; + } + if (error.message) { + output += ` +\t\t\t\t\tMessage: "${error.message}",`; + } + if (error.line) { + output += ` +\t\t\t\t\tLine: ${error.line},`; + } + if (error.column) { + output += ` +\t\t\t\t\tColumn: ${error.column},`; + } + if (error.endLine) { + output += ` +\t\t\t\t\tEndLine: ${error.endLine},`; + } + if (error.endColumn) { + output += ` +\t\t\t\t\tEndColumn: ${error.endColumn},`; + } + + if (error.suggestions && error.suggestions.length > 0) { + output += ` +\t\t\t\t\tSuggestions: []rule_tester.InvalidTestCaseSuggestion{`; + + for (const suggestion of error.suggestions) { + output += ` +\t\t\t\t\t\t{`; + + if (suggestion.messageId) { + output += ` +\t\t\t\t\t\t\tMessageId: "${suggestion.messageId}",`; + } + if (suggestion.message) { + output += ` +\t\t\t\t\t\t\tMessage: "${suggestion.message}",`; + } + if (suggestion.output) { + const escapedOutput = escapeGoString(suggestion.output); + output += ` +\t\t\t\t\t\t\tOutput: ${escapedOutput},`; + } + + output += ` +\t\t\t\t\t\t},`; + } + + output += ` +\t\t\t\t\t},`; + } + + output += ` +\t\t\t\t},`; + } + + output += ` +\t\t\t},`; + } + + output += ` +\t\t},`; + } + + output += ` +\t}) +} +`; + + return output; +} + +async function downloadTestFile(ruleName: string): Promise { + const url = + `https://raw.githubusercontent.com/typescript-eslint/typescript-eslint/main/packages/eslint-plugin/tests/rules/${ruleName}.test.ts`; + + console.log(`Downloading test file from: ${url}`); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to download test file: ${response.statusText}`); + } + + return await response.text(); +} + +async function downloadRuleSource(ruleName: string): Promise { + const url = + `https://raw.githubusercontent.com/typescript-eslint/typescript-eslint/main/packages/eslint-plugin/src/rules/${ruleName}.ts`; + + console.log(`Downloading rule source from: ${url}`); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to download rule source: ${response.statusText}`); + } + + return await response.text(); +} + +function checkRequiresTypeChecking(ruleSource: string): boolean { + // Check if the rule requires type checking by looking for requiresTypeChecking: true in the meta + // This regex looks for the meta object and checks if requiresTypeChecking is set to true + const metaRegex = /meta:\s*{[^}]*requiresTypeChecking:\s*true/s; + + // Also check for common type-aware patterns as a fallback + const typeAwarePatterns = [ + /getTypeChecker\(\)/, + /services\.program/, + /context\.sourceCode\.parserServices/, + /getParserServices\(/, + /requiresTypeChecking:\s*true/, + ]; + + // First check the explicit requiresTypeChecking flag + if (metaRegex.test(ruleSource)) { + return true; + } + + // Then check for type-aware patterns + return typeAwarePatterns.some(pattern => pattern.test(ruleSource)); +} + +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error('Usage: pnpm run rulegen '); + console.error('Example: pnpm run rulegen await-thenable'); + process.exit(1); + } + + const ruleName = args[0]; + + try { + // Download and check the rule source first + console.log('Checking if rule requires type information...'); + let ruleSource: string; + try { + ruleSource = await downloadRuleSource(ruleName); + } catch (e) { + throw new Error(`Failed to download rule source: ${e instanceof Error ? e.message : String(e)}`); + } + + const requiresTypeChecking = checkRequiresTypeChecking(ruleSource); + if (!requiresTypeChecking) { + throw new Error( + `Rule "${ruleName}" does not require type checking. ` + + `TSGolint is specifically for type-aware rules. ` + + `Non-type-aware rules should be implemented in oxlint directly.`, + ); + } + + console.log('✅ Rule requires type checking, proceeding...'); + + // Download the test file + const testContent = await downloadTestFile(ruleName); + + // Parse the TypeScript test file + console.log('Parsing test file...'); + let result: ParseResult; + try { + result = parseSync(`${ruleName}.test.ts`, testContent); + } catch (e) { + throw new Error(`Failed to parse TypeScript test file: ${e instanceof Error ? e.message : String(e)}`); + } + + if (!result.program) { + throw new Error('Parser returned no program AST'); + } + + if (result.errors && result.errors.length > 0) { + console.warn(`Warning: Parser reported ${result.errors.length} error(s) while parsing the test file`); + // Still continue - the test file might have intentional syntax errors + } + + // Extract test cases + let testCases: RuleTester; + try { + testCases = extractTestCases(result); + } catch (e) { + throw new Error(`Failed to extract test cases: ${e instanceof Error ? e.message : String(e)}`); + } + + if (testCases.valid.length === 0 && testCases.invalid.length === 0) { + throw new Error('No test cases found. The test file might have an unexpected structure.'); + } + + console.log(`Found ${testCases.valid.length} valid test cases and ${testCases.invalid.length} invalid test cases`); + + // Generate Go files + const goTestContent = generateGoTest(ruleName, testCases); + const goRuleContent = generateGoRuleImplementation(ruleName, testCases); + + // Create output directory + const outputDir = path.join(__dirname, '../../../internal/rules', ruleName.replace(/-/g, '_')); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the Go test file + const testOutputPath = path.join(outputDir, `${ruleName.replace(/-/g, '_')}_test.go`); + fs.writeFileSync(testOutputPath, goTestContent); + console.log(`✅ Generated test file: ${testOutputPath}`); + + // Write the Go rule implementation file (only if it doesn't exist) + const ruleOutputPath = path.join(outputDir, `${ruleName.replace(/-/g, '_')}.go`); + if (fs.existsSync(ruleOutputPath)) { + console.log(`⚠️ Rule implementation already exists: ${ruleOutputPath} (skipping)`); + } else { + fs.writeFileSync(ruleOutputPath, goRuleContent); + console.log(`✅ Generated rule implementation: ${ruleOutputPath}`); + } + } catch (error) { + console.error(`Error: ${error instanceof Error ? error.message : error}`); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/tasks/rulegen/tsconfig.json b/tasks/rulegen/tsconfig.json new file mode 100644 index 00000000..c7220a2e --- /dev/null +++ b/tasks/rulegen/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "node16", + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}