@sap/eslint-plugin-cds 3.0.4 → 3.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -1
  3. package/lib/api/index.js +4 -4
  4. package/lib/conf/all.js +17 -17
  5. package/lib/conf/experimental.js +12 -0
  6. package/lib/conf/index.js +12 -3
  7. package/lib/conf/recommended.js +14 -14
  8. package/lib/constants.js +2 -0
  9. package/lib/index.js +2 -1
  10. package/lib/parser.js +10 -1
  11. package/lib/rules/assoc2many-ambiguous-key.js +40 -11
  12. package/lib/rules/auth-no-empty-restrictions.js +36 -10
  13. package/lib/rules/auth-restrict-grant-service.js +19 -20
  14. package/lib/rules/auth-use-requires.js +25 -15
  15. package/lib/rules/auth-valid-restrict-grant.js +137 -81
  16. package/lib/rules/auth-valid-restrict-keys.js +34 -18
  17. package/lib/rules/auth-valid-restrict-to.js +67 -60
  18. package/lib/rules/auth-valid-restrict-where.js +31 -44
  19. package/lib/rules/extension-restrictions.js +11 -3
  20. package/lib/rules/index.js +5 -1
  21. package/lib/rules/latest-cds-version.js +5 -4
  22. package/lib/rules/no-db-keywords.js +14 -5
  23. package/lib/rules/no-dollar-prefixed-names.js +9 -2
  24. package/lib/rules/no-java-keywords.js +181 -0
  25. package/lib/rules/no-join-on-draft.js +9 -3
  26. package/lib/rules/sql-cast-suggestion.js +19 -15
  27. package/lib/rules/sql-null-comparison.js +60 -0
  28. package/lib/rules/start-elements-lowercase.js +6 -2
  29. package/lib/rules/start-entities-uppercase.js +12 -5
  30. package/lib/rules/valid-csv-header.js +33 -13
  31. package/lib/types.d.ts +4 -4
  32. package/lib/utils/Cache.js +4 -2
  33. package/lib/utils/Colors.js +2 -0
  34. package/lib/utils/LintError.js +17 -0
  35. package/lib/utils/createRule.js +160 -134
  36. package/lib/utils/csnTraversal.js +163 -0
  37. package/lib/utils/findFuzzy.js +15 -7
  38. package/lib/utils/getConfigPath.js +4 -2
  39. package/lib/utils/getConfiguredFileTypes.js +2 -0
  40. package/lib/utils/getFileExtensions.js +2 -0
  41. package/lib/utils/getProjectRootPath.js +53 -15
  42. package/lib/utils/isConfiguredFileType.js +8 -3
  43. package/lib/utils/rules.js +13 -7
  44. package/lib/utils/runRuleTester.js +69 -36
  45. package/package.json +1 -1
  46. package/lib/utils/genDocs.js +0 -346
