@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/.build.yml CHANGED
@@ -4,7 +4,7 @@ packages:
4
4
  - npm
5
5
  secrets:
6
6
  - e2e843cf-6f7e-4b74-989f-819a97ca70b8
7
- - 693e186a-109b-4e62-9d15-a123679909cc
7
+ - e3a02b8f-1f91-4a2c-9744-3bf7bb0257d6
8
8
  sources:
9
9
  - https://git.sr.ht/~mattmundell/build
10
10
  - git+ssh://git@git.sr.ht/~mattmundell/eslint-config
package/.husky/pre-commit CHANGED
@@ -1 +1 @@
1
- npm run check
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
- exports.plugins = { 'cookshack': { rules: { 'no-logical-not': { meta: { type: 'problem',
10
- docs: { description: 'Prevent !.' },
11
- messages: { logicalNot: 'Logical not used.',
12
- inequality: 'Inequality operator used.',
13
- strictInequality: 'Strict inequality operator used.' },
14
- schema: [] }, // options
15
- create(context) {
16
- return {
17
- UnaryExpression(node) {
18
- if (node.operator == '!')
19
- context.report({ node,
20
- messageId: 'logicalNot' });
21
- },
22
- BinaryExpression(node) {
23
- if (node.operator == '!=')
24
- context.report({ node,
25
- messageId: 'inequality' });
26
- else if (node.operator == '!==')
27
- context.report({ node,
28
- messageId: 'strictInequality' });
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,7 +456,8 @@ exports.rules = {
59
456
  { blankLine: 'never', prev: 'let', next: 'let' } ],
60
457
  'no-case-declarations': 'error',
61
458
  'no-global-assign': 'error',
62
- 'cookshack/no-logical-not': 'error',
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 } ],
@@ -97,3 +495,4 @@ var index = [ { ignores: [ 'TAGS.mjs' ] },
97
495
  rules: exports.rules } ];
98
496
 
99
497
  exports.default = index;
498
+ exports.getPrintBuffer = getPrintBuffer;