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