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