Claude Skill

install-anti-slop

Install and configure the generic and optional Effect anti-slop Oxlint plugins in a local TypeScript or JavaScript repository. Use whenever a user asks to add anti-slop lint rules, copy the anti-slop plugin, configure opinionated Oxlint rules, or migrate an existing local anti-sl

LLM Mart · 0 points · 10 views 14 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dmmulroy-anti-slop-skills_install-anti-slop-c44ef22.zip · 56 KB

Install

skills CLI npx skills add https://github.com/dmmulroy/anti-slop/tree/main/skills/install-anti-slop
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dmmulroy-anti-slop@llmmart
Git git clone https://github.com/dmmulroy/anti-slop.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dmmulroy/anti-slop collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Install or update anti-slop

Anti-slop is vendored code: the target repository owns its rules, diagnostics, tests, and configuration. Preserve those choices when bringing in upstream changes.

Choose the path

Read the repository's agent instructions and git status. Identify its package manager, Oxlint/Vite+ configuration, and any existing anti-slop entry points, including renamed or relocated copies referenced by jsPlugins.

  • Existing installation — update, upgrade, migrate, or reconfigure: read Update a vendored installation and follow that procedure instead of the fresh-install steps below.
  • No installation — fresh install: follow the procedure below. If the user requested an update but no installation can be found, confirm the target before installing.

Complete when the operation and target path are established and pre-existing work is identified.

