@timmo001/oxlint-rules 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/dist/cli.js +978 -83
  4. package/dist/configs/recommended-effect.js +1207 -312
  5. package/dist/configs/recommended.js +772 -70
  6. package/dist/upstream/anti-slop.js +765 -63
  7. package/dist/upstream/effect.js +196 -3
  8. package/package.json +5 -5
  9. package/vendor/anti-slop/src/effect/index.ts +8 -0
  10. package/vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts +52 -0
  11. package/vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts +45 -0
  12. package/vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts +37 -0
  13. package/vendor/anti-slop/src/effect/rules/prefer-effect-match.ts +54 -0
  14. package/vendor/anti-slop/src/effect/shared/tagged-values.ts +97 -0
  15. package/vendor/anti-slop/src/index.ts +6 -0
  16. package/vendor/anti-slop/src/rules/no-array-filter-map.ts +28 -0
  17. package/vendor/anti-slop/src/rules/no-known-value-widening.ts +2 -14
  18. package/vendor/anti-slop/src/rules/no-module-mocking.ts +3 -14
  19. package/vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts +109 -0
  20. package/vendor/anti-slop/src/rules/require-readable-spacing.ts +47 -0
  21. package/vendor/anti-slop/src/shared/array-method.ts +94 -0
  22. package/vendor/anti-slop/src/shared/reflect-method.ts +2 -13
  23. package/vendor/anti-slop/src/shared/scope.ts +15 -0
  24. package/vendor/anti-slop/src/vendor/eslint-stylistic/LICENSE +22 -0
  25. package/vendor/anti-slop/src/vendor/eslint-stylistic/UPSTREAM.md +28 -0
  26. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
  27. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
  28. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
