@cookshack/eslint-config 2.0.5 → 3.0.0

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.
@@ -0,0 +1,473 @@
1
+ let varIds, nextVarId, printBuffer
2
+
3
+ printBuffer = []
4
+ varIds = new Map()
5
+ nextVarId = 0
6
+
7
+ function print
8
+ (...args) {
9
+ printBuffer.push(args.join(' '))
10
+ }
11
+
12
+ function trace
13
+ (...args) {
14
+ if (0)
15
+ console.log('TRACE', ...args)
16
+ }
17
+
18
+ export
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 hasReadBeforeWriteInNestedScope
143
+ (variable, defScope) {
144
+ let nestedFunctions
145
+
146
+ nestedFunctions = new Set(variable.references
147
+ .filter(ref => {
148
+ let refScope
149
+
150
+ refScope = ref.from
151
+ if (refScope == defScope)
152
+ return 0
153
+ return isProperAncestor(defScope, refScope) && (refScope.type == 'function' || refScope.type == 'arrow')
154
+ })
155
+ .map(ref => ref.from))
156
+ for (let fnScope of nestedFunctions) {
157
+ let fnRefs, hasRead, hasWrite
158
+
159
+ fnRefs = variable.references.filter(ref => ref.from == fnScope || isProperAncestor(fnScope, ref.from))
160
+ hasRead = fnRefs.some(ref => isReadRef(ref))
161
+ hasWrite = fnRefs.some(ref => isWriteRef(ref))
162
+ if (hasRead && hasWrite)
163
+ return 1
164
+ }
165
+ return 0
166
+ }
167
+
168
+ function isConditionalRef
169
+ (ref, narrowestScope) {
170
+ let node
171
+
172
+ node = ref.identifier.parent
173
+
174
+ while (node) {
175
+ if (node == narrowestScope.block)
176
+ break
177
+ if (node.type == 'BlockStatement') {
178
+ let parent
179
+
180
+ parent = node.parent
181
+ if (parent?.type == 'IfStatement' && (parent.consequent == node || parent.alternate == node))
182
+ return true
183
+ if ([ 'WhileStatement', 'DoWhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement' ].includes(parent?.type) && parent.body == node)
184
+ return true
185
+ }
186
+ node = node.parent
187
+ }
188
+ return false
189
+ }
190
+
191
+ function markConditionalRefs
192
+ (variable, scopeToNode, narrowestScope) {
193
+ for (let ref of variable.references) {
194
+ let refNode, rItems, item
195
+
196
+ refNode = scopeToNode.get(ref.from)
197
+ rItems = refNode.items.filter(i => i.ref == ref)
198
+ item = rItems[0]
199
+ if (item && (item.ctx == 'B' || isConditionalRef(ref, narrowestScope)))
200
+ item.isConditional = true
201
+ }
202
+ }
203
+
204
+ function mayBeReadBeforeAnyWrite
205
+ (variable, scopeToNode, narrowestScope) {
206
+ let refs
207
+
208
+ refs = [ ...variable.references ]
209
+ refs.sort((a, b) => (a.cookshackNarrowestScopeItem?.pos ?? a.identifier.range[0]) - (b.cookshackNarrowestScopeItem?.pos ?? b.identifier.range[0]))
210
+
211
+ for (let ref of refs) {
212
+ let item
213
+
214
+ if (isReadRef(ref))
215
+ return 1
216
+
217
+ item = ref.cookshackNarrowestScopeItem
218
+ if (item.ctx == 'B' || isConditionalRef(ref, narrowestScope))
219
+ continue
220
+ return 0
221
+ }
222
+ }
223
+
224
+ function isProperAncestor
225
+ (ancestor, descendant) {
226
+ let s
227
+
228
+ s = descendant.upper
229
+ while (s) {
230
+ if (s == ancestor)
231
+ return 1
232
+ s = s.upper
233
+ }
234
+ return 0
235
+ }
236
+
237
+ function scopeStart(scope) {
238
+ if (scope.block == null)
239
+ return Infinity
240
+ if (scope.type == 'function' && scope.block.id)
241
+ return scope.block.id.range[1]
242
+ if (scope.type == 'class' && scope.block.id)
243
+ return scope.block.id.range[0]
244
+ return scope.block.range[0]
245
+ }
246
+
247
+ export { isReadRef, isWriteRef, buildScopeTree, scopeStart }
248
+
249
+ function ensureInVarIds
250
+ (variable) {
251
+ if (varIds.has(variable))
252
+ return
253
+ varIds.set(variable, nextVarId++)
254
+ }
255
+
256
+ function isCompoundAssignmentOp
257
+ (op) {
258
+ if (op == '=')
259
+ return 0
260
+ return 1
261
+ }
262
+
263
+ function buildScopeTree
264
+ (scope, prefix, scopeToNode, astToTree) {
265
+ let node, siblingNum
266
+
267
+ node = {
268
+ scope,
269
+ prefix,
270
+ items: [],
271
+ children: []
272
+ }
273
+ scopeToNode.set(scope, node)
274
+ if (scope.block && astToTree)
275
+ astToTree.set(scope.block, node)
276
+
277
+ siblingNum = 0
278
+ for (let child of scope.childScopes) {
279
+ siblingNum++
280
+ node.children.push(buildScopeTree(child, prefix + '.' + siblingNum, scopeToNode, astToTree))
281
+ }
282
+
283
+ for (let variable of scope.variables) {
284
+ if (variable.defs.length > 0) {
285
+ ensureInVarIds(variable)
286
+ node.items.push({ type: 'LET', name: variable.name, pos: variable.defs[0].name.range[0], defNode: variable.defs[0].node, defType: variable.defs[0].type, identifier: variable.defs[0].name, variable, varId: varIds.get(variable) })
287
+ }
288
+
289
+ for (let ref of variable.references) {
290
+ let targetNode
291
+
292
+ targetNode = scopeToNode.get(ref.from)
293
+ if (targetNode) {
294
+ let parent, sortPos, ctx, item1, item2
295
+
296
+ ctx = getConditionalContext(ref)
297
+ parent = ref.identifier.parent
298
+
299
+ if (isWriteRef(ref))
300
+ if (ref.identifier.parent?.type == 'UpdateExpression') {
301
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: ref.identifier.range[0] }
302
+ item2 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] }
303
+ }
304
+ else if (ref.identifier.parent?.type == 'AssignmentExpression') {
305
+ sortPos = parent.right.range[1] + 0.4
306
+ if (ref.identifier.parent.left == ref.identifier && isCompoundAssignmentOp(ref.identifier.parent.operator)) {
307
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: ref.identifier.range[0] }
308
+ item2 = { ref, type: 'WRITE', name: ref.identifier.name, pos: sortPos }
309
+ }
310
+ else
311
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, ctx, pos: sortPos }
312
+ }
313
+ else if (ref.identifier.parent?.type == 'VariableDeclarator')
314
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] + 0.4 }
315
+ else
316
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] }
317
+ else {
318
+ let declarator
319
+
320
+ declarator = parent
321
+ while (declarator)
322
+ if (declarator.type == 'VariableDeclarator')
323
+ break
324
+ else
325
+ declarator = declarator.parent
326
+ if (declarator?.type == 'VariableDeclarator' && nodeContains(declarator.init, ref.identifier))
327
+ sortPos = declarator.id ? declarator.id.range[0] - 0.4 : ref.identifier.range[0]
328
+ else
329
+ sortPos = ref.identifier.range[0]
330
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: sortPos }
331
+ }
332
+ ensureInVarIds(variable)
333
+ item1.varId = varIds.get(variable)
334
+ targetNode.items.push(item1)
335
+ if (item2) {
336
+ item2.varId = varIds.get(variable)
337
+ targetNode.items.push(item2)
338
+ }
339
+ ref.cookshackNarrowestScopeItem = item2 || item1
340
+ }
341
+ }
342
+ }
343
+
344
+ node.items.sort((a, b) => a.pos - b.pos)
345
+
346
+ return node
347
+ }
348
+
349
+ function checkScopeNode
350
+ (context, treeNode, reported, scopeToNode) {
351
+ let indent
352
+
353
+ reported = reported || new Set
354
+ indent = ' '.repeat(treeNode.prefix.split('.').length - 1)
355
+
356
+ for (let variable of treeNode.scope.variables) {
357
+ let defNode
358
+
359
+ if (reported.has(variable))
360
+ continue
361
+ if (variable.defs.length == 0)
362
+ continue
363
+ if ([ 'Parameter', 'FunctionName', 'ImportBinding', 'CatchClause', 'ClassName' ].includes(variable.defs[0].type))
364
+ continue
365
+ if (variable.defs[0].node.parent?.parent?.type == 'ExportNamedDeclaration')
366
+ continue
367
+
368
+ defNode = variable.defs[0]?.name
369
+ if (defNode) {
370
+ let defScope, narrowestScope, defNodePrefix
371
+
372
+ defScope = getDefinitionScope(variable)
373
+ defNodePrefix = scopeToNode.get(defScope)?.prefix ?? '?'
374
+ trace(indent, '1 found decl scope of', variable.name + ':', defNodePrefix + ' ' + defScope.type.toUpperCase())
375
+
376
+ narrowestScope = getNarrowestScope(variable)
377
+ if (narrowestScope) {
378
+ let narrowestPrefix
379
+
380
+ narrowestPrefix = scopeToNode.get(narrowestScope)?.prefix ?? '?'
381
+ trace(indent, '2 found narrowest scope of', variable.name + ':', narrowestPrefix + ' ' + narrowestScope?.type.toUpperCase())
382
+
383
+ markConditionalRefs(variable, scopeToNode, narrowestScope)
384
+
385
+ if (defScope == narrowestScope)
386
+ continue
387
+ trace(indent, '3', variable.name, 'could be moved to a narrower scope')
388
+
389
+ if (defScope.type == 'for') {
390
+ trace(indent, '4 exception:', variable.name, 'is in a for loop header')
391
+ continue
392
+ }
393
+ if (0 && hasReadBeforeWriteInNestedScope(variable, defScope)) {
394
+ trace(indent, '4 exception:', variable.name, 'hasReadBeforeWriteInNestedScope')
395
+ continue
396
+ }
397
+ if (mayBeReadBeforeAnyWrite(variable, scopeToNode, narrowestScope)) {
398
+ trace(indent, '4 exception:', variable.name, 'mayBeReadBeforeAnyWrite')
399
+ continue
400
+ }
401
+
402
+ trace(indent, '5', variable.name, 'is too broad')
403
+
404
+ reported.add(variable)
405
+ context.report({
406
+ node: defNode,
407
+ messageId: 'tooBroad',
408
+ data: { name: variable.name }
409
+ })
410
+ }
411
+ }
412
+ }
413
+
414
+ for (let child of treeNode.children)
415
+ checkScopeNode(context, child, reported, scopeToNode)
416
+ }
417
+
418
+ function printTree
419
+ (node, siblingNum) {
420
+ let prefix, all, indent
421
+
422
+ prefix = siblingNum == 0 ? node.prefix : node.prefix.split('.').slice(0, -1).join('.') + '.' + siblingNum
423
+ indent = ' '.repeat(prefix.split('.').length - 1)
424
+ {
425
+ let name
426
+
427
+ name = node.scope.block?.id?.name ?? node.scope.block?.parent?.key?.name
428
+ print(indent + 'SCOPE ' + prefix + ' ' + node.scope.type.toUpperCase() + ' pos ' + scopeStart(node.scope) + (name ? ' name ' + name : ''))
429
+ }
430
+
431
+ all = [ ...node.items.map(i => ({ pos: i.pos, type: 'item', data: i })),
432
+ ...node.children.map((c, i) => ({ pos: scopeStart(c.scope), type: 'scope', data: c, sibling: i + 1 })) ]
433
+ all.sort((a, b) => a.pos - b.pos)
434
+
435
+ for (let entry of all)
436
+ if (entry.type == 'item')
437
+ print(indent
438
+ + ' ' + entry.data.type.padEnd(5)
439
+ + ' ' + entry.data.name
440
+ + (entry.data.ctx ? ' ' + entry.data.ctx : '').padEnd(3)
441
+ + (entry.data.isConditional ? 'C' : ' ').padEnd(2)
442
+ + 'pos ' + entry.data.pos)
443
+ else
444
+ printTree(entry.data, entry.sibling)
445
+ }
446
+
447
+ export
448
+ function createNarrowestScope
449
+ (context) {
450
+ let scopeManager
451
+
452
+ clearPrintBuffer()
453
+ scopeManager = context.sourceCode.scopeManager
454
+ if (scopeManager)
455
+ return {
456
+ 'Program:exit'() {
457
+ let tree, scopeToNode
458
+
459
+ scopeToNode = new Map
460
+ nextVarId = 0
461
+ tree = buildScopeTree(scopeManager.scopes[0], '1', scopeToNode)
462
+ checkScopeNode(context, tree, null, scopeToNode)
463
+ printTree(tree, 0)
464
+ }
465
+ }
466
+ }
467
+
468
+ export
469
+ default { meta: { type: 'suggestion',
470
+ docs: { description: 'Enforce variables are declared in their narrowest possible scope.' },
471
+ messages: { tooBroad: 'Variable "{{ name }}" is declared in a broader scope than necessary.' },
472
+ schema: [] },
473
+ create: createNarrowestScope }
@@ -0,0 +1,27 @@
1
+ function createPositiveVibes
2
+ (context) {
3
+ return {
4
+ UnaryExpression(node) {
5
+ if (node.operator == '!')
6
+ context.report({ node,
7
+ messageId: 'positiveVibes' })
8
+ },
9
+ BinaryExpression(node) {
10
+ if (node.operator == '!=')
11
+ context.report({ node,
12
+ messageId: 'equality' })
13
+ else if (node.operator == '!==')
14
+ context.report({ node,
15
+ messageId: 'strictEquality' })
16
+ }
17
+ }
18
+ }
19
+
20
+ export
21
+ default { meta: { type: 'problem',
22
+ docs: { description: 'Prefer positive expressions.' },
23
+ messages: { positiveVibes: 'Be positive!',
24
+ equality: 'Use ==.',
25
+ strictEquality: 'Use ===.' },
26
+ schema: [] },
27
+ create: createPositiveVibes }
@@ -0,0 +1,11 @@
1
+ export
2
+ default { meta: { type: 'problem',
3
+ docs: { description: 'Enforce use of == instead of ===.' },
4
+ messages: { risky: 'Use ==.' },
5
+ schema: [] },
6
+ create(context) {
7
+ return { BinaryExpression(node) {
8
+ if (node.operator == '===')
9
+ context.report({ node, messageId: 'risky' })
10
+ } }
11
+ } }
@@ -0,0 +1,37 @@
1
+ function
2
+ VariableDeclaration
3
+ (context, node) {
4
+ let parent
5
+
6
+ parent = node.parent
7
+
8
+ for (parent = node.parent; parent; parent = parent.parent)
9
+ if (parent.type == 'BlockStatement')
10
+ break
11
+
12
+ if (parent) {
13
+ let idx
14
+
15
+ if (parent.parent?.type == 'CatchClause')
16
+ return
17
+
18
+ idx = parent.body.indexOf(node)
19
+ for (let i = 0; i < idx; i++) {
20
+ if (parent.body[i].type == 'VariableDeclaration')
21
+ continue
22
+ context.report({ node, messageId: 'varDeclBlockStart' })
23
+ return
24
+ }
25
+ }
26
+ }
27
+ function create
28
+ (context) {
29
+ return { VariableDeclaration: node => VariableDeclaration(context, node) }
30
+ }
31
+
32
+ export
33
+ default { meta: { type: 'suggestion',
34
+ docs: { description: 'Require variable declarations to be at the start of the block.' },
35
+ messages: { varDeclBlockStart: 'VarDecl must be at start of block.' },
36
+ schema: [] },
37
+ create }
@@ -0,0 +1,46 @@
1
+ import { RuleTester } from 'eslint'
2
+ import { plugins } from '../../index.js'
3
+
4
+ let ruleTester, validCases, invalidCases
5
+
6
+ ruleTester = new RuleTester()
7
+ validCases = []
8
+ invalidCases = []
9
+
10
+ function pass
11
+ (code) {
12
+ validCases.push({ code })
13
+ }
14
+
15
+ function fail
16
+ (count, code) {
17
+ let errors
18
+
19
+ errors = []
20
+ while (count > 0) {
21
+ errors.push({ messageId: 'useLet' })
22
+ count--
23
+ }
24
+
25
+ invalidCases.push({ code, errors })
26
+ }
27
+
28
+ pass('let x = 1')
29
+
30
+ pass('let x = 1, y = 2')
31
+
32
+ pass('for (let i = 1; i < 3; i++) console.log(i)')
33
+
34
+ fail(1, 'const x = 1')
35
+
36
+ fail(1, 'var x = 1')
37
+
38
+ fail(1, 'var x, y')
39
+
40
+ fail(1, 'for (const key in node) console.log(key)')
41
+
42
+ globalThis.describe('always-let',
43
+ () => ruleTester.run('always-let',
44
+ plugins.cookshack.rules['always-let'],
45
+ { valid: validCases,
46
+ invalid: invalidCases }))
@@ -0,0 +1,48 @@
1
+ import { RuleTester } from 'eslint'
2
+ import { plugins } from '../../index.js'
3
+
4
+ let ruleTester, validCases, invalidCases
5
+
6
+ ruleTester = new RuleTester()
7
+ validCases = []
8
+ invalidCases = []
9
+
10
+ function pass
11
+ (code) {
12
+ validCases.push({ code })
13
+ }
14
+
15
+ function fail
16
+ (count, code) {
17
+ let errors
18
+
19
+ errors = []
20
+ while (count > 0) {
21
+ errors.push({ messageId: 'fnDeclBlockStart' })
22
+ count--
23
+ }
24
+
25
+ invalidCases.push({ code, errors })
26
+ }
27
+
28
+ pass('function f() { let x; function g() {} }')
29
+
30
+ pass('{ function f() {} }')
31
+
32
+ pass('{ let x; function f() {} }')
33
+
34
+ pass('{ let x; let y; function f() {}; function g() {} }')
35
+
36
+ pass('{ function f() {}; function g() {}; code() }')
37
+
38
+ fail(1, 'function main2 () { let a; a = 0; function f() { return 1 } let b }')
39
+
40
+ fail(1, '{ x; function f() {} }')
41
+
42
+ fail(1, '{ let x; code(); function f() {} }')
43
+
44
+ globalThis.describe('fn-decl-block-start',
45
+ () => ruleTester.run('fn-decl-block-start',
46
+ plugins.cookshack.rules['fn-decl-block-start'],
47
+ { valid: validCases,
48
+ invalid: invalidCases }))