mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
6e03e8326f
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules Flag ad-hoc type checks that have @sindresorhus/is equivalents: - .filter(Boolean) -> .filter(isTruthy) - typeof x === 'string' -> isString(x) - x === null || x === undefined -> isNullOrUndefined(x) (and negation) The typeof x === 'object' shape lives in a separate suggestion-only rule (renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject() are not drop-in equivalents: null, arrays and functions behave differently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: fix renovate/prefer-is-helpers violations Replace ad-hoc type checks with @sindresorhus/is helpers across lib/: - 13x .filter(Boolean) -> .filter(isTruthy) - 36x typeof x ==/!== 'string' -> isString(x) / !isString(x) - 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation) Promote renovate/prefer-is-helpers to error now that the codebase is clean. renovate/prefer-is-object stays at warn: its 15 remaining sites guard parsed YAML/JSON or unknown values where isObject()/isPlainObject() are not provably equivalent (null/array/function semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: enforce renovate/prefer-is-object at error level Resolve the 15 remaining warnings and raise the rule from warn to error: - Migrate 10 sites to isObject() where the guarded value provably cannot be a function (parsed YAML/JSON, typed exec command arguments, or test-controlled fixtures): helm-values, woodpecker, bun, pre-commit managers, exec utils, file package cache, and the github http spec. In the bun manager this also fixes a latent crash where a null `workspaces` value would throw on the `in` operator. - Keep 5 sites as reasoned oxlint-disable comments where excluding functions is load-bearing: the byte-exact fingerprint canonicalizer (2), the circularity walker, the zod result guard, and the generic hasKey helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): add renovate/inline-single-use-fixtures rule In spec files, a module-scope const fixture that is referenced from exactly one it()/test() block should be inlined into that test so the test is self-contained. The rule is a warn-level nudge (naming for clarity is a valid exception) with no autofix (moving code is risky). Uses oxlint jsPlugins scope analysis (getDeclaredVariables) for exact, shadowing-safe reference counting and bails out conservatively: only top-level non-exported single-declarator consts with multiline, non-function initializers whose every reference sits inside the callback function of one and the same test call are flagged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: inline single-use spec fixtures into their tests Fix 26 renovate/inline-single-use-fixtures warnings in specs where the move is mechanical and value-preserving: codeBlock fixtures were re-indented uniformly (codeBlock strips the common indentation, so the produced string is unchanged) and Fixtures.get() calls resolve relative to the spec file, so relocating them inside the test body has no effect. The remaining 59 warnings are left for per-site judgment, as the rule is a warn-level nudge by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: inline remaining relocation-safe single-use spec fixtures Move the 24 remaining single-use module-scope fixtures whose initializer is a codeBlock tagged template or a Fixtures accessor call into the one it() block that uses them. Both shapes are value-preserving to relocate: codeBlock strips the common leading indentation, so the uniformly re-indented templates produce identical strings, and Fixtures.get() resolves relative to the spec file, not the call site's position in it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): enforce inline-single-use-fixtures as error Narrow the rule to the initializer shapes where moving the declaration into the test body is provably value-preserving and unambiguously an improvement: codeBlock/stripIndent tagged templates (indentation- normalizing, so re-indenting is safe) and Fixtures accessor calls (resolve relative to the spec file). Plain object/array literals, untagged template literals (whose leading-newline formatting would need ugly re-indentation) and arbitrary call expressions remain per-site judgment calls and are no longer flagged. With the rule reduced to true defects and all remaining violations inlined, bump it from warn to error in the spec override block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: inline contents of fixtures relocated into single-use tests The previous commits moved single-use module-scope Fixtures.get() consts into their one it() block, which only relocated the file indirection. Replace those relocated Fixtures.get() calls with the fixture file's actual content — codeBlock tagged templates for text fixtures — so the tests are self-contained, and delete the fixture files that no longer have any references. Relocated fixtures too large to inline comfortably (terraform-provider releases, sbt private-variable file, nuget v2 pkg list, bundled package-lock) keep their Fixtures.get() calls. Value preservation was verified by unchanged vitest results for every touched spec; all consumers parse the content, so codeBlock's trailing newline normalization is behavior-neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: inline remaining large single-use fixtures Inline the four Fixtures.get() sites that were previously exempted for size, so every single-use fixture is now inlined regardless of size: - nuget v2_withoutProjectUrl.xml -> codeBlock (served via http mock) - terraform-provider releaseBackendIndexGoogleBeta.json -> plain object literal passed to the nock reply - npm locked-dependency bundled.package-lock.json -> codeBlock JSON string (consumed as raw lockFileContent) - sbt private-variable-dependency-file.scala -> codeBlock All consumers parse the content, so codeBlock's trailing-newline normalization is behavior-neutral; vitest results are unchanged for every touched spec, including the two pre-existing nuget failures. The nuget, terraform-provider and sbt fixture files have no remaining references and are deleted. bundled.package-lock.json is kept because package-lock/get-locked.spec.ts still reads it via Fixtures.getJson(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: keep large bundled package-lock fixture in a file The bundled package-lock.json inlined into the "fails remediation if bundled" test is ~150 KB (~4,400 lines); inlining it as a codeBlock hurts readability more than it helps. Read it via Fixtures.get inside the test block instead — the inline-single-use-fixtures rule permits a Fixtures.get() call in test scope, and the fixture file is still present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
221 lines
7.7 KiB
TypeScript
221 lines
7.7 KiB
TypeScript
import type { ESTree } from '@oxlint/plugins';
|
|
import { defineRule } from '@oxlint/plugins';
|
|
|
|
/**
|
|
* Require inlining module-scope `const` fixtures that are referenced from
|
|
* exactly one `it()` / `test()` block ("inline to test if only used once").
|
|
*
|
|
* Keeping a fixture next to the only test that uses it makes the test
|
|
* self-contained and easier to read. To make this enforceable at "error"
|
|
* severity, the rule is deliberately narrow: it only fires on initializer
|
|
* shapes where moving the declaration into the test body is always
|
|
* value-preserving and unambiguously an improvement. Everything else
|
|
* (plain object/array literals, untagged template literals whose
|
|
* leading-newline formatting would need ugly re-indentation, arbitrary
|
|
* call expressions, ...) is a per-site judgment call and stays unflagged.
|
|
*
|
|
* A declaration is only flagged when ALL of the following hold:
|
|
*
|
|
* - It is a top-level (module-scope), non-exported `const` with a single
|
|
* declarator binding a plain identifier. Destructuring patterns and
|
|
* multi-declarator statements are skipped: "inline this binding" has no
|
|
* obvious mechanical meaning for them.
|
|
* - The initializer spans multiple source lines. Single-line consts used
|
|
* once are often still clearer named (e.g. `const timeout = 60_000`), and
|
|
* inlining them saves nothing.
|
|
* - The initializer is one of the relocation-safe fixture shapes:
|
|
* - a template literal tagged with `codeBlock` or `stripIndent` (the
|
|
* helpers this repo's `codeblock-in-spec-fixtures` rule mandates):
|
|
* both strip the common leading indentation, so the declaration can be
|
|
* re-indented to any nesting level without changing the produced
|
|
* string, or
|
|
* - a `Fixtures.get()` / `Fixtures.getPath()` / `Fixtures.getBinary()` /
|
|
* `Fixtures.getJson()` / `Fixtures.getJsonc()` call: these resolve
|
|
* relative to the spec file (not the call site's position in it), so
|
|
* relocating the call into the test body has no effect.
|
|
* - Every reference lives inside the callback function of one and the same
|
|
* `it()` / `test()` call (including `.only` / `.skip` / `.each(...)` and
|
|
* similar chained variants).
|
|
*
|
|
* The reference check uses scope analysis (`getDeclaredVariables`), so
|
|
* shadowing is handled inherently: only true references to the module-scope
|
|
* binding are counted. Anything that is not clearly "inside the one test
|
|
* callback" bails out — false negatives are fine, false positives are not.
|
|
* In particular, the rule stays silent when the const is referenced:
|
|
*
|
|
* - in `describe` / `beforeEach` / `afterEach` / `beforeAll` / `afterAll`
|
|
* bodies or any other non-test code (module-scope helper functions,
|
|
* other module-scope declarations, `vi.mock()` factories, ...),
|
|
* - in a test *title* (`` it(`parses ${fixture}`, ...) ``) or in an
|
|
* `it.each(table)` argument table — only references inside a function
|
|
* argument of the test call count,
|
|
* - from more than one test, or from zero tests.
|
|
*/
|
|
|
|
const TEST_NAMES = new Set(['it', 'test']);
|
|
|
|
/** Indentation-normalizing `common-tags` helpers: safe to re-indent. */
|
|
const RELOCATABLE_TAGS = new Set(['codeBlock', 'stripIndent']);
|
|
|
|
/** `Fixtures` accessors that resolve relative to the spec file. */
|
|
const FIXTURES_METHODS = new Set([
|
|
'get',
|
|
'getPath',
|
|
'getBinary',
|
|
'getJson',
|
|
'getJsonc',
|
|
]);
|
|
|
|
/**
|
|
* Whether the initializer is one of the shapes that can be moved into the
|
|
* test body mechanically without changing the produced value: an
|
|
* indentation-stripping tagged template, or a `Fixtures` accessor call.
|
|
*/
|
|
function isRelocatableInitializer(init: ESTree.Expression): boolean {
|
|
if (
|
|
init.type === 'TaggedTemplateExpression' &&
|
|
init.tag.type === 'Identifier' &&
|
|
RELOCATABLE_TAGS.has(init.tag.name)
|
|
) {
|
|
return true;
|
|
}
|
|
return (
|
|
init.type === 'CallExpression' &&
|
|
init.callee.type === 'MemberExpression' &&
|
|
!init.callee.computed &&
|
|
init.callee.object.type === 'Identifier' &&
|
|
init.callee.object.name === 'Fixtures' &&
|
|
init.callee.property.type === 'Identifier' &&
|
|
FIXTURES_METHODS.has(init.callee.property.name)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve the base identifier name of a (possibly chained) callee, e.g.
|
|
* `it` -> `it`, `it.only` -> `it`, `it.each(...)` -> `it`,
|
|
* `test.failing.each(...)` -> `test`.
|
|
*/
|
|
function getCalleeBaseName(callee: ESTree.Node): string | null {
|
|
let current = callee;
|
|
for (;;) {
|
|
switch (current.type) {
|
|
case 'Identifier':
|
|
return current.name;
|
|
case 'MemberExpression':
|
|
current = current.object;
|
|
break;
|
|
case 'CallExpression':
|
|
current = current.callee;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the `it()` / `test()` call whose callback function contains the
|
|
* given identifier reference. Returns `null` when the reference is not
|
|
* inside a function argument of a test call (bail-out).
|
|
*/
|
|
function findEnclosingTestCall(
|
|
identifier: ESTree.Node,
|
|
): ESTree.CallExpression | null {
|
|
let child: ESTree.Node = identifier;
|
|
for (
|
|
let current: ESTree.Node | null = child.parent;
|
|
current;
|
|
child = current, current = current.parent
|
|
) {
|
|
if (
|
|
current.type !== 'CallExpression' ||
|
|
!TEST_NAMES.has(getCalleeBaseName(current.callee) ?? '')
|
|
) {
|
|
continue;
|
|
}
|
|
// The reference must sit inside a function argument (the test
|
|
// callback), not e.g. in the title template or an `.each` table.
|
|
if (
|
|
current.arguments.some((arg) => arg === child) &&
|
|
(child.type === 'ArrowFunctionExpression' ||
|
|
child.type === 'FunctionExpression')
|
|
) {
|
|
return current;
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export default defineRule({
|
|
meta: {
|
|
type: 'suggestion',
|
|
messages: {
|
|
inlineSingleUseFixture:
|
|
'Fixture `{{name}}` is only used in a single test. Move its declaration into that `{{testName}}()` block.',
|
|
},
|
|
},
|
|
createOnce(context) {
|
|
return {
|
|
before() {
|
|
const filename = context.filename ?? context.physicalFilename ?? '';
|
|
return filename.endsWith('.spec.ts');
|
|
},
|
|
VariableDeclaration(node) {
|
|
if (
|
|
node.parent.type !== 'Program' ||
|
|
node.kind !== 'const' ||
|
|
node.declarations.length !== 1
|
|
) {
|
|
return;
|
|
}
|
|
const declarator = node.declarations[0];
|
|
if (declarator.id.type !== 'Identifier' || !declarator.init) {
|
|
return;
|
|
}
|
|
// Only relocation-safe fixture shapes (see the doc comment above):
|
|
// anything else is a per-site judgment call, not an error.
|
|
if (!isRelocatableInitializer(declarator.init)) {
|
|
return;
|
|
}
|
|
// Only multiline initializers: one-line consts are often clearer
|
|
// named, and inlining them saves nothing.
|
|
const initLoc = declarator.init.loc;
|
|
if (initLoc.end.line <= initLoc.start.line) {
|
|
return;
|
|
}
|
|
|
|
const variables = context.sourceCode.getDeclaredVariables(node);
|
|
if (variables.length !== 1) {
|
|
return;
|
|
}
|
|
const references = variables[0].references.filter((ref) => !ref.init);
|
|
if (references.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let testCall: ESTree.CallExpression | null = null;
|
|
for (const reference of references) {
|
|
const enclosing = findEnclosingTestCall(reference.identifier);
|
|
if (!enclosing || (testCall && enclosing !== testCall)) {
|
|
return;
|
|
}
|
|
testCall = enclosing;
|
|
}
|
|
if (!testCall) {
|
|
return;
|
|
}
|
|
|
|
context.report({
|
|
node: declarator.id,
|
|
messageId: 'inlineSingleUseFixture',
|
|
data: {
|
|
name: declarator.id.name,
|
|
testName: getCalleeBaseName(testCall.callee) ?? 'it',
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
});
|