diff --git a/.agents/skills/create-integration-package/package/deno.jsonc b/.agents/skills/create-integration-package/package/deno.jsonc index 1042c3470..8828e9f09 100644 --- a/.agents/skills/create-integration-package/package/deno.jsonc +++ b/.agents/skills/create-integration-package/package/deno.jsonc @@ -22,7 +22,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test" // Add if --allow-* for permissions is needed } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 51f9d8720..de5254da0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,6 +14,7 @@ "vscode": { "extensions": [ "denoland.vscode-deno", + "oxc.oxc-vscode", "redhat.vscode-yaml", "streetsidesoftware.code-spell-checker" ] diff --git a/.hongdown.toml b/.hongdown.toml index dab32d6f3..060d91191 100644 --- a/.hongdown.toml +++ b/.hongdown.toml @@ -15,6 +15,7 @@ exclude = [ "WARP.md", "changes.d/**", "packages/fedify/src/cfworkers/**", + "tools/oxlint/anti-slop/**", "plan.md", "**/plans/**", ] diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..e6ac2da32 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,249 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "typescript", + "react", + "unicorn", + "oxc", + "import", + "promise", + "vitest", + "jsx-a11y" + ], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "options": { + "reportUnusedDisableDirectives": "error" + }, + "jsPlugins": [ + { + "name": "anti-slop", + "specifier": "./tools/oxlint/anti-slop/index.ts" + }, + { + "name": "@logtape", + "specifier": "@logtape/lint/eslint" + } + ], + "overrides": [ + { + "files": [ + "packages/cli/**" + ], + "rules": { + "eslint/no-console": "off" + } + }, + { + "files": [ + "examples/fresh/**" + ], + "rules": { + "react/no-danger": "error", + "react/no-danger-with-children": "error", + "react/rules-of-hooks": "error" + } + } + ], + "rules": { + "typescript/adjacent-overload-signatures": "error", + "typescript/ban-ts-comment": [ + "error", + { + "ts-check": false, + "ts-expect-error": "allow-with-description", + "ts-ignore": "allow-with-description", + "ts-nocheck": "allow-with-description", + "minimumDescriptionLength": 1 + } + ], + "typescript/no-wrapper-object-types": "error", + "typescript/no-unsafe-function-type": "error", + "eslint/constructor-super": "error", + "eslint/for-direction": "error", + "eslint/getter-return": "error", + "react/jsx-boolean-value": "error", + "react/button-has-type": "error", + "react/jsx-curly-brace-presence": [ + "error", + { + "propElementValues": "always" + } + ], + "react/jsx-key": [ + "error", + { + "checkKeyMustBeforeSpread": false + } + ], + "react/no-children-prop": "error", + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/no-unescaped-entities": "error", + "react/jsx-no-useless-fragment": "error", + "react/jsx-props-no-spread-multi": "error", + "react/void-dom-elements-no-children": "error", + "eslint/no-array-constructor": "error", + "eslint/no-async-promise-executor": "error", + "eslint/no-await-in-loop": "warn", + "eslint/no-case-declarations": "error", + "eslint/no-class-assign": "error", + "eslint/no-compare-neg-zero": "error", + "eslint/no-cond-assign": "error", + "eslint/no-console": "warn", + "eslint/no-const-assign": "error", + "eslint/no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "eslint/no-control-regex": "error", + "eslint/no-debugger": "error", + "eslint/no-delete-var": "error", + "eslint/no-dupe-class-members": "error", + "eslint/no-dupe-else-if": "error", + "eslint/no-dupe-keys": "error", + "eslint/no-duplicate-case": "error", + "eslint/no-empty": "error", + "eslint/no-empty-character-class": "error", + "typescript/no-empty-interface": [ + "error", + { + "allowSingleExtends": true + } + ], + "typescript/no-empty-object-type": [ + "error", + { + "allowInterfaces": "always" + } + ], + "eslint/no-empty-pattern": "error", + "eslint/no-ex-assign": "error", + "typescript/no-explicit-any": "error", + "eslint/no-extra-boolean-cast": "error", + "typescript/no-extra-non-null-assertion": "error", + "eslint/no-fallthrough": "error", + "eslint/no-func-assign": "error", + "eslint/no-global-assign": "error", + "eslint/no-import-assign": "error", + "eslint/no-inner-declarations": "error", + "eslint/no-invalid-regexp": "error", + "eslint/no-irregular-whitespace": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "eslint/no-new-native-nonconstructor": "error", + "eslint/no-obj-calls": "error", + "eslint/no-prototype-builtins": "error", + "eslint/no-redeclare": "error", + "eslint/no-regex-spaces": "error", + "eslint/no-self-assign": "error", + "eslint/no-setter-return": "error", + "eslint/no-shadow-restricted-names": "error", + "typescript/no-this-alias": "error", + "eslint/no-this-before-super": "error", + "eslint/no-unreachable": "error", + "eslint/no-unsafe-finally": "error", + "eslint/no-unsafe-negation": "error", + "eslint/no-unused-labels": "error", + "eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "eslint/no-var": "error", + "eslint/no-with": "error", + "typescript/prefer-as-const": "error", + "eslint/prefer-const": [ + "error", + { + "destructuring": "all" + } + ], + "typescript/prefer-namespace-keyword": "error", + "eslint/require-await": "error", + "eslint/require-yield": "error", + "eslint/use-isnan": "error", + "eslint/valid-typeof": "error", + "oxc/no-accumulating-spread": "error", + "anti-slop/no-module-mocking": "error", + "anti-slop/no-reduce-accumulator-copy": "error", + "anti-slop/no-unknown-type-aliases": "error", + "anti-slop/no-widen-then-assert": "error", + "anti-slop/no-array-filter-map": "error", + "anti-slop/no-reflect-get": "error", + "anti-slop/no-reflect-apply": "error", + "import/no-duplicates": "error", + "promise/no-nesting": "error", + "promise/no-return-wrap": "error", + "eslint/no-throw-literal": "error", + "eslint/no-unsafe-optional-chaining": "error", + "eslint/no-self-compare": "error", + "eslint/no-useless-escape": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-includes": "error", + "unicorn/error-message": "error", + "unicorn/no-array-for-each": "error", + "typescript/no-import-type-side-effects": "error", + "typescript/prefer-for-of": "error", + "typescript/no-extraneous-class": "error", + "@logtape/no-message-interpolation": "error", + "@logtape/prefer-lazy-evaluation": "warn", + "@logtape/no-unawaited-log": "error", + "@logtape/require-meta-sink": "warn", + "eslint/no-useless-return": "error" + }, + "ignorePatterns": [ + "**/pnpm-lock.yaml", + "pnpm-workspace.yaml", + ".agents/skills/**", + ".claude/skills/**", + ".claude-plugin/**", + ".github/**", + "claude-plugin/**", + "docs/**", + "**/*.md", + "**/*.html", + "**/*.svg", + "**/*.astro", + "**/*.svelte", + "**/*.vue", + "**/*.css", + "examples/cloudflare-workers/worker-configuration.d.ts", + "packages/cli/src/init/templates/**", + "packages/init/src/templates/**", + "packages/cfworkers/test/**", + "packages/fedify/src/cfworkers/dist/**", + "packages/fedify/src/cfworkers/fixtures/**", + "packages/fedify/src/cfworkers/imports.ts", + "packages/fedify/src/cfworkers/server.ts", + "packages/fedify/src/cfworkers/runner.ts", + "packages/fedify/src/cfworkers/selftest.ts", + "packages/fedify/src/cfworkers/server.js", + "packages/fedify/src/cfworkers/server.js.map", + "packages/sqlite/src/sqlite.bun.ts", + "examples/fresh/_fresh/**", + "examples/astro/.astro/**", + "**/dist/**", + "**/dist-tests/**", + "**/node_modules/**", + "**/npm/**", + "**/apidoc/**", + "**/*.test-report.xml", + "tools/oxlint/anti-slop/**" + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6a8c18813..db0a28686 100755 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "denoland.vscode-deno", + "oxc.oxc-vscode", "redhat.vscode-yaml", "streetsidesoftware.code-spell-checker" ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 033f16c9a..69dc34b5f 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "deno.enable": true, + "deno.lint": false, "deno.unstable": true, "editor.detectIndentation": false, "editor.indentSize": 2, diff --git a/.zed/settings.json b/.zed/settings.json index 5090f9b40..ab43a03e1 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -4,6 +4,7 @@ "settings": { "deno": { "enable": true, + "lint": false, "unstable": true } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae6262dc2..3aa5040c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,8 +107,8 @@ Development environment ----------------------- Fedify is a TypeScript monorepo. Node.js runs dependency installation, code -generation, builds, type checking, maintenance scripts, and test harnesses. -Formatting, linting, and the local CLI still use Deno. Supported packages +generation, builds, type checking, maintenance scripts, test harnesses, and +linting. Formatting and the local CLI still use Deno. Supported packages are tested on Deno, Node.js, and Bun. [mise] installs the required tool versions and runs repository tasks. Install mise before bootstrapping; it provisions Node.js and the tools declared in *mise.toml*, so Node.js needs no @@ -135,8 +135,8 @@ Install or refresh the pre-commit hook explicitly with To bootstrap without installing Deno, set `MISE_DISABLE_TOOLS=deno` before running `mise install` (for example, `$env:MISE_DISABLE_TOOLS = "deno"` in -PowerShell). Deno is still needed for the current formatting, linting, and -Deno compatibility tasks; those tools are being migrated separately. +PowerShell). Deno is still needed for the current formatting and Deno +compatibility tasks; linting runs on Oxlint and does not need Deno. Run development workflows through mise. Do not call `npm` or `pnpm` directly for repository tasks. Start by inspecting the available commands: @@ -238,6 +238,25 @@ Deno task, since that option omits task dependencies. The existing Deno `compile` tasks still generate vocabulary, but no longer cache or type-check it as a side effect. +### Linting + +Linting uses [Oxlint] and the root *.oxlintrc.json*. Run it with: + +~~~~ bash +mise run check:lint +mise run check-each fedify +~~~~ + +The configuration mirrors the rule set that the repository previously used +with `deno lint`; [linting.md] documents the mapping and the accepted gaps. +Name rules explicitly in `oxlint-disable` comments and add a short reason when +the suppression is not obvious. The editor setup keeps the Deno language +server for formatting and type diagnostics but disables `deno.lint`, so Oxlint +is the only lint source. + +[Oxlint]: https://oxc.rs/docs/guide/usage/linter/ +[linting.md]: LINTING.md + ### Type checking `mise run check:types` type-checks the repository with TypeScript on Node.js, diff --git a/LINTING.md b/LINTING.md new file mode 100644 index 000000000..4d2d50e52 --- /dev/null +++ b/LINTING.md @@ -0,0 +1,187 @@ +Linting +======= + +Fedify lints its TypeScript and JavaScript sources with [Oxlint]. The default +lint task no longer invokes `deno lint`; Deno remains supported for formatting, +type checking, JSR publishing, and the published `@fedify/lint` plugin. + +Run the repository lint task with: + +~~~~ bash +mise run check:lint +~~~~ + +To lint only a package while developing: + +~~~~ bash +mise run check-each fedify +~~~~ + +The root *.oxlintrc.json* is authoritative. Pass it explicitly with +`-c .oxlintrc.json`; an explicit config disables Oxlint's nested-config lookup +and keeps command-line and editor behavior consistent. Oxlint walks up to the +nearest config when run inside a package, so an unqualified `oxlint` usually +finds the root config too, but repository tasks always pass it explicitly. +`check:lint` and `check-each` use `--quiet` so warning-level rules do not flood +the gate; `mise run lint` shows them. + +[Oxlint]: https://oxc.rs/docs/guide/usage/linter/ + + +Rule mapping +------------ + +The configuration disables all Oxlint categories and lists rules explicitly, so +the enabled set does not change when Oxlint updates its defaults. The parity +target is Deno 2.9.7's `recommended` tag plus the React rules from the `fresh` +tag. Direct mappings use the same rule name under the `eslint/`, `typescript/`, +or `react/` plugin. + +The following mappings need options or replacement rules: + +| Deno rule | Oxlint rule | Notes | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `ban-ts-comment` | `typescript/ban-ts-comment` | Descriptions required for `@ts-ignore`, `@ts-expect-error`, and `@ts-nocheck`; bare `@ts-check` allowed. | +| `ban-types` | `typescript/no-wrapper-object-types`, `typescript/no-unsafe-function-type`, `typescript/no-empty-object-type` | Deno rejects `{}` in intersections; Oxlint deliberately permits `T & {}`. | +| `no-unused-vars` | `eslint/no-unused-vars` | Uses `args: "all"`, `caughtErrors: "all"`, and `^_` ignore patterns. | +| `no-empty-interface` | `typescript/no-empty-interface` | Allows interfaces that extend a single base. | +| `no-empty-object-type` | `typescript/no-empty-object-type` | Allows empty interfaces; `no-empty-interface` owns those. | +| `no-new-symbol` | `eslint/no-new-native-nonconstructor` | Also rejects `new BigInt()`. | +| `prefer-const` | `eslint/prefer-const` | Uses `destructuring: "all"` to match Deno. | +| `no-constant-condition` | `eslint/no-constant-condition` | Uses `checkLoops: false`. | +| `jsx-curly-braces` | `react/jsx-curly-brace-presence` | Uses `propElementValues: "always"`. | +| `jsx-key` | `react/jsx-key` | Uses `checkKeyMustBeforeSpread: false`. | +| `react-no-danger` | `react/no-danger` | Scoped to _examples/fresh/\*\*_ with the other Fresh React rules. | +| `react-no-danger-with-children` | `react/no-danger-with-children` | Scoped to _examples/fresh/\*\*_. | +| `react-rules-of-hooks` | `react/rules-of-hooks` | Scoped to _examples/fresh/\*\*_. | + +The remaining recommended rules map directly to their Oxlint names. The +following Deno rules have no equivalent and are accepted gaps: + +| Deno rule | Reason | +| ----------------------------------------------------- | --------------------------------------------------------------------------------- | +| `ban-untagged-ignore` | Oxlint allows an unqualified `oxlint-disable`; a focused audit checks rule names. | +| `no-deprecated-deno-api` | Deno API surface only. | +| `no-sloppy-imports` | Deno `--unstable-sloppy-imports` only. | +| `no-unversioned-import` | Deno import specifiers only. | +| `no-window`, `no-window-prefix` | Deno global rules. | +| `no-invalid-triple-slash-reference` | Oxlint's `typescript/triple-slash-reference` has different semantics. | +| `no-empty-enum` | No Oxlint rule. | +| `no-octal` | Oxlint's parser only rejects legacy octal in module mode. | +| `no-import-assertions` | Oxlint accepts the deprecated `assert` syntax. | +| `no-import-prefix` | Deno-specific; only historical files used it. | +| `fresh-handler-export`, `fresh-server-event-handlers` | Fresh support is deprecated; no Oxlint equivalent. | +| `no-slow-types` | Deno-only JSR check, already disabled in *packages/init*. | + +`no-dupe-args` and `no-await-in-sync-fn` are parser-covered. +`no-new-symbol` is handled by `no-new-native-nonconstructor` above. + +Oxlint's `getter-return` does not currently report TypeScript getters that +return nothing, although Deno does. `eslint/no-useless-return` catches the +common bare-`return` form; the wider missing-return case remains an accepted +Oxlint gap. + + +Suppressions +------------ + +Use Oxlint's comment syntax and name the rules explicitly: + +~~~~ typescript +// oxlint-disable-next-line typescript/no-explicit-any +const value: any = source; + +// oxlint-disable-next-line eslint/no-unused-vars -- The generic is public API. +type Result = unknown; +~~~~ + +Prefer a short reason comment above the directive when the suppression is not +obvious. Do not use a bare `// oxlint-disable`; +`scripts/check_lint_directives.ts` rejects it. A file-level directive is +allowed when a generated or vendored file would otherwise need many line-level +suppressions. + +Run `mise run check:lint` after changing a suppression. Oxlint reports unused +directives as errors, so remove a directive when the underlying violation no +longer exists. + + +File scope +---------- + +*.oxlintrc.json* mirrors the Deno lint scope. In particular it ignores +documentation and agent skills, generated build outputs, Deno package +`exclude` entries that contain lintable files, the Fresh example's build +directory, and framework files (`.vue`, `.svelte`, `.astro`) that Deno lint did +not parse. + +Two special cases: + + - *packages/vocab/src/vocab.ts* is generated and gitignored. Oxlint's + directory traversal skips it, so `check:lint` and `check-each vocab` name + it explicitly. + - *examples/lint/oxlint* deliberately violates `@fedify/lint` rules. The + repository lint task uses the root config, while the example's own + `pnpm lint` task exercises its nested plugin configuration. + + +Additional rules and anti-slop +------------------------------ + +Additional Oxlint rules are added one at a time, each with its own commit and +a note here. The current additions are: + + - `eslint/no-await-in-loop` as a warning. Fedify contains many loops that + deliberately await sequentially, especially in tests and protocol code, + so this stays advisory rather than failing the gate. + - `eslint/no-console` as a warning, except in *packages/cli/* where console + output is the user interface. The init package suppresses the one + intentional user-facing warning. + - `import/no-duplicates`. + - `promise/no-nesting`. + - `promise/no-return-wrap`. + - `eslint/no-throw-literal`. + - `eslint/no-unsafe-optional-chaining`. + - `eslint/no-useless-return`. + - `eslint/no-self-compare`. + - `eslint/no-useless-escape`. + - `unicorn/prefer-node-protocol`. + - `unicorn/prefer-includes`. + - `unicorn/error-message`. + - `unicorn/no-array-for-each`. + - `typescript/no-import-type-side-effects`. + - `typescript/prefer-for-of`. + - `typescript/no-extraneous-class`. + +The [LogTape lint plugin] is registered as `@logtape` with its recommended +rules: `no-message-interpolation` and `no-unawaited-log` as errors, and +`prefer-lazy-evaluation` and `require-meta-sink` as warnings. The opt-in +`no-dynamic-message` and `no-unrendered-properties` rules remain off. + +Vendored [anti-slop] rules live in *tools/oxlint/anti-slop/*. They are copied +from upstream and recorded in *tools/oxlint/anti-slop/UPSTREAM.md*; update +them by following the upstream install skill and recording the new revision. +The vendored directory is excluded from Oxlint, `deno fmt`, and Hongdown, and +`@oxlint/plugins` is pinned to the same exact version as `oxlint`. + +The enabled generic rules are: + + - `oxc/no-accumulating-spread` + - `anti-slop/no-array-filter-map` + - `anti-slop/no-module-mocking` + - `anti-slop/no-reduce-accumulator-copy` + - `anti-slop/no-reflect-apply` + - `anti-slop/no-reflect-get` + - `anti-slop/no-unknown-type-aliases` + - `anti-slop/no-widen-then-assert` + +The remaining generic rules are deferred because they require broad type or +API refactoring: `no-chained-type-assertions`, +`no-conditional-empty-object-spread`, `no-known-value-widening`, +`no-object-parameters`, `no-runtime-typeof`, `no-shape-in-symbol-names`, +`no-unknown-parameters`, `no-unknown-returns`, `no-unsafe-dictionary-type`, and +`require-safety-comment-for-type-assertion`. `require-readable-spacing` is not +enabled because formatting is owned by the formatter. + +[LogTape lint plugin]: https://logtape.org/lint +[anti-slop]: https://github.com/dmmulroy/anti-slop diff --git a/deno.json b/deno.json index f7d9825d5..acc00740d 100644 --- a/deno.json +++ b/deno.json @@ -52,8 +52,8 @@ "@fxts/core": "npm:@fxts/core@^1.21.1", "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0", "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", - "@logtape/file": "jsr:@logtape/file@^2.2.0", - "@logtape/logtape": "jsr:@logtape/logtape@^2.2.0", + "@logtape/file": "jsr:@logtape/file@2.4.2", + "@logtape/logtape": "jsr:@logtape/logtape@2.4.2", "@nestjs/common": "npm:@nestjs/common@^11.0.1", "@nestjs/core": "npm:@nestjs/core@11.1.4", "@nestjs/platform-express": "npm:@nestjs/platform-express@11.1.4", @@ -112,13 +112,9 @@ "claude-plugin/", "docs/", "pnpm-lock.yaml", - "pnpm-workspace.yaml" + "pnpm-workspace.yaml", + "tools/" ], - "lint": { - "exclude": [ - "examples/cloudflare-workers/worker-configuration.d.ts" - ] - }, "fmt": { "exclude": [ "**/*.html", diff --git a/deno.lock b/deno.lock index 801a69951..94f2de053 100644 --- a/deno.lock +++ b/deno.lock @@ -23,9 +23,10 @@ "jsr:@hongminhee/localtunnel@0.5": "0.5.0", "jsr:@hono/hono@^4.7.1": "4.12.26", "jsr:@hono/hono@^4.8.3": "4.12.26", - "jsr:@logtape/file@^2.2.0": "2.2.0", + "jsr:@logtape/file@2.4.2": "2.4.2", "jsr:@logtape/logtape@^1.0.4": "1.3.11", - "jsr:@logtape/logtape@^2.2.0": "2.2.0", + "jsr:@logtape/logtape@2.4.2": "2.4.2", + "jsr:@logtape/logtape@^2.4.2": "2.4.2", "jsr:@optique/config@^1.3.2": "1.3.2", "jsr:@optique/core@1.3.2": "1.3.2", "jsr:@optique/core@^1.3.2": "1.3.2", @@ -166,8 +167,7 @@ "npm:mysql2@3.22.3": "3.22.3_@types+node@24.12.2", "npm:mysql2@^3.22.3": "3.22.3_@types+node@24.12.2", "npm:ora@^8.2.0": "8.2.0", - "npm:oxlint@*": "1.66.0", - "npm:oxlint@^1.66.0": "1.66.0", + "npm:oxlint@1.90.0": "1.90.0", "npm:pkijs@^3.2.5": "3.4.0", "npm:pkijs@^3.3.3": "3.4.0", "npm:pngjs@7": "7.0.0", @@ -326,10 +326,10 @@ "@hono/hono@4.12.26": { "integrity": "f473c70caaa2bb4aad40354195aef58a58c685692d701e1349d1abbf08da22bd" }, - "@logtape/file@2.2.0": { - "integrity": "60a1a8ec1256b88aacc65464f3f82a554bc49252cc9d4e4e4b2736237a227d52", + "@logtape/file@2.4.2": { + "integrity": "fe33b5db04f671c50337994850de32df27a33260bbb4e4d0a62e4d56dd78c669", "dependencies": [ - "jsr:@logtape/logtape@^2.2.0", + "jsr:@logtape/logtape@^2.4.2", "jsr:@std/path@^1.1.0" ] }, @@ -342,8 +342,8 @@ "@logtape/logtape@1.3.11": { "integrity": "eb673324344f1a4a01900ba81c815c361407543924c0387d85cdaf7846d73e8c" }, - "@logtape/logtape@2.2.0": { - "integrity": "537ceb4de93d00780a5d81ca8e053c4774ee95b1d32ab665a3d07ae302e8ea53" + "@logtape/logtape@2.4.2": { + "integrity": "308d84fb97124e5558a24eb23b94a8909fc60f4b0032e9328905a4264d14886b" }, "@optique/config@1.3.2": { "integrity": "48c0a21a2a2a897044a1cd265d1e12a976a629098bddad11c4cf516b4a4ebb83", @@ -4684,98 +4684,98 @@ "@oxc-project/types@0.139.0": { "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==" }, - "@oxlint/binding-android-arm-eabi@1.66.0": { - "integrity": "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==", + "@oxlint/binding-android-arm-eabi@1.90.0": { + "integrity": "sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==", "os": ["android"], "cpu": ["arm"] }, - "@oxlint/binding-android-arm64@1.66.0": { - "integrity": "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==", + "@oxlint/binding-android-arm64@1.90.0": { + "integrity": "sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==", "os": ["android"], "cpu": ["arm64"] }, - "@oxlint/binding-darwin-arm64@1.66.0": { - "integrity": "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==", + "@oxlint/binding-darwin-arm64@1.90.0": { + "integrity": "sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@oxlint/binding-darwin-x64@1.66.0": { - "integrity": "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==", + "@oxlint/binding-darwin-x64@1.90.0": { + "integrity": "sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==", "os": ["darwin"], "cpu": ["x64"] }, - "@oxlint/binding-freebsd-x64@1.66.0": { - "integrity": "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==", + "@oxlint/binding-freebsd-x64@1.90.0": { + "integrity": "sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==", "os": ["freebsd"], "cpu": ["x64"] }, - "@oxlint/binding-linux-arm-gnueabihf@1.66.0": { - "integrity": "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==", + "@oxlint/binding-linux-arm-gnueabihf@1.90.0": { + "integrity": "sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==", "os": ["linux"], "cpu": ["arm"] }, - "@oxlint/binding-linux-arm-musleabihf@1.66.0": { - "integrity": "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==", + "@oxlint/binding-linux-arm-musleabihf@1.90.0": { + "integrity": "sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==", "os": ["linux"], "cpu": ["arm"] }, - "@oxlint/binding-linux-arm64-gnu@1.66.0": { - "integrity": "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==", + "@oxlint/binding-linux-arm64-gnu@1.90.0": { + "integrity": "sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxlint/binding-linux-arm64-musl@1.66.0": { - "integrity": "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==", + "@oxlint/binding-linux-arm64-musl@1.90.0": { + "integrity": "sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxlint/binding-linux-ppc64-gnu@1.66.0": { - "integrity": "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==", + "@oxlint/binding-linux-ppc64-gnu@1.90.0": { + "integrity": "sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==", "os": ["linux"], "cpu": ["ppc64"] }, - "@oxlint/binding-linux-riscv64-gnu@1.66.0": { - "integrity": "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==", + "@oxlint/binding-linux-riscv64-gnu@1.90.0": { + "integrity": "sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxlint/binding-linux-riscv64-musl@1.66.0": { - "integrity": "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==", + "@oxlint/binding-linux-riscv64-musl@1.90.0": { + "integrity": "sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxlint/binding-linux-s390x-gnu@1.66.0": { - "integrity": "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==", + "@oxlint/binding-linux-s390x-gnu@1.90.0": { + "integrity": "sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==", "os": ["linux"], "cpu": ["s390x"] }, - "@oxlint/binding-linux-x64-gnu@1.66.0": { - "integrity": "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==", + "@oxlint/binding-linux-x64-gnu@1.90.0": { + "integrity": "sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==", "os": ["linux"], "cpu": ["x64"] }, - "@oxlint/binding-linux-x64-musl@1.66.0": { - "integrity": "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==", + "@oxlint/binding-linux-x64-musl@1.90.0": { + "integrity": "sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==", "os": ["linux"], "cpu": ["x64"] }, - "@oxlint/binding-openharmony-arm64@1.66.0": { - "integrity": "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==", + "@oxlint/binding-openharmony-arm64@1.90.0": { + "integrity": "sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@oxlint/binding-win32-arm64-msvc@1.66.0": { - "integrity": "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==", + "@oxlint/binding-win32-arm64-msvc@1.90.0": { + "integrity": "sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==", "os": ["win32"], "cpu": ["arm64"] }, - "@oxlint/binding-win32-ia32-msvc@1.66.0": { - "integrity": "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==", + "@oxlint/binding-win32-ia32-msvc@1.90.0": { + "integrity": "sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@oxlint/binding-win32-x64-msvc@1.66.0": { - "integrity": "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==", + "@oxlint/binding-win32-x64-msvc@1.90.0": { + "integrity": "sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==", "os": ["win32"], "cpu": ["x64"] }, @@ -13132,8 +13132,8 @@ "safe-push-apply" ] }, - "oxlint@1.66.0": { - "integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==", + "oxlint@1.90.0": { + "integrity": "sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==", "optionalDependencies": [ "@oxlint/binding-android-arm-eabi", "@oxlint/binding-android-arm64", @@ -16748,8 +16748,8 @@ "jsr:@dprint/formatter@0.6.0", "jsr:@hongminhee/localtunnel@0.3", "jsr:@hono/hono@^4.8.3", - "jsr:@logtape/file@^2.2.0", - "jsr:@logtape/logtape@^2.2.0", + "jsr:@logtape/file@2.4.2", + "jsr:@logtape/logtape@2.4.2", "jsr:@optique/config@^1.3.2", "jsr:@optique/core@^1.3.2", "jsr:@optique/discover@^1.3.2", @@ -16957,7 +16957,7 @@ "packages/fresh": { "dependencies": [ "jsr:@fresh/core@^2.1.4", - "jsr:@logtape/logtape@^2.2.0" + "jsr:@logtape/logtape@2.4.2" ] }, "packages/init": { @@ -16984,7 +16984,7 @@ "jsr:@fedify/fixture@^2.2.14", "npm:@types/estree@^1.0.8", "npm:eslint@9", - "npm:oxlint@^1.66.0" + "npm:oxlint@1.90.0" ], "packageJson": { "dependencies": [ diff --git a/examples/actor-lookup-cli/main.ts b/examples/actor-lookup-cli/main.ts index ab5686af4..feb74ba7b 100644 --- a/examples/actor-lookup-cli/main.ts +++ b/examples/actor-lookup-cli/main.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-empty no-import-prefix +// oxlint-disable eslint/no-empty import { Collection, getActorHandle, diff --git a/examples/adonisjs/start/container_bindings_middleware.ts b/examples/adonisjs/start/container_bindings_middleware.ts index ceb58c43d..e3dc01b0b 100644 --- a/examples/adonisjs/start/container_bindings_middleware.ts +++ b/examples/adonisjs/start/container_bindings_middleware.ts @@ -1,6 +1,6 @@ import { HttpContext } from "@adonisjs/core/http"; import { Logger } from "@adonisjs/core/logger"; -import { type NextFn } from "@adonisjs/core/types/http"; +import type { NextFn } from "@adonisjs/core/types/http"; /** * The container bindings middleware binds classes to their request diff --git a/examples/astro/src/lib/store.ts b/examples/astro/src/lib/store.ts index 9ef721a69..7331469fc 100644 --- a/examples/astro/src/lib/store.ts +++ b/examples/astro/src/lib/store.ts @@ -11,11 +11,12 @@ class PostStore { #timeline: URL[] = []; constructor() {} #append(posts: Note[]) { - posts.filter((p) => p.id && !this.#map.has(p.id.toString())) - .forEach((p) => { - this.#map.set(p.id!.toString(), p); - this.#timeline.push(p.id!); - }); + for ( + const p of posts.filter((p) => p.id && !this.#map.has(p.id.toString())) + ) { + this.#map.set(p.id!.toString(), p); + this.#timeline.push(p.id!); + } } append = this.#append.bind(this); #get(id: URL) { diff --git a/examples/fresh/deno.json b/examples/fresh/deno.json index 36138585b..0863a31f8 100644 --- a/examples/fresh/deno.json +++ b/examples/fresh/deno.json @@ -1,19 +1,11 @@ { "tasks": { - "check": "deno fmt --check . && deno lint . && deno check", + "check": "deno fmt --check . && deno check", "dev": "deno run -A npm:vite@^7.1.3 --configLoader native", "build": "deno run -A npm:vite@^7.1.3 build --configLoader native", "start": "deno serve -A _fresh/server.js", "update": "deno run -A -r jsr:@fresh/update ." }, - "lint": { - "rules": { - "tags": [ - "fresh", - "recommended" - ] - } - }, "exclude": [ "**/_fresh/*" ], diff --git a/examples/hono-sample/deno.json b/examples/hono-sample/deno.json index 2481e438f..deac7f2a5 100644 --- a/examples/hono-sample/deno.json +++ b/examples/hono-sample/deno.json @@ -5,7 +5,7 @@ "tasks": { "codegen": "deno task -f @fedify/vocab compile", "check": { - "command": "deno fmt --check && deno lint && deno check **/*.ts", + "command": "deno fmt --check && deno check **/*.ts", "dependencies": [ "codegen" ] diff --git a/examples/lint/oxlint/deno.json b/examples/lint/oxlint/deno.json index 7e1b4d1d0..60def2616 100644 --- a/examples/lint/oxlint/deno.json +++ b/examples/lint/oxlint/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "lint": "deno run -A npm:oxlint .", - "lint:fixed": "deno run -A npm:oxlint federation.fixed.ts" + "lint": "deno run -A npm:oxlint@1.90.0 .", + "lint:fixed": "deno run -A npm:oxlint@1.90.0 federation.fixed.ts" } } diff --git a/examples/next14-app-router/app/layout.tsx b/examples/next14-app-router/app/layout.tsx index b570ae5b7..656012175 100644 --- a/examples/next14-app-router/app/layout.tsx +++ b/examples/next14-app-router/app/layout.tsx @@ -13,7 +13,7 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ); } diff --git a/examples/next14-app-router/data/store.ts b/examples/next14-app-router/data/store.ts index 28a2f1c98..e3345485e 100644 --- a/examples/next14-app-router/data/store.ts +++ b/examples/next14-app-router/data/store.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any export const keyPairsStore: Map> = ( globalThis as any diff --git a/examples/next15-app-router/data/store.ts b/examples/next15-app-router/data/store.ts index 28a2f1c98..e3345485e 100644 --- a/examples/next15-app-router/data/store.ts +++ b/examples/next15-app-router/data/store.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any export const keyPairsStore: Map> = ( globalThis as any diff --git a/examples/next15-app-router/eslint.config.mjs b/examples/next15-app-router/eslint.config.mjs index c85fb67c4..f5b76f392 100644 --- a/examples/next15-app-router/eslint.config.mjs +++ b/examples/next15-app-router/eslint.config.mjs @@ -1,5 +1,5 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); diff --git a/examples/rfc-9421-test/app.ts b/examples/rfc-9421-test/app.ts index 89b64622c..913da2373 100644 --- a/examples/rfc-9421-test/app.ts +++ b/examples/rfc-9421-test/app.ts @@ -18,8 +18,8 @@ import { followersStore, followingStore, onStateChange, + setInboundSigSpec, } from "./federation.ts"; -import { setInboundSigSpec } from "./federation.ts"; const indexHtml = await Deno.readTextFile( new URL("index.html", import.meta.url), diff --git a/examples/rfc-9421-test/main.ts b/examples/rfc-9421-test/main.ts index c542ccb73..14d07254e 100644 --- a/examples/rfc-9421-test/main.ts +++ b/examples/rfc-9421-test/main.ts @@ -17,7 +17,7 @@ * CHALLENGE_ENABLED=0 deno run -A main.ts */ -import { type InboxChallengePolicy } from "@fedify/fedify"; +import type { InboxChallengePolicy } from "@fedify/fedify"; import type { HttpMessageSignaturesSpec } from "@fedify/fedify/sig"; import { getLogger } from "@logtape/logtape"; import createApp from "./app.ts"; diff --git a/examples/solidstart/src/routes/users/[identifier].tsx b/examples/solidstart/src/routes/users/[identifier].tsx index dc0d39dd9..14c8386f5 100644 --- a/examples/solidstart/src/routes/users/[identifier].tsx +++ b/examples/solidstart/src/routes/users/[identifier].tsx @@ -1,5 +1,4 @@ -import { useParams } from "@solidjs/router"; -import { createAsync, query } from "@solidjs/router"; +import { createAsync, query, useParams } from "@solidjs/router"; import { Show } from "solid-js"; const getProfile = query(async (identifier: string) => { @@ -38,7 +37,7 @@ export default function UserProfile() {

Try:{" "} - curl -H "Accept: application/activity+json" + curl -H "Accept: application/activity+json" http://localhost:3000/users/{params.identifier}

diff --git a/examples/sveltekit-sample/src/lib/fetch.ts b/examples/sveltekit-sample/src/lib/fetch.ts index c9818ea9e..798675906 100644 --- a/examples/sveltekit-sample/src/lib/fetch.ts +++ b/examples/sveltekit-sample/src/lib/fetch.ts @@ -1,4 +1,4 @@ -import { type RequestContext } from "@fedify/fedify"; +import type { RequestContext } from "@fedify/fedify"; import { Note } from "@fedify/vocab"; import type { Post, User } from "./types"; import { postStore } from "./store"; diff --git a/examples/sveltekit-sample/src/lib/store.ts b/examples/sveltekit-sample/src/lib/store.ts index 3e2f5b9e6..4c15ae713 100644 --- a/examples/sveltekit-sample/src/lib/store.ts +++ b/examples/sveltekit-sample/src/lib/store.ts @@ -11,11 +11,12 @@ class PostStore { #timeline: URL[] = []; constructor() {} #append(posts: Note[]) { - posts.filter((p) => p.id && !this.#map.has(p.id.toString())) - .forEach((p) => { - this.#map.set(p.id!.toString(), p); - this.#timeline.push(p.id!); - }); + for ( + const p of posts.filter((p) => p.id && !this.#map.has(p.id.toString())) + ) { + this.#map.set(p.id!.toString(), p); + this.#timeline.push(p.id!); + } } append = this.#append.bind(this); #get(id: URL) { diff --git a/examples/test-examples/mod.test.ts b/examples/test-examples/mod.test.ts index 7f3bb3baa..cc3f4a609 100644 --- a/examples/test-examples/mod.test.ts +++ b/examples/test-examples/mod.test.ts @@ -11,11 +11,10 @@ * mise run test:tooling */ import { fileURLToPath } from "node:url"; -import { readdir } from "node:fs/promises"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; import assert from "node:assert/strict"; import test from "node:test"; import process from "node:process"; -import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { commandOutput } from "../../scripts/tooling_process.ts"; diff --git a/mise.toml b/mise.toml index 4110e3811..4a49d7a32 100644 --- a/mise.toml +++ b/mise.toml @@ -228,9 +228,18 @@ description = "Check code formatting" run = "deno fmt --check" [tasks."check:lint"] -depends = ["deno:cache"] description = "Check code linting" -run = "deno --quiet lint" +run = [ + "node --experimental-strip-types scripts/check_lint_directives.ts", + "pnpm exec oxlint --quiet -c .oxlintrc.json . packages/vocab/src/vocab.ts", +] + +[tasks.lint] +description = "Lint the codebase with Oxlint" +run = [ + "node --experimental-strip-types scripts/check_lint_directives.ts", + "pnpm exec oxlint -c .oxlintrc.json . packages/vocab/src/vocab.ts", +] # Type-checks against the built declarations of workspace packages, so it # depends on `build` rather than `codegen` (see the note on `build`). @@ -302,7 +311,13 @@ for pkg in ($env.usage_packages | split row " " | where ($it | is-not-empty)) { exit 1 } deno fmt --check $dir - deno lint $dir + let lint_paths = if $pkg == "vocab" { + [$dir "packages/vocab/src/vocab.ts"] + } else { + [$dir] + } + node --experimental-strip-types scripts/check_lint_directives.ts $dir + pnpm exec oxlint --quiet -c .oxlintrc.json ...$lint_paths # Deno-only packages (no package.json) keep their Deno type check. if ($"($dir)/package.json" | path exists) { node --experimental-strip-types scripts/check_types.ts $pkg @@ -325,14 +340,15 @@ node scripts/test_testing_pack.mjs """ [tasks."test:deno"] -depends = ["deno:cache"] -wait_for = ["build"] +depends = ["deno:cache", "build"] description = "Run the test suite using Deno" +env = { FEDIFY_REQUIRE_OXLINT_TESTS = "1" } run = "deno test --check --doc --allow-all --unstable-kv --trace-leaks --parallel" [tasks."test:node"] description = "Run the test suite using Node.js" depends = ["build"] +env = { FEDIFY_REQUIRE_OXLINT_TESTS = "1" } # See the note on `build`: Nushell keeps the single-quoted pnpm filter working # on Windows, where `cmd` would otherwise pass it through literally. # Keep package-level concurrency bounded because each `node --test` command @@ -367,6 +383,7 @@ run = "node --experimental-strip-types scripts/check_temporal_polyfill.ts" [tasks.test-each] depends = ["deno:cache"] description = "Run tests for a specific package across all environments" +env = { FEDIFY_REQUIRE_OXLINT_TESTS = "1" } usage = 'arg "" help="Package name(s) (without @fedify/ prefix)" var=#true var_min=1' shell = "nu -c" run = ''' diff --git a/package.json b/package.json index c204b9722..6e17205ab 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "@cloudflare/workers-types": "catalog:", "@types/bun": "catalog:", "@types/deno": "catalog:", + "@oxlint/plugins": "catalog:", + "@logtape/lint": "catalog:", + "oxlint": "catalog:", "tsdown": "catalog:" }, "pnpm": { diff --git a/packages/adonisjs/providers/fedify_provider.ts b/packages/adonisjs/providers/fedify_provider.ts index a86bc8abb..de4e75e79 100644 --- a/packages/adonisjs/providers/fedify_provider.ts +++ b/packages/adonisjs/providers/fedify_provider.ts @@ -348,7 +348,13 @@ export default class FedifyProvider { this.#loggingConfigured = false; // A failing reset must not fail the shutdown queued behind it. The queue // promise never rejects, so only the reset is guarded. - return stopped.then(() => resetLogging().catch(() => {})); + return stopped.then(async () => { + try { + await resetLogging(); + } catch { + // Reset failures must not fail the shutdown queued behind them. + } + }); } /** diff --git a/packages/adonisjs/src/types.ts b/packages/adonisjs/src/types.ts index 52e49ae0b..a688ad2cb 100644 --- a/packages/adonisjs/src/types.ts +++ b/packages/adonisjs/src/types.ts @@ -46,7 +46,7 @@ import type { */ // The empty interface is intentional: it is the extension point applications // augment, so it has to start out with no members. -// deno-lint-ignore no-empty-interface +// oxlint-disable-next-line typescript/no-empty-interface export interface FedifyTypes {} /** diff --git a/packages/amqp/deno.json b/packages/amqp/deno.json index 8f7f48f93..5d1add3cf 100644 --- a/packages/amqp/deno.json +++ b/packages/amqp/deno.json @@ -24,7 +24,7 @@ }, "tasks": { "build": "pnpm build", - "check": "deno fmt --check && deno lint && deno check *.ts", + "check": "deno fmt --check && deno check *.ts", "test": "deno test --allow-net --allow-env", "test:node": { "dependencies": [ diff --git a/packages/amqp/src/mq.ts b/packages/amqp/src/mq.ts index d80023908..59b1bb6df 100644 --- a/packages/amqp/src/mq.ts +++ b/packages/amqp/src/mq.ts @@ -238,7 +238,7 @@ export class AmqpMessageQueue implements MessageQueue { * @param options The options for enqueueing the message. */ async enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions, ): Promise { @@ -317,7 +317,7 @@ export class AmqpMessageQueue implements MessageQueue { * @param options The options for enqueueing the messages. */ async enqueueMany( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any messages: readonly any[], options?: MessageQueueEnqueueOptions, ): Promise { @@ -528,7 +528,7 @@ export class AmqpMessageQueue implements MessageQueue { } async listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: (message: any) => void | Promise, options: MessageQueueListenOptions = {}, ): Promise { diff --git a/packages/astro/compat/run.ts b/packages/astro/compat/run.ts index b25389a09..5a6814ed0 100644 --- a/packages/astro/compat/run.ts +++ b/packages/astro/compat/run.ts @@ -99,12 +99,7 @@ async function testCompatibility(testCase: CompatibilityCase): Promise { astro: testCase.astro, }, pnpm: { - overrides: { - ...Object.fromEntries(tarballs), - // Version 1.2.0 requires @emnapi/* 2.0 prereleases, while its - // consumers still provide @emnapi/* 1.x. - "@napi-rs/wasm-runtime@^1.1.6": "1.1.6", - }, + overrides: Object.fromEntries(tarballs), }, }, null, diff --git a/packages/astro/deno.json b/packages/astro/deno.json index 747c8aae5..412c19bc0 100644 --- a/packages/astro/deno.json +++ b/packages/astro/deno.json @@ -17,6 +17,6 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts" + "check": "deno fmt --check && deno check src/*.ts" } } diff --git a/packages/backfill/deno.json b/packages/backfill/deno.json index 7168c6a1b..bc9c42a9e 100644 --- a/packages/backfill/deno.json +++ b/packages/backfill/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/**/*.ts", + "check": "deno fmt --check && deno check src/**/*.ts", "test": "deno test" } } diff --git a/packages/backfill/src/types.ts b/packages/backfill/src/types.ts index 58090e698..db042969d 100644 --- a/packages/backfill/src/types.ts +++ b/packages/backfill/src/types.ts @@ -72,6 +72,9 @@ export interface BackfillContext { * @since 2.3.0 */ export interface BackfillOptions< + // TObject is part of the public generic API even though this interface + // body does not reference it. + // oxlint-disable-next-line eslint/no-unused-vars TObject extends APObject = APObject, > { /** diff --git a/packages/cfworkers/deno.json b/packages/cfworkers/deno.json index 5bcafee7c..8d964546b 100644 --- a/packages/cfworkers/deno.json +++ b/packages/cfworkers/deno.json @@ -27,6 +27,6 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts" + "check": "deno fmt --check && deno check src/*.ts" } } diff --git a/packages/cfworkers/src/mod.test.ts b/packages/cfworkers/src/mod.test.ts index b3fcefd61..8ba435cae 100644 --- a/packages/cfworkers/src/mod.test.ts +++ b/packages/cfworkers/src/mod.test.ts @@ -102,9 +102,11 @@ class MockKvNamespace implements WorkersKvNamespaceLike { options?: KvListOptions, ): Promise> { this.listCalls++; - const matches = [...this.entries] - .filter(([name]) => name.startsWith(options?.prefix ?? "")) - .map(([name, entry]) => ({ name, metadata: entry.metadata as Metadata })); + const matches = [...this.entries].flatMap(([name, entry]) => + name.startsWith(options?.prefix ?? "") + ? [{ name, metadata: entry.metadata as Metadata }] + : [] + ); const start = options?.cursor == null ? 0 : Number(options.cursor); const keys = matches.slice(start, start + this.#pageSize); const nextStart = start + keys.length; @@ -225,7 +227,8 @@ describe("WorkersKvStore", () => { const entry = namespace.entries.get('["short-ttl"]'); expect(entry?.options?.expirationTtl).toBe(60); - const { expires } = entry?.metadata as { expires: number }; + const expires = (entry?.metadata as { expires: number } | undefined) + ?.expires; expect(expires).toBeGreaterThanOrEqual(before + 5000); expect(expires).toBeLessThanOrEqual(Date.now() + 5000); }); diff --git a/packages/cfworkers/src/mod.ts b/packages/cfworkers/src/mod.ts index 4e5a5416b..a78850caf 100644 --- a/packages/cfworkers/src/mod.ts +++ b/packages/cfworkers/src/mod.ts @@ -89,7 +89,7 @@ export interface WorkersKvNamespaceLike { */ interface WrappedMessage { readonly __fedify_ordering_key__?: string; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any readonly __fedify_payload__: any; } @@ -116,7 +116,7 @@ export interface ProcessMessageResult { * The unwrapped message payload to process. * Only present when `shouldProcess` is `true`. */ - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any readonly message?: any; /** * A cleanup function that must be called after processing the message. @@ -321,7 +321,7 @@ export class WorkersMessageQueue implements MessageQueue { } async enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions, ): Promise { @@ -337,7 +337,7 @@ export class WorkersMessageQueue implements MessageQueue { } async enqueueMany( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any messages: readonly any[], options?: MessageQueueEnqueueOptions, ): Promise { @@ -430,7 +430,7 @@ export class WorkersMessageQueue implements MessageQueue { * @returns A result object indicating whether to process the message. * @since 2.0.0 */ - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any async processMessage(rawMessage: any): Promise { // Handle both wrapped and unwrapped messages for backwards compatibility const wrapped = rawMessage as WrappedMessage; @@ -468,7 +468,7 @@ export class WorkersMessageQueue implements MessageQueue { } listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any _handler: (message: any) => Promise | void, _options?: MessageQueueListenOptions, ): Promise { diff --git a/packages/cli/deno.json b/packages/cli/deno.json index 6637a7765..ece498b54 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -41,7 +41,7 @@ "tasks": { "codegen": "deno task -f @fedify/vocab compile", "check": { - "command": "deno fmt --check && deno lint && deno check src/**/*.ts", + "command": "deno fmt --check && deno check src/**/*.ts", "dependencies": [ "codegen" ] @@ -72,11 +72,6 @@ "src/init/templates/**" ] }, - "lint": { - "exclude": [ - "src/init/templates/**" - ] - }, "test": { "exclude": [ "src/init/test/**" diff --git a/packages/cli/src/bench/result/build.ts b/packages/cli/src/bench/result/build.ts index 7fee0ba85..8c92742bc 100644 --- a/packages/cli/src/bench/result/build.ts +++ b/packages/cli/src/bench/result/build.ts @@ -185,11 +185,9 @@ function aggregateServer( function aggregateSignatureVerification( servers: readonly ServerMetrics[], ): NonNullable | null { - const values = servers - .map((s) => s.signatureVerificationMs) - .filter((s): s is NonNullable => - s != null - ); + const values = servers.flatMap((s) => + s.signatureVerificationMs == null ? [] : [s.signatureVerificationMs] + ); if (values.length !== servers.length) return null; const standards = new Set(); for (const value of values) { @@ -211,9 +209,7 @@ function aggregateSignatureVerification( function aggregateQueue( servers: readonly ServerMetrics[], ): NonNullable | null { - const values = servers - .map((s) => s.queue) - .filter((q): q is NonNullable => q != null); + const values = servers.flatMap((s) => s.queue == null ? [] : [s.queue]); if (values.length !== servers.length) return null; const drainMs = aggregatePartial(values.map((v) => v.drainMs)); const depths = values.map((v) => v.depthMax); diff --git a/packages/cli/src/bench/result/expect/evaluate.test.ts b/packages/cli/src/bench/result/expect/evaluate.test.ts index b28a8eabc..f54fdfe19 100644 --- a/packages/cli/src/bench/result/expect/evaluate.test.ts +++ b/packages/cli/src/bench/result/expect/evaluate.test.ts @@ -158,7 +158,7 @@ test("validateExpectBlock - throws on a malformed assertion", () => { test("validateExpectBlock - throws on an entry without an assertion", () => { assert.throws( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any () => validateExpectBlock({ successRate: { severity: "warn" } as any }), AssertionParseError, ); diff --git a/packages/cli/src/bench/scenarios/inbox.test.ts b/packages/cli/src/bench/scenarios/inbox.test.ts index 574f4fd98..9868434ce 100644 --- a/packages/cli/src/bench/scenarios/inbox.test.ts +++ b/packages/cli/src/bench/scenarios/inbox.test.ts @@ -313,7 +313,7 @@ test("inboxRunner.validate - rejects activity options it cannot honor", () => { name: "inbox", type: "inbox", recipient: "http://localhost:3000/users/alice", - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any activity: activity as any, }], }).scenarios[0]; diff --git a/packages/cli/src/bench/scenarios/mixed.ts b/packages/cli/src/bench/scenarios/mixed.ts index 51b9d0921..ebe63c75c 100644 --- a/packages/cli/src/bench/scenarios/mixed.ts +++ b/packages/cli/src/bench/scenarios/mixed.ts @@ -335,9 +335,9 @@ export function mergeMeasurements( const total = measurements.reduce((sum, m) => sum + m.requests.total, 0); const ok = measurements.reduce((sum, m) => sum + m.requests.ok, 0); const histogram = mergeHistograms(measurements); - const deliveryThroughputs = measurements - .map((m) => m.deliveryThroughputPerSec) - .filter((value): value is number => value != null); + const deliveryThroughputs = measurements.flatMap((m) => + m.deliveryThroughputPerSec == null ? [] : [m.deliveryThroughputPerSec] + ); return { requests: { total, diff --git a/packages/cli/src/diagnostics.ts b/packages/cli/src/diagnostics.ts index 8403170b4..dde5d2cc1 100644 --- a/packages/cli/src/diagnostics.ts +++ b/packages/cli/src/diagnostics.ts @@ -289,7 +289,7 @@ function describePortableFailure(failure: LookupFailure): string { // the terminal when displaying a diagnostic. function escapeControlCharacters(message: string): string { return message.replace( - // deno-lint-ignore no-control-regex + // oxlint-disable-next-line eslint/no-control-regex /[\x00-\x1f\x7f-\x9f]/g, (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`, diff --git a/packages/cli/src/inbox.tsx b/packages/cli/src/inbox.tsx index ab4d7c557..f132f34e5 100644 --- a/packages/cli/src/inbox.tsx +++ b/packages/cli/src/inbox.tsx @@ -289,12 +289,11 @@ export async function runInbox( peersCnt === 1 ? "peer" : "peers" }...`, ); - sendDeleteToPeers(server).then(() => { + sendDeleteToPeers(server).then(async () => { spinner.text = "Stopping server..."; - server.close().then(() => { - spinner.succeed("Server stopped."); - process.exit(0); - }); + await server.close(); + spinner.succeed("Server stopped."); + process.exit(0); }); }); spinner.start(); diff --git a/packages/cli/src/inbox/view.tsx b/packages/cli/src/inbox/view.tsx index 18b0f35ec..0f78b72c6 100644 --- a/packages/cli/src/inbox/view.tsx +++ b/packages/cli/src/inbox/view.tsx @@ -29,7 +29,6 @@ const Layout: FC> = ( {props.title == null ? "" - // deno-lint-ignore jsx-curly-braces : <Fragment>{props.title} —{" "}</Fragment>}Fedify Ephemeral Inbox ({props.handle}) @@ -303,7 +302,7 @@ const Tab: FC = ( ); }; -// deno-lint-ignore no-empty-interface +// oxlint-disable-next-line typescript/no-empty-interface interface TabListProps { } @@ -372,7 +371,6 @@ const Log: FC = ( {category.map((c, i) => - // deno-lint-ignore jsx-curly-braces i < 1 ? c : {" "}/ {c} )} @@ -541,6 +539,7 @@ const ActivityList: FC = ( const url = new URL(entry.request.url); return ( 0) { + if ((nodeInfo.services?.inbound?.length ?? 0) > 0) { layout[next()] += colors.bold(colors.dim("Inbound services:")); for (const service of nodeInfo.services?.inbound ?? []) { layout[next()] += ` ${service}`; } } - if (nodeInfo.services?.outbound?.length ?? 0 > 0) { + if ((nodeInfo.services?.outbound?.length ?? 0) > 0) { layout[next()] += colors.bold(colors.dim("Outbound services:")); for (const service of nodeInfo.services?.outbound ?? []) { layout[next()] += ` ${service}`; diff --git a/packages/cli/src/portable.test.ts b/packages/cli/src/portable.test.ts index d1788bccc..4db0cecb7 100644 --- a/packages/cli/src/portable.test.ts +++ b/packages/cli/src/portable.test.ts @@ -750,12 +750,12 @@ function createTimeoutLoaderFactory( ): typeof import("./docloader.ts").getDocumentLoader { const base = getDefaultDocumentLoader({ allowPrivateAddress: true }); return () => - Promise.resolve((url, options) => { - if (!timedOut(url)) return base(url, options); + Promise.resolve(async (url, options) => { + if (!timedOut(url)) return await base(url, options); // The same shape as a timeout of the built-in document loaders: const error = new FetchError(url, "Timed out after 10000 ms"); error.cause = new DOMException("Timed out.", "TimeoutError"); - return Promise.reject(error); + throw error; }); } diff --git a/packages/cli/src/relay.ts b/packages/cli/src/relay.ts index f21820853..1cf6f8251 100644 --- a/packages/cli/src/relay.ts +++ b/packages/cli/src/relay.ts @@ -43,7 +43,7 @@ export async function runRelay( kv = new MemoryKvStore(); } - // deno-lint-ignore prefer-const + // oxlint-disable-next-line eslint/prefer-const let relay: Relay; let server: TemporaryServer | null = null; const acceptFollows: string[] = []; diff --git a/packages/cli/src/tunnel.test.ts b/packages/cli/src/tunnel.test.ts index b69d22876..5abf80550 100644 --- a/packages/cli/src/tunnel.test.ts +++ b/packages/cli/src/tunnel.test.ts @@ -140,7 +140,7 @@ test("tunnel successfully creates and manages tunnel", async () => { }) as unknown as Ora; }, exit(): never { - throw new Error(); + throw new Error("mock exit"); }, }; diff --git a/packages/cli/src/webfinger/action.ts b/packages/cli/src/webfinger/action.ts index ce8c108c4..02789c3cc 100644 --- a/packages/cli/src/webfinger/action.ts +++ b/packages/cli/src/webfinger/action.ts @@ -1,9 +1,9 @@ import { isActor, lookupObject } from "@fedify/vocab"; import { formatIri } from "@fedify/vocab-runtime"; -import type { ResourceDescriptor } from "@fedify/webfinger"; import { lookupWebFinger, type LookupWebFingerOptions, + type ResourceDescriptor, } from "@fedify/webfinger"; import { formatMessage, message } from "@optique/core/message"; import { print } from "@optique/run"; diff --git a/packages/cli/src/webfinger/error.ts b/packages/cli/src/webfinger/error.ts index 518cc6051..6a74be071 100644 --- a/packages/cli/src/webfinger/error.ts +++ b/packages/cli/src/webfinger/error.ts @@ -38,6 +38,8 @@ export class InvalidHandleError extends Error { this.name = "InvalidHandleError"; } throw(): never { + // The receiver is the error instance itself. + // oxlint-disable-next-line eslint/no-throw-literal throw this; } } @@ -53,6 +55,8 @@ export class NotFoundError extends Error { this.name = "NotFoundError"; } throw(): never { + // The receiver is the error instance itself. + // oxlint-disable-next-line eslint/no-throw-literal throw this; } } @@ -73,6 +77,8 @@ export class WebFingerLookupError extends Error { this.name = "WebFingerLookupError"; } throw(): never { + // The receiver is the error instance itself. + // oxlint-disable-next-line eslint/no-throw-literal throw this; } } diff --git a/packages/cli/src/webfinger/mod.test.ts b/packages/cli/src/webfinger/mod.test.ts index b87780dcb..65a00c1d2 100644 --- a/packages/cli/src/webfinger/mod.test.ts +++ b/packages/cli/src/webfinger/mod.test.ts @@ -1,3 +1,4 @@ +// oxlint-disable anti-slop/no-reflect-apply -- Proxy forwarding requires dynamic invocation. import { formatMessage } from "@optique/core/message"; import { parse } from "@optique/core/parser"; import { run } from "@optique/run"; diff --git a/packages/cli/test/bench/fixture.ts b/packages/cli/test/bench/fixture.ts index 9c7d5a36f..c1b4e0dac 100644 --- a/packages/cli/test/bench/fixture.ts +++ b/packages/cli/test/bench/fixture.ts @@ -141,8 +141,8 @@ async function writeFetchResponse( outgoing: ServerResponse, ): Promise { outgoing.statusCode = response.status; - response.headers.forEach((value, name) => { + for (const [name, value] of response.headers) { outgoing.setHeader(name, value); - }); + } outgoing.end(Buffer.from(await response.arrayBuffer())); } diff --git a/packages/debugger/deno.json b/packages/debugger/deno.json index fa39b3527..89c723be5 100644 --- a/packages/debugger/deno.json +++ b/packages/debugger/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/**/*.ts src/**/*.tsx", + "check": "deno fmt --check && deno check src/**/*.ts src/**/*.tsx", "test": "deno test --allow-env" } } diff --git a/packages/debugger/src/mod.test.ts b/packages/debugger/src/mod.test.ts index 4eaecb202..13a56f676 100644 --- a/packages/debugger/src/mod.test.ts +++ b/packages/debugger/src/mod.test.ts @@ -53,7 +53,7 @@ function createMockFederation(): { function track(name: string) { if (!calls[name]) calls[name] = []; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any return (...args: any[]) => { calls[name].push(args); if (name === "fetch") { diff --git a/packages/debugger/src/mod.tsx b/packages/debugger/src/mod.tsx index 4d9cbe007..02bfe1518 100644 --- a/packages/debugger/src/mod.tsx +++ b/packages/debugger/src/mod.tsx @@ -123,7 +123,7 @@ function validatePathPrefix(path: string): string { ); } // Reject control characters, semicolons, and commas (unsafe in headers) - // deno-lint-ignore no-control-regex + // oxlint-disable-next-line eslint/no-control-regex if (/[\x00-\x1f\x7f;,]/.test(path)) { throw new TypeError( `Invalid debug dashboard path: ${JSON.stringify(path)}. ` + @@ -270,7 +270,7 @@ export function createFederationDebugger( reset: true, sinks, loggers, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any } as any, ); } else { @@ -313,6 +313,8 @@ export function createFederationDebugger( return new Proxy(federation as Federation & { sink: Sink }, { get(target, prop, receiver) { if (prop in overrides) return overrides[prop]; + // Proxy forwarding requires dynamic property access. + // oxlint-disable-next-line anti-slop/no-reflect-get const value = Reflect.get(target, prop, receiver); if (typeof value === "function") return value.bind(target); return value; diff --git a/packages/denokv/deno.json b/packages/denokv/deno.json index f2d4a23b6..3ec400bee 100644 --- a/packages/denokv/deno.json +++ b/packages/denokv/deno.json @@ -18,7 +18,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env --allow-read --allow-write" } } diff --git a/packages/denokv/src/mod.ts b/packages/denokv/src/mod.ts index ffbe743c3..4d03c4472 100644 --- a/packages/denokv/src/mod.ts +++ b/packages/denokv/src/mod.ts @@ -170,7 +170,7 @@ export class DenoKvStore implements KvStore { */ interface WrappedMessage { readonly __fedify_ordering_key__?: string; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any readonly __fedify_payload__: any; } @@ -242,7 +242,7 @@ export class DenoKvMessageQueue implements MessageQueue, Disposable { } async enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions | undefined, ): Promise { @@ -259,7 +259,7 @@ export class DenoKvMessageQueue implements MessageQueue, Disposable { } listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: (message: any) => void | Promise, options: MessageQueueListenOptions = {}, ): Promise { diff --git a/packages/elysia/deno.json b/packages/elysia/deno.json index b09b2fe5a..39fe40ca2 100644 --- a/packages/elysia/deno.json +++ b/packages/elysia/deno.json @@ -16,6 +16,6 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts" + "check": "deno fmt --check && deno check src/*.ts" } } diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 7f68ec7e4..ca6f83403 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -51,9 +51,9 @@ export const fedify = ( return response; } - response.headers.forEach((value, key) => { + for (const [key, value] of response.headers) { set.headers[key] = value; - }); + } // Return empty response for successful requests without body return new Response(null, { status: response.status }); @@ -111,7 +111,9 @@ export const fedify = ( ].includes(name) ) delete set.headers[header]; } - const fields = vary.map((field) => field.trim()).filter(Boolean); + const fields = vary.flatMap((field) => + field.trim() ? [field.trim()] : [] + ); if ( !fields.some((field) => field === "*" || field.toLowerCase() === "accept" diff --git a/packages/express/deno.json b/packages/express/deno.json index 7b3103c60..5a48089e8 100644 --- a/packages/express/deno.json +++ b/packages/express/deno.json @@ -19,7 +19,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check *.ts", + "check": "deno fmt --check && deno check *.ts", "test": "deno test --allow-all" } } diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index 7c9447fa6..6decad27e 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -230,7 +230,7 @@ function waitForReadable( function setEResponse(res: EResponse, response: Response): Promise { res.status(response.status); - response.headers.forEach((value, key) => res.setHeader(key, value)); + for (const [key, value] of response.headers) res.setHeader(key, value); if (response.body == null) return Promise.resolve(); const body = response.body; return new Promise((resolve) => { diff --git a/packages/fastify/deno.json b/packages/fastify/deno.json index 1c62ab1af..11ca68c2e 100644 --- a/packages/fastify/deno.json +++ b/packages/fastify/deno.json @@ -20,7 +20,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-all" } } diff --git a/packages/fedify/deno.json b/packages/fedify/deno.json index e066c46ad..313cba66a 100644 --- a/packages/fedify/deno.json +++ b/packages/fedify/deno.json @@ -60,7 +60,7 @@ ] }, "check": { - "command": "deno fmt --check && deno lint && deno check src/**/*.ts", + "command": "deno fmt --check && deno check src/**/*.ts", "dependencies": [ "codegen" ] diff --git a/packages/fedify/src/cfworkers/runner.ts b/packages/fedify/src/cfworkers/runner.ts index 98dc74791..0e338921d 100644 --- a/packages/fedify/src/cfworkers/runner.ts +++ b/packages/fedify/src/cfworkers/runner.ts @@ -12,7 +12,7 @@ export interface TestDefinition { ctx: { name: string; origin: string; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any step: any; env: unknown; messageBatches: MessageBatch[]; @@ -60,22 +60,22 @@ export function createTestWorker(tests: readonly TestDefinition[]) { let failed = false; let firstError: unknown; let capturedLogs: LogRecord[] | undefined = undefined; - // deno-lint-ignore no-inner-declarations + // oxlint-disable-next-line eslint/no-inner-declarations async function step( arg: string | { name: string; ignore?: boolean; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any fn: (def: any) => void | Promise; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any } | ((ctx: any) => void | Promise), - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any fn?: (ctx: any) => void | Promise, ) { let def: { name: string; ignore?: boolean; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any fn: (def: any) => void | Promise; }; if (typeof arg === "string") { diff --git a/packages/fedify/src/federation/activity-listener.ts b/packages/fedify/src/federation/activity-listener.ts index 36341935e..46bf8e0c5 100644 --- a/packages/fedify/src/federation/activity-listener.ts +++ b/packages/fedify/src/federation/activity-listener.ts @@ -1,7 +1,7 @@ import { Activity } from "@fedify/vocab"; type ActivityConstructor = - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any new (...args: any[]) => TActivity; type ActivityListener = ( diff --git a/packages/fedify/src/federation/builder.test.ts b/packages/fedify/src/federation/builder.test.ts index 8a597cfd0..4b18297e5 100644 --- a/packages/fedify/src/federation/builder.test.ts +++ b/packages/fedify/src/federation/builder.test.ts @@ -797,9 +797,7 @@ test("FederationBuilder", async (t) => { _ctx: unknown, _activity: Activity, _reason: UnverifiedActivityReason, - ) => { - return; - }; + ) => {}; builder .setInboxListeners("/users/{identifier}/inbox") diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index 268368598..14d236dc4 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -972,7 +972,7 @@ export class FederationBuilderImpl >(); const setters: OutboxListenerSetters = { on( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any type: new (...args: any[]) => TActivity, listener: OutboxListener, ): OutboxListenerSetters { @@ -1357,7 +1357,7 @@ export class FederationBuilderImpl >(); const setters: InboxListenerSetters = { on( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any type: new (...args: any[]) => TActivity, listener: InboxListener, options?: InboxListenerOptions, diff --git a/packages/fedify/src/federation/compound-fanout.test.ts b/packages/fedify/src/federation/compound-fanout.test.ts index a06ebe222..c387d41e9 100644 --- a/packages/fedify/src/federation/compound-fanout.test.ts +++ b/packages/fedify/src/federation/compound-fanout.test.ts @@ -199,8 +199,8 @@ test("forced fanout signs before serializing so the child survives", async () => // The fanout worker hands the activity on to the outbox queue, so drain // every message the run produces. assertEquals(queued.length, 1); - for (let i = 0; i < queued.length; i++) { - await federation.processQueuedTask(undefined, queued[i]); + for (const message of queued) { + await federation.processQueuedTask(undefined, message); } assert(delivered != null); @@ -288,8 +288,8 @@ test("an already signed activity keeps its single proof through fanout", async ( activity, { fanout: "force" }, ); - for (let i = 0; i < queued.length; i++) { - await federation.processQueuedTask(undefined, queued[i]); + for (const message of queued) { + await federation.processQueuedTask(undefined, message); } assert(delivered != null); diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index 78523ff85..f8e18c06b 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -1667,7 +1667,7 @@ export interface OutboxListenerSetters { * @since 2.2.0 */ on( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any type: new (...args: any[]) => TActivity, listener: OutboxListener, ): OutboxListenerSetters; @@ -1742,7 +1742,7 @@ export interface InboxListenerSetters { * @returns The setters object so that settings can be chained. */ on( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any type: new (...args: any[]) => TActivity, listener: InboxListener, options?: InboxListenerOptions, @@ -1997,7 +1997,7 @@ export interface CustomCollectionCallbackSetters< * @template TObject The type of the object. */ export type ConstructorWithTypeId = - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any (new (...args: any[]) => TObject) & { typeId: URL }; /** diff --git a/packages/fedify/src/federation/handler.ts b/packages/fedify/src/federation/handler.ts index 7a2c3a38b..73dbe27e0 100644 --- a/packages/fedify/src/federation/handler.ts +++ b/packages/fedify/src/federation/handler.ts @@ -6,7 +6,6 @@ import { verificationObservation, } from "../sig/verification.ts"; import type { AcceptSignatureParameters } from "@fedify/fedify/sig"; -import type { Recipient } from "@fedify/vocab"; import { Activity, Collection, @@ -18,6 +17,7 @@ import { Object, OrderedCollection, OrderedCollectionPage, + type Recipient, Tombstone, } from "@fedify/vocab"; import { @@ -3430,7 +3430,7 @@ async function _handleOrderedCollection< async function respondWithCustomCollection( request: Request, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: CustomCollectionHandler, context: RequestContext, portable: PortableCollectionRepresentation | undefined, @@ -4065,6 +4065,8 @@ class HandlerError extends Error { * @returns Never returns, always throws. */ throw(): never { + // The receiver is the error instance itself. + // oxlint-disable-next-line eslint/no-throw-literal throw this; } } diff --git a/packages/fedify/src/federation/inbox-observation.ts b/packages/fedify/src/federation/inbox-observation.ts index 2d495508b..65e2c986a 100644 --- a/packages/fedify/src/federation/inbox-observation.ts +++ b/packages/fedify/src/federation/inbox-observation.ts @@ -1,8 +1,7 @@ import { Activity } from "@fedify/vocab"; -import { getLogger } from "@logtape/logtape"; import type { Span } from "@opentelemetry/api"; import type { OutboundInboxRequest } from "./outbound-report.ts"; -import { withContext } from "@logtape/logtape"; +import { getLogger, withContext } from "@logtape/logtape"; import type { RequestContext } from "./context.ts"; import type { InboxAuthentication, diff --git a/packages/fedify/src/federation/inbox-report.test.ts b/packages/fedify/src/federation/inbox-report.test.ts index cc3fdf8bd..57edf0579 100644 --- a/packages/fedify/src/federation/inbox-report.test.ts +++ b/packages/fedify/src/federation/inbox-report.test.ts @@ -852,6 +852,8 @@ test("inbox request cancellation retains accepted queue work and its separate li federation.setInboxListeners("/users/{identifier}/inbox", "/inbox") .on(Create, (ctx) => { processed++; + // Read the internal signal property that is not part of the public type. + // oxlint-disable-next-line anti-slop/no-reflect-get workerSignal = Reflect.get(ctx, "signal"); }) .onRequestFinished((_ctx, report) => { diff --git a/packages/fedify/src/federation/inbox-view.ts b/packages/fedify/src/federation/inbox-view.ts index 82c28d5ee..618601c07 100644 --- a/packages/fedify/src/federation/inbox-view.ts +++ b/packages/fedify/src/federation/inbox-view.ts @@ -294,7 +294,7 @@ export function createSnapshotOnlyLoader( for (const entry of entries) { if (entry.policy === policy) responses.set(entry.url, entry.response); } - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await return markEffectiveContextLoader(async (url) => { const response = responses.get(normalizeUrl(url)); if (response == null) { @@ -351,10 +351,10 @@ export async function expandDocument( document: unknown, contextLoader: DocumentLoader, ): Promise[]> { - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any return await jsonld.expand(document as any, { documentLoader: contextLoader, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any } as any) as Record[]; } diff --git a/packages/fedify/src/federation/middleware.test.ts b/packages/fedify/src/federation/middleware.test.ts index 1bc0019ff..0d1cf728b 100644 --- a/packages/fedify/src/federation/middleware.test.ts +++ b/packages/fedify/src/federation/middleware.test.ts @@ -21,6 +21,7 @@ import { FetchError, getDocumentLoader, UrlError } from "@fedify/vocab-runtime"; import type { LookupWebFingerOptions } from "@fedify/webfinger"; import { configure, + getTextFormatter, type LogRecord, reset, withContext, @@ -274,10 +275,26 @@ test("createFederation()", async (t) => { assertEquals(records[0].level, "warning"); assertEquals( records[0].rawMessage, - "Fedify benchmarkMode is enabled; private address checks " + - "disabled (allowPrivateAddress=true); HTTP Signature time " + - "window disabled (signatureTimeWindow=false). Benchmark " + - "endpoints are active and must not be used in production.", + "Fedify benchmarkMode is enabled; {relaxationSummary}. " + + "Benchmark endpoints are active and must not be used in " + + "production.", + ); + assertEquals( + records[0].properties.relaxationSummary, + "private address checks disabled (allowPrivateAddress=true); " + + "HTTP Signature time window disabled " + + "(signatureTimeWindow=false)", + ); + const formatted = getTextFormatter({ timestamp: "disabled" })( + records[0], + ); + assert( + formatted.includes( + "private address checks disabled (allowPrivateAddress=true); " + + "HTTP Signature time window disabled " + + "(signatureTimeWindow=false)", + ), + formatted, ); assertEquals( records[0].properties.relaxations, @@ -2605,7 +2622,6 @@ test("Federation.fetch()", async (t) => { federation.setInboxListeners("/users/{identifier}/inbox", "/inbox") .on(vocab.Create, (_ctx, activity) => { inbox.push(activity.id!.toString()); - return; }); return { @@ -5297,7 +5313,7 @@ test("Federation.setMediaUploader()", async (t) => { headers: { "content-type": "multipart/form-data; boundary=abc" }, body: erroringBody, duplex: "half", - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any } as any), { contextData: undefined }, ); @@ -9409,7 +9425,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => { ]); assertEquals(state?.state, "open"); assertEquals(Array.isArray(state?.failures), true); - assertEquals((state?.failures as unknown[]).length, 1); + assertEquals((state?.failures as unknown[] | undefined)?.length, 1); assertExists(state?.opened); }); @@ -9909,7 +9925,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => { "unavailable.example", ]); assertEquals(state?.state, "closed"); - assertEquals((state?.failures as unknown[]).length, 1); + assertEquals((state?.failures as unknown[] | undefined)?.length, 1); }); await t.step("503 Retry-After delays newly opened circuit hold", async () => { @@ -13216,7 +13232,7 @@ test("createFederation() forwards DocumentLoaderFactoryOptions to a user-supplie impl.authenticatedDocumentLoaderFactory( { keyId: new URL("https://example.com/users/alice#main-key"), - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any privateKey: {} as any, }, { allowPrivateAddress: true, userAgent: "test-ua" }, @@ -13278,7 +13294,7 @@ class RendezvousQueue implements MessageQueue { this.#openGate(); } - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise { this.enqueued.push({ message, options }); if (++this.#count >= this.expected) this.#markDispatched(); @@ -13286,7 +13302,7 @@ class RendezvousQueue implements MessageQueue { } listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any _handler: (message: any) => Promise | void, _options?: MessageQueueListenOptions, ): Promise { @@ -13508,7 +13524,7 @@ test("ContextImpl.enqueueTaskMany()", async (t) => { undefined, ); await rejects( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any () => ctx.enqueueTaskMany(task, [1, "two", 3] as any), { name: "TypeError", message: /Task data failed schema validation/ }, ); diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index 04b369f7c..4ecd33359 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -845,9 +845,11 @@ export class FederationImpl ); const relaxationSummary = formatBenchmarkRelaxations(relaxations); getLogger(["fedify", "federation", "benchmark"]).warn( - `Fedify benchmarkMode is enabled; ${relaxationSummary}. Benchmark endpoints are active and must not be used in production.`, + "Fedify benchmarkMode is enabled; {relaxationSummary}. " + + "Benchmark endpoints are active and must not be used in production.", { relaxations, + relaxationSummary, }, ); } @@ -3869,9 +3871,9 @@ export class FederationImpl recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message); }), ); - const errors = promises - .filter((r) => r.status === "rejected") - .map((r) => (r as PromiseRejectedResult).reason); + const errors = promises.flatMap((r) => + r.status === "rejected" ? [(r as PromiseRejectedResult).reason] : [] + ); if (errors.length > 0) { logger.error( "Failed to enqueue activity {activityId} to send later: {errors}", @@ -6132,14 +6134,12 @@ export class ContextImpl implements Context { } else { identifierPromise = Promise.resolve(identity.identifier); } - return identifierPromise.then((identifier) => { + return identifierPromise.then(async (identifier) => { if (identifier == null) return this.documentLoader; - const keyPair = this.getRsaKeyPairFromIdentifier(identifier); - return keyPair.then((pair) => - pair == null - ? this.documentLoader - : this.federation.authenticatedDocumentLoaderFactory(pair) - ); + const pair = await this.getRsaKeyPairFromIdentifier(identifier); + return pair == null + ? this.documentLoader + : this.federation.authenticatedDocumentLoaderFactory(pair); }); } return this.federation.authenticatedDocumentLoaderFactory(identity); @@ -7043,7 +7043,7 @@ class RequestContextImpl extends ContextImpl isInActorDispatcher(): boolean { return this.#invokedFromActorDispatcher != null; } - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any override readonly url: URL = undefined as any; // An own property, so that the contexts derived by spreading this one, e.g., // in getActor() and getObject(), keep it: @@ -7722,9 +7722,9 @@ async function forwardActivityInternal( ); }), ); - const errors: unknown[] = promises - .filter((r) => r.status === "rejected") - .map((r) => (r as PromiseRejectedResult).reason); + const errors: unknown[] = promises.flatMap((r) => + r.status === "rejected" ? [(r as PromiseRejectedResult).reason] : [] + ); if (errors.length > 0) { logger.error( "Failed to enqueue activity {activityId} to forward later:\n{errors}", diff --git a/packages/fedify/src/federation/mq.ts b/packages/fedify/src/federation/mq.ts index 20f9f4261..2091cc08b 100644 --- a/packages/fedify/src/federation/mq.ts +++ b/packages/fedify/src/federation/mq.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any /** * Additional options for enqueuing a message in a queue. diff --git a/packages/fedify/src/federation/negotiation.ts b/packages/fedify/src/federation/negotiation.ts index ed2803be7..c64f3a00c 100644 --- a/packages/fedify/src/federation/negotiation.ts +++ b/packages/fedify/src/federation/negotiation.ts @@ -55,7 +55,7 @@ interface MediaTypeSpecificity extends Specificity { params: { [param: string]: string | undefined }; } -const simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/; +const simpleMediaTypeRegExp = /^\s*([^\s/;]+)\/([^;\s]+)\s*(?:;(.*))?$/; function splitKeyValuePair(str: string): [string, string | undefined] { const [key, value] = str.split("="); diff --git a/packages/fedify/src/federation/outbound-report.test.ts b/packages/fedify/src/federation/outbound-report.test.ts index 3028d1aab..d2531a8e3 100644 --- a/packages/fedify/src/federation/outbound-report.test.ts +++ b/packages/fedify/src/federation/outbound-report.test.ts @@ -1586,7 +1586,7 @@ test("outbound observers isolate callback and logging errors from success and or federation.setOutboundObservers({ onBroadcast() { calls++; - throw "observer failed"; + throw new Error("observer failed"); }, }); const ctx = federation.createContext( @@ -4435,6 +4435,8 @@ test("outbound worker-only observers count real sends and report local settlemen if (property === Symbol.iterator && ++reads === 1) { throw preparationError; } + // Proxy forwarding requires dynamic property access. + // oxlint-disable-next-line anti-slop/no-reflect-get return Reflect.get(target, property, receiver); }, }), diff --git a/packages/fedify/src/federation/outgoing-proof.test.ts b/packages/fedify/src/federation/outgoing-proof.test.ts index 9e0021163..4b961034e 100644 --- a/packages/fedify/src/federation/outgoing-proof.test.ts +++ b/packages/fedify/src/federation/outgoing-proof.test.ts @@ -570,8 +570,8 @@ test("forced fanout selects the DID-matching key before enqueueing", async () => // Every transport key still travels with the message. assertEquals(message.keys.length, 3); assertEquals(message.keys[0].keyId, rsaKey.keyId.href); - for (let i = 0; i < queued.length; i++) { - await federation.processQueuedTask(undefined, queued[i]); + for (const message of queued) { + await federation.processQueuedTask(undefined, message); } }); assertEquals(bodies.length, 1); @@ -972,8 +972,8 @@ test("forced fanout selects the DID-matching key for a compatible-ID activity", { fanout: "force" }, ); assertEquals(queued.length, 1); - for (let i = 0; i < queued.length; i++) { - await federation.processQueuedTask(undefined, queued[i]); + for (const message of queued) { + await federation.processQueuedTask(undefined, message); } }); assertEquals(bodies.length, 1); @@ -1187,8 +1187,8 @@ test("fanout warns once before enqueueing a compatible activity", async () => { { fanout: "force" }, ); assertEquals(queued.length, 1); - for (let i = 0; i < queued.length; i++) { - await federation.processQueuedTask(undefined, queued[i]); + for (const message of queued) { + await federation.processQueuedTask(undefined, message); } }) ); diff --git a/packages/fedify/src/federation/portable-collection.test.ts b/packages/fedify/src/federation/portable-collection.test.ts index 00144200e..397fa9e00 100644 --- a/packages/fedify/src/federation/portable-collection.test.ts +++ b/packages/fedify/src/federation/portable-collection.test.ts @@ -679,7 +679,7 @@ test("Federation.fetch() serves portable custom collections", async (t) => { portableId("/users/alice/bookmarks?tag=fedify&cursor=1"), ); assertEquals( - (json?.orderedItems as Record[])[0].id, + (json?.orderedItems as Record[] | undefined)?.[0]?.id, portableId("/notes/fedify-0"), ); }); diff --git a/packages/fedify/src/federation/send.test.ts b/packages/fedify/src/federation/send.test.ts index ecb32dc67..60e690d7e 100644 --- a/packages/fedify/src/federation/send.test.ts +++ b/packages/fedify/src/federation/send.test.ts @@ -1473,11 +1473,11 @@ async function captureLogs(fn: () => Promise): Promise { // Gets the URLs of the logged failures to cancel response bodies: function getCancelFailureUrls(records: readonly LogRecord[]): unknown[] { - return records - .filter((r) => - r.rawMessage === "Failed to cancel the response body from {url}: {error}" - ) - .map((r) => r.properties.url); + return records.flatMap((r) => + r.rawMessage === "Failed to cancel the response body from {url}: {error}" + ? [r.properties.url] + : [] + ); } test("fetchWithValidatedRedirects() logs a redirect body failing to cancel and follows the redirect", async (t) => { diff --git a/packages/fedify/src/federation/tasks/codec.ts b/packages/fedify/src/federation/tasks/codec.ts index 7a66336e0..ac75893bc 100644 --- a/packages/fedify/src/federation/tasks/codec.ts +++ b/packages/fedify/src/federation/tasks/codec.ts @@ -245,5 +245,5 @@ const classReviver = ( })(); }; -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any type Constructor = new (...arg: any[]) => T; diff --git a/packages/fedify/src/federation/tasks/enqueue.test.ts b/packages/fedify/src/federation/tasks/enqueue.test.ts index 97777f5c0..09a6a89f4 100644 --- a/packages/fedify/src/federation/tasks/enqueue.test.ts +++ b/packages/fedify/src/federation/tasks/enqueue.test.ts @@ -74,7 +74,7 @@ test("enqueueTasks() validation and dispatch", async (t) => { undefined, ); await rejects( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any () => ctx.enqueueTask(task, "not a number" as any), { name: "TypeError", message: /Task data failed schema validation/ }, ); diff --git a/packages/fedify/src/nodeinfo/client.test.ts b/packages/fedify/src/nodeinfo/client.test.ts index 9d3cd6f57..ed832d6e8 100644 --- a/packages/fedify/src/nodeinfo/client.test.ts +++ b/packages/fedify/src/nodeinfo/client.test.ts @@ -1183,7 +1183,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { let responses = 0; return { events, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async openHop(url) { opens++; events.push(`open ${url}`); @@ -1191,7 +1191,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { let closed = false; return { url: new URL(url), - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async fetch(input) { if (closed) throw new TypeError("The fetch session is closed."); const target = new URL(input instanceof Request ? input.url : input); @@ -1228,7 +1228,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { } return response; }, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async close() { if (closed) return; closed = true; diff --git a/packages/fedify/src/sig/http.test.ts b/packages/fedify/src/sig/http.test.ts index d84fa379e..e77f48a28 100644 --- a/packages/fedify/src/sig/http.test.ts +++ b/packages/fedify/src/sig/http.test.ts @@ -1485,9 +1485,9 @@ test("verifyRequestDetailed() [rfc9421] caps the number of signatures", async (t ); headers.set( "Signature", - labels.filter((label) => !label.startsWith("nobytes")) - .map((label) => signatures[label]) - .join(", "), + labels.flatMap((label) => + label.startsWith("nobytes") ? [] : [signatures[label]] + ).join(", "), ); return new Request(signed.url, { method: "POST", diff --git a/packages/fedify/src/sig/key.ts b/packages/fedify/src/sig/key.ts index 2dba6fa52..13e723d77 100644 --- a/packages/fedify/src/sig/key.ts +++ b/packages/fedify/src/sig/key.ts @@ -784,7 +784,7 @@ export function bypassKeyCacheReads(keyCache?: KeyCache): KeyCache { } type FetchableKeyClass = - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any (new (...args: any[]) => T) & { fromJsonLd( jsonLd: unknown, @@ -1911,7 +1911,11 @@ async function fetchKeyInternal( } catch { // A custom loader can provide a response whose body is already used. // Collecting evidence must preserve the original verification result. - result = { error: error instanceof Error ? error : new Error() }; + result = { + error: error instanceof Error + ? error + : new Error("Unknown verification error"), + }; } check.reason = { type: "keyFetchError", diff --git a/packages/fedify/src/sig/portable-collection.test.ts b/packages/fedify/src/sig/portable-collection.test.ts index e202fc6fa..73d466971 100644 --- a/packages/fedify/src/sig/portable-collection.test.ts +++ b/packages/fedify/src/sig/portable-collection.test.ts @@ -10,6 +10,7 @@ import { } from "@fedify/vocab"; import { type DocumentLoader, + encodeMultibase, exportDidKey, FetchError, type PortableObjectReferrer, @@ -19,7 +20,6 @@ import { import { deepStrictEqual, ok, strictEqual } from "node:assert/strict"; import serialize from "json-canon"; import { ed25519PrivateKey, ed25519PublicKey } from "../testing/keys.ts"; -import { encodeMultibase } from "@fedify/vocab-runtime"; import { verifyPortableObject, type VerifyPortableObjectOptions, diff --git a/packages/fedify/src/sig/proof.test.ts b/packages/fedify/src/sig/proof.test.ts index 83d85e565..35ad53fc0 100644 --- a/packages/fedify/src/sig/proof.test.ts +++ b/packages/fedify/src/sig/proof.test.ts @@ -2989,7 +2989,7 @@ test("verifyObject() does not pass its proof verifier on to cached proofs", asyn { "@graph": [{ "@id": proofUrl }] }, ]; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const rootVerifier = async () => ({ verified: false }); for (const verifyPortableObject of [undefined, rootVerifier]) { let proofFetches = 0; @@ -3714,7 +3714,7 @@ test("verifyPortableObjectProof() verifies objects fetched through gateways", as object, }, { contextLoader: mockDocumentLoader }); return await create.getObject({ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (requestUrl) => { if (requestUrl !== url) { throw new Error(`Unexpected URL: ${requestUrl}`); diff --git a/packages/fedify/src/sig/proof.ts b/packages/fedify/src/sig/proof.ts index 46cf468f8..e1e2be3c7 100644 --- a/packages/fedify/src/sig/proof.ts +++ b/packages/fedify/src/sig/proof.ts @@ -286,7 +286,7 @@ async function createProofInternal( const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes); created ??= Temporal.Now.instant(); const proofConfig = { - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any "@context": (compactMsg as any)["@context"], type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", @@ -2502,7 +2502,7 @@ export interface VerifyObjectOptions extends VerifyProofOptions { * @since 0.10.0 */ export async function verifyObject( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any cls: (new (...args: any[]) => T) & { fromJsonLd(jsonLd: unknown, options: VerifyObjectOptions): Promise; }, @@ -2559,7 +2559,7 @@ export type VerifyObjectDetailedResult = * @internal */ export async function verifyObjectDetailed( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any cls: (new (...args: any[]) => T) & { fromJsonLd(jsonLd: unknown, options: VerifyObjectOptions): Promise; }, @@ -2655,7 +2655,7 @@ function isRetryableKeyFetchError(result: FetchKeyErrorResult): boolean { } async function verifyObjectCore( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any cls: (new (...args: any[]) => T) & { fromJsonLd(jsonLd: unknown, options: VerifyObjectOptions): Promise; }, @@ -2704,7 +2704,7 @@ async function verifyObjectCore( let acceptedPortableProofs = 0; // A portable proof document carries no proof of its own; it is // authenticated below, by verifying it against the object: - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const acceptPortableProofDocument: PortableObjectVerifier = async () => { acceptedPortableProofs++; return { verified: true }; diff --git a/packages/fedify/src/utils/docloader.test.ts b/packages/fedify/src/utils/docloader.test.ts index 28fbcc7e0..cfc8fdddd 100644 --- a/packages/fedify/src/utils/docloader.test.ts +++ b/packages/fedify/src/utils/docloader.test.ts @@ -984,11 +984,11 @@ async function captureLogs(fn: () => Promise): Promise { // Gets the error messages of the logged failures to cancel response bodies: function getCancelFailures(records: readonly LogRecord[]): string[] { - return records - .filter((r) => - r.rawMessage === "Failed to cancel the response body from {url}: {error}" - ) - .map((r) => (r.properties.error as Error).message); + return records.flatMap((r) => + r.rawMessage === "Failed to cancel the response body from {url}: {error}" + ? [(r.properties.error as Error).message] + : [] + ); } const linkAlternate = (next: string): RecordedResponseInit => ({ diff --git a/packages/fedify/src/utils/kv-cache.test.ts b/packages/fedify/src/utils/kv-cache.test.ts index 4642aa9c2..2a792ed4b 100644 --- a/packages/fedify/src/utils/kv-cache.test.ts +++ b/packages/fedify/src/utils/kv-cache.test.ts @@ -162,7 +162,7 @@ test("kvCache()", async (t) => { delete(_key: KvKey): Promise { throw new Error("Failed to delete key"); } - // deno-lint-ignore require-yield + // oxlint-disable-next-line eslint/require-yield async *list(_prefix?: KvKey): AsyncIterable { throw new Error("Failed to list keys"); } diff --git a/packages/fixture/deno.json b/packages/fixture/deno.json index 7a7023aa9..a8e6d559b 100644 --- a/packages/fixture/deno.json +++ b/packages/fixture/deno.json @@ -11,7 +11,7 @@ "node_modules" ], "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts" + "check": "deno fmt --check && deno check src/*.ts" }, "publish": false } diff --git a/packages/fixture/src/docloader.ts b/packages/fixture/src/docloader.ts index 813945a1e..f0cd65395 100644 --- a/packages/fixture/src/docloader.ts +++ b/packages/fixture/src/docloader.ts @@ -50,7 +50,7 @@ export async function mockDocumentLoader( } const pkg = "Deno" in globalThis ? "." : "@fedify/fixture"; const fixturePath = `${pkg}/fixtures/${url.host}${url.pathname}.json`; - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any let document: any; try { document = (await import(fixturePath, { with: { type: "json" } })).default; diff --git a/packages/fixture/src/test.ts b/packages/fixture/src/test.ts index 0933400fc..81407abcb 100644 --- a/packages/fixture/src/test.ts +++ b/packages/fixture/src/test.ts @@ -134,16 +134,13 @@ export function test( }); } else if ("Bun" in globalThis) { let failed: unknown = undefined; - // deno-lint-ignore no-inner-declarations function step(def: TestStepDefinition): Promise; - // deno-lint-ignore no-inner-declarations function step( name: string, fn: TestFunction, ): Promise; - // deno-lint-ignore no-inner-declarations function step(fn: TestFunction): Promise; - // deno-lint-ignore no-inner-declarations + // oxlint-disable-next-line eslint/no-inner-declarations async function step( defOrNameOrFn: | TestStepDefinition @@ -177,7 +174,7 @@ export function test( origin: "", step, }; - // deno-lint-ignore no-inner-declarations + // oxlint-disable-next-line eslint/no-inner-declarations async function fn() { await def.fn(ctx); if (failed) throw failed; @@ -260,7 +257,7 @@ function caller() { }; const _ = Trace.prepareStackTrace; Trace.prepareStackTrace = (_, stack) => stack; - const { stack } = new Error(); + const { stack } = new Error("capture stack"); Trace.prepareStackTrace = _; const caller = (stack as unknown as CallSite[])[2]; return caller.getFileName().replaceAll("\\", "/"); diff --git a/packages/fresh/deno.json b/packages/fresh/deno.json index 0a853ee9f..33b42b8a8 100644 --- a/packages/fresh/deno.json +++ b/packages/fresh/deno.json @@ -4,7 +4,7 @@ "license": "MIT", "imports": { "@fresh/core": "jsr:@fresh/core@^2.1.4", - "@logtape/logtape": "jsr:@logtape/logtape@^2.2.0", + "@logtape/logtape": "jsr:@logtape/logtape@2.4.2", "fresh": "jsr:@fresh/core@^2.1.4" }, "exports": { @@ -20,7 +20,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-all src/mod.test.ts" } } diff --git a/packages/fresh/src/mod.test.ts b/packages/fresh/src/mod.test.ts index 2a504c271..c2b50db23 100644 --- a/packages/fresh/src/mod.test.ts +++ b/packages/fresh/src/mod.test.ts @@ -97,6 +97,8 @@ Deno.test("integrateFetchOptions() - onNotFound delegates to ctx.next with corre const mockCtx = { next(req?: Request) { + // This assignment intentionally captures the method receiver for the test. + // oxlint-disable-next-line typescript/no-this-alias capturedThis = this; passedRequest = req; return Promise.resolve(notFoundResponse); diff --git a/packages/h3/deno.json b/packages/h3/deno.json index 80e0fe924..b376c043b 100644 --- a/packages/h3/deno.json +++ b/packages/h3/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-all" } } diff --git a/packages/hono/deno.json b/packages/hono/deno.json index 3839e5061..9b0a63b0b 100644 --- a/packages/hono/deno.json +++ b/packages/hono/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" } } diff --git a/packages/hono/src/mod.test.ts b/packages/hono/src/mod.test.ts index 1db15697b..5fdd6fda0 100644 --- a/packages/hono/src/mod.test.ts +++ b/packages/hono/src/mod.test.ts @@ -9,6 +9,8 @@ interface MockHonoContext { res: Response; } +// The unused type parameter keeps the mock generic at its call sites. +// oxlint-disable-next-line eslint/no-unused-vars interface MockFederation { fetch(request: Request, options: unknown): Promise; } diff --git a/packages/init/deno.json b/packages/init/deno.json index 1ec086d8b..0480d4163 100644 --- a/packages/init/deno.json +++ b/packages/init/deno.json @@ -17,7 +17,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/**/*.ts", + "check": "deno fmt --check && deno check src/**/*.ts", "run": "deno run --allow-all src/mod.ts", "update-init-deps": "node --experimental-strip-types scripts/update_init_deps.ts" }, @@ -26,16 +26,6 @@ "src/templates/**" ] }, - "lint": { - "exclude": [ - "src/templates/**" - ], - "rules": { - "exclude": [ - "no-slow-types" - ] - } - }, "test": { "exclude": [ "src/test/**" diff --git a/packages/init/src/action/utils.ts b/packages/init/src/action/utils.ts index f36bd94e9..b2a052b5a 100644 --- a/packages/init/src/action/utils.ts +++ b/packages/init/src/action/utils.ts @@ -61,7 +61,8 @@ export function stringifyEnvs(object: Record): string { let escapedValue = value ?? ""; if (key.startsWith("#")) { - // deno-lint-ignore no-console + // This warning is user-facing CLI output, not debug logging. + // oxlint-disable-next-line eslint/no-console console.warn( `key starts with a '#' indicates a comment and is ignored: '${key}'`, ); diff --git a/packages/init/src/json/deps.json b/packages/init/src/json/deps.json index ad9d6693d..f900494ad 100644 --- a/packages/init/src/json/deps.json +++ b/packages/init/src/json/deps.json @@ -1,7 +1,7 @@ { "@hongminhee/x-forwarded-fetch": "^0.2.0", "@hono/hono": "^4.12.33", - "@logtape/logtape": "^2.3.0", + "@logtape/logtape": "^2.4.2", "@std/dotenv": "^0.225.7", "npm:@astrojs/node": "^11.0.3", "npm:@deno/astro-adapter": "^0.6.0", diff --git a/packages/init/src/test/fill.ts b/packages/init/src/test/fill.ts index bdf9063bb..eb1a9c181 100644 --- a/packages/init/src/test/fill.ts +++ b/packages/init/src/test/fill.ts @@ -25,6 +25,8 @@ export const fillEmptyOptions = ( ) as DefineAllOptions; type FilledOption = + // Deno accepts repeated `infer J` in this union; Oxlint's no-redeclare flags it. + // oxlint-disable-next-line eslint/no-redeclare T extends Awaited> | DefineOption ? DefineOption : DefineOption; diff --git a/packages/init/src/test/lookup.ts b/packages/init/src/test/lookup.ts index 99abaaef7..fdc9edf0d 100644 --- a/packages/init/src/test/lookup.ts +++ b/packages/init/src/test/lookup.ts @@ -88,6 +88,8 @@ export const isTestable = (dir: string): boolean => function printSkippedCases(dirs: string[]): void { const matchedPatterns = new Set( dirs.filter(Boolean).flatMap((dir) => + // The curried `join` helper makes this filter/map chain intentional. + // oxlint-disable-next-line anti-slop/no-array-filter-map BANNED_LOOKUP_CASES .filter(matchesLookupCasePattern(parseLookupCase(dir))) .map(join(",")) diff --git a/packages/interaction-controls/deno.json b/packages/interaction-controls/deno.json index 59be69eba..4a38f3e8b 100644 --- a/packages/interaction-controls/deno.json +++ b/packages/interaction-controls/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/**/*.ts", + "check": "deno fmt --check && deno check src/**/*.ts", "test": "deno test" } } diff --git a/packages/interaction-controls/src/collection.ts b/packages/interaction-controls/src/collection.ts index 420a4a163..7decf2769 100644 --- a/packages/interaction-controls/src/collection.ts +++ b/packages/interaction-controls/src/collection.ts @@ -127,9 +127,9 @@ export async function resolveAuthoritativeObject( // The candidate's loader serves the context documents the object // was parsed with, so the expansion is the parsed interpretation: const expanded = await jsonld.expand( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any candidate.document as any, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any { documentLoader: candidate.contextLoader } as any, ); const root = getRootNode(expanded); diff --git a/packages/interaction-controls/src/control.ts b/packages/interaction-controls/src/control.ts index ad1923e0f..325c5efde 100644 --- a/packages/interaction-controls/src/control.ts +++ b/packages/interaction-controls/src/control.ts @@ -1049,9 +1049,9 @@ async function materialize( documentUrl.origin === value.origin ) { const expanded = await jsonld.expand( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any remoteDocument.document as any, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any { documentLoader: memoizedContextLoader } as any, ); const root = getRootNode(expanded); @@ -1520,6 +1520,9 @@ export function recognized< TInteracting extends ASObject, TTarget extends ASObject, TImpoliteSource extends ASObject, + // This type parameter is part of the public generic API even though the + // function body does not reference it. + // oxlint-disable-next-line eslint/no-unused-vars TRequestValidationOptions extends object, >( values: { diff --git a/packages/interaction-controls/src/feature.test.ts b/packages/interaction-controls/src/feature.test.ts index e8d44cc7c..2e941232c 100644 --- a/packages/interaction-controls/src/feature.test.ts +++ b/packages/interaction-controls/src/feature.test.ts @@ -129,9 +129,9 @@ async function evidenceFor( requestSigners: readonly URL[] = [], ): Promise { const expandedActivity = await jsonld.expand( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any document as any, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any { documentLoader: getDocumentLoader() } as any, ) as Record[]; return { diff --git a/packages/koa/deno.json b/packages/koa/deno.json index c783de175..8862010cb 100644 --- a/packages/koa/deno.json +++ b/packages/koa/deno.json @@ -19,7 +19,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" } } diff --git a/packages/koa/src/index.ts b/packages/koa/src/index.ts index 026f03fc4..8900b1ad2 100644 --- a/packages/koa/src/index.ts +++ b/packages/koa/src/index.ts @@ -23,7 +23,7 @@ import { Readable } from "node:stream"; * @returns A context data for the {@link Federation} object. * @since 1.9.0 */ -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any export type ContextDataFactory = ( context: TKoaContext, ) => TContextData | Promise; @@ -40,7 +40,7 @@ export type ContextDataFactory = ( * @returns A Koa middleware. * @since 1.9.0 */ -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any export function createMiddleware( federation: Federation, contextDataFactory: ContextDataFactory, @@ -91,7 +91,7 @@ export function createMiddleware( }; } -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any function toRequest(ctx: any): { request: Request; body?: RequestBody } { const url = `${ctx.protocol}://${ctx.host}${ctx.url}`; const headers = new Headers(); @@ -239,10 +239,10 @@ function waitForReadable( }); } -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any function setResponse(ctx: any, response: Response): void { ctx.status = response.status; - response.headers.forEach((value, key) => ctx.set(key, value)); + for (const [key, value] of response.headers) ctx.set(key, value); if (response.body == null) return; const body = response.body; const reader = body.getReader(); @@ -259,7 +259,7 @@ function setResponse(ctx: any, response: Response): void { }); } -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any function setNotAcceptableResponse(ctx: any): void { // Downstream middleware has finished. Preserve application responses, // including explicit status-only 404s and raw Node responses. Koa sets diff --git a/packages/lint/deno.json b/packages/lint/deno.json index 7af92643c..6bc7fbc13 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -10,7 +10,7 @@ "@fedify/fixture": "jsr:@fedify/fixture@^2.2.14", "eslint": "npm:eslint@^9.0.0", "estree": "npm:@types/estree@^1.0.8", - "oxlint": "npm:oxlint@^1.66.0" + "oxlint": "npm:oxlint@1.90.0" }, "publish": { "exclude": [ diff --git a/packages/lint/src/lib/oxlint.ts b/packages/lint/src/lib/oxlint.ts index 74fa53479..11309116b 100644 --- a/packages/lint/src/lib/oxlint.ts +++ b/packages/lint/src/lib/oxlint.ts @@ -56,6 +56,13 @@ export const oxlintBin: string | null = findOxlint(); */ export const oxlintUnavailable: boolean = !pluginBuilt || oxlintBin == null; +if (oxlintUnavailable && process.env.FEDIFY_REQUIRE_OXLINT_TESTS === "1") { + throw new Error( + "Oxlint integration tests are required, but prerequisites are missing. " + + "Run `mise install && mise run prepare-each lint` first.", + ); +} + /** * Print a single warning explaining why the oxlint tests are being skipped. * Call this only when {@link oxlintUnavailable} is `true`. diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index ae512b423..fefcc661c 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -340,12 +340,11 @@ function* flatten(node: Node): Generator { case "WithStatement": yield* flatten(node.body); - return; + break; default: // For other node types (expressions, declarations, etc.), // we don't traverse deeper to avoid infinite recursion // from circular references like `parent` - return; } } diff --git a/packages/lint/src/lib/reachability.ts b/packages/lint/src/lib/reachability.ts index d3aa22e84..4788ddba0 100644 --- a/packages/lint/src/lib/reachability.ts +++ b/packages/lint/src/lib/reachability.ts @@ -296,7 +296,6 @@ export function collectReachableStatements( default: out.push(node); - return; } } @@ -468,7 +467,6 @@ function collectBoundNames(pattern: unknown, out: string[]): void { out, ); } - return; } } diff --git a/packages/lint/src/rules/outbox-listener-delivery-not-awaited.ts b/packages/lint/src/rules/outbox-listener-delivery-not-awaited.ts index be94a2dce..c083d40ef 100644 --- a/packages/lint/src/rules/outbox-listener-delivery-not-awaited.ts +++ b/packages/lint/src/rules/outbox-listener-delivery-not-awaited.ts @@ -520,7 +520,7 @@ function checkListener( }; for (let changed = true; changed;) { changed = false; - scopes.forEach((scope, index) => { + for (const [index, scope] of scopes.entries()) { for (const call of callsByScope[index]) { if ( carriesDelivery(call, scope) && noteCarrying(fateOf(call, analysis)) @@ -528,7 +528,7 @@ function checkListener( changed = true; } } - }); + } } const reported = new Set(); @@ -540,12 +540,12 @@ function checkListener( // A delivery call, or a call to a helper that delivers, whose promise is // dropped. - scopes.forEach((scope, index) => { + for (const [index, scope] of scopes.entries()) { for (const call of callsByScope[index]) { if (!carriesDelivery(call, scope)) continue; if (fateOf(call, analysis).fate === "dropped") flag(call); } - }); + } // A callback that awaits a delivery hands its own promise to whoever runs // it: `forEach()` drops it, an immediately invoked one is as safe as the @@ -561,7 +561,7 @@ function checkListener( // The same for a function that delivers when it is handed over by name, // as in `inboxes.forEach(deliver)`. - scopes.forEach((scope, index) => { + for (const [index, scope] of scopes.entries()) { for (const call of callsByScope[index]) { for (const arg of call.arguments as Node[]) { if (arg.type !== "Identifier") continue; @@ -572,7 +572,7 @@ function checkListener( } } } - }); + } } function createRule( diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts index 09eefe332..e45b63d0b 100644 --- a/packages/lint/src/tests/integration.test.ts +++ b/packages/lint/src/tests/integration.test.ts @@ -999,13 +999,17 @@ test("Integration: ❌ Multiple errors - missing id and inbox", () => map(prop("id")), toArray, (ids) => - [ - "actor-id-required", - "actor-inbox-property-required", - ].forEach((ruleName) => - ok( - ids.includes(`${PLUGIN_NAME}/${ruleName}`), - `Expected ${ruleName} error`, - ) - ), + (() => { + for ( + const ruleName of [ + "actor-id-required", + "actor-inbox-property-required", + ] + ) { + ok( + ids.includes(`${PLUGIN_NAME}/${ruleName}`), + `Expected ${ruleName} error`, + ); + } + })(), )); diff --git a/packages/mysql/deno.json b/packages/mysql/deno.json index 2a734867d..ead7a4987 100644 --- a/packages/mysql/deno.json +++ b/packages/mysql/deno.json @@ -18,7 +18,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" } } diff --git a/packages/mysql/src/mq.ts b/packages/mysql/src/mq.ts index d1178d8d0..36b241040 100644 --- a/packages/mysql/src/mq.ts +++ b/packages/mysql/src/mq.ts @@ -212,7 +212,7 @@ export class MysqlMessageQueue implements MessageQueue { * @since 2.1.0 */ async enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions, ): Promise { @@ -249,7 +249,7 @@ export class MysqlMessageQueue implements MessageQueue { * @since 2.1.0 */ async enqueueMany( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any messages: readonly any[], options?: MessageQueueEnqueueOptions, ): Promise { @@ -332,7 +332,7 @@ export class MysqlMessageQueue implements MessageQueue { * @since 2.1.0 */ async listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: (message: any) => void | Promise, options: MessageQueueListenOptions = {}, ): Promise { diff --git a/packages/nestjs/src/fedify.middleware.ts b/packages/nestjs/src/fedify.middleware.ts index b1a0d8221..5ae47d86d 100644 --- a/packages/nestjs/src/fedify.middleware.ts +++ b/packages/nestjs/src/fedify.middleware.ts @@ -93,7 +93,7 @@ function fromERequest(req: ERequest): Request { function setEResponse(res: EResponse, response: Response): Promise { res.status(response.status); - response.headers.forEach((value, key) => res.setHeader(key, value)); + for (const [key, value] of response.headers) res.setHeader(key, value); if (response.body == null) return Promise.resolve(); const body = response.body; return new Promise((resolve) => { diff --git a/packages/nestjs/src/fedify.module.ts b/packages/nestjs/src/fedify.module.ts index dc6e8c75d..68e3d1092 100644 --- a/packages/nestjs/src/fedify.module.ts +++ b/packages/nestjs/src/fedify.module.ts @@ -8,6 +8,8 @@ import { import { FEDIFY_FEDERATION } from "./fedify.constants.ts"; @Module({}) +// NestJS modules are classes that expose static factory methods by convention. +// oxlint-disable-next-line typescript/no-extraneous-class export class FedifyModule { static forRoot(options: FederationOptions): DynamicModule { const providers: Provider[] = [ diff --git a/packages/netlify/deno.json b/packages/netlify/deno.json index 0fc5b58a7..50b80f042 100644 --- a/packages/netlify/deno.json +++ b/packages/netlify/deno.json @@ -23,7 +23,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-env --allow-read" } } diff --git a/packages/netlify/test/kv.test.ts b/packages/netlify/test/kv.test.ts index 62fd669de..db576b91d 100644 --- a/packages/netlify/test/kv.test.ts +++ b/packages/netlify/test/kv.test.ts @@ -80,9 +80,9 @@ class MockStore { async *list(options: { prefix?: string }) { const prefix = options.prefix ?? ""; this.listPrefixes.push(prefix); - const blobs = [...this.entries] - .filter(([key]) => key.startsWith(prefix)) - .map(([key, entry]) => ({ key, etag: entry.etag ?? "" })); + const blobs = [...this.entries].flatMap(([key, entry]) => + key.startsWith(prefix) ? [{ key, etag: entry.etag ?? "" }] : [] + ); for (const blob of blobs) { yield { blobs: [blob], directories: [] }; } diff --git a/packages/nuxt/deno.json b/packages/nuxt/deno.json index 44f5f3858..367a0db8d 100644 --- a/packages/nuxt/deno.json +++ b/packages/nuxt/deno.json @@ -22,6 +22,6 @@ }, "tasks": { "test": "deno test --allow-net --allow-env", - "check": "deno fmt --check && deno lint && deno check src/*.ts src/runtime/server/*.ts" + "check": "deno fmt --check && deno check src/*.ts src/runtime/server/*.ts" } } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index b535e2c4a..ad127a24a 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -77,12 +77,12 @@ const contextDataFactory = contextFactoryModule.contextDataFactory; if (contextDataFactory == null) { throw new TypeError( - \'@fedify/nuxt: contextDataFactoryModule must export a function as "default" or named "contextDataFactory", but neither was found.\' + '@fedify/nuxt: contextDataFactoryModule must export a function as "default" or named "contextDataFactory", but neither was found.' ); } if (typeof contextDataFactory !== 'function') { throw new TypeError( - \'@fedify/nuxt: contextDataFactoryModule must export a function as "default" or named "contextDataFactory".\' + '@fedify/nuxt: contextDataFactoryModule must export a function as "default" or named "contextDataFactory".' ); }`; } diff --git a/packages/nuxt/src/runtime/server/plugin.ts b/packages/nuxt/src/runtime/server/plugin.ts index 5837c2f3a..2b532de0b 100644 --- a/packages/nuxt/src/runtime/server/plugin.ts +++ b/packages/nuxt/src/runtime/server/plugin.ts @@ -100,17 +100,18 @@ const fedifyPlugin: NitroAppPlugin = (nitroApp) => { : getResponseStatus(event); if (!shouldConvert(event, status)) return; if (body instanceof Response) { - body.headers.forEach((value, name) => { + for (const [name, headerValue] of body.headers) { + let value = headerValue; if (name === "set-cookie") { appendResponseHeader(event, name, splitCookiesString(value)); - return; + continue; } if (name === "vary") { const current = getResponseHeader(event, name); value = [current, value].filter(Boolean).join(", "); } setResponseHeader(event, name, value); - }); + } void body.body?.cancel().catch(() => {}); } setNotAcceptable(event); diff --git a/packages/pglite/deno.json b/packages/pglite/deno.json index 017e19640..caa381fb6 100644 --- a/packages/pglite/deno.json +++ b/packages/pglite/deno.json @@ -17,7 +17,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-read --allow-write --allow-env" } } diff --git a/packages/postgres/deno.json b/packages/postgres/deno.json index e6c3a925d..5e6199d48 100644 --- a/packages/postgres/deno.json +++ b/packages/postgres/deno.json @@ -18,7 +18,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" } } diff --git a/packages/postgres/src/kv.expiry.test.ts b/packages/postgres/src/kv.expiry.test.ts index fc7140f22..e6260e43a 100644 --- a/packages/postgres/src/kv.expiry.test.ts +++ b/packages/postgres/src/kv.expiry.test.ts @@ -1,3 +1,4 @@ +// oxlint-disable anti-slop/no-reflect-apply -- Proxy forwarding requires dynamic invocation. import { test } from "@fedify/fixture"; import { PostgresKvStore } from "@fedify/postgres/kv"; import * as temporal from "@js-temporal/polyfill"; diff --git a/packages/postgres/src/kv.index.test.ts b/packages/postgres/src/kv.index.test.ts index 40b6d1914..642d12572 100644 --- a/packages/postgres/src/kv.index.test.ts +++ b/packages/postgres/src/kv.index.test.ts @@ -1,3 +1,4 @@ +// oxlint-disable anti-slop/no-reflect-apply -- Proxy forwarding requires dynamic invocation. import { PostgresKvStore } from "@fedify/postgres/kv"; import * as temporal from "@js-temporal/polyfill"; import assert from "node:assert/strict"; diff --git a/packages/postgres/src/kv.test.ts b/packages/postgres/src/kv.test.ts index e0046a3d3..973e2010c 100644 --- a/packages/postgres/src/kv.test.ts +++ b/packages/postgres/src/kv.test.ts @@ -11,7 +11,7 @@ const Temporal = globalThis.Temporal ?? temporal.Temporal; const dbUrl = process.env.POSTGRES_URL; function getStore(): { - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any sql: postgres.Sql; tableName: string; store: PostgresKvStore; diff --git a/packages/postgres/src/kv.ts b/packages/postgres/src/kv.ts index 28c7ce295..6e244cd93 100644 --- a/packages/postgres/src/kv.ts +++ b/packages/postgres/src/kv.ts @@ -66,7 +66,8 @@ export interface PostgresKvStoreOptions { * ``` */ export class PostgresKvStore implements KvStore { - // deno-lint-ignore ban-types + // postgres.js uses Sql<{}> to mean an untyped SQL client. + // oxlint-disable-next-line typescript/no-empty-object-type readonly #sql: Sql<{}>; readonly #tableName: string; readonly #unlogged: boolean; @@ -83,7 +84,8 @@ export class PostgresKvStore implements KvStore { * @param options The options for the key–value store. */ constructor( - // deno-lint-ignore ban-types + // postgres.js uses Sql<{}> to mean an untyped SQL client. + // oxlint-disable-next-line typescript/no-empty-object-type sql: Sql<{}>, options: PostgresKvStoreOptions = {}, ) { diff --git a/packages/postgres/src/mq.test.ts b/packages/postgres/src/mq.test.ts index ba342fc42..71682e0bf 100644 --- a/packages/postgres/src/mq.test.ts +++ b/packages/postgres/src/mq.test.ts @@ -867,7 +867,7 @@ nodeTest( if (timeout !== delayMs) { return callOriginalSetTimeout(callback, timeout, ...args); } - // deno-lint-ignore prefer-const + // oxlint-disable-next-line eslint/prefer-const let handle: TimeoutHandle; const wrapped = (...wrappedArgs: unknown[]) => { firedDelayedHandles.add(handle); diff --git a/packages/postgres/src/mq.ts b/packages/postgres/src/mq.ts index 86d5472d6..e6bcdbd2e 100644 --- a/packages/postgres/src/mq.ts +++ b/packages/postgres/src/mq.ts @@ -141,7 +141,8 @@ export interface PostgresMessageQueueOptions { export class PostgresMessageQueue implements MessageQueue { readonly atomicEnqueueMany = false; - // deno-lint-ignore ban-types + // postgres.js uses Sql<{}> to mean an untyped SQL client. + // oxlint-disable-next-line typescript/no-empty-object-type readonly #sql: Sql<{}>; readonly #tableName: string; readonly #channelName: string; @@ -153,7 +154,8 @@ export class PostgresMessageQueue implements MessageQueue { #driverSerializesJson = false; constructor( - // deno-lint-ignore ban-types + // postgres.js uses Sql<{}> to mean an untyped SQL client. + // oxlint-disable-next-line typescript/no-empty-object-type sql: Sql<{}>, options: PostgresMessageQueueOptions = {}, ) { @@ -170,7 +172,7 @@ export class PostgresMessageQueue implements MessageQueue { } async enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions, ): Promise { @@ -204,7 +206,7 @@ export class PostgresMessageQueue implements MessageQueue { } async enqueueMany( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any messages: readonly any[], options?: MessageQueueEnqueueOptions, ): Promise { @@ -260,7 +262,7 @@ export class PostgresMessageQueue implements MessageQueue { } async listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: (message: any) => void | Promise, options: MessageQueueListenOptions = {}, ): Promise { diff --git a/packages/postgres/src/utils.ts b/packages/postgres/src/utils.ts index fa8bba149..7d93d3072 100644 --- a/packages/postgres/src/utils.ts +++ b/packages/postgres/src/utils.ts @@ -1,6 +1,7 @@ import type { Sql } from "postgres"; -// deno-lint-ignore ban-types +// postgres.js uses Sql<{}> to mean an untyped SQL client. +// oxlint-disable-next-line typescript/no-empty-object-type export async function driverSerializesJson(sql: Sql<{}>): Promise { const result = await sql`SELECT ${sql.json('{"foo":1}')}::jsonb AS test;`; return result[0].test === '{"foo":1}'; diff --git a/packages/redis/deno.json b/packages/redis/deno.json index 0d1b6ff16..3c1961517 100644 --- a/packages/redis/deno.json +++ b/packages/redis/deno.json @@ -19,7 +19,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env --doc --no-check=leaks" } } diff --git a/packages/redis/src/mq.ts b/packages/redis/src/mq.ts index d658d04f3..8b53568ba 100644 --- a/packages/redis/src/mq.ts +++ b/packages/redis/src/mq.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any import type { MessageQueue, MessageQueueDepth, @@ -295,7 +295,6 @@ export class RedisMessageQueue implements MessageQueue, Disposable { return { message: decoded[1], orderingKey }; } // All messages have ordering keys that are being processed - return; } finally { await this.#redis.del(this.#lockKey); } diff --git a/packages/relay/deno.json b/packages/relay/deno.json index f6c531c29..b0e21b332 100644 --- a/packages/relay/deno.json +++ b/packages/relay/deno.json @@ -18,7 +18,7 @@ "tasks": { "codegen": "deno task -f @fedify/vocab compile", "check": { - "command": "deno fmt --check && deno lint && deno check src/**/*.ts", + "command": "deno fmt --check && deno check src/**/*.ts", "dependencies": [ "codegen" ] @@ -34,10 +34,5 @@ "exclude": [ "src/init/templates/**" ] - }, - "lint": { - "exclude": [ - "src/init/templates/**" - ] } } diff --git a/packages/relay/src/litepub.test.ts b/packages/relay/src/litepub.test.ts index a0751770c..7188525f3 100644 --- a/packages/relay/src/litepub.test.ts +++ b/packages/relay/src/litepub.test.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any import { MemoryKvStore, signRequest } from "@fedify/fedify"; import { createRelay, type RelayOptions } from "@fedify/relay"; import { diff --git a/packages/relay/src/mastodon.test.ts b/packages/relay/src/mastodon.test.ts index ffd5d57b2..1e1bff915 100644 --- a/packages/relay/src/mastodon.test.ts +++ b/packages/relay/src/mastodon.test.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any import { MemoryKvStore, signJsonLd, signRequest } from "@fedify/fedify"; import { createRelay, type RelayOptions } from "@fedify/relay"; import { diff --git a/packages/solidstart/deno.json b/packages/solidstart/deno.json index eef7bd1ea..10e4d0c80 100644 --- a/packages/solidstart/deno.json +++ b/packages/solidstart/deno.json @@ -16,7 +16,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" }, "imports": { diff --git a/packages/solidstart/src/response.test.ts b/packages/solidstart/src/response.test.ts index 8e42dd87f..924714956 100644 --- a/packages/solidstart/src/response.test.ts +++ b/packages/solidstart/src/response.test.ts @@ -1,3 +1,4 @@ +// oxlint-disable anti-slop/no-reflect-apply -- Proxy forwarding requires dynamic invocation. import { test } from "@fedify/fixture"; import { deepEqual, equal, ok } from "node:assert/strict"; import { once } from "node:events"; diff --git a/packages/solidstart/src/response.ts b/packages/solidstart/src/response.ts index 173b8b29f..897fe9d99 100644 --- a/packages/solidstart/src/response.ts +++ b/packages/solidstart/src/response.ts @@ -1,3 +1,4 @@ +// oxlint-disable anti-slop/no-reflect-apply -- Proxy forwarding requires dynamic invocation. import type { ServerResponse } from "node:http"; export const NOT_ACCEPTABLE_BODY = "Not Acceptable"; @@ -68,7 +69,7 @@ export function interceptNotAcceptable( } const negotiated = notAcceptableHeaders(headers, new Headers()); for (const name of representationHeaders) this.removeHeader(name); - negotiated.forEach((value, name) => this.setHeader(name, value)); + for (const [name, value] of negotiated) this.setHeader(name, value); const callback = args.at(-1); return Reflect.apply(originalEnd, this, [ NOT_ACCEPTABLE_BODY, diff --git a/packages/sqlite/deno.json b/packages/sqlite/deno.json index cbbc476b7..8d362b909 100644 --- a/packages/sqlite/deno.json +++ b/packages/sqlite/deno.json @@ -23,7 +23,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check", + "check": "deno fmt --check && deno check", "test": "deno test --allow-net --allow-env --allow-read --allow-write --doc --no-check=leaks" } } diff --git a/packages/sqlite/src/kv.ts b/packages/sqlite/src/kv.ts index de63ab51e..9350f63f1 100644 --- a/packages/sqlite/src/kv.ts +++ b/packages/sqlite/src/kv.ts @@ -76,7 +76,7 @@ export class SqliteKvStore implements KvStore { /** * {@inheritDoc KvStore.get} */ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async get(key: KvKey): Promise { this.initialize(); @@ -100,7 +100,7 @@ export class SqliteKvStore implements KvStore { /** * {@inheritDoc KvStore.set} */ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async set( key: KvKey, value: unknown, @@ -130,13 +130,12 @@ export class SqliteKvStore implements KvStore { .run(encodedKey, encodedValue, now, expiresAt); this.#expire(); - return; } /** * {@inheritDoc KvStore.delete} */ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async delete(key: KvKey): Promise { this.initialize(); @@ -154,7 +153,7 @@ export class SqliteKvStore implements KvStore { /** * {@inheritDoc KvStore.cas} */ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async cas( key: KvKey, expectedValue: unknown, diff --git a/packages/sqlite/src/mq.ts b/packages/sqlite/src/mq.ts index 30332b784..8aedaa599 100644 --- a/packages/sqlite/src/mq.ts +++ b/packages/sqlite/src/mq.ts @@ -161,7 +161,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable { * {@inheritDoc MessageQueue.enqueue} */ enqueue( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any message: any, options?: MessageQueueEnqueueOptions, ): Promise { @@ -206,7 +206,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable { * {@inheritDoc MessageQueue.enqueueMany} */ enqueueMany( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any messages: readonly any[], options?: MessageQueueEnqueueOptions, ): Promise { @@ -288,7 +288,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable { * {@inheritDoc MessageQueue.listen} */ async listen( - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any handler: (message: any) => Promise | void, options?: MessageQueueListenOptions, ): Promise { diff --git a/packages/sveltekit/deno.json b/packages/sveltekit/deno.json index ed0e76caf..e3a857884 100644 --- a/packages/sveltekit/deno.json +++ b/packages/sveltekit/deno.json @@ -19,7 +19,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env" } } diff --git a/packages/testing/deno.json b/packages/testing/deno.json index 810bb7e64..09f591c9a 100644 --- a/packages/testing/deno.json +++ b/packages/testing/deno.json @@ -19,7 +19,7 @@ }, "tasks": { "build": "pnpm build", - "check": "deno fmt --check && deno lint && deno check", + "check": "deno fmt --check && deno check", "test": "deno test --allow-read --allow-net --allow-env", "test:node": { "dependencies": [ diff --git a/packages/testing/src/context.ts b/packages/testing/src/context.ts index d21eaee9d..ea9b13d75 100644 --- a/packages/testing/src/context.ts +++ b/packages/testing/src/context.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any import type { Context, Federation, diff --git a/packages/testing/src/docloader.ts b/packages/testing/src/docloader.ts index 4006e4a5d..dd764762f 100644 --- a/packages/testing/src/docloader.ts +++ b/packages/testing/src/docloader.ts @@ -1,6 +1,6 @@ import type { DocumentLoader } from "@fedify/vocab-runtime"; -// deno-lint-ignore require-await +// oxlint-disable-next-line eslint/require-await export const mockDocumentLoader: DocumentLoader = async (url: string) => ({ contextUrl: null, document: {}, diff --git a/packages/testing/src/mock.test.ts b/packages/testing/src/mock.test.ts index fde5aadc6..40d2614e7 100644 --- a/packages/testing/src/mock.test.ts +++ b/packages/testing/src/mock.test.ts @@ -19,8 +19,10 @@ import { computeDigestMultibase, exportDidKey, formatIri, + getDocumentLoader, parseIri, toCompatibleEf61Id, + UrlError, } from "@fedify/vocab-runtime"; import { deepStrictEqual, @@ -40,7 +42,6 @@ import { createOutboxContext, createRequestContext, } from "./mock.ts"; -import { getDocumentLoader, UrlError } from "@fedify/vocab-runtime"; import { metrics, trace } from "@opentelemetry/api"; test("MockFederation preserves telemetry providers with private-address loaders", () => { diff --git a/packages/testing/src/mock.ts b/packages/testing/src/mock.ts index b1161c54c..3396e4fd6 100644 --- a/packages/testing/src/mock.ts +++ b/packages/testing/src/mock.ts @@ -1,4 +1,4 @@ -// deno-lint-ignore-file no-explicit-any +// oxlint-disable typescript/no-explicit-any import type { ActorKeyPair, Context, @@ -603,7 +603,7 @@ class MockFederation implements Federation { setInboxListeners(inboxPath: any, sharedInboxPath?: string): any { this.inboxPath = inboxPath; this.sharedInboxPath = sharedInboxPath; - // deno-lint-ignore no-this-alias + // oxlint-disable-next-line typescript/no-this-alias const self = this; return { on(type: any, listener: any): any { @@ -639,7 +639,7 @@ class MockFederation implements Federation { validateOutboxListenerPath(outboxPath, this.outboxPath); this.outboxListenersInitialized = true; this.outboxPath = outboxPath; - // deno-lint-ignore no-this-alias + // oxlint-disable-next-line typescript/no-this-alias const self = this; return { on(type: any, listener: any): any { @@ -666,7 +666,7 @@ class MockFederation implements Federation { } this.mediaUploaderPath = path; this.mediaUploaderCallback = callback; - // deno-lint-ignore no-this-alias + // oxlint-disable-next-line typescript/no-this-alias const self = this; return { authorize(predicate: any): any { @@ -687,7 +687,7 @@ class MockFederation implements Federation { // Mock implementation - no-op } - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async startQueue( contextData: TContextData, options?: FederationStartQueueOptions, @@ -707,7 +707,7 @@ class MockFederation implements Federation { } } - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async processQueuedTask( contextData: TContextData, _message: Message, @@ -735,7 +735,7 @@ class MockFederation implements Federation { contextData: TContextData, options: { portableRequest?: PortableRequest } = {}, ): TestContext { - // deno-lint-ignore no-this-alias + // oxlint-disable-next-line typescript/no-this-alias const mockFederation = this; const request = baseUrlOrRequest instanceof Request @@ -872,6 +872,8 @@ class MockFederation implements Federation { Reflect.set( receivedActivity, "_cachedJsonLd", + // Preserve the internal JSON-LD cache across clones. + // oxlint-disable-next-line anti-slop/no-reflect-get Reflect.get(activity, "_cachedJsonLd"), ); } @@ -1038,7 +1040,11 @@ class MockFederation implements Federation { } setCollectionDispatcher< + // These type parameters are part of the public dispatcher API even though + // this mock body does not reference them. + // oxlint-disable-next-line eslint/no-unused-vars TObject extends Object, + // oxlint-disable-next-line eslint/no-unused-vars TParams extends Record, >( _name: string | symbol, @@ -1058,7 +1064,11 @@ class MockFederation implements Federation { } setOrderedCollectionDispatcher< + // These type parameters are part of the public dispatcher API even though + // this mock body does not reference them. + // oxlint-disable-next-line eslint/no-unused-vars TObject extends Object, + // oxlint-disable-next-line eslint/no-unused-vars TParams extends Record, >( _name: string | symbol, @@ -1247,7 +1257,7 @@ class MockContext implements Context { options.contextLoader != null; this.data = options.data; this.federation = options.federation; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await this.documentLoader = options.documentLoader ?? (async (url: string) => ({ contextUrl: null, document: {}, @@ -1700,6 +1710,9 @@ class MockContext implements Context { }); } + // TContext is part of the public generic API even though this mock body + // does not reference it. + // oxlint-disable-next-line eslint/no-unused-vars traverseCollection>( _collection: Collection | URL | null, _options?: TraverseCollectionOptions, diff --git a/packages/uri-template/deno.json b/packages/uri-template/deno.json index 1478e18e9..05887d2e6 100644 --- a/packages/uri-template/deno.json +++ b/packages/uri-template/deno.json @@ -27,7 +27,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check", + "check": "deno fmt --check && deno check", "test": "deno test --allow-env", "test:old": { "command": "OLD=true deno test --allow-env old/", diff --git a/packages/uri-template/old/uri-template-router.test.ts b/packages/uri-template/old/uri-template-router.test.ts index 2c82dfb98..2173fac17 100644 --- a/packages/uri-template/old/uri-template-router.test.ts +++ b/packages/uri-template/old/uri-template-router.test.ts @@ -1,4 +1,3 @@ -// deno-lint-ignore-file no-import-prefix import { test } from "@fedify/fixture"; import { cloneDeep } from "es-toolkit"; import { Router as InnerRouter } from "npm:uri-template-router@^1.0.0"; diff --git a/packages/uri-template/old/url-template.test.ts b/packages/uri-template/old/url-template.test.ts index 0e5851a17..333d2776a 100644 --- a/packages/uri-template/old/url-template.test.ts +++ b/packages/uri-template/old/url-template.test.ts @@ -1,5 +1,5 @@ import { test } from "@fedify/fixture"; -// deno-lint-ignore no-import-prefix + import { parseTemplate } from "npm:url-template@^3.1.1"; import { createFixedTemplateTest, diff --git a/packages/uri-template/src/template/errors.ts b/packages/uri-template/src/template/errors.ts index 13ee05fd4..7b563189e 100644 --- a/packages/uri-template/src/template/errors.ts +++ b/packages/uri-template/src/template/errors.ts @@ -129,7 +129,7 @@ export class InvalidLiteralError extends TemplateParseError { position, `Pct-encode '${char}' (e.g. '%${ char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0") - }') or remove it. Literals may not contain CTL, SP, '\"', '\\'', lone ` + + }') or remove it. Literals may not contain CTL, SP, '"', '\\'', lone ` + "'%', '<', '>', '\\\\', '^', '`', or '|'.", `Invalid literal character '${char}'`, ); diff --git a/packages/uri-template/src/template/expand.ts b/packages/uri-template/src/template/expand.ts index 51a27cfbe..14ef03b8d 100644 --- a/packages/uri-template/src/template/expand.ts +++ b/packages/uri-template/src/template/expand.ts @@ -121,8 +121,7 @@ const encodeListMembers = ( allowReserved: boolean, ): string[] => value - .filter((item) => item != null) - .map(String) + .flatMap((item) => item == null ? [] : [String(item)]) .map(encodeValue(allowReserved)); const encodeAssociativePairs = ( diff --git a/packages/uri-template/src/template/match.ts b/packages/uri-template/src/template/match.ts index 66877f469..9e76dcf04 100644 --- a/packages/uri-template/src/template/match.ts +++ b/packages/uri-template/src/template/match.ts @@ -56,6 +56,9 @@ function canExpandTo( const count = (text: string, delta: number): void => { // Code units compose across token boundaries, even when non-strict literal // tokens supply the two halves of a surrogate pair around an empty value. + // Must count UTF-16 code units so surrogate halves split across tokens + // still cancel. + // oxlint-disable-next-line typescript/prefer-for-of for (let index = 0; index < text.length; index++) { const char = text[index]; counts.set(char, (counts.get(char) ?? 0) + delta); diff --git a/packages/vocab-runtime/deno.json b/packages/vocab-runtime/deno.json index 4089387b2..140fad398 100644 --- a/packages/vocab-runtime/deno.json +++ b/packages/vocab-runtime/deno.json @@ -38,7 +38,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test --allow-net --allow-env=LOG" } } diff --git a/packages/vocab-runtime/src/docloader.test.ts b/packages/vocab-runtime/src/docloader.test.ts index a0a5e4b8a..1bcd2bed8 100644 --- a/packages/vocab-runtime/src/docloader.test.ts +++ b/packages/vocab-runtime/src/docloader.test.ts @@ -2120,16 +2120,16 @@ async function captureDocumentLoaderLogs( function cancelFailureLogs( records: readonly LogRecord[], ): [string, unknown, string][] { - return records - .filter((record) => - record.rawMessage === + return records.flatMap((record) => + record.rawMessage === "Failed to cancel the response body from {url}: {error}" - ) - .map((record) => [ - record.level, - record.properties.url, - String(record.properties.error), - ]); + ? [[ + record.level, + record.properties.url, + String(record.properties.error), + ]] + : [] + ); } function linkHeaderAlternate(next: string): RecordedResponseInit { diff --git a/packages/vocab-runtime/src/key.ts b/packages/vocab-runtime/src/key.ts index 8d9b1d156..a667ca4cb 100644 --- a/packages/vocab-runtime/src/key.ts +++ b/packages/vocab-runtime/src/key.ts @@ -243,7 +243,7 @@ export async function importMultibaseKey(key: string): Promise { const content = removeMulticodecPrefix(decoded); if (code === 0x1205) { // rsa-pub const keyObject = createPublicKey({ - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any key: content as any, format: "der", type: "pkcs1", diff --git a/packages/vocab-runtime/src/langstr.ts b/packages/vocab-runtime/src/langstr.ts index 236e7a55a..c75dbf720 100644 --- a/packages/vocab-runtime/src/langstr.ts +++ b/packages/vocab-runtime/src/langstr.ts @@ -29,7 +29,7 @@ export class LanguageString extends String { // Custom inspect hooks for Deno and Node.js debuggers are assigned outside // the class body because computed property names using Symbol.for() are // incompatible with isolatedDeclarations mode. -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any (LanguageString.prototype as any)[Symbol.for("Deno.customInspect")] = function ( this: LanguageString, inspect: (val: unknown, opts: unknown) => string, @@ -38,7 +38,7 @@ export class LanguageString extends String { return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`; }; -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any (LanguageString.prototype as any)[Symbol.for("nodejs.util.inspect.custom")] = function ( this: LanguageString, diff --git a/packages/vocab-runtime/src/link.ts b/packages/vocab-runtime/src/link.ts index 4c44acccb..c625a001a 100644 --- a/packages/vocab-runtime/src/link.ts +++ b/packages/vocab-runtime/src/link.ts @@ -200,26 +200,29 @@ export class HttpHeaderLink { ...input.map( ([uri, parameters]: HttpHeaderLinkEntry): HttpHeaderLinkEntry => { validateURI(uri); - Object.entries(parameters).forEach( - ([key, value]: [string, string]): void => { - if ( - key !== key.toLowerCase() || - !(/^[\w-]+\*?$/.test(key)) - ) { - throw new SyntaxError( - `\`${key}\` is not a valid parameter key!`, - ); - } - if ( - parametersNeedLowerCase.includes(key) && - value !== value.toLowerCase() - ) { - throw new SyntaxError( - `\`${value}\` is not a valid parameter value!`, - ); - } - }, - ); + for ( + const [key, value] of Object.entries(parameters) as [ + string, + string, + ][] + ) { + if ( + key !== key.toLowerCase() || + !(/^[\w-]+\*?$/.test(key)) + ) { + throw new SyntaxError( + `\`${key}\` is not a valid parameter key!`, + ); + } + if ( + parametersNeedLowerCase.includes(key) && + value !== value.toLowerCase() + ) { + throw new SyntaxError( + `\`${value}\` is not a valid parameter value!`, + ); + } + } return [uri, structuredClone(parameters)]; }, ), diff --git a/packages/vocab-runtime/src/multibase/base.ts b/packages/vocab-runtime/src/multibase/base.ts index 0f8faa171..369f87af5 100644 --- a/packages/vocab-runtime/src/multibase/base.ts +++ b/packages/vocab-runtime/src/multibase/base.ts @@ -25,7 +25,7 @@ export class Base { decode(string: string): Uint8Array { for (const char of string) { - if (this.alphabet && this.alphabet.indexOf(char) < 0) { + if (this.alphabet && !this.alphabet.includes(char)) { throw new Error(`invalid character '${char}' in '${string}'`); } } diff --git a/packages/vocab-runtime/src/multibase/rfc4648.ts b/packages/vocab-runtime/src/multibase/rfc4648.ts index 790fe7105..64fc4c766 100644 --- a/packages/vocab-runtime/src/multibase/rfc4648.ts +++ b/packages/vocab-runtime/src/multibase/rfc4648.ts @@ -61,9 +61,9 @@ const encode = ( let bits = 0; // Number of bits currently in the buffer let buffer = 0; // Bits waiting to be written out, MSB first - for (let i = 0; i < data.length; ++i) { + for (const byte of data) { // Slurp data into the buffer: - buffer = (buffer << 8) | data[i]; + buffer = (buffer << 8) | byte; bits += 8; // Write out as much as we can: diff --git a/packages/vocab-runtime/src/portable-media.test.ts b/packages/vocab-runtime/src/portable-media.test.ts index 3a672805b..984a249ba 100644 --- a/packages/vocab-runtime/src/portable-media.test.ts +++ b/packages/vocab-runtime/src/portable-media.test.ts @@ -72,6 +72,8 @@ async function withServers( networkTest( "fetchPortableMedia() accepts Link, Image, and Document", async () => { + // The type parameter is only used to instantiate the function. + // oxlint-disable-next-line eslint/no-unused-vars const accepts = () => {}; accepts(); accepts(); diff --git a/packages/vocab-runtime/src/public-fetch.test.ts b/packages/vocab-runtime/src/public-fetch.test.ts index 81de600c0..2ff79d450 100644 --- a/packages/vocab-runtime/src/public-fetch.test.ts +++ b/packages/vocab-runtime/src/public-fetch.test.ts @@ -176,16 +176,16 @@ async function settlesWithin( function cancelFailureLogs( records: readonly LogRecord[], ): [string, unknown, unknown][] { - return records - .filter((record) => - record.rawMessage === + return records.flatMap((record) => + record.rawMessage === "Failed to cancel a response body from {url}: {error}" - ) - .map((record) => [ - record.level, - record.properties.url, - record.properties.error, - ]); + ? [[ + record.level, + record.properties.url, + record.properties.error, + ]] + : [] + ); } async function withMockedGlobalFetch( diff --git a/packages/vocab-runtime/src/request.test.ts b/packages/vocab-runtime/src/request.test.ts index d75b17b09..842757694 100644 --- a/packages/vocab-runtime/src/request.test.ts +++ b/packages/vocab-runtime/src/request.test.ts @@ -207,7 +207,7 @@ test("isTransientFetchError() classifies aggregate errors by their members", () new AggregateError([ notFound("https://a.example/"), notFound("https://b.example/"), - ]), + ], "fetch errors"), ), false, ); @@ -216,26 +216,32 @@ test("isTransientFetchError() classifies aggregate errors by their members", () new AggregateError([ notFound("https://a.example/"), unavailable("https://b.example/"), - ]), + ], "fetch errors"), ), true, ); - deepStrictEqual(isTransientFetchError(new AggregateError([])), true); + deepStrictEqual( + isTransientFetchError(new AggregateError([], "fetch errors")), + true, + ); const gone = new FetchError( "https://a.example/", "HTTP 410", new Response(null, { status: 410 }), ); deepStrictEqual( - isTransientFetchError(new AggregateError([gone, gone])), + isTransientFetchError(new AggregateError([gone, gone], "fetch errors")), false, ); deepStrictEqual( isTransientFetchError( - new AggregateError([ - new FetchError("https://a.example/", "a", undefined), - new FetchError("https://b.example/", "b", undefined), - ].map((e) => Object.assign(e, { cause: gone }))), + new AggregateError( + [ + new FetchError("https://a.example/", "a", undefined), + new FetchError("https://b.example/", "b", undefined), + ].map((e) => Object.assign(e, { cause: gone })), + "fetch errors", + ), ), false, ); diff --git a/packages/vocab-runtime/src/testdata/public-fetch-deno-worker.ts b/packages/vocab-runtime/src/testdata/public-fetch-deno-worker.ts index 8423cc9b2..9b2d83147 100644 --- a/packages/vocab-runtime/src/testdata/public-fetch-deno-worker.ts +++ b/packages/vocab-runtime/src/testdata/public-fetch-deno-worker.ts @@ -61,14 +61,14 @@ for ( calls, hostnames, bodies, - logs: records - .filter((record) => - typeof record.rawMessage === "string" && + logs: records.flatMap((record) => + typeof record.rawMessage === "string" && record.rawMessage.startsWith("Outbound {protocol} connections are not") - ) - .map((record) => [ - record.level, - record.properties.runtime, - record.properties.protocol, - ]), + ? [[ + record.level, + record.properties.runtime, + record.properties.protocol, + ]] + : [] + ), }); diff --git a/packages/vocab-runtime/src/url.ts b/packages/vocab-runtime/src/url.ts index 358bb23bc..03277e5bf 100644 --- a/packages/vocab-runtime/src/url.ts +++ b/packages/vocab-runtime/src/url.ts @@ -80,7 +80,7 @@ function assertPortablePathCanBeParsed(iri: string): void { function assertCompatiblePathCanBeParsed(raw: string, parsed: URL): void { if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return; const prepared = prepareUrlInput(raw); - const match = prepared.match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i); + const match = prepared.match(/^https?:[/\\]*[^/?#\\]*([^?#]*)/i); if (match == null) return; const rawPath = match[1].replace(/\\/g, "/"); const compatiblePath = diff --git a/packages/vocab-tools/deno.json b/packages/vocab-tools/deno.json index 197bee5b4..f21ee8241 100644 --- a/packages/vocab-tools/deno.json +++ b/packages/vocab-tools/deno.json @@ -26,7 +26,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test -A" } } diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap index 4c5195eee..aaa2546c4 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap @@ -2,6 +2,7 @@ export const snapshot = {}; snapshot[`generateClasses() 1`] = ` "// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-unused-vars prefer-const verbatim-module-syntax +// oxlint-disable typescript/no-explicit-any, no-unused-vars, prefer-const import jsonld from \\"@fedify/vocab-runtime/jsonld\\"; import { getLogger } from \\"@logtape/logtape\\"; import { type Span, SpanStatusCode, type TracerProvider, trace } diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap index 378d6d385..c982071b4 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap @@ -1,5 +1,6 @@ exports[`generateClasses() 1`] = ` "// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-unused-vars prefer-const verbatim-module-syntax +// oxlint-disable typescript/no-explicit-any, no-unused-vars, prefer-const import jsonld from \\"@fedify/vocab-runtime/jsonld\\"; import { getLogger } from \\"@logtape/logtape\\"; import { type Span, SpanStatusCode, type TracerProvider, trace } diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.snap index 4af2b834d..a56ca4d86 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.snap @@ -2,6 +2,7 @@ exports[`generateClasses() 1`] = ` "// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-unused-vars prefer-const verbatim-module-syntax +// oxlint-disable typescript/no-explicit-any, no-unused-vars, prefer-const import jsonld from "@fedify/vocab-runtime/jsonld"; import { getLogger } from "@logtape/logtape"; import { type Span, SpanStatusCode, type TracerProvider, trace } diff --git a/packages/vocab-tools/src/class.ts b/packages/vocab-tools/src/class.ts index ef1d497ad..3eef20945 100644 --- a/packages/vocab-tools/src/class.ts +++ b/packages/vocab-tools/src/class.ts @@ -226,8 +226,9 @@ function* generateEntityTypeHelpers( sortedTypeUris: string[], types: Record, ): Iterable { - const entityTypes = sortedTypeUris.filter((typeUri) => types[typeUri].entity) - .map((typeUri) => ({ name: types[typeUri].name, uri: typeUri })); + const entityTypes = sortedTypeUris.flatMap((typeUri) => + types[typeUri].entity ? [{ name: types[typeUri].name, uri: typeUri }] : [] + ); const entityTypeNames = entityTypes.map((entityType) => entityType.name); const entityTypeUnion = entityTypeNames.length < 1 ? " never" @@ -317,6 +318,7 @@ export async function* generateClasses( ): AsyncIterable { validateTypeSchemas(types); yield "// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-unused-vars prefer-const verbatim-module-syntax\n"; + yield "// oxlint-disable typescript/no-explicit-any, no-unused-vars, prefer-const\n"; yield 'import jsonld from "@fedify/vocab-runtime/jsonld";\n'; yield 'import { getLogger } from "@logtape/logtape";\n'; yield `import { type Span, SpanStatusCode, type TracerProvider, trace } diff --git a/packages/vocab-tools/src/schema.ts b/packages/vocab-tools/src/schema.ts index 068f5d762..816271001 100644 --- a/packages/vocab-tools/src/schema.ts +++ b/packages/vocab-tools/src/schema.ts @@ -393,7 +393,7 @@ async function loadSchema(path: string): Promise { new SchemaError(path, `${path}:${e.instanceLocation}: ${e.error}`), ); } - throw new AggregateError(errors); + throw new AggregateError(errors, "Invalid schema data"); } /** @@ -430,7 +430,9 @@ export async function loadSchemaFiles( } result[schema.uri] = schema; } - if (errors.length > 0) throw new AggregateError(errors); + if (errors.length > 0) { + throw new AggregateError(errors, "Failed to load schema files"); + } const entries = Object.entries(result); entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0); return Object.fromEntries(entries); diff --git a/packages/vocab/deno.json b/packages/vocab/deno.json index 6dff8a852..37ee42dee 100644 --- a/packages/vocab/deno.json +++ b/packages/vocab/deno.json @@ -32,7 +32,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "compile": "deno run --allow-read --allow-write --allow-env scripts/codegen.ts", "test": "deno test --allow-read --allow-write --allow-env --unstable-kv --trace-leaks --parallel" } diff --git a/packages/vocab/src/deep-clone.test.ts b/packages/vocab/src/deep-clone.test.ts index 9f07f97bd..ce68da05b 100644 --- a/packages/vocab/src/deep-clone.test.ts +++ b/packages/vocab/src/deep-clone.test.ts @@ -29,7 +29,7 @@ import { const options = { contextLoader: mockDocumentLoader }; function personLoader(name: string, fetched: string[] = []): DocumentLoader { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await return async (url) => { fetched.push(url); return { @@ -203,7 +203,7 @@ test("deepClone() preserves origin checks and isolates hydrated trust", async () }, }, { ...options, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (url) => { fetches++; return { diff --git a/packages/vocab/src/gateway.test.ts b/packages/vocab/src/gateway.test.ts index f6d594679..78534d9c4 100644 --- a/packages/vocab/src/gateway.test.ts +++ b/packages/vocab/src/gateway.test.ts @@ -48,7 +48,7 @@ function createLoader( responses: Record, ): DocumentLoader & { readonly fetched: string[] } { const fetched: string[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const loader = async (url: string): Promise => { fetched.push(url); const response = responses[url]; @@ -78,7 +78,7 @@ function createVerifier( verified = true, ): PortableObjectVerifier & { readonly documents: unknown[] } { const documents: unknown[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const verifier = async (document: unknown) => { documents.push(document); return { verified }; @@ -461,7 +461,7 @@ test("getObject() applies the portable object verifier", async () => { documentLoader, contextLoader: mockDocumentLoader, gateways, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await verifyPortableObject: async () => { throw new Error("Verifier failure"); }, @@ -916,7 +916,7 @@ function createRecordingVerifier( document: unknown; options: PortableObjectVerifierOptions; }[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const verifier = async ( document: unknown, options: PortableObjectVerifierOptions, @@ -1206,7 +1206,7 @@ function createRedirectingLoader( responses: Record, ): DocumentLoader & { readonly fetched: string[] } { const fetched: string[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const loader = async (url: string): Promise => { fetched.push(url); const response = responses[url]; @@ -2298,7 +2298,7 @@ test("accessors pass the verifier of a call on to the objects they fetch", async const roots: [string, () => Promise][] = [ [ "constructed", - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async () => new Create({ id: new URL(gatewayUrl("https://gw.example", "/activities/1")), diff --git a/packages/vocab/src/lookup.test.ts b/packages/vocab/src/lookup.test.ts index c76b5bcbc..8fa510fe5 100644 --- a/packages/vocab/src/lookup.test.ts +++ b/packages/vocab/src/lookup.test.ts @@ -311,7 +311,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { "crossOrigin: ignore (default) - returns null for cross-origin objects", async () => { // Create a mock document loader that returns an object with different origin - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -341,7 +341,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step( "crossOrigin: throw - throws error for cross-origin objects", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -372,7 +372,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { ); await t.step("crossOrigin: trust - allows cross-origin objects", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -404,7 +404,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { }); await t.step("same-origin objects are always trusted", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const sameOriginDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -437,7 +437,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step( "same-authority non-FE34 IRIs are still trusted", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const atProtoDocumentLoader = async (url: string) => { if (url === "https://gateway.example/at-item") { return { @@ -474,7 +474,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step( "portable object IDs are trusted by DID cryptographic origin", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const portableDocumentLoader = async (url: string) => { if ( url === @@ -510,7 +510,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step( "portable object IDs reject mismatched cryptographic origins", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const portableDocumentLoader = async (url: string) => { if ( url === @@ -556,7 +556,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { ); await t.step("objects without @id are trusted", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const noIdDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -599,7 +599,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { }); // Mock document loader for the cross-origin actor - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const webfingerDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/actor") { return { @@ -653,7 +653,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step("subdomain same-origin check", async () => { // Test that different subdomains are considered different origins - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const subdomainDocumentLoader = async (url: string) => { if (url === "https://api.example.com/note") { return { @@ -680,7 +680,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step("different port same-origin check", async () => { // Test that different ports are considered different origins - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const differentPortDocumentLoader = async (url: string) => { if (url === "https://example.com:8080/note") { return { @@ -707,7 +707,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step("protocol difference same-origin check", async () => { // Test that different protocols are considered different origins - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const differentProtocolDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -734,7 +734,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { await t.step("error handling with crossOrigin throw option", async () => { // Test that other errors (not cross-origin) are still thrown normally - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const errorDocumentLoader = async (_url: string) => { throw new Error("Network error"); }; @@ -762,7 +762,7 @@ test("FEP-fe34: lookupObject() cross-origin security", { }); await t.step("malformed JSON handling with cross-origin policy", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const malformedJsonDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { diff --git a/packages/vocab/src/lookup.ts b/packages/vocab/src/lookup.ts index a8e5555a2..980053a13 100644 --- a/packages/vocab/src/lookup.ts +++ b/packages/vocab/src/lookup.ts @@ -1,15 +1,13 @@ -import type { - GetUserAgentOptions, - PortableObjectVerifier, -} from "@fedify/vocab-runtime"; import { canonicalizePortableUri, type DocumentLoader, formatIri, getDocumentLoader, + type GetUserAgentOptions, haveSameFe34Origin, haveSameIriOrigin, parseIri, + type PortableObjectVerifier, type RemoteDocument, } from "@fedify/vocab-runtime"; import { diff --git a/packages/vocab/src/portable-lookup.test.ts b/packages/vocab/src/portable-lookup.test.ts index 244848f31..ae0bd9fa6 100644 --- a/packages/vocab/src/portable-lookup.test.ts +++ b/packages/vocab/src/portable-lookup.test.ts @@ -41,7 +41,7 @@ function createLoader( responses: Record>, ): DocumentLoader & { readonly fetched: string[] } { const fetched: string[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const loader = async ( url: string, options?: { signal?: AbortSignal }, @@ -65,7 +65,7 @@ function createVerifier( verified = true, ): PortableObjectVerifier & { readonly documents: unknown[] } { const documents: unknown[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const verifier = async (document: unknown) => { documents.push(document); return { verified }; @@ -717,7 +717,7 @@ test("getActorHandle() with FEP-ef61 portable actors", { test("lookupObject() reports inferred gateways as hints", async () => { const calls: PortableObjectVerifierOptions[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const verifyPortableObject: PortableObjectVerifier = async (_, options) => { calls.push(options); return { verified: true }; @@ -735,7 +735,7 @@ test("lookupObject() reports inferred gateways as hints", async () => { test("lookupObject() reports explicit gateways to the verifier", async (t) => { const calls: PortableObjectVerifierOptions[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const verifyPortableObject: PortableObjectVerifier = async (_, options) => { calls.push(options); return { verified: true }; @@ -811,7 +811,7 @@ test("lookupObject() verifies fetched documents that stand for portable objects" document: Record, ): DocumentLoader & { readonly fetched: string[] } => { const fetched: string[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const loader = async (url: string): Promise => { fetched.push(url); if (url !== plainUrl) { @@ -832,7 +832,7 @@ test("lookupObject() verifies fetched documents that stand for portable objects" const actor = await lookupObject(plainUrl, { documentLoader, contextLoader: mockDocumentLoader, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await verifyPortableObject: async (_, options) => { calls.push(options); return { verified: true }; @@ -850,7 +850,7 @@ test("lookupObject() verifies fetched documents that stand for portable objects" documentLoader, contextLoader: mockDocumentLoader, gateways: ["https://other.example"], - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await verifyPortableObject: async (_, options) => { explicitCalls.push(options); return { verified: true }; diff --git a/packages/vocab/src/sparse-arrays.test.ts b/packages/vocab/src/sparse-arrays.test.ts index bbbd9a64a..18931552b 100644 --- a/packages/vocab/src/sparse-arrays.test.ts +++ b/packages/vocab/src/sparse-arrays.test.ts @@ -408,7 +408,7 @@ test("decoded and warmed object trust stays local across clones", async () => { content: "embedded", }, }, { contextLoader: mockDocumentLoader }); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { fetches++; return { @@ -478,7 +478,7 @@ for (const defaultKind of ["singular", "plural"] as const) { strictEqual(await new StatefulCreate({}).getObject(), defaultObject); const copies = [original, original.clone(), original.deepClone()]; let fetches = 0; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { fetches++; return { diff --git a/packages/vocab/src/utils.ts b/packages/vocab/src/utils.ts index 5185907b4..456e3b8e5 100644 --- a/packages/vocab/src/utils.ts +++ b/packages/vocab/src/utils.ts @@ -2,7 +2,7 @@ import { ok } from "node:assert"; export function assertInstanceOf( value: unknown, - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any constructor: new (...args: any[]) => T, ): asserts value is T { ok(value instanceof constructor); diff --git a/packages/vocab/src/vocab.test.ts b/packages/vocab/src/vocab.test.ts index a03840fd4..f2eab0660 100644 --- a/packages/vocab/src/vocab.test.ts +++ b/packages/vocab/src/vocab.test.ts @@ -2100,7 +2100,7 @@ test({ records.length = 0; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const invalidDocumentLoader: DocumentLoader = async (url) => ({ contextUrl: null, documentUrl: url, @@ -2152,7 +2152,7 @@ test({ test("Activity.getObject() fetches canonical portable IRIs", async () => { const fetchedUrls: string[] = []; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader: DocumentLoader = async (url) => { fetchedUrls.push(url); return { @@ -2175,7 +2175,7 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => { const object = await activity.getObject({ documentLoader, contextLoader: mockDocumentLoader, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await verifyPortableObject: async () => ({ verified: true }), }); @@ -4429,7 +4429,7 @@ test( attachments.push(a); } deepStrictEqual(attachments.length, 1); - // deno-lint-ignore no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any const doc = attachments[0] as any; deepStrictEqual( doc?.url?.href, @@ -4578,7 +4578,7 @@ for (const property of ["object", "attachment"] as const) { const fetching = fetchOriginal ? original : copy; const untouched = fetchOriginal ? copy : original; let fetches = 0; - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { fetches++; return { @@ -4722,7 +4722,7 @@ test("clone() preserves embedded object identity and trust", async () => { id: new URL("https://a.example/create"), object: note, }); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async () => { throw new Error("Trusted embedded objects must not be fetched."); }; @@ -4744,7 +4744,7 @@ test("clone() isolates crossOrigin trust when fetching", async () => { object: new URL("https://b.example/note"), }); const copy = original.clone(); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => ({ contextUrl: null, documentUrl: url, @@ -4765,7 +4765,7 @@ test("clone() isolates crossOrigin trust when fetching", async () => { test("FEP-fe34: crossOrigin ignore behavior (default)", async () => { // Create a mock document loader that returns objects with different origins - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note") { return { @@ -4796,7 +4796,7 @@ test("FEP-fe34: crossOrigin ignore behavior (default)", async () => { }); test("FEP-fe34: crossOrigin throw behavior", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note") { return { @@ -4831,7 +4831,7 @@ test("FEP-fe34: crossOrigin throw behavior", async () => { }); test("FEP-fe34: crossOrigin trust behavior", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note") { return { @@ -4879,7 +4879,7 @@ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => { const result = await create.getObject({ crossOrigin: "trust", - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (url) => { throw new Error(`Unexpected fetch: ${url}`); }, @@ -4891,7 +4891,7 @@ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => { }); test("FEP-fe34: Same origin objects are trusted", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const sameOriginDocumentLoader = async (url: string) => { if (url === "https://example.com/note") { return { @@ -4938,7 +4938,7 @@ test("FEP-fe34: Same-authority non-FE34 embedded objects are trusted", async () }); const result = await create.getObject({ - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (url) => { throw new Error(`Unexpected fetch: ${url}`); }, @@ -4952,7 +4952,7 @@ test( "FEP-fe34: Embedded cross-origin objects from JSON-LD are ignored by default", async () => { // Mock document loader for creating the Create object from JSON-LD - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const createDocumentLoader = async (url: string) => { if (url === "https://example.com/create") { return { @@ -4984,7 +4984,7 @@ test( ); // Mock document loader that would return the "legitimate" version - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const objectDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note") { return { @@ -5039,7 +5039,7 @@ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => { }); // Mock document loader for the cross-origin fetch - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { if (url === "https://different-origin.com/note") { return { @@ -5076,7 +5076,7 @@ test( }, }); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { if (url === "ap+ef61://did:key:z6MkOther/note") { return { @@ -5096,7 +5096,7 @@ test( const result = await create.getObject({ documentLoader, contextLoader: mockDocumentLoader, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await verifyPortableObject: async () => ({ verified: true }), }); assertInstanceOf(result, Note); @@ -5119,7 +5119,7 @@ test( }], }); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { throw new Error(`Unexpected fetch: ${url}`); }; @@ -5158,7 +5158,7 @@ test( for await ( const method of person.getAssertionMethods({ suppressError: true, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (url) => { fetches++; throw new Error(`Unexpected fetch: ${url}`); @@ -5173,7 +5173,7 @@ test( ); test("FEP-fe34: Array properties respect cross-origin policy", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note1") { return { @@ -5225,7 +5225,7 @@ test("FEP-fe34: Array properties respect cross-origin policy", async () => { }); test("FEP-fe34: Array properties with crossOrigin trust option", async () => { - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const crossOriginDocumentLoader = async (url: string) => { if (url === "https://different-origin.com/note1") { return { @@ -5301,7 +5301,7 @@ test("FEP-fe34: id-less arrays honor crossOrigin trust", async () => { for await ( const item of collection.getItems({ crossOrigin: "trust", - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await documentLoader: async (url) => { throw new Error(`Unexpected fetch: ${url}`); }, @@ -5328,7 +5328,7 @@ test("FEP-fe34: Array properties track trust per item", async () => { new URL("https://different-origin.com/note2"), ], }); - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const documentLoader = async (url: string) => { if (url === "https://different-origin.com/note2") { return { @@ -5359,7 +5359,7 @@ test( "FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy", async () => { // Mock document loader for creating the Collection object from JSON-LD - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const collectionDocumentLoader = async (url: string) => { if (url === "https://example.com/collection") { return { @@ -5396,7 +5396,7 @@ test( ); // Mock document loader for fetching objects - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await const itemDocumentLoader = async (url: string) => { if (url === "https://example.com/trusted-note") { return { @@ -5842,7 +5842,7 @@ const rsaPublicKey = new CryptographicKey({ ), }) as CryptographicKey & { publicKey: CryptoKey }; -// deno-lint-ignore no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any const sampleValues: Record = { "http://www.w3.org/2001/XMLSchema#boolean": true, "http://www.w3.org/2001/XMLSchema#integer": -123, @@ -6542,7 +6542,7 @@ test("attribution chains alternate ordinary and portable suppression", async () const contextLoader: DocumentLoader = (url, options) => { if (url !== contextUrl) return mockDocumentLoader(url, options); states.push(options?.suppressError); - const stack = new Error().stack; + const stack = new Error("capture stack").stack; ok(stack); depths[hop]?.push(stack.split("\n").length); return Promise.resolve({ diff --git a/packages/webfinger/deno.json b/packages/webfinger/deno.json index 701763bc2..b3ac3c151 100644 --- a/packages/webfinger/deno.json +++ b/packages/webfinger/deno.json @@ -26,7 +26,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check src/*.ts", + "check": "deno fmt --check && deno check src/*.ts", "test": "deno test" } } diff --git a/packages/webfinger/src/lookup.test.ts b/packages/webfinger/src/lookup.test.ts index e4b945396..cab2e1f91 100644 --- a/packages/webfinger/src/lookup.test.ts +++ b/packages/webfinger/src/lookup.test.ts @@ -1263,7 +1263,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { let responses = 0; return { events, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async openHop(url) { opens++; events.push(`open ${url}`); @@ -1271,7 +1271,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { let closed = false; return { url: new URL(url), - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async fetch(input) { if (closed) throw new TypeError("The fetch session is closed."); const target = new URL(input instanceof Request ? input.url : input); @@ -1308,7 +1308,7 @@ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder { } return response; }, - // deno-lint-ignore require-await + // oxlint-disable-next-line eslint/require-await async close() { if (closed) return; closed = true; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ca35219e..bcbfb0990 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,11 +28,14 @@ catalogs: specifier: ^0.5.1 version: 0.5.1 '@logtape/file': - specifier: ^2.2.0 - version: 2.2.0 + specifier: 2.4.2 + version: 2.4.2 + '@logtape/lint': + specifier: 2.4.2 + version: 2.4.2 '@logtape/logtape': - specifier: ^2.2.0 - version: 2.2.0 + specifier: 2.4.2 + version: 2.4.2 '@multiformats/base-x': specifier: ^4.0.1 version: 4.0.1 @@ -90,6 +93,9 @@ catalogs: '@optique/run': specifier: ^1.3.2 version: 1.3.2 + '@oxlint/plugins': + specifier: 1.90.0 + version: 1.90.0 '@standard-schema/spec': specifier: ^1.1.0 version: 1.1.0 @@ -193,8 +199,8 @@ catalogs: specifier: ^4.4.2 version: 4.4.2 oxlint: - specifier: ^1.66.0 - version: 1.66.0 + specifier: 1.90.0 + version: 1.90.0 pkijs: specifier: ^3.3.3 version: 3.3.3 @@ -237,12 +243,18 @@ importers: '@cloudflare/workers-types': specifier: 'catalog:' version: 4.20260511.1 + '@logtape/lint': + specifier: 'catalog:' + version: 2.4.2(eslint@9.32.0(jiti@2.6.1)) '@optique/core': specifier: 'catalog:' version: 1.3.2 '@optique/run': specifier: 'catalog:' version: 1.3.2 + '@oxlint/plugins': + specifier: 'catalog:' + version: 1.90.0 '@types/bun': specifier: 'catalog:' version: 1.3.3 @@ -258,6 +270,9 @@ importers: cross-spawn: specifier: 'catalog:' version: 7.0.6 + oxlint: + specifier: 'catalog:' + version: 1.90.0 tsdown: specifier: 'catalog:' version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) @@ -386,10 +401,10 @@ importers: version: 1.14.4(hono@4.8.3) '@logtape/file': specifier: 'catalog:' - version: 2.2.0(@logtape/logtape@2.2.0) + version: 2.4.2(@logtape/logtape@2.4.2) '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@lucide/vue': specifier: ^1.20.0 version: 1.20.0(vue@3.5.40(typescript@6.0.3)) @@ -638,7 +653,7 @@ importers: version: link:../../packages/vocab '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 express: specifier: 'catalog:' version: 5.2.1 @@ -691,7 +706,7 @@ importers: version: link:../../packages/vocab '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 koa: specifier: 'catalog:' version: 2.16.2 @@ -723,7 +738,7 @@ importers: version: link:../../../packages/lint oxlint: specifier: 'catalog:' - version: 1.66.0 + version: 1.90.0 examples/netlify-astro: dependencies: @@ -826,7 +841,7 @@ importers: version: link:../../packages/vocab '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 next: specifier: ^14.0.0 version: 14.2.30(@opentelemetry/api@1.9.1)(react-dom@19.0.0-rc-7771d3a7-20240827(react@19.0.0-rc-7771d3a7-20240827))(react@19.0.0-rc-7771d3a7-20240827) @@ -927,13 +942,13 @@ importers: version: link:../../packages/vocab '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 h3: specifier: 'catalog:' version: 1.15.11 nuxt: specifier: 'catalog:' - version: 4.4.2(d9f765576b8058af3393125d74d4050f) + version: 4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8) devDependencies: '@types/node': specifier: 'catalog:' @@ -1053,13 +1068,13 @@ importers: version: 0.5.0 '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 packages/adonisjs: dependencies: '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 devDependencies: '@adonisjs/assembler': specifier: 'catalog:' @@ -1214,10 +1229,10 @@ importers: version: 1.6.1 '@logtape/file': specifier: 'catalog:' - version: 2.2.0(@logtape/logtape@2.2.0) + version: 2.4.2(@logtape/logtape@2.4.2) '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@optique/config': specifier: 'catalog:' version: 1.3.2(@standard-schema/spec@1.1.0) @@ -1348,7 +1363,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.1 @@ -1463,7 +1478,7 @@ importers: version: link:../webfinger '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.1 @@ -1545,7 +1560,7 @@ importers: version: link:../vocab-runtime '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.1 @@ -1620,7 +1635,7 @@ importers: version: 7.10.1(@types/node@24.19.0) '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@optique/core': specifier: 'catalog:' version: 1.3.2 @@ -1735,7 +1750,7 @@ importers: version: 9.32.0(jiti@2.6.1) oxlint: specifier: 'catalog:' - version: 1.66.0 + version: 1.90.0 tsdown: specifier: 'catalog:' version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) @@ -1750,7 +1765,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 es-toolkit: specifier: 'catalog:' version: 1.46.1 @@ -1889,7 +1904,7 @@ importers: version: 1.15.11 nuxt: specifier: 'catalog:' - version: 4.4.2(d9f765576b8058af3393125d74d4050f) + version: 4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8) devDependencies: '@fedify/fixture': specifier: workspace:^ @@ -1914,7 +1929,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@types/emscripten': specifier: 'catalog:' version: 1.41.5 @@ -1945,7 +1960,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 postgres: specifier: 'catalog:' version: 3.4.7 @@ -1976,7 +1991,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 ioredis: specifier: 'catalog:' version: 5.10.1 @@ -2013,7 +2028,7 @@ importers: version: link:../vocab '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 temporal-polyfill: specifier: 'catalog:' version: 1.0.1 @@ -2060,7 +2075,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 es-toolkit: specifier: ^1.46.1 version: 1.46.1 @@ -2162,7 +2177,7 @@ importers: version: link:../webfinger '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@multiformats/base-x': specifier: 'catalog:' version: 4.0.1 @@ -2217,7 +2232,7 @@ importers: version: 0.5.1 '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@multiformats/base-x': specifier: 'catalog:' version: 4.0.1 @@ -2276,7 +2291,7 @@ importers: version: link:../vocab-runtime '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.1 @@ -2297,7 +2312,7 @@ importers: version: link:../vocab-runtime '@logtape/logtape': specifier: 'catalog:' - version: 2.2.0 + version: 2.4.2 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.1 @@ -5273,16 +5288,24 @@ packages: cpu: [x64] os: [win32] - '@logtape/file@2.2.0': - resolution: {integrity: sha512-QfAqxvf5RS7IH078kwBlGYOcgYpA30xVhB55iYk/h94fFTGk44MKReH2GmcJZSfAnWqYKpHYR4HkAr5AUrpLfQ==} + '@logtape/file@2.4.2': + resolution: {integrity: sha512-Wit1GrwBQ1mCERREsBX5C4TKe3SWoAZ2bFBh7e4sbAnh1w2rOjD46puW+/f/A3G7HFIsAYlL03/r1/9CeKZ0XQ==} peerDependencies: - '@logtape/logtape': ^2.2.0 + '@logtape/logtape': ^2.4.2 + + '@logtape/lint@2.4.2': + resolution: {integrity: sha512-2axZPC4gUivOT2elGPBEZTGO72sA2+boIR1mIU6D4oW728fFMn49uOCl4uLGq+P4vTAiE2ufLWAkQxhQw/I9ZA==} + peerDependencies: + eslint: '>=8.0.0' + peerDependenciesMeta: + eslint: + optional: true '@logtape/logtape@1.3.8': resolution: {integrity: sha512-iGk5CJFL0s/nTLmxCsVia/x1D/3oAFfw+40AaNMserTEwJf8m9eKV+R7RVWCXCcgYp41KLwk0/M0xiBvI0AFmA==} - '@logtape/logtape@2.2.0': - resolution: {integrity: sha512-lqjd2/2CVmyYBNBEUI377+G/sazT5qQvAAGB1msth8D94ovJ/kI6RtM/uoipwWoRjs6JG34cfFE1UNbgeSFsTA==} + '@logtape/logtape@2.4.2': + resolution: {integrity: sha512-YJyC1gEMgWmTqJTRtKkjgasEbi8/0gP7Dj8n6bL46gQIAhpTY8uXJ+1QYMOgF5asotlGMvy3G7ejXUe45Yd1MQ==} '@lucide/vue@1.20.0': resolution: {integrity: sha512-fy0XKdMPSGhhmfTp6evMN69ts7AJnopqflzbYM60a6vbnINhD5H/PbT+VeO9EQCHfSxQQiYBwUcBflbfKU57ng==} @@ -7157,120 +7180,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.66.0': - resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} + '@oxlint/binding-android-arm-eabi@1.90.0': + resolution: {integrity: sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.66.0': - resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} + '@oxlint/binding-android-arm64@1.90.0': + resolution: {integrity: sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.66.0': - resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} + '@oxlint/binding-darwin-arm64@1.90.0': + resolution: {integrity: sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.66.0': - resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} + '@oxlint/binding-darwin-x64@1.90.0': + resolution: {integrity: sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.66.0': - resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} + '@oxlint/binding-freebsd-x64@1.90.0': + resolution: {integrity: sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.66.0': - resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} + '@oxlint/binding-linux-arm-gnueabihf@1.90.0': + resolution: {integrity: sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.66.0': - resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} + '@oxlint/binding-linux-arm-musleabihf@1.90.0': + resolution: {integrity: sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.66.0': - resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} + '@oxlint/binding-linux-arm64-gnu@1.90.0': + resolution: {integrity: sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-arm64-musl@1.66.0': - resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} + '@oxlint/binding-linux-arm64-musl@1.90.0': + resolution: {integrity: sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-ppc64-gnu@1.66.0': - resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} + '@oxlint/binding-linux-ppc64-gnu@1.90.0': + resolution: {integrity: sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxlint/binding-linux-riscv64-gnu@1.66.0': - resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} + '@oxlint/binding-linux-riscv64-gnu@1.90.0': + resolution: {integrity: sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-riscv64-musl@1.66.0': - resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} + '@oxlint/binding-linux-riscv64-musl@1.90.0': + resolution: {integrity: sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-s390x-gnu@1.66.0': - resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} + '@oxlint/binding-linux-s390x-gnu@1.90.0': + resolution: {integrity: sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxlint/binding-linux-x64-gnu@1.66.0': - resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} + '@oxlint/binding-linux-x64-gnu@1.90.0': + resolution: {integrity: sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-linux-x64-musl@1.66.0': - resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} + '@oxlint/binding-linux-x64-musl@1.90.0': + resolution: {integrity: sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-openharmony-arm64@1.66.0': - resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} + '@oxlint/binding-openharmony-arm64@1.90.0': + resolution: {integrity: sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.66.0': - resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} + '@oxlint/binding-win32-arm64-msvc@1.90.0': + resolution: {integrity: sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.66.0': - resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} + '@oxlint/binding-win32-ia32-msvc@1.90.0': + resolution: {integrity: sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.66.0': - resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} + '@oxlint/binding-win32-x64-msvc@1.90.0': + resolution: {integrity: sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxlint/plugins@1.90.0': + resolution: {integrity: sha512-FT0JJDNbj9oobVWHpAzFckHY/bx1ftPjulBx4UCMXqmDUGqVWOkbKe5bj4zdEL+D5Ud7y/RfioLwSvlHBMjI4w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -15368,15 +15395,18 @@ packages: peerDependencies: oxc-parser: '>=0.98.0' - oxlint@1.66.0: - resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} + oxlint@1.90.0: + resolution: {integrity: sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.22.1' + oxlint-tsgolint: '>=7.0.2005' + vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: optional: true + vite-plus: + optional: true p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} @@ -21885,13 +21915,17 @@ snapshots: '@lmdb/lmdb-win32-x64@2.5.3': optional: true - '@logtape/file@2.2.0(@logtape/logtape@2.2.0)': + '@logtape/file@2.4.2(@logtape/logtape@2.4.2)': dependencies: - '@logtape/logtape': 2.2.0 + '@logtape/logtape': 2.4.2 + + '@logtape/lint@2.4.2(eslint@9.32.0(jiti@2.6.1))': + optionalDependencies: + eslint: 9.32.0(jiti@2.6.1) '@logtape/logtape@1.3.8': {} - '@logtape/logtape@2.2.0': {} + '@logtape/logtape@2.4.2': {} '@lucide/vue@1.20.0(vue@3.5.40(typescript@6.0.3))': dependencies: @@ -24252,7 +24286,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.4.2(@babel/core@7.29.0)(@electric-sql/pglite@0.5.8)(@netlify/blobs@10.7.9)(better-sqlite3@12.9.0)(db0@0.3.4(@electric-sql/pglite@0.5.8)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.19.0)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@24.19.0))(nuxt@4.4.2(d9f765576b8058af3393125d74d4050f))(rolldown@1.1.5)(typescript@6.0.3)': + '@nuxt/nitro-server@4.4.2(@babel/core@7.29.0)(@electric-sql/pglite@0.5.8)(@netlify/blobs@10.7.9)(better-sqlite3@12.9.0)(db0@0.3.4(@electric-sql/pglite@0.5.8)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.19.0)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@24.19.0))(nuxt@4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8))(rolldown@1.1.5)(typescript@6.0.3)': dependencies: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 @@ -24271,7 +24305,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.3(@electric-sql/pglite@0.5.8)(@netlify/blobs@10.7.9)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.19.0))(rolldown@1.1.5) - nuxt: 4.4.2(d9f765576b8058af3393125d74d4050f) + nuxt: 4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8) nypm: 0.6.5 ohash: 2.0.11 pathe: 2.0.3 @@ -24342,7 +24376,7 @@ snapshots: rc9: 3.0.1 std-env: 4.1.0 - '@nuxt/vite-builder@4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@24.19.0)(lightningcss@1.32.0)(magicast@0.5.2)(nuxt@4.4.2(d9f765576b8058af3393125d74d4050f))(optionator@0.9.4)(oxlint@1.66.0)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))(yaml@2.9.0)': + '@nuxt/vite-builder@4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@24.19.0)(eslint@9.32.0(jiti@2.6.1))(lightningcss@1.32.0)(magicast@0.5.2)(nuxt@4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8))(optionator@0.9.4)(oxlint@1.90.0)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))(yaml@2.9.0)': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.60.2) @@ -24360,7 +24394,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.4.2(d9f765576b8058af3393125d74d4050f) + nuxt: 4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8) nypm: 0.6.5 pathe: 2.0.3 pkg-types: 2.3.0 @@ -24371,7 +24405,7 @@ snapshots: unenv: 2.0.0-rc.24 vite: 7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) vite-node: 5.3.0(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) - vite-plugin-checker: 0.12.0(optionator@0.9.4)(oxlint@1.66.0)(typescript@6.0.3)(vite@7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + vite-plugin-checker: 0.12.0(eslint@9.32.0(jiti@2.6.1))(optionator@0.9.4)(oxlint@1.90.0)(typescript@6.0.3)(vite@7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) vue: 3.5.40(typescript@6.0.3) vue-bundle-renderer: 2.2.0 optionalDependencies: @@ -25413,63 +25447,65 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.117.0': optional: true - '@oxlint/binding-android-arm-eabi@1.66.0': + '@oxlint/binding-android-arm-eabi@1.90.0': optional: true - '@oxlint/binding-android-arm64@1.66.0': + '@oxlint/binding-android-arm64@1.90.0': optional: true - '@oxlint/binding-darwin-arm64@1.66.0': + '@oxlint/binding-darwin-arm64@1.90.0': optional: true - '@oxlint/binding-darwin-x64@1.66.0': + '@oxlint/binding-darwin-x64@1.90.0': optional: true - '@oxlint/binding-freebsd-x64@1.66.0': + '@oxlint/binding-freebsd-x64@1.90.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + '@oxlint/binding-linux-arm-gnueabihf@1.90.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.66.0': + '@oxlint/binding-linux-arm-musleabihf@1.90.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.66.0': + '@oxlint/binding-linux-arm64-gnu@1.90.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.66.0': + '@oxlint/binding-linux-arm64-musl@1.90.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.66.0': + '@oxlint/binding-linux-ppc64-gnu@1.90.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.66.0': + '@oxlint/binding-linux-riscv64-gnu@1.90.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.66.0': + '@oxlint/binding-linux-riscv64-musl@1.90.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.66.0': + '@oxlint/binding-linux-s390x-gnu@1.90.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.66.0': + '@oxlint/binding-linux-x64-gnu@1.90.0': optional: true - '@oxlint/binding-linux-x64-musl@1.66.0': + '@oxlint/binding-linux-x64-musl@1.90.0': optional: true - '@oxlint/binding-openharmony-arm64@1.66.0': + '@oxlint/binding-openharmony-arm64@1.90.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.66.0': + '@oxlint/binding-win32-arm64-msvc@1.90.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.66.0': + '@oxlint/binding-win32-ia32-msvc@1.90.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.66.0': + '@oxlint/binding-win32-x64-msvc@1.90.0': optional: true + '@oxlint/plugins@1.90.0': {} + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -35669,16 +35705,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.4.2(d9f765576b8058af3393125d74d4050f): + nuxt@4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8): dependencies: '@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@6.0.3) '@nuxt/cli': 3.34.0(@nuxt/schema@4.4.2)(cac@6.7.14)(magicast@0.5.2) '@nuxt/devtools': 3.2.4(vite@8.1.4(@types/node@24.19.0)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) '@nuxt/kit': 4.4.2(magicast@0.5.2) - '@nuxt/nitro-server': 4.4.2(@babel/core@7.29.0)(@electric-sql/pglite@0.5.8)(@netlify/blobs@10.7.9)(better-sqlite3@12.9.0)(db0@0.3.4(@electric-sql/pglite@0.5.8)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.19.0)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@24.19.0))(nuxt@4.4.2(d9f765576b8058af3393125d74d4050f))(rolldown@1.1.5)(typescript@6.0.3) + '@nuxt/nitro-server': 4.4.2(@babel/core@7.29.0)(@electric-sql/pglite@0.5.8)(@netlify/blobs@10.7.9)(better-sqlite3@12.9.0)(db0@0.3.4(@electric-sql/pglite@0.5.8)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.19.0)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.19.0))(pg@8.22.0)(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@24.19.0))(nuxt@4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8))(rolldown@1.1.5)(typescript@6.0.3) '@nuxt/schema': 4.4.2 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.4.2(magicast@0.5.2)) - '@nuxt/vite-builder': 4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@24.19.0)(lightningcss@1.32.0)(magicast@0.5.2)(nuxt@4.4.2(d9f765576b8058af3393125d74d4050f))(optionator@0.9.4)(oxlint@1.66.0)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))(yaml@2.9.0) + '@nuxt/vite-builder': 4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@24.19.0)(eslint@9.32.0(jiti@2.6.1))(lightningcss@1.32.0)(magicast@0.5.2)(nuxt@4.4.2(f147bdd4e83d5aab1290c9746e3bbfb8))(optionator@0.9.4)(oxlint@1.90.0)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))(yaml@2.9.0) '@unhead/vue': 2.1.13(vue@3.5.40(typescript@6.0.3)) '@vue/shared': 3.5.33 c12: 3.3.4(magicast@0.5.2) @@ -36104,27 +36140,27 @@ snapshots: magic-regexp: 0.10.0 oxc-parser: 0.117.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - oxlint@1.66.0: + oxlint@1.90.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.66.0 - '@oxlint/binding-android-arm64': 1.66.0 - '@oxlint/binding-darwin-arm64': 1.66.0 - '@oxlint/binding-darwin-x64': 1.66.0 - '@oxlint/binding-freebsd-x64': 1.66.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 - '@oxlint/binding-linux-arm-musleabihf': 1.66.0 - '@oxlint/binding-linux-arm64-gnu': 1.66.0 - '@oxlint/binding-linux-arm64-musl': 1.66.0 - '@oxlint/binding-linux-ppc64-gnu': 1.66.0 - '@oxlint/binding-linux-riscv64-gnu': 1.66.0 - '@oxlint/binding-linux-riscv64-musl': 1.66.0 - '@oxlint/binding-linux-s390x-gnu': 1.66.0 - '@oxlint/binding-linux-x64-gnu': 1.66.0 - '@oxlint/binding-linux-x64-musl': 1.66.0 - '@oxlint/binding-openharmony-arm64': 1.66.0 - '@oxlint/binding-win32-arm64-msvc': 1.66.0 - '@oxlint/binding-win32-ia32-msvc': 1.66.0 - '@oxlint/binding-win32-x64-msvc': 1.66.0 + '@oxlint/binding-android-arm-eabi': 1.90.0 + '@oxlint/binding-android-arm64': 1.90.0 + '@oxlint/binding-darwin-arm64': 1.90.0 + '@oxlint/binding-darwin-x64': 1.90.0 + '@oxlint/binding-freebsd-x64': 1.90.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.90.0 + '@oxlint/binding-linux-arm-musleabihf': 1.90.0 + '@oxlint/binding-linux-arm64-gnu': 1.90.0 + '@oxlint/binding-linux-arm64-musl': 1.90.0 + '@oxlint/binding-linux-ppc64-gnu': 1.90.0 + '@oxlint/binding-linux-riscv64-gnu': 1.90.0 + '@oxlint/binding-linux-riscv64-musl': 1.90.0 + '@oxlint/binding-linux-s390x-gnu': 1.90.0 + '@oxlint/binding-linux-x64-gnu': 1.90.0 + '@oxlint/binding-linux-x64-musl': 1.90.0 + '@oxlint/binding-openharmony-arm64': 1.90.0 + '@oxlint/binding-win32-arm64-msvc': 1.90.0 + '@oxlint/binding-win32-ia32-msvc': 1.90.0 + '@oxlint/binding-win32-x64-msvc': 1.90.0 p-cancelable@2.1.1: {} @@ -39532,7 +39568,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.12.0(optionator@0.9.4)(oxlint@1.66.0)(typescript@6.0.3)(vite@7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): + vite-plugin-checker@0.12.0(eslint@9.32.0(jiti@2.6.1))(optionator@0.9.4)(oxlint@1.90.0)(typescript@6.0.3)(vite@7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.0 chokidar: 4.0.3 @@ -39544,8 +39580,9 @@ snapshots: vite: 7.3.2(@types/node@24.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) vscode-uri: 3.1.0 optionalDependencies: + eslint: 9.32.0(jiti@2.6.1) optionator: 0.9.4 - oxlint: 1.66.0 + oxlint: 1.90.0 typescript: 6.0.3 vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@8.1.4(@types/node@24.19.0)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b6ff59116..f72419dde 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -87,8 +87,9 @@ catalog: "@electric-sql/pglite": ^0.5.8 "@fxts/core": ^1.21.1 "@js-temporal/polyfill": ^0.5.1 - "@logtape/file": ^2.2.0 - "@logtape/logtape": ^2.2.0 + "@logtape/file": 2.4.2 + "@logtape/lint": 2.4.2 + "@logtape/logtape": 2.4.2 "@multiformats/base-x": ^4.0.1 "@nestjs/common": ^11.0.1 "@netlify/async-workloads": ^0.0.106 @@ -141,7 +142,8 @@ catalog: pkijs: ^3.3.3 mysql2: ^3.22.3 nuxt: ^4.4.2 - oxlint: ^1.66.0 + "@oxlint/plugins": 1.90.0 + oxlint: 1.90.0 postgres: ^3.4.7 temporal-polyfill: ^1.0.1 tsdown: ^0.22.0 diff --git a/scripts/check_changelog_pr_refs.ts b/scripts/check_changelog_pr_refs.ts index 69ee88c70..f6a30dd34 100644 --- a/scripts/check_changelog_pr_refs.ts +++ b/scripts/check_changelog_pr_refs.ts @@ -87,18 +87,18 @@ export function findIntroducedEntries( ); if (before.length < 1 && existing.size < 1) return "all"; for (const key of ["paragraph", "firstLine", "references"] as const) { - after.forEach((entry, index) => { - if (existing.has(index) || entry[key] == null) return; + for (const [index, entry] of after.entries()) { + if (existing.has(index) || entry[key] == null) continue; const origin = before.findIndex((old, i) => !matched.has(i) && old[key] === entry[key] ); - if (origin < 0) return; + if (origin < 0) continue; // The same entry usually appears in several previous revisions: for (const [i, old] of before.entries()) { if (old.paragraph === before[origin].paragraph) matched.add(i); } existing.add(index); - }); + } } return new Set([...after.keys()].filter((index) => !existing.has(index))); } @@ -216,7 +216,7 @@ function extractEntries(content: string): Entry[] { * `[[#123], [#456] by John Doe]` or `\[[GHSA-xxxx-xxxx-xxxx], [#456]]`. */ const TRAILING_REFERENCES_PATTERN = - /\\?\[(\[[^\[\]]+\](?:\s*,\s*\[[^\[\]]+\])*)(?:\s+by\s+[^\[\]]+)?\]\s*$/; + /\\?\[(\[[^[\]]+\](?:\s*,\s*\[[^[\]]+\])*)(?:\s+by\s+[^[\]]+)?\]\s*$/; /** * Return the trailing references of a paragraph, such as `#123` or @@ -225,7 +225,7 @@ const TRAILING_REFERENCES_PATTERN = function extractTrailingReferences(paragraph: string): string[] | null { const match = TRAILING_REFERENCES_PATTERN.exec(paragraph); if (match == null) return null; - return Array.from(match[1].matchAll(/\[([^\[\]]+)\]/g), (m) => m[1]); + return Array.from(match[1].matchAll(/\[([^[\]]+)\]/g), (m) => m[1]); } /** Return the issue and pull request numbers among trailing references. */ diff --git a/scripts/check_lint_directives.test.ts b/scripts/check_lint_directives.test.ts new file mode 100644 index 000000000..2cd35c412 --- /dev/null +++ b/scripts/check_lint_directives.test.ts @@ -0,0 +1,309 @@ +import { deepStrictEqual } from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, it } from "node:test"; +import { + checkLintDirectives, + findProblemsInText, +} from "./check_lint_directives.ts"; + +const knownRules = new Set([ + "typescript/no-explicit-any", + "eslint/no-unused-vars", +]); + +describe("findProblemsInText()", () => { + it("rejects a bare disable directive", () => { + deepStrictEqual( + findProblemsInText("example.ts", "// oxlint-disable\n", knownRules), + [{ + file: "example.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("rejects an unknown rule name", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "// oxlint-disable-next-line no-such-rule\n", + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "unknown Oxlint rule in directive: no-such-rule", + }], + ); + }); + + it("accepts a named directive for a known rule", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "// oxlint-disable-next-line typescript/no-explicit-any\n", + knownRules, + ), + [], + ); + }); + + it("accepts multiple comma-separated rules", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "// oxlint-disable typescript/no-explicit-any, eslint/no-unused-vars\n", + knownRules, + ), + [], + ); + }); + + it("accepts a documented inline reason", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "// oxlint-disable-next-line eslint/no-unused-vars -- Public API.\n", + knownRules, + ), + [], + ); + }); + + it("rejects a trailing bare disable comment", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "const value = 1; // oxlint-disable\n", + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("rejects a trailing bare disable comment without a space", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "const value = 1;// oxlint-disable\n", + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("rejects a directive adjacent to a string literal", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + 'const value = "x"// oxlint-disable\n', + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("ignores directive-like text inside string literals", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + 'export const example = "// oxlint-disable";\n', + knownRules, + ), + [], + ); + }); + + it("ignores directive-like text inside template literals", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "export const text = `x// oxlint-disable`;\n", + knownRules, + ), + [], + ); + }); + + it("finds a directive after a template literal", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "export const text = `x`;\n// oxlint-disable\n", + knownRules, + ), + [{ + file: "example.ts", + line: 2, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("ignores directive-like text inside regex literals", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "export const pattern = /`[^`]*`/;\n", + knownRules, + ), + [], + ); + }); + + it("finds a directive before a closing brace", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "export function f() {\n // oxlint-disable\n}\n", + knownRules, + ), + [{ + file: "example.ts", + line: 2, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); + + it("accepts @typescript-eslint rule aliases", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "// eslint-disable-next-line @typescript-eslint/no-explicit-any\n", + knownRules, + ), + [], + ); + }); + + it("rejects a bare eslint-compatible directive", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "/* eslint-disable */\nexport const value: any = 1;\n", + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "bare eslint-disable must name at least one rule", + }], + ); + }); + + it("rejects a block bare disable comment", () => { + deepStrictEqual( + findProblemsInText( + "example.ts", + "/* oxlint-disable */\nconst value = 1;\n", + knownRules, + ), + [{ + file: "example.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + }); +}); + +describe("checkLintDirectives()", () => { + const cleanGitEnv = { ...process.env }; + delete cleanGitEnv.GIT_DIR; + delete cleanGitEnv.GIT_WORK_TREE; + delete cleanGitEnv.GIT_INDEX_FILE; + delete cleanGitEnv.GIT_PREFIX; + + it("audits non-ASCII paths without quoting", () => { + const root = mkdtempSync(join(tmpdir(), "fedify-lint-directives-")); + try { + execFileSync("git", ["init", "--quiet"], { + cwd: root, + env: cleanGitEnv, + }); + writeFileSync( + join(root, ".oxlintrc.json"), + JSON.stringify({ rules: { "typescript/no-explicit-any": "error" } }), + ); + writeFileSync(join(root, "café.ts"), "// oxlint-disable\n"); + deepStrictEqual( + checkLintDirectives(root, join(root, ".oxlintrc.json")), + [{ + file: "café.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("skips tracked files deleted from the worktree", () => { + const root = mkdtempSync(join(tmpdir(), "fedify-lint-directives-")); + try { + execFileSync("git", ["init", "--quiet"], { + cwd: root, + env: cleanGitEnv, + }); + writeFileSync( + join(root, ".oxlintrc.json"), + JSON.stringify({ rules: { "typescript/no-explicit-any": "error" } }), + ); + writeFileSync(join(root, "keep.ts"), "export const value = 1;\n"); + execFileSync("git", ["add", "keep.ts"], { + cwd: root, + env: cleanGitEnv, + }); + rmSync(join(root, "keep.ts")); + deepStrictEqual( + checkLintDirectives(root, join(root, ".oxlintrc.json")), + [], + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("includes untracked, non-ignored source files", () => { + const root = mkdtempSync(join(tmpdir(), "fedify-lint-directives-")); + try { + execFileSync("git", ["init", "--quiet"], { + cwd: root, + env: cleanGitEnv, + }); + writeFileSync( + join(root, ".oxlintrc.json"), + JSON.stringify({ rules: { "typescript/no-explicit-any": "error" } }), + ); + writeFileSync(join(root, "new-file.ts"), "// oxlint-disable\n"); + deepStrictEqual( + checkLintDirectives(root, join(root, ".oxlintrc.json")), + [{ + file: "new-file.ts", + line: 1, + message: "bare oxlint-disable must name at least one rule", + }], + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/scripts/check_lint_directives.ts b/scripts/check_lint_directives.ts new file mode 100644 index 000000000..7accdce17 --- /dev/null +++ b/scripts/check_lint_directives.ts @@ -0,0 +1,193 @@ +/** + * Checks that Oxlint disable/enable directives name explicit rules. + * + * Deno's `ban-untagged-ignore` rule rejected bare `deno-lint-ignore` + * comments. Oxlint permits a bare `oxlint-disable`, which would silently + * suppress every enabled rule. This script restores that guard and also + * verifies that named rules exist in the root configuration. + */ +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; +import { isMain } from "./tooling_main.ts"; + +const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +/** A single invalid directive. */ +export interface DirectiveProblem { + readonly file: string; + readonly line: number; + readonly message: string; +} + +/** Rules enabled by the root configuration, including overrides. */ +export function collectKnownRules(config: unknown): Set { + const rules = new Set(); + const addRules = (value: unknown) => { + if (value == null || typeof value !== "object") return; + for (const name of Object.keys(value)) rules.add(name); + }; + if (config != null && typeof config === "object") { + const record = config as Record; + addRules(record.rules); + if (Array.isArray(record.overrides)) { + for (const override of record.overrides) { + if (override != null && typeof override === "object") { + addRules((override as Record).rules); + } + } + } + } + return rules; +} + +/** Returns the 1-based line number of a character offset. */ +function addCommentRanges( + ranges: readonly ts.CommentRange[] | undefined, + comments: Map, +): void { + if (ranges == null) return; + for (const range of ranges) { + if ( + range.kind !== ts.SyntaxKind.SingleLineCommentTrivia && + range.kind !== ts.SyntaxKind.MultiLineCommentTrivia + ) continue; + comments.set(`${range.pos}:${range.end}`, range); + } +} + +/** Finds directives in one file's text. */ +export function findProblemsInText( + file: string, + text: string, + knownRules: ReadonlySet, +): DirectiveProblem[] { + const sourceFile = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + /* setParentNodes */ false, + ); + const comments = new Map(); + const visit = (node: ts.Node): void => { + addCommentRanges( + ts.getLeadingCommentRanges(text, node.getFullStart()), + comments, + ); + addCommentRanges( + ts.getTrailingCommentRanges(text, node.getEnd()), + comments, + ); + for (const child of node.getChildren(sourceFile)) visit(child); + }; + visit(sourceFile); + addCommentRanges( + ts.getLeadingCommentRanges(text, sourceFile.endOfFileToken.getFullStart()), + comments, + ); + const problems: DirectiveProblem[] = []; + for (const range of comments.values()) { + const comment = text.slice(range.pos, range.end); + const match = comment.match( + /^\/\/\s*(oxlint|eslint)-(disable(?:-next-line|-line)?|enable)\b([^\n]*)$/, + ) ?? comment.match( + /^\/\*\s*(oxlint|eslint)-(disable(?:-next-line|-line)?|enable)\b([\s\S]*?)\*\/$/, + ); + if (match == null) continue; + const line = sourceFile.getLineAndCharacterOfPosition(range.pos).line + 1; + const directive = `${match[1]}-${match[2]}`; + const remainder = match[3]; + const reasonIndex = remainder.indexOf(" -- "); + const rulesText = reasonIndex === -1 + ? remainder + : remainder.slice(0, reasonIndex); + const names = rulesText.trim().split(/[\s,]+/).filter((name) => + name.length > 0 + ); + if (names.length === 0) { + problems.push({ + file, + line, + message: `bare ${directive} must name at least one rule`, + }); + continue; + } + for (const name of names) { + const normalized = name.startsWith("@typescript-eslint/") + ? `typescript/${name.slice("@typescript-eslint/".length)}` + : name; + if (!knownRules.has(normalized)) { + problems.push({ + file, + line, + message: `unknown Oxlint rule in directive: ${name}`, + }); + } + } + } + return problems.sort((a, b) => a.line - b.line); +} + +/** Lists JS/TS files under the given paths, including untracked files. */ +function listFiles(root: string, paths: readonly string[]): string[] { + const env = { ...process.env }; + delete env.GIT_DIR; + delete env.GIT_WORK_TREE; + delete env.GIT_INDEX_FILE; + delete env.GIT_PREFIX; + const output = execFileSync( + "git", + [ + "ls-files", + "-z", + "--cached", + "--others", + "--exclude-standard", + "--", + ...(paths.length === 0 ? ["."] : paths), + ], + { cwd: root, encoding: "utf8", env }, + ); + return output.split("\0").filter((path) => + path.length > 0 && + /\.(?:[cm]?[jt]sx?)$/.test(path) && + !path.endsWith(".snap") && + !path.includes("/__snapshots__/") && + !path.startsWith("scripts/check_lint_directives") && + path !== "examples/cloudflare-workers/worker-configuration.d.ts" + ); +} + +/** Scans the repository, or the given paths, for invalid directives. */ +export function checkLintDirectives( + root: string, + configPath: string, + paths: readonly string[] = [], +): DirectiveProblem[] { + const config = JSON.parse(readFileSync(configPath, "utf8")); + const knownRules = collectKnownRules(config); + const problems: DirectiveProblem[] = []; + for (const file of listFiles(root, paths)) { + const path = join(root, file); + if (!existsSync(path)) continue; + const text = readFileSync(path, "utf8"); + problems.push(...findProblemsInText(file, text, knownRules)); + } + return problems; +} + +if (isMain(import.meta.url)) { + const paths = process.argv.slice(2); + const problems = checkLintDirectives( + projectRoot, + join(projectRoot, ".oxlintrc.json"), + paths, + ); + for (const problem of problems) { + console.error(`${problem.file}:${problem.line}: ${problem.message}`); + } + if (problems.length > 0) process.exitCode = 1; +} diff --git a/scripts/sponsors.ts b/scripts/sponsors.ts index 9a34ec8f3..ed6de2d92 100644 --- a/scripts/sponsors.ts +++ b/scripts/sponsors.ts @@ -71,7 +71,7 @@ function getCustomDonations(members: MemberList): MemberList { } function escape(string: string): string { - return string.replace(/[\\*_()\[\]]/g, (m) => `\\${m}`); + return string.replace(/[\\*_()[\]]/g, (m) => `\\${m}`); } function escapeTableCell(string: string | null): string { diff --git a/scripts/test_tooling_no_deno.mjs b/scripts/test_tooling_no_deno.mjs index 690cb82c6..d646b0d6e 100644 --- a/scripts/test_tooling_no_deno.mjs +++ b/scripts/test_tooling_no_deno.mjs @@ -39,6 +39,7 @@ try { PATH: `${bin}${delimiter}${process.env.PATH}`, }; const tasks = [ + ["check:lint"], ["check:types"], ["check:types", "--", "fedify", "vocab"], ["check-versions"], diff --git a/tools/oxlint/anti-slop/LICENSE b/tools/oxlint/anti-slop/LICENSE new file mode 100644 index 000000000..69239ead1 --- /dev/null +++ b/tools/oxlint/anti-slop/LICENSE @@ -0,0 +1,21 @@ +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/tools/oxlint/anti-slop/UPSTREAM.md b/tools/oxlint/anti-slop/UPSTREAM.md new file mode 100644 index 000000000..7488ee7b9 --- /dev/null +++ b/tools/oxlint/anti-slop/UPSTREAM.md @@ -0,0 +1,28 @@ +# Vendored anti-slop + +- Source: https://github.com/dmmulroy/anti-slop +- Revision: 13909c9e07590f5a85d93c62712259fe14a883f7 (2026-10-14) +- Copied with: `.agents/skills/install-anti-slop/scripts/install.mjs` +- Installed path: `tools/oxlint/anti-slop/` +- License: MIT; see `LICENSE` and `vendor/eslint-stylistic/LICENSE` + +The upstream skill bundle strips the upstream test files. `src/` is the +canonical upstream source; the vendored copy keeps the plugin entry point, +generic rules, shared helpers, the Effect rules, and the vendored ESLint +Stylistic padding-line helpers with their provenance. + +Local changes: + +- Only the rules enabled in `.oxlintrc.json` are active. Rules that need + broad type or API refactoring remain disabled and are tracked separately. +- `require-readable-spacing` is not enabled because formatting is owned by the + repository formatter. +- `shared/type-alias-resolution.ts` starts alias substitutions from an empty map + so caller type parameters do not leak into an alias body. This avoids a + false `no-unknown-type-aliases` report and is worth upstreaming. +- The vendored directory is excluded from Oxlint, `deno fmt`, and Hongdown. + +Update procedure: reinstall the skill, run its installer into a staging +directory, diff against this copy, review rule changes, then replace the +vendored files and update the revision above. Keep `oxlint` and +`@oxlint/plugins` on the same exact version. diff --git a/tools/oxlint/anti-slop/effect/index.ts b/tools/oxlint/anti-slop/effect/index.ts new file mode 100644 index 000000000..55eb13dde --- /dev/null +++ b/tools/oxlint/anti-slop/effect/index.ts @@ -0,0 +1,21 @@ +import { eslintCompatPlugin } from "@oxlint/plugins"; + +import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.ts"; +import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts"; +import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts"; +import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts"; +import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts"; + +/** Opt-in Oxlint rules for Effect service and Layer architecture. */ +const antiSlopEffectPlugin = eslintCompatPlugin({ + meta: { name: "anti-slop-effect" }, + rules: { + "no-manual-effect-error-tag": noManualEffectErrorTagRule, + "no-manual-tag-comparison": noManualTagComparisonRule, + "no-manual-tagged-construction": noManualTaggedConstructionRule, + "no-service-constructor-imports": noServiceConstructorImportsRule, + "prefer-effect-match": preferEffectMatchRule, + }, +}); + +export default antiSlopEffectPlugin; diff --git a/tools/oxlint/anti-slop/effect/rules/no-manual-effect-error-tag.ts b/tools/oxlint/anti-slop/effect/rules/no-manual-effect-error-tag.ts new file mode 100644 index 000000000..daaa109ae --- /dev/null +++ b/tools/oxlint/anti-slop/effect/rules/no-manual-effect-error-tag.ts @@ -0,0 +1,52 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isReasonTagMember, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualEffectErrorTagRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler.", + }, + messages: { + tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.", + reason: + "Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + const tagMember = tagMemberFromComparison(node); + if ( + tagMember === undefined || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(tagMember) ? "reason" : "tag", + }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag", + }); + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/effect/rules/no-manual-tag-comparison.ts b/tools/oxlint/anti-slop/effect/rules/no-manual-tag-comparison.ts new file mode 100644 index 000000000..868d1d274 --- /dev/null +++ b/tools/oxlint/anti-slop/effect/rules/no-manual-tag-comparison.ts @@ -0,0 +1,45 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualTagComparisonRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect Match or Predicate helpers instead of manually branching on `_tag`.", + }, + messages: { + manualComparison: + "Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.", + manualSwitch: + "Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + if ( + tagMemberFromComparison(node) === undefined || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualComparison" }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualSwitch" }); + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/effect/rules/no-manual-tagged-construction.ts b/tools/oxlint/anti-slop/effect/rules/no-manual-tagged-construction.ts new file mode 100644 index 000000000..382f166a1 --- /dev/null +++ b/tools/oxlint/anti-slop/effect/rules/no-manual-tagged-construction.ts @@ -0,0 +1,37 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isMatchPatternObject, + isStringLiteral, + propertyName, +} from "../shared/tagged-values.ts"; + +export const noManualTaggedConstructionRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Construct tagged values with their existing Effect constructor instead of writing `_tag` manually.", + }, + messages: { + manualConstruction: + "Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object.", + }, + }, + createOnce(context) { + return { + ObjectExpression(node) { + if (isMatchPatternObject(node)) return; + const tag = node.properties.find( + (property) => + property.type === "Property" && + propertyName(property) === "_tag" && + isStringLiteral(property.value), + ); + if (tag !== undefined) { + context.report({ node: tag, messageId: "manualConstruction" }); + } + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts b/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts new file mode 100644 index 000000000..55cefb7e7 --- /dev/null +++ b/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts @@ -0,0 +1,52 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u; +const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u; + +function isProjectLocalImport(source: string): boolean { + return source.startsWith("./") || source.startsWith("../"); +} + +function getImportedName(specifier: ESTree.ImportSpecifier): string { + if (specifier.imported.type === "Identifier") return specifier.imported.name; + return specifier.imported.value; +} + +/** Keep dependency-bearing Effect service constructors local to their owning capability modules. */ +export const noServiceConstructorImportsRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow project-local make imports outside test and spec files.", + }, + messages: { + serviceConstructorImport: + 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.', + }, + }, + create(context) { + const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/")); + + return { + ImportDeclaration(node) { + if (isTestFile || !isProjectLocalImport(node.source.value)) return; + + for (const specifier of node.specifiers) { + if (specifier.type !== "ImportSpecifier") continue; + + const importedName = getImportedName(specifier); + if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) continue; + + context.report({ + node: specifier, + messageId: "serviceConstructorImport", + data: { name: importedName }, + }); + } + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/effect/rules/prefer-effect-match.ts b/tools/oxlint/anti-slop/effect/rules/prefer-effect-match.ts new file mode 100644 index 000000000..d83454034 --- /dev/null +++ b/tools/oxlint/anti-slop/effect/rules/prefer-effect-match.ts @@ -0,0 +1,54 @@ +import { defineRule, type ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); + +export const preferEffectMatchRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Match from Effect for chained literal ternaries over the same value.", + }, + messages: { + preferMatch: + "Use Match from Effect instead of a chained literal ternary.", + }, + }, + createOnce(context) { + const isLiteral = (node: ESTree.Node): boolean => + node.type === "Literal" || + (node.type === "TemplateLiteral" && node.expressions.length === 0); + + const comparedValue = (node: ESTree.Expression): string | undefined => { + if ( + node.type !== "BinaryExpression" || + !equalityOperators.has(node.operator) + ) { + return undefined; + } + if (isLiteral(node.left)) return context.sourceCode.getText(node.right); + if (isLiteral(node.right)) return context.sourceCode.getText(node.left); + return undefined; + }; + + return { + ConditionalExpression(node) { + if (node.parent?.type === "ConditionalExpression") return; + const value = comparedValue(node.test); + if (value === undefined) return; + + let alternate = node.alternate; + let literalChecks = 1; + while (alternate.type === "ConditionalExpression") { + if (comparedValue(alternate.test) !== value) return; + literalChecks += 1; + alternate = alternate.alternate; + } + + if (literalChecks > 1) { + context.report({ node, messageId: "preferMatch" }); + } + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/effect/shared/tagged-values.ts b/tools/oxlint/anti-slop/effect/shared/tagged-values.ts new file mode 100644 index 000000000..21d10a2f8 --- /dev/null +++ b/tools/oxlint/anti-slop/effect/shared/tagged-values.ts @@ -0,0 +1,97 @@ +import type { ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); +const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]); + +export const isStringLiteral = ( + node: ESTree.Node | null | undefined, +): node is ESTree.StringLiteral => + node?.type === "Literal" && typeof node.value === "string"; + +export const isTagMember = ( + node: ESTree.Node | null | undefined, +): node is ESTree.MemberExpression => + node?.type === "MemberExpression" && + ((!node.computed && + node.property.type === "Identifier" && + node.property.name === "_tag") || + (node.computed && + isStringLiteral(node.property) && + node.property.value === "_tag")); + +export const tagMemberFromComparison = ( + node: ESTree.BinaryExpression, +): ESTree.MemberExpression | undefined => { + if (!equalityOperators.has(node.operator)) return undefined; + if (isTagMember(node.left) && isStringLiteral(node.right)) return node.left; + if (isTagMember(node.right) && isStringLiteral(node.left)) return node.right; + return undefined; +}; + +const isBroadEffectCatchCall = ( + node: ESTree.Node | null | undefined, +): node is ESTree.CallExpression => + node?.type === "CallExpression" && + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Effect" && + !node.callee.computed && + node.callee.property.type === "Identifier" && + broadEffectCatchMethods.has(node.callee.property.name); + +export const isInsideBroadEffectHandler = (node: ESTree.Node): boolean => { + let current: ESTree.Node | null | undefined = node.parent; + while (current !== null && current !== undefined) { + if ( + current.type === "ArrowFunctionExpression" || + current.type === "FunctionExpression" + ) { + return ( + isBroadEffectCatchCall(current.parent) && + current.parent.arguments.includes(current) + ); + } + current = current.parent; + } + return false; +}; + +export const isReasonTagMember = (node: ESTree.MemberExpression): boolean => + node.object.type === "MemberExpression" && + ((!node.object.computed && + node.object.property.type === "Identifier" && + node.object.property.name === "reason") || + (node.object.computed && + isStringLiteral(node.object.property) && + node.object.property.value === "reason")); + +export const propertyName = ( + property: ESTree.ObjectProperty, +): string | undefined => { + if (!property.computed && property.key.type === "Identifier") { + return property.key.name; + } + if ( + property.key.type === "Literal" && + typeof property.key.value === "string" + ) { + return property.key.value; + } + return undefined; +}; + +export const isMatchPatternObject = (node: ESTree.ObjectExpression): boolean => { + const call = node.parent; + if (call?.type !== "CallExpression" || !call.arguments.includes(node)) { + return false; + } + const callee = call.callee; + return ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Match" && + !callee.computed && + callee.property.type === "Identifier" && + (callee.property.name === "when" || callee.property.name === "not") + ); +}; diff --git a/tools/oxlint/anti-slop/index.ts b/tools/oxlint/anti-slop/index.ts new file mode 100644 index 000000000..a3876ec07 --- /dev/null +++ b/tools/oxlint/anti-slop/index.ts @@ -0,0 +1,47 @@ +import { eslintCompatPlugin } from "@oxlint/plugins"; + +import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts"; +import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts"; +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 { requireReadableSpacingRule } from "./rules/require-readable-spacing.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-array-filter-map": noArrayFilterMapRule, + "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule, + "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-readable-spacing": requireReadableSpacingRule, + "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule, + }, +}); + +export default antiSlopPlugin; diff --git a/tools/oxlint/anti-slop/rules/no-array-filter-map.ts b/tools/oxlint/anti-slop/rules/no-array-filter-map.ts new file mode 100644 index 000000000..dc001b381 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-array-filter-map.ts @@ -0,0 +1,28 @@ +import { defineRule } from "@oxlint/plugins"; + +import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts"; + +/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */ +export const noArrayFilterMapRule = defineRule({ + meta: { + type: "suggestion", + docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." }, + messages: { + arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const outer = arrayMethodTarget(node.callee); + if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return; + const innerCall = unwrapArrayExpression(outer.object); + if (innerCall.type !== "CallExpression") return; + const inner = arrayMethodTarget(innerCall.callee); + if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return; + if (!isKnownArrayExpression(context.sourceCode, inner.object)) return; + context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } }); + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts b/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts new file mode 100644 index 000000000..0d1185278 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts @@ -0,0 +1,77 @@ +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/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts b/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts new file mode 100644 index 000000000..ae7248d36 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts @@ -0,0 +1,49 @@ +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/tools/oxlint/anti-slop/rules/no-known-value-widening.ts b/tools/oxlint/anti-slop/rules/no-known-value-widening.ts new file mode 100644 index 000000000..0df39ab89 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-known-value-widening.ts @@ -0,0 +1,415 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + classifyUnsafeDictionaryValue, + classifyWideningTarget, + createTypeEnvironment, + isKnownEvidenceExpression, + type TypeEnvironment, + type WideningTarget, +} from "../shared/dictionary-types.ts"; +import { + containsUnknownType, + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +import { resolveVariable } from "../shared/scope.ts"; + +import type { ESTree, 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 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 isFunctionExpression(node: ESTree.Node): node is FunctionExpression { + return ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "TSDeclareFunction" || + node.type === "TSEmptyBodyFunctionExpression" + ); +} + +function localFunctionForCall( + sourceCode: SourceCode, + callee: ESTree.Expression, +): FunctionExpression | null { + const unwrapped = unwrapExpression(callee); + if (isFunctionExpression(unwrapped)) return unwrapped; + if (unwrapped.type !== "Identifier") return null; + const variable = resolveVariable(sourceCode, unwrapped); + if (variable === null || variable.defs.length !== 1) return null; + const [definition] = variable.defs; + if (definition === undefined) return null; + if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) { + return definition.node; + } + if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") { + return null; + } + const initializer = definition.node.init; + if (initializer === null) return null; + const unwrappedInitializer = unwrapExpression(initializer); + return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null; +} + +function variableTypeAnnotation( + sourceCode: SourceCode, + variable: Variable, +): ESTree.TSTypeAnnotation | null { + if (variable.defs.length !== 1) return null; + const [definition] = variable.defs; + if (definition === undefined) return null; + if ( + definition.type === "Variable" && + definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" + ) { + return definition.node.id.typeAnnotation ?? null; + } + if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) { + return null; + } + const parameter = definition.node.params.find( + (candidate) => + functionParameterBindingName(candidate, sourceCode) === variable.name, + ); + return parameter === undefined ? null : (functionParameterTypeAnnotation(parameter) ?? null); +} + +function hasInformativeType( + type: ESTree.TSType, + environment: TypeEnvironment, +): boolean { + return classifyUnsafeDictionaryValue(type, environment) === null; +} + +function hasKnownCallArgumentEvidence( + sourceCode: SourceCode, + expression: ESTree.Expression, + environment: TypeEnvironment, + visitedVariables = new Set(), +): boolean { + if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") { + return hasKnownCallArgumentEvidence( + sourceCode, + expression.expression, + environment, + visitedVariables, + ); + } + if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") { + return hasInformativeType(expression.typeAnnotation, environment); + } + if (expression.type === "TSSatisfiesExpression") { + return hasKnownCallArgumentEvidence( + sourceCode, + expression.expression, + environment, + visitedVariables, + ); + } + if (expression.type === "CallExpression") { + const owner = localFunctionForCall(sourceCode, expression.callee); + const returnType = owner?.returnType?.typeAnnotation; + return returnType !== undefined && hasInformativeType(returnType, environment); + } + if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression); + const variable = resolveVariable(sourceCode, expression); + if (variable === null || visitedVariables.has(variable)) return false; + const annotation = variableTypeAnnotation(sourceCode, variable); + if (annotation !== null) { + return hasInformativeType(annotation.typeAnnotation, environment); + } + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.init === null || + !isStableConstVariable(variable, declarator) + ) { + return false; + } + visitedVariables.add(variable); + return hasKnownCallArgumentEvidence( + sourceCode, + declarator.init, + environment, + visitedVariables, + ); +} + +function typePredicateSubjectIndex( + sourceCode: SourceCode, + owner: FunctionExpression, +): number | null { + const predicate = owner.returnType?.typeAnnotation; + if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") { + return null; + } + const predicateParameterName = predicate.parameterName.name; + const index = owner.params.findIndex( + (parameter) => + functionParameterBindingName(parameter, sourceCode) === predicateParameterName, + ); + return index === -1 ? null : index; +} + +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, + context.sourceCode.visitorKeys, + ); + }, + 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}\``, + ); + }, + CallExpression(node) { + if (environment === null) return; + const owner = localFunctionForCall(context.sourceCode, node.callee); + if (owner === null) return; + const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner); + if (parameterIndex === null) return; + const parameter = owner.params[parameterIndex]; + const argument = node.arguments[parameterIndex]; + if (parameter === undefined || argument === undefined || argument.type === "SpreadElement") { + return; + } + const parameterAnnotation = functionParameterTypeAnnotation(parameter); + if ( + parameterAnnotation === null || + parameterAnnotation === undefined || + !containsUnknownType(parameterAnnotation.typeAnnotation) + ) { + return; + } + if ( + !hasKnownCallArgumentEvidence( + context.sourceCode, + argument, + environment, + ) + ) { + return; + } + context.report({ + node: argument, + messageId: "widening", + data: { + subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``, + target: "unknown", + }, + }); + }, + 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/tools/oxlint/anti-slop/rules/no-module-mocking.ts b/tools/oxlint/anti-slop/rules/no-module-mocking.ts new file mode 100644 index 000000000..2d60d48c9 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-module-mocking.ts @@ -0,0 +1,80 @@ +import { defineRule } from "@oxlint/plugins"; + +import { resolveVariable } from "../shared/scope.ts"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]); + +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/tools/oxlint/anti-slop/rules/no-object-parameters.ts b/tools/oxlint/anti-slop/rules/no-object-parameters.ts new file mode 100644 index 000000000..6589ebf5f --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-object-parameters.ts @@ -0,0 +1,83 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; +type ParameterOwner = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +/** 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) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToObject = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSObjectKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + return ( + resolved.type === "TSUnionType" && resolved.types.some(matches) + ); + }); + + const checkParameters = (node: ParameterOwner) => { + for (const parameter of node.params) { + const annotation = functionParameterTypeAnnotation(parameter); + if (annotation === null || annotation === undefined) continue; + if (!resolvesToObject(annotation.typeAnnotation)) continue; + context.report({ + node: annotation.typeAnnotation, + messageId: "objectParameter", + data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }, + }); + } + }; + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + ArrowFunctionExpression: checkParameters, + FunctionDeclaration: checkParameters, + FunctionExpression: checkParameters, + TSCallSignatureDeclaration: checkParameters, + TSConstructSignatureDeclaration: checkParameters, + TSConstructorType: checkParameters, + TSDeclareFunction: checkParameters, + TSEmptyBodyFunctionExpression: checkParameters, + TSFunctionType: checkParameters, + TSMethodSignature: checkParameters, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-reduce-accumulator-copy.ts b/tools/oxlint/anti-slop/rules/no-reduce-accumulator-copy.ts new file mode 100644 index 000000000..47d8e2cf6 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-reduce-accumulator-copy.ts @@ -0,0 +1,109 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree, SourceCode, Variable } from "@oxlint/plugins"; + +import { + arrayMethodTarget, + isKnownArrayExpression, + resolveArrayBinding, + unwrapArrayExpression, +} from "../shared/array-method.ts"; + +function enclosingReducer(node: ESTree.Node) { + let parent = node.parent; + while (parent !== null) { + if (parent.type === "FunctionDeclaration") return null; + if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") { + const callback = parent; + let owner: ESTree.Node | null = callback.parent; + while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent; + if (owner?.type !== "CallExpression") return null; + const method = arrayMethodTarget(owner.callee); + const firstArgument = owner.arguments[0]; + if ( + method === null || (method.name !== "reduce" && method.name !== "reduceRight") || + owner.arguments.length > 2 || firstArgument === undefined || + unwrapArrayExpression(firstArgument) !== callback + ) return null; + const firstParameter = callback.params[0]; + const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter; + if (accumulator?.type !== "Identifier") return null; + return { callback, accumulator, initialValue: owner.arguments[1] }; + } + parent = parent.parent; + } + return null; +} + +function referencesAccumulator( + sourceCode: SourceCode, + node: ESTree.Node, + accumulator: Variable, + visited = new Set(), +): boolean { + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + if (variable === accumulator) return true; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited); + } + } + return false; +} + +function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier" || node.name !== name) return false; + const variable = resolveArrayBinding(sourceCode, node); + return variable === null || variable.defs.length === 0; +} + +/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */ +export const noReduceAccumulatorCopyRule = defineRule({ + meta: { + type: "problem", + docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." }, + messages: { + accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const method = arrayMethodTarget(node.callee); + if (method === null) return; + const reducer = enclosingReducer(node); + if (reducer === null) return; + const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable => + variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start), + ); + if (accumulator === undefined) return; + const isAccumulator = (expression: ESTree.Node) => + referencesAccumulator(context.sourceCode, expression, accumulator); + let copiesAccumulator = false; + if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) { + const target = node.arguments[0]; + copiesAccumulator = ( + target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" && + node.arguments.slice(1).some(isAccumulator) + ); + } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) { + const source = node.arguments[0]; + copiesAccumulator = source !== undefined && isAccumulator(source); + } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) { + const initialValue = reducer.initialValue; + const arrayAccumulator = initialValue !== undefined && + isKnownArrayExpression(context.sourceCode, initialValue); + copiesAccumulator = arrayAccumulator && isAccumulator(method.object); + } + if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" }); + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-reflect-apply.ts b/tools/oxlint/anti-slop/rules/no-reflect-apply.ts new file mode 100644 index 000000000..2cc30451b --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-reflect-apply.ts @@ -0,0 +1,28 @@ +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/tools/oxlint/anti-slop/rules/no-reflect-get.ts b/tools/oxlint/anti-slop/rules/no-reflect-get.ts new file mode 100644 index 000000000..cf630ecc0 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-reflect-get.ts @@ -0,0 +1,28 @@ +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/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts b/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts new file mode 100644 index 000000000..43259eb9e --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts @@ -0,0 +1,77 @@ +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; +} + +/** Return whether typeof safely probes for the existence of a possibly absent binding. */ +function isExistenceProbe(node: ESTree.UnaryExpression): boolean { + const parent = node.parent; + if (parent.type !== "BinaryExpression") return false; + if (!["===", "!==", "==", "!="].includes(parent.operator)) return false; + const other = parent.left === node ? parent.right : parent.left; + return other.type === "Literal" && other.value === "undefined"; +} + +/** 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" && + !isExistenceProbe(node) && + (!allowInTypeGuards || !isInsideTypeGuard(node)) + ) { + context.report({ node, messageId: "runtimeTypeof" }); + } + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts b/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts new file mode 100644 index 000000000..436d2a252 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts @@ -0,0 +1,46 @@ +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); +} + +/** Return whether an identifier names a statically accessed member owned by another value. */ +function isBorrowedMemberName(node: ESTree.Node): boolean { + const parent = node.parent; + if (parent === null || parent.type !== "MemberExpression") return false; + return parent.property === node && parent.computed === false; +} + +/** 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) || isBorrowedMemberName(node)) return; + context.report({ + node, + messageId: "forbiddenSymbolName", + data: { name: node.name }, + }); + }; + + return { + Identifier: reportForbiddenSymbolName, + PrivateIdentifier: reportForbiddenSymbolName, + JSXIdentifier: reportForbiddenSymbolName, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts b/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts new file mode 100644 index 000000000..b4a1545a7 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts @@ -0,0 +1,69 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree } from "@oxlint/plugins"; + +import { + containsUnknownType, + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +type ParameterOwner = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +function isTypePredicateSubject(owner: ParameterOwner, parameterName: string): boolean { + const predicate = owner.returnType?.typeAnnotation; + return ( + predicate?.type === "TSTypePredicate" && + predicate.parameterName.type === "Identifier" && + predicate.parameterName.name === parameterName + ); +} + +/** 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` and type-predicate subjects; 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 = functionParameterTypeAnnotation(parameter); + if (annotation === null || annotation === undefined) continue; + if (!containsUnknownType(annotation.typeAnnotation)) continue; + const name = functionParameterBindingName(parameter, context.sourceCode); + if (name === "cause" || isTypePredicateSubject(node, name)) 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/tools/oxlint/anti-slop/rules/no-unknown-returns.ts b/tools/oxlint/anti-slop/rules/no-unknown-returns.ts new file mode 100644 index 000000000..e1f43f8d0 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-unknown-returns.ts @@ -0,0 +1,82 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; + +type FunctionWithReturnType = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +/** 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) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToUnknown = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSUnknownKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + if (resolved.type === "TSUnionType") return resolved.types.some(matches); + if ( + resolved.type !== "TSTypeReference" || + resolved.typeName.type !== "Identifier" || + (resolved.typeName.name !== "Promise" && + resolved.typeName.name !== "PromiseLike") + ) { + return false; + } + const value = resolved.typeArguments?.params[0]; + return value !== undefined && matches(value); + }); + + const checkReturnType = (node: FunctionWithReturnType) => { + const annotation = node.returnType; + if (annotation === null || annotation === undefined) return; + if (!resolvesToUnknown(annotation.typeAnnotation)) return; + context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" }); + }; + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + ArrowFunctionExpression: checkReturnType, + FunctionDeclaration: checkReturnType, + FunctionExpression: checkReturnType, + TSCallSignatureDeclaration: checkReturnType, + TSConstructSignatureDeclaration: checkReturnType, + TSConstructorType: checkReturnType, + TSDeclareFunction: checkReturnType, + TSEmptyBodyFunctionExpression: checkReturnType, + TSFunctionType: checkReturnType, + TSMethodSignature: checkReturnType, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts b/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts new file mode 100644 index 000000000..af5f08a50 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts @@ -0,0 +1,54 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; + +/** 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) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToUnknown = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSUnknownKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + return resolved.type === "TSUnionType" && resolved.types.some(matches); + }); + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + TSTypeAliasDeclaration(node) { + if (!resolvesToUnknown(node.typeAnnotation)) return; + context.report({ + node: node.id, + messageId: "unknownAlias", + data: { alias: node.id.name }, + }); + }, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts b/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts new file mode 100644 index 000000000..8cb615a14 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts @@ -0,0 +1,154 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + classifyUnsafeDictionary, + classifyUnsafeDictionaryValue, + createTypeEnvironment, + type TypeEnvironment, +} from "../shared/dictionary-types.ts"; +import { visibleTypeAlias } from "../shared/type-alias-resolution.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 && + visibleTypeAlias(name, node, environment.typeAliases) !== null && + !isInsideTypeAliasDeclaration(node) + ); +} + +function isInsideTypeParameterConstraint(node: ESTree.TSType): boolean { + let child: ESTree.Node = node; + let parent: ESTree.Node | null = child.parent; + while (parent !== null && parent.type !== "Program") { + if (parent.type === "TSTypeParameter" && parent.constraint === child) return true; + child = parent; + parent = child.parent; + } + return false; +} + +function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean { + if (isInsideTypeParameterConstraint(node)) return false; + 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, + context.sourceCode.visitorKeys, + ); + }, + 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/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts b/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts new file mode 100644 index 000000000..c5e07f7fc --- /dev/null +++ b/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts @@ -0,0 +1,366 @@ +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/tools/oxlint/anti-slop/rules/require-readable-spacing.ts b/tools/oxlint/anti-slop/rules/require-readable-spacing.ts new file mode 100644 index 000000000..40735c731 --- /dev/null +++ b/tools/oxlint/anti-slop/rules/require-readable-spacing.ts @@ -0,0 +1,80 @@ +import type { CreateRule, ESTree, SourceCode } from "@oxlint/plugins"; + +import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts"; + +import type { NodeTestObject } from "../vendor/eslint-stylistic/padding-line-between-statements.ts"; + +/** A `const`/`let`/`var`/`using` declaration, optionally exported, written on one line. */ +function isSinglelineBinding(node: ESTree.Node): boolean { + const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node; + return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line; +} + +/** Comments above a statement belong to it; a comment trailing the previous line does not. */ +function hasLeadingComment(node: ESTree.Node, sourceCode: SourceCode): boolean { + const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line; + return sourceCode + .getCommentsBefore(node) + .some((comment) => comment.loc.start.line !== previousLine); +} + +const singlelineBinding: NodeTestObject = { + test: (node) => isSinglelineBinding(node), +}; + +const undocumentedSinglelineBinding: NodeTestObject = { + test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode), +}; + +const paddingRule = createPaddingLineRule( + [ + { blankLine: "always", prev: "import", next: "*" }, + { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } }, + { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" }, + { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] }, + { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" }, + { + blankLine: "always", + prev: "*", + next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + }, + { + blankLine: "always", + prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + next: "*", + }, + { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] }, + { blankLine: "always", prev: "block-like", next: "*" }, + { blankLine: "any", prev: "import", next: "import" }, + // Consecutive single-line bindings form one group at any scope, including + // module scope. A binding with its own leading comment starts a new group. + { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" }, + { + blankLine: "any", + prev: { + selector: + ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])', + }, + next: { + selector: + ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])', + }, + }, + ], + { + "singleline-binding": singlelineBinding, + "undocumented-singleline-binding": undocumentedSinglelineBinding, + }, +); + +/** Restore structural blank lines with whitespace-only fixes; keep single-line binding groups and overloads grouped. */ +export const requireReadableSpacingRule: CreateRule = { + ...paddingRule, + meta: { + ...paddingRule.meta, + docs: { + description: "Require readable spacing between declarations and logical statement groups.", + }, + schema: [], + }, +}; diff --git a/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts b/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts new file mode 100644 index 000000000..bfea1a23a --- /dev/null +++ b/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts @@ -0,0 +1,131 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion; + +const DEFAULT_SAFETY_MARKERS = ["SAFETY"] as const; + +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 configuredSafetyMarkers(option: unknown): readonly string[] { + if (typeof option !== "object" || option === null || !("markers" in option)) { + return DEFAULT_SAFETY_MARKERS; + } + const configured = option.markers; + if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS; + const markers = configured.flatMap((marker) => + typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : [], + ); + return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS; +} + +function markerPattern(markers: readonly string[]): RegExp { + const alternation = markers + .map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)) + .join("|"); + return new RegExp( + String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`, + "u", + ); +} + +function hasSafetyJustificationBefore( + sourceCode: SourceCode, + owner: ESTree.Node, + assertion: TypeAssertion, + pattern: RegExp, +): boolean { + return sourceCode + .getCommentsBefore(owner) + .some( + (comment) => comment.end <= assertion.start && pattern.test(comment.value), + ); +} + +function hasSafetyComment( + sourceCode: SourceCode, + node: TypeAssertion, + pattern: RegExp, +): boolean { + let current: ESTree.Node = node; + while (true) { + if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true; + if (commentOwnerKinds.has(current.type)) { + const exportDeclaration = current.parent; + return ( + exportDeclaration.type === "ExportNamedDeclaration" && + exportDeclaration.declaration === current && + hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern) + ); + } + if (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 `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.", + }, + schema: [ + { + type: "object", + properties: { + markers: { + type: "array", + items: { type: "string", minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + defaultOptions: [{ markers: ["SAFETY"] }], + }, + createOnce(context) { + const patterns = new Map(); + + const checkAssertion = (node: TypeAssertion) => { + if (isConstAssertion(node)) return; + const markers = configuredSafetyMarkers(context.options?.[0]); + const patternKey = markers.join("\u0000"); + const pattern = patterns.get(patternKey) ?? markerPattern(markers); + patterns.set(patternKey, pattern); + if (hasSafetyComment(context.sourceCode, node, pattern)) return; + context.report({ + node, + messageId: "missingSafetyComment", + data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] }, + }); + }; + + return { + TSAsExpression: checkAssertion, + TSTypeAssertion: checkAssertion, + }; + }, +}); diff --git a/tools/oxlint/anti-slop/shared/array-method.ts b/tools/oxlint/anti-slop/shared/array-method.ts new file mode 100644 index 000000000..87eb9d842 --- /dev/null +++ b/tools/oxlint/anti-slop/shared/array-method.ts @@ -0,0 +1,94 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */ +export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node { + while ( + node.type === "ParenthesizedExpression" || + node.type === "ChainExpression" || + node.type === "TSAsExpression" || + node.type === "TSTypeAssertion" || + node.type === "TSNonNullExpression" || + node.type === "TSSatisfiesExpression" + ) { + node = node.expression; + } + return node; +} + +/** Resolve a local binding by scope, not by identifier spelling. */ +export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier") return null; + let scope: Scope | null = sourceCode.getScope(node); + while (scope !== null) { + const variable = scope.set.get(node.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return null; +} + +/** Read static method names, including computed string literals, without evaluating expressions. */ +export function arrayMethodTarget( + node: ESTree.Node, +): { readonly name: string; readonly object: ESTree.Node } | null { + node = unwrapArrayExpression(node); + if (node.type !== "MemberExpression") return null; + const property = node.property; + if (!node.computed && property.type === "Identifier") { + return { name: property.name, object: node.object }; + } + if (node.computed && property.type === "Literal" && typeof property.value === "string") { + return { name: property.value, object: node.object }; + } + return null; +} + +function isArrayAnnotation(type: ESTree.TSType): boolean { + if (type.type === "TSArrayType" || type.type === "TSTupleType") return true; + if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation); + if (type.type === "TSTypeOperator" && type.operator === "readonly") { + return isArrayAnnotation(type.typeAnnotation); + } + return ( + type.type === "TSTypeReference" && type.typeName.type === "Identifier" && + (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray") + ); +} + +/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */ +export function isKnownArrayExpression( + sourceCode: SourceCode, + node: ESTree.Node, + visited = new Set(), +): boolean { + node = unwrapArrayExpression(node); + if (node.type === "ArrayExpression") return true; + if (node.type === "CallExpression") { + const method = arrayMethodTarget(node.callee); + return ( + method !== null && + ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) && + isKnownArrayExpression(sourceCode, method.object, visited) + ); + } + if (node.type !== "Identifier") return false; + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const identifier of variable.identifiers) { + const annotation = identifier.typeAnnotation?.typeAnnotation; + if (annotation !== undefined) return isArrayAnnotation(annotation); + } + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return isKnownArrayExpression(sourceCode, definition.node.init, visited); + } + } + return false; +} diff --git a/tools/oxlint/anti-slop/shared/dictionary-types.ts b/tools/oxlint/anti-slop/shared/dictionary-types.ts new file mode 100644 index 000000000..8db73ffab --- /dev/null +++ b/tools/oxlint/anti-slop/shared/dictionary-types.ts @@ -0,0 +1,515 @@ +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + hasVisibleTypeBinding, + visibleTypeAlias, + type TypeAliasEnvironment as LexicalTypeAliasEnvironment, +} from "./type-alias-resolution.ts"; + +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 interfaces: ReadonlyMap; + readonly typeAliases: LexicalTypeAliasEnvironment; +}; + +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, + visitorKeys: Readonly>, +): TypeEnvironment { + const interfaces = new Map(); + + for (const statement of program.body) { + const declaration = declaredStatement(statement); + if (declaration?.type !== "TSInterfaceDeclaration") continue; + const declarations = interfaces.get(declaration.id.name) ?? []; + declarations.push(declaration); + interfaces.set(declaration.id.name, declarations); + } + + return { + interfaces, + typeAliases: createTypeAliasEnvironment(program, visitorKeys), + }; +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function isBuiltIn( + name: string, + use: ESTree.Node, + environment: TypeEnvironment, +): boolean { + return ( + BUILT_INS.has(name) && + !hasVisibleTypeBinding(name, use, environment.typeAliases) + ); +} + +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, unwrapped, 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 = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || 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, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined + ? [] + : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases); + } + + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + const value = unwrapped.typeArguments?.params[1] ?? null; + return value === null ? [] : [{ type: value, substitutions }]; + } + + if ( + (name === "Pick" || name === "Omit") && + isBuiltIn(name, unwrapped, environment) + ) { + const source = unwrapped.typeArguments?.params[0]; + return source === undefined + ? [] + : dictionaryValueTypes(source, environment, substitutions, resolvingAliases); + } + + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || 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; +} + +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, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment); + } + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + return hasBroadRecordKey(unwrapped, environment, new Map()) + ? { kind: "open dictionary" } + : null; + } + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null) return null; + if ((alias.typeParameters?.params.length ?? 0) > 0) { + const substitutions = aliasSubstitution(alias, unwrapped, new Map()); + const resolved = + substitutions === null + ? null + : classifyAliasBroadTarget( + alias.typeAnnotation, + environment, + substitutions, + new Set([name]), + ); + return resolved?.kind === "open dictionary" ? { 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 hasBroadRecordKey( + type: ESTree.TSTypeReference, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, +): boolean { + const key = type.typeArguments?.params[0]; + return key === undefined || isBroadMappedKey(key, environment, substitutions); +} + +function isBroadMappedKey( + type: ESTree.TSType, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, + visitedAliases: ReadonlySet = new Set(), +): boolean { + const unwrapped = unwrapTransparentType(type); + if ( + unwrapped.type === "TSStringKeyword" || + unwrapped.type === "TSNumberKeyword" || + unwrapped.type === "TSSymbolKeyword" + ) { + return true; + } + if (unwrapped.type === "TSUnionType") { + return unwrapped.types.some((member) => + isBroadMappedKey(member, environment, substitutions, visitedAliases), + ); + } + 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, visitedAliases); + } + if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true; + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if ( + alias === null || + (alias.typeParameters?.params.length ?? 0) > 0 || + visitedAliases.has(name) + ) { + return false; + } + const nextVisited = new Set(visitedAliases); + nextVisited.add(name); + return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited); +} + +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, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined + ? null + : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases); + } + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + return hasBroadRecordKey(unwrapped, environment, substitutions) + ? { kind: "open dictionary" } + : null; + } + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || 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/tools/oxlint/anti-slop/shared/function-parameters.ts b/tools/oxlint/anti-slop/shared/function-parameters.ts new file mode 100644 index 000000000..80de91d7a --- /dev/null +++ b/tools/oxlint/anti-slop/shared/function-parameters.ts @@ -0,0 +1,49 @@ +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +export type FunctionParameter = ESTree.ParamPattern; + +/** Return whether a type is or contains TypeScript's absorbing unknown top type. */ +export function containsUnknownType(type: ESTree.TSType): boolean { + if (type.type === "TSUnknownKeyword") return true; + if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation); + return type.type === "TSUnionType" && type.types.some(containsUnknownType); +} + +/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */ +export function functionParameterTypeAnnotation( + parameter: FunctionParameter, +): ESTree.TSTypeAnnotation | null | undefined { + if (parameter.type === "TSParameterProperty") { + return functionParameterTypeAnnotation(parameter.parameter); + } + if (parameter.type === "RestElement") { + return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument); + } + if (parameter.type === "AssignmentPattern") { + return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left); + } + return parameter.typeAnnotation; +} + +/** Return only a function parameter's local binding, excluding its annotation and default value. */ +export function functionParameterBindingName( + parameter: FunctionParameter, + sourceCode: SourceCode, +): string { + if (parameter.type === "TSParameterProperty") { + return functionParameterBindingName(parameter.parameter, sourceCode); + } + if (parameter.type === "AssignmentPattern") { + return functionParameterBindingName(parameter.left, sourceCode); + } + if (parameter.type === "RestElement") { + return functionParameterBindingName(parameter.argument, sourceCode); + } + if (parameter.type === "Identifier") return parameter.name; + + const sourceText = sourceCode.getText(parameter); + const annotationStart = parameter.typeAnnotation?.start; + return annotationStart === undefined + ? sourceText + : sourceText.slice(0, annotationStart - parameter.start).trimEnd(); +} diff --git a/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts b/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts new file mode 100644 index 000000000..7cdb18c91 --- /dev/null +++ b/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts @@ -0,0 +1,61 @@ +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/tools/oxlint/anti-slop/shared/reflect-method.ts b/tools/oxlint/anti-slop/shared/reflect-method.ts new file mode 100644 index 000000000..43a7cfcc1 --- /dev/null +++ b/tools/oxlint/anti-slop/shared/reflect-method.ts @@ -0,0 +1,24 @@ +import { resolveVariable } from "./scope.ts"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +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; +} diff --git a/tools/oxlint/anti-slop/shared/scope.ts b/tools/oxlint/anti-slop/shared/scope.ts new file mode 100644 index 000000000..602414aba --- /dev/null +++ b/tools/oxlint/anti-slop/shared/scope.ts @@ -0,0 +1,15 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Resolve an identifier to its binding by walking lexical scopes upward. */ +export 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; +} diff --git a/tools/oxlint/anti-slop/shared/type-alias-resolution.ts b/tools/oxlint/anti-slop/shared/type-alias-resolution.ts new file mode 100644 index 000000000..c3c3a2584 --- /dev/null +++ b/tools/oxlint/anti-slop/shared/type-alias-resolution.ts @@ -0,0 +1,250 @@ +import type { ESTree } from "@oxlint/plugins"; + +import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts"; + +type VisitorKeys = Readonly>; +type TypeScope = ESTree.Node; + +type TypeBinding = { + readonly alias: ESTree.TSTypeAliasDeclaration | null; + readonly name: string; + readonly scope: TypeScope; +}; + +type Substitution = { + readonly substitutions: Substitutions; + readonly type: ESTree.TSType; +}; + +type Substitutions = ReadonlyMap; + +export type TypeAliasEnvironment = { + readonly aliases: readonly ESTree.TSTypeAliasDeclaration[]; + readonly bindingsByName: ReadonlyMap; + readonly visitorKeys: VisitorKeys; +}; + +export type ResolvedTypeMatcher = ( + type: ESTree.TSType, + matches: (child: ESTree.TSType) => boolean, +) => boolean; + +const environmentsByProgram = new WeakMap(); + +function isNode(value: unknown): value is ESTree.Node { + return ( + typeof value === "object" && + value !== null && + "type" in value && + typeof value.type === "string" + ); +} + +function enclosingTypeScope(node: ESTree.Node): TypeScope { + let current: ESTree.Node | null = node.parent; + while (current !== null) { + if ( + current.type === "Program" || + current.type === "BlockStatement" || + current.type === "TSModuleBlock" || + current.type === "StaticBlock" || + current.type === "SwitchStatement" + ) { + return current; + } + current = current.parent; + } + return node; +} + +function declaredTypeBinding(node: ESTree.Node): { + readonly alias: ESTree.TSTypeAliasDeclaration | null; + readonly name: string; +} | null { + if (node.type === "TSTypeAliasDeclaration") { + return { alias: node, name: node.id.name }; + } + if ( + node.type === "TSInterfaceDeclaration" || + node.type === "TSEnumDeclaration" || + node.type === "ClassDeclaration" || + node.type === "ClassExpression" + ) { + return node.id === null ? null : { alias: null, name: node.id.name }; + } + if ( + node.type === "ImportSpecifier" || + node.type === "ImportDefaultSpecifier" || + node.type === "ImportNamespaceSpecifier" + ) { + return { alias: null, name: node.local.name }; + } + return null; +} + +function collectTypeBindings( + node: ESTree.Node, + visitorKeys: VisitorKeys, + bindingsByName: Map, + aliases: ESTree.TSTypeAliasDeclaration[], +): void { + const declared = declaredTypeBinding(node); + if (declared !== null) { + const bindings = bindingsByName.get(declared.name) ?? []; + bindings.push({ ...declared, scope: enclosingTypeScope(node) }); + bindingsByName.set(declared.name, bindings); + if (declared.alias !== null) aliases.push(declared.alias); + } + + // SAFETY: Oxlint's visitor keys identify only ESTree child-node properties. + const fields = node as unknown as Readonly>; + for (const key of visitorKeys[node.type] ?? []) { + const value = fields[key]; + if (isNode(value)) { + collectTypeBindings(value, visitorKeys, bindingsByName, aliases); + continue; + } + if (!Array.isArray(value)) continue; + for (const child of value) { + if (isNode(child)) { + collectTypeBindings(child, visitorKeys, bindingsByName, aliases); + } + } + } +} + +/** Collect every lexical type alias and competing type binding in a program. */ +export function createTypeAliasEnvironment( + program: ESTree.Program, + visitorKeys: VisitorKeys, +): TypeAliasEnvironment { + const cached = environmentsByProgram.get(program); + if (cached !== undefined) return cached; + const bindingsByName = new Map(); + const aliases: ESTree.TSTypeAliasDeclaration[] = []; + collectTypeBindings(program, visitorKeys, bindingsByName, aliases); + const environment = { aliases, bindingsByName, visitorKeys }; + environmentsByProgram.set(program, environment); + return environment; +} + +function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null { + let current: ESTree.Node | null = node; + let distance = 0; + while (current !== null) { + if (current === ancestor) return distance; + current = current.parent; + distance += 1; + } + return null; +} + +function nearestTypeBindings( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): readonly TypeBinding[] { + const candidates = environment.bindingsByName.get(name) ?? []; + let nearestDistance = Number.POSITIVE_INFINITY; + let nearest: TypeBinding[] = []; + for (const candidate of candidates) { + const distance = ancestorDistance(candidate.scope, use); + if (distance === null || distance > nearestDistance) continue; + if (distance === nearestDistance) { + nearest.push(candidate); + continue; + } + nearestDistance = distance; + nearest = [candidate]; + } + return nearest; +} + +/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */ +export function visibleTypeAlias( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): ESTree.TSTypeAliasDeclaration | null { + if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null; + const bindings = nearestTypeBindings(name, use, environment); + return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null; +} + +/** Return whether a local declaration shadows a built-in type at this use. */ +export function hasVisibleTypeBinding( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): boolean { + return ( + lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || + nearestTypeBindings(name, use, environment).length > 0 + ); +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function aliasSubstitutions( + alias: ESTree.TSTypeAliasDeclaration, + reference: ESTree.TSTypeReference, + base: Substitutions, +): Substitutions | null { + const parameters = alias.typeParameters?.params ?? []; + const arguments_ = reference.typeArguments?.params ?? []; + const next = new Map(); + for (const [index, parameter] of parameters.entries()) { + const explicitArgument = arguments_[index]; + const argument = explicitArgument ?? parameter.default; + if (argument === null || argument === undefined) return null; + const argumentSubstitutions = explicitArgument === undefined ? next : base; + next.set(parameter.name.name, { + type: argument, + substitutions: new Map(argumentSubstitutions), + }); + } + return next; +} + +/** Match a type after resolving visible aliases and substituting their type parameters. */ +export function resolvedTypeMatches( + type: ESTree.TSType, + environment: TypeAliasEnvironment, + matcher: ResolvedTypeMatcher, +): boolean { + const evaluate = ( + current: ESTree.TSType, + substitutions: Substitutions, + resolvingAliases: ReadonlySet, + ): boolean => { + if (current.type === "TSTypeReference") { + const name = typeReferenceName(current); + if (name !== null) { + const substitution = substitutions.get(name); + if (substitution !== undefined && !current.typeArguments?.params.length) { + return evaluate( + substitution.type, + substitution.substitutions, + resolvingAliases, + ); + } + const alias = visibleTypeAlias(name, current, environment); + if (alias !== null && !resolvingAliases.has(alias)) { + const nextSubstitutions = aliasSubstitutions(alias, current, substitutions); + if (nextSubstitutions !== null) { + const nextResolving = new Set(resolvingAliases); + nextResolving.add(alias); + return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving); + } + } + } + } + return matcher(current, (child) => + evaluate(child, substitutions, resolvingAliases), + ); + }; + + return evaluate(type, new Map(), new Set()); +} diff --git a/tools/oxlint/anti-slop/vendor/eslint-stylistic/LICENSE b/tools/oxlint/anti-slop/vendor/eslint-stylistic/LICENSE new file mode 100644 index 000000000..38dbc3547 --- /dev/null +++ b/tools/oxlint/anti-slop/vendor/eslint-stylistic/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright OpenJS Foundation and other contributors, +Copyright (c) 2023-PRESENT ESLint Stylistic contributors + +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/tools/oxlint/anti-slop/vendor/eslint-stylistic/UPSTREAM.md b/tools/oxlint/anti-slop/vendor/eslint-stylistic/UPSTREAM.md new file mode 100644 index 000000000..95d15ed3d --- /dev/null +++ b/tools/oxlint/anti-slop/vendor/eslint-stylistic/UPSTREAM.md @@ -0,0 +1,28 @@ +# Vendored padding-line-between-statements + +Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`. + +Copied files: + +- `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts` +- `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts` +- Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply. + +The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, including skill assets. No Stylistic, ESLint, TypeScript-ESLint, or additional parser runtime dependency is required. + +## Local adaptations + +- Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately. +- Replace `AST_NODE_TYPES` enum members with identical string literals. +- Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions. +- Replace the repository-specific `createRule` factory with `createPaddingLineRule(options, statementTypes?)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options. The optional second argument registers caller-defined statement types, consulted before the upstream table, so a policy can match syntax the upstream matchers do not distinguish (exported bindings, comment ownership) without editing the vendored tables. +- Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads. +- Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code. + +The opinionated policy lives outside this directory in `../../rules/require-readable-spacing.ts`. It adds spacing without collapsing existing blank lines. Short local bindings, consecutive imports, and adjacent overload signatures/implementation remain grouped. Spacing is syntactic, not an inference of business-logic boundaries. + +## Updating and verification + +Fetch an explicit upstream revision, compare the original rule and types against this revision, and port relevant fixes while retaining the adapters above. Update this record and preserve the license. Run `pnpm check` and `pnpm sync:skill-assets` as required by repository guidance. + +Focused Oxlint RuleTester cases live in `../../rules/require-readable-spacing.test.ts`; they test exact fixes, JSDoc/trailing comments, same-line statements, semicolon-free code, TypeScript exports/overloads, Effect-style generators, and upstream removal behavior. `../../rules/require-readable-spacing-cli.test.ts` verifies the exported plugin through the native Oxlint CLI on multiple files, including rejection, autofix, and repeated-fix stability. The complete upstream JS/TS test suites have not been ported; this is focused compatibility evidence, not a claim of full upstream conformance. diff --git a/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts new file mode 100644 index 000000000..10f34cc8e --- /dev/null +++ b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts @@ -0,0 +1,51 @@ +// Local replacements for the upstream helper imports. See UPSTREAM.md. +import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins"; + +type Token = SyntaxToken | Comment; + +/** Line terminators recognized by the upstream padding matcher. */ +export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); + +/** Test a closing brace without treating comment text as punctuation. */ +export const isClosingBraceToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === "}"; + +/** Test a semicolon without treating comment text as punctuation. */ +export const isSemicolonToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === ";"; + +/** Filter the optional final semicolon when identifying block-like statements. */ +export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token); + +/** Compare token/node boundaries, including attached comments. */ +export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean => + left.loc.end.line === right.loc.start.line; + +/** Recognize declarations and expressions used by the upstream IIFE matcher. */ +export const isFunction = (node: ESTree.Node): boolean => + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression"; + +/** Preserve the upstream multiline statement heuristic. */ +export const isSingleLine = (node: ESTree.Node): boolean => + node.loc.start.line === node.loc.end.line; + +/** Unwrap optional chaining before checking IIFE syntax. */ +export const skipChainExpression = (node: ESTree.Node): ESTree.Node => + node.type === "ChainExpression" ? node.expression : node; + +/** Only a program or function-body expression can begin a directive prologue. */ +export const isTopLevelExpressionStatement = ( + node: ESTree.Node, +): node is ESTree.ExpressionStatement => + node.type === "ExpressionStatement" && + (node.parent.type === "Program" || + (node.parent.type === "BlockStatement" && isFunction(node.parent.parent))); + +/** A single wrapping pair suffices to exclude a string from directive syntax. */ +export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean { + const before = sourceCode.getTokenBefore(node); + const after = sourceCode.getTokenAfter(node); + return before?.value === "(" && after?.value === ")"; +} diff --git a/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts new file mode 100644 index 000000000..8e2c99f31 --- /dev/null +++ b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts @@ -0,0 +1,929 @@ +// Vendored from ESLint Stylistic; see UPSTREAM.md and LICENSE in this directory. +import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken, Comment, CreateRule, Location } from '@oxlint/plugins' +type ASTNode = ESTree.Node +type Token = SyntaxToken | Comment +import type { + PaddingType, + SelectorOption, + StatementMatcher, +} from './padding-line-options.d.ts' +import { + isClosingBraceToken, + isFunction, + isNotSemicolonToken, + isParenthesized, + isSemicolonToken, + isSingleLine, + isTokenOnSameLine, + isTopLevelExpressionStatement, + LINEBREAKS, + skipChainExpression, +} from './padding-line-ast.ts' + +const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u +const CJS_IMPORT = /^require\(/u + +/** + * This rule is a replica of padding-line-between-statements. + * + * Ideally we would want to extend the rule support typescript specific support. + * But since not all the state is exposed by the eslint and eslint has frozen stylistic rules, + * (see - https://eslint.org/blog/2020/05/changes-to-rules-policies for details.) + * we are forced to re-implement the rule here. + * + * We have tried to keep the implementation as close as possible to the eslint implementation, to make + * patching easier for future contributors. + * + * Reference rule - https://github.com/eslint/eslint/blob/main/lib/rules/padding-line-between-statements.js + */ + +type NodeTest = ( + node: ASTNode, + sourceCode: SourceCode, +) => boolean + +export interface NodeTestObject { + test: NodeTest +} + +/** + * A policy entry may name a caller-defined statement type (registered through + * `createPaddingLineRule`) anywhere the upstream statement types are accepted. + */ +export type PolicyStatementOption = + | StatementMatcher + | Extra + | [StatementMatcher | Extra, ...(StatementMatcher | Extra)[]] + +export type PolicyOptions = { + blankLine: PaddingType + prev: PolicyStatementOption + next: PolicyStatementOption +}[] + +const LT = `[${Array.from(LINEBREAKS).join('')}]` +const PADDING_LINE_SEQUENCE = new RegExp( + String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, + 'u', +) + +function isSelectorOption(option: PolicyStatementOption): option is SelectorOption { + return typeof option === 'object' && !Array.isArray(option) +} + +/** + * Creates tester which check if a node starts with specific keyword with the + * appropriate AST_NODE_TYPES. + * @param keyword The keyword to test. + * @returns the created tester. + * @private + */ +function newKeywordTester( + type: string | string[], + keyword: string, +): NodeTestObject { + return { + test(node, sourceCode): boolean { + const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword + const isSameType = Array.isArray(type) + ? type.includes(node.type) + : type === node.type + + return isSameKeyword && isSameType + }, + } +} + +/** + * Creates tester which check if a node is specific type. + * @param type The node type to test. + * @returns the created tester. + * @private + */ +function newNodeTypeTester(type: string): NodeTestObject { + return { + test: (node): boolean => node.type === type, + } +} + +/** + * Checks the given node is an expression statement of IIFE. + * @param node The node to check. + * @returns `true` if the node is an expression statement of IIFE. + * @private + */ +function isIIFEStatement(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + let expression = skipChainExpression(node.expression) + if (expression.type === 'UnaryExpression') + expression = skipChainExpression(expression.argument) + + if (expression.type === 'CallExpression') { + let node: ASTNode = expression.callee + while (node.type === 'SequenceExpression') { + const lastExpression = node.expressions.at(-1) + if (lastExpression === undefined) + throw new Error('Padding rule invariant: sequence expression is empty') + node = lastExpression + } + + return isFunction(node) + } + } + return false +} + +/** + * Checks the given node is a CommonJS require statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS require statement. + * @private + */ +function isCJSRequire(node: ASTNode): boolean { + if (node.type === 'VariableDeclaration') { + const declaration = node.declarations[0] + if (declaration?.init) { + let call = declaration?.init + while (call.type === 'MemberExpression') + call = call.object + + if ( + call.type === 'CallExpression' + && call.callee.type === 'Identifier' + ) { + return call.callee.name === 'require' + } + } + } + return false +} + +/** + * Checks whether the given node is a block-like statement. + * This checks the last token of the node is the closing brace of a block. + * @param sourceCode The source code to get tokens. + * @param node The node to check. + * @returns `true` if the node is a block-like statement. + * @private + */ +function isBlockLikeStatement( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + // do-while with a block is a block-like statement. + if ( + node.type === 'DoWhileStatement' + && node.body.type === 'BlockStatement' + ) { + return true + } + + /** + * IIFE is a block-like statement specially from + * JSCS#disallowPaddingNewLinesAfterBlocks. + */ + if (isIIFEStatement(node)) + return true + + // Checks the last token is a closing brace of blocks. + const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken) + const belongingNode + = lastToken && isClosingBraceToken(lastToken) + ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) + : null + + return ( + !!belongingNode + && (belongingNode.type === 'BlockStatement' + || belongingNode.type === 'SwitchStatement') + ) +} + +/** + * Check whether the given node is a directive or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a directive. + */ +function isDirective( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + isTopLevelExpressionStatement(node) + && node.expression.type === 'Literal' + && typeof node.expression.value === 'string' + && !isParenthesized(node.expression, sourceCode) + ) +} + +/** + * Check whether the given node is a part of directive prologue or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a part of directive prologue. + */ +function isDirectivePrologue( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + if ( + isDirective(node, sourceCode) + && node.parent + && 'body' in node.parent + && Array.isArray(node.parent.body) + ) { + for (const sibling of node.parent.body) { + if (sibling === node) + break + + if (!isDirective(sibling, sourceCode)) + return false + } + return true + } + return false +} + +/** + * Checks the given node is a CommonJS export statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS export statement. + * @private + */ +function isCJSExport(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + const expression = node.expression + if (expression.type === 'AssignmentExpression') { + let left = expression.left + if (left.type === 'MemberExpression') { + while (left.object.type === 'MemberExpression') + left = left.object + + return ( + left.object.type === 'Identifier' + && (left.object.name === 'exports' + || (left.object.name === 'module' + && left.property.type === 'Identifier' + && left.property.name === 'exports')) + ) + } + } + } + return false +} + +/** + * Check whether the given node is an expression + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is an expression + */ +function isExpression( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + node.type === 'ExpressionStatement' + && !isDirectivePrologue(node, sourceCode) + ) +} + +/** + * Gets the actual last token. + * + * If a semicolon is semicolon-less style's semicolon, this ignores it. + * For example: + * + * foo() + * ;[1, 2, 3].forEach(bar) + * @param sourceCode The source code to get tokens. + * @param node The node to get. + * @returns The actual last token. + * @private + */ +function getActualLastToken( + node: ASTNode, + sourceCode: SourceCode, +): Token | null { + const semiToken = sourceCode.getLastToken(node)! + const prevToken = sourceCode.getTokenBefore(semiToken) + const nextToken = sourceCode.getTokenAfter(semiToken) + const isSemicolonLessStyle + = prevToken + && nextToken + && prevToken.range[0] >= node.range[0] + && isSemicolonToken(semiToken) + && !isTokenOnSameLine(prevToken, semiToken) + && isTokenOnSameLine(semiToken, nextToken) + + return isSemicolonLessStyle ? prevToken : semiToken +} + +/** + * This returns the concatenation of the first 2 captured strings. + * @param _ Unused. Whole matched string. + * @param trailingSpaces The trailing spaces of the first line. + * @param indentSpaces The indentation spaces of the last line. + * @returns The concatenation of trailingSpaces and indentSpaces. + * @private + */ +function replacerToRemovePaddingLines( + _: string, + trailingSpaces: string, + indentSpaces: string, +): string { + return trailingSpaces + indentSpaces +} + +function getReportLoc(node: ASTNode, sourceCode: SourceCode): Location { + if (isSingleLine(node)) + return node.loc + + const line = node.loc.start.line + const sourceLine = sourceCode.lines[line - 1] + if (sourceLine === undefined) + throw new Error('Padding rule invariant: statement source line is missing') + + return { + start: node.loc.start, + end: { + line, + column: sourceLine.length, + }, + } +} + +/** + * Check and report statements for `any` configuration. + * It does nothing. + * + * @private + */ +function verifyForAny(): void { + // Empty +} + +/** + * Check and report statements for `never` configuration. + * This autofix removes blank lines between the given 2 statements. + * However, if comments exist between 2 blank lines, it does not remove those + * blank lines automatically. + * @param context The rule context to report. + * @param _ Unused. The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForNever( + context: RuleContext, + _: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length === 0) + return + + context.report({ + node: nextNode, + messageId: 'unexpectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + if (paddingLines.length >= 2) + return null + + const paddingPair = paddingLines[0] + if (paddingPair === undefined) + throw new Error('Padding rule invariant: reported padding pair is missing') + const [prevToken, nextToken] = paddingPair + const start = prevToken.range[1] + const end = nextToken.range[0] + const text = context + .sourceCode + .text + .slice(start, end) + .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines) + + return fixer.replaceTextRange([start, end], text) + }, + }) +} + +/** + * Check and report statements for `always` configuration. + * This autofix inserts a blank line between the given 2 statements. + * If the `prevNode` has trailing comments, it inserts a blank line after the + * trailing comments. + * @param context The rule context to report. + * @param prevNode The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForAlways( + context: RuleContext, + prevNode: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length > 0) + return + + context.report({ + node: nextNode, + messageId: 'expectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + const sourceCode = context.sourceCode + let prevToken = getActualLastToken(prevNode, sourceCode)! + const nextToken + = sourceCode.getFirstTokenBetween(prevToken, nextNode, { + includeComments: true, + + /** + * Skip the trailing comments of the previous node. + * This inserts a blank line after the last trailing comment. + * + * For example: + * + * foo(); // trailing comment. + * // comment. + * bar(); + * + * Get fixed to: + * + * foo(); // trailing comment. + * + * // comment. + * bar(); + * @param token The token to check. + * @returns `true` if the token is not a trailing comment. + * @private + */ + filter(token) { + if (isTokenOnSameLine(prevToken, token)) { + prevToken = token + return false + } + return true + }, + })! || nextNode + const insertText = isTokenOnSameLine(prevToken, nextToken) + ? '\n\n' + : '\n' + + return fixer.insertTextAfter(prevToken, insertText) + }, + }) +} + +/** + * Types of blank lines. + * `any`, `never`, and `always` are defined. + * Those have `verify` method to check and report statements. + * @private + */ +const PaddingTypes = { + any: { verify: verifyForAny }, + never: { verify: verifyForNever }, + always: { verify: verifyForAlways }, +} + +const MaybeMultilineStatementType: Record = { + 'block-like': { test: isBlockLikeStatement }, + 'expression': { test: isExpression }, + 'return': newKeywordTester('ReturnStatement', 'return'), + 'export': newKeywordTester( + [ + 'ExportAllDeclaration', + 'ExportDefaultDeclaration', + 'ExportNamedDeclaration', + ], + 'export', + ), + 'var': newKeywordTester('VariableDeclaration', 'var'), + 'let': newKeywordTester('VariableDeclaration', 'let'), + 'const': newKeywordTester('VariableDeclaration', 'const'), + 'using': { + test: node => node.type === 'VariableDeclaration' + && (node.kind === 'using' || node.kind === 'await using'), + }, + 'type': newKeywordTester('TSTypeAliasDeclaration', 'type'), +} + +/** + * Types of statements. + * Those have `test` method to check it matches to the given statement. + * @private + */ +const StatementTypes: Record = { + '*': { test: (): boolean => true }, + 'exports': { test: isCJSExport }, + 'require': { test: isCJSRequire }, + 'directive': { test: isDirectivePrologue }, + 'iife': { test: isIIFEStatement }, + + 'block': newNodeTypeTester('BlockStatement'), + 'empty': newNodeTypeTester('EmptyStatement'), + 'function': newNodeTypeTester('FunctionDeclaration'), + 'ts-method': newNodeTypeTester('TSMethodSignature'), + + 'break': newKeywordTester('BreakStatement', 'break'), + 'case': newKeywordTester('SwitchCase', 'case'), + 'class': newKeywordTester('ClassDeclaration', 'class'), + 'continue': newKeywordTester('ContinueStatement', 'continue'), + 'debugger': newKeywordTester('DebuggerStatement', 'debugger'), + 'default': newKeywordTester( + ['SwitchCase', 'ExportDefaultDeclaration'], + 'default', + ), + 'do': newKeywordTester('DoWhileStatement', 'do'), + 'for': newKeywordTester( + [ + 'ForStatement', + 'ForInStatement', + 'ForOfStatement', + ], + 'for', + ), + 'if': newKeywordTester('IfStatement', 'if'), + 'import': newKeywordTester('ImportDeclaration', 'import'), + 'switch': newKeywordTester('SwitchStatement', 'switch'), + 'throw': newKeywordTester('ThrowStatement', 'throw'), + 'try': newKeywordTester('TryStatement', 'try'), + 'while': newKeywordTester( + ['WhileStatement', 'DoWhileStatement'], + 'while', + ), + 'with': newKeywordTester('WithStatement', 'with'), + + 'cjs-export': { + test: (node, sourceCode) => node.type === 'ExpressionStatement' + && node.expression.type === 'AssignmentExpression' + && CJS_EXPORT.test(sourceCode.getText(node.expression.left)), + }, + 'cjs-import': { + test: (node, sourceCode) => node.type === 'VariableDeclaration' + && node.declarations.length > 0 + && node.declarations[0]?.init != null + && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)), + }, + + 'enum': newKeywordTester( + 'TSEnumDeclaration', + 'enum', + ), + 'interface': newKeywordTester( + 'TSInterfaceDeclaration', + 'interface', + ), + 'function-overload': newNodeTypeTester('TSDeclareFunction'), + ...Object.fromEntries( + Object.entries(MaybeMultilineStatementType) + .flatMap(([key, value]) => [ + [key, value], + [ + `singleline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node), + }, + ], + [ + `multiline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node), + }, + ], + ]), + ), +} + +/** + * Build the vendored padding rule with caller-owned, typed policy options. + * `statementTypes` registers caller-defined statement types, looked up before + * the upstream table, for syntax the upstream matchers do not distinguish. + */ +export default function createPaddingLineRule( + options: PolicyOptions>, + statementTypes?: Record, +): CreateRule { +const customStatementTypes: Partial> = statementTypes ?? {} +return { + meta: { + type: 'layout', + docs: { + description: 'Require or disallow padding lines between statements', + }, + fixable: 'whitespace', + hasSuggestions: false, + // This is intentionally an array schema as you can pass 0..n config objects + schema: { + $defs: { + paddingType: { + type: 'string', + enum: Object.keys(PaddingTypes), + }, + statementType: { + type: 'string', + enum: Object.keys(StatementTypes), + }, + selectorOption: { + type: 'object', + properties: { + selector: { + type: 'string', + }, + lineMode: { + type: 'string', + enum: ['any', 'singleline', 'multiline'], + }, + }, + required: ['selector'], + additionalProperties: false, + }, + statementMatcher: { + anyOf: [ + { $ref: '#/$defs/statementType' }, + { $ref: '#/$defs/selectorOption' }, + ], + }, + statementOption: { + anyOf: [ + { $ref: '#/$defs/statementMatcher' }, + { + type: 'array', + items: { $ref: '#/$defs/statementMatcher' }, + minItems: 1, + uniqueItems: true, + additionalItems: false, + }, + ], + }, + }, + type: 'array', + additionalItems: false, + items: { + type: 'object', + properties: { + blankLine: { $ref: '#/$defs/paddingType' }, + prev: { $ref: '#/$defs/statementOption' }, + next: { $ref: '#/$defs/statementOption' }, + }, + additionalProperties: false, + required: ['blankLine', 'prev', 'next'], + }, + }, + messages: { + unexpectedBlankLine: 'Unexpected blank line before this statement.', + expectedBlankLine: 'Expected blank line before this statement.', + }, + }, + create(context) { + const sourceCode = context.sourceCode + + const selectorMatchedNodes = new Map>() + const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = [] + + function collectSelectorOption(option: PolicyStatementOption): void { + if (Array.isArray(option)) { + for (const item of option) + collectSelectorOption(item) + return + } + + if (!isSelectorOption(option)) + return + + selectorMatchedNodes.set(option.selector, new Set()) + } + + for (const configure of options) { + collectSelectorOption(configure.prev) + collectSelectorOption(configure.next) + } + + type Scope = { + upper: Scope + prevNode: ASTNode | null + } | null + + let scopeInfo: Scope = null + + /** + * Processes to enter to new scope. + * This manages the current previous statement. + * + * @private + */ + function enterScope(): void { + scopeInfo = { + upper: scopeInfo, + prevNode: null, + } + } + + /** + * Processes to exit from the current scope. + * + * @private + */ + function exitScope(): void { + if (scopeInfo) + scopeInfo = scopeInfo.upper + } + + /** + * Checks whether the given node matches the given type. + * @param node The statement node to check. + * @param type The statement type to check. + * @returns `true` if the statement node matched the type. + * @private + */ + function match(node: ASTNode, type: PolicyStatementOption): boolean { + let innerStatementNode = node + + while (innerStatementNode.type === 'LabeledStatement') + innerStatementNode = innerStatementNode.body + + if (Array.isArray(type)) + return type.some(match.bind(null, innerStatementNode)) + + if (isSelectorOption(type)) { + const matchedNodes = selectorMatchedNodes.get(type.selector) + if (!matchedNodes?.has(innerStatementNode)) + return false + + const lineMode = type.lineMode + + if (lineMode === 'singleline') + return isSingleLine(innerStatementNode) + else if (lineMode === 'multiline') + return !isSingleLine(innerStatementNode) + + return true + } + else { + const statementType = customStatementTypes[type] ?? StatementTypes[type] + if (statementType === undefined) + throw new Error(`Padding rule invariant: unsupported statement type ${type}`) + return statementType.test(innerStatementNode, sourceCode) + } + } + + /** + * Finds the last matched configure from options. + * @param prevNode The previous statement to match. + * @param nextNode The current statement to match. + * @returns The tester of the last matched configure. + * @private + */ + function getPaddingType( + prevNode: ASTNode, + nextNode: ASTNode, + ): (typeof PaddingTypes)[keyof typeof PaddingTypes] { + for (let i = options.length - 1; i >= 0; --i) { + const configure = options[i] + if (configure === undefined) + throw new Error('Padding rule invariant: configuration entry is missing') + if ( + match(prevNode, configure.prev) + && match(nextNode, configure.next) + ) { + return PaddingTypes[configure.blankLine] + } + } + return PaddingTypes.any + } + + /** + * Gets padding line sequences between the given 2 statements. + * Comments are separators of the padding line sequences. + * @param prevNode The previous statement to count. + * @param nextNode The current statement to count. + * @returns The array of token pairs. + * @private + */ + function getPaddingLineSequences( + prevNode: ASTNode, + nextNode: ASTNode, + ): [Token, Token][] { + const pairs: [Token, Token][] = [] + let prevToken: Token = getActualLastToken(prevNode, sourceCode)! + + if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { + do { + const token: Token = sourceCode.getTokenAfter(prevToken, { + includeComments: true, + })! + + if (token.loc.start.line - prevToken.loc.end.line >= 2) + pairs.push([prevToken, token]) + + prevToken = token + } while (prevToken.range[0] < nextNode.range[0]) + } + + return pairs + } + + /** + * Verify padding lines between the given node and the previous node. + * @param node The node to verify. + * + * @private + */ + function verify(node: ASTNode): void { + if ( + !node.parent + || ![ + 'BlockStatement', + 'Program', + 'StaticBlock', + 'SwitchCase', + 'SwitchStatement', + 'TSInterfaceBody', + 'TSModuleBlock', + 'TSTypeLiteral', + ].includes(node.parent.type) + ) { + return + } + + // Save this node as the current previous statement. + const prevNode = scopeInfo!.prevNode + + // Verify. + if (prevNode) + pendingPairs.push({ prevNode, nextNode: node }) + + scopeInfo!.prevNode = node + } + + function verifyPendingPairs(): void { + for (const { prevNode, nextNode } of pendingPairs) { + const type = getPaddingType(prevNode, nextNode) + const paddingLines = getPaddingLineSequences(prevNode, nextNode) + + type.verify(context, prevNode, nextNode, paddingLines) + } + } + + /** + * Verify padding lines between the given node and the previous node. + * Then process to enter to new scope. + * @param node The node to verify. + * + * @private + */ + function verifyThenEnterScope(node: ASTNode): void { + verify(node) + enterScope() + } + + const selectorMatchListeners = Object.fromEntries( + Array.from(selectorMatchedNodes.keys(), selector => [ + selector, + (node: ASTNode): void => { + selectorMatchedNodes.get(selector)?.add(node) + }, + ]), + ) + + return { + 'Program': enterScope, + 'Program:exit': () => { + verifyPendingPairs() + exitScope() + }, + 'BlockStatement': enterScope, + 'BlockStatement:exit': exitScope, + 'SwitchStatement': enterScope, + 'SwitchStatement:exit': exitScope, + 'SwitchCase': verifyThenEnterScope, + 'SwitchCase:exit': exitScope, + 'StaticBlock': enterScope, + 'StaticBlock:exit': exitScope, + + 'TSInterfaceBody': enterScope, + 'TSInterfaceBody:exit': exitScope, + 'TSModuleBlock': enterScope, + 'TSModuleBlock:exit': exitScope, + 'TSTypeLiteral': enterScope, + 'TSTypeLiteral:exit': exitScope, + 'TSDeclareFunction': verifyThenEnterScope, + 'TSDeclareFunction:exit': exitScope, + 'TSMethodSignature': verifyThenEnterScope, + 'TSMethodSignature:exit': exitScope, + + ':statement': verify, + ...selectorMatchListeners, + } + }, +} +} diff --git a/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts new file mode 100644 index 000000000..45068ec52 --- /dev/null +++ b/tools/oxlint/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts @@ -0,0 +1,87 @@ +/* GENERATED, DO NOT EDIT DIRECTLY */ + +/* @checksum: 3QCTtOH6rJM5_AGJ58rGpeEaBEfaJz17MSCxWB4X_PU */ + +export type PaddingType = 'any' | 'never' | 'always' +export type StatementOption = + | StatementMatcher + | [StatementMatcher, ...StatementMatcher[]] +export type StatementMatcher = + | StatementType + | SelectorOption +export type StatementType = + | '*' + | 'exports' + | 'require' + | 'directive' + | 'iife' + | 'block' + | 'empty' + | 'function' + | 'ts-method' + | 'break' + | 'case' + | 'class' + | 'continue' + | 'debugger' + | 'default' + | 'do' + | 'for' + | 'if' + | 'import' + | 'switch' + | 'throw' + | 'try' + | 'while' + | 'with' + | 'cjs-export' + | 'cjs-import' + | 'enum' + | 'interface' + | 'function-overload' + | 'block-like' + | 'singleline-block-like' + | 'multiline-block-like' + | 'expression' + | 'singleline-expression' + | 'multiline-expression' + | 'return' + | 'singleline-return' + | 'multiline-return' + | 'export' + | 'singleline-export' + | 'multiline-export' + | 'var' + | 'singleline-var' + | 'multiline-var' + | 'let' + | 'singleline-let' + | 'multiline-let' + | 'const' + | 'singleline-const' + | 'multiline-const' + | 'using' + | 'singleline-using' + | 'multiline-using' + | 'type' + | 'singleline-type' + | 'multiline-type' +export type PaddingLineBetweenStatementsSchema0 = { + blankLine: PaddingType + prev: StatementOption + next: StatementOption +}[] + +export interface SelectorOption { + selector: string + lineMode?: 'any' | 'singleline' | 'multiline' +} + +export type PaddingLineBetweenStatementsRuleOptions + = PaddingLineBetweenStatementsSchema0 + +export type RuleOptions + = PaddingLineBetweenStatementsRuleOptions +export type MessageIds = + | 'unexpectedBlankLine' + | 'expectedBlankLine'