@rdjperron/lint 0.2.0 → 0.3.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/README.md +54 -53
- package/dist/config.d.mts +13 -4
- package/dist/config.mjs +70 -70
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +50 -16
- package/dist/index.mjs.map +1 -1
- package/dist/verify.d.mts +2 -2
- package/dist/verify.mjs +4 -4
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +4 -4
- package/fixtures/no-array-filter-map.fixture.ts +1 -1
- package/fixtures/no-chained-type-assertions.fixture.ts +1 -1
- package/fixtures/no-conditional-empty-object-spread.fixture.ts +1 -1
- package/fixtures/no-hardcoded-jsx-text.fixture.tsx +1 -1
- package/fixtures/no-known-value-widening.fixture.ts +1 -1
- package/fixtures/no-module-mocking.fixture.ts +1 -1
- package/fixtures/no-object-parameters.fixture.ts +1 -1
- package/fixtures/no-reduce-accumulator-copy.fixture.ts +1 -1
- package/fixtures/no-reflect-apply.fixture.ts +1 -1
- package/fixtures/no-reflect-get.fixture.ts +1 -1
- package/fixtures/no-runtime-typeof.fixture.ts +1 -1
- package/fixtures/no-shape-in-symbol-names.fixture.ts +1 -1
- package/fixtures/no-unknown-parameters.fixture.ts +1 -1
- package/fixtures/no-unknown-returns.fixture.ts +1 -1
- package/fixtures/no-unknown-type-aliases.fixture.ts +1 -1
- package/fixtures/no-unsafe-dictionary-type.fixture.ts +1 -1
- package/fixtures/no-use-effect.fixture.tsx +1 -1
- package/fixtures/no-widen-then-assert.fixture.ts +1 -1
- package/fixtures/require-safety-comment-for-type-assertion.fixture.ts +1 -1
- package/fixtures/static-public-env.fixture.ts +1 -1
- package/package.json +3 -4
- package/docs/rules/README.md +0 -43
- package/docs/rules/no-array-filter-map.md +0 -60
- package/docs/rules/no-chained-type-assertions.md +0 -52
- package/docs/rules/no-conditional-empty-object-spread.md +0 -50
- package/docs/rules/no-hardcoded-jsx-text.md +0 -52
- package/docs/rules/no-known-value-widening.md +0 -62
- package/docs/rules/no-module-mocking.md +0 -53
- package/docs/rules/no-object-parameters.md +0 -55
- package/docs/rules/no-reduce-accumulator-copy.md +0 -57
- package/docs/rules/no-reflect-apply.md +0 -56
- package/docs/rules/no-reflect-get.md +0 -51
- package/docs/rules/no-runtime-typeof.md +0 -61
- package/docs/rules/no-shape-in-symbol-names.md +0 -53
- package/docs/rules/no-unknown-parameters.md +0 -55
- package/docs/rules/no-unknown-returns.md +0 -61
- package/docs/rules/no-unknown-type-aliases.md +0 -52
- package/docs/rules/no-unsafe-dictionary-type.md +0 -56
- package/docs/rules/no-use-effect.md +0 -55
- package/docs/rules/no-widen-then-assert.md +0 -53
- package/docs/rules/require-safety-comment-for-type-assertion.md +0 -59
- package/docs/rules/static-public-env.md +0 -55
package/dist/verify.d.mts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Run the package's own suite from a consumer's repo root:
|
|
16
16
|
*
|
|
17
|
-
* npx
|
|
17
|
+
* npx rdjperron-lint-verify
|
|
18
18
|
*
|
|
19
19
|
* ---------------------------------------------------------------------------
|
|
20
20
|
* Why the symlink dance
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* `fixtures()` override from `@rdjperron/lint/config`.
|
|
42
42
|
*/
|
|
43
43
|
export interface Suite {
|
|
44
|
-
/** The plugin alias the diagnostics carry, e.g. `
|
|
44
|
+
/** The plugin alias the diagnostics carry, e.g. `rdjperron` in `rdjperron(no-reflect-get)`. */
|
|
45
45
|
plugin: string;
|
|
46
46
|
/** The fixture directory, relative to `root`. */
|
|
47
47
|
fixtureDir: string;
|
package/dist/verify.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { fileURLToPath } from "node:url";
|
|
|
20
20
|
*
|
|
21
21
|
* Run the package's own suite from a consumer's repo root:
|
|
22
22
|
*
|
|
23
|
-
* npx
|
|
23
|
+
* npx rdjperron-lint-verify
|
|
24
24
|
*
|
|
25
25
|
* ---------------------------------------------------------------------------
|
|
26
26
|
* Why the symlink dance
|
|
@@ -170,7 +170,7 @@ function removeFixtureView(root, suite) {
|
|
|
170
170
|
rmdirSync(dir);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
/** `
|
|
173
|
+
/** `rdjperron(no-reflect-get)` -> `no-reflect-get` for this suite's plugin; anything else -> null. */
|
|
174
174
|
function ruleIdFor(plugin, code) {
|
|
175
175
|
const match = new RegExp(`^${plugin}\\(([^)]+)\\)$`, "u").exec(code);
|
|
176
176
|
return match === null ? null : match[1] ?? null;
|
|
@@ -361,9 +361,9 @@ function verifyPluginApiPin(root = process.cwd()) {
|
|
|
361
361
|
function packageSuite(root = process.cwd()) {
|
|
362
362
|
const packageDir = "node_modules/@rdjperron/lint";
|
|
363
363
|
return {
|
|
364
|
-
plugin: "
|
|
364
|
+
plugin: "rdjperron",
|
|
365
365
|
fixtureDir: `${packageDir}/fixtures`,
|
|
366
|
-
viewDir: "scripts/.lint-fixtures/
|
|
366
|
+
viewDir: "scripts/.lint-fixtures/rdjperron",
|
|
367
367
|
alwaysView: true,
|
|
368
368
|
readme: `${packageDir}/fixtures/README.md`,
|
|
369
369
|
registeredRules: async () => {
|
package/dist/verify.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.mjs","names":[],"sources":["../src/verify.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Proof that every lint rule still fires.\n *\n * The linter lives inside the `vp` binary, so it moves independently of any\n * lockfile, and the JS-plugin bridge it loads these rules through is alpha. A\n * rule can stop firing with no diff anywhere, and a dead lint rule is silent:\n * \"reports nothing\" looks exactly like \"no violations\". This is the thing that\n * notices.\n *\n * A suite is a directory holding one deliberately-broken `<rule>.fixture.ts(x)`\n * per rule. The check asserts a positive: every rule fired, each on the file\n * named after it.\n *\n * Run the package's own suite from a consumer's repo root:\n *\n * npx rdj-lint-verify\n *\n * ---------------------------------------------------------------------------\n * Why the symlink dance\n * ---------------------------------------------------------------------------\n * The fixtures ship inside `node_modules`, which oxlint never walks, and a\n * consumer's own fixture directories are usually in `lint.ignorePatterns` so a\n * plain `vp check` stays quiet. Measured on Vite+ 0.2.9 / oxlint 1.77.0 and\n * again on 0.3.1 / oxlint 1.81.0: no flag re-includes an ignored path.\n * `--no-ignore`, `--ignore-pattern '!<dir>/**'`, an explicit file, an absolute\n * path, a cwd inside the directory and `-W <rule>` all answer \"No files found\n * to lint.\" `--no-ignore` only disables `.eslintignore`, `--ignore-path` and\n * `--ignore-pattern`; it does not touch the config's own `ignorePatterns`, and\n * `vp lint` has no `--config` flag.\n *\n * What does work: oxlint matches ignore patterns against the path it walked,\n * not the realpath. Linting through a symlink outside the ignored prefix\n * reaches the fixtures normally, plugin and all. This creates that view per\n * suite, lints through it, and removes it in a `finally`.\n *\n * The severities cannot come from the command line either: `-A`, `-W` and `-D`\n * reach built-in rules only and leave every JS plugin rule exactly as the config\n * left it (measured on vite-plus 0.3.1 / oxlint 1.81.0, with `vp lint` and with\n * the bare binary). That is why the consumer's config must carry the\n * `fixtures()` override from `@rdjperron/lint/config`.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n rmdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n symlinkSync,\n} from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface Suite {\n /** The plugin alias the diagnostics carry, e.g. `rdj` in `rdj(no-reflect-get)`. */\n plugin: string;\n /** The fixture directory, relative to `root`. */\n fixtureDir: string;\n /** Where the un-ignored view is created, relative to `root`. */\n viewDir: string;\n /** Fixture file endings; the part before one of them is the rule name. */\n suffixes?: string[];\n /** Skip the direct path and always lint through the view (fixtures under `node_modules`). */\n alwaysView?: boolean;\n /** Advisory: the rule names the plugin registers, so a new rule without a fixture is named. */\n registeredRules?: () => Promise<string[] | null>;\n /** Where the failure message sends the reader. */\n readme?: string;\n}\n\nexport interface VerifyOptions {\n /** The repo root the paths are relative to and `vp lint` runs from. Defaults to the cwd. */\n root?: string;\n}\n\nconst DEFAULT_SUFFIXES = [\".fixture.ts\", \".fixture.tsx\"];\n\n/** Deterministic, locale-independent ordering for the rule and file name lists. */\nfunction byName(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction basename(path: string): string {\n const parts = path.split(/[\\\\/]/u);\n return parts[parts.length - 1] ?? path;\n}\n\n/**\n * Locate the `vp` binary: PATH first, then the default install locations, the\n * XDG layout used since Vite+ 0.3.0 and the pre-0.3.0 single-root layout that\n * upgraded installs keep.\n */\nexport function resolveViteplusBinary(): string {\n const probe = spawnSync(\"vp\", [\"--version\"], { encoding: \"utf8\" });\n if (probe.status === 0) return \"vp\";\n const home = process.env.HOME ?? \"\";\n const fallbacks = [\n join(home, \".local\", \"share\", \"vite-plus\", \"bin\", \"vp\"),\n join(home, \".vite-plus\", \"bin\", \"vp\"),\n ];\n const found = fallbacks.find((candidate) => existsSync(candidate));\n if (found !== undefined) return found;\n throw new Error(\n \"Could not find the `vp` binary. Add it to PATH: \" +\n 'export PATH=\"$HOME/.local/share/vite-plus/bin:$PATH\" (or \"$HOME/.vite-plus/bin\" for a pre-0.3.0 install)',\n );\n}\n\ninterface LintDiagnostic {\n code: string;\n filename: string;\n}\n\ninterface LintReport {\n numberOfFiles: number;\n diagnostics: LintDiagnostic[];\n}\n\n/** The two fields of oxlint's JSON report this reads, before any of them is trusted. */\ninterface RawReport {\n number_of_files?: unknown;\n diagnostics?: unknown;\n}\n\n/** The two fields of one raw diagnostic, before either of them is trusted. */\ninterface RawDiagnostic {\n code?: unknown;\n filename?: unknown;\n}\n\n/**\n * `vp lint -f json` is an outside value, so this is the one place its shape is\n * checked. Anything that does not look like a diagnostic is dropped rather than\n * guessed at.\n */\nfunction decodeReport(json: string): LintReport {\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new Error(\"`vp lint -f json` did not return an object.\");\n }\n // SAFETY: the check above makes it a non-null object, and every field of\n // `RawReport` is `unknown`, so nothing is claimed about its contents yet.\n const report = parsed as RawReport;\n const numberOfFiles = typeof report.number_of_files === \"number\" ? report.number_of_files : 0;\n const rows = Array.isArray(report.diagnostics) ? report.diagnostics : [];\n const diagnostics: LintDiagnostic[] = [];\n for (const row of rows) {\n if (typeof row !== \"object\" || row === null) continue;\n // SAFETY: same check, one level down; both fields of `RawDiagnostic` are\n // `unknown` and each is typed by its own test on the next lines.\n const entry = row as RawDiagnostic;\n diagnostics.push({\n code: typeof entry.code === \"string\" ? entry.code : \"\",\n filename: typeof entry.filename === \"string\" ? entry.filename : \"\",\n });\n }\n return { numberOfFiles, diagnostics };\n}\n\n/** Run `vp lint <target> -f json` from `root` and return the decoded report. */\nfunction lintAsJson(binary: string, root: string, target: string): LintReport {\n const result = spawnSync(\n binary,\n [\"lint\", target, \"-f\", \"json\", \"--no-error-on-unmatched-pattern\"],\n {\n cwd: root,\n encoding: \"utf8\",\n maxBuffer: 64 * 1024 * 1024,\n },\n );\n\n if (result.error !== undefined && result.error !== null) throw result.error;\n\n // `--no-error-on-unmatched-pattern` keeps the \"No files found to lint.\" banner\n // off stdout, so stdout is pure JSON even when nothing matched.\n const stdout = result.stdout ?? \"\";\n const start = stdout.indexOf(\"{\");\n if (start === -1) {\n throw new Error(\n `\\`vp lint ${target} -f json\\` produced no JSON.\\nstdout: ${stdout}\\nstderr: ${result.stderr ?? \"\"}`,\n );\n }\n\n try {\n return decodeReport(stdout.slice(start));\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n throw new Error(`Could not read \\`vp lint\\` JSON output: ${reason}\\nstdout: ${stdout}`, {\n cause,\n });\n }\n}\n\n/**\n * Why a path is gitignored, or null. oxlint honours `.gitignore`, so a\n * gitignored view yields \"0 files\" instead of a useful diagnostic.\n */\nfunction gitIgnoreReason(root: string, relPath: string): string | null {\n const result = spawnSync(\"git\", [\"check-ignore\", \"-v\", relPath], { cwd: root, encoding: \"utf8\" });\n const output = result.status === 0 ? result.stdout.trim() : \"\";\n return output === \"\" ? null : (output.split(\"\\t\")[0] ?? null);\n}\n\n/** Rule name to fixture file name, taken from the files that exist on disk. */\nfunction fixtureFiles(root: string, suite: Suite): Map<string, string> {\n const dir = join(root, suite.fixtureDir);\n if (!existsSync(dir)) throw new Error(`Fixture directory is missing: ${dir}`);\n const suffixes = suite.suffixes ?? DEFAULT_SUFFIXES;\n const files = new Map<string, string>();\n for (const name of readdirSync(dir).toSorted(byName)) {\n const suffix = suffixes.find((candidate) => name.endsWith(candidate));\n if (suffix === undefined) continue;\n files.set(name.slice(0, -suffix.length), name);\n }\n return files;\n}\n\n/** Create the un-ignored view; prefer a symlink, fall back to a copy on Windows. */\nfunction createFixtureView(root: string, suite: Suite): string {\n removeFixtureView(root, suite);\n const viewPath = join(root, suite.viewDir);\n mkdirSync(dirname(viewPath), { recursive: true });\n try {\n symlinkSync(join(root, suite.fixtureDir), viewPath, \"junction\");\n return \"symlink\";\n } catch {\n cpSync(join(root, suite.fixtureDir), viewPath, { recursive: true });\n return \"copy\";\n }\n}\n\nfunction removeFixtureView(root: string, suite: Suite): void {\n const viewPath = join(root, suite.viewDir);\n rmSync(viewPath, { recursive: true, force: true });\n // The parents were created for the view alone when nothing else lives there, so they go too.\n const top = resolve(root);\n for (let dir = dirname(viewPath); dir !== top; dir = dirname(dir)) {\n if (!existsSync(dir) || readdirSync(dir).length > 0) break;\n rmdirSync(dir);\n }\n}\n\n/** `rdj(no-reflect-get)` -> `no-reflect-get` for this suite's plugin; anything else -> null. */\nfunction ruleIdFor(plugin: string, code: string): string | null {\n const match = new RegExp(`^${plugin}\\\\(([^)]+)\\\\)$`, \"u\").exec(code);\n return match === null ? null : (match[1] ?? null);\n}\n\ninterface Row {\n rule: string;\n fixture: string;\n firedOnOwnFixture: boolean;\n note: string;\n}\n\n/** Lint one suite and print its table. Returns the number of rules that did not fire. */\nasync function verifySuite(binary: string, root: string, suite: Suite): Promise<number> {\n const files = fixtureFiles(root, suite);\n const expectedRules = [...files.keys()];\n\n console.log(`${suite.plugin} rule verification`);\n console.log(` fixtures: ${suite.fixtureDir}`);\n\n if (expectedRules.length === 0) {\n console.error(` No fixture files found in ${suite.fixtureDir}. Nothing to verify.\\n`);\n return 1;\n }\n\n // 1. Try the fixture directory directly. Works only while it is un-ignored, so\n // a suite under node_modules skips it: oxlint never walks in there.\n let mechanism = \"direct path\";\n let report: LintReport =\n suite.alwaysView === true\n ? { numberOfFiles: 0, diagnostics: [] }\n : lintAsJson(binary, root, suite.fixtureDir);\n\n // 2. Ignored by the config or under node_modules: lint through an un-ignored view.\n if (report.numberOfFiles === 0) {\n const kind = createFixtureView(root, suite);\n try {\n mechanism = `${kind} at ${suite.viewDir} (\\`${suite.fixtureDir}\\` is not walked by \\`vp lint\\`)`;\n report = lintAsJson(binary, root, suite.viewDir);\n } finally {\n // Never leave the view behind: a stale one would show up in `vp check`.\n removeFixtureView(root, suite);\n }\n }\n\n console.log(` lint via: ${mechanism}`);\n console.log(` files: ${report.numberOfFiles}`);\n console.log(\"\");\n\n if (report.numberOfFiles === 0) {\n const ignoredBy = gitIgnoreReason(root, suite.viewDir);\n console.error(\n \"FAIL: `vp lint` matched 0 files through both the direct path and the un-ignored view.\\n\" +\n (ignoredBy === null\n ? \"Check that the config ignores the fixture directory itself and not a pattern such as\\n\" +\n `\"**/*.fixture.ts\", which would also hide the view at \"${suite.viewDir}\".\\n` +\n \"Check too that the config carries the `fixtures()` override from @rdjperron/lint/config,\\n\" +\n \"on the view path: no command-line flag can switch a JS plugin rule on.\\n\"\n : `CAUSE: \"${suite.viewDir}\" is gitignored (${ignoredBy}). oxlint skips gitignored paths,\\n` +\n \"so the view this lints through is invisible to it. Remove that ignore rule; the view is\\n\" +\n \"deleted in a `finally` block anyway.\\n\"),\n );\n return expectedRules.length;\n }\n\n const firedByRule = new Map<string, Set<string>>();\n const foreignDiagnostics: LintDiagnostic[] = [];\n\n for (const diagnostic of report.diagnostics) {\n const ruleId = ruleIdFor(suite.plugin, diagnostic.code);\n if (ruleId === null) {\n foreignDiagnostics.push(diagnostic);\n continue;\n }\n const seen = firedByRule.get(ruleId) ?? new Set<string>();\n seen.add(basename(diagnostic.filename));\n firedByRule.set(ruleId, seen);\n }\n\n const rows: Row[] = expectedRules.map((rule) => {\n const seen = firedByRule.get(rule) ?? new Set<string>();\n const fixture = files.get(rule) ?? rule;\n const firedOnOwnFixture = seen.has(fixture);\n return {\n rule,\n fixture,\n firedOnOwnFixture,\n note: firedOnOwnFixture\n ? \"\"\n : seen.size > 0\n ? `fired only on: ${[...seen].toSorted(byName).join(\", \")}`\n : \"rule did not fire on any fixture\",\n };\n });\n\n const width = Math.max(...rows.map((row) => row.rule.length), 4);\n console.log(`${\"RULE\".padEnd(width)} RESULT FIXTURE`);\n console.log(`${\"-\".repeat(width)} ------ -------`);\n for (const row of rows) {\n const verdict = row.firedOnOwnFixture ? \"PASS \" : \"FAIL \";\n const suffix = row.note === \"\" ? \"\" : ` <-- ${row.note}`;\n console.log(`${row.rule.padEnd(width)} ${verdict} ${row.fixture}${suffix}`);\n }\n console.log(\"\");\n\n const failures = rows.filter((row) => !row.firedOnOwnFixture);\n console.log(\n `${rows.length - failures.length}/${rows.length} ${suite.plugin} rules fired on their own fixture.`,\n );\n\n // Advisory: a plugin bump that adds a rule should also add a fixture.\n const registered = suite.registeredRules === undefined ? null : await suite.registeredRules();\n if (registered !== null) {\n const missingFixture = registered.filter((rule) => !expectedRules.includes(rule));\n const orphanFixture = expectedRules.filter((rule) => !registered.includes(rule));\n if (missingFixture.length > 0) {\n console.log(\n `\\nWARN: registered by the ${suite.plugin} plugin but has no fixture: ${missingFixture.join(\", \")}`,\n );\n }\n if (orphanFixture.length > 0) {\n console.log(\n `\\nWARN: fixture exists but the ${suite.plugin} plugin registers no such rule: ${orphanFixture.join(\", \")}`,\n );\n }\n }\n\n // Advisory: fixtures are meant to trip exactly one rule each.\n if (foreignDiagnostics.length > 0) {\n console.log(\n `\\nWARN: ${foreignDiagnostics.length} non-${suite.plugin} diagnostic(s) in the fixtures:`,\n );\n for (const diagnostic of foreignDiagnostics.slice(0, 20)) {\n console.log(` ${basename(diagnostic.filename)}: ${diagnostic.code}`);\n }\n }\n\n if (failures.length > 0) {\n const where = suite.readme === undefined ? \"\" : ` See ${suite.readme}.`;\n console.error(\n `\\nFAIL: ${failures.length} ${suite.plugin} rule(s) did not fire.${where}\\n` +\n `First thing to check: the config's override on \"${suite.viewDir}\" has to put every rule\\n` +\n \"at error. For this plugin that override is `fixtures()` from @rdjperron/lint/config; no\\n\" +\n \"command-line flag can switch a JS plugin rule on.\",\n );\n }\n console.log(\"\");\n return failures.length;\n}\n\n/**\n * Lint every suite and print one table each. Returns the number of rules that\n * did not fire, so zero means every rule of every suite is alive.\n */\nexport async function verifySuites(suites: Suite[], options: VerifyOptions = {}): Promise<number> {\n const root = resolve(options.root ?? process.cwd());\n const binary = resolveViteplusBinary();\n let failures = 0;\n try {\n for (const suite of suites) {\n failures += await verifySuite(binary, root, suite);\n }\n } finally {\n for (const suite of suites) removeFixtureView(root, suite);\n }\n if (failures > 0) {\n console.error(`FAIL: ${failures} rule(s) did not fire across ${suites.length} suite(s).`);\n return failures;\n }\n console.log(\n `OK: every rule of every suite (${suites.map((suite) => suite.plugin).join(\", \")}) still fires.`,\n );\n return 0;\n}\n\nconst PLUGINS_PACKAGE = \"@oxlint/plugins\";\n\n/** What this needs from a `package.json`: its version, and what it says about the plugin API. */\ninterface Manifest {\n version: string;\n /** The `@oxlint/plugins` spec in `dependencies`, or \"\". */\n declaredPlugins: string;\n /** The `@oxlint/plugins` spec in `peerDependencies`, or \"\". */\n peerPlugins: string;\n}\n\n/** npm's manifest schema: both tables map a package name to a version spec. */\ninterface DependencyTable {\n [name: string]: string | undefined;\n}\n\ninterface RawManifest {\n version?: unknown;\n dependencies?: DependencyTable;\n peerDependencies?: DependencyTable;\n}\n\nfunction readManifest(path: string): Manifest {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n if (typeof parsed !== \"object\" || parsed === null) throw new Error(`Not a manifest: ${path}`);\n // SAFETY: the check above makes it a non-null object, and npm's own schema makes\n // both dependency tables name-to-spec; a package.json that breaks that is not\n // installable, and a non-string spec reaching here simply fails the comparisons.\n const entry = parsed as RawManifest;\n return {\n version: typeof entry.version === \"string\" ? entry.version : \"\",\n declaredPlugins: entry.dependencies?.[PLUGINS_PACKAGE] ?? \"\",\n peerPlugins: entry.peerDependencies?.[PLUGINS_PACKAGE] ?? \"\",\n };\n}\n\n/** The nearest `package.json` at or above a resolved module file. */\nfunction manifestNear(modulePath: string): string | null {\n let dir = dirname(modulePath);\n for (;;) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** `^x.y.z` is satisfied by the same major, at or above the base. */\nfunction caretAccepts(range: string, version: string): boolean {\n const match = /^\\^(\\d+)\\.(\\d+)\\.(\\d+)$/u.exec(range);\n if (match === null) throw new Error(`Expected a caret range, got \"${range}\"`);\n const [major, minor, patch] = match.slice(1).map(Number);\n const [candidateMajor, candidateMinor, candidatePatch] = version.split(\".\").map(Number);\n if (candidateMajor !== major) return false;\n if (candidateMinor !== minor) return (candidateMinor ?? 0) > (minor ?? 0);\n return (candidatePatch ?? 0) >= (patch ?? 0);\n}\n\n/**\n * Two promises about the plugin API, held here as a failure instead.\n *\n * - the installed `@oxlint/plugins` is the exact version `vite-plus` declares;\n * - this package's peer range accepts that version.\n *\n * The rules import `@oxlint/plugins`, so the installed one must be the one the\n * linter was built against, or the rules compile against one plugin API and run\n * under another with no error anywhere. Returns the failures, empty when the\n * pins agree or when `vite-plus` is not installed (a consumer on another\n * toolchain has nothing to pin against).\n */\nexport function verifyPluginApiPin(root = process.cwd()): string[] {\n const requireFromRoot = createRequire(join(resolve(root), \"package.json\"));\n const requireFromHere = createRequire(import.meta.url);\n\n let vitePlus: Manifest;\n try {\n vitePlus = readManifest(requireFromRoot.resolve(\"vite-plus/package.json\"));\n } catch {\n console.log(\"plugin API pin: skipped, `vite-plus` is not installed here.\");\n return [];\n }\n\n const declared = vitePlus.declaredPlugins.replace(/^=/u, \"\");\n if (!/^\\d+\\.\\d+\\.\\d+$/u.test(declared)) {\n return [`vite-plus ${vitePlus.version} no longer declares an exact @oxlint/plugins version.`];\n }\n\n let installed: Manifest;\n try {\n const entry = requireFromRoot.resolve(\"@oxlint/plugins\");\n const manifest = manifestNear(entry);\n if (manifest === null) throw new Error(\"no package.json above @oxlint/plugins\");\n installed = readManifest(manifest);\n } catch {\n return [\"`@oxlint/plugins` is not installed. It is a peer dependency of @rdjperron/lint.\"];\n }\n\n const self = readManifest(requireFromHere.resolve(\"../package.json\"));\n const peer = self.peerPlugins;\n const failures: string[] = [];\n\n if (installed.version !== declared) {\n failures.push(\n `@oxlint/plugins ${installed.version} is installed and vite-plus ${vitePlus.version} declares ` +\n `${declared}. Move the pin: install @oxlint/plugins@${declared} exactly.`,\n );\n }\n if (!caretAccepts(peer, declared)) {\n failures.push(\n `@rdjperron/lint ${self.version} wants @oxlint/plugins ${peer} and vite-plus ${vitePlus.version} ` +\n `declares ${declared}. Move one of the two pins.`,\n );\n }\n if (failures.length === 0) {\n console.log(\n `plugin API pin: @oxlint/plugins ${installed.version}, what vite-plus ${vitePlus.version} declares.`,\n );\n }\n return failures;\n}\n\n/** The suite for this package's own fixtures, as they sit in a consumer's `node_modules`. */\nexport function packageSuite(root = process.cwd()): Suite {\n const packageDir = \"node_modules/@rdjperron/lint\";\n return {\n plugin: \"rdj\",\n fixtureDir: `${packageDir}/fixtures`,\n viewDir: \"scripts/.lint-fixtures/rdj\",\n alwaysView: true,\n readme: `${packageDir}/fixtures/README.md`,\n registeredRules: async () => {\n try {\n const requireFromRoot = createRequire(join(resolve(root), \"package.json\"));\n const loaded: unknown = await import(requireFromRoot.resolve(\"@rdjperron/lint\"));\n // SAFETY: the plugin's default export is the object oxlint loads; `rules` is\n // still `unknown` here and is checked on the next line.\n const module_ = loaded as { default?: { rules?: unknown } };\n const rules = module_.default?.rules;\n return typeof rules === \"object\" && rules !== null\n ? Object.keys(rules).toSorted(byName)\n : null;\n } catch {\n return null;\n }\n },\n };\n}\n\n/** The bin: this package's suite, plus the plugin API pin, from the consumer's repo root. */\nexport async function main(root = process.cwd()): Promise<number> {\n const pinFailures = verifyPluginApiPin(root);\n for (const failure of pinFailures) console.error(`FAIL: ${failure}`);\n console.log(\"\");\n const ruleFailures = await verifySuites([packageSuite(root)], { root });\n return pinFailures.length + ruleFailures > 0 ? 1 : 0;\n}\n\nconst invoked = process.argv[1];\nconst isBin =\n invoked !== undefined &&\n existsSync(invoked) &&\n realpathSync(invoked) === fileURLToPath(import.meta.url);\n\nif (isBin) {\n let exitCode = 1;\n try {\n exitCode = await main();\n } catch (error) {\n console.error(`\\nFAIL: ${error instanceof Error ? error.message : String(error)}`);\n }\n process.exit(exitCode);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"verify.mjs","names":[],"sources":["../src/verify.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Proof that every lint rule still fires.\n *\n * The linter lives inside the `vp` binary, so it moves independently of any\n * lockfile, and the JS-plugin bridge it loads these rules through is alpha. A\n * rule can stop firing with no diff anywhere, and a dead lint rule is silent:\n * \"reports nothing\" looks exactly like \"no violations\". This is the thing that\n * notices.\n *\n * A suite is a directory holding one deliberately-broken `<rule>.fixture.ts(x)`\n * per rule. The check asserts a positive: every rule fired, each on the file\n * named after it.\n *\n * Run the package's own suite from a consumer's repo root:\n *\n * npx rdjperron-lint-verify\n *\n * ---------------------------------------------------------------------------\n * Why the symlink dance\n * ---------------------------------------------------------------------------\n * The fixtures ship inside `node_modules`, which oxlint never walks, and a\n * consumer's own fixture directories are usually in `lint.ignorePatterns` so a\n * plain `vp check` stays quiet. Measured on Vite+ 0.2.9 / oxlint 1.77.0 and\n * again on 0.3.1 / oxlint 1.81.0: no flag re-includes an ignored path.\n * `--no-ignore`, `--ignore-pattern '!<dir>/**'`, an explicit file, an absolute\n * path, a cwd inside the directory and `-W <rule>` all answer \"No files found\n * to lint.\" `--no-ignore` only disables `.eslintignore`, `--ignore-path` and\n * `--ignore-pattern`; it does not touch the config's own `ignorePatterns`, and\n * `vp lint` has no `--config` flag.\n *\n * What does work: oxlint matches ignore patterns against the path it walked,\n * not the realpath. Linting through a symlink outside the ignored prefix\n * reaches the fixtures normally, plugin and all. This creates that view per\n * suite, lints through it, and removes it in a `finally`.\n *\n * The severities cannot come from the command line either: `-A`, `-W` and `-D`\n * reach built-in rules only and leave every JS plugin rule exactly as the config\n * left it (measured on vite-plus 0.3.1 / oxlint 1.81.0, with `vp lint` and with\n * the bare binary). That is why the consumer's config must carry the\n * `fixtures()` override from `@rdjperron/lint/config`.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n rmdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n symlinkSync,\n} from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface Suite {\n /** The plugin alias the diagnostics carry, e.g. `rdjperron` in `rdjperron(no-reflect-get)`. */\n plugin: string;\n /** The fixture directory, relative to `root`. */\n fixtureDir: string;\n /** Where the un-ignored view is created, relative to `root`. */\n viewDir: string;\n /** Fixture file endings; the part before one of them is the rule name. */\n suffixes?: string[];\n /** Skip the direct path and always lint through the view (fixtures under `node_modules`). */\n alwaysView?: boolean;\n /** Advisory: the rule names the plugin registers, so a new rule without a fixture is named. */\n registeredRules?: () => Promise<string[] | null>;\n /** Where the failure message sends the reader. */\n readme?: string;\n}\n\nexport interface VerifyOptions {\n /** The repo root the paths are relative to and `vp lint` runs from. Defaults to the cwd. */\n root?: string;\n}\n\nconst DEFAULT_SUFFIXES = [\".fixture.ts\", \".fixture.tsx\"];\n\n/** Deterministic, locale-independent ordering for the rule and file name lists. */\nfunction byName(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction basename(path: string): string {\n const parts = path.split(/[\\\\/]/u);\n return parts[parts.length - 1] ?? path;\n}\n\n/**\n * Locate the `vp` binary: PATH first, then the default install locations, the\n * XDG layout used since Vite+ 0.3.0 and the pre-0.3.0 single-root layout that\n * upgraded installs keep.\n */\nexport function resolveViteplusBinary(): string {\n const probe = spawnSync(\"vp\", [\"--version\"], { encoding: \"utf8\" });\n if (probe.status === 0) return \"vp\";\n const home = process.env.HOME ?? \"\";\n const fallbacks = [\n join(home, \".local\", \"share\", \"vite-plus\", \"bin\", \"vp\"),\n join(home, \".vite-plus\", \"bin\", \"vp\"),\n ];\n const found = fallbacks.find((candidate) => existsSync(candidate));\n if (found !== undefined) return found;\n throw new Error(\n \"Could not find the `vp` binary. Add it to PATH: \" +\n 'export PATH=\"$HOME/.local/share/vite-plus/bin:$PATH\" (or \"$HOME/.vite-plus/bin\" for a pre-0.3.0 install)',\n );\n}\n\ninterface LintDiagnostic {\n code: string;\n filename: string;\n}\n\ninterface LintReport {\n numberOfFiles: number;\n diagnostics: LintDiagnostic[];\n}\n\n/** The two fields of oxlint's JSON report this reads, before any of them is trusted. */\ninterface RawReport {\n number_of_files?: unknown;\n diagnostics?: unknown;\n}\n\n/** The two fields of one raw diagnostic, before either of them is trusted. */\ninterface RawDiagnostic {\n code?: unknown;\n filename?: unknown;\n}\n\n/**\n * `vp lint -f json` is an outside value, so this is the one place its shape is\n * checked. Anything that does not look like a diagnostic is dropped rather than\n * guessed at.\n */\nfunction decodeReport(json: string): LintReport {\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new Error(\"`vp lint -f json` did not return an object.\");\n }\n // SAFETY: the check above makes it a non-null object, and every field of\n // `RawReport` is `unknown`, so nothing is claimed about its contents yet.\n const report = parsed as RawReport;\n const numberOfFiles = typeof report.number_of_files === \"number\" ? report.number_of_files : 0;\n const rows = Array.isArray(report.diagnostics) ? report.diagnostics : [];\n const diagnostics: LintDiagnostic[] = [];\n for (const row of rows) {\n if (typeof row !== \"object\" || row === null) continue;\n // SAFETY: same check, one level down; both fields of `RawDiagnostic` are\n // `unknown` and each is typed by its own test on the next lines.\n const entry = row as RawDiagnostic;\n diagnostics.push({\n code: typeof entry.code === \"string\" ? entry.code : \"\",\n filename: typeof entry.filename === \"string\" ? entry.filename : \"\",\n });\n }\n return { numberOfFiles, diagnostics };\n}\n\n/** Run `vp lint <target> -f json` from `root` and return the decoded report. */\nfunction lintAsJson(binary: string, root: string, target: string): LintReport {\n const result = spawnSync(\n binary,\n [\"lint\", target, \"-f\", \"json\", \"--no-error-on-unmatched-pattern\"],\n {\n cwd: root,\n encoding: \"utf8\",\n maxBuffer: 64 * 1024 * 1024,\n },\n );\n\n if (result.error !== undefined && result.error !== null) throw result.error;\n\n // `--no-error-on-unmatched-pattern` keeps the \"No files found to lint.\" banner\n // off stdout, so stdout is pure JSON even when nothing matched.\n const stdout = result.stdout ?? \"\";\n const start = stdout.indexOf(\"{\");\n if (start === -1) {\n throw new Error(\n `\\`vp lint ${target} -f json\\` produced no JSON.\\nstdout: ${stdout}\\nstderr: ${result.stderr ?? \"\"}`,\n );\n }\n\n try {\n return decodeReport(stdout.slice(start));\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n throw new Error(`Could not read \\`vp lint\\` JSON output: ${reason}\\nstdout: ${stdout}`, {\n cause,\n });\n }\n}\n\n/**\n * Why a path is gitignored, or null. oxlint honours `.gitignore`, so a\n * gitignored view yields \"0 files\" instead of a useful diagnostic.\n */\nfunction gitIgnoreReason(root: string, relPath: string): string | null {\n const result = spawnSync(\"git\", [\"check-ignore\", \"-v\", relPath], { cwd: root, encoding: \"utf8\" });\n const output = result.status === 0 ? result.stdout.trim() : \"\";\n return output === \"\" ? null : (output.split(\"\\t\")[0] ?? null);\n}\n\n/** Rule name to fixture file name, taken from the files that exist on disk. */\nfunction fixtureFiles(root: string, suite: Suite): Map<string, string> {\n const dir = join(root, suite.fixtureDir);\n if (!existsSync(dir)) throw new Error(`Fixture directory is missing: ${dir}`);\n const suffixes = suite.suffixes ?? DEFAULT_SUFFIXES;\n const files = new Map<string, string>();\n for (const name of readdirSync(dir).toSorted(byName)) {\n const suffix = suffixes.find((candidate) => name.endsWith(candidate));\n if (suffix === undefined) continue;\n files.set(name.slice(0, -suffix.length), name);\n }\n return files;\n}\n\n/** Create the un-ignored view; prefer a symlink, fall back to a copy on Windows. */\nfunction createFixtureView(root: string, suite: Suite): string {\n removeFixtureView(root, suite);\n const viewPath = join(root, suite.viewDir);\n mkdirSync(dirname(viewPath), { recursive: true });\n try {\n symlinkSync(join(root, suite.fixtureDir), viewPath, \"junction\");\n return \"symlink\";\n } catch {\n cpSync(join(root, suite.fixtureDir), viewPath, { recursive: true });\n return \"copy\";\n }\n}\n\nfunction removeFixtureView(root: string, suite: Suite): void {\n const viewPath = join(root, suite.viewDir);\n rmSync(viewPath, { recursive: true, force: true });\n // The parents were created for the view alone when nothing else lives there, so they go too.\n const top = resolve(root);\n for (let dir = dirname(viewPath); dir !== top; dir = dirname(dir)) {\n if (!existsSync(dir) || readdirSync(dir).length > 0) break;\n rmdirSync(dir);\n }\n}\n\n/** `rdjperron(no-reflect-get)` -> `no-reflect-get` for this suite's plugin; anything else -> null. */\nfunction ruleIdFor(plugin: string, code: string): string | null {\n const match = new RegExp(`^${plugin}\\\\(([^)]+)\\\\)$`, \"u\").exec(code);\n return match === null ? null : (match[1] ?? null);\n}\n\ninterface Row {\n rule: string;\n fixture: string;\n firedOnOwnFixture: boolean;\n note: string;\n}\n\n/** Lint one suite and print its table. Returns the number of rules that did not fire. */\nasync function verifySuite(binary: string, root: string, suite: Suite): Promise<number> {\n const files = fixtureFiles(root, suite);\n const expectedRules = [...files.keys()];\n\n console.log(`${suite.plugin} rule verification`);\n console.log(` fixtures: ${suite.fixtureDir}`);\n\n if (expectedRules.length === 0) {\n console.error(` No fixture files found in ${suite.fixtureDir}. Nothing to verify.\\n`);\n return 1;\n }\n\n // 1. Try the fixture directory directly. Works only while it is un-ignored, so\n // a suite under node_modules skips it: oxlint never walks in there.\n let mechanism = \"direct path\";\n let report: LintReport =\n suite.alwaysView === true\n ? { numberOfFiles: 0, diagnostics: [] }\n : lintAsJson(binary, root, suite.fixtureDir);\n\n // 2. Ignored by the config or under node_modules: lint through an un-ignored view.\n if (report.numberOfFiles === 0) {\n const kind = createFixtureView(root, suite);\n try {\n mechanism = `${kind} at ${suite.viewDir} (\\`${suite.fixtureDir}\\` is not walked by \\`vp lint\\`)`;\n report = lintAsJson(binary, root, suite.viewDir);\n } finally {\n // Never leave the view behind: a stale one would show up in `vp check`.\n removeFixtureView(root, suite);\n }\n }\n\n console.log(` lint via: ${mechanism}`);\n console.log(` files: ${report.numberOfFiles}`);\n console.log(\"\");\n\n if (report.numberOfFiles === 0) {\n const ignoredBy = gitIgnoreReason(root, suite.viewDir);\n console.error(\n \"FAIL: `vp lint` matched 0 files through both the direct path and the un-ignored view.\\n\" +\n (ignoredBy === null\n ? \"Check that the config ignores the fixture directory itself and not a pattern such as\\n\" +\n `\"**/*.fixture.ts\", which would also hide the view at \"${suite.viewDir}\".\\n` +\n \"Check too that the config carries the `fixtures()` override from @rdjperron/lint/config,\\n\" +\n \"on the view path: no command-line flag can switch a JS plugin rule on.\\n\"\n : `CAUSE: \"${suite.viewDir}\" is gitignored (${ignoredBy}). oxlint skips gitignored paths,\\n` +\n \"so the view this lints through is invisible to it. Remove that ignore rule; the view is\\n\" +\n \"deleted in a `finally` block anyway.\\n\"),\n );\n return expectedRules.length;\n }\n\n const firedByRule = new Map<string, Set<string>>();\n const foreignDiagnostics: LintDiagnostic[] = [];\n\n for (const diagnostic of report.diagnostics) {\n const ruleId = ruleIdFor(suite.plugin, diagnostic.code);\n if (ruleId === null) {\n foreignDiagnostics.push(diagnostic);\n continue;\n }\n const seen = firedByRule.get(ruleId) ?? new Set<string>();\n seen.add(basename(diagnostic.filename));\n firedByRule.set(ruleId, seen);\n }\n\n const rows: Row[] = expectedRules.map((rule) => {\n const seen = firedByRule.get(rule) ?? new Set<string>();\n const fixture = files.get(rule) ?? rule;\n const firedOnOwnFixture = seen.has(fixture);\n return {\n rule,\n fixture,\n firedOnOwnFixture,\n note: firedOnOwnFixture\n ? \"\"\n : seen.size > 0\n ? `fired only on: ${[...seen].toSorted(byName).join(\", \")}`\n : \"rule did not fire on any fixture\",\n };\n });\n\n const width = Math.max(...rows.map((row) => row.rule.length), 4);\n console.log(`${\"RULE\".padEnd(width)} RESULT FIXTURE`);\n console.log(`${\"-\".repeat(width)} ------ -------`);\n for (const row of rows) {\n const verdict = row.firedOnOwnFixture ? \"PASS \" : \"FAIL \";\n const suffix = row.note === \"\" ? \"\" : ` <-- ${row.note}`;\n console.log(`${row.rule.padEnd(width)} ${verdict} ${row.fixture}${suffix}`);\n }\n console.log(\"\");\n\n const failures = rows.filter((row) => !row.firedOnOwnFixture);\n console.log(\n `${rows.length - failures.length}/${rows.length} ${suite.plugin} rules fired on their own fixture.`,\n );\n\n // Advisory: a plugin bump that adds a rule should also add a fixture.\n const registered = suite.registeredRules === undefined ? null : await suite.registeredRules();\n if (registered !== null) {\n const missingFixture = registered.filter((rule) => !expectedRules.includes(rule));\n const orphanFixture = expectedRules.filter((rule) => !registered.includes(rule));\n if (missingFixture.length > 0) {\n console.log(\n `\\nWARN: registered by the ${suite.plugin} plugin but has no fixture: ${missingFixture.join(\", \")}`,\n );\n }\n if (orphanFixture.length > 0) {\n console.log(\n `\\nWARN: fixture exists but the ${suite.plugin} plugin registers no such rule: ${orphanFixture.join(\", \")}`,\n );\n }\n }\n\n // Advisory: fixtures are meant to trip exactly one rule each.\n if (foreignDiagnostics.length > 0) {\n console.log(\n `\\nWARN: ${foreignDiagnostics.length} non-${suite.plugin} diagnostic(s) in the fixtures:`,\n );\n for (const diagnostic of foreignDiagnostics.slice(0, 20)) {\n console.log(` ${basename(diagnostic.filename)}: ${diagnostic.code}`);\n }\n }\n\n if (failures.length > 0) {\n const where = suite.readme === undefined ? \"\" : ` See ${suite.readme}.`;\n console.error(\n `\\nFAIL: ${failures.length} ${suite.plugin} rule(s) did not fire.${where}\\n` +\n `First thing to check: the config's override on \"${suite.viewDir}\" has to put every rule\\n` +\n \"at error. For this plugin that override is `fixtures()` from @rdjperron/lint/config; no\\n\" +\n \"command-line flag can switch a JS plugin rule on.\",\n );\n }\n console.log(\"\");\n return failures.length;\n}\n\n/**\n * Lint every suite and print one table each. Returns the number of rules that\n * did not fire, so zero means every rule of every suite is alive.\n */\nexport async function verifySuites(suites: Suite[], options: VerifyOptions = {}): Promise<number> {\n const root = resolve(options.root ?? process.cwd());\n const binary = resolveViteplusBinary();\n let failures = 0;\n try {\n for (const suite of suites) {\n failures += await verifySuite(binary, root, suite);\n }\n } finally {\n for (const suite of suites) removeFixtureView(root, suite);\n }\n if (failures > 0) {\n console.error(`FAIL: ${failures} rule(s) did not fire across ${suites.length} suite(s).`);\n return failures;\n }\n console.log(\n `OK: every rule of every suite (${suites.map((suite) => suite.plugin).join(\", \")}) still fires.`,\n );\n return 0;\n}\n\nconst PLUGINS_PACKAGE = \"@oxlint/plugins\";\n\n/** What this needs from a `package.json`: its version, and what it says about the plugin API. */\ninterface Manifest {\n version: string;\n /** The `@oxlint/plugins` spec in `dependencies`, or \"\". */\n declaredPlugins: string;\n /** The `@oxlint/plugins` spec in `peerDependencies`, or \"\". */\n peerPlugins: string;\n}\n\n/** npm's manifest schema: both tables map a package name to a version spec. */\ninterface DependencyTable {\n [name: string]: string | undefined;\n}\n\ninterface RawManifest {\n version?: unknown;\n dependencies?: DependencyTable;\n peerDependencies?: DependencyTable;\n}\n\nfunction readManifest(path: string): Manifest {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n if (typeof parsed !== \"object\" || parsed === null) throw new Error(`Not a manifest: ${path}`);\n // SAFETY: the check above makes it a non-null object, and npm's own schema makes\n // both dependency tables name-to-spec; a package.json that breaks that is not\n // installable, and a non-string spec reaching here simply fails the comparisons.\n const entry = parsed as RawManifest;\n return {\n version: typeof entry.version === \"string\" ? entry.version : \"\",\n declaredPlugins: entry.dependencies?.[PLUGINS_PACKAGE] ?? \"\",\n peerPlugins: entry.peerDependencies?.[PLUGINS_PACKAGE] ?? \"\",\n };\n}\n\n/** The nearest `package.json` at or above a resolved module file. */\nfunction manifestNear(modulePath: string): string | null {\n let dir = dirname(modulePath);\n for (;;) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** `^x.y.z` is satisfied by the same major, at or above the base. */\nfunction caretAccepts(range: string, version: string): boolean {\n const match = /^\\^(\\d+)\\.(\\d+)\\.(\\d+)$/u.exec(range);\n if (match === null) throw new Error(`Expected a caret range, got \"${range}\"`);\n const [major, minor, patch] = match.slice(1).map(Number);\n const [candidateMajor, candidateMinor, candidatePatch] = version.split(\".\").map(Number);\n if (candidateMajor !== major) return false;\n if (candidateMinor !== minor) return (candidateMinor ?? 0) > (minor ?? 0);\n return (candidatePatch ?? 0) >= (patch ?? 0);\n}\n\n/**\n * Two promises about the plugin API, held here as a failure instead.\n *\n * - the installed `@oxlint/plugins` is the exact version `vite-plus` declares;\n * - this package's peer range accepts that version.\n *\n * The rules import `@oxlint/plugins`, so the installed one must be the one the\n * linter was built against, or the rules compile against one plugin API and run\n * under another with no error anywhere. Returns the failures, empty when the\n * pins agree or when `vite-plus` is not installed (a consumer on another\n * toolchain has nothing to pin against).\n */\nexport function verifyPluginApiPin(root = process.cwd()): string[] {\n const requireFromRoot = createRequire(join(resolve(root), \"package.json\"));\n const requireFromHere = createRequire(import.meta.url);\n\n let vitePlus: Manifest;\n try {\n vitePlus = readManifest(requireFromRoot.resolve(\"vite-plus/package.json\"));\n } catch {\n console.log(\"plugin API pin: skipped, `vite-plus` is not installed here.\");\n return [];\n }\n\n const declared = vitePlus.declaredPlugins.replace(/^=/u, \"\");\n if (!/^\\d+\\.\\d+\\.\\d+$/u.test(declared)) {\n return [`vite-plus ${vitePlus.version} no longer declares an exact @oxlint/plugins version.`];\n }\n\n let installed: Manifest;\n try {\n const entry = requireFromRoot.resolve(\"@oxlint/plugins\");\n const manifest = manifestNear(entry);\n if (manifest === null) throw new Error(\"no package.json above @oxlint/plugins\");\n installed = readManifest(manifest);\n } catch {\n return [\"`@oxlint/plugins` is not installed. It is a peer dependency of @rdjperron/lint.\"];\n }\n\n const self = readManifest(requireFromHere.resolve(\"../package.json\"));\n const peer = self.peerPlugins;\n const failures: string[] = [];\n\n if (installed.version !== declared) {\n failures.push(\n `@oxlint/plugins ${installed.version} is installed and vite-plus ${vitePlus.version} declares ` +\n `${declared}. Move the pin: install @oxlint/plugins@${declared} exactly.`,\n );\n }\n if (!caretAccepts(peer, declared)) {\n failures.push(\n `@rdjperron/lint ${self.version} wants @oxlint/plugins ${peer} and vite-plus ${vitePlus.version} ` +\n `declares ${declared}. Move one of the two pins.`,\n );\n }\n if (failures.length === 0) {\n console.log(\n `plugin API pin: @oxlint/plugins ${installed.version}, what vite-plus ${vitePlus.version} declares.`,\n );\n }\n return failures;\n}\n\n/** The suite for this package's own fixtures, as they sit in a consumer's `node_modules`. */\nexport function packageSuite(root = process.cwd()): Suite {\n const packageDir = \"node_modules/@rdjperron/lint\";\n return {\n plugin: \"rdjperron\",\n fixtureDir: `${packageDir}/fixtures`,\n viewDir: \"scripts/.lint-fixtures/rdjperron\",\n alwaysView: true,\n readme: `${packageDir}/fixtures/README.md`,\n registeredRules: async () => {\n try {\n const requireFromRoot = createRequire(join(resolve(root), \"package.json\"));\n const loaded: unknown = await import(requireFromRoot.resolve(\"@rdjperron/lint\"));\n // SAFETY: the plugin's default export is the object oxlint loads; `rules` is\n // still `unknown` here and is checked on the next line.\n const module_ = loaded as { default?: { rules?: unknown } };\n const rules = module_.default?.rules;\n return typeof rules === \"object\" && rules !== null\n ? Object.keys(rules).toSorted(byName)\n : null;\n } catch {\n return null;\n }\n },\n };\n}\n\n/** The bin: this package's suite, plus the plugin API pin, from the consumer's repo root. */\nexport async function main(root = process.cwd()): Promise<number> {\n const pinFailures = verifyPluginApiPin(root);\n for (const failure of pinFailures) console.error(`FAIL: ${failure}`);\n console.log(\"\");\n const ruleFailures = await verifySuites([packageSuite(root)], { root });\n return pinFailures.length + ruleFailures > 0 ? 1 : 0;\n}\n\nconst invoked = process.argv[1];\nconst isBin =\n invoked !== undefined &&\n existsSync(invoked) &&\n realpathSync(invoked) === fileURLToPath(import.meta.url);\n\nif (isBin) {\n let exitCode = 1;\n try {\n exitCode = await main();\n } catch (error) {\n console.error(`\\nFAIL: ${error instanceof Error ? error.message : String(error)}`);\n }\n process.exit(exitCode);\n}\n"],"mappings":"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"}
|
package/fixtures/README.md
CHANGED
|
@@ -16,7 +16,7 @@ pattern swallows the wrong path.
|
|
|
16
16
|
And a dead lint rule is silent. "Rule reports nothing" looks identical to "code has no
|
|
17
17
|
violations". Nothing else would notice.
|
|
18
18
|
|
|
19
|
-
`npx
|
|
19
|
+
`npx rdjperron-lint-verify` lints these files and asserts a positive: every rule must fire, each on its
|
|
20
20
|
own fixture. If a rule goes dead, the run fails loudly instead of going quietly green.
|
|
21
21
|
|
|
22
22
|
## How it relates to the unit tests
|
|
@@ -25,7 +25,7 @@ Two layers, different questions:
|
|
|
25
25
|
|
|
26
26
|
| Layer | Question it answers | Where |
|
|
27
27
|
| --------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------- |
|
|
28
|
-
| This directory + the verify command | Does each rule fire through the real pipeline: the bundled oxlint, the `jsPlugins` wiring, the actual config? | `fixtures/`, `
|
|
28
|
+
| This directory + the verify command | Does each rule fire through the real pipeline: the bundled oxlint, the `jsPlugins` wiring, the actual config? | `fixtures/`, `rdjperron-lint-verify` |
|
|
29
29
|
| Unit tests (RuleTester) | Is each rule's logic right? Valid cases stay quiet, invalid cases report, with per-case precision. | `tests/` in the source repo (not published) |
|
|
30
30
|
|
|
31
31
|
The unit tests catch a locally edited rule that starts over- or under-firing. This layer catches
|
|
@@ -39,7 +39,7 @@ the rule not running at all. Both are needed.
|
|
|
39
39
|
puts every rule at `error` on the view path. No command-line flag can do it: `-A`, `-W` and `-D`
|
|
40
40
|
reach built-in rules only and leave a JS plugin rule exactly as the config left it.
|
|
41
41
|
- The fixtures sit under `node_modules`, which oxlint never walks, so the check lints them through
|
|
42
|
-
a transient symlink at `scripts/.lint-fixtures/
|
|
42
|
+
a transient symlink at `scripts/.lint-fixtures/rdjperron`. In the source repo they are also in
|
|
43
43
|
`lint.ignorePatterns` and `fmt.ignorePatterns`, so a plain `vp check` stays quiet; no `vp lint`
|
|
44
44
|
flag re-includes a config-ignored path.
|
|
45
45
|
- Never gitignore that view. oxlint honours `.gitignore`, so gitignoring anything on the
|
|
@@ -56,7 +56,7 @@ the rule not running at all. Both are needed.
|
|
|
56
56
|
|
|
57
57
|
1. `vp toolchain oxlint`: did the bundled oxlint move?
|
|
58
58
|
2. `vp why @oxlint/plugins`: if `vite-plus` now declares a different version, move the exact
|
|
59
|
-
pin to it and reinstall. `
|
|
59
|
+
pin to it and reinstall. `rdjperron-lint-verify` fails on that skew before it reaches the rules.
|
|
60
60
|
3. Re-run the check.
|
|
61
61
|
4. Still dead? The rule's AST expectations may have changed under the new oxc version. Read the
|
|
62
62
|
rule in `src/rules/` or `src/anti-slop/rules/`, fix it, add the case to `tests/`, and release.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-known-value-widening.
|
|
2
2
|
export const retrySettings: unknown = { retries: 3 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-module-mocking.
|
|
2
2
|
// `vi` is deliberately left undeclared: the rule's global/unresolved-reference
|
|
3
3
|
// path is what recognises `vi.mock` without importing from "vitest" (which
|
|
4
4
|
// `vite-plus/prefer-vite-plus-imports` forbids in this repo).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-reduce-accumulator-copy.
|
|
2
2
|
export function indexById(items: { id: string }[]): Record<string, { id: string }> {
|
|
3
3
|
return items.reduce((acc, item) => Object.assign({}, acc, { [item.id]: item }), {});
|
|
4
4
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-shape-in-symbol-names.
|
|
2
2
|
export const shapeCount = 1;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-unknown-type-aliases.
|
|
2
2
|
export type Payload = unknown;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/no-unsafe-dictionary-type.
|
|
2
2
|
export type Registry = Record<string, unknown>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Fixture for
|
|
1
|
+
// Fixture for rdjperron/require-safety-comment-for-type-assertion.
|
|
2
2
|
export function lengthOf(entry: { id: string }): number {
|
|
3
3
|
const widerEntry = entry as { id: string; extra?: number };
|
|
4
4
|
return widerEntry.id.length;
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rdjperron/lint",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One Oxlint JS plugin, `
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "One Oxlint JS plugin, `rdjperron`: 20 rules against low-evidence code, with the config data, the fixture that proves each rule fires, and the command that checks it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"rdjperron-lint-verify": "dist/verify.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"docs",
|
|
12
11
|
"fixtures",
|
|
13
12
|
"LICENSE",
|
|
14
13
|
"README.md"
|
package/docs/rules/README.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# The rules
|
|
2
|
-
|
|
3
|
-
One page per rule, each with the diagnostic it emits, the fix, and the cases where a scoped
|
|
4
|
-
override is honest. Every rule ships in the `rdj` plugin, so a rule id is `rdj/<rule-name>`.
|
|
5
|
-
|
|
6
|
-
Seventeen of them are vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop);
|
|
7
|
-
`no-use-effect`, `no-hardcoded-jsx-text` and `static-public-env` are written here. Each page
|
|
8
|
-
says which.
|
|
9
|
-
|
|
10
|
-
## Types and assertions
|
|
11
|
-
|
|
12
|
-
| Rule | What it rejects |
|
|
13
|
-
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
14
|
-
| [no-chained-type-assertions](no-chained-type-assertions.md) | Two or more assertions stacked on one expression, unless all are `as const`. |
|
|
15
|
-
| [no-known-value-widening](no-known-value-widening.md) | A value the file already describes flowing into `unknown`, `object` or an open dictionary. |
|
|
16
|
-
| [no-object-parameters](no-object-parameters.md) | A parameter annotated `object`, alias included. |
|
|
17
|
-
| [no-runtime-typeof](no-runtime-typeof.md) | `typeof`, except when probing for an absent binding with `"undefined"`. |
|
|
18
|
-
| [no-unknown-parameters](no-unknown-parameters.md) | An `unknown` parameter, except `cause` and a type predicate's subject. |
|
|
19
|
-
| [no-unknown-returns](no-unknown-returns.md) | A return contract of `unknown` or `Promise<unknown>`. |
|
|
20
|
-
| [no-unknown-type-aliases](no-unknown-type-aliases.md) | An alias whose resolved type is `unknown`. |
|
|
21
|
-
| [no-unsafe-dictionary-type](no-unsafe-dictionary-type.md) | A dictionary whose value type is `unknown`, `any`, `object` or `{}`. |
|
|
22
|
-
| [no-widen-then-assert](no-widen-then-assert.md) | A local const widened and then asserted back in the same function. |
|
|
23
|
-
| [require-safety-comment-for-type-assertion](require-safety-comment-for-type-assertion.md) | A non-const assertion with no `SAFETY:` justification before it. |
|
|
24
|
-
|
|
25
|
-
## Code patterns
|
|
26
|
-
|
|
27
|
-
| Rule | What it rejects |
|
|
28
|
-
| --------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
29
|
-
| [no-array-filter-map](no-array-filter-map.md) | Adjacent `.filter()` and `.map()` passes on a known array. |
|
|
30
|
-
| [no-conditional-empty-object-spread](no-conditional-empty-object-spread.md) | A spread of a conditional with `{}` on one branch. |
|
|
31
|
-
| [no-module-mocking](no-module-mocking.md) | `vi.mock`, `jest.mock` and their variants. |
|
|
32
|
-
| [no-reduce-accumulator-copy](no-reduce-accumulator-copy.md) | A reducer that copies its accumulator on every step. |
|
|
33
|
-
| [no-reflect-apply](no-reflect-apply.md) | `Reflect.apply` on the global `Reflect`. |
|
|
34
|
-
| [no-reflect-get](no-reflect-get.md) | `Reflect.get` on the global `Reflect`. |
|
|
35
|
-
| [no-shape-in-symbol-names](no-shape-in-symbol-names.md) | The substring `shape` in a name you own. |
|
|
36
|
-
|
|
37
|
-
## Framework rules
|
|
38
|
-
|
|
39
|
-
| Rule | What it rejects |
|
|
40
|
-
| ------------------------------------------------- | ------------------------------------------------------------- |
|
|
41
|
-
| [no-use-effect](no-use-effect.md) | Every `useEffect` and `useLayoutEffect` call or value import. |
|
|
42
|
-
| [no-hardcoded-jsx-text](no-hardcoded-jsx-text.md) | Words written straight into JSX children. |
|
|
43
|
-
| [static-public-env](static-public-env.md) | Every `process.env` use but a static `process.env.NAME` read. |
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# rdj/no-array-filter-map
|
|
2
|
-
|
|
3
|
-
Rejects a `.filter()` and a `.map()` pass placed back to back on a receiver the file already
|
|
4
|
-
shows to be an array.
|
|
5
|
-
|
|
6
|
-
## Why
|
|
7
|
-
|
|
8
|
-
Two adjacent passes walk the data twice and allocate an array nobody reads. That intermediate
|
|
9
|
-
array is invisible in review: the reader has to hold both callbacks in mind to see what one
|
|
10
|
-
pass would have produced. The rule fires only on local array evidence, an array literal, an
|
|
11
|
-
annotation of `T[]`, `readonly T[]`, `Array<T>` or `ReadonlyArray<T>`, or a `const` alias of
|
|
12
|
-
one, so an unknown receiver and an iterator pipeline stay out of its way.
|
|
13
|
-
|
|
14
|
-
## Rejected
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
const users = [];
|
|
18
|
-
users.filter(active).map(email);
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
> Avoid consecutive array `filter` and `map` passes. Prefer
|
|
22
|
-
> `.values().filter(...).map(...).toArray()` where iterator helpers are supported, or a single
|
|
23
|
-
> `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
function collect(users: readonly User[]) {
|
|
27
|
-
return users.map(email).filter(present);
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
> Avoid consecutive array `map` and `filter` passes. Prefer
|
|
32
|
-
> `.values().map(...).filter(...).toArray()` where iterator helpers are supported, or a single
|
|
33
|
-
> `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.
|
|
34
|
-
|
|
35
|
-
## Accepted
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
const users = [];
|
|
39
|
-
users.values().filter(active).map(email).toArray();
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
const users = [];
|
|
44
|
-
users.flatMap((user) => (user.active ? [user.email] : []));
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Options
|
|
48
|
-
|
|
49
|
-
None.
|
|
50
|
-
|
|
51
|
-
## When it is right to turn it off
|
|
52
|
-
|
|
53
|
-
Nothing needs an exception. On a runtime without iterator helpers, Hermes for instance, the
|
|
54
|
-
message's first suggestion does not apply, but `flatMap` and a plain loop both satisfy the
|
|
55
|
-
rule, so the right move is to fix the call and note the runtime beside the severity rather
|
|
56
|
-
than scope the rule off.
|
|
57
|
-
|
|
58
|
-
## Origin
|
|
59
|
-
|
|
60
|
-
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# rdj/no-chained-type-assertions
|
|
2
|
-
|
|
3
|
-
Rejects two or more type assertions stacked on one expression, unless every one of them is
|
|
4
|
-
`as const`.
|
|
5
|
-
|
|
6
|
-
## Why
|
|
7
|
-
|
|
8
|
-
`input as unknown as User` exists to make the compiler stop objecting. The first assertion
|
|
9
|
-
throws away what the value was, the second invents what it now is, and nothing checked the
|
|
10
|
-
value in between. A reader sees a named type and has no way to tell whether anything ever
|
|
11
|
-
validated it. Parenthesized and angle bracket spellings are the same move, so the rule counts
|
|
12
|
-
through both.
|
|
13
|
-
|
|
14
|
-
## Rejected
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
const value = input as unknown as User;
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
> This assertion chain discards type evidence. Keep the original precise type, or parse
|
|
21
|
-
> untrusted input at its boundary before narrowing it.
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
const value = { id: 1 } as const as User;
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
> This assertion chain discards type evidence. Keep the original precise type, or parse
|
|
28
|
-
> untrusted input at its boundary before narrowing it.
|
|
29
|
-
|
|
30
|
-
## Accepted
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
const value = input as User;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
const value = { id: 1 } as const as const;
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Options
|
|
41
|
-
|
|
42
|
-
None.
|
|
43
|
-
|
|
44
|
-
## When it is right to turn it off
|
|
45
|
-
|
|
46
|
-
Nothing legitimate needs a chain. One assertion is still allowed, so the fix is to drop the
|
|
47
|
-
laundering step and let `rdj/require-safety-comment-for-type-assertion` ask what the remaining
|
|
48
|
-
assertion knows.
|
|
49
|
-
|
|
50
|
-
## Origin
|
|
51
|
-
|
|
52
|
-
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# rdj/no-conditional-empty-object-spread
|
|
2
|
-
|
|
3
|
-
Rejects a spread inside an object literal whose argument is a conditional with `{}` on either
|
|
4
|
-
branch.
|
|
5
|
-
|
|
6
|
-
## Why
|
|
7
|
-
|
|
8
|
-
`...(value !== undefined ? { value } : {})` is how a field becomes optional without anyone
|
|
9
|
-
saying so. The object's keys can only be read by evaluating the conditional, so a reviewer
|
|
10
|
-
counting the keys of the literal gets the wrong answer. The rule ships no autofix on purpose,
|
|
11
|
-
and a test asserts that: any rewrite changes which keys the object ends up with.
|
|
12
|
-
|
|
13
|
-
## Rejected
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
const result = { ...(value !== undefined ? { value } : {}) };
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
> This conditional spread hides property omission behind an empty object. Build the object in
|
|
20
|
-
> separate statements and add the property only when present.
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
const result = { ...(condition ? {} : { value }) };
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
> This conditional spread hides property omission behind an empty object. Build the object in
|
|
27
|
-
> separate statements and add the property only when present.
|
|
28
|
-
|
|
29
|
-
## Accepted
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
const result = { value };
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
const result = condition ? { value } : {};
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Options
|
|
40
|
-
|
|
41
|
-
None.
|
|
42
|
-
|
|
43
|
-
## When it is right to turn it off
|
|
44
|
-
|
|
45
|
-
Nothing needs it off. Building the object in statements and adding the property inside an `if`
|
|
46
|
-
produces the same object and states the omission where a reader looks for it.
|
|
47
|
-
|
|
48
|
-
## Origin
|
|
49
|
-
|
|
50
|
-
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|