From 7ca39df513b94b5901500b9f4390423c6b3953da Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 12:32:38 -0400 Subject: [PATCH] Revert "Add a lint gate for assertions that fabricate type evidence" This reverts commit 1ff47caf158fbbca4fb104f9ddb471bd451980d3. --- js/oxlint.config.ts | 72 --- js/package.json | 10 +- js/pnpm-lock.yaml | 229 +------- js/tools/oxlint/anti-slop/LICENSE | 21 - js/tools/oxlint/anti-slop/VENDORED_FROM.txt | 14 - js/tools/oxlint/anti-slop/index.ts | 41 -- .../rules/no-chained-type-assertions.ts | 77 --- .../no-conditional-empty-object-spread.ts | 49 -- .../rules/no-known-value-widening.ts | 247 --------- .../anti-slop/rules/no-module-mocking.ts | 91 ---- .../anti-slop/rules/no-object-parameters.ts | 126 ----- .../anti-slop/rules/no-reflect-apply.ts | 28 - .../oxlint/anti-slop/rules/no-reflect-get.ts | 28 - .../anti-slop/rules/no-runtime-typeof.ts | 67 --- .../rules/no-shape-in-symbol-names.ts | 39 -- .../anti-slop/rules/no-unknown-parameters.ts | 83 --- .../anti-slop/rules/no-unknown-returns.ts | 115 ---- .../rules/no-unknown-type-aliases.ts | 70 --- .../rules/no-unsafe-dictionary-type.ts | 134 ----- .../anti-slop/rules/no-widen-then-assert.ts | 366 ------------- ...quire-safety-comment-for-type-assertion.ts | 62 --- .../anti-slop/shared/dictionary-types.ts | 502 ------------------ .../shared/lexical-type-parameters.ts | 61 --- .../oxlint/anti-slop/shared/reflect-method.ts | 35 -- 24 files changed, 3 insertions(+), 2564 deletions(-) delete mode 100644 js/oxlint.config.ts delete mode 100644 js/tools/oxlint/anti-slop/LICENSE delete mode 100644 js/tools/oxlint/anti-slop/VENDORED_FROM.txt delete mode 100644 js/tools/oxlint/anti-slop/index.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-known-value-widening.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-module-mocking.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-object-parameters.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-reflect-apply.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-reflect-get.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-unknown-returns.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts delete mode 100644 js/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts delete mode 100644 js/tools/oxlint/anti-slop/shared/dictionary-types.ts delete mode 100644 js/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts delete mode 100644 js/tools/oxlint/anti-slop/shared/reflect-method.ts diff --git a/js/oxlint.config.ts b/js/oxlint.config.ts deleted file mode 100644 index 0b0d1d68..00000000 --- a/js/oxlint.config.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { defineConfig } from "oxlint"; - -/** - * Lint gate for the TypeScript port. - * - * WHAT THIS IS AND IS NOT - * - * The rules under tools/oxlint/anti-slop are vendored from - * github.com/dmmulroy/anti-slop, which offers fifteen. TWO are enabled. The - * rest were measured against this codebase and rejected, and the reasons are - * worth keeping because "why is this off" is the question a future reader - * will actually have: - * - * no-conditional-empty-object-spread -- 77 hits, all of them correct. - * `...(x !== undefined && { key: x })` is the idiom REQUIRED by - * exactOptionalPropertyTypes, which tsconfig.base.json turns on. The rule - * would fight the compiler settings. - * - * no-unsafe-dictionary-type (123 hits) and no-runtime-typeof (53) -- these - * are overwhelmingly the agent's socket protocol: `Record` for an untrusted message, then explicit `typeof x !== - * "string"` checks per field. That IS boundary validation, which is what - * both rules ask for. Typing the wire message more precisely would be - * false confidence at the one place honesty about untrusted input matters - * most. - * - * no-module-mocking, no-reflect-get, no-reflect-apply -- zero hits. The - * suite already uses real dependency seams: real daemons, real sockets, - * real temp directories. Nothing to enforce. - * - * require-safety-comment-for-type-assertion -- 220 hits. Plausible for this - * codebase, which comments heavily already, but not until the number - * comes down. Revisit. - * - * The two that are on both point at the same thing: an assertion that - * fabricates certainty the code never established. That is the failure mode - * this vault cannot afford, because a wrong belief about a decrypted record's - * shape is a wrong belief about someone's secrets. - */ -export default defineConfig({ - ignorePatterns: [ - "**/dist/**", - "**/node_modules/**", - "packages/test-vectors/fixtures/**", - // The vendored rules are third-party source we do not police. - "tools/oxlint/anti-slop/**", - ], - jsPlugins: [ - { name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" }, - ], - rules: { - // `x as unknown as T` launders an unproven claim into a certainty. Either - // the precise type survives, or the value is parsed at its boundary. - "anti-slop/no-chained-type-assertions": "error", - // Widening a known value and asserting it back is the same move spread - // across two statements. - "anti-slop/no-widen-then-assert": "error", - }, - overrides: [ - { - // Tests may construct deliberately malformed values -- that is what a - // test of a validator IS. Policing them would push toward testing only - // shapes the type system already permits, which is the opposite of the - // point. - files: ["**/test/**", "**/*.test.ts"], - rules: { - "anti-slop/no-chained-type-assertions": "off", - "anti-slop/no-widen-then-assert": "off", - }, - }, - ], -}); diff --git a/js/package.json b/js/package.json index cd352bf9..d5fc584b 100644 --- a/js/package.json +++ b/js/package.json @@ -5,13 +5,7 @@ "description": "TypeScript port of SeedPass (workspace root)", "scripts": { "test": "pnpm -r test", - "typecheck": "pnpm -r typecheck", - "lint": "oxlint" + "typecheck": "pnpm -r typecheck" }, - "packageManager": "pnpm@11.21.0", - "devDependencies": { - "@oxlint/plugins": "1.78.0", - "oxlint": "1.78.0" - }, - "type": "module" + "packageManager": "pnpm@11.21.0" } diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 98d4e593..588830a4 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -6,14 +6,7 @@ settings: importers: - .: - devDependencies: - '@oxlint/plugins': - specifier: 1.78.0 - version: 1.78.0 - oxlint: - specifier: 1.78.0 - version: 1.78.0 + .: {} packages/cli: devDependencies: @@ -327,132 +320,6 @@ packages: resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} engines: {node: '>= 20.19.0'} - '@oxlint/binding-android-arm-eabi@1.78.0': - resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxlint/binding-android-arm64@1.78.0': - resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxlint/binding-darwin-arm64@1.78.0': - resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxlint/binding-darwin-x64@1.78.0': - resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxlint/binding-freebsd-x64@1.78.0': - resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': - resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm-musleabihf@1.78.0': - resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm64-gnu@1.78.0': - resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-arm64-musl@1.78.0': - resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-ppc64-gnu@1.78.0': - resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-riscv64-gnu@1.78.0': - resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-riscv64-musl@1.78.0': - resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-s390x-gnu@1.78.0': - resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-x64-gnu@1.78.0': - resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-x64-musl@1.78.0': - resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxlint/binding-openharmony-arm64@1.78.0': - resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxlint/binding-win32-arm64-msvc@1.78.0': - resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxlint/binding-win32-ia32-msvc@1.78.0': - resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxlint/binding-win32-x64-msvc@1.78.0': - resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxlint/plugins@1.78.0': - resolution: {integrity: sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@rollup/rollup-android-arm-eabi@4.62.4': resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} cpu: [arm] @@ -767,19 +634,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - oxlint@1.78.0: - resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - oxlint-tsgolint: '>=7.0.2001' - vite-plus: '*' - peerDependenciesMeta: - oxlint-tsgolint: - optional: true - vite-plus: - optional: true - parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -1152,65 +1006,6 @@ snapshots: '@noble/hashes@2.3.0': {} - '@oxlint/binding-android-arm-eabi@1.78.0': - optional: true - - '@oxlint/binding-android-arm64@1.78.0': - optional: true - - '@oxlint/binding-darwin-arm64@1.78.0': - optional: true - - '@oxlint/binding-darwin-x64@1.78.0': - optional: true - - '@oxlint/binding-freebsd-x64@1.78.0': - optional: true - - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': - optional: true - - '@oxlint/binding-linux-arm-musleabihf@1.78.0': - optional: true - - '@oxlint/binding-linux-arm64-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-arm64-musl@1.78.0': - optional: true - - '@oxlint/binding-linux-ppc64-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-riscv64-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-riscv64-musl@1.78.0': - optional: true - - '@oxlint/binding-linux-s390x-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-x64-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-x64-musl@1.78.0': - optional: true - - '@oxlint/binding-openharmony-arm64@1.78.0': - optional: true - - '@oxlint/binding-win32-arm64-msvc@1.78.0': - optional: true - - '@oxlint/binding-win32-ia32-msvc@1.78.0': - optional: true - - '@oxlint/binding-win32-x64-msvc@1.78.0': - optional: true - - '@oxlint/plugins@1.78.0': {} - '@rollup/rollup-android-arm-eabi@4.62.4': optional: true @@ -1505,28 +1300,6 @@ snapshots: nanoid@3.3.18: {} - oxlint@1.78.0: - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.78.0 - '@oxlint/binding-android-arm64': 1.78.0 - '@oxlint/binding-darwin-arm64': 1.78.0 - '@oxlint/binding-darwin-x64': 1.78.0 - '@oxlint/binding-freebsd-x64': 1.78.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 - '@oxlint/binding-linux-arm-musleabihf': 1.78.0 - '@oxlint/binding-linux-arm64-gnu': 1.78.0 - '@oxlint/binding-linux-arm64-musl': 1.78.0 - '@oxlint/binding-linux-ppc64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-musl': 1.78.0 - '@oxlint/binding-linux-s390x-gnu': 1.78.0 - '@oxlint/binding-linux-x64-gnu': 1.78.0 - '@oxlint/binding-linux-x64-musl': 1.78.0 - '@oxlint/binding-openharmony-arm64': 1.78.0 - '@oxlint/binding-win32-arm64-msvc': 1.78.0 - '@oxlint/binding-win32-ia32-msvc': 1.78.0 - '@oxlint/binding-win32-x64-msvc': 1.78.0 - parse5@8.0.1: dependencies: entities: 8.0.0 diff --git a/js/tools/oxlint/anti-slop/LICENSE b/js/tools/oxlint/anti-slop/LICENSE deleted file mode 100644 index 69239ead..00000000 --- a/js/tools/oxlint/anti-slop/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Dillon Mulroy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/js/tools/oxlint/anti-slop/VENDORED_FROM.txt b/js/tools/oxlint/anti-slop/VENDORED_FROM.txt deleted file mode 100644 index 9ca91455..00000000 --- a/js/tools/oxlint/anti-slop/VENDORED_FROM.txt +++ /dev/null @@ -1,14 +0,0 @@ -Vendored from https://github.com/dmmulroy/anti-slop -commit: 6d538555cb151d4121ed51a27db81890eacf8ae9 -vendored: 2026-08-20 - -This is a COPY, deliberately. The upstream project describes itself as -something to vendor and own rather than depend on, and its rules are -opinionated -- several of them are wrong for this repository and are not -enabled (see ../../oxlint.config.ts for which two are, and why). - -Editing these files is expected. If you change a rule, say so here. -Upstream's own tests were dropped: we do not run them, and keeping tests we -never execute would be its own kind of slop. - -Local changes since vendoring: none. diff --git a/js/tools/oxlint/anti-slop/index.ts b/js/tools/oxlint/anti-slop/index.ts deleted file mode 100644 index 2b4ae222..00000000 --- a/js/tools/oxlint/anti-slop/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { eslintCompatPlugin } from "@oxlint/plugins"; - -import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts"; -import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts"; -import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts"; -import { noModuleMockingRule } from "./rules/no-module-mocking.ts"; -import { noObjectParametersRule } from "./rules/no-object-parameters.ts"; -import { noReflectApplyRule } from "./rules/no-reflect-apply.ts"; -import { noReflectGetRule } from "./rules/no-reflect-get.ts"; -import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts"; -import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts"; -import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts"; -import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts"; -import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts"; -import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts"; -import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts"; -import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts"; - -/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */ -const antiSlopPlugin = eslintCompatPlugin({ - meta: { name: "anti-slop" }, - rules: { - "no-chained-type-assertions": noChainedTypeAssertionsRule, - "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule, - "no-known-value-widening": noKnownValueWideningRule, - "no-module-mocking": noModuleMockingRule, - "no-object-parameters": noObjectParametersRule, - "no-reflect-apply": noReflectApplyRule, - "no-reflect-get": noReflectGetRule, - "no-runtime-typeof": noRuntimeTypeofRule, - "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule, - "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule, - "no-unknown-parameters": noUnknownParametersRule, - "no-unknown-returns": noUnknownReturnsRule, - "no-unknown-type-aliases": noUnknownTypeAliasesRule, - "no-widen-then-assert": noWidenThenAssertRule, - "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule, - }, -}); - -export default antiSlopPlugin; diff --git a/js/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts b/js/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts deleted file mode 100644 index 0d118527..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; -import type { ESTree } from "@oxlint/plugins"; - -type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion; - -function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression { - return node.type === "TSAsExpression" || node.type === "TSTypeAssertion"; -} - -function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression { - let current = expression; - while (current.type === "ParenthesizedExpression") { - current = current.expression; - } - return current; -} - -function isConstAssertion(node: TypeAssertionExpression): boolean { - const { typeAnnotation } = node; - return ( - typeAnnotation.type === "TSTypeReference" && - typeAnnotation.typeName.type === "Identifier" && - typeAnnotation.typeName.name === "const" - ); -} - -function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean { - let current: ESTree.Expression = node; - let parent = node.parent; - - while (parent.type === "ParenthesizedExpression" && parent.expression === current) { - current = parent; - parent = parent.parent; - } - - return !isTypeAssertionExpression(parent) || parent.expression !== current; -} - -function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean { - let assertionCount = 0; - let hasNonConstAssertion = false; - let current: ESTree.Expression = node; - - while (isTypeAssertionExpression(current)) { - assertionCount += 1; - hasNonConstAssertion ||= !isConstAssertion(current); - current = unwrapParenthesizedExpression(current.expression); - } - - return assertionCount > 1 && hasNonConstAssertion; -} - -/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */ -export const noChainedTypeAssertionsRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.", - }, - messages: { - chained: - "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.", - }, - }, - createOnce(context) { - const checkTypeAssertion = (node: TypeAssertionExpression) => { - if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return; - context.report({ node, messageId: "chained" }); - }; - - return { - TSAsExpression: checkTypeAssertion, - TSTypeAssertion: checkTypeAssertion, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts b/js/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts deleted file mode 100644 index ae7248d3..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; -import type { ESTree } from "@oxlint/plugins"; - -function unwrapParentheses(node: ESTree.Expression): ESTree.Expression { - let current = node; - while (current.type === "ParenthesizedExpression") { - current = current.expression; - } - return current; -} - -function isEmptyObjectExpression(node: ESTree.Expression): boolean { - return node.type === "ObjectExpression" && node.properties.length === 0; -} - -function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean { - const conditional = unwrapParentheses(node); - return ( - conditional.type === "ConditionalExpression" && - (isEmptyObjectExpression(conditional.consequent) || - isEmptyObjectExpression(conditional.alternate)) - ); -} - -/** Ban conditional empty-object spreads without changing their omission semantics. */ -export const noConditionalEmptyObjectSpreadRule = defineRule({ - meta: { - type: "suggestion", - docs: { - description: - "Disallow object spreads that conditionally spread an empty object to omit fields.", - }, - messages: { - avoid: - "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.", - }, - }, - createOnce(context) { - return { - SpreadElement(node) { - if (node.parent.type !== "ObjectExpression") return; - - if (isConditionalEmptyObjectSpread(node.argument)) { - context.report({ node, messageId: "avoid" }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-known-value-widening.ts b/js/tools/oxlint/anti-slop/rules/no-known-value-widening.ts deleted file mode 100644 index 2a6806c6..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import { - classifyWideningTarget, - createTypeEnvironment, - isKnownEvidenceExpression, - type TypeEnvironment, - type WideningTarget, -} from "../shared/dictionary-types.ts"; - -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; - -type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function; - -function unwrapExpression(expression: ESTree.Expression): ESTree.Expression { - let current = expression; - while ( - current.type === "ParenthesizedExpression" || - current.type === "TSAsExpression" || - current.type === "TSSatisfiesExpression" || - current.type === "TSTypeAssertion" || - current.type === "TSNonNullExpression" - ) { - current = current.expression; - } - return current; -} - -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} - -function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null { - if (variable.defs.length !== 1) return null; - const [definition] = variable.defs; - return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" - ? definition.node - : null; -} - -function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean { - return ( - declarator.parent.type === "VariableDeclaration" && - declarator.parent.kind === "const" && - variable.references.every((reference) => reference.init || !reference.isWrite()) - ); -} - -function hasKnownEvidence( - sourceCode: SourceCode, - expression: ESTree.Expression, - visitedVariables = new Set(), -): boolean { - if (isKnownEvidenceExpression(expression)) return true; - const unwrapped = unwrapExpression(expression); - if (unwrapped.type !== "Identifier") return false; - const variable = resolveVariable(sourceCode, unwrapped); - if (variable === null || visitedVariables.has(variable)) return false; - const declarator = variableDeclarator(variable); - if ( - declarator === null || - declarator.init === null || - !isStableConstVariable(variable, declarator) - ) { - return false; - } - visitedVariables.add(variable); - return hasKnownEvidence(sourceCode, declarator.init, visitedVariables); -} - -function annotationTarget( - annotation: ESTree.TSTypeAnnotation | null | undefined, - environment: TypeEnvironment, -): WideningTarget | null { - return annotation === null || annotation === undefined - ? null - : classifyWideningTarget(annotation.typeAnnotation, environment); -} - -function enclosingFunction(node: ESTree.Node): FunctionExpression | null { - let current: ESTree.Node | null = node.parent; - while (current !== null && current.type !== "Program") { - if ( - current.type === "ArrowFunctionExpression" || - current.type === "FunctionDeclaration" || - current.type === "FunctionExpression" - ) { - return current; - } - current = current.parent; - } - return null; -} - -function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string { - if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name; - if (key.type === "Literal") return String(key.value); - return sourceCode.getText(key); -} - -function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string { - if (owner === null) return "anonymous function"; - if (owner.id !== null) return owner.id.name; - const parent = owner.parent; - if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") - return parent.id.name; - if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key); - return "anonymous function"; -} - -function isEmptyObjectExpression(expression: ESTree.Expression): boolean { - const unwrapped = unwrapExpression(expression); - return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0; -} - -function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean { - return destination.kind === "open dictionary" || destination.kind === "generic container"; -} - -function hasParentAssertion(node: ESTree.Node): boolean { - return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion"; -} - -/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */ -export const noKnownValueWideningRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.", - }, - messages: { - widening: - "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.", - }, - }, - createOnce(context) { - let environment: TypeEnvironment | null = null; - - const reportFlow = ( - expression: ESTree.Expression, - destination: WideningTarget | null, - subject: string, - ) => { - if (destination === null) return; - if ( - isDictionaryAccumulatorTarget(destination) && - isEmptyObjectExpression(expression) - ) { - return; - } - if (!hasKnownEvidence(context.sourceCode, expression)) return; - context.report({ - node: expression, - messageId: "widening", - data: { subject, target: destination.kind }, - }); - }; - - const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) => - environment === null ? null : annotationTarget(annotation, environment); - - return { - Program(node) { - environment = createTypeEnvironment(node); - }, - VariableDeclarator(node) { - if (node.init === null || node.id.type !== "Identifier") return; - reportFlow( - node.init, - targetFromAnnotation(node.id.typeAnnotation), - `binding \`${node.id.name}\``, - ); - }, - PropertyDefinition(node) { - if (node.value === null) return; - reportFlow( - node.value, - targetFromAnnotation(node.typeAnnotation), - `property \`${sourceKeyName(context.sourceCode, node.key)}\``, - ); - }, - AccessorProperty(node) { - if (node.value === null) return; - reportFlow( - node.value, - targetFromAnnotation(node.typeAnnotation), - `property \`${sourceKeyName(context.sourceCode, node.key)}\``, - ); - }, - AssignmentExpression(node) { - if (node.operator !== "=" || node.left.type !== "Identifier") return; - const variable = resolveVariable(context.sourceCode, node.left); - if (variable === null) return; - const declarator = variableDeclarator(variable); - if (declarator === null || declarator.id.type !== "Identifier") return; - reportFlow( - node.right, - targetFromAnnotation(declarator.id.typeAnnotation), - `binding \`${declarator.id.name}\``, - ); - }, - ReturnStatement(node) { - if (node.argument === null) return; - const owner = enclosingFunction(node); - reportFlow( - node.argument, - targetFromAnnotation(owner?.returnType), - `return value of \`${functionName(context.sourceCode, owner)}\``, - ); - }, - ArrowFunctionExpression(node) { - if (node.body.type === "BlockStatement") return; - reportFlow( - node.body, - targetFromAnnotation(node.returnType), - `return value of \`${functionName(context.sourceCode, node)}\``, - ); - }, - TSAsExpression(node) { - if (environment === null || hasParentAssertion(node)) return; - reportFlow( - node.expression, - classifyWideningTarget(node.typeAnnotation, environment), - "assertion", - ); - }, - TSTypeAssertion(node) { - if (environment === null || hasParentAssertion(node)) return; - reportFlow( - node.expression, - classifyWideningTarget(node.typeAnnotation, environment), - "assertion", - ); - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-module-mocking.ts b/js/tools/oxlint/anti-slop/rules/no-module-mocking.ts deleted file mode 100644 index d6fb5b45..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-module-mocking.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; - -const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]); - -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} - -function importedName(node: ESTree.Node): string | null { - if (node.type !== "ImportSpecifier") return null; - return node.imported.type === "Identifier" ? node.imported.name : node.imported.value; -} - -function isTestFrameworkObject( - sourceCode: SourceCode, - expression: ESTree.Expression, -): expression is ESTree.IdentifierReference { - if (expression.type !== "Identifier") return false; - if ( - (expression.name === "vi" || expression.name === "jest") && - sourceCode.isGlobalReference(expression) - ) { - return true; - } - - const variable = resolveVariable(sourceCode, expression); - if (variable === null || variable.defs.length === 0) { - return expression.name === "vi" || expression.name === "jest"; - } - return variable.defs.some((definition) => { - if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") { - return false; - } - const source = definition.parent.source.value; - const name = importedName(definition.node); - return (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest"); - }); -} - -function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean { - if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false; - if (!isTestFrameworkObject(sourceCode, callee.object)) return false; - const property = callee.property; - const method = callee.computed - ? property.type === "Literal" && - (property.value === "doMock" || - property.value === "mock" || - property.value === "unstable_mockModule") - ? property.value - : null - : property.type === "Identifier" - ? property.name - : null; - return method !== null && moduleMockMethods.has(method); -} - -/** Ban test framework module mocking in favor of real dependency seams. */ -export const noModuleMockingRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.", - }, - messages: { - moduleMock: - "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.", - }, - }, - createOnce(context) { - return { - CallExpression(node) { - if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; - if (moduleMockCall(context.sourceCode, node.callee)) { - context.report({ node, messageId: "moduleMock" }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-object-parameters.ts b/js/tools/oxlint/anti-slop/rules/no-object-parameters.ts deleted file mode 100644 index 29b990f3..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-object-parameters.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree, SourceCode } from "@oxlint/plugins"; - -import { lexicalTypeParameterNames } from "../shared/lexical-type-parameters.ts"; - -type Parameter = ESTree.ParamPattern; -type ParameterOwner = - | ESTree.ArrowFunctionExpression - | ESTree.Function - | ESTree.TSCallSignatureDeclaration - | ESTree.TSConstructSignatureDeclaration - | ESTree.TSConstructorType - | ESTree.TSFunctionType - | ESTree.TSMethodSignature; - -function parameterAnnotation(parameter: Parameter): ESTree.TSTypeAnnotation | null | undefined { - if (parameter.type === "TSParameterProperty") { - return parameterAnnotation(parameter.parameter); - } - if (parameter.type === "RestElement") { - return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument); - } - if (parameter.type === "AssignmentPattern") { - return parameter.typeAnnotation ?? parameter.left.typeAnnotation; - } - return parameter.typeAnnotation; -} - -function parameterName(parameter: Parameter, sourceCode: SourceCode): string { - return parameter.type === "Identifier" - ? parameter.name - : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, ""); -} - -/** Ban the broad object type on function inputs, including local aliases to object. */ -export const noObjectParametersRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.", - }, - messages: { - objectParameter: - "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.", - }, - }, - createOnce(context) { - const aliases = new Map(); - - const resolvesToObject = ( - type: ESTree.TSType, - shadowedAliases: ReadonlySet, - visited = new Set(), - ): boolean => { - if (type.type === "TSObjectKeyword") return true; - if (type.type === "TSParenthesizedType") - return resolvesToObject(type.typeAnnotation, shadowedAliases, visited); - if (type.type === "TSUnionType") { - return type.types.some((member) => - resolvesToObject(member, shadowedAliases, visited), - ); - } - if ( - type.type !== "TSTypeReference" || - type.typeName.type !== "Identifier" || - (type.typeArguments !== null && - type.typeArguments !== undefined && - type.typeArguments.params.length > 0) || - visited.has(type.typeName.name) || - shadowedAliases.has(type.typeName.name) - ) { - return false; - } - const alias = aliases.get(type.typeName.name); - if (alias === undefined) return false; - const nextVisited = new Set(visited); - nextVisited.add(type.typeName.name); - return resolvesToObject(alias, shadowedAliases, nextVisited); - }; - - const checkParameters = (node: ParameterOwner) => { - const shadowedAliases = lexicalTypeParameterNames( - node, - context.sourceCode.visitorKeys, - ); - for (const parameter of node.params) { - const annotation = parameterAnnotation(parameter); - if (annotation === null || annotation === undefined) continue; - if (!resolvesToObject(annotation.typeAnnotation, shadowedAliases)) continue; - context.report({ - node: annotation.typeAnnotation, - messageId: "objectParameter", - data: { parameter: parameterName(parameter, context.sourceCode) }, - }); - } - }; - - return { - Program(node) { - aliases.clear(); - for (const statement of node.body) { - const declaration = - statement.type === "ExportNamedDeclaration" ? statement.declaration : statement; - if ( - declaration?.type === "TSTypeAliasDeclaration" && - (declaration.typeParameters === null || declaration.typeParameters === undefined) - ) { - aliases.set(declaration.id.name, declaration.typeAnnotation); - } - } - }, - ArrowFunctionExpression: checkParameters, - FunctionDeclaration: checkParameters, - FunctionExpression: checkParameters, - TSCallSignatureDeclaration: checkParameters, - TSConstructSignatureDeclaration: checkParameters, - TSConstructorType: checkParameters, - TSDeclareFunction: checkParameters, - TSEmptyBodyFunctionExpression: checkParameters, - TSFunctionType: checkParameters, - TSMethodSignature: checkParameters, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-reflect-apply.ts b/js/tools/oxlint/anti-slop/rules/no-reflect-apply.ts deleted file mode 100644 index 2cc30451..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts"; - -/** Ban Reflect.apply, which bypasses ordinary typed function calls. */ -export const noReflectApplyRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.", - }, - messages: { - reflectApply: - "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.", - }, - }, - createOnce(context) { - return { - CallExpression(node) { - if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; - if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) { - context.report({ node, messageId: "reflectApply" }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-reflect-get.ts b/js/tools/oxlint/anti-slop/rules/no-reflect-get.ts deleted file mode 100644 index cf630ecc..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-reflect-get.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts"; - -/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */ -export const noReflectGetRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.", - }, - messages: { - reflectGet: - "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.", - }, - }, - createOnce(context) { - return { - CallExpression(node) { - if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; - if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) { - context.report({ node, messageId: "reflectGet" }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts b/js/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts deleted file mode 100644 index 6a25c247..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree } from "@oxlint/plugins"; - -type RuntimeFunction = ESTree.ArrowFunctionExpression | ESTree.Function; - -function isRuntimeFunction(node: ESTree.Node): node is RuntimeFunction { - return ( - node.type === "ArrowFunctionExpression" || - node.type === "FunctionDeclaration" || - node.type === "FunctionExpression" - ); -} - -function isInsideTypeGuard(node: ESTree.Node): boolean { - let current: ESTree.Node | null = node.parent; - while (current !== null && current.type !== "Program") { - if (isRuntimeFunction(current)) { - return current.returnType?.typeAnnotation.type === "TSTypePredicate"; - } - current = current.parent; - } - return false; -} - -/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */ -export const noRuntimeTypeofRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.", - }, - messages: { - runtimeTypeof: - "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.", - }, - schema: [ - { - type: "object", - properties: { - allowInTypeGuards: { type: "boolean" }, - }, - additionalProperties: false, - }, - ], - defaultOptions: [{ allowInTypeGuards: false }], - }, - createOnce(context) { - return { - UnaryExpression(node) { - const option = context.options?.[0]; - const allowInTypeGuards = - typeof option === "object" && - option !== null && - !Array.isArray(option) && - option.allowInTypeGuards === true; - if ( - node.operator === "typeof" && - (!allowInTypeGuards || !isInsideTypeGuard(node)) - ) { - context.report({ node, messageId: "runtimeTypeof" }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts b/js/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts deleted file mode 100644 index afc00dd4..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; -import type { ESTree } from "@oxlint/plugins"; - -const FORBIDDEN_SYMBOL_NAME = "shape"; - -function containsForbiddenSymbolName(name: string): boolean { - return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME); -} - -/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */ -export const noForbiddenTermInSymbolNamesRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - 'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.', - }, - messages: { - forbiddenSymbolName: - 'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.', - }, - }, - createOnce(context) { - const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => { - if (!containsForbiddenSymbolName(node.name)) return; - context.report({ - node, - messageId: "forbiddenSymbolName", - data: { name: node.name }, - }); - }; - - return { - Identifier: reportForbiddenSymbolName, - PrivateIdentifier: reportForbiddenSymbolName, - JSXIdentifier: reportForbiddenSymbolName, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts b/js/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts deleted file mode 100644 index cdc6c235..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; -import type { ESTree } from "@oxlint/plugins"; - -type Parameter = ESTree.ParamPattern; -type ParameterOwner = - | ESTree.ArrowFunctionExpression - | ESTree.Function - | ESTree.TSCallSignatureDeclaration - | ESTree.TSConstructSignatureDeclaration - | ESTree.TSConstructorType - | ESTree.TSFunctionType - | ESTree.TSMethodSignature; - -function parameterAnnotation(parameter: Parameter): ESTree.TSTypeAnnotation | null | undefined { - if (parameter.type === "TSParameterProperty") { - return parameterAnnotation(parameter.parameter); - } - if (parameter.type === "RestElement") { - return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument); - } - if (parameter.type === "AssignmentPattern") { - return parameter.typeAnnotation ?? parameter.left.typeAnnotation; - } - return parameter.typeAnnotation; -} - -function parameterName(parameter: Parameter, sourceText: string): string { - if (parameter.type === "TSParameterProperty") { - return parameterName(parameter.parameter, sourceText); - } - if (parameter.type === "AssignmentPattern") { - return parameterName(parameter.left, sourceText); - } - if (parameter.type === "RestElement") { - return parameterName(parameter.argument, sourceText); - } - return parameter.type === "Identifier" - ? parameter.name - : sourceText.replace(/\s*:\s*unknown\s*$/u, ""); -} - -/** Disallow unknown inputs except explicitly named error-cause enrichment. */ -export const noUnknownParametersRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow explicitly unknown function parameters except `cause`; decode unknown input at its I/O boundary instead.", - }, - messages: { - unknownParameter: - "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.", - }, - }, - createOnce(context) { - const checkParameters = (node: ParameterOwner) => { - for (const parameter of node.params) { - const annotation = parameterAnnotation(parameter); - if (annotation?.typeAnnotation.type !== "TSUnknownKeyword") continue; - const name = parameterName(parameter, context.sourceCode.getText(parameter)); - if (name === "cause") continue; - context.report({ - node: annotation.typeAnnotation, - messageId: "unknownParameter", - data: { parameter: name }, - }); - } - }; - - return { - ArrowFunctionExpression: checkParameters, - FunctionDeclaration: checkParameters, - FunctionExpression: checkParameters, - TSCallSignatureDeclaration: checkParameters, - TSConstructSignatureDeclaration: checkParameters, - TSConstructorType: checkParameters, - TSDeclareFunction: checkParameters, - TSEmptyBodyFunctionExpression: checkParameters, - TSFunctionType: checkParameters, - TSMethodSignature: checkParameters, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-unknown-returns.ts b/js/tools/oxlint/anti-slop/rules/no-unknown-returns.ts deleted file mode 100644 index 4b16d6ef..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree } from "@oxlint/plugins"; - -import { lexicalTypeParameterNames } from "../shared/lexical-type-parameters.ts"; - -type FunctionWithReturnType = - | ESTree.ArrowFunctionExpression - | ESTree.Function - | ESTree.TSCallSignatureDeclaration - | ESTree.TSConstructSignatureDeclaration - | ESTree.TSConstructorType - | ESTree.TSFunctionType - | ESTree.TSMethodSignature; - -function referencedAliasName(type: ESTree.TSType): string | null { - if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation); - if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null; - return type.typeArguments === null || - type.typeArguments === undefined || - type.typeArguments.params.length === 0 - ? type.typeName.name - : null; -} - -/** Ban function contracts that return unknown instead of a parsed domain type. */ -export const noUnknownReturnsRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow functions whose explicit return contract is unknown or Promise.", - }, - messages: { - unknownReturn: - "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.", - }, - }, - createOnce(context) { - const aliases = new Map(); - - const resolvesToUnknown = ( - type: ESTree.TSType, - shadowedAliases: ReadonlySet, - visited = new Set(), - ): boolean => { - if (type.type === "TSUnknownKeyword") return true; - if (type.type === "TSParenthesizedType") { - return resolvesToUnknown(type.typeAnnotation, shadowedAliases, visited); - } - if (type.type === "TSUnionType") { - return type.types.some((member) => - resolvesToUnknown(member, shadowedAliases, visited), - ); - } - if ( - type.type === "TSTypeReference" && - type.typeName.type === "Identifier" && - (type.typeName.name === "Promise" || type.typeName.name === "PromiseLike") - ) { - const value = type.typeArguments?.params[0]; - return value !== undefined && resolvesToUnknown(value, shadowedAliases, visited); - } - const name = referencedAliasName(type); - if (name === null || visited.has(name) || shadowedAliases.has(name)) return false; - const alias = aliases.get(name); - if ( - alias === undefined || - (alias.typeParameters !== null && alias.typeParameters !== undefined) - ) { - return false; - } - const nextVisited = new Set(visited); - nextVisited.add(name); - return resolvesToUnknown(alias.typeAnnotation, shadowedAliases, nextVisited); - }; - - const checkReturnType = (node: FunctionWithReturnType) => { - const annotation = node.returnType; - if (annotation === null || annotation === undefined) return; - if ( - !resolvesToUnknown( - annotation.typeAnnotation, - lexicalTypeParameterNames(node, context.sourceCode.visitorKeys), - ) - ) { - return; - } - context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" }); - }; - - return { - Program(node) { - aliases.clear(); - for (const statement of node.body) { - const declaration = - statement.type === "ExportNamedDeclaration" ? statement.declaration : statement; - if (declaration?.type === "TSTypeAliasDeclaration") { - aliases.set(declaration.id.name, declaration); - } - } - }, - ArrowFunctionExpression: checkReturnType, - FunctionDeclaration: checkReturnType, - FunctionExpression: checkReturnType, - TSCallSignatureDeclaration: checkReturnType, - TSConstructSignatureDeclaration: checkReturnType, - TSConstructorType: checkReturnType, - TSDeclareFunction: checkReturnType, - TSEmptyBodyFunctionExpression: checkReturnType, - TSFunctionType: checkReturnType, - TSMethodSignature: checkReturnType, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts b/js/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts deleted file mode 100644 index 3e328fdf..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree } from "@oxlint/plugins"; - -function referencedAliasName(type: ESTree.TSType): string | null { - if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation); - if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null; - return type.typeArguments === null || - type.typeArguments === undefined || - type.typeArguments.params.length === 0 - ? type.typeName.name - : null; -} - -/** Ban named aliases that merely conceal TypeScript's unknown top type. */ -export const noUnknownTypeAliasesRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.", - }, - messages: { - unknownAlias: - "Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.", - }, - }, - createOnce(context) { - const aliases = new Map(); - - const resolvesToUnknown = (type: ESTree.TSType, visited = new Set()): boolean => { - if (type.type === "TSUnknownKeyword") return true; - if (type.type === "TSParenthesizedType") - return resolvesToUnknown(type.typeAnnotation, visited); - const name = referencedAliasName(type); - if (name === null || visited.has(name)) return false; - const alias = aliases.get(name); - if ( - alias === undefined || - (alias.typeParameters !== null && alias.typeParameters !== undefined) - ) { - return false; - } - const nextVisited = new Set(visited); - nextVisited.add(name); - return resolvesToUnknown(alias.typeAnnotation, nextVisited); - }; - - return { - Program(node) { - aliases.clear(); - for (const statement of node.body) { - const declaration = - statement.type === "ExportNamedDeclaration" ? statement.declaration : statement; - if (declaration?.type === "TSTypeAliasDeclaration") { - aliases.set(declaration.id.name, declaration); - } - } - for (const alias of aliases.values()) { - if (!resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name]))) continue; - context.report({ - node: alias.id, - messageId: "unknownAlias", - data: { alias: alias.id.name }, - }); - } - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts b/js/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts deleted file mode 100644 index 8c45eed2..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import { - classifyUnsafeDictionary, - classifyUnsafeDictionaryValue, - createTypeEnvironment, - type TypeEnvironment, -} from "../shared/dictionary-types.ts"; - -import type { ESTree } from "@oxlint/plugins"; - -const typeNodeKinds: ReadonlySet = new Set([ - "JSDocNonNullableType", - "JSDocNullableType", - "JSDocUnknownType", - "TSAnyKeyword", - "TSArrayType", - "TSBigIntKeyword", - "TSBooleanKeyword", - "TSConditionalType", - "TSConstructorType", - "TSFunctionType", - "TSImportType", - "TSIndexedAccessType", - "TSInferType", - "TSIntersectionType", - "TSIntrinsicKeyword", - "TSLiteralType", - "TSMappedType", - "TSNamedTupleMember", - "TSNeverKeyword", - "TSNullKeyword", - "TSNumberKeyword", - "TSObjectKeyword", - "TSParenthesizedType", - "TSStringKeyword", - "TSSymbolKeyword", - "TSTemplateLiteralType", - "TSThisType", - "TSTupleType", - "TSTypeLiteral", - "TSTypeOperator", - "TSTypePredicate", - "TSTypeQuery", - "TSTypeReference", - "TSUndefinedKeyword", - "TSUnionType", - "TSUnknownKeyword", - "TSVoidKeyword", -]); - -function isTypeNode(node: ESTree.Node): node is ESTree.TSType { - return typeNodeKinds.has(node.type); -} - -function typeReferenceName(type: ESTree.TSTypeReference): string | null { - return type.typeName.type === "Identifier" ? type.typeName.name : null; -} - -function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean { - let current: ESTree.Node | null = node.parent; - while (current !== null && current.type !== "Program") { - if (current.type === "TSTypeAliasDeclaration") return true; - current = current.parent; - } - return false; -} - -function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean { - if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false; - const name = typeReferenceName(node); - return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node); -} - -function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean { - if (isPlainAliasConsumerUse(node, environment)) return false; - if (classifyUnsafeDictionary(node, environment) === null) return false; - let current: ESTree.Node | null = node.parent; - while (current !== null && current.type !== "Program") { - if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) - return false; - current = current.parent; - } - return true; -} - -/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */ -export const noUnsafeDictionaryTypeRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.", - }, - messages: { - unsafeDictionary: - "This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.", - }, - }, - createOnce(context) { - let environment: TypeEnvironment | null = null; - const report = (node: ESTree.Node, value: string) => { - context.report({ node, messageId: "unsafeDictionary", data: { value } }); - }; - const reportIfUnsafe = (node: ESTree.TSType) => { - if (environment === null || !shouldReportType(node, environment)) return; - const unsafe = classifyUnsafeDictionary(node, environment); - if (unsafe === null) return; - report(node, unsafe.unsafeValue); - }; - - return { - Program(node) { - environment = createTypeEnvironment(node); - }, - TSTypeReference: reportIfUnsafe, - TSTypeLiteral: reportIfUnsafe, - TSMappedType: reportIfUnsafe, - TSIndexSignature(node) { - if ( - environment === null || - node.typeAnnotation === null || - node.parent.type === "TSTypeLiteral" - ) - return; - const unsafe = classifyUnsafeDictionaryValue( - node.typeAnnotation.typeAnnotation, - environment, - ); - if (unsafe !== null) report(node, unsafe.unsafeValue); - }, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts b/js/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts deleted file mode 100644 index c5e07f7f..00000000 --- a/js/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; -import type { ESTree, Variable } from "@oxlint/plugins"; - -type BroadTypeKind = "top" | "object" | "record"; - -type KnownValueEvidence = { - readonly type: ESTree.TSType | null; -}; - -const functionBoundaryTypes = new Set([ - "ArrowFunctionExpression", - "FunctionDeclaration", - "FunctionExpression", - "TSDeclareFunction", - "TSEmptyBodyFunctionExpression", -]); - -function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression { - let current = expression; - while (current.type === "ParenthesizedExpression") current = current.expression; - return current; -} - -function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType { - let current = type; - while (current.type === "TSParenthesizedType") current = current.typeAnnotation; - return current; -} - -function typeReferenceName(type: ESTree.TSTypeReference): string | null { - return type.typeName.type === "Identifier" ? type.typeName.name : null; -} - -function isUnknownOrAnyType(type: ESTree.TSType): boolean { - const unwrapped = unwrapTypeParentheses(type); - return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword"; -} - -function isBroadRecordKeyType(type: ESTree.TSType): boolean { - const unwrapped = unwrapTypeParentheses(type); - if ( - unwrapped.type === "TSStringKeyword" || - unwrapped.type === "TSNumberKeyword" || - unwrapped.type === "TSSymbolKeyword" - ) { - return true; - } - if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType); - return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey"; -} - -function isBroadRecordType(type: ESTree.TSType): boolean { - const unwrapped = unwrapTypeParentheses(type); - - if (unwrapped.type === "TSTypeReference") { - if (typeReferenceName(unwrapped) === "Readonly") { - const [inner] = unwrapped.typeArguments?.params ?? []; - return inner !== undefined && isBroadRecordType(inner); - } - - if (typeReferenceName(unwrapped) !== "Record") return false; - const parameters = unwrapped.typeArguments?.params ?? []; - return ( - parameters.length === 2 && - parameters[0] !== undefined && - parameters[1] !== undefined && - isBroadRecordKeyType(parameters[0]) && - isUnknownOrAnyType(parameters[1]) - ); - } - - if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false; - const [member] = unwrapped.members; - const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : []; - return ( - member?.type === "TSIndexSignature" && - member.parameters.length === 1 && - parameter !== undefined && - isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && - isUnknownOrAnyType(member.typeAnnotation.typeAnnotation) - ); -} - -function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null { - const unwrapped = unwrapTypeParentheses(type); - if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top"; - if (unwrapped.type === "TSObjectKeyword") return "object"; - return isBroadRecordType(unwrapped) ? "record" : null; -} - -function assertedExpression( - node: ESTree.TSAsExpression | ESTree.TSTypeAssertion, -): ESTree.Expression { - return unwrapExpressionParentheses(node.expression); -} - -function assertionFromExpression( - expression: ESTree.Expression, -): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null { - const unwrapped = unwrapExpressionParentheses(expression); - return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" - ? unwrapped - : null; -} - -function normalizedTypeText(sourceText: string, type: ESTree.TSType): string { - return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, ""); -} - -function typesHaveSameSyntax( - sourceText: string, - left: ESTree.TSType | null, - right: ESTree.TSType, -): boolean { - return ( - left !== null && - normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === - normalizedTypeText(sourceText, unwrapTypeParentheses(right)) - ); -} - -function isDefinitelyObjectType(type: ESTree.TSType): boolean { - const unwrapped = unwrapTypeParentheses(type); - switch (unwrapped.type) { - case "TSArrayType": - case "TSConstructorType": - case "TSFunctionType": - case "TSMappedType": - case "TSObjectKeyword": - case "TSTupleType": - return true; - case "TSTypeLiteral": - return unwrapped.members.length > 0; - case "TSIntersectionType": - return unwrapped.types.every(isDefinitelyObjectType); - case "TSTypeOperator": - return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation); - default: - return false; - } -} - -function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean { - const unwrapped = unwrapTypeParentheses(type); - if (unwrapped.type === "TSTypeLiteral") { - return unwrapped.members.some((member) => member.type !== "TSIndexSignature"); - } - - if (unwrapped.type !== "TSTypeReference") return false; - if (typeReferenceName(unwrapped) === "Readonly") { - const [inner] = unwrapped.typeArguments?.params ?? []; - return inner !== undefined && isDefinitelyNarrowerRecordType(inner); - } - if (typeReferenceName(unwrapped) !== "Record") return false; - - const parameters = unwrapped.typeArguments?.params ?? []; - return ( - parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1]) - ); -} - -function functionBoundary(node: ESTree.Node): ESTree.Node | null { - let current = node.parent; - while (current !== null && current.type !== "Program") { - if (functionBoundaryTypes.has(current.type)) return current; - current = current.parent; - } - return null; -} - -function resolvedVariableForIdentifier( - scopes: readonly { - readonly references: readonly { - readonly identifier: ESTree.Node; - readonly resolved: Variable | null; - }[]; - }[], - identifier: ESTree.IdentifierReference, -): Variable | null { - for (const scope of scopes) { - const reference = scope.references.find( - (candidate) => - candidate.identifier.start === identifier.start && - candidate.identifier.end === identifier.end, - ); - if (reference !== undefined) return reference.resolved; - } - return null; -} - -function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null { - for (const definition of variable.defs) { - if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") { - return definition.node; - } - } - return null; -} - -function knownValueEvidence( - expression: ESTree.Expression, - scopes: Parameters[0], - boundary: ESTree.Node | null, - visitedVariables: ReadonlySet, -): KnownValueEvidence | null { - const unwrapped = unwrapExpressionParentheses(expression); - - if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") { - if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null; - return { type: unwrapped.typeAnnotation }; - } - - if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") { - return { type: null }; - } - - if ( - unwrapped.type === "ArrayExpression" || - unwrapped.type === "ArrowFunctionExpression" || - unwrapped.type === "ClassExpression" || - unwrapped.type === "FunctionExpression" || - unwrapped.type === "NewExpression" || - unwrapped.type === "ObjectExpression" - ) { - return { type: null }; - } - - if (unwrapped.type !== "Identifier") return null; - const variable = resolvedVariableForIdentifier(scopes, unwrapped); - if (variable === null || visitedVariables.has(variable)) return null; - - const annotatedIdentifier = variable.identifiers.find( - (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined, - ); - const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation; - if (annotation !== undefined && annotatedIdentifier !== undefined) { - if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) { - return null; - } - return { type: annotation }; - } - - const declarator = variableDeclarator(variable); - if ( - declarator === null || - declarator.parent.type !== "VariableDeclaration" || - declarator.parent.kind !== "const" || - declarator.init === null || - variable.references.some((reference) => reference.isWrite() && !reference.init) || - functionBoundary(declarator) !== boundary - ) { - return null; - } - - return knownValueEvidence( - declarator.init, - scopes, - boundary, - new Set([...visitedVariables, variable]), - ); -} - -function widenedBinding( - variable: Variable, - scopes: Parameters[0], -): { - readonly broadKind: BroadTypeKind; - readonly evidence: KnownValueEvidence; - readonly declaredAt: number; - readonly boundary: ESTree.Node | null; -} | null { - const declarator = variableDeclarator(variable); - if ( - declarator === null || - declarator.parent.type !== "VariableDeclaration" || - declarator.parent.kind !== "const" || - declarator.id.type !== "Identifier" || - declarator.init === null || - variable.references.some((reference) => reference.isWrite() && !reference.init) - ) { - return null; - } - - const boundary = functionBoundary(declarator); - const declaredType = declarator.id.typeAnnotation?.typeAnnotation; - const initializerAssertion = assertionFromExpression(declarator.init); - const initializerBroadKind = - initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation); - const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType); - const broadKind = declaredBroadKind ?? initializerBroadKind; - if (broadKind === null) return null; - - const originalExpression = - initializerAssertion !== null && initializerBroadKind !== null - ? assertedExpression(initializerAssertion) - : declarator.init; - const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])); - return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary }; -} - -function assertionIsNarrower( - sourceText: string, - broadKind: BroadTypeKind, - evidence: KnownValueEvidence, - assertedType: ESTree.TSType, -): boolean { - if (broadTypeKind(assertedType) !== null) return false; - if (broadKind === "top") return true; - if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true; - if (broadKind === "object") return isDefinitelyObjectType(assertedType); - return isDefinitelyNarrowerRecordType(assertedType); -} - -/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */ -export const noWidenThenAssertRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.", - }, - messages: { - widenThenAssert: - 'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.', - }, - }, - createOnce(context) { - let scopes: Parameters[0] = []; - - const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => { - const expression = assertedExpression(node); - if (expression.type !== "Identifier") return; - - const variable = resolvedVariableForIdentifier(scopes, expression); - if (variable === null) return; - const widened = widenedBinding(variable, scopes); - if ( - widened === null || - node.start <= widened.declaredAt || - functionBoundary(node) !== widened.boundary || - !assertionIsNarrower( - context.sourceCode.text, - widened.broadKind, - widened.evidence, - node.typeAnnotation, - ) - ) { - return; - } - - context.report({ - node, - messageId: "widenThenAssert", - data: { name: expression.name }, - }); - }; - - return { - Program() { - scopes = context.sourceCode.scopeManager.scopes; - }, - TSAsExpression: checkAssertion, - TSTypeAssertion: checkAssertion, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts b/js/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts deleted file mode 100644 index f1a2ffcf..00000000 --- a/js/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { defineRule } from "@oxlint/plugins"; - -import type { ESTree, SourceCode } from "@oxlint/plugins"; - -type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion; - -const commentOwnerKinds = new Set([ - "ExpressionStatement", - "PropertyDefinition", - "ReturnStatement", - "ThrowStatement", - "VariableDeclaration", -]); - -function isConstAssertion(node: TypeAssertion): boolean { - return ( - node.typeAnnotation.type === "TSTypeReference" && - node.typeAnnotation.typeName.type === "Identifier" && - node.typeAnnotation.typeName.name === "const" - ); -} - -function hasSafetyComment(sourceCode: SourceCode, node: TypeAssertion): boolean { - let current: ESTree.Node = node; - while (true) { - if ( - sourceCode - .getCommentsBefore(current) - .some((comment) => comment.end <= node.start && /\bSAFETY\s*:/u.test(comment.value)) - ) { - return true; - } - if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false; - current = current.parent; - } -} - -/** Require every non-const type assertion to state the invariant TypeScript cannot express. */ -export const requireSafetyCommentForTypeAssertionRule = defineRule({ - meta: { - type: "problem", - docs: { - description: - "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.", - }, - messages: { - missingSafetyComment: - "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement.", - }, - }, - createOnce(context) { - const checkAssertion = (node: TypeAssertion) => { - if (isConstAssertion(node) || hasSafetyComment(context.sourceCode, node)) return; - context.report({ node, messageId: "missingSafetyComment" }); - }; - - return { - TSAsExpression: checkAssertion, - TSTypeAssertion: checkAssertion, - }; - }, -}); diff --git a/js/tools/oxlint/anti-slop/shared/dictionary-types.ts b/js/tools/oxlint/anti-slop/shared/dictionary-types.ts deleted file mode 100644 index 86517004..00000000 --- a/js/tools/oxlint/anti-slop/shared/dictionary-types.ts +++ /dev/null @@ -1,502 +0,0 @@ -import type { ESTree } from "@oxlint/plugins"; - -const BUILT_INS = new Set([ - "Record", - "Readonly", - "Partial", - "Required", - "Pick", - "Omit", - "PropertyKey", - "NonNullable", -]); -const TRANSPARENT_WRAPPERS = new Set(["Readonly", "Partial", "Required", "NonNullable"]); - -type TypeAliasEnvironment = ReadonlyMap; - -type ResolvedType = { - readonly type: ESTree.TSType; - readonly substitutions: TypeAliasEnvironment; -}; - -export type UnsafeDictionary = { - readonly kind: "unsafe-dictionary"; - readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown"; -}; - -export type WideningTargetKind = - | "anonymous object" - | "generic container" - | "object" - | "open dictionary" - | "unknown"; - -export type WideningTarget = { - readonly kind: WideningTargetKind; -}; - -export type TypeEnvironment = { - readonly aliases: ReadonlyMap; - readonly interfaces: ReadonlyMap; - readonly shadowedBuiltIns: ReadonlySet; -}; - -function declaredStatement(statement: ESTree.Statement): ESTree.Node | null { - return statement.type === "ExportNamedDeclaration" || - statement.type === "ExportDefaultDeclaration" - ? (statement.declaration ?? null) - : statement; -} - -export function createTypeEnvironment(program: ESTree.Program): TypeEnvironment { - const aliases = new Map(); - const interfaces = new Map(); - const shadowedBuiltIns = new Set(); - - for (const statement of program.body) { - const declaration = declaredStatement(statement); - if (declaration?.type === "ImportDeclaration") { - for (const specifier of declaration.specifiers) { - if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name); - } - continue; - } - - if (declaration?.type === "TSTypeAliasDeclaration") { - const existing = aliases.get(declaration.id.name); - if (existing === undefined) aliases.set(declaration.id.name, declaration); - else shadowedBuiltIns.add(declaration.id.name); - if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name); - continue; - } - - if (declaration?.type === "TSInterfaceDeclaration") { - const declarations = interfaces.get(declaration.id.name) ?? []; - declarations.push(declaration); - interfaces.set(declaration.id.name, declarations); - if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name); - continue; - } - - if (declaration?.type === "TSEnumDeclaration") { - if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name); - continue; - } - - if ( - (declaration?.type === "ClassDeclaration" || - declaration?.type === "FunctionDeclaration") && - declaration.id !== null - ) { - if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name); - } - } - - return { aliases, interfaces, shadowedBuiltIns }; -} - -function typeReferenceName(type: ESTree.TSTypeReference): string | null { - return type.typeName.type === "Identifier" ? type.typeName.name : null; -} - -function isBuiltIn(name: string, environment: TypeEnvironment): boolean { - return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name); -} - -function isUnappliedReferenceTo(type: ESTree.TSType, name: string): boolean { - const unwrapped = unwrapTransparentType(type); - return ( - unwrapped.type === "TSTypeReference" && - typeReferenceName(unwrapped) === name && - (unwrapped.typeArguments === null || - unwrapped.typeArguments === undefined || - unwrapped.typeArguments.params.length === 0) - ); -} - -function unwrapTransparentType(type: ESTree.TSType): ESTree.TSType { - let current = type; - while ( - current.type === "TSParenthesizedType" || - (current.type === "TSTypeOperator" && current.operator === "readonly") - ) { - current = current.typeAnnotation; - } - return current; -} - -function isNeverType(type: ESTree.TSType): boolean { - return unwrapTransparentType(type).type === "TSNeverKeyword"; -} - -function isEffectivelyEmptyMember(member: ESTree.TSSignature): boolean { - return ( - member.type === "TSPropertySignature" && - member.optional === true && - member.typeAnnotation !== null && - member.typeAnnotation !== undefined && - isNeverType(member.typeAnnotation.typeAnnotation) - ); -} - -function isEffectivelyEmptyTypeLiteral(type: ESTree.TSTypeLiteral): boolean { - return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember); -} - -function isEffectivelyEmptyInterface( - declarations: readonly ESTree.TSInterfaceDeclaration[], -): boolean { - if (declarations.length !== 1) return false; - const [type] = declarations; - return ( - type !== undefined && - type.extends.length === 0 && - (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember)) - ); -} - -function resolvedSubstitutionArgument( - type: ESTree.TSType, - base: TypeAliasEnvironment, - resolving: ReadonlySet = new Set(), -): ESTree.TSType { - const unwrapped = unwrapTransparentType(type); - if (unwrapped.type !== "TSTypeReference") return type; - const name = typeReferenceName(unwrapped); - if (name === null || resolving.has(name)) return type; - const substitution = base.get(name); - if (substitution === undefined) return type; - const nextResolving = new Set(resolving); - nextResolving.add(name); - return resolvedSubstitutionArgument(substitution, base, nextResolving); -} - -function aliasSubstitution( - alias: ESTree.TSTypeAliasDeclaration, - type: ESTree.TSTypeReference, - base: TypeAliasEnvironment, -): TypeAliasEnvironment | null { - const parameters = alias.typeParameters?.params ?? []; - const arguments_ = type.typeArguments?.params ?? []; - const next = new Map(base); - for (const [index, parameter] of parameters.entries()) { - const argument = arguments_[index] ?? parameter.default; - if (argument === null || argument === undefined) return null; - next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next)); - } - return next; -} - -function unsafeDirectValue( - type: ESTree.TSType, - environment: TypeEnvironment, - substitutions: TypeAliasEnvironment, - resolvingAliases: ReadonlySet, -): UnsafeDictionary["unsafeValue"] | null { - const unwrapped = unwrapTransparentType(type); - if (unwrapped.type === "TSUnknownKeyword") return "unknown"; - if (unwrapped.type === "TSAnyKeyword") return "any"; - if (unwrapped.type === "TSObjectKeyword") return "object"; - if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) - return "empty-object"; - if (unwrapped.type === "TSUnionType") { - return unwrapped.types.some( - (member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null, - ) - ? "union" - : null; - } - if (unwrapped.type === "TSIntersectionType") { - const unsafeMembers = unwrapped.types.map((member) => - unsafeDirectValue(member, environment, substitutions, resolvingAliases), - ); - if (unsafeMembers.includes("any")) return "any"; - return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) - ? unsafeMembers[0] - : null; - } - if (unwrapped.type !== "TSTypeReference") return null; - const name = typeReferenceName(unwrapped); - if (name === null) return null; - if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) { - const wrapped = unwrapped.typeArguments?.params[0]; - return wrapped === undefined - ? null - : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases); - } - const substitution = substitutions.get(name); - if (substitution !== undefined) { - return isUnappliedReferenceTo(substitution, name) - ? null - : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases); - } - const interfaceDeclarations = environment.interfaces.get(name); - if (interfaceDeclarations !== undefined) { - return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null; - } - const alias = environment.aliases.get(name); - if (alias === undefined || resolvingAliases.has(name)) return null; - const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); - if (nextSubstitutions === null) return null; - const nextResolving = new Set(resolvingAliases); - nextResolving.add(name); - return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving); -} - -function dictionaryValueTypes( - type: ESTree.TSType, - environment: TypeEnvironment, - substitutions: TypeAliasEnvironment, - resolvingAliases: ReadonlySet, -): readonly ResolvedType[] { - const unwrapped = unwrapTransparentType(type); - - if (unwrapped.type === "TSTypeLiteral") { - return unwrapped.members.flatMap((member): readonly ResolvedType[] => - member.type === "TSIndexSignature" && member.typeAnnotation !== null - ? [{ type: member.typeAnnotation.typeAnnotation, substitutions }] - : [], - ); - } - - if (unwrapped.type === "TSMappedType") { - return unwrapped.typeAnnotation === null - ? [] - : [{ type: unwrapped.typeAnnotation, substitutions }]; - } - - if (unwrapped.type !== "TSTypeReference") return []; - const name = typeReferenceName(unwrapped); - if (name === null) return []; - - const substitution = substitutions.get(name); - if (substitution !== undefined) { - return isUnappliedReferenceTo(substitution, name) - ? [] - : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases); - } - - if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) { - const wrapped = unwrapped.typeArguments?.params[0]; - return wrapped === undefined - ? [] - : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases); - } - - if (name === "Record" && isBuiltIn(name, environment)) { - const value = unwrapped.typeArguments?.params[1] ?? null; - return value === null ? [] : [{ type: value, substitutions }]; - } - - if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) { - const source = unwrapped.typeArguments?.params[0]; - return source === undefined - ? [] - : dictionaryValueTypes(source, environment, substitutions, resolvingAliases); - } - - const alias = environment.aliases.get(name); - if (alias === undefined || resolvingAliases.has(name)) return []; - const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); - if (nextSubstitutions === null) return []; - const nextResolving = new Set(resolvingAliases); - nextResolving.add(name); - return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving); -} - -export function classifyUnsafeDictionaryValue( - valueType: ESTree.TSType, - environment: TypeEnvironment, -): UnsafeDictionary | null { - const unsafeValue = unsafeDirectValue(valueType, environment, new Map(), new Set()); - return unsafeValue === null ? null : { kind: "unsafe-dictionary", unsafeValue }; -} - -export function classifyUnsafeDictionary( - type: ESTree.TSType, - environment: TypeEnvironment, -): UnsafeDictionary | null { - for (const valueType of dictionaryValueTypes(type, environment, new Map(), new Set())) { - const unsafeValue = unsafeDirectValue( - valueType.type, - environment, - valueType.substitutions, - new Set(), - ); - if (unsafeValue !== null) return { kind: "unsafe-dictionary", unsafeValue }; - } - return null; -} - -function resolvesToDictionary( - type: ESTree.TSType, - environment: TypeEnvironment, - substitutions: TypeAliasEnvironment, - resolvingAliases: ReadonlySet, -): boolean { - return dictionaryValueTypes(type, environment, substitutions, resolvingAliases).length > 0; -} - -export function classifyWideningTarget( - type: ESTree.TSType, - environment: TypeEnvironment, -): WideningTarget | null { - const unwrapped = unwrapTransparentType(type); - if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" }; - if (unwrapped.type === "TSObjectKeyword") return { kind: "object" }; - if (unwrapped.type === "TSTypeLiteral") { - return unwrapped.members.some((member) => member.type === "TSIndexSignature") - ? { kind: "open dictionary" } - : unwrapped.members.length > 0 - ? { kind: "anonymous object" } - : null; - } - if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" }; - if (unwrapped.type !== "TSTypeReference") return null; - const name = typeReferenceName(unwrapped); - if (name === null) return null; - if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) { - const wrapped = unwrapped.typeArguments?.params[0]; - return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment); - } - if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" }; - const alias = environment.aliases.get(name); - if (alias === undefined) return null; - if ((alias.typeParameters?.params.length ?? 0) > 0) { - const substitutions = aliasSubstitution(alias, unwrapped, new Map()); - return substitutions !== null && - resolvesToDictionary(alias.typeAnnotation, environment, substitutions, new Set([name])) - ? { kind: "generic container" } - : null; - } - const substitutions = aliasSubstitution(alias, unwrapped, new Map()); - if (substitutions === null) return null; - const resolved = classifyAliasBroadTarget( - alias.typeAnnotation, - environment, - substitutions, - new Set([name]), - ); - return resolved; -} - -function isBroadMappedKey( - type: ESTree.TSType, - environment: TypeEnvironment, - substitutions: TypeAliasEnvironment, -): boolean { - const unwrapped = unwrapTransparentType(type); - if ( - unwrapped.type === "TSStringKeyword" || - unwrapped.type === "TSNumberKeyword" || - unwrapped.type === "TSSymbolKeyword" - ) { - return true; - } - if (unwrapped.type === "TSUnionType") { - return unwrapped.types.every((member) => - isBroadMappedKey(member, environment, substitutions), - ); - } - if (unwrapped.type !== "TSTypeReference") return false; - const name = typeReferenceName(unwrapped); - if (name === null) return false; - const substitution = substitutions.get(name); - if (substitution !== undefined && !isUnappliedReferenceTo(substitution, name)) { - return isBroadMappedKey(substitution, environment, substitutions); - } - return name === "PropertyKey" && isBuiltIn(name, environment); -} - -function classifyAliasBroadTarget( - type: ESTree.TSType, - environment: TypeEnvironment, - substitutions: TypeAliasEnvironment, - resolvingAliases: ReadonlySet, -): WideningTarget | null { - const unwrapped = unwrapTransparentType(type); - if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" }; - if (unwrapped.type === "TSObjectKeyword") return { kind: "object" }; - if (unwrapped.type === "TSTypeLiteral") { - return unwrapped.members.some((member) => member.type === "TSIndexSignature") - ? { kind: "open dictionary" } - : null; - } - if (unwrapped.type === "TSMappedType") { - return isBroadMappedKey(unwrapped.constraint, environment, substitutions) - ? { kind: "open dictionary" } - : null; - } - if (unwrapped.type !== "TSTypeReference") return null; - const name = typeReferenceName(unwrapped); - if (name === null) return null; - const substitution = substitutions.get(name); - if (substitution !== undefined) { - return isUnappliedReferenceTo(substitution, name) - ? null - : classifyAliasBroadTarget( - substitution, - environment, - substitutions, - resolvingAliases, - ); - } - if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) { - const wrapped = unwrapped.typeArguments?.params[0]; - return wrapped === undefined - ? null - : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases); - } - if (name === "Record" && isBuiltIn(name, environment)) { - return { kind: "open dictionary" }; - } - const alias = environment.aliases.get(name); - if (alias === undefined || resolvingAliases.has(name)) return null; - const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); - if (nextSubstitutions === null) return null; - const nextResolving = new Set(resolvingAliases); - nextResolving.add(name); - return classifyAliasBroadTarget( - alias.typeAnnotation, - environment, - nextSubstitutions, - nextResolving, - ); -} - -export function isPopulatedObjectExpression(expression: ESTree.Expression): boolean { - let current = expression; - while ( - current.type === "ParenthesizedExpression" || - current.type === "TSAsExpression" || - current.type === "TSTypeAssertion" || - current.type === "TSNonNullExpression" - ) { - current = current.expression; - } - return current.type === "ObjectExpression" && current.properties.length > 0; -} - -export function isKnownEvidenceExpression(expression: ESTree.Expression): boolean { - let current = expression; - while ( - current.type === "ParenthesizedExpression" || - current.type === "TSAsExpression" || - current.type === "TSTypeAssertion" || - current.type === "TSNonNullExpression" || - current.type === "TSSatisfiesExpression" - ) { - current = current.expression; - } - if (current.type === "ObjectExpression") return true; - return ( - current.type === "ArrayExpression" || - current.type === "ArrowFunctionExpression" || - current.type === "ClassExpression" || - current.type === "FunctionExpression" || - current.type === "NewExpression" || - current.type === "Literal" || - current.type === "TemplateLiteral" || - current.type === "UnaryExpression" - ); -} diff --git a/js/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts b/js/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts deleted file mode 100644 index 7cdb18c9..00000000 --- a/js/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { ESTree } from "@oxlint/plugins"; - -type VisitorKeys = Readonly>; - -function isNode(value: unknown): value is ESTree.Node { - return ( - typeof value === "object" && - value !== null && - "type" in value && - typeof value.type === "string" - ); -} - -function collectInferTypeParameterNames( - node: ESTree.Node, - visitorKeys: VisitorKeys, - names: Set, -): void { - if (node.type === "TSInferType") names.add(node.typeParameter.name.name); - const record = node as unknown as Readonly>; - for (const key of visitorKeys[node.type] ?? []) { - const value = record[key]; - if (isNode(value)) { - collectInferTypeParameterNames(value, visitorKeys, names); - continue; - } - if (!Array.isArray(value)) continue; - for (const child of value) { - if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names); - } - } -} - -/** Collect type binders that are in scope at a node and can shadow module aliases. */ -export function lexicalTypeParameterNames( - node: ESTree.Node, - visitorKeys: VisitorKeys, -): ReadonlySet { - const names = new Set(); - let descendant: ESTree.Node = node; - let current: ESTree.Node | null = node; - while (current !== null && current.type !== "Program") { - if ("typeParameters" in current) { - for (const parameter of current.typeParameters?.params ?? []) { - names.add(parameter.name.name); - } - } - if ( - current.type === "TSMappedType" && - (descendant === current.nameType || descendant === current.typeAnnotation) - ) { - names.add(current.key.name); - } - if (current.type === "TSConditionalType" && descendant === current.trueType) { - collectInferTypeParameterNames(current.extendsType, visitorKeys, names); - } - descendant = current; - current = current.parent; - } - return names; -} diff --git a/js/tools/oxlint/anti-slop/shared/reflect-method.ts b/js/tools/oxlint/anti-slop/shared/reflect-method.ts deleted file mode 100644 index 39bc218c..00000000 --- a/js/tools/oxlint/anti-slop/shared/reflect-method.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; - -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} - -function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean { - if (expression.type !== "Identifier" || expression.name !== "Reflect") return false; - if (sourceCode.isGlobalReference(expression)) return true; - const variable = resolveVariable(sourceCode, expression); - return variable === null || variable.defs.length === 0; -} - -/** Reports whether a call target names one method on the global Reflect object. */ -export function isGlobalReflectMethodCall( - sourceCode: SourceCode, - callee: ESTree.Expression, - methodName: string, -): boolean { - if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false; - if (!isGlobalReflect(sourceCode, callee.object)) return false; - const property = callee.property; - return callee.computed - ? property.type === "Literal" && property.value === methodName - : property.type === "Identifier" && property.name === methodName; -}