@cookshack/eslint-config 2.0.5 → 4.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,472 @@
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
125
+ (node, target) {
126
+ if (node == target)
127
+ return true
128
+ if (node && typeof node == 'object')
129
+ for (let key in node)
130
+ if (nodeHas(node[key], target))
131
+ return true
132
+ return false
133
+ }
134
+
135
+ function nodeHas
136
+ (value, target) {
137
+ if (value == target)
138
+ return true
139
+ if (Array.isArray(value))
140
+ return value.some(v => nodeContains(v, target))
141
+ return false
142
+ }
143
+
144
+ function hasReadBeforeWriteInNestedScope
145
+ (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
+ return 1
218
+
219
+ item = ref.cookshackNarrowestScopeItem
220
+ if (item.ctx == 'B' || isConditionalRef(ref, narrowestScope))
221
+ continue
222
+ return 0
223
+ }
224
+ }
225
+
226
+ function isProperAncestor
227
+ (ancestor, descendant) {
228
+ let s
229
+
230
+ s = descendant.upper
231
+ while (s) {
232
+ if (s == ancestor)
233
+ return 1
234
+ s = s.upper
235
+ }
236
+ return 0
237
+ }
238
+
239
+ function scopeStart
240
+ (scope) {
241
+ if (scope.block == null)
242
+ return Infinity
243
+ if (scope.type == 'function' && scope.block.id)
244
+ return scope.block.id.range[1]
245
+ if (scope.type == 'class' && scope.block.id)
246
+ return scope.block.id.range[0]
247
+ return scope.block.range[0]
248
+ }
249
+
250
+ export { isReadRef, isWriteRef, buildScopeTree, scopeStart }
251
+
252
+ function ensureInVarIds
253
+ (variable) {
254
+ if (varIds.has(variable))
255
+ return
256
+ varIds.set(variable, nextVarId++)
257
+ }
258
+
259
+ function isCompoundAssignmentOp
260
+ (op) {
261
+ if (op == '=')
262
+ return 0
263
+ return 1
264
+ }
265
+
266
+ function buildScopeTree
267
+ (scope, prefix, scopeToNode, astToTree) {
268
+ let node, siblingNum
269
+
270
+ node = { scope,
271
+ prefix,
272
+ items: [],
273
+ children: [] }
274
+
275
+ scopeToNode.set(scope, node)
276
+ if (scope.block && astToTree)
277
+ astToTree.set(scope.block, node)
278
+
279
+ siblingNum = 0
280
+ for (let child of scope.childScopes) {
281
+ siblingNum++
282
+ node.children.push(buildScopeTree(child, prefix + '.' + siblingNum, scopeToNode, astToTree))
283
+ }
284
+
285
+ for (let variable of scope.variables) {
286
+ if (variable.defs.length > 0) {
287
+ ensureInVarIds(variable)
288
+ 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) })
289
+ }
290
+
291
+ for (let ref of variable.references) {
292
+ let targetNode
293
+
294
+ targetNode = scopeToNode.get(ref.from)
295
+ if (targetNode) {
296
+ let parent, sortPos, ctx, item1, item2
297
+
298
+ ctx = getConditionalContext(ref)
299
+ parent = ref.identifier.parent
300
+
301
+ if (isWriteRef(ref))
302
+ if (ref.identifier.parent?.type == 'UpdateExpression') {
303
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: ref.identifier.range[0] }
304
+ item2 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] }
305
+ }
306
+ else if (ref.identifier.parent?.type == 'AssignmentExpression') {
307
+ sortPos = parent.right.range[1] + 0.4
308
+ if (ref.identifier.parent.left == ref.identifier && isCompoundAssignmentOp(ref.identifier.parent.operator)) {
309
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: ref.identifier.range[0] }
310
+ item2 = { ref, type: 'WRITE', name: ref.identifier.name, pos: sortPos }
311
+ }
312
+ else
313
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, ctx, pos: sortPos }
314
+ }
315
+ else if (ref.identifier.parent?.type == 'VariableDeclarator')
316
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] + 0.4 }
317
+ else
318
+ item1 = { ref, type: 'WRITE', name: ref.identifier.name, pos: ref.identifier.range[0] }
319
+ else {
320
+ let declarator
321
+
322
+ declarator = parent
323
+ while (declarator)
324
+ if (declarator.type == 'VariableDeclarator')
325
+ break
326
+ else
327
+ declarator = declarator.parent
328
+ if (declarator?.type == 'VariableDeclarator' && nodeContains(declarator.init, ref.identifier))
329
+ sortPos = declarator.id ? declarator.id.range[0] - 0.4 : ref.identifier.range[0]
330
+ else
331
+ sortPos = ref.identifier.range[0]
332
+ item1 = { ref, type: 'READ', name: ref.identifier.name, ctx, pos: sortPos }
333
+ }
334
+ ensureInVarIds(variable)
335
+ item1.varId = varIds.get(variable)
336
+ targetNode.items.push(item1)
337
+ if (item2) {
338
+ item2.varId = varIds.get(variable)
339
+ targetNode.items.push(item2)
340
+ }
341
+ ref.cookshackNarrowestScopeItem = item2 || item1
342
+ }
343
+ }
344
+ }
345
+
346
+ node.items.sort((a, b) => a.pos - b.pos)
347
+
348
+ return node
349
+ }
350
+
351
+ function checkScopeNode
352
+ (context, treeNode, reported, scopeToNode) {
353
+ let indent
354
+
355
+ reported = reported || new Set
356
+ indent = ' '.repeat(treeNode.prefix.split('.').length - 1)
357
+
358
+ for (let variable of treeNode.scope.variables) {
359
+ let defNode
360
+
361
+ if (reported.has(variable))
362
+ continue
363
+ if (variable.defs.length == 0)
364
+ continue
365
+ if ([ 'Parameter', 'FunctionName', 'ImportBinding', 'CatchClause', 'ClassName' ].includes(variable.defs[0].type))
366
+ continue
367
+ if (variable.defs[0].node.parent?.parent?.type == 'ExportNamedDeclaration')
368
+ continue
369
+
370
+ defNode = variable.defs[0]?.name
371
+ if (defNode) {
372
+ let defScope, narrowestScope, defNodePrefix
373
+
374
+ defScope = getDefinitionScope(variable)
375
+ defNodePrefix = scopeToNode.get(defScope)?.prefix ?? '?'
376
+ trace(indent, '1 found decl scope of', variable.name + ':', defNodePrefix + ' ' + defScope.type.toUpperCase())
377
+
378
+ narrowestScope = getNarrowestScope(variable)
379
+ if (narrowestScope) {
380
+ let narrowestPrefix
381
+
382
+ narrowestPrefix = scopeToNode.get(narrowestScope)?.prefix ?? '?'
383
+ trace(indent, '2 found narrowest scope of', variable.name + ':', narrowestPrefix + ' ' + narrowestScope?.type.toUpperCase())
384
+
385
+ markConditionalRefs(variable, scopeToNode, narrowestScope)
386
+
387
+ if (defScope == narrowestScope)
388
+ continue
389
+ trace(indent, '3', variable.name, 'could be moved to a narrower scope')
390
+
391
+ if (defScope.type == 'for') {
392
+ trace(indent, '4 exception:', variable.name, 'is in a for loop header')
393
+ continue
394
+ }
395
+ if (0 && hasReadBeforeWriteInNestedScope(variable, defScope)) {
396
+ trace(indent, '4 exception:', variable.name, 'hasReadBeforeWriteInNestedScope')
397
+ continue
398
+ }
399
+ if (mayBeReadBeforeAnyWrite(variable, scopeToNode, narrowestScope)) {
400
+ trace(indent, '4 exception:', variable.name, 'mayBeReadBeforeAnyWrite')
401
+ continue
402
+ }
403
+
404
+ trace(indent, '5', variable.name, 'is too broad')
405
+
406
+ reported.add(variable)
407
+ context.report({ node: defNode,
408
+ messageId: 'tooBroad',
409
+ data: { name: variable.name } })
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 { 'Program:exit'
456
+ () {
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
+ export
468
+ default { meta: { type: 'suggestion',
469
+ docs: { description: 'Enforce variables are declared in their narrowest possible scope.' },
470
+ messages: { tooBroad: 'Variable "{{ name }}" is declared in a broader scope than necessary.' },
471
+ schema: [] },
472
+ create: createNarrowestScope }
@@ -0,0 +1,27 @@
1
+ function createPositiveVibes
2
+ (context) {
3
+ return { UnaryExpression
4
+ (node) {
5
+ if (node.operator == '!')
6
+ context.report({ node,
7
+ messageId: 'positiveVibes' })
8
+ },
9
+ BinaryExpression
10
+ (node) {
11
+ if (node.operator == '!=')
12
+ context.report({ node,
13
+ messageId: 'equality' })
14
+ else if (node.operator == '!==')
15
+ context.report({ node,
16
+ messageId: 'strictEquality' })
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,13 @@
1
+ export
2
+ default { meta: { type: 'problem',
3
+ docs: { description: 'Enforce use of == instead of ===.' },
4
+ messages: { risky: 'Use ==.' },
5
+ schema: [] },
6
+ create
7
+ (context) {
8
+ return { BinaryExpression
9
+ (node) {
10
+ if (node.operator == '===')
11
+ context.report({ node, messageId: 'risky' })
12
+ } }
13
+ } }
@@ -0,0 +1,37 @@
1
+ function VariableDeclaration
2
+ (context, node) {
3
+ let parent
4
+
5
+ parent = node.parent
6
+
7
+ for (parent = node.parent; parent; parent = parent.parent)
8
+ if (parent.type == 'BlockStatement')
9
+ break
10
+
11
+ if (parent) {
12
+ let idx
13
+
14
+ if (parent.parent?.type == 'CatchClause')
15
+ return
16
+
17
+ idx = parent.body.indexOf(node)
18
+ for (let i = 0; i < idx; i++) {
19
+ if (parent.body[i].type == 'VariableDeclaration')
20
+ continue
21
+ context.report({ node, messageId: 'varDeclBlockStart' })
22
+ return
23
+ }
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,114 @@
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: 'fnArgsNl' })
22
+ count--
23
+ }
24
+ invalidCases.push({ code, errors })
25
+ }
26
+
27
+ pass(`function f
28
+ (arg) {
29
+ }`)
30
+
31
+ pass(`function f
32
+ () {}`)
33
+
34
+ pass(`function f
35
+ (arg1, arg2) {}`)
36
+
37
+ pass(`function f
38
+ (
39
+ arg
40
+ ) {}`)
41
+
42
+ pass(`export default
43
+ function
44
+ () {
45
+ return 1
46
+ }
47
+ `)
48
+
49
+ pass(`
50
+ function f
51
+ () {
52
+ return { f1
53
+ () {
54
+ return 1
55
+ } }
56
+ }
57
+ `)
58
+
59
+ pass(`
60
+ function f
61
+ () {
62
+ return {
63
+ f1
64
+ () {
65
+ return 1
66
+ }
67
+ }
68
+ }
69
+ `)
70
+
71
+ pass(`
72
+ class Marker {
73
+ constructor
74
+ (name, num) {
75
+ }
76
+ toDOM
77
+ () {
78
+ }
79
+ }
80
+ `)
81
+
82
+ pass(`
83
+ area = { get name
84
+ () {
85
+ return name
86
+ },
87
+ get value
88
+ () {
89
+ return value
90
+ } }
91
+ `)
92
+
93
+ fail(1, 'function f (arg) {}')
94
+
95
+ fail(1, 'function f() {}')
96
+
97
+ fail(1, `function f (arg,
98
+ arg2) {
99
+ }`)
100
+
101
+ fail(1, `function f
102
+
103
+ (arg) {}`)
104
+
105
+ fail(1, `function f
106
+
107
+
108
+ (arg) {}`)
109
+
110
+ globalThis.describe('fn-args-nl',
111
+ () => ruleTester.run('fn-args-nl',
112
+ plugins.cookshack.rules['fn-args-nl'],
113
+ { valid: validCases,
114
+ invalid: invalidCases }))