@crescware/eslint-plugin-crescware-iteration-var-names 0.0.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.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @crescware/eslint-plugin-crescware-iteration-var-names
2
+
3
+ ESLint plugin that bans thoughtless single-character variables in iteration contexts.
4
+
5
+ ## Stack
6
+
7
+ - **Runtime**: Node.js 24 (via [mise](https://mise.jdx.dev/))
8
+ - **Package manager**: pnpm (via corepack)
9
+ - **Language**: TypeScript ([native preview](https://github.com/nicolo-ribaudo/tc39-proposal-type-annotations))
10
+ - **Test**: [Vitest](https://vitest.dev/)
11
+ - **Lint**: [oxlint](https://oxc.rs/docs/guide/usage/linter)
12
+ - **Format**: [oxfmt](https://github.com/nicolo-ribaudo/oxfmt)
13
+ - **Unused code**: [Knip](https://knip.dev/)
14
+
15
+ ## Setup
16
+
17
+ ```sh
18
+ mise install
19
+ corepack enable
20
+ pnpm install
21
+ ```
22
+
23
+ ## Scripts
24
+
25
+ | Command | Description |
26
+ | ------------------ | ---------------------------------------- |
27
+ | `pnpm check` | Run all checks (types, lint, knip, test) |
28
+ | `pnpm check:types` | Type check |
29
+ | `pnpm check:lint` | Lint and format check |
30
+ | `pnpm check:knip` | Unused files/exports check |
31
+ | `pnpm test` | Run tests |
32
+ | `pnpm format` | Fix lint and format |
@@ -0,0 +1,21 @@
1
+ type ReportDescriptor = {
2
+ message: string;
3
+ node: unknown;
4
+ };
5
+ type RuleContext = {
6
+ report: (descriptor: ReportDescriptor) => void;
7
+ };
8
+ type Visitor = Record<string, (node: never) => void>;
9
+ type Rule = {
10
+ meta?: Record<string, unknown>;
11
+ create: (context: RuleContext) => Visitor;
12
+ };
13
+ declare const plugin: {
14
+ meta: {
15
+ name: string;
16
+ };
17
+ rules: {
18
+ "array-callback-arg-names": Rule;
19
+ };
20
+ };
21
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ const TARGET_METHODS = new Set([
2
+ "map",
3
+ "filter",
4
+ "forEach",
5
+ "find",
6
+ "findIndex",
7
+ "findLast",
8
+ "findLastIndex",
9
+ "some",
10
+ "every",
11
+ "flatMap",
12
+ "reduce",
13
+ "reduceRight",
14
+ "sort",
15
+ ]);
16
+ const expectedNamesFor = (kind) => {
17
+ switch (kind) {
18
+ case "reduce-sync":
19
+ return ["acc", "v", "i", "arr"];
20
+ case "reduce-async":
21
+ return ["prev", "v", "i", "arr"];
22
+ case "sort":
23
+ return ["a", "b"];
24
+ case "iterator":
25
+ return ["v", "i", "arr"];
26
+ }
27
+ };
28
+ const methodKindOf = (methodName, isAsync) => {
29
+ if (methodName === "reduce" || methodName === "reduceRight") {
30
+ return isAsync ? "reduce-async" : "reduce-sync";
31
+ }
32
+ if (methodName === "sort") {
33
+ return "sort";
34
+ }
35
+ if (TARGET_METHODS.has(methodName)) {
36
+ return "iterator";
37
+ }
38
+ return null;
39
+ };
40
+ const getTargetMethodName = (call) => {
41
+ const callee = call.callee;
42
+ if (callee.type !== "MemberExpression") {
43
+ return null;
44
+ }
45
+ if (callee.computed === true) {
46
+ return null;
47
+ }
48
+ const property = callee.property;
49
+ if (property === undefined || property.type !== "Identifier") {
50
+ return null;
51
+ }
52
+ const name = property.name;
53
+ if (name === undefined || !TARGET_METHODS.has(name)) {
54
+ return null;
55
+ }
56
+ return name;
57
+ };
58
+ const getCallbackFunction = (call) => {
59
+ const first = call.arguments[0];
60
+ if (first === undefined) {
61
+ return null;
62
+ }
63
+ if (first.type !== "ArrowFunctionExpression" &&
64
+ first.type !== "FunctionExpression") {
65
+ return null;
66
+ }
67
+ return first;
68
+ };
69
+ const isIdentifier = (node) => {
70
+ return (node !== undefined &&
71
+ node.type === "Identifier" &&
72
+ typeof node.name === "string");
73
+ };
74
+ const rule = {
75
+ meta: {
76
+ type: "suggestion",
77
+ docs: {
78
+ description: "Enforce naming convention for Array.prototype callback arguments (v/i/arr, acc, prev, a/b).",
79
+ },
80
+ schema: [],
81
+ },
82
+ create(context) {
83
+ const callbackStack = [];
84
+ const callbackToFrame = new WeakMap();
85
+ const reportInner = (param, methodName, kind, paramIndex) => {
86
+ const expected = expectedNamesFor(kind)[paramIndex];
87
+ if (expected === undefined) {
88
+ return;
89
+ }
90
+ if (param.name.length >= 2) {
91
+ return;
92
+ }
93
+ if (param.name === expected) {
94
+ return;
95
+ }
96
+ context.report({
97
+ message: `Array.prototype.${methodName} expects '${expected}' for argument ${paramIndex + 1} (got: '${param.name}').`,
98
+ node: param,
99
+ });
100
+ };
101
+ const reportOuter = (param, methodName) => {
102
+ if (param.name.length >= 2) {
103
+ return;
104
+ }
105
+ context.report({
106
+ message: `Avoid the single-character name '${param.name}' on an outer Array.prototype.${methodName} callback; use a meaningful name with 2 or more characters.`,
107
+ node: param,
108
+ });
109
+ };
110
+ const onCallEnter = (node) => {
111
+ const methodName = getTargetMethodName(node);
112
+ if (methodName === null) {
113
+ return;
114
+ }
115
+ const callback = getCallbackFunction(node);
116
+ if (callback === null) {
117
+ return;
118
+ }
119
+ const kind = methodKindOf(methodName, callback.async === true);
120
+ if (kind === null) {
121
+ return;
122
+ }
123
+ const frame = { kind, methodName, hasNested: false };
124
+ callbackToFrame.set(callback, frame);
125
+ const top = callbackStack[callbackStack.length - 1];
126
+ if (top !== undefined) {
127
+ top.hasNested = true;
128
+ }
129
+ };
130
+ const onFunctionEnter = (node) => {
131
+ const frame = callbackToFrame.get(node);
132
+ if (frame === undefined) {
133
+ return;
134
+ }
135
+ callbackStack.push(frame);
136
+ };
137
+ const onFunctionExit = (node) => {
138
+ const frame = callbackToFrame.get(node);
139
+ if (frame === undefined) {
140
+ return;
141
+ }
142
+ const popped = callbackStack.pop();
143
+ if (popped !== frame) {
144
+ return;
145
+ }
146
+ const expected = expectedNamesFor(frame.kind);
147
+ const params = node.params;
148
+ const limit = Math.min(params.length, expected.length);
149
+ for (let i = 0; i < limit; i++) {
150
+ const param = params[i];
151
+ if (!isIdentifier(param)) {
152
+ continue;
153
+ }
154
+ if (frame.hasNested) {
155
+ reportOuter(param, frame.methodName);
156
+ }
157
+ else {
158
+ reportInner(param, frame.methodName, frame.kind, i);
159
+ }
160
+ }
161
+ };
162
+ return {
163
+ CallExpression: onCallEnter,
164
+ ArrowFunctionExpression: onFunctionEnter,
165
+ "ArrowFunctionExpression:exit": onFunctionExit,
166
+ FunctionExpression: onFunctionEnter,
167
+ "FunctionExpression:exit": onFunctionExit,
168
+ };
169
+ },
170
+ };
171
+ const plugin = {
172
+ meta: { name: "crescware-iteration-var-names" },
173
+ rules: {
174
+ "array-callback-arg-names": rule,
175
+ },
176
+ };
177
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@crescware/eslint-plugin-crescware-iteration-var-names",
3
+ "version": "0.0.2",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "rimraf dist && tsgo -p tsconfig.build.json",
14
+ "check": "pnpm run check:types && pnpm run check:lint && pnpm run check:knip && pnpm run test",
15
+ "check:knip": "knip",
16
+ "check:lint": "oxlint && oxfmt --check",
17
+ "check:types": "tsgo -p tsconfig.json --noEmit",
18
+ "exec:fixtures": "oxlint -c fixtures/oxlintrc.fixtures.json --no-ignore -f json fixtures/",
19
+ "format": "oxlint --fix && oxfmt",
20
+ "test": "vitest run"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "24.12.2",
24
+ "@typescript/native-preview": "7.0.0-dev.20260422.1",
25
+ "knip": "6.7.0",
26
+ "oxfmt": "0.47.0",
27
+ "oxlint": "1.62.0",
28
+ "rimraf": "6.1.3",
29
+ "vitest": "4.1.5"
30
+ },
31
+ "packageManager": "pnpm@10.33.2"
32
+ }