@crescware/eslint-plugin-crescware-iteration-var-names 0.0.2 → 0.0.3

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/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ declare const plugin: {
15
15
  name: string;
16
16
  };
17
17
  rules: {
18
- "array-callback-arg-names": Rule;
18
+ "iteration-var-names": Rule;
19
19
  };
20
20
  };
21
21
  export default plugin;
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ const TARGET_METHODS = new Set([
13
13
  "reduceRight",
14
14
  "sort",
15
15
  ]);
16
+ const FOR_ALLOWED_INNER = new Set(["k", "v", "i"]);
16
17
  const expectedNamesFor = (kind) => {
17
18
  switch (kind) {
18
19
  case "reduce-sync":
@@ -71,19 +72,195 @@ const isIdentifier = (node) => {
71
72
  node.type === "Identifier" &&
72
73
  typeof node.name === "string");
73
74
  };
75
+ const collectPatternIdentifiers = (node) => {
76
+ if (node === null) {
77
+ return [];
78
+ }
79
+ switch (node.type) {
80
+ case "Identifier":
81
+ if (typeof node.name !== "string") {
82
+ return [];
83
+ }
84
+ return [node];
85
+ case "ArrayPattern":
86
+ return (node.elements ?? []).flatMap((el) => collectPatternIdentifiers(el));
87
+ case "ObjectPattern":
88
+ return (node.properties ?? []).flatMap((v) => {
89
+ if (v.type === "Property") {
90
+ return collectPatternIdentifiers(v.value);
91
+ }
92
+ if (v.type === "RestElement") {
93
+ return collectPatternIdentifiers(v.argument);
94
+ }
95
+ return [];
96
+ });
97
+ case "RestElement":
98
+ return collectPatternIdentifiers(node.argument ?? null);
99
+ case "AssignmentPattern":
100
+ return collectPatternIdentifiers(node.left ?? null);
101
+ default:
102
+ return [];
103
+ }
104
+ };
105
+ const collectForLoopVarIdentifiers = (node) => {
106
+ if (node.type === "ForStatement") {
107
+ const init = node.init;
108
+ if (init === null || init === undefined) {
109
+ return [];
110
+ }
111
+ if (init.type !== "VariableDeclaration") {
112
+ return [];
113
+ }
114
+ const decl = init;
115
+ return decl.declarations.flatMap((v) => collectPatternIdentifiers(v.id));
116
+ }
117
+ const left = node.left;
118
+ if (left.type === "VariableDeclaration") {
119
+ const decl = left;
120
+ return decl.declarations.flatMap((v) => collectPatternIdentifiers(v.id));
121
+ }
122
+ return [];
123
+ };
124
+ const forKindOf = (node) => {
125
+ if (node.type === "ForOfStatement") {
126
+ return "for-of";
127
+ }
128
+ if (node.type === "ForInStatement") {
129
+ return "for-in";
130
+ }
131
+ return "for";
132
+ };
133
+ const DISCARD_NAME = "_";
134
+ const hasIdentifierUseIn = (root, name) => {
135
+ if (root === null || root === undefined) {
136
+ return false;
137
+ }
138
+ if (typeof root !== "object") {
139
+ return false;
140
+ }
141
+ if (Array.isArray(root)) {
142
+ for (const item of root) {
143
+ if (hasIdentifierUseIn(item, name)) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+ const node = root;
150
+ const type = node.type;
151
+ if (type === "Identifier" && node.name === name) {
152
+ return true;
153
+ }
154
+ if (type === "VariableDeclarator") {
155
+ return hasIdentifierUseIn(node.init, name);
156
+ }
157
+ if (type === "FunctionDeclaration" ||
158
+ type === "FunctionExpression" ||
159
+ type === "ArrowFunctionExpression") {
160
+ return hasIdentifierUseIn(node.body, name);
161
+ }
162
+ if (type === "MemberExpression") {
163
+ if (hasIdentifierUseIn(node.object, name)) {
164
+ return true;
165
+ }
166
+ if (node.computed === true) {
167
+ return hasIdentifierUseIn(node.property, name);
168
+ }
169
+ return false;
170
+ }
171
+ if (type === "Property") {
172
+ if (node.computed === true && hasIdentifierUseIn(node.key, name)) {
173
+ return true;
174
+ }
175
+ return hasIdentifierUseIn(node.value, name);
176
+ }
177
+ if (type === "ForStatement") {
178
+ const init = node.init;
179
+ if (init !== null &&
180
+ init !== undefined &&
181
+ init.type !== "VariableDeclaration") {
182
+ if (hasIdentifierUseIn(init, name)) {
183
+ return true;
184
+ }
185
+ }
186
+ if (hasIdentifierUseIn(node.test, name)) {
187
+ return true;
188
+ }
189
+ if (hasIdentifierUseIn(node.update, name)) {
190
+ return true;
191
+ }
192
+ return hasIdentifierUseIn(node.body, name);
193
+ }
194
+ if (type === "ForOfStatement" || type === "ForInStatement") {
195
+ const left = node.left;
196
+ if (left !== null &&
197
+ left !== undefined &&
198
+ left.type !== "VariableDeclaration") {
199
+ if (hasIdentifierUseIn(left, name)) {
200
+ return true;
201
+ }
202
+ }
203
+ if (hasIdentifierUseIn(node.right, name)) {
204
+ return true;
205
+ }
206
+ return hasIdentifierUseIn(node.body, name);
207
+ }
208
+ if (type === "CatchClause") {
209
+ return hasIdentifierUseIn(node.body, name);
210
+ }
211
+ if (type === "ImportSpecifier" ||
212
+ type === "ImportDefaultSpecifier" ||
213
+ type === "ImportNamespaceSpecifier" ||
214
+ type === "LabeledStatement" ||
215
+ type === "BreakStatement" ||
216
+ type === "ContinueStatement") {
217
+ return hasIdentifierUseIn(node.body, name);
218
+ }
219
+ if (type === "ClassDeclaration" || type === "ClassExpression") {
220
+ if (hasIdentifierUseIn(node.superClass, name)) {
221
+ return true;
222
+ }
223
+ return hasIdentifierUseIn(node.body, name);
224
+ }
225
+ const indexed = node;
226
+ for (const key of Object.keys(indexed)) {
227
+ if (key === "loc" ||
228
+ key === "range" ||
229
+ key === "parent" ||
230
+ key === "type") {
231
+ continue;
232
+ }
233
+ if (hasIdentifierUseIn(indexed[key], name)) {
234
+ return true;
235
+ }
236
+ }
237
+ return false;
238
+ };
239
+ const outerDescriptorOf = (frame) => {
240
+ if (frame.kind === "array-callback") {
241
+ return `Array.prototype.${frame.methodName} callback`;
242
+ }
243
+ return `${frame.kind} loop`;
244
+ };
74
245
  const rule = {
75
246
  meta: {
76
247
  type: "suggestion",
77
248
  docs: {
78
- description: "Enforce naming convention for Array.prototype callback arguments (v/i/arr, acc, prev, a/b).",
249
+ description: "Ban thoughtless single-character variable names in iteration contexts (Array.prototype callbacks, for / for-of / for-in).",
79
250
  },
80
251
  schema: [],
81
252
  },
82
253
  create(context) {
83
- const callbackStack = [];
254
+ const frameStack = [];
84
255
  const callbackToFrame = new WeakMap();
85
- const reportInner = (param, methodName, kind, paramIndex) => {
86
- const expected = expectedNamesFor(kind)[paramIndex];
256
+ const forNodeToFrame = new WeakMap();
257
+ const isReduceFirstPosition = (frame, paramIndex) => {
258
+ return (paramIndex === 0 &&
259
+ (frame.methodKind === "reduce-sync" ||
260
+ frame.methodKind === "reduce-async"));
261
+ };
262
+ const reportInnerArrayCallback = (param, frame, paramIndex, body) => {
263
+ const expected = expectedNamesFor(frame.methodKind)[paramIndex];
87
264
  if (expected === undefined) {
88
265
  return;
89
266
  }
@@ -93,20 +270,60 @@ const rule = {
93
270
  if (param.name === expected) {
94
271
  return;
95
272
  }
273
+ if (param.name === DISCARD_NAME &&
274
+ !isReduceFirstPosition(frame, paramIndex)) {
275
+ if (!hasIdentifierUseIn(body, DISCARD_NAME)) {
276
+ return;
277
+ }
278
+ context.report({
279
+ message: `Discard parameter '_' is referenced inside the body; rename it to a meaningful name.`,
280
+ node: param,
281
+ });
282
+ return;
283
+ }
96
284
  context.report({
97
- message: `Array.prototype.${methodName} expects '${expected}' for argument ${paramIndex + 1} (got: '${param.name}').`,
285
+ message: `Array.prototype.${frame.methodName} expects '${expected}' for argument ${paramIndex + 1} (got: '${param.name}').`,
98
286
  node: param,
99
287
  });
100
288
  };
101
- const reportOuter = (param, methodName) => {
289
+ const reportInnerFor = (param, frame, body) => {
102
290
  if (param.name.length >= 2) {
103
291
  return;
104
292
  }
293
+ if (FOR_ALLOWED_INNER.has(param.name)) {
294
+ return;
295
+ }
296
+ if (param.name === DISCARD_NAME) {
297
+ if (!hasIdentifierUseIn(body, DISCARD_NAME)) {
298
+ return;
299
+ }
300
+ context.report({
301
+ message: `Discard parameter '_' is referenced inside the body; rename it to a meaningful name.`,
302
+ node: param,
303
+ });
304
+ return;
305
+ }
105
306
  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.`,
307
+ message: `${frame.kind} loop variable '${param.name}' is not allowed; use 'k', 'v', 'i' or a meaningful name with 2 or more characters.`,
107
308
  node: param,
108
309
  });
109
310
  };
311
+ const reportOuter = (param, frame) => {
312
+ if (param.name.length >= 2) {
313
+ return;
314
+ }
315
+ const descriptor = outerDescriptorOf(frame);
316
+ context.report({
317
+ message: `Avoid the single-character name '${param.name}' on an outer ${descriptor}; use a meaningful name with 2 or more characters.`,
318
+ node: param,
319
+ });
320
+ };
321
+ const markParentHasNested = () => {
322
+ const top = frameStack[frameStack.length - 1];
323
+ if (top !== undefined) {
324
+ top.hasNested = true;
325
+ }
326
+ };
110
327
  const onCallEnter = (node) => {
111
328
  const methodName = getTargetMethodName(node);
112
329
  if (methodName === null) {
@@ -116,46 +333,78 @@ const rule = {
116
333
  if (callback === null) {
117
334
  return;
118
335
  }
119
- const kind = methodKindOf(methodName, callback.async === true);
120
- if (kind === null) {
336
+ const methodKind = methodKindOf(methodName, callback.async === true);
337
+ if (methodKind === null) {
121
338
  return;
122
339
  }
123
- const frame = { kind, methodName, hasNested: false };
340
+ const frame = {
341
+ kind: "array-callback",
342
+ methodKind,
343
+ methodName,
344
+ hasNested: false,
345
+ };
124
346
  callbackToFrame.set(callback, frame);
125
- const top = callbackStack[callbackStack.length - 1];
126
- if (top !== undefined) {
127
- top.hasNested = true;
128
- }
347
+ markParentHasNested();
129
348
  };
130
349
  const onFunctionEnter = (node) => {
131
350
  const frame = callbackToFrame.get(node);
132
351
  if (frame === undefined) {
133
352
  return;
134
353
  }
135
- callbackStack.push(frame);
354
+ frameStack.push(frame);
136
355
  };
137
356
  const onFunctionExit = (node) => {
138
357
  const frame = callbackToFrame.get(node);
139
358
  if (frame === undefined) {
140
359
  return;
141
360
  }
142
- const popped = callbackStack.pop();
361
+ const popped = frameStack.pop();
143
362
  if (popped !== frame) {
144
363
  return;
145
364
  }
146
- const expected = expectedNamesFor(frame.kind);
365
+ const expected = expectedNamesFor(frame.methodKind);
147
366
  const params = node.params;
148
367
  const limit = Math.min(params.length, expected.length);
368
+ const body = node.body;
149
369
  for (let i = 0; i < limit; i++) {
150
370
  const param = params[i];
151
371
  if (!isIdentifier(param)) {
152
372
  continue;
153
373
  }
154
374
  if (frame.hasNested) {
155
- reportOuter(param, frame.methodName);
375
+ reportOuter(param, frame);
376
+ }
377
+ else {
378
+ reportInnerArrayCallback(param, frame, i, body);
379
+ }
380
+ }
381
+ };
382
+ const onForEnter = (node) => {
383
+ const frame = {
384
+ kind: forKindOf(node),
385
+ hasNested: false,
386
+ };
387
+ markParentHasNested();
388
+ frameStack.push(frame);
389
+ forNodeToFrame.set(node, frame);
390
+ };
391
+ const onForExit = (node) => {
392
+ const frame = forNodeToFrame.get(node);
393
+ if (frame === undefined) {
394
+ return;
395
+ }
396
+ const popped = frameStack.pop();
397
+ if (popped !== frame) {
398
+ return;
399
+ }
400
+ const params = collectForLoopVarIdentifiers(node);
401
+ const body = node.body;
402
+ for (const param of params) {
403
+ if (frame.hasNested) {
404
+ reportOuter(param, frame);
156
405
  }
157
406
  else {
158
- reportInner(param, frame.methodName, frame.kind, i);
407
+ reportInnerFor(param, frame, body);
159
408
  }
160
409
  }
161
410
  };
@@ -165,13 +414,19 @@ const rule = {
165
414
  "ArrowFunctionExpression:exit": onFunctionExit,
166
415
  FunctionExpression: onFunctionEnter,
167
416
  "FunctionExpression:exit": onFunctionExit,
417
+ ForStatement: onForEnter,
418
+ "ForStatement:exit": onForExit,
419
+ ForOfStatement: onForEnter,
420
+ "ForOfStatement:exit": onForExit,
421
+ ForInStatement: onForEnter,
422
+ "ForInStatement:exit": onForExit,
168
423
  };
169
424
  },
170
425
  };
171
426
  const plugin = {
172
427
  meta: { name: "crescware-iteration-var-names" },
173
428
  rules: {
174
- "array-callback-arg-names": rule,
429
+ "iteration-var-names": rule,
175
430
  },
176
431
  };
177
432
  export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crescware/eslint-plugin-crescware-iteration-var-names",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "files": [
5
5
  "dist"
6
6
  ],