@@ -1,4 +1,6 @@
1
- /**
1
+ 'use strict'
2
+
3
+ /*
2
4
  * Wrapper for ESLint's Rule creator:
3
5
  * https://eslint.org/docs/developer-guide/working-with-rules
4
6
  * - Must follow the ESLint prescribed convention for all rule exports
@@ -13,25 +15,27 @@
13
15
  const { SourceCode } = require('eslint')
14
16
  const fs = require('fs')
15
17
  const path = require('path')
18
+ const cds = require('@sap/cds')
19
+
16
20
  const Cache = require('./Cache')
17
21
  const constants = require('../constants')
18
22
  const isConfiguredFileType = require('./isConfiguredFileType')
19
23
  const getProjectRootPath = require('./getProjectRootPath')
20
- const cds = require('@sap/cds')
21
- const { exit } = require('process')
24
+ const { CdsLintAssertionError } = require('./LintError')
25
+
22
26
  const LOG = cds.debug('lint:plugin')
23
27
  let filePrev = ''
24
28
 
25
29
  const REGEX_COMMENT_START = '(/\\*|(.+)?//)(\\s?)+eslint-'
26
30
  const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`
27
31
 
28
- module.exports = (spec) => {
32
+ module.exports = function createRule(spec) {
29
33
  let { meta, create } = spec
30
34
  meta = setMetaDefaults(meta)
31
35
 
32
36
  return {
33
37
  meta,
34
- create: (context) => {
38
+ create: context => {
35
39
  // do a fast check to exclude most cases, i.e. irrelevant files
36
40
  const isRelevant =
37
41
  context.getSourceCode().lines[0] === '' || // env. rules
@@ -41,63 +45,59 @@ module.exports = (spec) => {
41
45
  }
42
46
 
43
47
  return {
44
- Program: (node) => {
45
- try {
46
- const file = context.getFilename()
47
- if (file !== filePrev) {
48
- LOG && LOG(`File: ${context.getFilename()}`)
48
+ Program: node => {
49
+ const file = context.getFilename()
50
+ if (file !== filePrev) {
51
+ LOG && LOG(`File: ${context.getFilename()}`)
52
+ }
53
+ const cdscontext = extendContext(node, context, meta)
54
+ Cache.set('context', cdscontext)
55
+ const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdscontext)
56
+ switch (meta.model) {
57
+ case 'none':
58
+ if (doEnvironmentChecks) {
59
+ if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
60
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
61
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
62
+ createReport(node, cdscontext, meta, create)
63
+ }
49
64
  }
50
- const cdscontext = extendContext(node, context, meta)
51
- Cache.set('context', cdscontext)
52
- const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdscontext)
53
- switch (meta.model) {
54
- case 'none':
55
- if (doEnvironmentChecks) {
56
- if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
57
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
58
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
59
- createReport(node, cdscontext, meta, create)
60
- }
61
- }
62
- break
65
+ break
63
66
 
64
- case 'inferred':
65
- if (isValidFile && (doRootModelChecks)) {
66
- if (showInEditor) {
67
- Cache.remove(`model:${Cache.get('rootpath')}`)
68
- Cache.remove(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)
69
- Cache.remove(`report:${context.getFilename()}:${context.id}`)
70
- }
71
- if (isTest || showInEditor || Cache.has(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)) {
72
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
73
- if (!showInEditor) {
74
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
75
- }
76
- createReport(node, cdscontext, meta, create)
77
- } else {
78
- if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
79
- const reports = Cache.get(`report:${context.getFilename()}:${context.id}`)
80
- for (const r of Array.from(reports)) {
81
- context.report(JSON.parse(r))
82
- }
83
- Cache.remove(`report:${context.getFilename()}:${context.id}`)
84
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
85
- }
67
+ case 'inferred':
68
+ if (isValidFile && doRootModelChecks) {
69
+ if (showInEditor) {
70
+ Cache.remove(`model:${Cache.get('rootpath')}`)
71
+ Cache.remove(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)
72
+ Cache.remove(`report:${context.getFilename()}:${context.id}`)
73
+ }
74
+ if (isTest || showInEditor || !Cache.has(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)) {
75
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
76
+ if (!showInEditor) {
77
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
78
+ }
79
+ createReport(node, cdscontext, meta, create)
80
+ } else {
81
+ if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
82
+ const reports = Cache.get(`report:${context.getFilename()}:${context.id}`)
83
+ for (const r of Array.from(reports)) {
84
+ context.report(JSON.parse(r))
86
85
  }
86
+ Cache.remove(`report:${context.getFilename()}:${context.id}`)
87
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
87
88
  }
88
- break
89
+ }
90
+ }
91
+ break
89
92
 
90
- default:
91
- if (isValidFile) {
92
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
93
- createReport(node, cdscontext, meta, create)
94
- }
95
- break
93
+ default:
94
+ if (isValidFile) {
95
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
96
+ createReport(node, cdscontext, meta, create)
96
97
  }
97
- filePrev = file
98
- } catch (err) {
99
- console.error(err)
98
+ break
100
99
  }
100
+ filePrev = file
101
101
  }
102
102
  }
103
103
  }
@@ -125,25 +125,30 @@ function checkEntryCriteria (meta, cdscontext) {
125
125
  }
126
126
 
127
127
  function setMetaDefaults (meta) {
128
- if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY
129
- if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY
130
- if (!meta.model) meta.model = 'parsed'
128
+ meta ??= {}
129
+ meta.severity ??= constants.DEFAULT_RULE_SEVERITY
130
+ meta.docs ??= {}
131
+ meta.docs.category ??= constants.DEFAULT_RULE_CATEGORY
132
+ meta.model ??= 'parsed'
131
133
  return meta
132
134
  }
133
135
 
134
136
  /**
135
- * Get report descriptors from created rules. These can take varios forms,
137
+ * Get report descriptors from created rules. These can take various forms,
136
138
  * from minimal return, up to fully defined ESLint report descriptors values,
137
139
  * with or without visitor keys:
138
140
  * - String is interpreted as the 'message' property
139
141
  * - Object with known CDS Visitor keys and ESLint report descriptor values
140
- * - Object with ESLint report dedscriptor keys/ values
141
- * @param {*} cdscontext
142
- * @param {*} create
142
+ * - Object with ESLint report descriptor keys/ values
143
+ *
144
+ * @param {object} node
145
+ * @param {CDSRuleContext} cdsContext
146
+ * @param {object} meta
147
+ * @param {Function} create
143
148
  * @returns
144
149
  */
