@sap/eslint-plugin-cds 2.4.1 → 2.6.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 +31 -2
- package/README.md +2 -1
- package/lib/api/index.js +13 -12
- package/lib/conf/all.js +22 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +19 -0
- package/lib/constants.js +19 -16
- package/lib/index.js +11 -33
- package/lib/parser.js +169 -10
- package/lib/rules/assoc2many-ambiguous-key.js +80 -96
- package/lib/rules/auth-no-empty-restrictions.js +23 -30
- package/lib/rules/auth-use-requires.js +25 -24
- package/lib/rules/auth-valid-restrict-grant.js +87 -49
- package/lib/rules/auth-valid-restrict-keys.js +30 -23
- package/lib/rules/auth-valid-restrict-to.js +97 -52
- package/lib/rules/auth-valid-restrict-where.js +52 -42
- package/lib/rules/extension-restrictions.js +69 -0
- package/lib/rules/index.js +27 -0
- package/lib/rules/latest-cds-version.js +23 -21
- package/lib/rules/min-node-version.js +25 -24
- package/lib/rules/no-db-keywords.js +23 -31
- package/lib/rules/no-dollar-prefixed-names.js +17 -14
- package/lib/rules/no-join-on-draft.js +27 -0
- package/lib/rules/require-2many-oncond.js +11 -16
- package/lib/rules/sql-cast-suggestion.js +16 -29
- package/lib/rules/start-elements-lowercase.js +42 -44
- package/lib/rules/start-entities-uppercase.js +29 -31
- package/lib/rules/valid-csv-header.js +65 -64
- package/lib/{api/lint.d.ts → types.d.ts} +5 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +317 -0
- package/lib/utils/findFuzzy.js +87 -0
- package/lib/utils/genDocs.js +345 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/getProjectRootPath.js +25 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/rules.js +112 -1041
- package/lib/utils/runRuleTester.js +116 -0
- package/package.json +10 -4
- package/lib/processor.js +0 -50
- package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
- package/lib/utils/fuzzySearch.js +0 -94
- package/lib/utils/helpers.js +0 -94
- package/lib/utils/jsonc.js +0 -1
- package/lib/utils/model.js +0 -393
- package/lib/utils/ruleHelpers.js +0 -199
- package/lib/utils/ruleTester.js +0 -78
- package/lib/utils/validate.js +0 -36
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper for ESLint's Rule creator:
|
|
3
|
+
* https://eslint.org/docs/developer-guide/working-with-rules
|
|
4
|
+
* - Must follow the ESLint prescribed convention for all rule exports
|
|
5
|
+
* - ESLint uses 'create' function to traverse its AST nodes
|
|
6
|
+
* - Since we do not work with an AST for cds models, a dummy 'Programm' node is used as an entry point
|
|
7
|
+
* - More eslint-like API
|
|
8
|
+
* - More convenience for error reports
|
|
9
|
+
* @param {CDSRuleSpec} spec
|
|
10
|
+
* @returns {RuleModule}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { SourceCode } = require('eslint')
|
|
14
|
+
const fs = require('fs')
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const Cache = require('./Cache')
|
|
17
|
+
const constants = require('../constants')
|
|
18
|
+
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
19
|
+
const getProjectRootPath = require('./getProjectRootPath')
|
|
20
|
+
const cds = require('@sap/cds')
|
|
21
|
+
const { exit } = require('process')
|
|
22
|
+
const LOG = cds.debug('lint:plugin')
|
|
23
|
+
let filePrev = ''
|
|
24
|
+
|
|
25
|
+
const REGEX_COMMENT_START = '(/\\*|(.+)?//)(\\s?)+eslint-'
|
|
26
|
+
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`
|
|
27
|
+
|
|
28
|
+
module.exports = (spec) => {
|
|
29
|
+
let { meta, create } = spec
|
|
30
|
+
meta = setMetaDefaults(meta)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
meta,
|
|
34
|
+
create: (context) => ({
|
|
35
|
+
Program: (node) => {
|
|
36
|
+
try {
|
|
37
|
+
const file = context.getFilename()
|
|
38
|
+
if (file !== filePrev) {
|
|
39
|
+
LOG && LOG(`File: ${context.getFilename()}`)
|
|
40
|
+
}
|
|
41
|
+
const cdscontext = extendContext(node, context, meta)
|
|
42
|
+
Cache.set('context', cdscontext)
|
|
43
|
+
const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks } = checkEntryCriteria(meta, cdscontext)
|
|
44
|
+
switch (meta.model) {
|
|
45
|
+
case 'none':
|
|
46
|
+
if (doEnvironmentChecks) {
|
|
47
|
+
if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
|
|
48
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
49
|
+
Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
|
|
50
|
+
createReport(node, cdscontext, meta, create)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
case 'inferred':
|
|
56
|
+
if (isValidFile && doRootModelChecks) {
|
|
57
|
+
if (isTest || !Cache.has(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)) {
|
|
58
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
59
|
+
Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
|
|
60
|
+
createReport(node, cdscontext, meta, create)
|
|
61
|
+
} else {
|
|
62
|
+
if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
|
|
63
|
+
const reports = Cache.get(`report:${context.getFilename()}:${context.id}`)
|
|
64
|
+
for (const r of Array.from(reports)) {
|
|
65
|
+
context.report(JSON.parse(r))
|
|
66
|
+
}
|
|
67
|
+
Cache.remove(`report:${context.getFilename()}:${context.id}`)
|
|
68
|
+
Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
if (isValidFile) {
|
|
76
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
77
|
+
createReport(node, cdscontext, meta, create)
|
|
78
|
+
}
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
filePrev = file
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(err)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isRunningWithCDSLint () {
|
|
91
|
+
return process.argv[0].endsWith('node') && process.argv[1].endsWith('cds') && process.argv[2] === 'lint'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function checkEntryCriteria (meta, cdscontext) {
|
|
95
|
+
const isTest = Cache.has('test')
|
|
96
|
+
const hasProjectRoots = Cache.has(`roots:${Cache.get('rootpath')}`)
|
|
97
|
+
const isValidFile = isConfiguredFileType(cdscontext.getFilename(), 'FILES')
|
|
98
|
+
const doRootModelChecks = isTest || (hasProjectRoots && (isRunningWithCDSLint() || cdscontext.options.includes('show')))
|
|
99
|
+
// Also lint empty folders (i.e. lintText "" API)
|
|
100
|
+
const doEnvironmentChecks =
|
|
101
|
+
isTest || (hasProjectRoots && isRunningWithCDSLint() && cdscontext.getSourceCode().lines[0] === '')
|
|
102
|
+
return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setMetaDefaults (meta) {
|
|
106
|
+
if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY
|
|
107
|
+
if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY
|
|
108
|
+
if (!meta.model) meta.model = 'parsed'
|
|
109
|
+
return meta
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get report descriptors from created rules. These can take varios forms,
|
|
114
|
+
* from minimal return, up to fully defined ESLint report descriptors values,
|
|
115
|
+
* with or without visitor keys:
|
|
116
|
+
* - String is interpreted as the 'message' property
|
|
117
|
+
* - Object with known CDS Visitor keys and ESLint report descriptor values
|
|
118
|
+
* - Object with ESLint report dedscriptor keys/ values
|
|
119
|
+
* @param {*} cdscontext
|
|
120
|
+
* @param {*} create
|
|
121
|
+
* @returns
|
|
122
|
+
*/
|
|
123
|
+
function createReport (node, cdscontext, meta, create) {
|
|
124
|
+
const handlers = create(cdscontext)
|
|
125
|
+
/**
|
|
126
|
+
* TODO: Can these be rewritten to have a visitor? Note, that so far,
|
|
127
|
+
* rules without a visitor cannot use eslint disable comments
|
|
128
|
+
* A rule have no visitors and just return a single check:
|
|
129
|
+
* - Model Validation rules which have no well-defined CSN entry point
|
|
130
|
+
* - Environment rules
|
|
131
|
+
*/
|
|
132
|
+
switch (typeof handlers) {
|
|
133
|
+
case 'function':
|
|
134
|
+
handlers()
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
case 'object': {
|
|
138
|
+
if (meta.model !== 'none') {
|
|
139
|
+
const model = cdscontext.getModel()
|
|
140
|
+
|
|
141
|
+
if (model) {
|
|
142
|
+
model.forall((d) => {
|
|
143
|
+
Object.entries(handlers)
|
|
144
|
+
.filter(([type, lazy]) => d.is(type))
|
|
145
|
+
.forEach(([lazy, handler]) => {
|
|
146
|
+
try {
|
|
147
|
+
handler(d)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extendContext (node, context, meta) {
|
|
161
|
+
if (!Cache.has('test')) {
|
|
162
|
+
const filePath = context.getFilename()
|
|
163
|
+
const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
|
|
164
|
+
if (rootPath) {
|
|
165
|
+
Cache.set('rootpath', rootPath)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const reportWrapper = (r) => {
|
|
170
|
+
const line = r.loc ? r.loc.start.line : r.node.loc.start.line
|
|
171
|
+
if (!isRuleDisabled(line, context)) {
|
|
172
|
+
if (meta.model === 'inferred') {
|
|
173
|
+
if (!r.file) {
|
|
174
|
+
console.error(`Rule ${context.id} must return a "file" property in the rule report!`)
|
|
175
|
+
exit(1)
|
|
176
|
+
}
|
|
177
|
+
const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
|
|
178
|
+
if (cdscontext.getFilename() === file) {
|
|
179
|
+
delete r.file
|
|
180
|
+
context.report(r)
|
|
181
|
+
}
|
|
182
|
+
if (r.file) {
|
|
183
|
+
cacheReport(r, file, context, meta)
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
context.report(r)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const descriptors = Object.getOwnPropertyDescriptors(context)
|
|
192
|
+
descriptors.report = {
|
|
193
|
+
value: reportWrapper,
|
|
194
|
+
writable: false,
|
|
195
|
+
enumerable: true,
|
|
196
|
+
configurable: false
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
|
|
200
|
+
cdscontext.getModel =
|
|
201
|
+
meta.model === 'inferred' ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn
|
|
202
|
+
cdscontext.getEnvironment = () => {
|
|
203
|
+
const options = context.options
|
|
204
|
+
return options && options[0] && options[0].environment ? options[0].environment : undefined
|
|
205
|
+
}
|
|
206
|
+
cdscontext.getLocation = context.parserServices.getLocation
|
|
207
|
+
cdscontext.getNode = Object.keys(context.parserServices).length > 0 ? context.parserServices.getNode : () => node
|
|
208
|
+
return cdscontext
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isRuleDisabled (line, cdscontext) {
|
|
212
|
+
let isDisabled = false
|
|
213
|
+
if (cdscontext) {
|
|
214
|
+
const sourcecode = cdscontext.getSourceCode()
|
|
215
|
+
const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line)
|
|
216
|
+
const id = cdscontext.id
|
|
217
|
+
isDisabled = line && id in rulesDisabled && rulesDisabled[id] === 'off'
|
|
218
|
+
}
|
|
219
|
+
return isDisabled
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cacheReport (r, file, context, meta) {
|
|
223
|
+
delete r.file
|
|
224
|
+
r.node.range = []
|
|
225
|
+
if (r.messageId) {
|
|
226
|
+
r.message = meta.messages[r.messageId]
|
|
227
|
+
delete r.message
|
|
228
|
+
}
|
|
229
|
+
if (r) {
|
|
230
|
+
let reports = new Set()
|
|
231
|
+
if (Cache.has(`report:${file}:${context.id}`)) {
|
|
232
|
+
reports = Cache.get(`report:${file}:${context.id}`)
|
|
233
|
+
}
|
|
234
|
+
reports.add(JSON.stringify(r))
|
|
235
|
+
Cache.set(`report:${file}:${context.id}`, reports)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getDisabled (code, sourcecode, line) {
|
|
240
|
+
const listDisabled = []
|
|
241
|
+
const rules = Cache.get('rules')
|
|
242
|
+
const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
|
|
243
|
+
let matches = []
|
|
244
|
+
if (code) {
|
|
245
|
+
matches = [...code.matchAll(REGEX_COMMENTS)]
|
|
246
|
+
if (matches.length > 0) {
|
|
247
|
+
matches.forEach((match) => {
|
|
248
|
+
if (match) {
|
|
249
|
+
const index = match.index
|
|
250
|
+
match = match[0]
|
|
251
|
+
if (match.includes('*/')) {
|
|
252
|
+
match = match.split('*/')[0].replace('/*', '')
|
|
253
|
+
} else if (match.includes('//')) {
|
|
254
|
+
match = match.split('//')[1]
|
|
255
|
+
}
|
|
256
|
+
if (match) {
|
|
257
|
+
match = match.trim()
|
|
258
|
+
}
|
|
259
|
+
['disable', 'enable'].forEach((keyword) => {
|
|
260
|
+
const loc = sourcecode.getLocFromIndex(index)
|
|
261
|
+
const disableType = match.split(' ')[0]
|
|
262
|
+
let disableRules = match.split(`${disableType} `)[1]
|
|
263
|
+
disableRules = disableRules
|
|
264
|
+
? disableRules.split(',').map((rule) => rule.trim())
|
|
265
|
+
: Object.keys(rules).map((rule) => `@sap/cds/${rule}`)
|
|
266
|
+
let comment = {}
|
|
267
|
+
if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
|
|
268
|
+
comment = disableType.includes('-next-line')
|
|
269
|
+
? {
|
|
270
|
+
lineComment: loc.line,
|
|
271
|
+
lineDisabled: loc.line + 1,
|
|
272
|
+
rules: disableRules,
|
|
273
|
+
type: keyword
|
|
274
|
+
}
|
|
275
|
+
: {
|
|
276
|
+
lineComment: loc.line,
|
|
277
|
+
lineDisabled: loc.line,
|
|
278
|
+
rules: disableRules,
|
|
279
|
+
type: keyword
|
|
280
|
+
}
|
|
281
|
+
if (!disableType.includes('-line')) {
|
|
282
|
+
comment.lineDisabled = 'EOF'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
listDisabled.push(comment)
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
for (const el of listDisabled.filter(
|
|
290
|
+
(d) => d.lineComment > line && (d.lineDisabled === 'EOF' || d.lineDisabled === line)
|
|
291
|
+
)) {
|
|
292
|
+
if (el.lineDisabled === 'EOF') {
|
|
293
|
+
el.lineDisabled = getLastLine(code)
|
|
294
|
+
}
|
|
295
|
+
if (el.rules) {
|
|
296
|
+
el.rules.forEach((rule) => {
|
|
297
|
+
if (el.type === 'disable') {
|
|
298
|
+
rulesDisabled[rule] = 'off'
|
|
299
|
+
} else if (el.type === 'enable') {
|
|
300
|
+
rulesDisabled[rule] = 'on'
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return rulesDisabled
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getLastLine (code) {
|
|
311
|
+
const lines = typeof code === 'string' ? SourceCode.splitLines(code) : code
|
|
312
|
+
return lines.length - 1
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resolveFilePath (file) {
|
|
316
|
+
return path.isAbsolute(file) ? file : path.join(Cache.get('rootpath'), file)
|
|
317
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levenshtein distance algorithm using recursive calls and cache
|
|
3
|
+
*
|
|
4
|
+
* @param input search the list for a best match for this string
|
|
5
|
+
* @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
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const cache = {}
|
|
11
|
+
|
|
12
|
+
module.exports = (input, list, log, keepCase = false) => {
|
|
13
|
+
let minDistWords = []
|
|
14
|
+
|
|
15
|
+
if (input.length > 50 || list.length > 50) {
|
|
16
|
+
return minDistWords
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let minDist = Number.MAX_SAFE_INTEGER
|
|
20
|
+
|
|
21
|
+
log && log('\nword\t\tlevDist\t\ttime(ms)')
|
|
22
|
+
|
|
23
|
+
let runtime = 0
|
|
24
|
+
|
|
25
|
+
for (const word of list) {
|
|
26
|
+
const start = log && Date.now()
|
|
27
|
+
let levDist
|
|
28
|
+
if (word === word.toUpperCase() && !keepCase) {
|
|
29
|
+
levDist = levDistance(input.toUpperCase(), word)
|
|
30
|
+
} else {
|
|
31
|
+
levDist = levDistance(input, word)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (log) {
|
|
35
|
+
const duration = Date.now() - start
|
|
36
|
+
runtime = runtime + duration
|
|
37
|
+
log(`${word}\t\t${levDist}\t\t${duration}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (levDist === minDist) {
|
|
41
|
+
minDistWords.push(word)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (levDist < minDist) {
|
|
45
|
+
minDist = levDist
|
|
46
|
+
minDistWords = [word]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
log && log(`runtime: ${runtime}ms`)
|
|
51
|
+
|
|
52
|
+
return minDistWords.sort()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const levDistance = (a, b) => {
|
|
56
|
+
if (cache[a] && cache[a][b]) {
|
|
57
|
+
return cache[a][b]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (a.length === 0) {
|
|
61
|
+
return addToCache(a, b, b.length)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (b.length === 0) {
|
|
65
|
+
return addToCache(a, b, a.length)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tailA = a.substring(1)
|
|
69
|
+
const tailB = b.substring(1)
|
|
70
|
+
|
|
71
|
+
if (a[0] === b[0]) {
|
|
72
|
+
return levDistance(tailA, tailB)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lev1 = levDistance(tailA, b)
|
|
76
|
+
const lev2 = levDistance(a, tailB)
|
|
77
|
+
const lev3 = levDistance(tailA, tailB)
|
|
78
|
+
|
|
79
|
+
const levDist = Math.min(lev1, lev2, lev3) + 1
|
|
80
|
+
return addToCache(a, b, levDist)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const addToCache = (a, b, value) => {
|
|
84
|
+
cache[a] = cache[a] || {}
|
|
85
|
+
cache[a][b] = value
|
|
86
|
+
return value
|
|
87
|
+
}
|