@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.
- package/CHANGELOG.md +39 -0
- package/README.md +1 -1
- package/lib/api/index.js +4 -4
- package/lib/conf/all.js +17 -17
- package/lib/conf/experimental.js +12 -0
- package/lib/conf/index.js +12 -3
- package/lib/conf/recommended.js +14 -14
- package/lib/constants.js +2 -0
- package/lib/index.js +2 -1
- package/lib/parser.js +10 -1
- package/lib/rules/assoc2many-ambiguous-key.js +40 -11
- package/lib/rules/auth-no-empty-restrictions.js +36 -10
- package/lib/rules/auth-restrict-grant-service.js +19 -20
- package/lib/rules/auth-use-requires.js +25 -15
- package/lib/rules/auth-valid-restrict-grant.js +137 -81
- package/lib/rules/auth-valid-restrict-keys.js +34 -18
- package/lib/rules/auth-valid-restrict-to.js +67 -60
- package/lib/rules/auth-valid-restrict-where.js +31 -44
- package/lib/rules/extension-restrictions.js +11 -3
- package/lib/rules/index.js +5 -1
- package/lib/rules/latest-cds-version.js +5 -4
- package/lib/rules/no-db-keywords.js +14 -5
- package/lib/rules/no-dollar-prefixed-names.js +9 -2
- package/lib/rules/no-java-keywords.js +181 -0
- package/lib/rules/no-join-on-draft.js +9 -3
- package/lib/rules/sql-cast-suggestion.js +19 -15
- package/lib/rules/sql-null-comparison.js +60 -0
- package/lib/rules/start-elements-lowercase.js +6 -2
- package/lib/rules/start-entities-uppercase.js +12 -5
- package/lib/rules/valid-csv-header.js +33 -13
- package/lib/types.d.ts +4 -4
- package/lib/utils/Cache.js +4 -2
- package/lib/utils/Colors.js +2 -0
- package/lib/utils/LintError.js +17 -0
- package/lib/utils/createRule.js +160 -134
- package/lib/utils/csnTraversal.js +163 -0
- package/lib/utils/findFuzzy.js +15 -7
- package/lib/utils/getConfigPath.js +4 -2
- package/lib/utils/getConfiguredFileTypes.js +2 -0
- package/lib/utils/getFileExtensions.js +2 -0
- package/lib/utils/getProjectRootPath.js +53 -15
- package/lib/utils/isConfiguredFileType.js +8 -3
- package/lib/utils/rules.js +13 -7
- package/lib/utils/runRuleTester.js +69 -36
- package/package.json +1 -1
- package/lib/utils/genDocs.js +0 -346
package/lib/utils/createRule.js
CHANGED
|
@@ -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
|
|
21
|
-
|
|
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:
|
|
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:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
break
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
|
141
|
-
*
|
|
142
|
-
* @param {
|
|
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,
|
|
146
|
-
const handlers = create(
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
case 'function':
|
|
161
|
+
handlers()
|
|
162
|
+
break
|
|
158
163
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
case 'object': {
|
|
165
|
+
if (meta.model !== 'none') {
|
|
166
|
+
const model = cdsContext.getModel()
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {number} line
|
|
253
|
+
* @param {CDSRuleContext} cdsContext
|
|
254
|
+
*/
|
|
255
|
+
function isRuleDisabled (line, cdsContext) {
|
|
246
256
|
let isDisabled = false
|
|
247
|
-
if (
|
|
248
|
-
const sourcecode =
|
|
257
|
+
if (cdsContext) {
|
|
258
|
+
const sourcecode = cdsContext.getSourceCode()
|
|
249
259
|
const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line)
|
|
250
|
-
const 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
|
-
|
|
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:${
|
|
268
|
-
reports = Cache.get(`report:${
|
|
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:${
|
|
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(
|
|
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(
|
|
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(
|
|
301
|
-
: Object.keys(rules).map(
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
lineComment: loc.line,
|
|
327
|
+
lineDisabled: loc.line + 1,
|
|
328
|
+
rules: disableRules,
|
|
329
|
+
type: keyword
|
|
330
|
+
}
|
|
311
331
|
: {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/lib/utils/findFuzzy.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|