@cookshack/eslint-config 1.1.0 → 2.0.5
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/.build.yml +1 -1
- package/.husky/pre-commit +1 -0
- package/dist/index.cjs +424 -24
- package/dist/index.js +427 -28
- package/index.js +470 -27
- package/package.json +7 -3
- package/test/rules/narrowest-scope.js +571 -0
- package/test/rules/positive-vibes.js +34 -0
package/.build.yml
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npm run check && npm test
|
package/dist/index.cjs
CHANGED
|
@@ -6,29 +6,426 @@ var globals = require('globals');
|
|
|
6
6
|
|
|
7
7
|
exports.rules = void 0; exports.languageOptions = void 0; exports.plugins = void 0;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
9
|
+
let printBuffer;
|
|
10
|
+
|
|
11
|
+
printBuffer = [];
|
|
12
|
+
|
|
13
|
+
function print
|
|
14
|
+
(...args) {
|
|
15
|
+
//console.log(args.join(' '))
|
|
16
|
+
printBuffer.push(args.join(' '));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function trace
|
|
20
|
+
(...args) {
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getPrintBuffer
|
|
24
|
+
() {
|
|
25
|
+
return printBuffer.join('\n')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearPrintBuffer
|
|
29
|
+
() {
|
|
30
|
+
printBuffer = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getNarrowestScope
|
|
34
|
+
(variable) {
|
|
35
|
+
let common;
|
|
36
|
+
|
|
37
|
+
common = null;
|
|
38
|
+
for (let ref of variable.references) {
|
|
39
|
+
if (variable.defs.some(def => def.name === ref.identifier))
|
|
40
|
+
continue
|
|
41
|
+
if (ref.from)
|
|
42
|
+
if (common)
|
|
43
|
+
common = getCommonAncestor(common, ref.from);
|
|
44
|
+
else
|
|
45
|
+
common = ref.from;
|
|
46
|
+
}
|
|
47
|
+
return common
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getCommonAncestor
|
|
51
|
+
(scope1, scope2) {
|
|
52
|
+
let ancestors, s;
|
|
53
|
+
|
|
54
|
+
ancestors = [];
|
|
55
|
+
s = scope1;
|
|
56
|
+
while (s) {
|
|
57
|
+
if (s.type == 'global')
|
|
58
|
+
break
|
|
59
|
+
ancestors.push(s);
|
|
60
|
+
s = s.upper;
|
|
61
|
+
}
|
|
62
|
+
s = scope2;
|
|
63
|
+
while (s) {
|
|
64
|
+
if (s.type == 'global')
|
|
65
|
+
break
|
|
66
|
+
if (ancestors.includes(s))
|
|
67
|
+
return s
|
|
68
|
+
s = s.upper;
|
|
69
|
+
}
|
|
70
|
+
return scope1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getDefinitionScope
|
|
74
|
+
(variable) {
|
|
75
|
+
return variable.scope
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isWriteRef
|
|
79
|
+
(ref) {
|
|
80
|
+
let parent;
|
|
81
|
+
|
|
82
|
+
parent = ref.identifier.parent;
|
|
83
|
+
if (parent) {
|
|
84
|
+
if (parent.type == 'AssignmentExpression' && parent.left == ref.identifier)
|
|
85
|
+
return 1
|
|
86
|
+
if (parent.type == 'UpdateExpression')
|
|
87
|
+
return 1
|
|
88
|
+
if (parent.type == 'VariableDeclarator' && parent.id == ref.identifier)
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isReadRef
|
|
95
|
+
(ref) {
|
|
96
|
+
if (isWriteRef(ref))
|
|
97
|
+
return 0
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getConditionalContext
|
|
102
|
+
(ref) {
|
|
103
|
+
let node, prevNode, scopeBlock;
|
|
104
|
+
|
|
105
|
+
scopeBlock = ref.from.block;
|
|
106
|
+
prevNode = ref.identifier;
|
|
107
|
+
node = ref.identifier.parent;
|
|
108
|
+
while (node) {
|
|
109
|
+
if (node === scopeBlock)
|
|
110
|
+
break
|
|
111
|
+
if (node.type === 'IfStatement')
|
|
112
|
+
if (prevNode === node.test || nodeContains(node.test, prevNode))
|
|
113
|
+
prevNode = node;
|
|
114
|
+
else
|
|
115
|
+
return 'B'
|
|
116
|
+
else if ([ 'WhileStatement', 'DoWhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement', 'SwitchStatement' ].includes(node.type))
|
|
117
|
+
if (prevNode === node.test || nodeContains(node.test, prevNode))
|
|
118
|
+
prevNode = node;
|
|
119
|
+
else
|
|
120
|
+
return 'B'
|
|
121
|
+
else
|
|
122
|
+
prevNode = node;
|
|
123
|
+
node = node.parent;
|
|
124
|
+
}
|
|
125
|
+
return ''
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function nodeContains(node, target) {
|
|
129
|
+
if (node === target)
|
|
130
|
+
return true
|
|
131
|
+
if (node && typeof node === 'object')
|
|
132
|
+
for (let key in node)
|
|
133
|
+
if (nodeHas(node[key], target))
|
|
134
|
+
return true
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function nodeHas(value, target) {
|
|
139
|
+
if (value === target)
|
|
140
|
+
return true
|
|
141
|
+
if (Array.isArray(value))
|
|
142
|
+
return value.some(v => nodeContains(v, target))
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isConditionalRef
|
|
147
|
+
(ref, narrowestScope) {
|
|
148
|
+
let node;
|
|
149
|
+
|
|
150
|
+
node = ref.identifier.parent;
|
|
151
|
+
|
|
152
|
+
while (node) {
|
|
153
|
+
if (node === narrowestScope.block)
|
|
154
|
+
break
|
|
155
|
+
if (node.type === 'BlockStatement') {
|
|
156
|
+
let parent;
|
|
157
|
+
|
|
158
|
+
parent = node.parent;
|
|
159
|
+
if (parent?.type === 'IfStatement' && (parent.consequent === node || parent.alternate === node))
|
|
160
|
+
return true
|
|
161
|
+
if ([ 'WhileStatement', 'DoWhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement' ].includes(parent?.type) && parent.body === node)
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
node = node.parent;
|
|
165
|
+
}
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function markConditionalRefs
|
|
170
|
+
(variable, scopeToNode, narrowestScope) {
|
|
171
|
+
for (let ref of variable.references) {
|
|
172
|
+
let refNode, rItems, item;
|
|
173
|
+
|
|
174
|
+
refNode = scopeToNode.get(ref.from);
|
|
175
|
+
rItems = refNode.items.filter(i => i.ref == ref);
|
|
176
|
+
item = rItems[0];
|
|
177
|
+
if (item && (item.ctx == 'B' || isConditionalRef(ref, narrowestScope)))
|
|
178
|
+
item.isConditional = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function mayBeReadBeforeAnyWrite
|
|
183
|
+
(variable, scopeToNode, narrowestScope) {
|
|
184
|
+
let refs;
|
|
185
|
+
|
|
186
|
+
refs = [ ...variable.references ];
|
|
187
|
+
refs.sort((a, b) => (a.cookshackNarrowestScopeItem?.pos ?? a.identifier.range[0]) - (b.cookshackNarrowestScopeItem?.pos ?? b.identifier.range[0]));
|
|
188
|
+
|
|
189
|
+
for (let ref of refs) {
|
|
190
|
+
let item;
|
|
191
|
+
|
|
192
|
+
if (isReadRef(ref))
|
|
193
|
+
// a possible read
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
item = ref.cookshackNarrowestScopeItem;
|
|
197
|
+
if (item.ctx == 'B' || isConditionalRef(ref, narrowestScope))
|
|
198
|
+
// a conditional write
|
|
199
|
+
continue
|
|
200
|
+
// A guaranteed write before any possible read.
|
|
201
|
+
return 0
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function scopeStart(scope) {
|
|
206
|
+
if (scope.block == null)
|
|
207
|
+
return Infinity
|
|
208
|
+
if (scope.type == 'function' && scope.block.id)
|
|
209
|
+
return scope.block.id.range[1]
|
|
210
|
+
if (scope.type == 'class' && scope.block.id)
|
|
211
|
+
return scope.block.id.range[0]
|
|
212
|
+
return scope.block.range[0]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildScopeTree(scope, prefix, scopeToNode) {
|
|
216
|
+
let node, siblingNum;
|
|
217
|
+
|
|
218
|
+
node = {
|
|
219
|
+
scope,
|
|
220
|
+
prefix,
|
|
221
|
+
items: [],
|
|
222
|
+
children: []
|
|
223
|
+
};
|
|
224
|
+
scopeToNode.set(scope, node);
|
|
225
|
+
|
|
226
|
+
siblingNum = 0;
|
|
227
|
+
for (let child of scope.childScopes) {
|
|
228
|
+
siblingNum++;
|
|
229
|
+
node.children.push(buildScopeTree(child, prefix + '.' + siblingNum, scopeToNode));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (let variable of scope.variables) {
|
|
233
|
+
if (variable.defs.length > 0)
|
|
234
|
+
node.items.push({ type: 'LET', name: variable.name, pos: variable.defs[0].name.range[0] });
|
|
235
|
+
|
|
236
|
+
for (let ref of variable.references) {
|
|
237
|
+
let targetNode;
|
|
238
|
+
|
|
239
|
+
targetNode = scopeToNode.get(ref.from);
|
|
240
|
+
if (targetNode) {
|
|
241
|
+
let parent, sortPos, ctx, item;
|
|
242
|
+
|
|
243
|
+
ctx = getConditionalContext(ref);
|
|
244
|
+
parent = ref.identifier.parent;
|
|
245
|
+
|
|
246
|
+
if (isWriteRef(ref))
|
|
247
|
+
if (ref.identifier.parent?.type == 'UpdateExpression') {
|
|
248
|
+
targetNode.items.push({ type: 'READ', name: ref.identifier.name, ctx, pos: ref.identifier.range[0] });
|
|
249
|
+
item = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] };
|
|
250
|
+
}
|
|
251
|
+
else if (ref.identifier.parent?.type == 'AssignmentExpression') {
|
|
252
|
+
sortPos = parent.right.range[1] + 0.4;
|
|
253
|
+
item = { ref, type: 'WRITE', name: ref.identifier.name, ctx, pos: sortPos };
|
|
254
|
+
}
|
|
255
|
+
else if (ref.identifier.parent?.type == 'VariableDeclarator')
|
|
256
|
+
item = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] + 0.4 };
|
|
257
|
+
else
|
|
258
|
+
item = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] };
|
|
259
|
+
else {
|
|
260
|
+
let declarator;
|
|
261
|
+
|
|
262
|
+
declarator = parent;
|
|
263
|
+
while (declarator)
|
|
264
|
+
if (declarator.type == 'VariableDeclarator')
|
|
265
|
+
break
|
|
266
|
+
else
|
|
267
|
+
declarator = declarator.parent;
|
|
268
|
+
if (declarator?.type == 'VariableDeclarator' && nodeContains(declarator.init, ref.identifier))
|
|
269
|
+
sortPos = declarator.id ? declarator.id.range[0] - 0.4 : ref.identifier.range[0];
|
|
270
|
+
else
|
|
271
|
+
sortPos = ref.identifier.range[0];
|
|
272
|
+
item = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: sortPos };
|
|
273
|
+
}
|
|
274
|
+
targetNode.items.push(item);
|
|
275
|
+
ref.cookshackNarrowestScopeItem = item;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
node.items.sort((a, b) => a.pos - b.pos);
|
|
281
|
+
|
|
282
|
+
return node
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function checkScopeNode(context, treeNode, reported, scopeToNode) {
|
|
286
|
+
let indent;
|
|
287
|
+
|
|
288
|
+
reported = reported || new Set;
|
|
289
|
+
indent = ' '.repeat(treeNode.prefix.split('.').length - 1);
|
|
290
|
+
|
|
291
|
+
for (let variable of treeNode.scope.variables) {
|
|
292
|
+
let defNode;
|
|
293
|
+
|
|
294
|
+
if (reported.has(variable))
|
|
295
|
+
continue
|
|
296
|
+
if (variable.defs.length === 0)
|
|
297
|
+
continue
|
|
298
|
+
if ([ 'Parameter', 'FunctionName', 'ImportBinding', 'CatchClause', 'ClassName' ].includes(variable.defs[0].type))
|
|
299
|
+
continue
|
|
300
|
+
if (variable.defs[0].node.parent?.parent?.type === 'ExportNamedDeclaration')
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
defNode = variable.defs[0]?.name;
|
|
304
|
+
if (defNode) {
|
|
305
|
+
let defScope, narrowestScope, defNodePrefix;
|
|
306
|
+
|
|
307
|
+
defScope = getDefinitionScope(variable);
|
|
308
|
+
defNodePrefix = scopeToNode.get(defScope)?.prefix ?? '?';
|
|
309
|
+
trace(indent, '1 found decl scope of', variable.name + ':', defNodePrefix + ' ' + defScope.type.toUpperCase());
|
|
310
|
+
|
|
311
|
+
narrowestScope = getNarrowestScope(variable);
|
|
312
|
+
if (narrowestScope) {
|
|
313
|
+
let narrowestPrefix;
|
|
314
|
+
|
|
315
|
+
narrowestPrefix = scopeToNode.get(narrowestScope)?.prefix ?? '?';
|
|
316
|
+
trace(indent, '2 found narrowest scope of', variable.name + ':', narrowestPrefix + ' ' + narrowestScope?.type.toUpperCase());
|
|
317
|
+
|
|
318
|
+
markConditionalRefs(variable, scopeToNode, narrowestScope);
|
|
319
|
+
|
|
320
|
+
if (defScope == narrowestScope)
|
|
321
|
+
continue
|
|
322
|
+
trace(indent, '3', variable.name, 'could be moved to a narrower scope');
|
|
323
|
+
|
|
324
|
+
if (defScope.type == 'for') {
|
|
325
|
+
trace(indent, '4 exception:', variable.name, 'is in a for loop header');
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (mayBeReadBeforeAnyWrite(variable, scopeToNode, narrowestScope)) {
|
|
329
|
+
trace(indent, '4 exception:', variable.name, 'mayBeReadBeforeAnyWrite');
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
trace(indent, '5', variable.name, 'is too broad');
|
|
334
|
+
|
|
335
|
+
reported.add(variable);
|
|
336
|
+
context.report({
|
|
337
|
+
node: defNode,
|
|
338
|
+
messageId: 'tooBroad',
|
|
339
|
+
data: { name: variable.name }
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (let child of treeNode.children)
|
|
346
|
+
checkScopeNode(context, child, reported, scopeToNode);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printTree(node, siblingNum) {
|
|
350
|
+
let prefix, all, indent;
|
|
351
|
+
|
|
352
|
+
prefix = siblingNum === 0 ? node.prefix : node.prefix.split('.').slice(0, -1).join('.') + '.' + siblingNum;
|
|
353
|
+
indent = ' '.repeat(prefix.split('.').length - 1);
|
|
354
|
+
{
|
|
355
|
+
let name;
|
|
356
|
+
|
|
357
|
+
name = node.scope.block?.id?.name ?? node.scope.block?.parent?.key?.name;
|
|
358
|
+
print(indent + 'SCOPE ' + prefix + ' ' + node.scope.type.toUpperCase() + ' pos ' + scopeStart(node.scope) + (name ? ' name ' + name : ''));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
all = [ ...node.items.map(i => ({ pos: i.pos, type: 'item', data: i })),
|
|
362
|
+
...node.children.map((c, i) => ({ pos: scopeStart(c.scope), type: 'scope', data: c, sibling: i + 1 })) ];
|
|
363
|
+
all.sort((a, b) => a.pos - b.pos);
|
|
364
|
+
|
|
365
|
+
for (let entry of all)
|
|
366
|
+
if (entry.type === 'item')
|
|
367
|
+
print(indent
|
|
368
|
+
+ ' ' + entry.data.type.padEnd(5)
|
|
369
|
+
+ ' ' + entry.data.name
|
|
370
|
+
// B: is the ref conditional within the scope that "owns" the ref (for single statement `if`)
|
|
371
|
+
+ (entry.data.ctx ? ' ' + entry.data.ctx : '').padEnd(3)
|
|
372
|
+
// C: is the ref conditional within the variable's narrowestScope?
|
|
373
|
+
+ (entry.data.isConditional ? 'C' : ' ').padEnd(2)
|
|
374
|
+
+ 'pos ' + entry.data.pos);
|
|
375
|
+
else
|
|
376
|
+
printTree(entry.data, entry.sibling);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function createNarrowestScope
|
|
380
|
+
(context) {
|
|
381
|
+
let scopeManager;
|
|
382
|
+
|
|
383
|
+
clearPrintBuffer();
|
|
384
|
+
scopeManager = context.sourceCode.scopeManager;
|
|
385
|
+
if (scopeManager)
|
|
386
|
+
return {
|
|
387
|
+
'Program:exit'() {
|
|
388
|
+
let tree, scopeToNode;
|
|
389
|
+
|
|
390
|
+
scopeToNode = new Map;
|
|
391
|
+
tree = buildScopeTree(scopeManager.scopes[0], '1', scopeToNode);
|
|
392
|
+
checkScopeNode(context, tree, null, scopeToNode);
|
|
393
|
+
printTree(tree, 0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function createPositiveVibes
|
|
399
|
+
(context) {
|
|
400
|
+
return {
|
|
401
|
+
UnaryExpression(node) {
|
|
402
|
+
if (node.operator == '!')
|
|
403
|
+
context.report({ node,
|
|
404
|
+
messageId: 'positiveVibes' });
|
|
405
|
+
},
|
|
406
|
+
BinaryExpression(node) {
|
|
407
|
+
if (node.operator == '!=')
|
|
408
|
+
context.report({ node,
|
|
409
|
+
messageId: 'equality' });
|
|
410
|
+
else if (node.operator == '!==')
|
|
411
|
+
context.report({ node,
|
|
412
|
+
messageId: 'strictEquality' });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
exports.plugins = { 'cookshack': { rules: { 'positive-vibes': { meta: { type: 'problem',
|
|
418
|
+
docs: { description: 'Prefer positive expressions.' },
|
|
419
|
+
messages: { positiveVibes: 'Be positive!',
|
|
420
|
+
equality: 'Use ==.',
|
|
421
|
+
strictEquality: 'Use ===.' },
|
|
422
|
+
schema: [] },
|
|
423
|
+
create: createPositiveVibes },
|
|
424
|
+
'narrowest-scope': { meta: { type: 'suggestion',
|
|
425
|
+
docs: { description: 'Enforce variables are declared in their narrowest possible scope.' },
|
|
426
|
+
messages: { tooBroad: 'Variable "{{ name }}" is declared in a broader scope than necessary.' },
|
|
427
|
+
schema: [] },
|
|
428
|
+
create: createNarrowestScope } } } };
|
|
32
429
|
|
|
33
430
|
exports.rules = {
|
|
34
431
|
'array-bracket-newline': [ 'error', 'never' ],
|
|
@@ -59,13 +456,15 @@ exports.rules = {
|
|
|
59
456
|
{ blankLine: 'never', prev: 'let', next: 'let' } ],
|
|
60
457
|
'no-case-declarations': 'error',
|
|
61
458
|
'no-global-assign': 'error',
|
|
62
|
-
'cookshack/
|
|
459
|
+
'cookshack/narrowest-scope': 'error',
|
|
460
|
+
'cookshack/positive-vibes': 'error',
|
|
63
461
|
'no-mixed-operators': 'error',
|
|
64
462
|
'no-multi-spaces': 'error',
|
|
65
463
|
'no-multiple-empty-lines': [ 'error', { max: 1, maxEOF: 0 } ],
|
|
66
464
|
'no-negated-condition': 'error',
|
|
67
465
|
'no-redeclare': 'error',
|
|
68
466
|
'no-sequences': 'error',
|
|
467
|
+
'no-sparse-arrays': 'error',
|
|
69
468
|
'no-tabs': 'error',
|
|
70
469
|
'no-trailing-spaces': 'error',
|
|
71
470
|
'no-undef': 'error',
|
|
@@ -96,3 +495,4 @@ var index = [ { ignores: [ 'TAGS.mjs' ] },
|
|
|
96
495
|
rules: exports.rules } ];
|
|
97
496
|
|
|
98
497
|
exports.default = index;
|
|
498
|
+
exports.getPrintBuffer = getPrintBuffer;
|