@workerstack/repo 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ESTree, CreateRule } from "@oxlint/plugins";
|
|
2
|
+
import { declarationOf, movedOf, type Statement } from "./names.ts";
|
|
3
|
+
|
|
4
|
+
const MESSAGE = "a class file holds one class, its imports and its export";
|
|
5
|
+
|
|
6
|
+
/** The class a top-level statement declares, bare or exported, or null when it declares none. */
|
|
7
|
+
function classOf(statement: Statement): ESTree.Class | null {
|
|
8
|
+
const declaration = declarationOf(statement);
|
|
9
|
+
return declaration?.type === "ClassDeclaration" ? declaration : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `export { C }` or `export default C`: the export of a class declared bare. */
|
|
13
|
+
function exportsClass(statement: Statement, name: string | null): boolean {
|
|
14
|
+
if (statement.type === "ExportDefaultDeclaration")
|
|
15
|
+
return statement.declaration.type === "Identifier" && statement.declaration.name === name;
|
|
16
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.source !== null) return false;
|
|
17
|
+
const [only, ...rest] = statement.specifiers;
|
|
18
|
+
return only !== undefined && rest.length === 0 && only.local.type === "Identifier" && only.local.name === name;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fails, in a file with a top-level class, every top-level statement but its imports, that class and its export: a
|
|
23
|
+
* helper, a type or a second class goes to its own file, so a file declares functions or a class, never both.
|
|
24
|
+
*/
|
|
25
|
+
export const classFile: CreateRule = {
|
|
26
|
+
create(context) {
|
|
27
|
+
return {
|
|
28
|
+
Program(program) {
|
|
29
|
+
const owner = program.body.find((statement) => classOf(statement) !== null);
|
|
30
|
+
if (owner === undefined) return;
|
|
31
|
+
const name = classOf(owner)?.id?.name ?? null;
|
|
32
|
+
for (const statement of program.body) {
|
|
33
|
+
if (statement === owner || statement.type === "ImportDeclaration" || exportsClass(statement, name)) continue;
|
|
34
|
+
context.report({ message: `${MESSAGE}; move ${movedOf(statement)} to its own file`, node: statement });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { CreateRule } from "@oxlint/plugins";
|
|
3
|
+
import { movedOf, type Statement } from "./names.ts";
|
|
4
|
+
|
|
5
|
+
/** `vite.config.ts`, `workerstack.config.ts`, `eslint.config.mjs`. */
|
|
6
|
+
const CONFIG = /\.config\.(?:ts|mts|js|mjs)$/;
|
|
7
|
+
const MESSAGE = "a config file holds only imports and exports";
|
|
8
|
+
|
|
9
|
+
/** An import, `export default`, or an export that declares nothing: `export { a }`, `export … from`. */
|
|
10
|
+
const allowed = (statement: Statement): boolean =>
|
|
11
|
+
statement.type === "ImportDeclaration" ||
|
|
12
|
+
statement.type === "ExportDefaultDeclaration" ||
|
|
13
|
+
statement.type === "ExportAllDeclaration" ||
|
|
14
|
+
(statement.type === "ExportNamedDeclaration" && statement.declaration === null);
|
|
15
|
+
|
|
16
|
+
/** Fails any other statement in a config file: what a config builds lives in a module it imports. */
|
|
17
|
+
export const configFile: CreateRule = {
|
|
18
|
+
create(context) {
|
|
19
|
+
if (!CONFIG.test(path.basename(context.filename))) return {};
|
|
20
|
+
return {
|
|
21
|
+
Program(program) {
|
|
22
|
+
for (const statement of program.body) {
|
|
23
|
+
if (allowed(statement)) continue;
|
|
24
|
+
context.report({
|
|
25
|
+
message: `${MESSAGE}; move ${movedOf(statement)} into a module it imports`,
|
|
26
|
+
node: statement,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ESTree, CreateRule } from "@oxlint/plugins";
|
|
3
|
+
import { nearestManifest, readManifest } from "./manifest.ts";
|
|
4
|
+
import type { Statement } from "./names.ts";
|
|
5
|
+
|
|
6
|
+
const SHAPE = "a bin file holds only imports and one main() call";
|
|
7
|
+
const MESSAGE = `${SHAPE}; move this into the module main comes from`;
|
|
8
|
+
const MISSING = `${SHAPE}; import main and call it once`;
|
|
9
|
+
|
|
10
|
+
/** Whether `filename` is a `bin` of its nearest package.json, both resolved from the manifest's dir. */
|
|
11
|
+
function isBin(filename: string): boolean {
|
|
12
|
+
const file = nearestManifest(path.dirname(filename));
|
|
13
|
+
const manifest = file === null ? null : readManifest(file);
|
|
14
|
+
if (manifest === null) return false;
|
|
15
|
+
const self = path.resolve(filename);
|
|
16
|
+
return manifest.bins.some((bin) => path.resolve(path.dirname(manifest.file), bin) === self);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const importsMain = (program: ESTree.Program): boolean =>
|
|
20
|
+
program.body.some(
|
|
21
|
+
(statement) =>
|
|
22
|
+
statement.type === "ImportDeclaration" && statement.specifiers.some((item) => item.local.name === "main"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/** `main(…)`, `await main(…)` or `void main(…)` as a statement. */
|
|
26
|
+
function callsMain(statement: Statement): boolean {
|
|
27
|
+
if (statement.type !== "ExpressionStatement") return false;
|
|
28
|
+
let call = statement.expression;
|
|
29
|
+
if (call.type === "AwaitExpression" || (call.type === "UnaryExpression" && call.operator === "void"))
|
|
30
|
+
call = call.argument;
|
|
31
|
+
return call.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "main";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fails a file its nearest package.json lists as a `bin` unless it holds only imports and one call of an imported
|
|
36
|
+
* `main`, so what the program does lives in a module a test can import. A file whose manifest is unreadable is skipped.
|
|
37
|
+
*/
|
|
38
|
+
export const entryFile: CreateRule = {
|
|
39
|
+
create(context) {
|
|
40
|
+
if (!isBin(context.filename)) return {};
|
|
41
|
+
return {
|
|
42
|
+
Program(program) {
|
|
43
|
+
const imported = importsMain(program);
|
|
44
|
+
let called = false;
|
|
45
|
+
for (const statement of program.body) {
|
|
46
|
+
if (statement.type === "ImportDeclaration") continue;
|
|
47
|
+
if (imported && !called && callsMain(statement)) called = true;
|
|
48
|
+
else context.report({ message: MESSAGE, node: statement });
|
|
49
|
+
}
|
|
50
|
+
if (!called) context.report({ message: MISSING, node: program });
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { CreateRule, ESTree } from "@oxlint/plugins";
|
|
3
|
+
import { ENV_FILE, ENV_PROPERTY, IMPORT_KEYWORD } from "../env.constants.ts";
|
|
4
|
+
|
|
5
|
+
/** Modules whose `env` export is the process environment. */
|
|
6
|
+
const ENV_MODULES = ["process", "node:process", "cloudflare:workers"];
|
|
7
|
+
/** Globals that carry the environment as `.env`. */
|
|
8
|
+
const GLOBALS = ["process", "Bun"];
|
|
9
|
+
const tail = "is read outside env.ts; add the variable to this package's env.ts schema and read it through that module";
|
|
10
|
+
const messageOf = (what: string) => `${what} ${tail}`;
|
|
11
|
+
|
|
12
|
+
/** The property name of `.name` or `["name"]`, when it is a plain name. */
|
|
13
|
+
function propertyName(node: ESTree.MemberExpression): string | undefined {
|
|
14
|
+
if (!node.computed) return node.property.type === "Identifier" ? node.property.name : undefined;
|
|
15
|
+
return node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `process.env` or `Bun.env`, or `import.meta.env`: what the member read is of, or undefined. */
|
|
19
|
+
function envOwner(node: ESTree.MemberExpression): string | undefined {
|
|
20
|
+
if (propertyName(node) !== ENV_PROPERTY) return undefined;
|
|
21
|
+
const { object } = node;
|
|
22
|
+
if (object.type === "Identifier" && GLOBALS.includes(object.name)) return `${object.name}.env`;
|
|
23
|
+
return object.type === "MetaProperty" && object.meta.name === IMPORT_KEYWORD ? "import.meta.env" : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `const { env } = process`: a destructuring that pulls `env` off a global. */
|
|
27
|
+
function destructured(node: ESTree.VariableDeclarator): string | undefined {
|
|
28
|
+
const { id, init } = node;
|
|
29
|
+
if (id.type !== "ObjectPattern" || init?.type !== "Identifier" || !GLOBALS.includes(init.name)) return undefined;
|
|
30
|
+
const pulls = id.properties.some(
|
|
31
|
+
(property) =>
|
|
32
|
+
property.type === "Property" &&
|
|
33
|
+
(property.key.type === "Identifier"
|
|
34
|
+
? property.key.name
|
|
35
|
+
: property.key.type === "Literal" && property.key.value) === ENV_PROPERTY,
|
|
36
|
+
);
|
|
37
|
+
return pulls ? `${init.name}.env` : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The name an import specifier takes from its module, quoted or not. */
|
|
41
|
+
const importedName = (imported: ESTree.ModuleExportName): string =>
|
|
42
|
+
imported.type === "Identifier" ? imported.name : String(imported.value);
|
|
43
|
+
|
|
44
|
+
/** Fails a read of the process environment in any file not named `env.ts`; tests are not exempt. */
|
|
45
|
+
export const envInConfig: CreateRule = {
|
|
46
|
+
create(context) {
|
|
47
|
+
if (path.basename(context.filename) === ENV_FILE) return {};
|
|
48
|
+
return {
|
|
49
|
+
MemberExpression(node) {
|
|
50
|
+
const what = envOwner(node);
|
|
51
|
+
if (what !== undefined) context.report({ message: messageOf(what), node });
|
|
52
|
+
},
|
|
53
|
+
VariableDeclarator(node) {
|
|
54
|
+
const what = destructured(node);
|
|
55
|
+
if (what !== undefined) context.report({ message: messageOf(what), node });
|
|
56
|
+
},
|
|
57
|
+
ImportDeclaration(node) {
|
|
58
|
+
if (!ENV_MODULES.includes(node.source.value)) return;
|
|
59
|
+
for (const specifier of node.specifiers) {
|
|
60
|
+
if (specifier.type === "ImportSpecifier" && importedName(specifier.imported) === ENV_PROPERTY)
|
|
61
|
+
context.report({ message: messageOf(ENV_PROPERTY), node: specifier });
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ESTree, CreateRule } from "@oxlint/plugins";
|
|
3
|
+
import { declaredNames } from "./names.ts";
|
|
4
|
+
|
|
5
|
+
/** Index files collect, declaration files describe, and test files test: none is named after one export. */
|
|
6
|
+
const SKIPPED = /^index\.|\.d\.[cm]?ts$|\.(?:test|spec|e2e)\./;
|
|
7
|
+
|
|
8
|
+
/** An entry or bin file's `main` is named by its role, not its file; the entry rule governs those. */
|
|
9
|
+
const ENTRY_EXPORT = "main";
|
|
10
|
+
|
|
11
|
+
type Node = ESTree.Node;
|
|
12
|
+
|
|
13
|
+
const exportedName = (name: ESTree.ModuleExportName): string =>
|
|
14
|
+
name.type === "Identifier" ? name.name : String(name.value);
|
|
15
|
+
|
|
16
|
+
/** The names a statement exports, or null when it re-exports a module's every name; `export default` adds none. */
|
|
17
|
+
function exportsOf(statement: Node): string[] | null {
|
|
18
|
+
if (statement.type === "ExportAllDeclaration")
|
|
19
|
+
return statement.exported === null ? null : [exportedName(statement.exported)];
|
|
20
|
+
if (statement.type !== "ExportNamedDeclaration") return [];
|
|
21
|
+
if (statement.declaration !== null) return declaredNames(statement.declaration);
|
|
22
|
+
return statement.specifiers.map((specifier) => exportedName(specifier.exported));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const letters = (name: string): string => name.toLowerCase().replaceAll(/[^a-z\d]/g, "");
|
|
26
|
+
const kebab = (name: string): string =>
|
|
27
|
+
name
|
|
28
|
+
.replaceAll(/([a-z\d])([A-Z])/g, "$1-$2")
|
|
29
|
+
.replaceAll(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
30
|
+
.replaceAll(/[^A-Za-z\d]+/g, "-")
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
|
|
33
|
+
/** The one name `program` exports, or null when it exports none, several, or an unknown set. */
|
|
34
|
+
function soleExport(program: ESTree.Program): string | null {
|
|
35
|
+
const names = new Set<string>();
|
|
36
|
+
for (const statement of program.body) {
|
|
37
|
+
const found = exportsOf(statement);
|
|
38
|
+
if (found === null) return null;
|
|
39
|
+
for (const name of found) names.add(name);
|
|
40
|
+
}
|
|
41
|
+
return names.size === 1 ? [...names].join("") : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fails a file with exactly one exported name that is not named after it, compared on letters and digits so
|
|
46
|
+
* `read-workspace.ts`, `readWorkspace.ts` and `read_workspace.ts` all match `readWorkspace`.
|
|
47
|
+
*/
|
|
48
|
+
export const filenameMatchesExport: CreateRule = {
|
|
49
|
+
create(context) {
|
|
50
|
+
const base = path.basename(context.filename);
|
|
51
|
+
if (SKIPPED.test(base)) return {};
|
|
52
|
+
// oxlint lints only files with an extension, so the name has a dot.
|
|
53
|
+
const stem = base.slice(0, base.indexOf("."));
|
|
54
|
+
return {
|
|
55
|
+
Program(program) {
|
|
56
|
+
const name = soleExport(program);
|
|
57
|
+
if (name === null || name === ENTRY_EXPORT || letters(name) === letters(stem)) return;
|
|
58
|
+
const message = `${base} exports only "${name}"; rename the file ${kebab(name)}${base.slice(stem.length)}`;
|
|
59
|
+
context.report({ message, node: program });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { CreateRule } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
const INDEX = /^index\.(?:ts|tsx|mts|js)$/;
|
|
5
|
+
const MESSAGE = "an index file only re-exports; move this into its own module and re-export it";
|
|
6
|
+
|
|
7
|
+
/** Fails every statement of an index file but `export … from` and `export * from`, type-only forms included. */
|
|
8
|
+
export const indexFile: CreateRule = {
|
|
9
|
+
create(context) {
|
|
10
|
+
if (!INDEX.test(path.basename(context.filename))) return {};
|
|
11
|
+
return {
|
|
12
|
+
Program(program) {
|
|
13
|
+
for (const statement of program.body) {
|
|
14
|
+
const reexport =
|
|
15
|
+
statement.type === "ExportAllDeclaration" ||
|
|
16
|
+
(statement.type === "ExportNamedDeclaration" && statement.source !== null);
|
|
17
|
+
if (!reexport) context.report({ message: MESSAGE, node: statement });
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@workerstack/repo/lint`: the oxlint JS plugin `repo`, which the standards pack's generated `.oxlintrc.json` loads
|
|
3
|
+
* with every rule on. oxlint reads a plugin from its default export.
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin } from "@oxlint/plugins";
|
|
6
|
+
import { asNeedsReason } from "./as-needs-reason.ts";
|
|
7
|
+
import { boundaries } from "./boundaries.ts";
|
|
8
|
+
import { classFile } from "./class-file.ts";
|
|
9
|
+
import { configFile } from "./config-file.ts";
|
|
10
|
+
import { entryFile } from "./entry-file.ts";
|
|
11
|
+
import { envInConfig } from "./env-in-config.ts";
|
|
12
|
+
import { filenameMatchesExport } from "./filename-matches-export.ts";
|
|
13
|
+
import { indexFile } from "./index-file.ts";
|
|
14
|
+
import { noCommentedOutCode } from "./no-commented-out-code.ts";
|
|
15
|
+
import { noMockOfSubject } from "./no-mock-of-subject.ts";
|
|
16
|
+
import { noModuleSideEffects } from "./no-module-side-effects.ts";
|
|
17
|
+
import { noModuleState } from "./no-module-state.ts";
|
|
18
|
+
import { noSideEffectImport } from "./no-side-effect-import.ts";
|
|
19
|
+
import { noV8Ignore } from "./no-v8-ignore.ts";
|
|
20
|
+
import { skipOnPlatform } from "./skip-on-platform.ts";
|
|
21
|
+
import { todoNeedsId } from "./todo-needs-id.ts";
|
|
22
|
+
import { typesFile } from "./types-file.ts";
|
|
23
|
+
|
|
24
|
+
const plugin: Plugin & { meta: { name: string } } = {
|
|
25
|
+
meta: { name: "repo" },
|
|
26
|
+
rules: {
|
|
27
|
+
"as-needs-reason": asNeedsReason,
|
|
28
|
+
boundaries,
|
|
29
|
+
"class-file": classFile,
|
|
30
|
+
"config-file": configFile,
|
|
31
|
+
"entry-file": entryFile,
|
|
32
|
+
"env-in-config": envInConfig,
|
|
33
|
+
"filename-matches-export": filenameMatchesExport,
|
|
34
|
+
"index-file": indexFile,
|
|
35
|
+
"no-commented-out-code": noCommentedOutCode,
|
|
36
|
+
"no-mock-of-subject": noMockOfSubject,
|
|
37
|
+
"no-module-side-effects": noModuleSideEffects,
|
|
38
|
+
"no-module-state": noModuleState,
|
|
39
|
+
"no-side-effect-import": noSideEffectImport,
|
|
40
|
+
"no-v8-ignore": noV8Ignore,
|
|
41
|
+
"skip-on-platform": skipOnPlatform,
|
|
42
|
+
"todo-needs-id": todoNeedsId,
|
|
43
|
+
"types-file": typesFile,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default plugin;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A reason is at least this many words of letters; fewer is a marker like `ok` or `safe`. */
|
|
2
|
+
const MIN_WORDS = 3;
|
|
3
|
+
const WORD = /\p{L}+(?:['’-]\p{L}+)*/gu;
|
|
4
|
+
|
|
5
|
+
/** Whether `text` holds a written reason: at least three words of letters. */
|
|
6
|
+
export const isReason = (text: string): boolean => (text.match(WORD) ?? []).length >= MIN_WORDS;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a package.json declares, parsed from untrusted JSON: a field of the wrong shape counts as absent. `bins` are
|
|
6
|
+
* the `bin` paths as written, relative to the manifest's dir.
|
|
7
|
+
*/
|
|
8
|
+
export type Manifest = { file: string; name: string | null; declared: string[]; bins: string[] };
|
|
9
|
+
|
|
10
|
+
const DEPENDENCY_MAPS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
11
|
+
|
|
12
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
13
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
|
|
15
|
+
/** The nearest package.json at or above `start`, or null when none is. */
|
|
16
|
+
export function nearestManifest(start: string): string | null {
|
|
17
|
+
let dir = start;
|
|
18
|
+
while (!fs.existsSync(path.join(dir, "package.json"))) {
|
|
19
|
+
if (path.dirname(dir) === dir) return null;
|
|
20
|
+
dir = path.dirname(dir);
|
|
21
|
+
}
|
|
22
|
+
return path.join(dir, "package.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A manifest's bin scripts: `bin` as one path or a map of them; anything else names none. */
|
|
26
|
+
export function binsOf(bin: unknown): string[] {
|
|
27
|
+
const all = isRecord(bin) ? Object.values(bin) : [bin];
|
|
28
|
+
return all.filter((file) => typeof file === "string");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The manifest at `file`, or null when it cannot be read as a JSON object. */
|
|
32
|
+
export function readManifest(file: string): Manifest | null {
|
|
33
|
+
let raw: unknown;
|
|
34
|
+
try {
|
|
35
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(raw)) return null;
|
|
40
|
+
const name = typeof raw.name === "string" ? raw.name : null;
|
|
41
|
+
const declared = DEPENDENCY_MAPS.flatMap((key) => {
|
|
42
|
+
const map = raw[key];
|
|
43
|
+
return isRecord(map) ? Object.keys(map) : [];
|
|
44
|
+
});
|
|
45
|
+
return { file, name, declared, bins: binsOf(raw.bin) };
|
|
46
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
/** A top-level statement of a module. */
|
|
4
|
+
export type Statement = ESTree.Program["body"][number];
|
|
5
|
+
type Binding = ESTree.BindingPattern | ESTree.BindingRestElement;
|
|
6
|
+
type Declaration = NonNullable<ESTree.ExportNamedDeclaration["declaration"]>;
|
|
7
|
+
|
|
8
|
+
/** The names a binding pattern declares: `{ a, b: [c] }` → a, c. */
|
|
9
|
+
function boundNames(pattern: Binding | null): string[] {
|
|
10
|
+
if (pattern === null) return [];
|
|
11
|
+
switch (pattern.type) {
|
|
12
|
+
case "Identifier":
|
|
13
|
+
return [pattern.name];
|
|
14
|
+
case "ObjectPattern":
|
|
15
|
+
return pattern.properties.flatMap((property) =>
|
|
16
|
+
boundNames(property.type === "RestElement" ? property : property.value),
|
|
17
|
+
);
|
|
18
|
+
case "ArrayPattern":
|
|
19
|
+
return pattern.elements.flatMap(boundNames);
|
|
20
|
+
case "AssignmentPattern":
|
|
21
|
+
return boundNames(pattern.left);
|
|
22
|
+
case "RestElement":
|
|
23
|
+
return boundNames(pattern.argument);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The names a declaration binds, as after `export`. */
|
|
28
|
+
export function declaredNames(declaration: Declaration): string[] {
|
|
29
|
+
if (declaration.type === "VariableDeclaration")
|
|
30
|
+
return declaration.declarations.flatMap((item) => boundNames(item.id));
|
|
31
|
+
return declaration.id !== null && declaration.id.type === "Identifier" ? [declaration.id.name] : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DECLARATIONS = [
|
|
35
|
+
"ClassDeclaration",
|
|
36
|
+
"FunctionDeclaration",
|
|
37
|
+
"TSDeclareFunction",
|
|
38
|
+
"TSEnumDeclaration",
|
|
39
|
+
"TSInterfaceDeclaration",
|
|
40
|
+
"TSModuleDeclaration",
|
|
41
|
+
"TSTypeAliasDeclaration",
|
|
42
|
+
"VariableDeclaration",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const isDeclaration = (node: { type: string }): node is Declaration => DECLARATIONS.includes(node.type);
|
|
46
|
+
|
|
47
|
+
/** The declaration a statement makes, bare, after `export` or after `export default`, or null when it makes none. */
|
|
48
|
+
export function declarationOf(statement: Statement): Declaration | null {
|
|
49
|
+
if (statement.type === "ExportNamedDeclaration") return statement.declaration;
|
|
50
|
+
const inner = statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
51
|
+
return isDeclaration(inner) ? inner : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What a message asks to move: the names `statement` declares, quoted, or "this" when it declares none. */
|
|
55
|
+
export function movedOf(statement: Statement): string {
|
|
56
|
+
const declaration = declarationOf(statement);
|
|
57
|
+
const names = declaration === null ? [] : declaredNames(declaration);
|
|
58
|
+
return names.length === 0 ? "this" : names.map((name) => `"${name}"`).join(", ");
|
|
59
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Comment, CreateRule } from "@oxlint/plugins";
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
|
+
|
|
4
|
+
const MESSAGE = "this comment is code; delete it, git keeps the old version";
|
|
5
|
+
/** Comments a tool reads: type and lint directives, pure marks, and coverage hints. */
|
|
6
|
+
const TOOL = /^\s*(?:@ts-|oxlint-|eslint-|[@#]__PURE__|(?:v8|c8|istanbul)\s+ignore\b)/;
|
|
7
|
+
/** Two line comments on adjacent lines, with nothing but spaces between them. */
|
|
8
|
+
const ADJACENT = /^[ \t]*\r?\n[ \t]*$/;
|
|
9
|
+
const BLOCK_STAR = /^[ \t]*\*[ \t]?/gm;
|
|
10
|
+
/** What only code holds: a parsed expression or a label alone can be prose like `workspace` or `note: see a.b`. */
|
|
11
|
+
const CODE = [
|
|
12
|
+
"AssignmentExpression",
|
|
13
|
+
"CallExpression",
|
|
14
|
+
"ClassDeclaration",
|
|
15
|
+
"DoWhileStatement",
|
|
16
|
+
"ExportAllDeclaration",
|
|
17
|
+
"ExportDefaultDeclaration",
|
|
18
|
+
"ExportNamedDeclaration",
|
|
19
|
+
"ForInStatement",
|
|
20
|
+
"ForOfStatement",
|
|
21
|
+
"ForStatement",
|
|
22
|
+
"FunctionDeclaration",
|
|
23
|
+
"IfStatement",
|
|
24
|
+
"ImportDeclaration",
|
|
25
|
+
"NewExpression",
|
|
26
|
+
"ReturnStatement",
|
|
27
|
+
"SwitchStatement",
|
|
28
|
+
"TSEnumDeclaration",
|
|
29
|
+
"TSInterfaceDeclaration",
|
|
30
|
+
"TSTypeAliasDeclaration",
|
|
31
|
+
"ThrowStatement",
|
|
32
|
+
"TryStatement",
|
|
33
|
+
"VariableDeclaration",
|
|
34
|
+
"WhileStatement",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** JSDoc documents, a triple-slash comment directs the compiler, and a tool comment is read by its tool. */
|
|
38
|
+
function skipped(comment: Comment): boolean {
|
|
39
|
+
if (comment.type === "Block" && comment.value.startsWith("*")) return true;
|
|
40
|
+
if (comment.type === "Line" && comment.value.startsWith("/")) return true;
|
|
41
|
+
return TOOL.test(comment.value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const joins = (text: string, previous: Comment, comment: Comment): boolean =>
|
|
45
|
+
previous.type === "Line" && comment.type === "Line" && ADJACENT.test(text.slice(previous.end, comment.start));
|
|
46
|
+
|
|
47
|
+
type Group = [Comment, ...Comment[]];
|
|
48
|
+
|
|
49
|
+
/** The comments to read as one text each: a run of line comments on adjacent lines, or one block comment. */
|
|
50
|
+
function groupsOf(text: string, comments: readonly Comment[]): Group[] {
|
|
51
|
+
const groups: Group[] = [];
|
|
52
|
+
let open: { group: Group; last: Comment } | null = null;
|
|
53
|
+
for (const comment of comments) {
|
|
54
|
+
if (skipped(comment)) {
|
|
55
|
+
open = null;
|
|
56
|
+
} else if (open !== null && joins(text, open.last, comment)) {
|
|
57
|
+
open.group.push(comment);
|
|
58
|
+
open.last = comment;
|
|
59
|
+
} else {
|
|
60
|
+
open = { group: [comment], last: comment };
|
|
61
|
+
groups.push(open.group);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function holdsCode(node: unknown): boolean {
|
|
68
|
+
if (typeof node !== "object" || node === null) return false;
|
|
69
|
+
if (Array.isArray(node)) return node.some(holdsCode);
|
|
70
|
+
if ("type" in node && typeof node.type === "string" && CODE.includes(node.type)) return true;
|
|
71
|
+
return Object.values(node).some(holdsCode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether `source` parses as TypeScript with no error, at module scope or in a function body, and holds code. */
|
|
75
|
+
function isCode(source: string): boolean {
|
|
76
|
+
for (const text of [source, `async function* comment() {\n${source}\n}`]) {
|
|
77
|
+
const parsed = parseSync("comment.ts", text, { sourceType: "module" });
|
|
78
|
+
if (parsed.errors.length === 0) return holdsCode(parsed.program.body);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const textOf = (group: readonly Comment[]): string =>
|
|
84
|
+
group
|
|
85
|
+
.map((comment) => (comment.type === "Block" ? comment.value.replaceAll(BLOCK_STAR, "") : comment.value))
|
|
86
|
+
.join("\n");
|
|
87
|
+
|
|
88
|
+
/** Code as a whole, like a multi-line `if`, or on any one line, like a commented call under a prose line. */
|
|
89
|
+
const isCodeGroup = (text: string): boolean => isCode(text) || text.split("\n").some(isCode);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fails a comment that parses as code: git keeps the old version, so code left in a comment only rots. Consecutive
|
|
93
|
+
* line comments are read as one; JSDoc, triple-slash and tool comments are skipped.
|
|
94
|
+
*/
|
|
95
|
+
export const noCommentedOutCode: CreateRule = {
|
|
96
|
+
create(context) {
|
|
97
|
+
return {
|
|
98
|
+
Program() {
|
|
99
|
+
for (const group of groupsOf(context.sourceCode.text, context.sourceCode.getAllComments())) {
|
|
100
|
+
if (isCodeGroup(textOf(group))) context.report({ message: MESSAGE, node: group[0] });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ESTree, CreateRule } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
const TEST_FILE = /\.(?:test|spec)(\.[cm]?[jt]sx?)$/;
|
|
5
|
+
const EXTENSION = /\.[cm]?[jt]sx?$/;
|
|
6
|
+
/** `vi.mock`, `vi.doMock` and `jest.mock` replace a module for the whole test file. */
|
|
7
|
+
const MOCKS: Readonly<Record<string, readonly string[]>> = { vi: ["mock", "doMock"], jest: ["mock"] };
|
|
8
|
+
|
|
9
|
+
type Node = ESTree.Node;
|
|
10
|
+
|
|
11
|
+
const stringOf = (node: Node | undefined): string | null =>
|
|
12
|
+
node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
|
|
13
|
+
|
|
14
|
+
const isImportMetaUrl = (node: Node | undefined): boolean =>
|
|
15
|
+
node?.type === "MemberExpression" &&
|
|
16
|
+
node.object.type === "MetaProperty" &&
|
|
17
|
+
node.object.meta.name === "import" &&
|
|
18
|
+
node.property.type === "Identifier" &&
|
|
19
|
+
node.property.name === "url";
|
|
20
|
+
|
|
21
|
+
/** The relative path of `new URL("<rel>", import.meta.url).href`, or null for any other node. */
|
|
22
|
+
function urlPath(node: Node): string | null {
|
|
23
|
+
if (node.type !== "MemberExpression" || node.property.type !== "Identifier" || node.property.name !== "href")
|
|
24
|
+
return null;
|
|
25
|
+
const { object } = node;
|
|
26
|
+
if (object.type !== "NewExpression" || object.callee.type !== "Identifier" || object.callee.name !== "URL")
|
|
27
|
+
return null;
|
|
28
|
+
return isImportMetaUrl(object.arguments[1]) ? stringOf(object.arguments[0]) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isMock = (callee: Node): boolean =>
|
|
32
|
+
callee.type === "MemberExpression" &&
|
|
33
|
+
callee.object.type === "Identifier" &&
|
|
34
|
+
callee.property.type === "Identifier" &&
|
|
35
|
+
(MOCKS[callee.object.name] ?? []).includes(callee.property.name);
|
|
36
|
+
|
|
37
|
+
/** The relative specifier a mock call names, as a literal or the `import.meta.url` form, or null. */
|
|
38
|
+
function mockedPath(node: ESTree.CallExpression): string | null {
|
|
39
|
+
const [first] = node.arguments;
|
|
40
|
+
if (!isMock(node.callee) || first === undefined) return null;
|
|
41
|
+
const specifier = stringOf(first) ?? urlPath(first);
|
|
42
|
+
return specifier?.startsWith(".") ? specifier : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fails a test that mocks the module it tests: `parse.test.ts` mocking `./parse.ts` checks the mock, not the code.
|
|
47
|
+
* The subject is the test's path minus `.test` or `.spec`, compared without extensions.
|
|
48
|
+
*/
|
|
49
|
+
export const noMockOfSubject: CreateRule = {
|
|
50
|
+
create(context) {
|
|
51
|
+
const { filename } = context;
|
|
52
|
+
if (!TEST_FILE.test(filename)) return {};
|
|
53
|
+
const subject = filename.replace(TEST_FILE, "");
|
|
54
|
+
return {
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
const specifier = mockedPath(node);
|
|
57
|
+
if (specifier === null) return;
|
|
58
|
+
if (path.resolve(path.dirname(filename), specifier).replace(EXTENSION, "") !== subject) return;
|
|
59
|
+
const message = `"${specifier}" is the module this test covers; test it for real and mock what it calls`;
|
|
60
|
+
context.report({ message, node });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|