@@ -0,0 +1,109 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
3
+
4
+ import {
5
+ arrayMethodTarget,
6
+ isKnownArrayExpression,
7
+ resolveArrayBinding,
8
+ unwrapArrayExpression,
9
+ } from "../shared/array-method.ts";
10
+
11
+ function enclosingReducer(node: ESTree.Node) {
12
+ let parent = node.parent;
13
+ while (parent !== null) {
14
+ if (parent.type === "FunctionDeclaration") return null;
15
+ if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
16
+ const callback = parent;
17
+ let owner: ESTree.Node | null = callback.parent;
18
+ while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
19
+ if (owner?.type !== "CallExpression") return null;
20
+ const method = arrayMethodTarget(owner.callee);
21
+ const firstArgument = owner.arguments[0];
22
+ if (
23
+ method === null || (method.name !== "reduce" && method.name !== "reduceRight") ||
24
+ owner.arguments.length > 2 || firstArgument === undefined ||
25
+ unwrapArrayExpression(firstArgument) !== callback
26
+ ) return null;
27
+ const firstParameter = callback.params[0];
28
+ const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
29
+ if (accumulator?.type !== "Identifier") return null;
30
+ return { callback, accumulator, initialValue: owner.arguments[1] };
31
+ }
32
+ parent = parent.parent;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function referencesAccumulator(
38
+ sourceCode: SourceCode,
39
+ node: ESTree.Node,
40
+ accumulator: Variable,
41
+ visited = new Set<Variable>(),
42
+ ): boolean {
43
+ const variable = resolveArrayBinding(sourceCode, node);
44
+ if (variable === null || visited.has(variable)) return false;
45
+ if (variable === accumulator) return true;
46
+ visited.add(variable);
47
+ if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
48
+ for (const definition of variable.defs) {
49
+ if (
50
+ definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
51
+ definition.node.id.type === "Identifier" && definition.node.init !== null &&
52
+ definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
53
+ ) {
54
+ return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+
60
+ function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean {
61
+ node = unwrapArrayExpression(node);
62
+ if (node.type !== "Identifier" || node.name !== name) return false;
63
+ const variable = resolveArrayBinding(sourceCode, node);
64
+ return variable === null || variable.defs.length === 0;
65
+ }
66
+
67
+ /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
68
+ export const noReduceAccumulatorCopyRule = defineRule({
69
+ meta: {
70
+ type: "problem",
71
+ docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
72
+ messages: {
73
+ 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.",
74
+ },
75
+ },
76
+ createOnce(context) {
77
+ return {
78
+ CallExpression(node) {
79
+ const method = arrayMethodTarget(node.callee);
80
+ if (method === null) return;
81
+ const reducer = enclosingReducer(node);
82
+ if (reducer === null) return;
83
+ const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
84
+ variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
85
+ );
86
+ if (accumulator === undefined) return;
87
+ const isAccumulator = (expression: ESTree.Node) =>
88
+ referencesAccumulator(context.sourceCode, expression, accumulator);
89
+ let copiesAccumulator = false;
90
+ if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
91
+ const target = node.arguments[0];
92
+ copiesAccumulator = (
93
+ target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" &&
94
+ node.arguments.slice(1).some(isAccumulator)
95
+ );
96
+ } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
97
+ const source = node.arguments[0];
98
+ copiesAccumulator = source !== undefined && isAccumulator(source);
99
+ } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
100
+ const initialValue = reducer.initialValue;
101
+ const arrayAccumulator = initialValue !== undefined &&
102
+ isKnownArrayExpression(context.sourceCode, initialValue);
103
+ copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
104
+ }
105
+ if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" });
106
+ },
107
+ };
108
+ },
109
+ });
@@ -0,0 +1,47 @@
1
+ import type { CreateRule } from "@oxlint/plugins";
2
+
3
+ import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts";
4
+
5
+ const paddingRule = createPaddingLineRule([
6
+ { blankLine: "always", prev: "import", next: "*" },
7
+ { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
8
+ { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" },
9
+ { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] },
10
+ { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" },
11
+ {
12
+ blankLine: "always",
13
+ prev: "*",
14
+ next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
15
+ },
16
+ {
17
+ blankLine: "always",
18
+ prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
19
+ next: "*",
20
+ },
21
+ { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
22
+ { blankLine: "always", prev: "block-like", next: "*" },
23
+ { blankLine: "any", prev: "import", next: "import" },
24
+ {
25
+ blankLine: "any",
26
+ prev: {
27
+ selector:
28
+ ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])',
29
+ },
30
+ next: {
31
+ selector:
32
+ ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])',
33
+ },
34
+ },
35
+ ]);
36
+
37
+ /** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */
38
+ export const requireReadableSpacingRule: CreateRule = {
39
+ ...paddingRule,
40
+ meta: {
41
+ ...paddingRule.meta,
42
+ docs: {
43
+ description: "Require readable spacing between declarations and logical statement groups.",
44
+ },
45
+ schema: [],
46
+ },
47
+ };
@@ -0,0 +1,94 @@
1
+ import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
2
+
3
+ /** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
4
+ export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node {
5
+ while (
6
+ node.type === "ParenthesizedExpression" ||
7
+ node.type === "ChainExpression" ||
8
+ node.type === "TSAsExpression" ||
9
+ node.type === "TSTypeAssertion" ||
10
+ node.type === "TSNonNullExpression" ||
11
+ node.type === "TSSatisfiesExpression"
12
+ ) {
13
+ node = node.expression;
14
+ }
15
+ return node;
16
+ }
17
+
18
+ /** Resolve a local binding by scope, not by identifier spelling. */
19
+ export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null {
20
+ node = unwrapArrayExpression(node);
21
+ if (node.type !== "Identifier") return null;
22
+ let scope: Scope | null = sourceCode.getScope(node);
23
+ while (scope !== null) {
24
+ const variable = scope.set.get(node.name);
25
+ if (variable !== undefined) return variable;
26
+ scope = scope.upper;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /** Read static method names, including computed string literals, without evaluating expressions. */
32
+ export function arrayMethodTarget(
33
+ node: ESTree.Node,
34
+ ): { readonly name: string; readonly object: ESTree.Node } | null {
35
+ node = unwrapArrayExpression(node);
36
+ if (node.type !== "MemberExpression") return null;
37
+ const property = node.property;
38
+ if (!node.computed && property.type === "Identifier") {
39
+ return { name: property.name, object: node.object };
40
+ }
41
+ if (node.computed && property.type === "Literal" && typeof property.value === "string") {
42
+ return { name: property.value, object: node.object };
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function isArrayAnnotation(type: ESTree.TSType): boolean {
48
+ if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
49
+ if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
50
+ if (type.type === "TSTypeOperator" && type.operator === "readonly") {
51
+ return isArrayAnnotation(type.typeAnnotation);
52
+ }
53
+ return (
54
+ type.type === "TSTypeReference" && type.typeName.type === "Identifier" &&
55
+ (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray")
56
+ );
57
+ }
58
+
59
+ /** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
60
+ export function isKnownArrayExpression(
61
+ sourceCode: SourceCode,
62
+ node: ESTree.Node,
63
+ visited = new Set<Variable>(),
64
+ ): boolean {
65
+ node = unwrapArrayExpression(node);
66
+ if (node.type === "ArrayExpression") return true;
67
+ if (node.type === "CallExpression") {
68
+ const method = arrayMethodTarget(node.callee);
69
+ return (
70
+ method !== null &&
71
+ ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) &&
72
+ isKnownArrayExpression(sourceCode, method.object, visited)
73
+ );
74
+ }
75
+ if (node.type !== "Identifier") return false;
76
+ const variable = resolveArrayBinding(sourceCode, node);
77
+ if (variable === null || visited.has(variable)) return false;
78
+ visited.add(variable);
79
+ if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
80
+ for (const identifier of variable.identifiers) {
81
+ const annotation = identifier.typeAnnotation?.typeAnnotation;
82
+ if (annotation !== undefined) return isArrayAnnotation(annotation);
83
+ }
84
+ for (const definition of variable.defs) {
85
+ if (
86
+ definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
87
+ definition.node.id.type === "Identifier" && definition.node.init !== null &&
88
+ definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
89
+ ) {
90
+ return isKnownArrayExpression(sourceCode, definition.node.init, visited);
91
+ }
92
+ }
93
+ return false;
94
+ }
@@ -1,17 +1,6 @@
1
- import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
1
+ import { resolveVariable } from "./scope.ts";
2
2
 
3
- function resolveVariable(
4
- sourceCode: SourceCode,
5
- identifier: ESTree.IdentifierReference,
6
- ): Variable | null {
7
- let scope: Scope | null = sourceCode.getScope(identifier);
8
- while (scope !== null) {
9
- const variable = scope.set.get(identifier.name);
10
- if (variable !== undefined) return variable;
11
- scope = scope.upper;
12
- }
13
- return null;
14
- }
3
+ import type { ESTree, SourceCode } from "@oxlint/plugins";
15
4
 
16
5
  function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean {
17
6
  if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
@@ -0,0 +1,15 @@
1
+ import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
2
+
3
+ /** Resolve an identifier to its binding by walking lexical scopes upward. */
4
+ export function resolveVariable(
5
+ sourceCode: SourceCode,
6
+ identifier: ESTree.IdentifierReference,
7
+ ): Variable | null {
8
+ let scope: Scope | null = sourceCode.getScope(identifier);
9
+ while (scope !== null) {
10
+ const variable = scope.set.get(identifier.name);
11
+ if (variable !== undefined) return variable;
12
+ scope = scope.upper;
13
+ }
14
+ return null;
15
+ }
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
4
+ Copyright (c) 2023-PRESENT ESLint Stylistic contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Vendored padding-line-between-statements
2
+
3
+ Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`.
4
+
5
+ Copied files:
6
+
7
+ - `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts`
8
+ - `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts`
9
+ - Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply.
10
+
11
+ 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.
12
+
13
+ ## Local adaptations
14
+
15
+ - Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately.
16
+ - Replace `AST_NODE_TYPES` enum members with identical string literals.
17
+ - Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions.
18
+ - Replace the repository-specific `createRule` factory with `createPaddingLineRule(options)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options.
19
+ - 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.
20
+ - 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.
21
+
22
+ 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.
23
+
24
+ ## Updating and verification
25
+
26
+ 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.
27
+
28
+ 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.
@@ -0,0 +1,51 @@
1
+ // Local replacements for the upstream helper imports. See UPSTREAM.md.
2
+ import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins";
3
+
4
+ type Token = SyntaxToken | Comment;
5
+
6
+ /** Line terminators recognized by the upstream padding matcher. */
7
+ export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
8
+
9
+ /** Test a closing brace without treating comment text as punctuation. */
10
+ export const isClosingBraceToken = (token: Token): boolean =>
11
+ token.type === "Punctuator" && token.value === "}";
12
+
13
+ /** Test a semicolon without treating comment text as punctuation. */
14
+ export const isSemicolonToken = (token: Token): boolean =>
15
+ token.type === "Punctuator" && token.value === ";";
16
+
17
+ /** Filter the optional final semicolon when identifying block-like statements. */
18
+ export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token);
19
+
20
+ /** Compare token/node boundaries, including attached comments. */
21
+ export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean =>
22
+ left.loc.end.line === right.loc.start.line;
23
+
24
+ /** Recognize declarations and expressions used by the upstream IIFE matcher. */
25
+ export const isFunction = (node: ESTree.Node): boolean =>
26
+ node.type === "FunctionDeclaration" ||
27
+ node.type === "FunctionExpression" ||
28
+ node.type === "ArrowFunctionExpression";
29
+
30
+ /** Preserve the upstream multiline statement heuristic. */
31
+ export const isSingleLine = (node: ESTree.Node): boolean =>
32
+ node.loc.start.line === node.loc.end.line;
33
+
34
+ /** Unwrap optional chaining before checking IIFE syntax. */
35
+ export const skipChainExpression = (node: ESTree.Node): ESTree.Node =>
36
+ node.type === "ChainExpression" ? node.expression : node;
37
+
38
+ /** Only a program or function-body expression can begin a directive prologue. */
39
+ export const isTopLevelExpressionStatement = (
40
+ node: ESTree.Node,
41
+ ): node is ESTree.ExpressionStatement =>
42
+ node.type === "ExpressionStatement" &&
43
+ (node.parent.type === "Program" ||
44
+ (node.parent.type === "BlockStatement" && isFunction(node.parent.parent)));
45
+
46
+ /** A single wrapping pair suffices to exclude a string from directive syntax. */
47
+ export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean {
48
+ const before = sourceCode.getTokenBefore(node);
49
+ const after = sourceCode.getTokenAfter(node);
50
+ return before?.value === "(" && after?.value === ")";
51
+ }