Fresh install

  1. Copy the bundled plugin from this skill. Run from the target repository:

    node <skill-directory>/scripts/install.mjs
    

    This creates tools/oxlint/anti-slop/. Pass another relative destination as the first argument when the repository has an established tooling layout. The script refuses to replace an existing destination; route existing copies through the update procedure rather than --force.

    Preserve the nested vendor/eslint-stylistic/LICENSE and UPSTREAM.md; they travel with the copied rule. Readability enforcement is self-contained and requires no Stylistic plugin dependency.

    Complete when the files, including vendored license and provenance, exist at the agreed destination without replacing an existing copy.

  2. Install current compatible dependencies rather than trusting versions remembered by the agent:

    • If the repository already depends on oxlint, read its installed version from the package manager or lockfile and install @oxlint/plugins at exactly that version. Pin it exactly rather than by range so future upgrades move both packages together.
    • Only when the repository has no oxlint dependency, query npm view oxlint version and npm view @oxlint/plugins version, then install the same current version of both packages.
    • oxlint is a development dependency. The copied source imports @oxlint/plugins, so install it as a development dependency for a local-only plugin.
    • Do not replace the package manager or rewrite unrelated dependency ranges.

    Complete when matching compatible versions are installed and unrelated dependency ranges are preserved.

  3. Register the generic plugin, configure ignores, and enable all generic rules. For oxlint.config.ts or .oxlintrc.json, merge these fields with the existing configuration:

    ignorePatterns: [
      ".agent/**",
      ".agents/**",
      ".claude/**",
      ".codex/**",
      ".continue/**",
      ".cursor/**",
      ".gemini/**",
      ".opencode/**",
      ".pi/**",
      ".roo/**",
      ".windsurf/**",
      "tools/oxlint/anti-slop/**",
    ],
    jsPlugins: [
      { name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
    ],
    

    Keep every existing ignore. Adjust the final pattern when the plugin was copied elsewhere. Inspect the repository for other project-local agent tooling directories and add them rather than linting installed skills, hooks, or generated agent configuration as application source. Do not broadly ignore all dot-directories, because some repositories keep owned source or checks in them.

    For Vite+, add these fields to lint.ignorePatterns and lint.jsPlugins. Also merge the same patterns into fmt.ignorePatterns so vp check does not reformat installed agent assets or the vendored plugin. Merge existing entries instead of replacing them.

    Enable these rules at "error", including the native Oxlint companion rule:

    {
      "oxc/no-accumulating-spread": "error",
      "anti-slop/no-array-filter-map": "error",
      "anti-slop/no-reduce-accumulator-copy": "error",
      "anti-slop/no-chained-type-assertions": "error",
      "anti-slop/no-conditional-empty-object-spread": "error",
      "anti-slop/no-known-value-widening": "error",
      "anti-slop/no-module-mocking": "error",
      "anti-slop/no-object-parameters": "error",
      "anti-slop/no-reflect-apply": "error",
      "anti-slop/no-reflect-get": "error",
      "anti-slop/no-runtime-typeof": "error",
      "anti-slop/no-shape-in-symbol-names": "error",
      "anti-slop/no-unknown-parameters": "error",
      "anti-slop/no-unknown-returns": "error",
      "anti-slop/no-unknown-type-aliases": "error",
      "anti-slop/no-unsafe-dictionary-type": "error",
      "anti-slop/no-widen-then-assert": "error",
      "anti-slop/require-readable-spacing": "error",
      "anti-slop/require-safety-comment-for-type-assertion": "error"
    }
    

    For no-array-filter-map, prefer lazy .values().filter(...).map(...).toArray() pipelines only when the target runtime supports iterator helpers; otherwise use an appropriate single flatMap or locally mutating reducer. Review callback order, indexes, sparse arrays, thisArg, and filtering semantics rather than mechanically rewriting chains. Unknown receiver types are deliberately not inferred by this AST/scope rule.

    Pair no-reduce-accumulator-copy with native oxc/no-accumulating-spread: the custom rule catches supported non-spread copies such as Object.assign({}, acc, item), Array.from(acc), and array accumulator concat/slice calls. Mutating a fresh local accumulator is allowed; copying individual input items is also allowed. Named callbacks, indirect helpers, and nested accumulator properties are not fully analyzed, so do not claim all quadratic reducers are ruled out.

    If the repository declares effect in a package manifest, or the user explicitly requests Effect rules, also register the opt-in Effect plugin:

    jsPlugins: [
      {
        name: "anti-slop-effect",
        specifier: "./tools/oxlint/anti-slop/effect/index.ts",
      },
    ],
    rules: {
      "anti-slop-effect/no-manual-effect-error-tag": "error",
      "anti-slop-effect/no-manual-tag-comparison": "error",
      "anti-slop-effect/no-manual-tagged-construction": "error",
      "anti-slop-effect/no-service-constructor-imports": "error",
      "anti-slop-effect/prefer-effect-match": "error",
    },
    

    Merge these entries with the generic plugin configuration rather than replacing it. Do not enable the Effect plugin merely because Effect appears transitively in a lockfile; require a direct package-manifest dependency or an explicit user request. The rule covers relative project imports. Report package-alias imports as a current limitation rather than pretending they are enforced.

    Complete when the generic rules and eligible Effect rules are registered and existing configuration is preserved.

  4. Run the repository's lint command and typecheck. For Vite+, run the repository's full vp check command after adding both lint and format ignores. If findings appear in owned project source, report them and fix them only when the user asked for migration/cleanup. Do not suppress rules, weaken rule severity, add unsafe casts, or mechanically launder types to make lint pass.

    When cleanup is authorized, apply require-readable-spacing with lint autofix, then run the repository's formatter and lint again. Confirm a second fix/format pass leaves files unchanged. Keep whitespace fixes separate from semantic edits, preserve documentation attachment and overload groups, and do not enable an entire competing formatting preset.

    Complete when checks have run, fix/format stability has been verified for authorized cleanup, and every failure is resolved or reported with its diagnostics.

  5. Record provenance in UPSTREAM.md beside the vendored entry point: source repository, exact source commit or recoverable pristine snapshot when available, installed plugin paths, and intentional deviations. Verify that the revision identifies the actual copied assets; a package version or the current upstream HEAD alone is insufficient. If provenance cannot be established, record it as unknown rather than guessing.

    Review the final diff and report the installed path, source identity, dependency/configuration changes, and check results. Complete when the record and report describe the files actually installed and any remaining findings.

Files (anti-slop)
  • assets
    • anti-slop
      • effect
        • rules
          • no-manual-effect-error-tag.ts 1.3 KB
            import { defineRule } from "@oxlint/plugins";
            
            import {
            	isInsideBroadEffectHandler,
            	isReasonTagMember,
            	isTagMember,
            	tagMemberFromComparison,
            } from "../shared/tagged-values.ts";
            
            export const noManualEffectErrorTagRule = defineRule({
            	meta: {
            		type: "problem",
            		docs: {
            			description:
            				"Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler.",
            		},
            		messages: {
            			tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.",
            			reason:
            				"Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler.",
            		},
            	},
            	createOnce(context) {
            		return {
            			BinaryExpression(node) {
            				const tagMember = tagMemberFromComparison(node);
            				if (
            					tagMember === undefined ||
            					!isInsideBroadEffectHandler(node)
            				) {
            					return;
            				}
            				context.report({
            					node,
            					messageId: isReasonTagMember(tagMember) ? "reason" : "tag",
            				});
            			},
            			SwitchStatement(node) {
            				if (
            					!isTagMember(node.discriminant) ||
            					!isInsideBroadEffectHandler(node)
            				) {
            					return;
            				}
            				context.report({
            					node,
            					messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag",
            				});
            			},
            		};
            	},
            });
            
          • no-manual-tag-comparison.ts 1.1 KB
            import { defineRule } from "@oxlint/plugins";
            
            import {
            	isInsideBroadEffectHandler,
            	isTagMember,
            	tagMemberFromComparison,
            } from "../shared/tagged-values.ts";
            
            export const noManualTagComparisonRule = defineRule({
            	meta: {
            		type: "problem",
            		docs: {
            			description:
            				"Use Effect Match or Predicate helpers instead of manually branching on `_tag`.",
            		},
            		messages: {
            			manualComparison:
            				"Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.",
            			manualSwitch:
            				"Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`.",
            		},
            	},
            	createOnce(context) {
            		return {
            			BinaryExpression(node) {
            				if (
            					tagMemberFromComparison(node) === undefined ||
            					isInsideBroadEffectHandler(node)
            				) {
            					return;
            				}
            				context.report({ node, messageId: "manualComparison" });
            			},
            			SwitchStatement(node) {
            				if (
            					!isTagMember(node.discriminant) ||
            					isInsideBroadEffectHandler(node)
            				) {
            					return;
            				}
            				context.report({ node, messageId: "manualSwitch" });
            			},
            		};
            	},
            });
            
          • no-manual-tagged-construction.ts 995 B
            import { defineRule } from "@oxlint/plugins";
            
            import {
            	isMatchPatternObject,
            	isStringLiteral,
            	propertyName,
            } from "../shared/tagged-values.ts";
            
            export const noManualTaggedConstructionRule = defineRule({
            	meta: {
            		type: "problem",
            		docs: {
            			description:
            				"Construct tagged values with their existing Effect constructor instead of writing `_tag` manually.",
            		},
            		messages: {
            			manualConstruction:
            				"Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object.",
            		},
            	},
            	createOnce(context) {
            		return {
            			ObjectExpression(node) {
            				if (isMatchPatternObject(node)) return;
            				const tag = node.properties.find(
            					(property) =>
            						property.type === "Property" &&
            						propertyName(property) === "_tag" &&
            						isStringLiteral(property.value),
            				);
            				if (tag !== undefined) {
            					context.report({ node: tag, messageId: "manualConstruction" });
            				}
            			},
            		};
            	},
            });
            
          • no-service-constructor-imports.ts 1.6 KB
            import { defineRule } from "@oxlint/plugins";
            
            import type { ESTree } from "@oxlint/plugins";
            
            const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
            const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
            
            function isProjectLocalImport(source: string): boolean {
            	return source.startsWith("./") || source.startsWith("../");
            }
            
            function getImportedName(specifier: ESTree.ImportSpecifier): string {
            	if (specifier.imported.type === "Identifier") return specifier.imported.name;
            	return specifier.imported.value;
            }
            
            /** Keep dependency-bearing Effect service constructors local to their owning capability modules. */
            export const noServiceConstructorImportsRule = defineRule({
            	meta: {
            		type: "problem",
            		docs: {
            			description:
            				"Disallow project-local make<CapabilityName> imports outside test and spec files.",
            		},
            		messages: {
            			serviceConstructorImport:
            				'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
            		},
            	},
            	create(context) {
            		const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
            
            		return {
            			ImportDeclaration(node) {
            				if (isTestFile || !isProjectLocalImport(node.source.value)) return;
            
            				for (const specifier of node.specifiers) {
            					if (specifier.type !== "ImportSpecifier") continue;
            
            					const importedName = getImportedName(specifier);
            					if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) continue;
            
            					context.report({
            						node: specifier,
            						messageId: "serviceConstructorImport",
            						data: { name: importedName },
            					});
            				}
            			},
            		};
            	},
            });
            
          • prefer-effect-match.ts 1.5 KB
            import { defineRule, type ESTree } from "@oxlint/plugins";
            
            const equalityOperators = new Set(["==", "===", "!=", "!=="]);
            
            export const preferEffectMatchRule = defineRule({
            	meta: {
            		type: "problem",
            		docs: {
            			description:
            				"Use Match from Effect for chained literal ternaries over the same value.",
            		},
            		messages: {
            			preferMatch:
            				"Use Match from Effect instead of a chained literal ternary.",
            		},
            	},
            	createOnce(context) {
            		const isLiteral = (node: ESTree.Node): boolean =>
            			node.type === "Literal" ||
            			(node.type === "TemplateLiteral" && node.expressions.length === 0);
            
            		const comparedValue = (node: ESTree.Expression): string | undefined => {
            			if (
            				node.type !== "BinaryExpression" ||
            				!equalityOperators.has(node.operator)
            			) {
            				return undefined;
            			}
            			if (isLiteral(node.left)) return context.sourceCode.getText(node.right);
            			if (isLiteral(node.right)) return context.sourceCode.getText(node.left);
            			return undefined;
            		};
            
            		return {
            			ConditionalExpression(node) {
            				if (node.parent?.type === "ConditionalExpression") return;
            				const value = comparedValue(node.test);
            				if (value === undefined) return;
            
            				let alternate = node.alternate;
            				let literalChecks = 1;
            				while (alternate.type === "ConditionalExpression") {
            					if (comparedValue(alternate.test) !== value) return;
            					literalChecks += 1;
            					alternate = alternate.alternate;
            				}
            
            				if (literalChecks > 1) {
            					context.report({ node, messageId: "preferMatch" });
            				}
            			},
            		};
            	},
            });
            
        • shared
          • tagged-values.ts 3 KB
            import type { ESTree } from "@oxlint/plugins";
            
            const equalityOperators = new Set(["==", "===", "!=", "!=="]);
            const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]);
            
            export const isStringLiteral = (
            	node: ESTree.Node | null | undefined,
            ): node is ESTree.StringLiteral =>
            	node?.type === "Literal" && typeof node.value === "string";
            
            export const isTagMember = (
            	node: ESTree.Node | null | undefined,
            ): node is ESTree.MemberExpression =>
            	node?.type === "MemberExpression" &&
            	((!node.computed &&
            		node.property.type === "Identifier" &&
            		node.property.name === "_tag") ||
            		(node.computed &&
            			isStringLiteral(node.property) &&
            			node.property.value === "_tag"));
            
            export const tagMemberFromComparison = (
            	node: ESTree.BinaryExpression,
            ): ESTree.MemberExpression | undefined => {
            	if (!equalityOperators.has(node.operator)) return undefined;
            	if (isTagMember(node.left) && isStringLiteral(node.right)) return node.left;
            	if (isTagMember(node.right) && isStringLiteral(node.left)) return node.right;
            	return undefined;
            };
            
            const isBroadEffectCatchCall = (
            	node: ESTree.Node | null | undefined,
            ): node is ESTree.CallExpression =>
            	node?.type === "CallExpression" &&
            	node.callee.type === "MemberExpression" &&
            	node.callee.object.type === "Identifier" &&
            	node.callee.object.name === "Effect" &&
            	!node.callee.computed &&
            	node.callee.property.type === "Identifier" &&
            	broadEffectCatchMethods.has(node.callee.property.name);
            
            export const isInsideBroadEffectHandler = (node: ESTree.Node): boolean => {
            	let current: ESTree.Node | null | undefined = node.parent;
            	while (current !== null && current !== undefined) {
            		if (
            			current.type === "ArrowFunctionExpression" ||
            			current.type === "FunctionExpression"
            		) {
            			return (
            				isBroadEffectCatchCall(current.parent) &&
            				current.parent.arguments.includes(current)
            			);
            		}
            		current = current.parent;
            	}
            	return false;
            };
            
            export const isReasonTagMember = (node: ESTree.MemberExpression): boolean =>
            	node.object.type === "MemberExpression" &&
            	((!node.object.computed &&
            		node.object.property.type === "Identifier" &&
            		node.object.property.name === "reason") ||
            		(node.object.computed &&
            			isStringLiteral(node.object.property) &&
            			node.object.property.value === "reason"));
            
            export const propertyName = (
            	property: ESTree.ObjectProperty,
            ): string | undefined => {
            	if (!property.computed && property.key.type === "Identifier") {
            		return property.key.name;
            	}
            	if (
            		property.key.type === "Literal" &&
            		typeof property.key.value === "string"
            	) {
            		return property.key.value;
            	}
            	return undefined;
            };
            
            export const isMatchPatternObject = (node: ESTree.ObjectExpression): boolean => {
            	const call = node.parent;
            	if (call?.type !== "CallExpression" || !call.arguments.includes(node)) {
            		return false;
            	}
            	const callee = call.callee;
            	return (
            		callee.type === "MemberExpression" &&
            		callee.object.type === "Identifier" &&
            		callee.object.name === "Match" &&
            		!callee.computed &&
            		callee.property.type === "Identifier" &&
            		(callee.property.name === "when" || callee.property.name === "not")
            	);
            };
            
        • index.ts 991 B
          import { eslintCompatPlugin } from "@oxlint/plugins";
          
          import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.ts";
          import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts";
          import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts";
          import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
          import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts";
          
          /** Opt-in Oxlint rules for Effect service and Layer architecture. */
          const antiSlopEffectPlugin = eslintCompatPlugin({
          	meta: { name: "anti-slop-effect" },
          	rules: {
          		"no-manual-effect-error-tag": noManualEffectErrorTagRule,
          		"no-manual-tag-comparison": noManualTagComparisonRule,
          		"no-manual-tagged-construction": noManualTaggedConstructionRule,
          		"no-service-constructor-imports": noServiceConstructorImportsRule,
          		"prefer-effect-match": preferEffectMatchRule,
          	},
          });
          
          export default antiSlopEffectPlugin;
          
      • rules
        • no-array-filter-map.ts 1.4 KB
          import { defineRule } from "@oxlint/plugins";
          
          import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts";
          
          /** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
          export const noArrayFilterMapRule = defineRule({
            meta: {
              type: "suggestion",
              docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
              messages: {
                arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.",
              },
            },
            createOnce(context) {
              return {
                CallExpression(node) {
                  const outer = arrayMethodTarget(node.callee);
                  if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return;
                  const innerCall = unwrapArrayExpression(outer.object);
                  if (innerCall.type !== "CallExpression") return;
                  const inner = arrayMethodTarget(innerCall.callee);
                  if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return;
                  if (!isKnownArrayExpression(context.sourceCode, inner.object)) return;
                  context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
                },
              };
            },
          });
          
        • no-chained-type-assertions.ts 2.5 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree } from "@oxlint/plugins";
          
          type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
          
          function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
            return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
          }
          
          function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
            let current = expression;
            while (current.type === "ParenthesizedExpression") {
              current = current.expression;
            }
            return current;
          }
          
          function isConstAssertion(node: TypeAssertionExpression): boolean {
            const { typeAnnotation } = node;
            return (
              typeAnnotation.type === "TSTypeReference" &&
              typeAnnotation.typeName.type === "Identifier" &&
              typeAnnotation.typeName.name === "const"
            );
          }
          
          function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
            let current: ESTree.Expression = node;
            let parent = node.parent;
          
            while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
              current = parent;
              parent = parent.parent;
            }
          
            return !isTypeAssertionExpression(parent) || parent.expression !== current;
          }
          
          function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
            let assertionCount = 0;
            let hasNonConstAssertion = false;
            let current: ESTree.Expression = node;
          
            while (isTypeAssertionExpression(current)) {
              assertionCount += 1;
              hasNonConstAssertion ||= !isConstAssertion(current);
              current = unwrapParenthesizedExpression(current.expression);
            }
          
            return assertionCount > 1 && hasNonConstAssertion;
          }
          
          /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
          export const noChainedTypeAssertionsRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
              },
              messages: {
                chained:
                  "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
              },
            },
            createOnce(context) {
              const checkTypeAssertion = (node: TypeAssertionExpression) => {
                if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
                context.report({ node, messageId: "chained" });
              };
          
              return {
                TSAsExpression: checkTypeAssertion,
                TSTypeAssertion: checkTypeAssertion,
              };
            },
          });
          
        • no-conditional-empty-object-spread.ts 1.5 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree } from "@oxlint/plugins";
          
          function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
            let current = node;
            while (current.type === "ParenthesizedExpression") {
              current = current.expression;
            }
            return current;
          }
          
          function isEmptyObjectExpression(node: ESTree.Expression): boolean {
            return node.type === "ObjectExpression" && node.properties.length === 0;
          }
          
          function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
            const conditional = unwrapParentheses(node);
            return (
              conditional.type === "ConditionalExpression" &&
              (isEmptyObjectExpression(conditional.consequent) ||
                isEmptyObjectExpression(conditional.alternate))
            );
          }
          
          /** Ban conditional empty-object spreads without changing their omission semantics. */
          export const noConditionalEmptyObjectSpreadRule = defineRule({
            meta: {
              type: "suggestion",
              docs: {
                description:
                  "Disallow object spreads that conditionally spread an empty object to omit fields.",
              },
              messages: {
                avoid:
                  "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
              },
            },
            createOnce(context) {
              return {
                SpreadElement(node) {
                  if (node.parent.type !== "ObjectExpression") return;
          
                  if (isConditionalEmptyObjectSpread(node.argument)) {
                    context.report({ node, messageId: "avoid" });
                  }
                },
              };
            },
          });
          
        • no-known-value-widening.ts 13 KB
          import { defineRule } from "@oxlint/plugins";
          
          import {
          	classifyUnsafeDictionaryValue,
          	classifyWideningTarget,
          	createTypeEnvironment,
          	isKnownEvidenceExpression,
          	type TypeEnvironment,
          	type WideningTarget,
          } from "../shared/dictionary-types.ts";
          import {
          	containsUnknownType,
          	functionParameterBindingName,
          	functionParameterTypeAnnotation,
          } from "../shared/function-parameters.ts";
          import { resolveVariable } from "../shared/scope.ts";
          
          import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
          
          type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function;
          
          function unwrapExpression(expression: ESTree.Expression): ESTree.Expression {
          	let current = expression;
          	while (
          		current.type === "ParenthesizedExpression" ||
          		current.type === "TSAsExpression" ||
          		current.type === "TSSatisfiesExpression" ||
          		current.type === "TSTypeAssertion" ||
          		current.type === "TSNonNullExpression"
          	) {
          		current = current.expression;
          	}
          	return current;
          }
          
          function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
          	if (variable.defs.length !== 1) return null;
          	const [definition] = variable.defs;
          	return definition?.type === "Variable" && definition.node.type === "VariableDeclarator"
          		? definition.node
          		: null;
          }
          
          function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean {
          	return (
          		declarator.parent.type === "VariableDeclaration" &&
          		declarator.parent.kind === "const" &&
          		variable.references.every((reference) => reference.init || !reference.isWrite())
          	);
          }
          
          function hasKnownEvidence(
          	sourceCode: SourceCode,
          	expression: ESTree.Expression,
          	visitedVariables = new Set<Variable>(),
          ): boolean {
          	if (isKnownEvidenceExpression(expression)) return true;
          	const unwrapped = unwrapExpression(expression);
          	if (unwrapped.type !== "Identifier") return false;
          	const variable = resolveVariable(sourceCode, unwrapped);
          	if (variable === null || visitedVariables.has(variable)) return false;
          	const declarator = variableDeclarator(variable);
          	if (
          		declarator === null ||
          		declarator.init === null ||
          		!isStableConstVariable(variable, declarator)
          	) {
          		return false;
          	}
          	visitedVariables.add(variable);
          	return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
          }
          
          function isFunctionExpression(node: ESTree.Node): node is FunctionExpression {
          	return (
          		node.type === "ArrowFunctionExpression" ||
          		node.type === "FunctionDeclaration" ||
          		node.type === "FunctionExpression" ||
          		node.type === "TSDeclareFunction" ||
          		node.type === "TSEmptyBodyFunctionExpression"
          	);
          }
          
          function localFunctionForCall(
          	sourceCode: SourceCode,
          	callee: ESTree.Expression,
          ): FunctionExpression | null {
          	const unwrapped = unwrapExpression(callee);
          	if (isFunctionExpression(unwrapped)) return unwrapped;
          	if (unwrapped.type !== "Identifier") return null;
          	const variable = resolveVariable(sourceCode, unwrapped);
          	if (variable === null || variable.defs.length !== 1) return null;
          	const [definition] = variable.defs;
          	if (definition === undefined) return null;
          	if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) {
          		return definition.node;
          	}
          	if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") {
          		return null;
          	}
          	const initializer = definition.node.init;
          	if (initializer === null) return null;
          	const unwrappedInitializer = unwrapExpression(initializer);
          	return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null;
          }
          
          function variableTypeAnnotation(
          	sourceCode: SourceCode,
          	variable: Variable,
          ): ESTree.TSTypeAnnotation | null {
          	if (variable.defs.length !== 1) return null;
          	const [definition] = variable.defs;
          	if (definition === undefined) return null;
          	if (
          		definition.type === "Variable" &&
          		definition.node.type === "VariableDeclarator" &&
          		definition.node.id.type === "Identifier"
          	) {
          		return definition.node.id.typeAnnotation ?? null;
          	}
          	if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) {
          		return null;
          	}
          	const parameter = definition.node.params.find(
          		(candidate) =>
          			functionParameterBindingName(candidate, sourceCode) === variable.name,
          	);
          	return parameter === undefined ? null : (functionParameterTypeAnnotation(parameter) ?? null);
          }
          
          function hasInformativeType(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          ): boolean {
          	return classifyUnsafeDictionaryValue(type, environment) === null;
          }
          
          function hasKnownCallArgumentEvidence(
          	sourceCode: SourceCode,
          	expression: ESTree.Expression,
          	environment: TypeEnvironment,
          	visitedVariables = new Set<Variable>(),
          ): boolean {
          	if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") {
          		return hasKnownCallArgumentEvidence(
          			sourceCode,
          			expression.expression,
          			environment,
          			visitedVariables,
          		);
          	}
          	if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") {
          		return hasInformativeType(expression.typeAnnotation, environment);
          	}
          	if (expression.type === "TSSatisfiesExpression") {
          		return hasKnownCallArgumentEvidence(
          			sourceCode,
          			expression.expression,
          			environment,
          			visitedVariables,
          		);
          	}
          	if (expression.type === "CallExpression") {
          		const owner = localFunctionForCall(sourceCode, expression.callee);
          		const returnType = owner?.returnType?.typeAnnotation;
          		return returnType !== undefined && hasInformativeType(returnType, environment);
          	}
          	if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression);
          	const variable = resolveVariable(sourceCode, expression);
          	if (variable === null || visitedVariables.has(variable)) return false;
          	const annotation = variableTypeAnnotation(sourceCode, variable);
          	if (annotation !== null) {
          		return hasInformativeType(annotation.typeAnnotation, environment);
          	}
          	const declarator = variableDeclarator(variable);
          	if (
          		declarator === null ||
          		declarator.init === null ||
          		!isStableConstVariable(variable, declarator)
          	) {
          		return false;
          	}
          	visitedVariables.add(variable);
          	return hasKnownCallArgumentEvidence(
          		sourceCode,
          		declarator.init,
          		environment,
          		visitedVariables,
          	);
          }
          
          function typePredicateSubjectIndex(
          	sourceCode: SourceCode,
          	owner: FunctionExpression,
          ): number | null {
          	const predicate = owner.returnType?.typeAnnotation;
          	if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") {
          		return null;
          	}
          	const predicateParameterName = predicate.parameterName.name;
          	const index = owner.params.findIndex(
          		(parameter) =>
          			functionParameterBindingName(parameter, sourceCode) === predicateParameterName,
          	);
          	return index === -1 ? null : index;
          }
          
          function annotationTarget(
          	annotation: ESTree.TSTypeAnnotation | null | undefined,
          	environment: TypeEnvironment,
          ): WideningTarget | null {
          	return annotation === null || annotation === undefined
          		? null
          		: classifyWideningTarget(annotation.typeAnnotation, environment);
          }
          
          function enclosingFunction(node: ESTree.Node): FunctionExpression | null {
          	let current: ESTree.Node | null = node.parent;
          	while (current !== null && current.type !== "Program") {
          		if (
          			current.type === "ArrowFunctionExpression" ||
          			current.type === "FunctionDeclaration" ||
          			current.type === "FunctionExpression"
          		) {
          			return current;
          		}
          		current = current.parent;
          	}
          	return null;
          }
          
          function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string {
          	if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
          	if (key.type === "Literal") return String(key.value);
          	return sourceCode.getText(key);
          }
          
          function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string {
          	if (owner === null) return "anonymous function";
          	if (owner.id !== null) return owner.id.name;
          	const parent = owner.parent;
          	if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier")
          		return parent.id.name;
          	if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
          	return "anonymous function";
          }
          
          function isEmptyObjectExpression(expression: ESTree.Expression): boolean {
          	const unwrapped = unwrapExpression(expression);
          	return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
          }
          
          function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean {
          	return destination.kind === "open dictionary" || destination.kind === "generic container";
          }
          
          function hasParentAssertion(node: ESTree.Node): boolean {
          	return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
          }
          
          /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
          export const noKnownValueWideningRule = defineRule({
          	meta: {
          		type: "problem",
          		docs: {
          			description:
          				"Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
          		},
          		messages: {
          			widening:
          				"The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.",
          		},
          	},
          	createOnce(context) {
          		let environment: TypeEnvironment | null = null;
          
          		const reportFlow = (
          			expression: ESTree.Expression,
          			destination: WideningTarget | null,
          			subject: string,
          		) => {
          			if (destination === null) return;
          			if (
          				isDictionaryAccumulatorTarget(destination) &&
          				isEmptyObjectExpression(expression)
          			) {
          				return;
          			}
          			if (!hasKnownEvidence(context.sourceCode, expression)) return;
          			context.report({
          				node: expression,
          				messageId: "widening",
          				data: { subject, target: destination.kind },
          			});
          		};
          
          		const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) =>
          			environment === null ? null : annotationTarget(annotation, environment);
          
          		return {
          			Program(node) {
          				environment = createTypeEnvironment(
          					node,
          					context.sourceCode.visitorKeys,
          				);
          			},
          			VariableDeclarator(node) {
          				if (node.init === null || node.id.type !== "Identifier") return;
          				reportFlow(
          					node.init,
          					targetFromAnnotation(node.id.typeAnnotation),
          					`binding \`${node.id.name}\``,
          				);
          			},
          			PropertyDefinition(node) {
          				if (node.value === null) return;
          				reportFlow(
          					node.value,
          					targetFromAnnotation(node.typeAnnotation),
          					`property \`${sourceKeyName(context.sourceCode, node.key)}\``,
          				);
          			},
          			AccessorProperty(node) {
          				if (node.value === null) return;
          				reportFlow(
          					node.value,
          					targetFromAnnotation(node.typeAnnotation),
          					`property \`${sourceKeyName(context.sourceCode, node.key)}\``,
          				);
          			},
          			AssignmentExpression(node) {
          				if (node.operator !== "=" || node.left.type !== "Identifier") return;
          				const variable = resolveVariable(context.sourceCode, node.left);
          				if (variable === null) return;
          				const declarator = variableDeclarator(variable);
          				if (declarator === null || declarator.id.type !== "Identifier") return;
          				reportFlow(
          					node.right,
          					targetFromAnnotation(declarator.id.typeAnnotation),
          					`binding \`${declarator.id.name}\``,
          				);
          			},
          			CallExpression(node) {
          				if (environment === null) return;
          				const owner = localFunctionForCall(context.sourceCode, node.callee);
          				if (owner === null) return;
          				const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner);
          				if (parameterIndex === null) return;
          				const parameter = owner.params[parameterIndex];
          				const argument = node.arguments[parameterIndex];
          				if (parameter === undefined || argument === undefined || argument.type === "SpreadElement") {
          					return;
          				}
          				const parameterAnnotation = functionParameterTypeAnnotation(parameter);
          				if (
          					parameterAnnotation === null ||
          					parameterAnnotation === undefined ||
          					!containsUnknownType(parameterAnnotation.typeAnnotation)
          				) {
          					return;
          				}
          				if (
          					!hasKnownCallArgumentEvidence(
          						context.sourceCode,
          						argument,
          						environment,
          					)
          				) {
          					return;
          				}
          				context.report({
          					node: argument,
          					messageId: "widening",
          					data: {
          						subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``,
          						target: "unknown",
          					},
          				});
          			},
          			ReturnStatement(node) {
          				if (node.argument === null) return;
          				const owner = enclosingFunction(node);
          				reportFlow(
          					node.argument,
          					targetFromAnnotation(owner?.returnType),
          					`return value of \`${functionName(context.sourceCode, owner)}\``,
          				);
          			},
          			ArrowFunctionExpression(node) {
          				if (node.body.type === "BlockStatement") return;
          				reportFlow(
          					node.body,
          					targetFromAnnotation(node.returnType),
          					`return value of \`${functionName(context.sourceCode, node)}\``,
          				);
          			},
          			TSAsExpression(node) {
          				if (environment === null || hasParentAssertion(node)) return;
          				reportFlow(
          					node.expression,
          					classifyWideningTarget(node.typeAnnotation, environment),
          					"assertion",
          				);
          			},
          			TSTypeAssertion(node) {
          				if (environment === null || hasParentAssertion(node)) return;
          				reportFlow(
          					node.expression,
          					classifyWideningTarget(node.typeAnnotation, environment),
          					"assertion",
          				);
          			},
          		};
          	},
          });
          
        • no-module-mocking.ts 2.7 KB
          import { defineRule } from "@oxlint/plugins";
          
          import { resolveVariable } from "../shared/scope.ts";
          
          import type { ESTree, SourceCode } from "@oxlint/plugins";
          
          const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
          
          function importedName(node: ESTree.Node): string | null {
            if (node.type !== "ImportSpecifier") return null;
            return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
          }
          
          function isTestFrameworkObject(
            sourceCode: SourceCode,
            expression: ESTree.Expression,
          ): expression is ESTree.IdentifierReference {
            if (expression.type !== "Identifier") return false;
            if (
              (expression.name === "vi" || expression.name === "jest") &&
              sourceCode.isGlobalReference(expression)
            ) {
              return true;
            }
          
            const variable = resolveVariable(sourceCode, expression);
            if (variable === null || variable.defs.length === 0) {
              return expression.name === "vi" || expression.name === "jest";
            }
            return variable.defs.some((definition) => {
              if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
                return false;
              }
              const source = definition.parent.source.value;
              const name = importedName(definition.node);
              return (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest");
            });
          }
          
          function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean {
            if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
            if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
            const property = callee.property;
            const method = callee.computed
              ? property.type === "Literal" &&
                (property.value === "doMock" ||
                  property.value === "mock" ||
                  property.value === "unstable_mockModule")
                ? property.value
                : null
              : property.type === "Identifier"
                ? property.name
                : null;
            return method !== null && moduleMockMethods.has(method);
          }
          
          /** Ban test framework module mocking in favor of real dependency seams. */
          export const noModuleMockingRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.",
              },
              messages: {
                moduleMock:
                  "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
              },
            },
            createOnce(context) {
              return {
                CallExpression(node) {
                  if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
                  if (moduleMockCall(context.sourceCode, node.callee)) {
                    context.report({ node, messageId: "moduleMock" });
                  }
                },
              };
            },
          });
          
        • no-object-parameters.ts 2.6 KB
          import { defineRule } from "@oxlint/plugins";
          
          import type { ESTree } from "@oxlint/plugins";
          
          import {
          	functionParameterBindingName,
          	functionParameterTypeAnnotation,
          } from "../shared/function-parameters.ts";
          import {
          	createTypeAliasEnvironment,
          	resolvedTypeMatches,
          	type TypeAliasEnvironment,
          } from "../shared/type-alias-resolution.ts";
          type ParameterOwner =
          	| ESTree.ArrowFunctionExpression
          	| ESTree.Function
          	| ESTree.TSCallSignatureDeclaration
          	| ESTree.TSConstructSignatureDeclaration
          	| ESTree.TSConstructorType
          	| ESTree.TSFunctionType
          	| ESTree.TSMethodSignature;
          
          /** Ban the broad object type on function inputs, including local aliases to object. */
          export const noObjectParametersRule = defineRule({
          	meta: {
          		type: "problem",
          		docs: {
          			description:
          				"Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
          		},
          		messages: {
          			objectParameter:
          				"Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.",
          		},
          	},
          	createOnce(context) {
          		let environment: TypeAliasEnvironment | null = null;
          
          		const resolvesToObject = (type: ESTree.TSType): boolean =>
          			environment !== null &&
          			resolvedTypeMatches(type, environment, (resolved, matches) => {
          				if (resolved.type === "TSObjectKeyword") return true;
          				if (resolved.type === "TSParenthesizedType") {
          					return matches(resolved.typeAnnotation);
          				}
          				return (
          					resolved.type === "TSUnionType" && resolved.types.some(matches)
          				);
          			});
          
          		const checkParameters = (node: ParameterOwner) => {
          			for (const parameter of node.params) {
          				const annotation = functionParameterTypeAnnotation(parameter);
          				if (annotation === null || annotation === undefined) continue;
          				if (!resolvesToObject(annotation.typeAnnotation)) continue;
          				context.report({
          					node: annotation.typeAnnotation,
          					messageId: "objectParameter",
          					data: { parameter: functionParameterBindingName(parameter, context.sourceCode) },
          				});
          			}
          		};
          
          		return {
          			Program(node) {
          				environment = createTypeAliasEnvironment(
          					node,
          					context.sourceCode.visitorKeys,
          				);
          			},
          			ArrowFunctionExpression: checkParameters,
          			FunctionDeclaration: checkParameters,
          			FunctionExpression: checkParameters,
          			TSCallSignatureDeclaration: checkParameters,
          			TSConstructSignatureDeclaration: checkParameters,
          			TSConstructorType: checkParameters,
          			TSDeclareFunction: checkParameters,
          			TSEmptyBodyFunctionExpression: checkParameters,
          			TSFunctionType: checkParameters,
          			TSMethodSignature: checkParameters,
          		};
          	},
          });
          
        • no-reduce-accumulator-copy.ts 4.9 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
          
          import {
            arrayMethodTarget,
            isKnownArrayExpression,
            resolveArrayBinding,
            unwrapArrayExpression,
          } from "../shared/array-method.ts";
          
          function enclosingReducer(node: ESTree.Node) {
            let parent = node.parent;
            while (parent !== null) {
              if (parent.type === "FunctionDeclaration") return null;
              if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
                const callback = parent;
                let owner: ESTree.Node | null = callback.parent;
                while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
                if (owner?.type !== "CallExpression") return null;
                const method = arrayMethodTarget(owner.callee);
                const firstArgument = owner.arguments[0];
                if (
                  method === null || (method.name !== "reduce" && method.name !== "reduceRight") ||
                  owner.arguments.length > 2 || firstArgument === undefined ||
                  unwrapArrayExpression(firstArgument) !== callback
                ) return null;
                const firstParameter = callback.params[0];
                const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
                if (accumulator?.type !== "Identifier") return null;
                return { callback, accumulator, initialValue: owner.arguments[1] };
              }
              parent = parent.parent;
            }
            return null;
          }
          
          function referencesAccumulator(
            sourceCode: SourceCode,
            node: ESTree.Node,
            accumulator: Variable,
            visited = new Set<Variable>(),
          ): boolean {
            const variable = resolveArrayBinding(sourceCode, node);
            if (variable === null || visited.has(variable)) return false;
            if (variable === accumulator) return true;
            visited.add(variable);
            if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
            for (const definition of variable.defs) {
              if (
                definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
                definition.node.id.type === "Identifier" && definition.node.init !== null &&
                definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
              ) {
                return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
              }
            }
            return false;
          }
          
          function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean {
            node = unwrapArrayExpression(node);
            if (node.type !== "Identifier" || node.name !== name) return false;
            const variable = resolveArrayBinding(sourceCode, node);
            return variable === null || variable.defs.length === 0;
          }
          
          /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
          export const noReduceAccumulatorCopyRule = defineRule({
            meta: {
              type: "problem",
              docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
              messages: {
                accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
              },
            },
            createOnce(context) {
              return {
                CallExpression(node) {
                  const method = arrayMethodTarget(node.callee);
                  if (method === null) return;
                  const reducer = enclosingReducer(node);
                  if (reducer === null) return;
                  const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
                    variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
                  );
                  if (accumulator === undefined) return;
                  const isAccumulator = (expression: ESTree.Node) =>
                    referencesAccumulator(context.sourceCode, expression, accumulator);
                  let copiesAccumulator = false;
                  if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
                    const target = node.arguments[0];
                    copiesAccumulator = (
                      target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" &&
                      node.arguments.slice(1).some(isAccumulator)
                    );
                  } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
                    const source = node.arguments[0];
                    copiesAccumulator = source !== undefined && isAccumulator(source);
                  } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
                    const initialValue = reducer.initialValue;
                    const arrayAccumulator = initialValue !== undefined &&
                      isKnownArrayExpression(context.sourceCode, initialValue);
                    copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
                  }
                  if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" });
                },
              };
            },
          });
          
        • no-reflect-apply.ts 926 B
          import { defineRule } from "@oxlint/plugins";
          
          import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
          
          /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
          export const noReflectApplyRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
              },
              messages: {
                reflectApply:
                  "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.",
              },
            },
            createOnce(context) {
              return {
                CallExpression(node) {
                  if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
                  if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) {
                    context.report({ node, messageId: "reflectApply" });
                  }
                },
              };
            },
          });
          
        • no-reflect-get.ts 939 B
          import { defineRule } from "@oxlint/plugins";
          
          import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
          
          /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
          export const noReflectGetRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
              },
              messages: {
                reflectGet:
                  "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.",
              },
            },
            createOnce(context) {
              return {
                CallExpression(node) {
                  if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
                  if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) {
                    context.report({ node, messageId: "reflectGet" });
                  }
                },
              };
            },
          });
          
        • no-runtime-typeof.ts 2.3 KB
          import { defineRule } from "@oxlint/plugins";
          
          import type { ESTree } from "@oxlint/plugins";
          
          type RuntimeFunction = ESTree.ArrowFunctionExpression | ESTree.Function;
          
          function isRuntimeFunction(node: ESTree.Node): node is RuntimeFunction {
          	return (
          		node.type === "ArrowFunctionExpression" ||
          		node.type === "FunctionDeclaration" ||
          		node.type === "FunctionExpression"
          	);
          }
          
          function isInsideTypeGuard(node: ESTree.Node): boolean {
          	let current: ESTree.Node | null = node.parent;
          	while (current !== null && current.type !== "Program") {
          		if (isRuntimeFunction(current)) {
          			return current.returnType?.typeAnnotation.type === "TSTypePredicate";
          		}
          		current = current.parent;
          	}
          	return false;
          }
          
          /** Return whether typeof safely probes for the existence of a possibly absent binding. */
          function isExistenceProbe(node: ESTree.UnaryExpression): boolean {
          	const parent = node.parent;
          	if (parent.type !== "BinaryExpression") return false;
          	if (!["===", "!==", "==", "!="].includes(parent.operator)) return false;
          	const other = parent.left === node ? parent.right : parent.left;
          	return other.type === "Literal" && other.value === "undefined";
          }
          
          /** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
          export const noRuntimeTypeofRule = defineRule({
          	meta: {
          		type: "problem",
          		docs: {
          			description:
          				"Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
          		},
          		messages: {
          			runtimeTypeof:
          				"A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
          		},
          		schema: [
          			{
          				type: "object",
          				properties: {
          					allowInTypeGuards: { type: "boolean" },
          				},
          				additionalProperties: false,
          			},
          		],
          		defaultOptions: [{ allowInTypeGuards: false }],
          	},
          	createOnce(context) {
          		return {
          			UnaryExpression(node) {
          				const option = context.options?.[0];
          				const allowInTypeGuards =
          					typeof option === "object" &&
          					option !== null &&
          					!Array.isArray(option) &&
          					option.allowInTypeGuards === true;
          				if (
          					node.operator === "typeof" &&
          					!isExistenceProbe(node) &&
          					(!allowInTypeGuards || !isInsideTypeGuard(node))
          				) {
          					context.report({ node, messageId: "runtimeTypeof" });
          				}
          			},
          		};
          	},
          });
          
        • no-shape-in-symbol-names.ts 1.6 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree } from "@oxlint/plugins";
          
          const FORBIDDEN_SYMBOL_NAME = "shape";
          
          function containsForbiddenSymbolName(name: string): boolean {
            return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
          }
          
          /** Return whether an identifier names a statically accessed member owned by another value. */
          function isBorrowedMemberName(node: ESTree.Node): boolean {
            const parent = node.parent;
            if (parent === null || parent.type !== "MemberExpression") return false;
            return parent.property === node && parent.computed === false;
          }
          
          /** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
          export const noForbiddenTermInSymbolNamesRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.',
              },
              messages: {
                forbiddenSymbolName:
                  'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.',
              },
            },
            createOnce(context) {
              const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => {
                if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return;
                context.report({
                  node,
                  messageId: "forbiddenSymbolName",
                  data: { name: node.name },
                });
              };
          
              return {
                Identifier: reportForbiddenSymbolName,
                PrivateIdentifier: reportForbiddenSymbolName,
                JSXIdentifier: reportForbiddenSymbolName,
              };
            },
          });
          
        • no-unknown-parameters.ts 2.5 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree } from "@oxlint/plugins";
          
          import {
            containsUnknownType,
            functionParameterBindingName,
            functionParameterTypeAnnotation,
          } from "../shared/function-parameters.ts";
          type ParameterOwner =
            | ESTree.ArrowFunctionExpression
            | ESTree.Function
            | ESTree.TSCallSignatureDeclaration
            | ESTree.TSConstructSignatureDeclaration
            | ESTree.TSConstructorType
            | ESTree.TSFunctionType
            | ESTree.TSMethodSignature;
          
          function isTypePredicateSubject(owner: ParameterOwner, parameterName: string): boolean {
            const predicate = owner.returnType?.typeAnnotation;
            return (
              predicate?.type === "TSTypePredicate" &&
              predicate.parameterName.type === "Identifier" &&
              predicate.parameterName.name === parameterName
            );
          }
          
          /** Disallow unknown inputs except explicitly named error-cause enrichment. */
          export const noUnknownParametersRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead.",
              },
              messages: {
                unknownParameter:
                  "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.",
              },
            },
            createOnce(context) {
              const checkParameters = (node: ParameterOwner) => {
                for (const parameter of node.params) {
                  const annotation = functionParameterTypeAnnotation(parameter);
                  if (annotation === null || annotation === undefined) continue;
                  if (!containsUnknownType(annotation.typeAnnotation)) continue;
                  const name = functionParameterBindingName(parameter, context.sourceCode);
                  if (name === "cause" || isTypePredicateSubject(node, name)) continue;
                  context.report({
                    node: annotation.typeAnnotation,
                    messageId: "unknownParameter",
                    data: { parameter: name },
                  });
                }
              };
          
              return {
                ArrowFunctionExpression: checkParameters,
                FunctionDeclaration: checkParameters,
                FunctionExpression: checkParameters,
                TSCallSignatureDeclaration: checkParameters,
                TSConstructSignatureDeclaration: checkParameters,
                TSConstructorType: checkParameters,
                TSDeclareFunction: checkParameters,
                TSEmptyBodyFunctionExpression: checkParameters,
                TSFunctionType: checkParameters,
                TSMethodSignature: checkParameters,
              };
            },
          });
          
        • no-unknown-returns.ts 2.8 KB
          import { defineRule } from "@oxlint/plugins";
          
          import type { ESTree } from "@oxlint/plugins";
          
          import {
            createTypeAliasEnvironment,
            resolvedTypeMatches,
            type TypeAliasEnvironment,
          } from "../shared/type-alias-resolution.ts";
          
          type FunctionWithReturnType =
            | ESTree.ArrowFunctionExpression
            | ESTree.Function
            | ESTree.TSCallSignatureDeclaration
            | ESTree.TSConstructSignatureDeclaration
            | ESTree.TSConstructorType
            | ESTree.TSFunctionType
            | ESTree.TSMethodSignature;
          
          /** Ban function contracts that return unknown instead of a parsed domain type. */
          export const noUnknownReturnsRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
              },
              messages: {
                unknownReturn:
                  "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
              },
            },
            createOnce(context) {
              let environment: TypeAliasEnvironment | null = null;
          
              const resolvesToUnknown = (type: ESTree.TSType): boolean =>
                environment !== null &&
                resolvedTypeMatches(type, environment, (resolved, matches) => {
                  if (resolved.type === "TSUnknownKeyword") return true;
                  if (resolved.type === "TSParenthesizedType") {
                    return matches(resolved.typeAnnotation);
                  }
                  if (resolved.type === "TSUnionType") return resolved.types.some(matches);
                  if (
                    resolved.type !== "TSTypeReference" ||
                    resolved.typeName.type !== "Identifier" ||
                    (resolved.typeName.name !== "Promise" &&
                      resolved.typeName.name !== "PromiseLike")
                  ) {
                    return false;
                  }
                  const value = resolved.typeArguments?.params[0];
                  return value !== undefined && matches(value);
                });
          
              const checkReturnType = (node: FunctionWithReturnType) => {
                const annotation = node.returnType;
                if (annotation === null || annotation === undefined) return;
                if (!resolvesToUnknown(annotation.typeAnnotation)) return;
                context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" });
              };
          
              return {
                Program(node) {
                  environment = createTypeAliasEnvironment(
                    node,
                    context.sourceCode.visitorKeys,
                  );
                },
                ArrowFunctionExpression: checkReturnType,
                FunctionDeclaration: checkReturnType,
                FunctionExpression: checkReturnType,
                TSCallSignatureDeclaration: checkReturnType,
                TSConstructSignatureDeclaration: checkReturnType,
                TSConstructorType: checkReturnType,
                TSDeclareFunction: checkReturnType,
                TSEmptyBodyFunctionExpression: checkReturnType,
                TSFunctionType: checkReturnType,
                TSMethodSignature: checkReturnType,
              };
            },
          });
          
        • no-unknown-type-aliases.ts 1.5 KB
          import { defineRule } from "@oxlint/plugins";
          
          import type { ESTree } from "@oxlint/plugins";
          
          import {
          	createTypeAliasEnvironment,
          	resolvedTypeMatches,
          	type TypeAliasEnvironment,
          } from "../shared/type-alias-resolution.ts";
          
          /** Ban named aliases that merely conceal TypeScript's unknown top type. */
          export const noUnknownTypeAliasesRule = defineRule({
          	meta: {
          		type: "problem",
          		docs: {
          			description:
          				"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
          		},
          		messages: {
          			unknownAlias:
          				"Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.",
          		},
          	},
          	createOnce(context) {
          		let environment: TypeAliasEnvironment | null = null;
          
          		const resolvesToUnknown = (type: ESTree.TSType): boolean =>
          			environment !== null &&
          			resolvedTypeMatches(type, environment, (resolved, matches) => {
          				if (resolved.type === "TSUnknownKeyword") return true;
          				if (resolved.type === "TSParenthesizedType") {
          					return matches(resolved.typeAnnotation);
          				}
          				return resolved.type === "TSUnionType" && resolved.types.some(matches);
          			});
          
          		return {
          			Program(node) {
          				environment = createTypeAliasEnvironment(
          					node,
          					context.sourceCode.visitorKeys,
          				);
          			},
          			TSTypeAliasDeclaration(node) {
          				if (!resolvesToUnknown(node.typeAnnotation)) return;
          				context.report({
          					node: node.id,
          					messageId: "unknownAlias",
          					data: { alias: node.id.name },
          				});
          			},
          		};
          	},
          });
          
        • no-unsafe-dictionary-type.ts 4.5 KB
          import { defineRule } from "@oxlint/plugins";
          
          import {
          	classifyUnsafeDictionary,
          	classifyUnsafeDictionaryValue,
          	createTypeEnvironment,
          	type TypeEnvironment,
          } from "../shared/dictionary-types.ts";
          import { visibleTypeAlias } from "../shared/type-alias-resolution.ts";
          
          import type { ESTree } from "@oxlint/plugins";
          
          const typeNodeKinds: ReadonlySet<string> = new Set([
          	"JSDocNonNullableType",
          	"JSDocNullableType",
          	"JSDocUnknownType",
          	"TSAnyKeyword",
          	"TSArrayType",
          	"TSBigIntKeyword",
          	"TSBooleanKeyword",
          	"TSConditionalType",
          	"TSConstructorType",
          	"TSFunctionType",
          	"TSImportType",
          	"TSIndexedAccessType",
          	"TSInferType",
          	"TSIntersectionType",
          	"TSIntrinsicKeyword",
          	"TSLiteralType",
          	"TSMappedType",
          	"TSNamedTupleMember",
          	"TSNeverKeyword",
          	"TSNullKeyword",
          	"TSNumberKeyword",
          	"TSObjectKeyword",
          	"TSParenthesizedType",
          	"TSStringKeyword",
          	"TSSymbolKeyword",
          	"TSTemplateLiteralType",
          	"TSThisType",
          	"TSTupleType",
          	"TSTypeLiteral",
          	"TSTypeOperator",
          	"TSTypePredicate",
          	"TSTypeQuery",
          	"TSTypeReference",
          	"TSUndefinedKeyword",
          	"TSUnionType",
          	"TSUnknownKeyword",
          	"TSVoidKeyword",
          ]);
          
          function isTypeNode(node: ESTree.Node): node is ESTree.TSType {
          	return typeNodeKinds.has(node.type);
          }
          
          function typeReferenceName(type: ESTree.TSTypeReference): string | null {
          	return type.typeName.type === "Identifier" ? type.typeName.name : null;
          }
          
          function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean {
          	let current: ESTree.Node | null = node.parent;
          	while (current !== null && current.type !== "Program") {
          		if (current.type === "TSTypeAliasDeclaration") return true;
          		current = current.parent;
          	}
          	return false;
          }
          
          function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean {
          	if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
          	const name = typeReferenceName(node);
          	return (
          		name !== null &&
          		visibleTypeAlias(name, node, environment.typeAliases) !== null &&
          		!isInsideTypeAliasDeclaration(node)
          	);
          }
          
          function isInsideTypeParameterConstraint(node: ESTree.TSType): boolean {
          	let child: ESTree.Node = node;
          	let parent: ESTree.Node | null = child.parent;
          	while (parent !== null && parent.type !== "Program") {
          		if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
          		child = parent;
          		parent = child.parent;
          	}
          	return false;
          }
          
          function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean {
          	if (isInsideTypeParameterConstraint(node)) return false;
          	if (isPlainAliasConsumerUse(node, environment)) return false;
          	if (classifyUnsafeDictionary(node, environment) === null) return false;
          	let current: ESTree.Node | null = node.parent;
          	while (current !== null && current.type !== "Program") {
          		if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null)
          			return false;
          		current = current.parent;
          	}
          	return true;
          }
          
          /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
          export const noUnsafeDictionaryTypeRule = defineRule({
          	meta: {
          		type: "problem",
          		docs: {
          			description:
          				"Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
          		},
          		messages: {
          			unsafeDictionary:
          				"This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.",
          		},
          	},
          	createOnce(context) {
          		let environment: TypeEnvironment | null = null;
          		const report = (node: ESTree.Node, value: string) => {
          			context.report({ node, messageId: "unsafeDictionary", data: { value } });
          		};
          		const reportIfUnsafe = (node: ESTree.TSType) => {
          			if (environment === null || !shouldReportType(node, environment)) return;
          			const unsafe = classifyUnsafeDictionary(node, environment);
          			if (unsafe === null) return;
          			report(node, unsafe.unsafeValue);
          		};
          
          		return {
          			Program(node) {
          				environment = createTypeEnvironment(
          					node,
          					context.sourceCode.visitorKeys,
          				);
          			},
          			TSTypeReference: reportIfUnsafe,
          			TSTypeLiteral: reportIfUnsafe,
          			TSMappedType: reportIfUnsafe,
          			TSIndexSignature(node) {
          				if (
          					environment === null ||
          					node.typeAnnotation === null ||
          					node.parent.type === "TSTypeLiteral"
          				)
          					return;
          				const unsafe = classifyUnsafeDictionaryValue(
          					node.typeAnnotation.typeAnnotation,
          					environment,
          				);
          				if (unsafe !== null) report(node, unsafe.unsafeValue);
          			},
          		};
          	},
          });
          
        • no-widen-then-assert.ts 12 KB
          import { defineRule } from "@oxlint/plugins";
          import type { ESTree, Variable } from "@oxlint/plugins";
          
          type BroadTypeKind = "top" | "object" | "record";
          
          type KnownValueEvidence = {
            readonly type: ESTree.TSType | null;
          };
          
          const functionBoundaryTypes = new Set([
            "ArrowFunctionExpression",
            "FunctionDeclaration",
            "FunctionExpression",
            "TSDeclareFunction",
            "TSEmptyBodyFunctionExpression",
          ]);
          
          function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression {
            let current = expression;
            while (current.type === "ParenthesizedExpression") current = current.expression;
            return current;
          }
          
          function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType {
            let current = type;
            while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
            return current;
          }
          
          function typeReferenceName(type: ESTree.TSTypeReference): string | null {
            return type.typeName.type === "Identifier" ? type.typeName.name : null;
          }
          
          function isUnknownOrAnyType(type: ESTree.TSType): boolean {
            const unwrapped = unwrapTypeParentheses(type);
            return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
          }
          
          function isBroadRecordKeyType(type: ESTree.TSType): boolean {
            const unwrapped = unwrapTypeParentheses(type);
            if (
              unwrapped.type === "TSStringKeyword" ||
              unwrapped.type === "TSNumberKeyword" ||
              unwrapped.type === "TSSymbolKeyword"
            ) {
              return true;
            }
            if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
            return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
          }
          
          function isBroadRecordType(type: ESTree.TSType): boolean {
            const unwrapped = unwrapTypeParentheses(type);
          
            if (unwrapped.type === "TSTypeReference") {
              if (typeReferenceName(unwrapped) === "Readonly") {
                const [inner] = unwrapped.typeArguments?.params ?? [];
                return inner !== undefined && isBroadRecordType(inner);
              }
          
              if (typeReferenceName(unwrapped) !== "Record") return false;
              const parameters = unwrapped.typeArguments?.params ?? [];
              return (
                parameters.length === 2 &&
                parameters[0] !== undefined &&
                parameters[1] !== undefined &&
                isBroadRecordKeyType(parameters[0]) &&
                isUnknownOrAnyType(parameters[1])
              );
            }
          
            if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
            const [member] = unwrapped.members;
            const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
            return (
              member?.type === "TSIndexSignature" &&
              member.parameters.length === 1 &&
              parameter !== undefined &&
              isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
              isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
            );
          }
          
          function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null {
            const unwrapped = unwrapTypeParentheses(type);
            if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
            if (unwrapped.type === "TSObjectKeyword") return "object";
            return isBroadRecordType(unwrapped) ? "record" : null;
          }
          
          function assertedExpression(
            node: ESTree.TSAsExpression | ESTree.TSTypeAssertion,
          ): ESTree.Expression {
            return unwrapExpressionParentheses(node.expression);
          }
          
          function assertionFromExpression(
            expression: ESTree.Expression,
          ): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null {
            const unwrapped = unwrapExpressionParentheses(expression);
            return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
              ? unwrapped
              : null;
          }
          
          function normalizedTypeText(sourceText: string, type: ESTree.TSType): string {
            return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
          }
          
          function typesHaveSameSyntax(
            sourceText: string,
            left: ESTree.TSType | null,
            right: ESTree.TSType,
          ): boolean {
            return (
              left !== null &&
              normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
                normalizedTypeText(sourceText, unwrapTypeParentheses(right))
            );
          }
          
          function isDefinitelyObjectType(type: ESTree.TSType): boolean {
            const unwrapped = unwrapTypeParentheses(type);
            switch (unwrapped.type) {
              case "TSArrayType":
              case "TSConstructorType":
              case "TSFunctionType":
              case "TSMappedType":
              case "TSObjectKeyword":
              case "TSTupleType":
                return true;
              case "TSTypeLiteral":
                return unwrapped.members.length > 0;
              case "TSIntersectionType":
                return unwrapped.types.every(isDefinitelyObjectType);
              case "TSTypeOperator":
                return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
              default:
                return false;
            }
          }
          
          function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean {
            const unwrapped = unwrapTypeParentheses(type);
            if (unwrapped.type === "TSTypeLiteral") {
              return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
            }
          
            if (unwrapped.type !== "TSTypeReference") return false;
            if (typeReferenceName(unwrapped) === "Readonly") {
              const [inner] = unwrapped.typeArguments?.params ?? [];
              return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
            }
            if (typeReferenceName(unwrapped) !== "Record") return false;
          
            const parameters = unwrapped.typeArguments?.params ?? [];
            return (
              parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
            );
          }
          
          function functionBoundary(node: ESTree.Node): ESTree.Node | null {
            let current = node.parent;
            while (current !== null && current.type !== "Program") {
              if (functionBoundaryTypes.has(current.type)) return current;
              current = current.parent;
            }
            return null;
          }
          
          function resolvedVariableForIdentifier(
            scopes: readonly {
              readonly references: readonly {
                readonly identifier: ESTree.Node;
                readonly resolved: Variable | null;
              }[];
            }[],
            identifier: ESTree.IdentifierReference,
          ): Variable | null {
            for (const scope of scopes) {
              const reference = scope.references.find(
                (candidate) =>
                  candidate.identifier.start === identifier.start &&
                  candidate.identifier.end === identifier.end,
              );
              if (reference !== undefined) return reference.resolved;
            }
            return null;
          }
          
          function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
            for (const definition of variable.defs) {
              if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
                return definition.node;
              }
            }
            return null;
          }
          
          function knownValueEvidence(
            expression: ESTree.Expression,
            scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
            boundary: ESTree.Node | null,
            visitedVariables: ReadonlySet<Variable>,
          ): KnownValueEvidence | null {
            const unwrapped = unwrapExpressionParentheses(expression);
          
            if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
              if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
              return { type: unwrapped.typeAnnotation };
            }
          
            if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
              return { type: null };
            }
          
            if (
              unwrapped.type === "ArrayExpression" ||
              unwrapped.type === "ArrowFunctionExpression" ||
              unwrapped.type === "ClassExpression" ||
              unwrapped.type === "FunctionExpression" ||
              unwrapped.type === "NewExpression" ||
              unwrapped.type === "ObjectExpression"
            ) {
              return { type: null };
            }
          
            if (unwrapped.type !== "Identifier") return null;
            const variable = resolvedVariableForIdentifier(scopes, unwrapped);
            if (variable === null || visitedVariables.has(variable)) return null;
          
            const annotatedIdentifier = variable.identifiers.find(
              (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
            );
            const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
            if (annotation !== undefined && annotatedIdentifier !== undefined) {
              if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
                return null;
              }
              return { type: annotation };
            }
          
            const declarator = variableDeclarator(variable);
            if (
              declarator === null ||
              declarator.parent.type !== "VariableDeclaration" ||
              declarator.parent.kind !== "const" ||
              declarator.init === null ||
              variable.references.some((reference) => reference.isWrite() && !reference.init) ||
              functionBoundary(declarator) !== boundary
            ) {
              return null;
            }
          
            return knownValueEvidence(
              declarator.init,
              scopes,
              boundary,
              new Set([...visitedVariables, variable]),
            );
          }
          
          function widenedBinding(
            variable: Variable,
            scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
          ): {
            readonly broadKind: BroadTypeKind;
            readonly evidence: KnownValueEvidence;
            readonly declaredAt: number;
            readonly boundary: ESTree.Node | null;
          } | null {
            const declarator = variableDeclarator(variable);
            if (
              declarator === null ||
              declarator.parent.type !== "VariableDeclaration" ||
              declarator.parent.kind !== "const" ||
              declarator.id.type !== "Identifier" ||
              declarator.init === null ||
              variable.references.some((reference) => reference.isWrite() && !reference.init)
            ) {
              return null;
            }
          
            const boundary = functionBoundary(declarator);
            const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
            const initializerAssertion = assertionFromExpression(declarator.init);
            const initializerBroadKind =
              initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
            const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
            const broadKind = declaredBroadKind ?? initializerBroadKind;
            if (broadKind === null) return null;
          
            const originalExpression =
              initializerAssertion !== null && initializerBroadKind !== null
                ? assertedExpression(initializerAssertion)
                : declarator.init;
            const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
            return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
          }
          
          function assertionIsNarrower(
            sourceText: string,
            broadKind: BroadTypeKind,
            evidence: KnownValueEvidence,
            assertedType: ESTree.TSType,
          ): boolean {
            if (broadTypeKind(assertedType) !== null) return false;
            if (broadKind === "top") return true;
            if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
            if (broadKind === "object") return isDefinitelyObjectType(assertedType);
            return isDefinitelyNarrowerRecordType(assertedType);
          }
          
          /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
          export const noWidenThenAssertRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
              },
              messages: {
                widenThenAssert:
                  'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
              },
            },
            createOnce(context) {
              let scopes: Parameters<typeof resolvedVariableForIdentifier>[0] = [];
          
              const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => {
                const expression = assertedExpression(node);
                if (expression.type !== "Identifier") return;
          
                const variable = resolvedVariableForIdentifier(scopes, expression);
                if (variable === null) return;
                const widened = widenedBinding(variable, scopes);
                if (
                  widened === null ||
                  node.start <= widened.declaredAt ||
                  functionBoundary(node) !== widened.boundary ||
                  !assertionIsNarrower(
                    context.sourceCode.text,
                    widened.broadKind,
                    widened.evidence,
                    node.typeAnnotation,
                  )
                ) {
                  return;
                }
          
                context.report({
                  node,
                  messageId: "widenThenAssert",
                  data: { name: expression.name },
                });
              };
          
              return {
                Program() {
                  scopes = context.sourceCode.scopeManager.scopes;
                },
                TSAsExpression: checkAssertion,
                TSTypeAssertion: checkAssertion,
              };
            },
          });
          
        • require-readable-spacing.ts 1.8 KB
          import type { CreateRule } from "@oxlint/plugins";
          
          import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts";
          
          const paddingRule = createPaddingLineRule([
            { blankLine: "always", prev: "import", next: "*" },
            { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
            { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" },
            { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] },
            { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" },
            {
              blankLine: "always",
              prev: "*",
              next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
            },
            {
              blankLine: "always",
              prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
              next: "*",
            },
            { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
            { blankLine: "always", prev: "block-like", next: "*" },
            { blankLine: "any", prev: "import", next: "import" },
            {
              blankLine: "any",
              prev: {
                selector:
                  ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])',
              },
              next: {
                selector:
                  ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])',
              },
            },
          ]);
          
          /** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */
          export const requireReadableSpacingRule: CreateRule = {
            ...paddingRule,
            meta: {
              ...paddingRule.meta,
              docs: {
                description: "Require readable spacing between declarations and logical statement groups.",
              },
              schema: [],
            },
          };
          
        • require-safety-comment-for-type-assertion.ts 3.9 KB
          import { defineRule } from "@oxlint/plugins";
          
          import type { ESTree, SourceCode } from "@oxlint/plugins";
          
          type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
          
          const DEFAULT_SAFETY_MARKERS = ["SAFETY"] as const;
          
          const commentOwnerKinds = new Set([
            "ExpressionStatement",
            "PropertyDefinition",
            "ReturnStatement",
            "ThrowStatement",
            "VariableDeclaration",
          ]);
          
          function isConstAssertion(node: TypeAssertion): boolean {
            return (
              node.typeAnnotation.type === "TSTypeReference" &&
              node.typeAnnotation.typeName.type === "Identifier" &&
              node.typeAnnotation.typeName.name === "const"
            );
          }
          
          function configuredSafetyMarkers(option: unknown): readonly string[] {
            if (typeof option !== "object" || option === null || !("markers" in option)) {
              return DEFAULT_SAFETY_MARKERS;
            }
            const configured = option.markers;
            if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS;
            const markers = configured.flatMap((marker) =>
              typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : [],
            );
            return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS;
          }
          
          function markerPattern(markers: readonly string[]): RegExp {
            const alternation = markers
              .map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`))
              .join("|");
            return new RegExp(
              String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`,
              "u",
            );
          }
          
          function hasSafetyJustificationBefore(
            sourceCode: SourceCode,
            owner: ESTree.Node,
            assertion: TypeAssertion,
            pattern: RegExp,
          ): boolean {
            return sourceCode
              .getCommentsBefore(owner)
              .some(
                (comment) => comment.end <= assertion.start && pattern.test(comment.value),
              );
          }
          
          function hasSafetyComment(
            sourceCode: SourceCode,
            node: TypeAssertion,
            pattern: RegExp,
          ): boolean {
            let current: ESTree.Node = node;
            while (true) {
              if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true;
              if (commentOwnerKinds.has(current.type)) {
                const exportDeclaration = current.parent;
                return (
                  exportDeclaration.type === "ExportNamedDeclaration" &&
                  exportDeclaration.declaration === current &&
                  hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern)
                );
              }
              if (current.parent.type === "Program") return false;
              current = current.parent;
            }
          }
          
          /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
          export const requireSafetyCommentForTypeAssertionRule = defineRule({
            meta: {
              type: "problem",
              docs: {
                description:
                  "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
              },
              messages: {
                missingSafetyComment:
                  "This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.",
              },
              schema: [
                {
                  type: "object",
                  properties: {
                    markers: {
                      type: "array",
                      items: { type: "string", minLength: 1 },
                      minItems: 1,
                      uniqueItems: true,
                    },
                  },
                  additionalProperties: false,
                },
              ],
              defaultOptions: [{ markers: ["SAFETY"] }],
            },
            createOnce(context) {
              const patterns = new Map<string, RegExp>();
          
              const checkAssertion = (node: TypeAssertion) => {
                if (isConstAssertion(node)) return;
                const markers = configuredSafetyMarkers(context.options?.[0]);
                const patternKey = markers.join("\u0000");
                const pattern = patterns.get(patternKey) ?? markerPattern(markers);
                patterns.set(patternKey, pattern);
                if (hasSafetyComment(context.sourceCode, node, pattern)) return;
                context.report({
                  node,
                  messageId: "missingSafetyComment",
                  data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] },
                });
              };
          
              return {
                TSAsExpression: checkAssertion,
                TSTypeAssertion: checkAssertion,
              };
            },
          });
          
      • shared
        • array-method.ts 3.7 KB
          import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
          
          /** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
          export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node {
            while (
              node.type === "ParenthesizedExpression" ||
              node.type === "ChainExpression" ||
              node.type === "TSAsExpression" ||
              node.type === "TSTypeAssertion" ||
              node.type === "TSNonNullExpression" ||
              node.type === "TSSatisfiesExpression"
            ) {
              node = node.expression;
            }
            return node;
          }
          
          /** Resolve a local binding by scope, not by identifier spelling. */
          export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null {
            node = unwrapArrayExpression(node);
            if (node.type !== "Identifier") return null;
            let scope: Scope | null = sourceCode.getScope(node);
            while (scope !== null) {
              const variable = scope.set.get(node.name);
              if (variable !== undefined) return variable;
              scope = scope.upper;
            }
            return null;
          }
          
          /** Read static method names, including computed string literals, without evaluating expressions. */
          export function arrayMethodTarget(
            node: ESTree.Node,
          ): { readonly name: string; readonly object: ESTree.Node } | null {
            node = unwrapArrayExpression(node);
            if (node.type !== "MemberExpression") return null;
            const property = node.property;
            if (!node.computed && property.type === "Identifier") {
              return { name: property.name, object: node.object };
            }
            if (node.computed && property.type === "Literal" && typeof property.value === "string") {
              return { name: property.value, object: node.object };
            }
            return null;
          }
          
          function isArrayAnnotation(type: ESTree.TSType): boolean {
            if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
            if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
            if (type.type === "TSTypeOperator" && type.operator === "readonly") {
              return isArrayAnnotation(type.typeAnnotation);
            }
            return (
              type.type === "TSTypeReference" && type.typeName.type === "Identifier" &&
              (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray")
            );
          }
          
          /** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
          export function isKnownArrayExpression(
            sourceCode: SourceCode,
            node: ESTree.Node,
            visited = new Set<Variable>(),
          ): boolean {
            node = unwrapArrayExpression(node);
            if (node.type === "ArrayExpression") return true;
            if (node.type === "CallExpression") {
              const method = arrayMethodTarget(node.callee);
              return (
                method !== null &&
                ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) &&
                isKnownArrayExpression(sourceCode, method.object, visited)
              );
            }
            if (node.type !== "Identifier") return false;
            const variable = resolveArrayBinding(sourceCode, node);
            if (variable === null || visited.has(variable)) return false;
            visited.add(variable);
            if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
            for (const identifier of variable.identifiers) {
              const annotation = identifier.typeAnnotation?.typeAnnotation;
              if (annotation !== undefined) return isArrayAnnotation(annotation);
            }
            for (const definition of variable.defs) {
              if (
                definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
                definition.node.id.type === "Identifier" && definition.node.init !== null &&
                definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
              ) {
                return isKnownArrayExpression(sourceCode, definition.node.init, visited);
              }
            }
            return false;
          }
          
        • dictionary-types.ts 16.7 KB
          import type { ESTree } from "@oxlint/plugins";
          
          import {
          	createTypeAliasEnvironment,
          	hasVisibleTypeBinding,
          	visibleTypeAlias,
          	type TypeAliasEnvironment as LexicalTypeAliasEnvironment,
          } from "./type-alias-resolution.ts";
          
          const BUILT_INS = new Set([
          	"Record",
          	"Readonly",
          	"Partial",
          	"Required",
          	"Pick",
          	"Omit",
          	"PropertyKey",
          	"NonNullable",
          ]);
          const TRANSPARENT_WRAPPERS = new Set(["Readonly", "Partial", "Required", "NonNullable"]);
          
          type TypeAliasEnvironment = ReadonlyMap<string, ESTree.TSType>;
          
          type ResolvedType = {
          	readonly type: ESTree.TSType;
          	readonly substitutions: TypeAliasEnvironment;
          };
          
          export type UnsafeDictionary = {
          	readonly kind: "unsafe-dictionary";
          	readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
          };
          
          export type WideningTargetKind =
          	| "anonymous object"
          	| "generic container"
          	| "object"
          	| "open dictionary"
          	| "unknown";
          
          export type WideningTarget = {
          	readonly kind: WideningTargetKind;
          };
          
          export type TypeEnvironment = {
          	readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
          	readonly typeAliases: LexicalTypeAliasEnvironment;
          };
          
          function declaredStatement(statement: ESTree.Statement): ESTree.Node | null {
          	return statement.type === "ExportNamedDeclaration" ||
          		statement.type === "ExportDefaultDeclaration"
          		? (statement.declaration ?? null)
          		: statement;
          }
          
          export function createTypeEnvironment(
          	program: ESTree.Program,
          	visitorKeys: Readonly<Record<string, readonly string[]>>,
          ): TypeEnvironment {
          	const interfaces = new Map<string, ESTree.TSInterfaceDeclaration[]>();
          
          	for (const statement of program.body) {
          		const declaration = declaredStatement(statement);
          		if (declaration?.type !== "TSInterfaceDeclaration") continue;
          		const declarations = interfaces.get(declaration.id.name) ?? [];
          		declarations.push(declaration);
          		interfaces.set(declaration.id.name, declarations);
          	}
          
          	return {
          		interfaces,
          		typeAliases: createTypeAliasEnvironment(program, visitorKeys),
          	};
          }
          
          function typeReferenceName(type: ESTree.TSTypeReference): string | null {
          	return type.typeName.type === "Identifier" ? type.typeName.name : null;
          }
          
          function isBuiltIn(
          	name: string,
          	use: ESTree.Node,
          	environment: TypeEnvironment,
          ): boolean {
          	return (
          		BUILT_INS.has(name) &&
          		!hasVisibleTypeBinding(name, use, environment.typeAliases)
          	);
          }
          
          function isUnappliedReferenceTo(type: ESTree.TSType, name: string): boolean {
          	const unwrapped = unwrapTransparentType(type);
          	return (
          		unwrapped.type === "TSTypeReference" &&
          		typeReferenceName(unwrapped) === name &&
          		(unwrapped.typeArguments === null ||
          			unwrapped.typeArguments === undefined ||
          			unwrapped.typeArguments.params.length === 0)
          	);
          }
          
          function unwrapTransparentType(type: ESTree.TSType): ESTree.TSType {
          	let current = type;
          	while (
          		current.type === "TSParenthesizedType" ||
          		(current.type === "TSTypeOperator" && current.operator === "readonly")
          	) {
          		current = current.typeAnnotation;
          	}
          	return current;
          }
          
          function isNeverType(type: ESTree.TSType): boolean {
          	return unwrapTransparentType(type).type === "TSNeverKeyword";
          }
          
          function isEffectivelyEmptyMember(member: ESTree.TSSignature): boolean {
          	return (
          		member.type === "TSPropertySignature" &&
          		member.optional === true &&
          		member.typeAnnotation !== null &&
          		member.typeAnnotation !== undefined &&
          		isNeverType(member.typeAnnotation.typeAnnotation)
          	);
          }
          
          function isEffectivelyEmptyTypeLiteral(type: ESTree.TSTypeLiteral): boolean {
          	return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
          }
          
          function isEffectivelyEmptyInterface(
          	declarations: readonly ESTree.TSInterfaceDeclaration[],
          ): boolean {
          	if (declarations.length !== 1) return false;
          	const [type] = declarations;
          	return (
          		type !== undefined &&
          		type.extends.length === 0 &&
          		(type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember))
          	);
          }
          
          function resolvedSubstitutionArgument(
          	type: ESTree.TSType,
          	base: TypeAliasEnvironment,
          	resolving: ReadonlySet<string> = new Set(),
          ): ESTree.TSType {
          	const unwrapped = unwrapTransparentType(type);
          	if (unwrapped.type !== "TSTypeReference") return type;
          	const name = typeReferenceName(unwrapped);
          	if (name === null || resolving.has(name)) return type;
          	const substitution = base.get(name);
          	if (substitution === undefined) return type;
          	const nextResolving = new Set(resolving);
          	nextResolving.add(name);
          	return resolvedSubstitutionArgument(substitution, base, nextResolving);
          }
          
          function aliasSubstitution(
          	alias: ESTree.TSTypeAliasDeclaration,
          	type: ESTree.TSTypeReference,
          	base: TypeAliasEnvironment,
          ): TypeAliasEnvironment | null {
          	const parameters = alias.typeParameters?.params ?? [];
          	const arguments_ = type.typeArguments?.params ?? [];
          	const next = new Map(base);
          	for (const [index, parameter] of parameters.entries()) {
          		const argument = arguments_[index] ?? parameter.default;
          		if (argument === null || argument === undefined) return null;
          		next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
          	}
          	return next;
          }
          
          function unsafeDirectValue(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          	substitutions: TypeAliasEnvironment,
          	resolvingAliases: ReadonlySet<string>,
          ): UnsafeDictionary["unsafeValue"] | null {
          	const unwrapped = unwrapTransparentType(type);
          	if (unwrapped.type === "TSUnknownKeyword") return "unknown";
          	if (unwrapped.type === "TSAnyKeyword") return "any";
          	if (unwrapped.type === "TSObjectKeyword") return "object";
          	if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped))
          		return "empty-object";
          	if (unwrapped.type === "TSUnionType") {
          		return unwrapped.types.some(
          			(member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null,
          		)
          			? "union"
          			: null;
          	}
          	if (unwrapped.type === "TSIntersectionType") {
          		const unsafeMembers = unwrapped.types.map((member) =>
          			unsafeDirectValue(member, environment, substitutions, resolvingAliases),
          		);
          		if (unsafeMembers.includes("any")) return "any";
          		return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null)
          			? unsafeMembers[0]
          			: null;
          	}
          	if (unwrapped.type !== "TSTypeReference") return null;
          	const name = typeReferenceName(unwrapped);
          	if (name === null) return null;
          	if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
          		const wrapped = unwrapped.typeArguments?.params[0];
          		return wrapped === undefined
          			? null
          			: unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
          	}
          	const substitution = substitutions.get(name);
          	if (substitution !== undefined) {
          		return isUnappliedReferenceTo(substitution, name)
          			? null
          			: unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
          	}
          	const interfaceDeclarations = environment.interfaces.get(name);
          	if (interfaceDeclarations !== undefined) {
          		return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
          	}
          	const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
          	if (alias === null || resolvingAliases.has(name)) return null;
          	const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
          	if (nextSubstitutions === null) return null;
          	const nextResolving = new Set(resolvingAliases);
          	nextResolving.add(name);
          	return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
          }
          
          function dictionaryValueTypes(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          	substitutions: TypeAliasEnvironment,
          	resolvingAliases: ReadonlySet<string>,
          ): readonly ResolvedType[] {
          	const unwrapped = unwrapTransparentType(type);
          
          	if (unwrapped.type === "TSTypeLiteral") {
          		return unwrapped.members.flatMap((member): readonly ResolvedType[] =>
          			member.type === "TSIndexSignature" && member.typeAnnotation !== null
          				? [{ type: member.typeAnnotation.typeAnnotation, substitutions }]
          				: [],
          		);
          	}
          
          	if (unwrapped.type === "TSMappedType") {
          		return unwrapped.typeAnnotation === null
          			? []
          			: [{ type: unwrapped.typeAnnotation, substitutions }];
          	}
          
          	if (unwrapped.type !== "TSTypeReference") return [];
          	const name = typeReferenceName(unwrapped);
          	if (name === null) return [];
          
          	const substitution = substitutions.get(name);
          	if (substitution !== undefined) {
          		return isUnappliedReferenceTo(substitution, name)
          			? []
          			: dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
          	}
          
          	if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
          		const wrapped = unwrapped.typeArguments?.params[0];
          		return wrapped === undefined
          			? []
          			: dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
          	}
          
          	if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
          		const value = unwrapped.typeArguments?.params[1] ?? null;
          		return value === null ? [] : [{ type: value, substitutions }];
          	}
          
          	if (
          		(name === "Pick" || name === "Omit") &&
          		isBuiltIn(name, unwrapped, environment)
          	) {
          		const source = unwrapped.typeArguments?.params[0];
          		return source === undefined
          			? []
          			: dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
          	}
          
          	const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
          	if (alias === null || resolvingAliases.has(name)) return [];
          	const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
          	if (nextSubstitutions === null) return [];
          	const nextResolving = new Set(resolvingAliases);
          	nextResolving.add(name);
          	return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
          }
          
          export function classifyUnsafeDictionaryValue(
          	valueType: ESTree.TSType,
          	environment: TypeEnvironment,
          ): UnsafeDictionary | null {
          	const unsafeValue = unsafeDirectValue(valueType, environment, new Map(), new Set());
          	return unsafeValue === null ? null : { kind: "unsafe-dictionary", unsafeValue };
          }
          
          export function classifyUnsafeDictionary(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          ): UnsafeDictionary | null {
          	for (const valueType of dictionaryValueTypes(type, environment, new Map(), new Set())) {
          		const unsafeValue = unsafeDirectValue(
          			valueType.type,
          			environment,
          			valueType.substitutions,
          			new Set(),
          		);
          		if (unsafeValue !== null) return { kind: "unsafe-dictionary", unsafeValue };
          	}
          	return null;
          }
          
          export function classifyWideningTarget(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          ): WideningTarget | null {
          	const unwrapped = unwrapTransparentType(type);
          	if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
          	if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
          	if (unwrapped.type === "TSTypeLiteral") {
          		return unwrapped.members.some((member) => member.type === "TSIndexSignature")
          			? { kind: "open dictionary" }
          			: unwrapped.members.length > 0
          				? { kind: "anonymous object" }
          				: null;
          	}
          	if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
          	if (unwrapped.type !== "TSTypeReference") return null;
          	const name = typeReferenceName(unwrapped);
          	if (name === null) return null;
          	if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
          		const wrapped = unwrapped.typeArguments?.params[0];
          		return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment);
          	}
          	if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
          		return hasBroadRecordKey(unwrapped, environment, new Map())
          			? { kind: "open dictionary" }
          			: null;
          	}
          	const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
          	if (alias === null) return null;
          	if ((alias.typeParameters?.params.length ?? 0) > 0) {
          		const substitutions = aliasSubstitution(alias, unwrapped, new Map());
          		const resolved =
          			substitutions === null
          				? null
          				: classifyAliasBroadTarget(
          						alias.typeAnnotation,
          						environment,
          						substitutions,
          						new Set([name]),
          					);
          		return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null;
          	}
          	const substitutions = aliasSubstitution(alias, unwrapped, new Map());
          	if (substitutions === null) return null;
          	const resolved = classifyAliasBroadTarget(
          		alias.typeAnnotation,
          		environment,
          		substitutions,
          		new Set([name]),
          	);
          	return resolved;
          }
          
          function hasBroadRecordKey(
          	type: ESTree.TSTypeReference,
          	environment: TypeEnvironment,
          	substitutions: TypeAliasEnvironment,
          ): boolean {
          	const key = type.typeArguments?.params[0];
          	return key === undefined || isBroadMappedKey(key, environment, substitutions);
          }
          
          function isBroadMappedKey(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          	substitutions: TypeAliasEnvironment,
          	visitedAliases: ReadonlySet<string> = new Set(),
          ): boolean {
          	const unwrapped = unwrapTransparentType(type);
          	if (
          		unwrapped.type === "TSStringKeyword" ||
          		unwrapped.type === "TSNumberKeyword" ||
          		unwrapped.type === "TSSymbolKeyword"
          	) {
          		return true;
          	}
          	if (unwrapped.type === "TSUnionType") {
          		return unwrapped.types.some((member) =>
          			isBroadMappedKey(member, environment, substitutions, visitedAliases),
          		);
          	}
          	if (unwrapped.type !== "TSTypeReference") return false;
          	const name = typeReferenceName(unwrapped);
          	if (name === null) return false;
          	const substitution = substitutions.get(name);
          	if (substitution !== undefined && !isUnappliedReferenceTo(substitution, name)) {
          		return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
          	}
          	if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
          	const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
          	if (
          		alias === null ||
          		(alias.typeParameters?.params.length ?? 0) > 0 ||
          		visitedAliases.has(name)
          	) {
          		return false;
          	}
          	const nextVisited = new Set(visitedAliases);
          	nextVisited.add(name);
          	return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
          }
          
          function classifyAliasBroadTarget(
          	type: ESTree.TSType,
          	environment: TypeEnvironment,
          	substitutions: TypeAliasEnvironment,
          	resolvingAliases: ReadonlySet<string>,
          ): WideningTarget | null {
          	const unwrapped = unwrapTransparentType(type);
          	if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
          	if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
          	if (unwrapped.type === "TSTypeLiteral") {
          		return unwrapped.members.some((member) => member.type === "TSIndexSignature")
          			? { kind: "open dictionary" }
          			: null;
          	}
          	if (unwrapped.type === "TSMappedType") {
          		return isBroadMappedKey(unwrapped.constraint, environment, substitutions)
          			? { kind: "open dictionary" }
          			: null;
          	}
          	if (unwrapped.type !== "TSTypeReference") return null;
          	const name = typeReferenceName(unwrapped);
          	if (name === null) return null;
          	const substitution = substitutions.get(name);
          	if (substitution !== undefined) {
          		return isUnappliedReferenceTo(substitution, name)
          			? null
          			: classifyAliasBroadTarget(
          					substitution,
          					environment,
          					substitutions,
          					resolvingAliases,
          				);
          	}
          	if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
          		const wrapped = unwrapped.typeArguments?.params[0];
          		return wrapped === undefined
          			? null
          			: classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
          	}
          	if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
          		return hasBroadRecordKey(unwrapped, environment, substitutions)
          			? { kind: "open dictionary" }
          			: null;
          	}
          	const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
          	if (alias === null || resolvingAliases.has(name)) return null;
          	const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
          	if (nextSubstitutions === null) return null;
          	const nextResolving = new Set(resolvingAliases);
          	nextResolving.add(name);
          	return classifyAliasBroadTarget(
          		alias.typeAnnotation,
          		environment,
          		nextSubstitutions,
          		nextResolving,
          	);
          }
          
          export function isPopulatedObjectExpression(expression: ESTree.Expression): boolean {
          	let current = expression;
          	while (
          		current.type === "ParenthesizedExpression" ||
          		current.type === "TSAsExpression" ||
          		current.type === "TSTypeAssertion" ||
          		current.type === "TSNonNullExpression"
          	) {
          		current = current.expression;
          	}
          	return current.type === "ObjectExpression" && current.properties.length > 0;
          }
          
          export function isKnownEvidenceExpression(expression: ESTree.Expression): boolean {
          	let current = expression;
          	while (
          		current.type === "ParenthesizedExpression" ||
          		current.type === "TSAsExpression" ||
          		current.type === "TSTypeAssertion" ||
          		current.type === "TSNonNullExpression" ||
          		current.type === "TSSatisfiesExpression"
          	) {
          		current = current.expression;
          	}
          	if (current.type === "ObjectExpression") return true;
          	return (
          		current.type === "ArrayExpression" ||
          		current.type === "ArrowFunctionExpression" ||
          		current.type === "ClassExpression" ||
          		current.type === "FunctionExpression" ||
          		current.type === "NewExpression" ||
          		current.type === "Literal" ||
          		current.type === "TemplateLiteral" ||
          		current.type === "UnaryExpression"
          	);
          }
          
        • function-parameters.ts 2 KB
          import type { ESTree, SourceCode } from "@oxlint/plugins";
          
          export type FunctionParameter = ESTree.ParamPattern;
          
          /** Return whether a type is or contains TypeScript's absorbing unknown top type. */
          export function containsUnknownType(type: ESTree.TSType): boolean {
          	if (type.type === "TSUnknownKeyword") return true;
          	if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
          	return type.type === "TSUnionType" && type.types.some(containsUnknownType);
          }
          
          /** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
          export function functionParameterTypeAnnotation(
          	parameter: FunctionParameter,
          ): ESTree.TSTypeAnnotation | null | undefined {
          	if (parameter.type === "TSParameterProperty") {
          		return functionParameterTypeAnnotation(parameter.parameter);
          	}
          	if (parameter.type === "RestElement") {
          		return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
          	}
          	if (parameter.type === "AssignmentPattern") {
          		return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
          	}
          	return parameter.typeAnnotation;
          }
          
          /** Return only a function parameter's local binding, excluding its annotation and default value. */
          export function functionParameterBindingName(
          	parameter: FunctionParameter,
          	sourceCode: SourceCode,
          ): string {
          	if (parameter.type === "TSParameterProperty") {
          		return functionParameterBindingName(parameter.parameter, sourceCode);
          	}
          	if (parameter.type === "AssignmentPattern") {
          		return functionParameterBindingName(parameter.left, sourceCode);
          	}
          	if (parameter.type === "RestElement") {
          		return functionParameterBindingName(parameter.argument, sourceCode);
          	}
          	if (parameter.type === "Identifier") return parameter.name;
          
          	const sourceText = sourceCode.getText(parameter);
          	const annotationStart = parameter.typeAnnotation?.start;
          	return annotationStart === undefined
          		? sourceText
          		: sourceText.slice(0, annotationStart - parameter.start).trimEnd();
          }
          
        • lexical-type-parameters.ts 1.8 KB
          import type { ESTree } from "@oxlint/plugins";
          
          type VisitorKeys = Readonly<Record<string, readonly string[]>>;
          
          function isNode(value: unknown): value is ESTree.Node {
          	return (
          		typeof value === "object" &&
          		value !== null &&
          		"type" in value &&
          		typeof value.type === "string"
          	);
          }
          
          function collectInferTypeParameterNames(
          	node: ESTree.Node,
          	visitorKeys: VisitorKeys,
          	names: Set<string>,
          ): void {
          	if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
          	const record = node as unknown as Readonly<Record<string, unknown>>;
          	for (const key of visitorKeys[node.type] ?? []) {
          		const value = record[key];
          		if (isNode(value)) {
          			collectInferTypeParameterNames(value, visitorKeys, names);
          			continue;
          		}
          		if (!Array.isArray(value)) continue;
          		for (const child of value) {
          			if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
          		}
          	}
          }
          
          /** Collect type binders that are in scope at a node and can shadow module aliases. */
          export function lexicalTypeParameterNames(
          	node: ESTree.Node,
          	visitorKeys: VisitorKeys,
          ): ReadonlySet<string> {
          	const names = new Set<string>();
          	let descendant: ESTree.Node = node;
          	let current: ESTree.Node | null = node;
          	while (current !== null && current.type !== "Program") {
          		if ("typeParameters" in current) {
          			for (const parameter of current.typeParameters?.params ?? []) {
          				names.add(parameter.name.name);
          			}
          		}
          		if (
          			current.type === "TSMappedType" &&
          			(descendant === current.nameType || descendant === current.typeAnnotation)
          		) {
          			names.add(current.key.name);
          		}
          		if (current.type === "TSConditionalType" && descendant === current.trueType) {
          			collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
          		}
          		descendant = current;
          		current = current.parent;
          	}
          	return names;
          }
          
        • reflect-method.ts 1 KB
          import { resolveVariable } from "./scope.ts";
          
          import type { ESTree, SourceCode } from "@oxlint/plugins";
          
          function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean {
            if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
            if (sourceCode.isGlobalReference(expression)) return true;
            const variable = resolveVariable(sourceCode, expression);
            return variable === null || variable.defs.length === 0;
          }
          
          /** Reports whether a call target names one method on the global Reflect object. */
          export function isGlobalReflectMethodCall(
            sourceCode: SourceCode,
            callee: ESTree.Expression,
            methodName: string,
          ): boolean {
            if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
            if (!isGlobalReflect(sourceCode, callee.object)) return false;
            const property = callee.property;
            return callee.computed
              ? property.type === "Literal" && property.value === methodName
              : property.type === "Identifier" && property.name === methodName;
          }
          
        • scope.ts 501 B
          import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
          
          /** Resolve an identifier to its binding by walking lexical scopes upward. */
          export function resolveVariable(
          	sourceCode: SourceCode,
          	identifier: ESTree.IdentifierReference,
          ): Variable | null {
          	let scope: Scope | null = sourceCode.getScope(identifier);
          	while (scope !== null) {
          		const variable = scope.set.get(identifier.name);
          		if (variable !== undefined) return variable;
          		scope = scope.upper;
          	}
          	return null;
          }
          
        • type-alias-resolution.ts 7.5 KB
          import type { ESTree } from "@oxlint/plugins";
          
          import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts";
          
          type VisitorKeys = Readonly<Record<string, readonly string[]>>;
          type TypeScope = ESTree.Node;
          
          type TypeBinding = {
          	readonly alias: ESTree.TSTypeAliasDeclaration | null;
          	readonly name: string;
          	readonly scope: TypeScope;
          };
          
          type Substitution = {
          	readonly substitutions: Substitutions;
          	readonly type: ESTree.TSType;
          };
          
          type Substitutions = ReadonlyMap<string, Substitution>;
          
          export type TypeAliasEnvironment = {
          	readonly aliases: readonly ESTree.TSTypeAliasDeclaration[];
          	readonly bindingsByName: ReadonlyMap<string, readonly TypeBinding[]>;
          	readonly visitorKeys: VisitorKeys;
          };
          
          export type ResolvedTypeMatcher = (
          	type: ESTree.TSType,
          	matches: (child: ESTree.TSType) => boolean,
          ) => boolean;
          
          const environmentsByProgram = new WeakMap<ESTree.Program, TypeAliasEnvironment>();
          
          function isNode(value: unknown): value is ESTree.Node {
          	return (
          		typeof value === "object" &&
          		value !== null &&
          		"type" in value &&
          		typeof value.type === "string"
          	);
          }
          
          function enclosingTypeScope(node: ESTree.Node): TypeScope {
          	let current: ESTree.Node | null = node.parent;
          	while (current !== null) {
          		if (
          			current.type === "Program" ||
          			current.type === "BlockStatement" ||
          			current.type === "TSModuleBlock" ||
          			current.type === "StaticBlock" ||
          			current.type === "SwitchStatement"
          		) {
          			return current;
          		}
          		current = current.parent;
          	}
          	return node;
          }
          
          function declaredTypeBinding(node: ESTree.Node): {
          	readonly alias: ESTree.TSTypeAliasDeclaration | null;
          	readonly name: string;
          } | null {
          	if (node.type === "TSTypeAliasDeclaration") {
          		return { alias: node, name: node.id.name };
          	}
          	if (
          		node.type === "TSInterfaceDeclaration" ||
          		node.type === "TSEnumDeclaration" ||
          		node.type === "ClassDeclaration" ||
          		node.type === "ClassExpression"
          	) {
          		return node.id === null ? null : { alias: null, name: node.id.name };
          	}
          	if (
          		node.type === "ImportSpecifier" ||
          		node.type === "ImportDefaultSpecifier" ||
          		node.type === "ImportNamespaceSpecifier"
          	) {
          		return { alias: null, name: node.local.name };
          	}
          	return null;
          }
          
          function collectTypeBindings(
          	node: ESTree.Node,
          	visitorKeys: VisitorKeys,
          	bindingsByName: Map<string, TypeBinding[]>,
          	aliases: ESTree.TSTypeAliasDeclaration[],
          ): void {
          	const declared = declaredTypeBinding(node);
          	if (declared !== null) {
          		const bindings = bindingsByName.get(declared.name) ?? [];
          		bindings.push({ ...declared, scope: enclosingTypeScope(node) });
          		bindingsByName.set(declared.name, bindings);
          		if (declared.alias !== null) aliases.push(declared.alias);
          	}
          
          	// SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
          	const fields = node as unknown as Readonly<Record<string, unknown>>;
          	for (const key of visitorKeys[node.type] ?? []) {
          		const value = fields[key];
          		if (isNode(value)) {
          			collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
          			continue;
          		}
          		if (!Array.isArray(value)) continue;
          		for (const child of value) {
          			if (isNode(child)) {
          				collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
          			}
          		}
          	}
          }
          
          /** Collect every lexical type alias and competing type binding in a program. */
          export function createTypeAliasEnvironment(
          	program: ESTree.Program,
          	visitorKeys: VisitorKeys,
          ): TypeAliasEnvironment {
          	const cached = environmentsByProgram.get(program);
          	if (cached !== undefined) return cached;
          	const bindingsByName = new Map<string, TypeBinding[]>();
          	const aliases: ESTree.TSTypeAliasDeclaration[] = [];
          	collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
          	const environment = { aliases, bindingsByName, visitorKeys };
          	environmentsByProgram.set(program, environment);
          	return environment;
          }
          
          function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null {
          	let current: ESTree.Node | null = node;
          	let distance = 0;
          	while (current !== null) {
          		if (current === ancestor) return distance;
          		current = current.parent;
          		distance += 1;
          	}
          	return null;
          }
          
          function nearestTypeBindings(
          	name: string,
          	use: ESTree.Node,
          	environment: TypeAliasEnvironment,
          ): readonly TypeBinding[] {
          	const candidates = environment.bindingsByName.get(name) ?? [];
          	let nearestDistance = Number.POSITIVE_INFINITY;
          	let nearest: TypeBinding[] = [];
          	for (const candidate of candidates) {
          		const distance = ancestorDistance(candidate.scope, use);
          		if (distance === null || distance > nearestDistance) continue;
          		if (distance === nearestDistance) {
          			nearest.push(candidate);
          			continue;
          		}
          		nearestDistance = distance;
          		nearest = [candidate];
          	}
          	return nearest;
          }
          
          /** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
          export function visibleTypeAlias(
          	name: string,
          	use: ESTree.Node,
          	environment: TypeAliasEnvironment,
          ): ESTree.TSTypeAliasDeclaration | null {
          	if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
          	const bindings = nearestTypeBindings(name, use, environment);
          	return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null;
          }
          
          /** Return whether a local declaration shadows a built-in type at this use. */
          export function hasVisibleTypeBinding(
          	name: string,
          	use: ESTree.Node,
          	environment: TypeAliasEnvironment,
          ): boolean {
          	return (
          		lexicalTypeParameterNames(use, environment.visitorKeys).has(name) ||
          		nearestTypeBindings(name, use, environment).length > 0
          	);
          }
          
          function typeReferenceName(type: ESTree.TSTypeReference): string | null {
          	return type.typeName.type === "Identifier" ? type.typeName.name : null;
          }
          
          function aliasSubstitutions(
          	alias: ESTree.TSTypeAliasDeclaration,
          	reference: ESTree.TSTypeReference,
          	base: Substitutions,
          ): Substitutions | null {
          	const parameters = alias.typeParameters?.params ?? [];
          	const arguments_ = reference.typeArguments?.params ?? [];
          	const next = new Map(base);
          	for (const [index, parameter] of parameters.entries()) {
          		const explicitArgument = arguments_[index];
          		const argument = explicitArgument ?? parameter.default;
          		if (argument === null || argument === undefined) return null;
          		const argumentSubstitutions = explicitArgument === undefined ? next : base;
          		next.set(parameter.name.name, {
          			type: argument,
          			substitutions: new Map(argumentSubstitutions),
          		});
          	}
          	return next;
          }
          
          /** Match a type after resolving visible aliases and substituting their type parameters. */
          export function resolvedTypeMatches(
          	type: ESTree.TSType,
          	environment: TypeAliasEnvironment,
          	matcher: ResolvedTypeMatcher,
          ): boolean {
          	const evaluate = (
          		current: ESTree.TSType,
          		substitutions: Substitutions,
          		resolvingAliases: ReadonlySet<ESTree.TSTypeAliasDeclaration>,
          	): boolean => {
          		if (current.type === "TSTypeReference") {
          			const name = typeReferenceName(current);
          			if (name !== null) {
          				const substitution = substitutions.get(name);
          				if (substitution !== undefined && !current.typeArguments?.params.length) {
          					return evaluate(
          						substitution.type,
          						substitution.substitutions,
          						resolvingAliases,
          					);
          				}
          				const alias = visibleTypeAlias(name, current, environment);
          				if (alias !== null && !resolvingAliases.has(alias)) {
          					const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
          					if (nextSubstitutions !== null) {
          						const nextResolving = new Set(resolvingAliases);
          						nextResolving.add(alias);
          						return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
          					}
          				}
          			}
          		}
          		return matcher(current, (child) =>
          			evaluate(child, substitutions, resolvingAliases),
          		);
          	};
          
          	return evaluate(type, new Map(), new Set());
          }
          
      • vendor
        • eslint-stylistic
          • LICENSE 1.1 KB · in bundle
          • padding-line-ast.ts 2.4 KB
            // Local replacements for the upstream helper imports. See UPSTREAM.md.
            import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins";
            
            type Token = SyntaxToken | Comment;
            
            /** Line terminators recognized by the upstream padding matcher. */
            export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
            
            /** Test a closing brace without treating comment text as punctuation. */
            export const isClosingBraceToken = (token: Token): boolean =>
              token.type === "Punctuator" && token.value === "}";
            
            /** Test a semicolon without treating comment text as punctuation. */
            export const isSemicolonToken = (token: Token): boolean =>
              token.type === "Punctuator" && token.value === ";";
            
            /** Filter the optional final semicolon when identifying block-like statements. */
            export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token);
            
            /** Compare token/node boundaries, including attached comments. */
            export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean =>
              left.loc.end.line === right.loc.start.line;
            
            /** Recognize declarations and expressions used by the upstream IIFE matcher. */
            export const isFunction = (node: ESTree.Node): boolean =>
              node.type === "FunctionDeclaration" ||
              node.type === "FunctionExpression" ||
              node.type === "ArrowFunctionExpression";
            
            /** Preserve the upstream multiline statement heuristic. */
            export const isSingleLine = (node: ESTree.Node): boolean =>
              node.loc.start.line === node.loc.end.line;
            
            /** Unwrap optional chaining before checking IIFE syntax. */
            export const skipChainExpression = (node: ESTree.Node): ESTree.Node =>
              node.type === "ChainExpression" ? node.expression : node;
            
            /** Only a program or function-body expression can begin a directive prologue. */
            export const isTopLevelExpressionStatement = (
              node: ESTree.Node,
            ): node is ESTree.ExpressionStatement =>
              node.type === "ExpressionStatement" &&
              (node.parent.type === "Program" ||
                (node.parent.type === "BlockStatement" && isFunction(node.parent.parent)));
            
            /** A single wrapping pair suffices to exclude a string from directive syntax. */
            export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean {
              const before = sourceCode.getTokenBefore(node);
              const after = sourceCode.getTokenAfter(node);
              return before?.value === "(" && after?.value === ")";
            }
            
          • padding-line-between-statements.ts 25.1 KB
            // Vendored from ESLint Stylistic; see UPSTREAM.md and LICENSE in this directory.
            import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken, Comment, CreateRule, Location } from '@oxlint/plugins'
            type ASTNode = ESTree.Node
            type Token = SyntaxToken | Comment
            import type {
              RuleOptions,
              SelectorOption,
              StatementOption,
            } from './padding-line-options.d.ts'
            import {
              isClosingBraceToken,
              isFunction,
              isNotSemicolonToken,
              isParenthesized,
              isSemicolonToken,
              isSingleLine,
              isTokenOnSameLine,
              isTopLevelExpressionStatement,
              LINEBREAKS,
              skipChainExpression,
            } from './padding-line-ast.ts'
            
            const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u
            const CJS_IMPORT = /^require\(/u
            
            /**
             * This rule is a replica of padding-line-between-statements.
             *
             * Ideally we would want to extend the rule support typescript specific support.
             * But since not all the state is exposed by the eslint and eslint has frozen stylistic rules,
             * (see - https://eslint.org/blog/2020/05/changes-to-rules-policies for details.)
             * we are forced to re-implement the rule here.
             *
             * We have tried to keep the implementation as close as possible to the eslint implementation, to make
             * patching easier for future contributors.
             *
             * Reference rule - https://github.com/eslint/eslint/blob/main/lib/rules/padding-line-between-statements.js
             */
            
            type NodeTest = (
              node: ASTNode,
              sourceCode: SourceCode,
            ) => boolean
            
            interface NodeTestObject {
              test: NodeTest
            }
            
            const LT = `[${Array.from(LINEBREAKS).join('')}]`
            const PADDING_LINE_SEQUENCE = new RegExp(
              String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
              'u',
            )
            
            function isSelectorOption(option: StatementOption): option is SelectorOption {
              return typeof option === 'object' && !Array.isArray(option)
            }
            
            /**
             * Creates tester which check if a node starts with specific keyword with the
             * appropriate AST_NODE_TYPES.
             * @param keyword The keyword to test.
             * @returns the created tester.
             * @private
             */
            function newKeywordTester(
              type: string | string[],
              keyword: string,
            ): NodeTestObject {
              return {
                test(node, sourceCode): boolean {
                  const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword
                  const isSameType = Array.isArray(type)
                    ? type.includes(node.type)
                    : type === node.type
            
                  return isSameKeyword && isSameType
                },
              }
            }
            
            /**
             * Creates tester which check if a node is specific type.
             * @param type The node type to test.
             * @returns the created tester.
             * @private
             */
            function newNodeTypeTester(type: string): NodeTestObject {
              return {
                test: (node): boolean => node.type === type,
              }
            }
            
            /**
             * Checks the given node is an expression statement of IIFE.
             * @param node The node to check.
             * @returns `true` if the node is an expression statement of IIFE.
             * @private
             */
            function isIIFEStatement(node: ASTNode): boolean {
              if (node.type === 'ExpressionStatement') {
                let expression = skipChainExpression(node.expression)
                if (expression.type === 'UnaryExpression')
                  expression = skipChainExpression(expression.argument)
            
                if (expression.type === 'CallExpression') {
                  let node: ASTNode = expression.callee
                  while (node.type === 'SequenceExpression') {
                    const lastExpression = node.expressions.at(-1)
                    if (lastExpression === undefined)
                      throw new Error('Padding rule invariant: sequence expression is empty')
                    node = lastExpression
                  }
            
                  return isFunction(node)
                }
              }
              return false
            }
            
            /**
             * Checks the given node is a CommonJS require statement
             * @param node The node to check.
             * @returns `true` if the node is a CommonJS require statement.
             * @private
             */
            function isCJSRequire(node: ASTNode): boolean {
              if (node.type === 'VariableDeclaration') {
                const declaration = node.declarations[0]
                if (declaration?.init) {
                  let call = declaration?.init
                  while (call.type === 'MemberExpression')
                    call = call.object
            
                  if (
                    call.type === 'CallExpression'
                    && call.callee.type === 'Identifier'
                  ) {
                    return call.callee.name === 'require'
                  }
                }
              }
              return false
            }
            
            /**
             * Checks whether the given node is a block-like statement.
             * This checks the last token of the node is the closing brace of a block.
             * @param sourceCode The source code to get tokens.
             * @param node The node to check.
             * @returns `true` if the node is a block-like statement.
             * @private
             */
            function isBlockLikeStatement(
              node: ASTNode,
              sourceCode: SourceCode,
            ): boolean {
              // do-while with a block is a block-like statement.
              if (
                node.type === 'DoWhileStatement'
                && node.body.type === 'BlockStatement'
              ) {
                return true
              }
            
              /**
               * IIFE is a block-like statement specially from
               * JSCS#disallowPaddingNewLinesAfterBlocks.
               */
              if (isIIFEStatement(node))
                return true
            
              // Checks the last token is a closing brace of blocks.
              const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken)
              const belongingNode
                = lastToken && isClosingBraceToken(lastToken)
                  ? sourceCode.getNodeByRangeIndex(lastToken.range[0])
                  : null
            
              return (
                !!belongingNode
                && (belongingNode.type === 'BlockStatement'
                  || belongingNode.type === 'SwitchStatement')
              )
            }
            
            /**
             * Check whether the given node is a directive or not.
             * @param node The node to check.
             * @param sourceCode The source code object to get tokens.
             * @returns `true` if the node is a directive.
             */
            function isDirective(
              node: ASTNode,
              sourceCode: SourceCode,
            ): boolean {
              return (
                isTopLevelExpressionStatement(node)
                && node.expression.type === 'Literal'
                && typeof node.expression.value === 'string'
                && !isParenthesized(node.expression, sourceCode)
              )
            }
            
            /**
             * Check whether the given node is a part of directive prologue or not.
             * @param node The node to check.
             * @param sourceCode The source code object to get tokens.
             * @returns `true` if the node is a part of directive prologue.
             */
            function isDirectivePrologue(
              node: ASTNode,
              sourceCode: SourceCode,
            ): boolean {
              if (
                isDirective(node, sourceCode)
                && node.parent
                && 'body' in node.parent
                && Array.isArray(node.parent.body)
              ) {
                for (const sibling of node.parent.body) {
                  if (sibling === node)
                    break
            
                  if (!isDirective(sibling, sourceCode))
                    return false
                }
                return true
              }
              return false
            }
            
            /**
             * Checks the given node is a CommonJS export statement
             * @param node The node to check.
             * @returns `true` if the node is a CommonJS export statement.
             * @private
             */
            function isCJSExport(node: ASTNode): boolean {
              if (node.type === 'ExpressionStatement') {
                const expression = node.expression
                if (expression.type === 'AssignmentExpression') {
                  let left = expression.left
                  if (left.type === 'MemberExpression') {
                    while (left.object.type === 'MemberExpression')
                      left = left.object
            
                    return (
                      left.object.type === 'Identifier'
                      && (left.object.name === 'exports'
                        || (left.object.name === 'module'
                          && left.property.type === 'Identifier'
                          && left.property.name === 'exports'))
                    )
                  }
                }
              }
              return false
            }
            
            /**
             * Check whether the given node is an expression
             * @param node The node to check.
             * @param sourceCode The source code object to get tokens.
             * @returns `true` if the node is an expression
             */
            function isExpression(
              node: ASTNode,
              sourceCode: SourceCode,
            ): boolean {
              return (
                node.type === 'ExpressionStatement'
                && !isDirectivePrologue(node, sourceCode)
              )
            }
            
            /**
             * Gets the actual last token.
             *
             * If a semicolon is semicolon-less style's semicolon, this ignores it.
             * For example:
             *
             *     foo()
             *     ;[1, 2, 3].forEach(bar)
             * @param sourceCode The source code to get tokens.
             * @param node The node to get.
             * @returns The actual last token.
             * @private
             */
            function getActualLastToken(
              node: ASTNode,
              sourceCode: SourceCode,
            ): Token | null {
              const semiToken = sourceCode.getLastToken(node)!
              const prevToken = sourceCode.getTokenBefore(semiToken)
              const nextToken = sourceCode.getTokenAfter(semiToken)
              const isSemicolonLessStyle
                = prevToken
                  && nextToken
                  && prevToken.range[0] >= node.range[0]
                  && isSemicolonToken(semiToken)
                  && !isTokenOnSameLine(prevToken, semiToken)
                  && isTokenOnSameLine(semiToken, nextToken)
            
              return isSemicolonLessStyle ? prevToken : semiToken
            }
            
            /**
             * This returns the concatenation of the first 2 captured strings.
             * @param _ Unused. Whole matched string.
             * @param trailingSpaces The trailing spaces of the first line.
             * @param indentSpaces The indentation spaces of the last line.
             * @returns The concatenation of trailingSpaces and indentSpaces.
             * @private
             */
            function replacerToRemovePaddingLines(
              _: string,
              trailingSpaces: string,
              indentSpaces: string,
            ): string {
              return trailingSpaces + indentSpaces
            }
            
            function getReportLoc(node: ASTNode, sourceCode: SourceCode): Location {
              if (isSingleLine(node))
                return node.loc
            
              const line = node.loc.start.line
              const sourceLine = sourceCode.lines[line - 1]
              if (sourceLine === undefined)
                throw new Error('Padding rule invariant: statement source line is missing')
            
              return {
                start: node.loc.start,
                end: {
                  line,
                  column: sourceLine.length,
                },
              }
            }
            
            /**
             * Check and report statements for `any` configuration.
             * It does nothing.
             *
             * @private
             */
            function verifyForAny(): void {
              // Empty
            }
            
            /**
             * Check and report statements for `never` configuration.
             * This autofix removes blank lines between the given 2 statements.
             * However, if comments exist between 2 blank lines, it does not remove those
             * blank lines automatically.
             * @param context The rule context to report.
             * @param _ Unused. The previous node to check.
             * @param nextNode The next node to check.
             * @param paddingLines The array of token pairs that blank
             * lines exist between the pair.
             *
             * @private
             */
            function verifyForNever(
              context: RuleContext,
              _: ASTNode,
              nextNode: ASTNode,
              paddingLines: [Token, Token][],
            ): void {
              if (paddingLines.length === 0)
                return
            
              context.report({
                node: nextNode,
                messageId: 'unexpectedBlankLine',
                loc: getReportLoc(nextNode, context.sourceCode),
                fix(fixer) {
                  if (paddingLines.length >= 2)
                    return null
            
                  const paddingPair = paddingLines[0]
                  if (paddingPair === undefined)
                    throw new Error('Padding rule invariant: reported padding pair is missing')
                  const [prevToken, nextToken] = paddingPair
                  const start = prevToken.range[1]
                  const end = nextToken.range[0]
                  const text = context
                    .sourceCode
                    .text
                    .slice(start, end)
                    .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines)
            
                  return fixer.replaceTextRange([start, end], text)
                },
              })
            }
            
            /**
             * Check and report statements for `always` configuration.
             * This autofix inserts a blank line between the given 2 statements.
             * If the `prevNode` has trailing comments, it inserts a blank line after the
             * trailing comments.
             * @param context The rule context to report.
             * @param prevNode The previous node to check.
             * @param nextNode The next node to check.
             * @param paddingLines The array of token pairs that blank
             * lines exist between the pair.
             *
             * @private
             */
            function verifyForAlways(
              context: RuleContext,
              prevNode: ASTNode,
              nextNode: ASTNode,
              paddingLines: [Token, Token][],
            ): void {
              if (paddingLines.length > 0)
                return
            
              context.report({
                node: nextNode,
                messageId: 'expectedBlankLine',
                loc: getReportLoc(nextNode, context.sourceCode),
                fix(fixer) {
                  const sourceCode = context.sourceCode
                  let prevToken = getActualLastToken(prevNode, sourceCode)!
                  const nextToken
                    = sourceCode.getFirstTokenBetween(prevToken, nextNode, {
                      includeComments: true,
            
                      /**
                       * Skip the trailing comments of the previous node.
                       * This inserts a blank line after the last trailing comment.
                       *
                       * For example:
                       *
                       *     foo(); // trailing comment.
                       *     // comment.
                       *     bar();
                       *
                       * Get fixed to:
                       *
                       *     foo(); // trailing comment.
                       *
                       *     // comment.
                       *     bar();
                       * @param token The token to check.
                       * @returns `true` if the token is not a trailing comment.
                       * @private
                       */
                      filter(token) {
                        if (isTokenOnSameLine(prevToken, token)) {
                          prevToken = token
                          return false
                        }
                        return true
                      },
                    })! || nextNode
                  const insertText = isTokenOnSameLine(prevToken, nextToken)
                    ? '\n\n'
                    : '\n'
            
                  return fixer.insertTextAfter(prevToken, insertText)
                },
              })
            }
            
            /**
             * Types of blank lines.
             * `any`, `never`, and `always` are defined.
             * Those have `verify` method to check and report statements.
             * @private
             */
            const PaddingTypes = {
              any: { verify: verifyForAny },
              never: { verify: verifyForNever },
              always: { verify: verifyForAlways },
            }
            
            const MaybeMultilineStatementType: Record<string, NodeTestObject> = {
              'block-like': { test: isBlockLikeStatement },
              'expression': { test: isExpression },
              'return': newKeywordTester('ReturnStatement', 'return'),
              'export': newKeywordTester(
                [
                  'ExportAllDeclaration',
                  'ExportDefaultDeclaration',
                  'ExportNamedDeclaration',
                ],
                'export',
              ),
              'var': newKeywordTester('VariableDeclaration', 'var'),
              'let': newKeywordTester('VariableDeclaration', 'let'),
              'const': newKeywordTester('VariableDeclaration', 'const'),
              'using': {
                test: node => node.type === 'VariableDeclaration'
                  && (node.kind === 'using' || node.kind === 'await using'),
              },
              'type': newKeywordTester('TSTypeAliasDeclaration', 'type'),
            }
            
            /**
             * Types of statements.
             * Those have `test` method to check it matches to the given statement.
             * @private
             */
            const StatementTypes: Record<string, NodeTestObject> = {
              '*': { test: (): boolean => true },
              'exports': { test: isCJSExport },
              'require': { test: isCJSRequire },
              'directive': { test: isDirectivePrologue },
              'iife': { test: isIIFEStatement },
            
              'block': newNodeTypeTester('BlockStatement'),
              'empty': newNodeTypeTester('EmptyStatement'),
              'function': newNodeTypeTester('FunctionDeclaration'),
              'ts-method': newNodeTypeTester('TSMethodSignature'),
            
              'break': newKeywordTester('BreakStatement', 'break'),
              'case': newKeywordTester('SwitchCase', 'case'),
              'class': newKeywordTester('ClassDeclaration', 'class'),
              'continue': newKeywordTester('ContinueStatement', 'continue'),
              'debugger': newKeywordTester('DebuggerStatement', 'debugger'),
              'default': newKeywordTester(
                ['SwitchCase', 'ExportDefaultDeclaration'],
                'default',
              ),
              'do': newKeywordTester('DoWhileStatement', 'do'),
              'for': newKeywordTester(
                [
                  'ForStatement',
                  'ForInStatement',
                  'ForOfStatement',
                ],
                'for',
              ),
              'if': newKeywordTester('IfStatement', 'if'),
              'import': newKeywordTester('ImportDeclaration', 'import'),
              'switch': newKeywordTester('SwitchStatement', 'switch'),
              'throw': newKeywordTester('ThrowStatement', 'throw'),
              'try': newKeywordTester('TryStatement', 'try'),
              'while': newKeywordTester(
                ['WhileStatement', 'DoWhileStatement'],
                'while',
              ),
              'with': newKeywordTester('WithStatement', 'with'),
            
              'cjs-export': {
                test: (node, sourceCode) => node.type === 'ExpressionStatement'
                  && node.expression.type === 'AssignmentExpression'
                  && CJS_EXPORT.test(sourceCode.getText(node.expression.left)),
              },
              'cjs-import': {
                test: (node, sourceCode) => node.type === 'VariableDeclaration'
                  && node.declarations.length > 0
                  && node.declarations[0]?.init != null
                  && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)),
              },
            
              'enum': newKeywordTester(
                'TSEnumDeclaration',
                'enum',
              ),
              'interface': newKeywordTester(
                'TSInterfaceDeclaration',
                'interface',
              ),
              'function-overload': newNodeTypeTester('TSDeclareFunction'),
              ...Object.fromEntries(
                Object.entries(MaybeMultilineStatementType)
                  .flatMap(([key, value]) => [
                    [key, value],
                    [
                      `singleline-${key}`,
                      {
                        ...value,
                        test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node),
                      },
                    ],
                    [
                      `multiline-${key}`,
                      {
                        ...value,
                        test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node),
                      },
                    ],
                  ]),
              ),
            }
            
            /** Build the vendored padding rule with caller-owned, typed policy options. */
            export default function createPaddingLineRule(options: RuleOptions): CreateRule {
            return {
              meta: {
                type: 'layout',
                docs: {
                  description: 'Require or disallow padding lines between statements',
                },
                fixable: 'whitespace',
                hasSuggestions: false,
                // This is intentionally an array schema as you can pass 0..n config objects
                schema: {
                  $defs: {
                    paddingType: {
                      type: 'string',
                      enum: Object.keys(PaddingTypes),
                    },
                    statementType: {
                      type: 'string',
                      enum: Object.keys(StatementTypes),
                    },
                    selectorOption: {
                      type: 'object',
                      properties: {
                        selector: {
                          type: 'string',
                        },
                        lineMode: {
                          type: 'string',
                          enum: ['any', 'singleline', 'multiline'],
                        },
                      },
                      required: ['selector'],
                      additionalProperties: false,
                    },
                    statementMatcher: {
                      anyOf: [
                        { $ref: '#/$defs/statementType' },
                        { $ref: '#/$defs/selectorOption' },
                      ],
                    },
                    statementOption: {
                      anyOf: [
                        { $ref: '#/$defs/statementMatcher' },
                        {
                          type: 'array',
                          items: { $ref: '#/$defs/statementMatcher' },
                          minItems: 1,
                          uniqueItems: true,
                          additionalItems: false,
                        },
                      ],
                    },
                  },
                  type: 'array',
                  additionalItems: false,
                  items: {
                    type: 'object',
                    properties: {
                      blankLine: { $ref: '#/$defs/paddingType' },
                      prev: { $ref: '#/$defs/statementOption' },
                      next: { $ref: '#/$defs/statementOption' },
                    },
                    additionalProperties: false,
                    required: ['blankLine', 'prev', 'next'],
                  },
                },
                messages: {
                  unexpectedBlankLine: 'Unexpected blank line before this statement.',
                  expectedBlankLine: 'Expected blank line before this statement.',
                },
              },
              create(context) {
                const sourceCode = context.sourceCode
            
                const selectorMatchedNodes = new Map<string, Set<ASTNode>>()
                const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = []
            
                function collectSelectorOption(option: StatementOption): void {
                  if (Array.isArray(option)) {
                    for (const item of option)
                      collectSelectorOption(item)
                    return
                  }
            
                  if (!isSelectorOption(option))
                    return
            
                  selectorMatchedNodes.set(option.selector, new Set())
                }
            
                for (const configure of options) {
                  collectSelectorOption(configure.prev)
                  collectSelectorOption(configure.next)
                }
            
                type Scope = {
                  upper: Scope
                  prevNode: ASTNode | null
                } | null
            
                let scopeInfo: Scope = null
            
                /**
                 * Processes to enter to new scope.
                 * This manages the current previous statement.
                 *
                 * @private
                 */
                function enterScope(): void {
                  scopeInfo = {
                    upper: scopeInfo,
                    prevNode: null,
                  }
                }
            
                /**
                 * Processes to exit from the current scope.
                 *
                 * @private
                 */
                function exitScope(): void {
                  if (scopeInfo)
                    scopeInfo = scopeInfo.upper
                }
            
                /**
                 * Checks whether the given node matches the given type.
                 * @param node The statement node to check.
                 * @param type The statement type to check.
                 * @returns `true` if the statement node matched the type.
                 * @private
                 */
                function match(node: ASTNode, type: StatementOption): boolean {
                  let innerStatementNode = node
            
                  while (innerStatementNode.type === 'LabeledStatement')
                    innerStatementNode = innerStatementNode.body
            
                  if (Array.isArray(type))
                    return type.some(match.bind(null, innerStatementNode))
            
                  if (isSelectorOption(type)) {
                    const matchedNodes = selectorMatchedNodes.get(type.selector)
                    if (!matchedNodes?.has(innerStatementNode))
                      return false
            
                    const lineMode = type.lineMode
            
                    if (lineMode === 'singleline')
                      return isSingleLine(innerStatementNode)
                    else if (lineMode === 'multiline')
                      return !isSingleLine(innerStatementNode)
            
                    return true
                  }
                  else {
                    const statementType = StatementTypes[type]
                    if (statementType === undefined)
                      throw new Error(`Padding rule invariant: unsupported statement type ${type}`)
                    return statementType.test(innerStatementNode, sourceCode)
                  }
                }
            
                /**
                 * Finds the last matched configure from options.
                 * @param prevNode The previous statement to match.
                 * @param nextNode The current statement to match.
                 * @returns The tester of the last matched configure.
                 * @private
                 */
                function getPaddingType(
                  prevNode: ASTNode,
                  nextNode: ASTNode,
                ): (typeof PaddingTypes)[keyof typeof PaddingTypes] {
                  for (let i = options.length - 1; i >= 0; --i) {
                    const configure = options[i]
                    if (configure === undefined)
                      throw new Error('Padding rule invariant: configuration entry is missing')
                    if (
                      match(prevNode, configure.prev)
                      && match(nextNode, configure.next)
                    ) {
                      return PaddingTypes[configure.blankLine]
                    }
                  }
                  return PaddingTypes.any
                }
            
                /**
                 * Gets padding line sequences between the given 2 statements.
                 * Comments are separators of the padding line sequences.
                 * @param prevNode The previous statement to count.
                 * @param nextNode The current statement to count.
                 * @returns The array of token pairs.
                 * @private
                 */
                function getPaddingLineSequences(
                  prevNode: ASTNode,
                  nextNode: ASTNode,
                ): [Token, Token][] {
                  const pairs: [Token, Token][] = []
                  let prevToken: Token = getActualLastToken(prevNode, sourceCode)!
            
                  if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
                    do {
                      const token: Token = sourceCode.getTokenAfter(prevToken, {
                        includeComments: true,
                      })!
            
                      if (token.loc.start.line - prevToken.loc.end.line >= 2)
                        pairs.push([prevToken, token])
            
                      prevToken = token
                    } while (prevToken.range[0] < nextNode.range[0])
                  }
            
                  return pairs
                }
            
                /**
                 * Verify padding lines between the given node and the previous node.
                 * @param node The node to verify.
                 *
                 * @private
                 */
                function verify(node: ASTNode): void {
                  if (
                    !node.parent
                    || ![
                      'BlockStatement',
                      'Program',
                      'StaticBlock',
                      'SwitchCase',
                      'SwitchStatement',
                      'TSInterfaceBody',
                      'TSModuleBlock',
                      'TSTypeLiteral',
                    ].includes(node.parent.type)
                  ) {
                    return
                  }
            
                  // Save this node as the current previous statement.
                  const prevNode = scopeInfo!.prevNode
            
                  // Verify.
                  if (prevNode)
                    pendingPairs.push({ prevNode, nextNode: node })
            
                  scopeInfo!.prevNode = node
                }
            
                function verifyPendingPairs(): void {
                  for (const { prevNode, nextNode } of pendingPairs) {
                    const type = getPaddingType(prevNode, nextNode)
                    const paddingLines = getPaddingLineSequences(prevNode, nextNode)
            
                    type.verify(context, prevNode, nextNode, paddingLines)
                  }
                }
            
                /**
                 * Verify padding lines between the given node and the previous node.
                 * Then process to enter to new scope.
                 * @param node The node to verify.
                 *
                 * @private
                 */
                function verifyThenEnterScope(node: ASTNode): void {
                  verify(node)
                  enterScope()
                }
            
                const selectorMatchListeners = Object.fromEntries(
                  Array.from(selectorMatchedNodes.keys(), selector => [
                    selector,
                    (node: ASTNode): void => {
                      selectorMatchedNodes.get(selector)?.add(node)
                    },
                  ]),
                )
            
                return {
                  'Program': enterScope,
                  'Program:exit': () => {
                    verifyPendingPairs()
                    exitScope()
                  },
                  'BlockStatement': enterScope,
                  'BlockStatement:exit': exitScope,
                  'SwitchStatement': enterScope,
                  'SwitchStatement:exit': exitScope,
                  'SwitchCase': verifyThenEnterScope,
                  'SwitchCase:exit': exitScope,
                  'StaticBlock': enterScope,
                  'StaticBlock:exit': exitScope,
            
                  'TSInterfaceBody': enterScope,
                  'TSInterfaceBody:exit': exitScope,
                  'TSModuleBlock': enterScope,
                  'TSModuleBlock:exit': exitScope,
                  'TSTypeLiteral': enterScope,
                  'TSTypeLiteral:exit': exitScope,
                  'TSDeclareFunction': verifyThenEnterScope,
                  'TSDeclareFunction:exit': exitScope,
                  'TSMethodSignature': verifyThenEnterScope,
                  'TSMethodSignature:exit': exitScope,
            
                  ':statement': verify,
                  ...selectorMatchListeners,
                }
              },
            }
            }
            
          • padding-line-options.d.ts 1.7 KB
            /* GENERATED, DO NOT EDIT DIRECTLY */
            
            /* @checksum: 3QCTtOH6rJM5_AGJ58rGpeEaBEfaJz17MSCxWB4X_PU */
            
            export type PaddingType = 'any' | 'never' | 'always'
            export type StatementOption =
              | StatementMatcher
              | [StatementMatcher, ...StatementMatcher[]]
            export type StatementMatcher =
              | StatementType
              | SelectorOption
            export type StatementType =
              | '*'
              | 'exports'
              | 'require'
              | 'directive'
              | 'iife'
              | 'block'
              | 'empty'
              | 'function'
              | 'ts-method'
              | 'break'
              | 'case'
              | 'class'
              | 'continue'
              | 'debugger'
              | 'default'
              | 'do'
              | 'for'
              | 'if'
              | 'import'
              | 'switch'
              | 'throw'
              | 'try'
              | 'while'
              | 'with'
              | 'cjs-export'
              | 'cjs-import'
              | 'enum'
              | 'interface'
              | 'function-overload'
              | 'block-like'
              | 'singleline-block-like'
              | 'multiline-block-like'
              | 'expression'
              | 'singleline-expression'
              | 'multiline-expression'
              | 'return'
              | 'singleline-return'
              | 'multiline-return'
              | 'export'
              | 'singleline-export'
              | 'multiline-export'
              | 'var'
              | 'singleline-var'
              | 'multiline-var'
              | 'let'
              | 'singleline-let'
              | 'multiline-let'
              | 'const'
              | 'singleline-const'
              | 'multiline-const'
              | 'using'
              | 'singleline-using'
              | 'multiline-using'
              | 'type'
              | 'singleline-type'
              | 'multiline-type'
            export type PaddingLineBetweenStatementsSchema0 = {
              blankLine: PaddingType
              prev: StatementOption
              next: StatementOption
            }[]
            
            export interface SelectorOption {
              selector: string
              lineMode?: 'any' | 'singleline' | 'multiline'
            }
            
            export type PaddingLineBetweenStatementsRuleOptions
              = PaddingLineBetweenStatementsSchema0
            
            export type RuleOptions
              = PaddingLineBetweenStatementsRuleOptions
            export type MessageIds =
              | 'unexpectedBlankLine'
              | 'expectedBlankLine'
            
          • UPSTREAM.md 3.1 KB
            # Vendored padding-line-between-statements
            
            Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`.
            
            Copied files:
            
            - `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts`
            - `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts`
            - Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply.
            
            The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, including skill assets. No Stylistic, ESLint, TypeScript-ESLint, or additional parser runtime dependency is required.
            
            ## Local adaptations
            
            - Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately.
            - Replace `AST_NODE_TYPES` enum members with identical string literals.
            - Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions.
            - Replace the repository-specific `createRule` factory with `createPaddingLineRule(options)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options.
            - Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads.
            - Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code.
            
            The opinionated policy lives outside this directory in `../../rules/require-readable-spacing.ts`. It adds spacing without collapsing existing blank lines. Short local bindings, consecutive imports, and adjacent overload signatures/implementation remain grouped. Spacing is syntactic, not an inference of business-logic boundaries.
            
            ## Updating and verification
            
            Fetch an explicit upstream revision, compare the original rule and types against this revision, and port relevant fixes while retaining the adapters above. Update this record and preserve the license. Run `pnpm check` and `pnpm sync:skill-assets` as required by repository guidance.
            
            Focused Oxlint RuleTester cases live in `../../rules/require-readable-spacing.test.ts`; they test exact fixes, JSDoc/trailing comments, same-line statements, semicolon-free code, TypeScript exports/overloads, Effect-style generators, and upstream removal behavior. `../../rules/require-readable-spacing-cli.test.ts` verifies the exported plugin through the native Oxlint CLI on multiple files, including rejection, autofix, and repeated-fix stability. The complete upstream JS/TS test suites have not been ported; this is focused compatibility evidence, not a claim of full upstream conformance.
            
      • index.ts 2.6 KB
        import { eslintCompatPlugin } from "@oxlint/plugins";
        
        import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts";
        import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts";
        import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
        import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
        import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
        import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
        import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
        import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
        import { noReflectGetRule } from "./rules/no-reflect-get.ts";
        import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
        import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
        import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
        import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
        import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
        import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
        import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
        import { requireReadableSpacingRule } from "./rules/require-readable-spacing.ts";
        import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
        
        /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
        const antiSlopPlugin = eslintCompatPlugin({
        	meta: { name: "anti-slop" },
        	rules: {
        		"no-array-filter-map": noArrayFilterMapRule,
        		"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
        		"no-chained-type-assertions": noChainedTypeAssertionsRule,
        		"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
        		"no-known-value-widening": noKnownValueWideningRule,
        		"no-module-mocking": noModuleMockingRule,
        		"no-object-parameters": noObjectParametersRule,
        		"no-reflect-apply": noReflectApplyRule,
        		"no-reflect-get": noReflectGetRule,
        		"no-runtime-typeof": noRuntimeTypeofRule,
        		"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
        		"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
        		"no-unknown-parameters": noUnknownParametersRule,
        		"no-unknown-returns": noUnknownReturnsRule,
        		"no-unknown-type-aliases": noUnknownTypeAliasesRule,
        		"no-widen-then-assert": noWidenThenAssertRule,
        		"require-readable-spacing": requireReadableSpacingRule,
        		"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
        	},
        });
        
        export default antiSlopPlugin;
        
  • references
    • update.md 8.5 KB
      # Update a vendored installation
      
      Use this procedure for an existing anti-slop installation. Local edits are owned policy, not drift to erase. An update is a reviewed merge, not a directory replacement.
      
      ## 1. Establish scope and protect local work
      
      Locate the active generic and optional Effect entry points through lint configuration, then inspect their rules, helpers, tests, options, severities, overrides, and dependency versions. Read any `UPSTREAM.md` or equivalent provenance record and relevant Git history. Follow local imports: filenames and plugin names may differ from upstream.
      
      Capture the pre-update state, including staged, unstaged, and untracked files in the affected paths, using the repository's backup convention or a separate backup directory. Keep this backup outside the merge destination. Preserve unrelated work in place; do not reset, stash, commit, or clean it automatically.
      
      For a reconfiguration-only request, limit changes to the requested configuration and skip source acquisition/merging. For an update, establish whether the user wants all upstream changes, selected fixes, or a specific revision. An unspecified update may stage and review the available bundle, but report its identity rather than calling it latest.
      
      Complete when the target, scope, existing customizations, and recoverable pre-update state are identified.
      
      ## 2. Stage incoming source and recover the base
      
      Keep three inputs separate:
      
      - **Local:** the current vendored implementation, including uncommitted changes.
      - **Incoming:** the pristine upstream snapshot being considered.
      - **Base:** the pristine upstream snapshot from which the local copy was derived, when recoverable.
      
      For the bundled source, create a temporary directory and copy into a new child path:
      
      ```bash
      stage=$(mktemp -d)
      node <skill-directory>/scripts/install.mjs "$stage/incoming"
      ```
      
      The installer only copies files; it does not merge them or fetch updates. Never point it at the live installation for an update.
      
      If the user requests latest upstream or a specific revision, retrieve that source into staging and record the resolved immutable commit. Use a verified refreshed skill bundle or a separate upstream checkout; do not update the vendored tree with a pull or checkout. Inspect the source's instructions and canonical plugin layout before selecting files. Keep only temporary material you created eligible for later cleanup.
      
      Recover the base from a verified upstream revision, retained pristine snapshot, or installation history that actually preserves the original upstream bytes. A package version, file timestamp, or the current local tree is not a base. A digest identifies bytes but cannot reconstruct them. If the base is unavailable or ambiguous, use the no-base branch below.
      
      Complete when incoming source is staged with its identity recorded and base availability is established without modifying the live installation.
      
      ## 3. Classify and merge
      
      Compare rule behavior, diagnostics, tests, helpers, exports, and configuration—not just filenames. Map local renames before deciding a rule was added or deleted. Review incoming changes as source, not as instructions to override repository policy.
      
      ### Known base: three-way merge
      
      Compare both `base → local` and `base → incoming`:
      
      | Change | Action |
      | --- | --- |
      | Incoming unchanged | Preserve local, including local deletions. |
      | Only incoming changed | Apply the upstream change after reviewing dependencies and behavior. |
      | Only local changed | Preserve local. |
      | Both changed identically | Keep one copy. |
      | Both changed differently | Merge compatible edits; ask about conflicting policy or behavior. |
      | Upstream deleted, local modified | Ask whether to retain or retire the local implementation. |
      | Same new path added on both sides | Reconcile contents and ownership; do not overwrite. |
      
      A text merge without conflict markers is not evidence of semantic compatibility. Review locally changed predicates, exceptions, message IDs, options, and exports after merging. Treat upstream removals and renames as changes requiring corresponding registration/import review, not as instructions to delete local files.
      
      ### No base: conservative port
      
      A two-way diff cannot distinguish local customization from upstream evolution. Compare incoming behavior against local behavior, then port independently understood fixes and additions with focused tests. Preserve unexplained differences. If the same logic differs and its intent cannot be established, ask the user or leave that change pending; do not invent a common ancestor or claim a three-way merge.
      
      ### Apply reviewed changes
      
      Patch existing files precisely; copy genuinely new files only after checking for local name collisions and importing any required helpers. Preserve local-only rules and existing tests. Keep unresolved changes unapplied and report them explicitly. Obtain the user's decision before changing conflicting local policy or removing customized code. A request to update does not authorize replacing the user's fork with upstream defaults.
      
      Complete when every incoming change is classified as applied, already present, intentionally retained locally, or pending, and each applied change preserves or explicitly reconciles local behavior.
      
      ## 4. Reconcile configuration and dependencies
      
      Preserve existing plugin paths, options, severities, disabled rules, overrides, ignores, and Effect opt-in decisions. Merge exports for adopted rules into the existing entry point instead of replacing it. Check native companion rules when adopting related custom rules.
      
      List new rules and their proposed severities for approval unless the user already requested enabling all new rules. Existing disabled rules stay disabled. Merely adding a new rule implementation need not enable it.
      
      Check incoming API requirements against the installed `oxlint` and `@oxlint/plugins` versions. Keep a compatible pair and follow repository version policy; change dependencies only when required by the adopted source or requested by the user. Preserve unrelated dependency ranges and the package manager.
      
      Complete when every adopted rule is reachable through the intended entry point, configuration reflects approved policy, and the dependency pair supports the merged implementation.
      
      ## 5. Verify local and incoming behavior
      
      Run existing tests and add focused RuleTester cases for changed rule semantics, including regressions that preserve local customizations. Incoming tests are useful evidence, not replacements for local tests. When a skill bundle lacks upstream tests, retrieve relevant tests from the identified upstream revision or write focused cases for the adopted changes.
      
      Exercise the registered plugin with representative accepted and rejected code. Run the repository's lint, typecheck, and required checks; for Vite+, run `vp check`. Vendored files may be excluded from application lint, so verify changed rules explicitly rather than treating an ignored directory as tested.
      
      Report application findings separately from plugin/test failures. Application cleanup requires the user's authorization; retain rule severity and safety checks while reporting findings. If a required check is unavailable, name the blocker and mark verification incomplete.
      
      Complete when adopted behavior and preserved local behavior have test evidence, checks have run, and every failure or verification gap is accounted for.
      
      ## 6. Record the merge and hand back ownership
      
      Update `UPSTREAM.md` beside the vendored entry point, or the repository's existing provenance record, with:
      
      - incoming source identity and the recoverable base used, or explicit unknown-base status;
      - adopted changes and intentional local deviations;
      - deferred/conflicting changes and the decisions still needed;
      - dependency/configuration changes and verification results.
      
      Advance the whole-installation baseline only when the complete incoming snapshot has been reconciled and remaining differences are recorded as intentional local changes. For a partial update, retain the prior baseline and record exactly which changes were adopted or remain pending; do not label the entire tree as matching the new revision. Without a recoverable base, keep that limitation explicit for the next update.
      
      Review the final diff against the pre-update state. Report applied updates, preserved customizations, pending decisions, checks, and the backup location. Keep backups until the user accepts the result; remove only disposable staging material you created.
      
      Complete when the user can distinguish upstream updates from local policy, recover the previous state, and identify what a future update must still reconcile.
      
  • scripts
    • install.mjs 937 B · in bundle
  • SKILL.md 8.5 KB
    ---
    name: install-anti-slop
    description: Install, configure, update, or upgrade vendored anti-slop Oxlint plugins. Use when adding anti-slop, picking up upstream rules or fixes, or migrating an existing installation while preserving local customizations.
    ---
    
    # Install or update anti-slop
    
    Anti-slop is vendored code: the target repository owns its rules, diagnostics, tests, and configuration. Preserve those choices when bringing in upstream changes.
    
    ## Choose the path
    
    Read the repository's agent instructions and `git status`. Identify its package manager, Oxlint/Vite+ configuration, and any existing anti-slop entry points, including renamed or relocated copies referenced by `jsPlugins`.
    
    - **Existing installation — update, upgrade, migrate, or reconfigure:** read [Update a vendored installation](references/update.md) and follow that procedure instead of the fresh-install steps below.
    - **No installation — fresh install:** follow the procedure below. If the user requested an update but no installation can be found, confirm the target before installing.
    
    Complete when the operation and target path are established and pre-existing work is identified.
    
    ## Fresh install
    
    1. Copy the bundled plugin from this skill. Run from the target repository:
    
       ```bash
       node <skill-directory>/scripts/install.mjs
       ```
    
       This creates `tools/oxlint/anti-slop/`. Pass another relative destination as the first argument when the repository has an established tooling layout. The script refuses to replace an existing destination; route existing copies through the update procedure rather than `--force`.
    
       Preserve the nested `vendor/eslint-stylistic/LICENSE` and `UPSTREAM.md`; they travel with the copied rule. Readability enforcement is self-contained and requires no Stylistic plugin dependency.
    
       Complete when the files, including vendored license and provenance, exist at the agreed destination without replacing an existing copy.
    
    2. Install current compatible dependencies rather than trusting versions remembered by the agent:
       - If the repository already depends on `oxlint`, read its installed version from the package manager or lockfile and install `@oxlint/plugins` at exactly that version. Pin it exactly rather than by range so future upgrades move both packages together.
       - Only when the repository has no `oxlint` dependency, query `npm view oxlint version` and `npm view @oxlint/plugins version`, then install the same current version of both packages.
       - `oxlint` is a development dependency. The copied source imports `@oxlint/plugins`, so install it as a development dependency for a local-only plugin.
       - Do not replace the package manager or rewrite unrelated dependency ranges.
    
       Complete when matching compatible versions are installed and unrelated dependency ranges are preserved.
    
    3. Register the generic plugin, configure ignores, and enable all generic rules. For `oxlint.config.ts` or `.oxlintrc.json`, merge these fields with the existing configuration:
    
       ```ts
       ignorePatterns: [
         ".agent/**",
         ".agents/**",
         ".claude/**",
         ".codex/**",
         ".continue/**",
         ".cursor/**",
         ".gemini/**",
         ".opencode/**",
         ".pi/**",
         ".roo/**",
         ".windsurf/**",
         "tools/oxlint/anti-slop/**",
       ],
       jsPlugins: [
         { name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
       ],
       ```
    
       Keep every existing ignore. Adjust the final pattern when the plugin was copied elsewhere. Inspect the repository for other project-local agent tooling directories and add them rather than linting installed skills, hooks, or generated agent configuration as application source. Do not broadly ignore all dot-directories, because some repositories keep owned source or checks in them.
    
       For Vite+, add these fields to `lint.ignorePatterns` and `lint.jsPlugins`. Also merge the same patterns into `fmt.ignorePatterns` so `vp check` does not reformat installed agent assets or the vendored plugin. Merge existing entries instead of replacing them.
    
       Enable these rules at `"error"`, including the native Oxlint companion rule:
    
       ```json
       {
         "oxc/no-accumulating-spread": "error",
         "anti-slop/no-array-filter-map": "error",
         "anti-slop/no-reduce-accumulator-copy": "error",
         "anti-slop/no-chained-type-assertions": "error",
         "anti-slop/no-conditional-empty-object-spread": "error",
         "anti-slop/no-known-value-widening": "error",
         "anti-slop/no-module-mocking": "error",
         "anti-slop/no-object-parameters": "error",
         "anti-slop/no-reflect-apply": "error",
         "anti-slop/no-reflect-get": "error",
         "anti-slop/no-runtime-typeof": "error",
         "anti-slop/no-shape-in-symbol-names": "error",
         "anti-slop/no-unknown-parameters": "error",
         "anti-slop/no-unknown-returns": "error",
         "anti-slop/no-unknown-type-aliases": "error",
         "anti-slop/no-unsafe-dictionary-type": "error",
         "anti-slop/no-widen-then-assert": "error",
         "anti-slop/require-readable-spacing": "error",
         "anti-slop/require-safety-comment-for-type-assertion": "error"
       }
       ```
    
       For `no-array-filter-map`, prefer lazy `.values().filter(...).map(...).toArray()` pipelines only when the target runtime supports iterator helpers; otherwise use an appropriate single `flatMap` or locally mutating reducer. Review callback order, indexes, sparse arrays, `thisArg`, and filtering semantics rather than mechanically rewriting chains. Unknown receiver types are deliberately not inferred by this AST/scope rule.
    
       Pair `no-reduce-accumulator-copy` with native `oxc/no-accumulating-spread`: the custom rule catches supported non-spread copies such as `Object.assign({}, acc, item)`, `Array.from(acc)`, and array accumulator `concat`/`slice` calls. Mutating a fresh local accumulator is allowed; copying individual input items is also allowed. Named callbacks, indirect helpers, and nested accumulator properties are not fully analyzed, so do not claim all quadratic reducers are ruled out.
    
       If the repository declares `effect` in a package manifest, or the user explicitly requests Effect rules, also register the opt-in Effect plugin:
    
       ```ts
       jsPlugins: [
         {
           name: "anti-slop-effect",
           specifier: "./tools/oxlint/anti-slop/effect/index.ts",
         },
       ],
       rules: {
         "anti-slop-effect/no-manual-effect-error-tag": "error",
         "anti-slop-effect/no-manual-tag-comparison": "error",
         "anti-slop-effect/no-manual-tagged-construction": "error",
         "anti-slop-effect/no-service-constructor-imports": "error",
         "anti-slop-effect/prefer-effect-match": "error",
       },
       ```
    
       Merge these entries with the generic plugin configuration rather than replacing it. Do not enable the Effect plugin merely because Effect appears transitively in a lockfile; require a direct package-manifest dependency or an explicit user request. The rule covers relative project imports. Report package-alias imports as a current limitation rather than pretending they are enforced.
    
       Complete when the generic rules and eligible Effect rules are registered and existing configuration is preserved.
    
    4. Run the repository's lint command and typecheck. For Vite+, run the repository's full `vp check` command after adding both lint and format ignores. If findings appear in owned project source, report them and fix them only when the user asked for migration/cleanup. Do not suppress rules, weaken rule severity, add unsafe casts, or mechanically launder types to make lint pass.
    
       When cleanup is authorized, apply `require-readable-spacing` with lint autofix, then run the repository's formatter and lint again. Confirm a second fix/format pass leaves files unchanged. Keep whitespace fixes separate from semantic edits, preserve documentation attachment and overload groups, and do not enable an entire competing formatting preset.
    
       Complete when checks have run, fix/format stability has been verified for authorized cleanup, and every failure is resolved or reported with its diagnostics.
    
    5. Record provenance in `UPSTREAM.md` beside the vendored entry point: source repository, exact source commit or recoverable pristine snapshot when available, installed plugin paths, and intentional deviations. Verify that the revision identifies the actual copied assets; a package version or the current upstream HEAD alone is insufficient. If provenance cannot be established, record it as unknown rather than guessing.
    
       Review the final diff and report the installed path, source identity, dependency/configuration changes, and check results. Complete when the record and report describe the files actually installed and any remaining findings.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related