/** * The slice of ESTree/ESLint this plugin needs, declared locally. * * Importing `eslint`'s own types would put `@types/eslint` in the way of every consumer of the * published `.d.ts`, for a plugin most of them never load. The shapes below are the ones the rules * actually read, and the guards narrow to them at runtime — the same check ESLint itself performs. */ /** Where a node begins and ends, in the coordinates a message quotes back to a reader. */ interface EsSourceLocation { start: { line: number; }; end: { line: number; }; } /** A comment, as `SourceCode#getAllComments()` returns it — `value` without its `//` or `/* … *\/`. */ interface EsComment { value: string; loc: { start: { line: number; column: number; }; end: { line: number; column: number; }; }; } /** Any ESTree node, as ESLint hands it to a rule. */ interface EsNode { type: string; /** ESLint sets this on every node a rule can visit (only `Program` has none, and no rule here visits it). */ parent: EsNode; /** Character offsets in the source text. ESLint guarantees them on every node; a fixer is nothing without them. */ range: [number, number]; /** The same position in lines, which is the only form a message can name a *different* node in. */ loc: EsSourceLocation; } /** One declaration of a variable, as the scope manager recorded it. */ interface EsVariableDefinition { /** `'ImportBinding'` for a name an import introduced, `'Variable'` for a `const` / `let`, and so on. */ type: string; node: EsNode; } /** One mention of a variable. `writeExpr` is set for the ones that assign to it, initialisers included. */ interface EsReference { identifier: EsNode; writeExpr?: EsNode | null; } /** A name the scope manager resolved, with everything that declares and mentions it. */ interface EsVariable { name: string; defs: EsVariableDefinition[]; /** Type positions count: `Mocked` is a reference to `Mocked`, which is what makes an orphan detectable. */ references: EsReference[]; } /** A lexical scope, and the chain out of it. */ interface EsScope { variables: EsVariable[]; upper: EsScope | null; } /** One edit. ESLint sorts and merges the array a fixer returns, and refuses overlapping ranges. */ interface EsFix { range: [number, number]; text: string; } /** The sliver of ESLint's fixer the fixes here need. */ interface EsFixer { replaceText(node: EsNode, text: string): EsFix; replaceTextRange(range: [number, number], text: string): EsFix; insertTextBeforeRange(range: [number, number], text: string): EsFix; remove(node: EsNode): EsFix; } /** What a rule hands ESLint to perform an edit; `null` when it turns out there is nothing to do. */ type FixFunction = (fixer: EsFixer) => EsFix | EsFix[] | null; /** * An edit ESLint offers but never applies on its own. * * `desc` rather than `messageId`: a suggestion is read in a one-line editor menu, and the messages * of this plugin all carry a recipe URL that has no business there. */ interface SuggestionDescriptor { desc: string; fix: FixFunction; } /** What a rule passes to `context.report`. */ interface ReportDescriptor { node: EsNode; /** Where to report instead of `node` — a comment, which an `eslint-disable-next-line` above it can then reach. */ loc?: EsComment['loc']; messageId: string; /** Values for the `{{placeholders}}` of the message. */ data?: Record; fix?: FixFunction; suggest?: SuggestionDescriptor[]; } /** The sliver of ESLint's `SourceCode` the rules read. */ interface EsSourceCode { /** The scope a node sits in. Available since ESLint 8.37 — flat config is well past that. */ getScope(node: EsNode): EsScope; /** Every comment of the file, in source order. */ getAllComments(): EsComment[]; /** The source of one node, or — with no argument — of the whole file. */ getText(node?: EsNode): string; /** * The token after a node, comments skipped. * * Declared as always returning one, which is true of every call made here: the only node this is * asked about is an array element that another element of the same array follows, so the token is * the comma between them. */ getTokenAfter(node: EsNode): EsNode; /** * The token before a node, comments skipped. * * Declared as always returning one for the same reason: the only node this is asked about is the * first argument of a call, so the token is that call's opening parenthesis. */ getTokenBefore(node: EsNode): EsNode; /** * What the parser published. ESLint always sets the property — it is `{}` for a parser that * publishes nothing — so a rule that needs types can tell the difference and stay silent rather * than guess. */ readonly parserServices: ParserServices; /** * The child keys of every node type, as ESLint's own traversal uses them. * * The table the parser ships merged over ESLint's; a downward walk that reads it visits the * children and nothing else, where enumerating a node's own properties also pays for `range`, * `loc`, the type annotations a TypeScript node carries and the `parent` back-reference on each * of them. */ readonly visitorKeys: Readonly>; } /** * A TypeScript AST node, in the one shape this plugin reads off it. * * Declared rather than imported, for the same reason ESLint's own types are: `typescript` would * then sit in the way of every consumer of the published `.d.ts`, for a plugin most of them never * load. A modifier is read as **text**, not as a `ts.SyntaxKind`, which is what makes that possible * and is the more durable of the two anyway — the numbers move between TypeScript releases, * `private` does not. */ interface TsNode { readonly modifiers?: readonly { getText(): string; }[]; } /** The one thing this plugin asks a TypeScript type: what it holds, and — for a literal — what it is. */ interface TsType { getProperty(name: string): { readonly declarations?: readonly TsNode[]; } | undefined; /** Present on a literal type only; a string here is what makes `obj['x']` a named member read. */ readonly value?: unknown; } /** * ESTree → TypeScript. `@typescript-eslint/parser` fills it for every node it converted. * * The reverse map exists too and is not used here: it covers the **file being linted** alone, so it * answers nothing for the class under test, which lives in another file nine times out of ten. */ interface EsToTsMap { get(node: EsNode): TsNode; } /** * The `@typescript-eslint/parser` services a type-aware rule needs. * * Both members are optional because the property is also `{}` under a parser that publishes none — * espree, or `@typescript-eslint/parser` with neither `project` nor `projectService`. A rule reads * both or neither. */ interface ParserServices { readonly program?: { getTypeChecker(): { getTypeAtLocation(node: TsNode): TsType; }; }; readonly esTreeNodeToTSNodeMap?: EsToTsMap; } /** The sliver of ESLint's rule context the rules use. */ interface RuleContext { readonly sourceCode: EsSourceCode; /** Whatever the ESLint config passed after the severity, validated against `meta.schema`. */ readonly options: readonly unknown[]; report(descriptor: ReportDescriptor): void; } /** * Visitor map returned by `create`, keyed by node type or esquery selector. * * The parameter is `never` so that each visitor may declare the concrete node type its selector * guarantees — a runtime re-check of what the selector already matched would be an untestable * branch, which is exactly what this library's own 100% coverage gate forbids. */ type RuleListener = Record void>; /** An ESLint rule module, as `plugin.rules[name]`. */ interface RuleModule { meta: { type: 'problem' | 'suggestion'; docs: { description: string; url: string; }; messages: Record; /** JSON Schema for the rule's options — the empty list for a rule that takes none. */ schema: readonly object[]; /** Present only where the rule ships a fix — ESLint refuses one from a rule that has not said so. */ fixable?: 'code'; /** The same declaration for suggestions, which ESLint gates separately. */ hasSuggestions?: boolean; }; create(context: RuleContext): RuleListener; } /** * `vitest-auto-spy/eslint-plugin` — the lint rules that keep a suite on the library's API. * * ```js * // eslint.config.js (flat config) * import autoSpy from 'vitest-auto-spy/eslint-plugin'; * * export default [ * { files: ['**\/*.spec.ts'], ...autoSpy.configs.recommended }, * ]; * ``` * * Scope the config to spec files yourself: every rule here is about test code, and `Object. * defineProperty` or an object of `vi.fn()`s is perfectly reasonable in application code. * * `configs.typeErrors` is the second config: the subset whose findings are compile errors, to spread * back over a blanket downgrade while a large suite is being adopted. * * Flat config only. The legacy `.eslintrc` `plugins: ['…']` form resolves plugin names to * `eslint-plugin-*` packages, which a subpath export of this package can never be. */ /** Severity map shared by the shipped configs. */ type RuleSeverity = 'error' | 'off' | 'warn'; /** A flat-config object: the plugin under its name, plus the rules it turns on. */ interface FlatConfig { plugins: Record; rules: Record; } /** The plugin object, as ESLint consumes it. */ interface AutoSpyEslintPlugin { rules: Record; configs: { recommended: FlatConfig; typeErrors: FlatConfig; }; } declare const plugin: AutoSpyEslintPlugin; export { type AutoSpyEslintPlugin, type FlatConfig, type RuleSeverity, plugin as default };