145
- function createReport (node, cdscontext, meta, create) {
146
- const handlers = create(cdscontext)
150
+ function createReport (node, cdsContext, meta, create) {
151
+ const handlers = create(cdsContext)
147
152
  /**
148
153
  * TODO: Can these be rewritten to have a visitor? Note, that so far,
149
154
  * rules without a visitor cannot use eslint disable comments
@@ -152,33 +157,29 @@ function createReport (node, cdscontext, meta, create) {
152
157
  * - Environment rules
153
158
  */
154
159
  switch (typeof handlers) {
155
- case 'function':
156
- handlers()
157
- break
160
+ case 'function':
161
+ handlers()
162
+ break
158
163
 
159
- case 'object': {
160
- if (meta.model !== 'none') {
161
- const model = cdscontext.getModel()
164
+ case 'object': {
165
+ if (meta.model !== 'none') {
166
+ const model = cdsContext.getModel()
162
167
 
163
- if (model) {
164
- model.forall((d) => {
165
- d = (meta.model === 'inferred') ? sanitizeFileLocation(d) : d
166
- const isValidLocation = (meta.model === 'parsed' && d.$location) ||
168
+ if (model) {
169
+ model.forall(d => {
170
+ d = (meta.model === 'inferred') ? sanitizeFileLocation(d) : d
171
+ const isValidLocation = (meta.model === 'parsed' && d.$location) ||
167
172
  (meta.model === 'inferred' && d.$location?.file)
168
- Object.entries(handlers)
169
- .filter(([type, _lazy]) => d.is(type) && isValidLocation)
170
- .forEach(([_lazy, handler]) => {
171
- try {
172
- handler(d)
173
- } catch (err) {
174
- console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err)
175
- }
176
- })
177
- })
178
- }
173
+ Object.entries(handlers)
174
+ .filter(([type, ]) => d.is(type) && isValidLocation)
175
+ .forEach(([, handler]) => {
176
+ handler(d)
177
+ })
178
+ })
179
179
  }
180
- break
181
180
  }
181
+ break
182
+ }
182
183
  }
183
184
  }
184
185
 
@@ -189,6 +190,11 @@ function sanitizeFileLocation (d) {
189
190
  return d
190
191
  }
191
192
 
193
+ /**
194
+ * @param node
195
+ * @param {CDSRuleContext} context
196
+ * @param meta
197
+ */
192
198
  function extendContext (node, context, meta) {
193
199
  if (!Cache.has('test')) {
194
200
  const filePath = context.getFilename()
@@ -198,28 +204,6 @@ function extendContext (node, context, meta) {
198
204
  }
199
205
  }
200
206
 
201
- const reportWrapper = (r) => {
202
- const line = r.loc ? r.loc.start.line : r.node.loc.start.line
203
- if (!isRuleDisabled(line, context)) {
204
- if (meta.model === 'inferred') {
205
- if (!r.file) {
206
- console.error(`Rule ${context.id} must return a "file" property in the rule report!`)
207
- exit(1)
208
- }
209
- const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
210
- if (cdscontext.getFilename() === file) {
211
- delete r.file
212
- context.report(r)
213
- }
214
- if (r.file) {
215
- cacheReport(r, file, context, meta)
216
- }
217
- } else {
218
- context.report(r)
219
- }
220
- }
221
- }
222
-
223
207
  const descriptors = Object.getOwnPropertyDescriptors(context)
224
208
  descriptors.report = {
225
209
  value: reportWrapper,
@@ -240,20 +224,52 @@ function extendContext (node, context, meta) {
240
224
  cdscontext.getNode = Object.keys(parserServices).length > 0 ? parserServices.getNode : () => node
241
225
  cdscontext.getRootPath = () => Cache.get('rootpath')
242
226
  return cdscontext
227
+
228
+ function reportWrapper(r) {
229
+ const line = r.loc ? r.loc.start.line : r.node.loc.start.line
230
+ if (!isRuleDisabled(line, context)) {
231
+ if (meta.model === 'inferred') {
232
+ if (!r.file) {
233
+ throw new CdsLintAssertionError(`Rule ${context.id} must return a "file" property in the rule report!`)
234
+ }
235
+ const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
236
+ if (cdscontext.getFilename() === file) {
237
+ delete r.file
238
+ context.report(r)
239
+ }
240
+ if (r.file) {
241
+ cacheReport(r, file, context, meta)
242
+ }
243
+ } else {
244
+ context.report(r)
245
+ }
246
+ }
247
+ }
243
248
  }
244
249
 
245
- function isRuleDisabled (line, cdscontext) {
250
+
251
+ /**
252
+ * @param {number} line
253
+ * @param {CDSRuleContext} cdsContext
254
+ */
255
+ function isRuleDisabled (line, cdsContext) {
246
256
  let isDisabled = false
247
- if (cdscontext) {
248
- const sourcecode = cdscontext.getSourceCode()
257
+ if (cdsContext) {
258
+ const sourcecode = cdsContext.getSourceCode()
249
259
  const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line)
250
- const id = cdscontext.id
260
+ const id = cdsContext.id
251
261
  isDisabled = line && id in rulesDisabled && rulesDisabled[id] === 'off'
252
262
  }
253
263
  return isDisabled
254
264
  }
255
265
 
256
- function cacheReport (r, file, context, meta) {
266
+ /**
267
+ * @param r
268
+ * @param {string} filepath
269
+ * @param {CDSRuleContext} context
270
+ * @param meta
271
+ */
272
+ function cacheReport (r, filepath, context, meta) {
257
273
  delete r.file
258
274
  if (r.node && r.node.range) {
259
275
  r.node.range = []
@@ -264,23 +280,27 @@ function cacheReport (r, file, context, meta) {
264
280
  }
265
281
  if (r) {
266
282
  let reports = new Set()
267
- if (Cache.has(`report:${file}:${context.id}`)) {
268
- reports = Cache.get(`report:${file}:${context.id}`)
283
+ if (Cache.has(`report:${filepath}:${context.id}`)) {
284
+ reports = Cache.get(`report:${filepath}:${context.id}`)
269
285
  }
270
286
  reports.add(JSON.stringify(r))
271
- Cache.set(`report:${file}:${context.id}`, reports)
287
+ Cache.set(`report:${filepath}:${context.id}`, reports)
272
288
  }
273
289
  }
274
290
 
291
+ /**
292
+ * @param {string} code
293
+ * @param {string} sourcecode
294
+ * @param {number} line
295
+ */
275
296
  function getDisabled (code, sourcecode, line) {
276
297
  const listDisabled = []
277
298
  const rules = Cache.get('rules')
278
299
  const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
279
- let matches = []
280
300
  if (code) {
281
- matches = [...code.matchAll(REGEX_COMMENTS)]
301
+ const matches = [...code.matchAll(REGEX_COMMENTS)]
282
302
  if (matches.length > 0) {
283
- matches.forEach((match) => {
303
+ matches.forEach(match => {
284
304
  if (match) {
285
305
  const index = match.index
286
306
  match = match[0]
@@ -292,28 +312,28 @@ function getDisabled (code, sourcecode, line) {
292
312
  if (match) {
293
313
  match = match.trim()
294
314
  }
295
- ['disable', 'enable'].forEach((keyword) => {
315
+ ['disable', 'enable'].forEach(keyword => {
296
316
  const loc = sourcecode.getLocFromIndex(index)
297
317
  const disableType = match.split(' ')[0]
298
318
  let disableRules = match.split(`${disableType} `)[1]
299
319
  disableRules = disableRules
300
- ? disableRules.split(',').map((rule) => rule.trim())
301
- : Object.keys(rules).map((rule) => `@sap/cds/${rule}`)
320
+ ? disableRules.split(',').map(rule => rule.trim())
321
+ : Object.keys(rules).map(rule => `@sap/cds/${rule}`)
302
322
  let comment = {}
303
323
  if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
304
324
  comment = disableType.includes('-next-line')
305
325
  ? {
306
- lineComment: loc.line,
307
- lineDisabled: loc.line + 1,
308
- rules: disableRules,
309
- type: keyword
310
- }
326
+ lineComment: loc.line,
327
+ lineDisabled: loc.line + 1,
328
+ rules: disableRules,
329
+ type: keyword
330
+ }
311
331
  : {
312
- lineComment: loc.line,
313
- lineDisabled: loc.line,
314
- rules: disableRules,
315
- type: keyword
316
- }
332
+ lineComment: loc.line,
333
+ lineDisabled: loc.line,
334
+ rules: disableRules,
335
+ type: keyword
336
+ }
317
337
  if (!disableType.includes('-line')) {
318
338
  comment.lineDisabled = 'EOF'
319
339
  }
@@ -323,13 +343,13 @@ function getDisabled (code, sourcecode, line) {
323
343
  }
324
344
  })
325
345
  for (const el of listDisabled.filter(
326
- (d) => d.lineComment < line && (d.lineDisabled === 'EOF' || d.lineDisabled === line)
346
+ d => d.lineComment < line && (d.lineDisabled === 'EOF' || d.lineDisabled === line)
327
347
  )) {
328
348
  if (el.lineDisabled === 'EOF') {
329
349
  el.lineDisabled = getLastLine(code)
330
350
  }
331
351
  if (el.rules) {
332
- el.rules.forEach((rule) => {
352
+ el.rules.forEach(rule => {
333
353
  if (el.type === 'disable') {
334
354
  rulesDisabled[rule] = 'off'
335
355
  } else if (el.type === 'enable') {
@@ -343,11 +363,17 @@ function getDisabled (code, sourcecode, line) {
343
363
  return rulesDisabled
344
364
  }
345
365
 
366
+ /**
367
+ * @param {string} code
368
+ */
346
369
  function getLastLine (code) {
347
370
  const lines = typeof code === 'string' ? SourceCode.splitLines(code) : code
348
371
  return lines.length - 1
349
372
  }
350
373
 
374
+ /**
375
+ * @param {string} file
376
+ */
351
377
  function resolveFilePath (file) {
352
378
  return path.isAbsolute(file) ? file : path.join(Cache.get('rootpath'), file)
353
379
  }
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Utility class for traversing a CSN.
5
+ *
6
+ * Experimental class. Not used directly, but instead wrapped in `forEach*` functions,
7
+ * which are exported by this file.
8
+ */
9
+ class CsnTraversal {
10
+ csnIterators = {
11
+ __proto__: null,
12
+
13
+ definitions: this.dictionary,
14
+ extensions: this.array,
15
+
16
+ type: this.standard,
17
+ target: this.standard,
18
+ targetAspect: this.standard,
19
+ returns: this.standard,
20
+ items: this.standard,
21
+ elements: this.dictionary,
22
+
23
+ enum: this.dictionary,
24
+ key: this.array,
25
+ actions: this.dictionary,
26
+ params: this.dictionary,
27
+ mixin: this.dictionary,
28
+
29
+ query: this.standard,
30
+ SELECT: this.standard,
31
+ SET: this.standard,
32
+
33
+ from: this.standard,
34
+ columns: this.array,
35
+ expand: this.array,
36
+ inline: this.array,
37
+
38
+ ref: this.array,
39
+ xpr: this.array,
40
+ list: this.array,
41
+
42
+ args: this.args,
43
+ on: this.standard,
44
+ default: this.standard,
45
+ where: this.standard,
46
+ groupBy: this.standard,
47
+ orderBy: this.standard,
48
+ having: this.standard,
49
+ limit: this.standard,
50
+ rows: this.standard,
51
+ offset: this.standard,
52
+
53
+ '@': this.annotation,
54
+ }
55
+
56
+ constructor(callbacks) {
57
+ this.callbacks = callbacks
58
+ this.ctx = new CsnTraversalContext
59
+ }
60
+
61
+ array( node, _prop, arr ) {
62
+ if (!Array.isArray(arr))
63
+ return
64
+
65
+ for (let i = 0; i < arr.length; ++i) {
66
+ const entry = arr[i]
67
+ this.standard(arr, i, entry)
68
+ }
69
+ }
70
+
71
+ dictionary( node, _prop, dict ) {
72
+ if (!node || typeof node !== 'object')
73
+ return
74
+
75
+ for (const name of Object.getOwnPropertyNames( dict ))
76
+ this.standard( dict, name, dict[name] )
77
+ }
78
+
79
+ // eslint-disable-next-line no-unused-vars
80
+ annotation( parent, prop, node ) {
81
+ // no-op for now; remove eslint-comment once implemented
82
+ }
83
+
84
+ // Arguments can be named (dictionary) or unnamed/positional (array).
85
+ args( parent, prop, node ) {
86
+ if (Array.isArray(node))
87
+ this.array(parent, prop, node)
88
+ else
89
+ this.dictionary(parent, prop, node)
90
+ }
91
+
92
+ /**
93
+ * Traverse a "standard" CSN object, i.e. an object with CSN properties and
94
+ * not a dictionary such as "elements".
95
+ */
96
+ standard( _parent, _prop, node ) {
97
+ if (!node || typeof node !== 'object')
98
+ return
99
+
100
+ this.ctx.pushLocationOf(node)
101
+
102
+ if (Array.isArray(node)) {
103
+ node.forEach( (n, i) => {
104
+ this.standard(node, i, n)
105
+ } )
106
+ }
107
+ else {
108
+ for (const prop of Object.getOwnPropertyNames( node )) {
109
+ if (this.callbacks[prop]) {
110
+ this.ctx.pushLocationOf(node[prop])
111
+ this.ctx.property = prop
112
+ this.callbacks[prop](node[prop], this.ctx)
113
+ this.ctx.popLocation()
114
+ }
115
+
116
+ const traverse = this.csnIterators[prop] || this.csnIterators[prop.charAt(0)]
117
+ if (traverse)
118
+ traverse.call(this, node, prop, node[prop] )
119
+ }
120
+ }
121
+
122
+ this.ctx.popLocation()
123
+ }
124
+
125
+ }
126
+
127
+ class CsnTraversalContext {
128
+ constructor(parent) {
129
+ this.parent = parent || null
130
+ this.property = null
131
+ this.locations = []
132
+ }
133
+
134
+ /** If a location on "node" is available, add it to the stack. */
135
+ pushLocationOf(node) {
136
+ this.locations.push(node.$location ?? this.locations.at(-1))
137
+ }
138
+ popLocation() {
139
+ this.locations.pop()
140
+ }
141
+
142
+ get $location() {
143
+ return this.locations.at(-1)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * For each `xpr` (i.e. expression) in a definition, invoke the callback.
149
+ *
150
+ * @param {object} def
151
+ * @param {Function} callback
152
+ */
153
+ function forEachXprInDefinition( def, callback ) {
154
+ const traversal = new CsnTraversal({
155
+ xpr: callback,
156
+ where: callback,
157
+ })
158
+ traversal.standard(null, def.name, def)
159
+ }
160
+
161
+ module.exports = {
162
+ forEachXprInDefinition,
163
+ }
@@ -1,15 +1,23 @@
1
+ 'use strict'
2
+
3
+ const cache = {}
4
+
1
5
  /**
2
6
  * Levenshtein distance algorithm using recursive calls and cache
3
7
  *
4
8
  * @param input search the list for a best match for this string
5
9
  * @param list a list of strings to match input against
6
- * @param log logging method to use, might be null if no logging is wanted
7
- * @returns array with best matches, is never null but might be empty in case no search was possible
10
+ * @param {Function} [log] logging method to use, might be null if no logging is wanted
11
+ * @param {boolean} [keepCase]
12
+ * @param {Number} [threshold]
13
+ * @returns {string[]} array with best matches, is never null but might be empty in case no search was possible
14
+ *
15
+ * @todo: Describe in which range the threshold should be.
8
16
  */
17
+ module.exports = function findFuzzy(input, list, log = null, keepCase = false, threshold = Number.MAX_SAFE_INTEGER) {
18
+ if (typeof input !== 'string')
19
+ return []
9
20
 
10
- const cache = {}
11
-
12
- module.exports = (input, list, log, keepCase = false, threshold = Number.MAX_SAFE_INTEGER) => {
13
21
  let minDistWords = []
14
22
 
15
23
  if (input.length > 50 || list.length > 50) {
@@ -56,7 +64,7 @@ module.exports = (input, list, log, keepCase = false, threshold = Number.MAX_SAF
56
64
  return minDistWords.sort()
57
65
  }
58
66
 
59
- const levDistance = (a, b) => {
67
+ function levDistance(a, b) {
60
68
  if (cache[a] && cache[a][b]) {
61
69
  return cache[a][b]
62
70
  }
@@ -84,7 +92,7 @@ const levDistance = (a, b) => {
84
92
  return addToCache(a, b, levDist)
85
93
  }
86
94
 
87
- const addToCache = (a, b, value) => {
95
+ function addToCache(a, b, value) {
88
96
  cache[a] = cache[a] || {}
89
97
  cache[a][b] = value
90
98
  return value
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  /**
2
4
  * Searches for ESLint config file types (in order or precedence)
3
5
  * and returns corresponding directory (usually project's root dir)
@@ -6,8 +8,8 @@
6
8
  * @returns {string} dir containing ESLint config file (empty if not exists)
7
9
  */
8
10
 
9
- const fs = require('fs')
10
- const path = require('path')
11
+ const fs = require('node:fs')
12
+ const path = require('node:path')
11
13
 
12
14
  module.exports = (currentDir = '.', legacy=false) => {
13
15
  let configFiles = [
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { FILES } = require('../constants')
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const constants = require('../constants')
2
4
 
3
5
  /**