Files
Sebastian Poxhofer e0cec3dc95 refactor(lint): add types-location rule and enable it for lib/modules (#45497)
feat(lint): add `types-location` rule and enable it for `lib/modules`

Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.

Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.

Relocating the 87 existing violations means:

- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
  else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
  a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
  now live in `parser/types.ts` and infer through a namespace import so
  each schema keeps its merged name
2026-08-27 19:52:09 +00:00

261 lines
6.4 KiB
TypeScript

import * as fragments from './fragments.ts';
import type {
AllFragments,
ArrayFragment,
CtxCompatible,
ExtensionTagFragment,
PreparedExtensionTagFragment,
RepoRuleCallFragment,
ResultFragment,
RuleFragment,
UseRepoRuleFragment,
} from './types.ts';
export class CtxProcessingError extends Error {
readonly current: AllFragments;
readonly parent?: AllFragments;
constructor(current: AllFragments, parent?: AllFragments) {
const msg = `Invalid context state. current: ${current.type}, parent: ${
parent?.type ?? 'none'
}`;
super(msg);
this.name = 'CtxProcessingError';
this.current = current;
this.parent = parent;
}
}
export class Ctx implements CtxCompatible {
readonly source: string;
results: ResultFragment[];
stack: AllFragments[];
// Temporary state for use_repo_rule parsing
_tempVariableName?: string;
_tempStrings?: string[];
constructor(source: string) {
this.source = source;
this.results = [];
this.stack = [];
}
private get safeCurrent(): AllFragments | undefined {
return this.stack.at(-1);
}
private get current(): AllFragments {
const c = this.safeCurrent;
if (c === undefined) {
throw new Error('Requested current, but no value.');
}
return c;
}
private get currentRule(): RuleFragment {
const current = this.current;
if (current.type === 'rule') {
return current;
}
throw new Error('Requested current rule, but does not exist.');
}
private get currentExtensionTag(): ExtensionTagFragment {
const current = this.current;
if (current.type === 'extensionTag') {
return current;
}
throw new Error('Requested current extension tag, but does not exist.');
}
private get currentUseRepoRule(): UseRepoRuleFragment {
const current = this.current;
if (current.type === 'useRepoRule') {
return current;
}
throw new Error('Requested current use repo rule, but does not exist.');
}
private get currentRepoRuleCall(): RepoRuleCallFragment {
const current = this.current;
if (current.type === 'repoRuleCall') {
return current;
}
throw new Error('Requested current repo rule call, but does not exist.');
}
private get currentArray(): ArrayFragment {
const current = this.current;
if (current.type === 'array') {
return current;
}
throw new Error('Requested current array, but does not exist.');
}
private popPreparedExtensionTag(): PreparedExtensionTagFragment {
const c = this.stack.pop();
if (c === undefined) {
throw new Error('Requested current, but no value.');
}
if (c.type === 'preparedExtensionTag') {
return c;
}
throw new Error(
'Requested current prepared extension tag, but does not exist.',
);
}
private popStack(): boolean {
const current = this.stack.pop();
if (!current) {
return false;
}
if (!current.isComplete) {
this.stack.push(current);
return false;
}
const parent = this.safeCurrent;
if (parent) {
if (parent.type === 'attribute' && fragments.isValue(current)) {
parent.value = current;
parent.isComplete = true;
return true;
}
if (parent.type === 'array' && fragments.isPrimitive(current)) {
parent.items.push(current);
return true;
}
// v8 ignore else -- TODO: add test #40625
if (
(parent.type === 'rule' ||
parent.type === 'extensionTag' ||
parent.type === 'repoRuleCall') &&
current.type === 'attribute' &&
current.value !== undefined
) {
parent.children[current.name] = current.value;
return true;
}
} else if (
current.type === 'rule' ||
current.type === 'extensionTag' ||
current.type === 'useRepoRule' ||
current.type === 'repoRuleCall'
) {
this.results.push(current);
return true;
}
throw new CtxProcessingError(current, parent);
}
private processStack(): Ctx {
while (this.popStack()) {
// Nothing to do
}
return this;
}
addString(value: string): Ctx {
this.stack.push(fragments.string(value));
return this.processStack();
}
addBoolean(value: string | boolean): Ctx {
this.stack.push(fragments.boolean(value));
return this.processStack();
}
startRule(name: string): Ctx {
const rule = fragments.rule(name);
this.stack.push(rule);
return this;
}
endRule(): Ctx {
const rule = this.currentRule;
rule.isComplete = true;
return this.processStack();
}
prepareExtensionTag(
extension: string,
rawExtension: string,
offset: number,
): Ctx {
const preppedTag = fragments.preparedExtensionTag(
extension,
rawExtension,
offset,
);
this.stack.push(preppedTag);
return this;
}
startExtensionTag(tag: string): Ctx {
const { extension, rawExtension, offset } = this.popPreparedExtensionTag();
const extensionTag = fragments.extensionTag(
extension,
rawExtension,
tag,
offset,
);
this.stack.push(extensionTag);
return this;
}
endExtensionTag(offset: number): Ctx {
const tag = this.currentExtensionTag;
tag.isComplete = true;
tag.rawString = this.source.slice(tag.offset, offset);
return this.processStack();
}
startUseRepoRule(
variableName: string,
bzlFile: string,
ruleName: string,
): Ctx {
const useRepoRule = fragments.useRepoRule(variableName, bzlFile, ruleName);
this.stack.push(useRepoRule);
return this;
}
endUseRepoRule(): Ctx {
const useRepoRule = this.currentUseRepoRule;
useRepoRule.isComplete = true;
return this.processStack();
}
startRepoRuleCall(functionName: string, offset: number): Ctx {
const repoRuleCall = fragments.repoRuleCall(functionName, offset);
this.stack.push(repoRuleCall);
return this;
}
endRepoRuleCall(offset: number): Ctx {
const repoRuleCall = this.currentRepoRuleCall;
repoRuleCall.isComplete = true;
repoRuleCall.rawString = this.source.slice(repoRuleCall.offset, offset);
return this.processStack();
}
startAttribute(name: string): Ctx {
this.stack.push(fragments.attribute(name));
return this.processStack();
}
startArray(): Ctx {
this.stack.push(fragments.array());
return this.processStack();
}
endArray(): Ctx {
const array = this.currentArray;
array.isComplete = true;
return this.processStack();
}
}