@sap/eslint-plugin-cds 4.1.2 → 4.2.2
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 +12 -0
- package/lib/rules/auth-valid-restrict-grant.js +33 -1
- package/lib/rules/extension-restrictions.js +2 -2
- package/lib/rules/java/cql-class-targets.js +7 -7
- package/lib/rules/js/CdsHandlerRule.js +83 -83
- package/lib/rules/js/cql-template-strings.js +2 -2
- package/lib/rules/js/no-cross-service-import.js +1 -1
- package/lib/rules/valid-csv-header.js +2 -2
- package/lib/utils/createRule.js +74 -74
- package/lib/utils/csnTraversal.js +1 -1
- package/lib/utils/rules.js +1 -1
- package/lib/utils/runRuleTester.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## [4.2.2] - 2026-05-04
|
|
10
|
+
### Fixed
|
|
11
|
+
- `auth-valid-restrict-grant` now also considers inherited actions.
|
|
12
|
+
|
|
13
|
+
## [4.2.1] - 2026-02-26
|
|
14
|
+
### Fixed
|
|
15
|
+
- Removed usage of deprecated API in `lib/utils/rules.js`
|
|
16
|
+
|
|
17
|
+
## [4.2.0] - 2026-02-26
|
|
18
|
+
### Fixed
|
|
19
|
+
- Use ESLint API supported by versions `9` and `10`.
|
|
20
|
+
|
|
9
21
|
## [4.1.2] - 2026-02-13
|
|
10
22
|
### Fixed
|
|
11
23
|
- No longer crash on .java files larger than 32 kB.
|
|
@@ -8,6 +8,38 @@ const SAME_AS_WRITE_EVENT = [ 'CREATE', 'DELETE', 'UPDATE', 'UPSERT' ]
|
|
|
8
8
|
// Note that 'INSERT' is not meant to be used by users. They should use 'CREATE' instead.
|
|
9
9
|
const VALID_EVENTS = [ ...SAME_AS_WRITE_EVENT, 'READ', 'INSERT', '*', 'WRITE']
|
|
10
10
|
|
|
11
|
+
const isJoin = e => Boolean(e.query?.SELECT?.from.join)
|
|
12
|
+
const isProjection = e => Boolean(e.query?.SELECT?.from.ref)
|
|
13
|
+
const isSetUnion = e => Boolean(e.query?.SET?.op === 'union')
|
|
14
|
+
const projectionTarget = e => e.query.SELECT.from.ref[0]
|
|
15
|
+
|
|
16
|
+
function extractActions (e, csn) {
|
|
17
|
+
const getActions = e => Object.keys(e.actions ?? {})
|
|
18
|
+
const queue = [e]
|
|
19
|
+
const actions = []
|
|
20
|
+
while (queue.length) {
|
|
21
|
+
const entity = queue.pop()
|
|
22
|
+
actions.push(...getActions(entity))
|
|
23
|
+
if (isJoin(entity)) {
|
|
24
|
+
for (const { ref } of entity.query.SELECT.from.args[0].args) {
|
|
25
|
+
const ancestor = csn.definitions[ref[0]]
|
|
26
|
+
if (ancestor) {
|
|
27
|
+
queue.push(ancestor)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} else if (isProjection(entity)) {
|
|
31
|
+
const ancestor = csn.definitions[projectionTarget(entity)]
|
|
32
|
+
if (ancestor) {
|
|
33
|
+
queue.push(ancestor)
|
|
34
|
+
}
|
|
35
|
+
} else if (isSetUnion(entity)) {
|
|
36
|
+
// these are entities/ queries themselves. Just queue those.
|
|
37
|
+
queue.push(...entity.query.SET.args.map(e => ({query: e}) ))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return actions
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
const TYPICAL_ISSUES = {
|
|
12
44
|
__proto__: null,
|
|
13
45
|
any: '*'
|
|
@@ -45,7 +77,7 @@ module.exports = {
|
|
|
45
77
|
|
|
46
78
|
const node = context.getNode(e)
|
|
47
79
|
const file = e.$location.file
|
|
48
|
-
const actionNames = e
|
|
80
|
+
const actionNames = extractActions(e, context.getModel())
|
|
49
81
|
const validEventsAndActions = [ ...VALID_EVENTS, ...actionNames ]
|
|
50
82
|
|
|
51
83
|
for (const entry of e['@restrict']) {
|
|
@@ -31,7 +31,7 @@ const rule = module.exports = {
|
|
|
31
31
|
const base = baseModel(context)
|
|
32
32
|
if (!base) return
|
|
33
33
|
|
|
34
|
-
const file = context.
|
|
34
|
+
const file = context.filename
|
|
35
35
|
const findings = rule.mtxApi().lint(extModel, base.model, base.env)
|
|
36
36
|
for (const finding of findings) {
|
|
37
37
|
context.report({
|
|
@@ -48,7 +48,7 @@ const rule = module.exports = {
|
|
|
48
48
|
* @param {CDSRuleContext} context
|
|
49
49
|
*/
|
|
50
50
|
function baseModel (context) {
|
|
51
|
-
let dir = context.
|
|
51
|
+
let dir = context.filename
|
|
52
52
|
do {
|
|
53
53
|
dir = dirname(dir)
|
|
54
54
|
const projEnv = cds.env.for('cds', dir)
|
|
@@ -25,13 +25,13 @@ module.exports = {
|
|
|
25
25
|
const knownClasses = new Set()
|
|
26
26
|
|
|
27
27
|
/** @param {Node} node */
|
|
28
|
-
const refersToClass = node =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
// const refersToClass = node =>
|
|
29
|
+
// node.type === 'class_literal'
|
|
30
|
+
// // or part of explicit named imports
|
|
31
|
+
// || knownClasses.has(node.text)
|
|
32
|
+
// // or first letter of last part of text is uppercase: a.b.C -> true
|
|
33
|
+
// // https://unicode.org/reports/tr18/#General_Category_Property
|
|
34
|
+
// || /^\p{Lu}/u.test(node.text.split('.').at(-1))
|
|
35
35
|
|
|
36
36
|
const isString = node =>
|
|
37
37
|
node?.type === 'string_literal'
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
const produceScope = name => ({ name, variables: [] })
|
|
15
15
|
|
|
16
|
-
const produceHandlerRegistration = ({call, handler}) => ({ call, handler })
|
|
16
|
+
const produceHandlerRegistration = ({ call, handler }) => ({ call, handler })
|
|
17
17
|
|
|
18
18
|
// matches: @sap/cds, "@sap/cds", '@sap/cds', `@sap/cds`, but not @sap/cds-compiler etc
|
|
19
19
|
const isSapCds = name => Boolean(name?.match(/^\W*@sap\/cds\W*$/))
|
|
@@ -21,15 +21,15 @@ const isSapCds = name => Boolean(name?.match(/^\W*@sap\/cds\W*$/))
|
|
|
21
21
|
/**
|
|
22
22
|
* @param {VariableType} type - type of the variable
|
|
23
23
|
*/
|
|
24
|
-
const produceVariable = ({name, type, isCdsVariable, original}) => ({ name, type, original, isCdsVariable })
|
|
24
|
+
const produceVariable = ({ name, type, isCdsVariable, original }) => ({ name, type, original, isCdsVariable })
|
|
25
25
|
|
|
26
26
|
// like: require('@sap/cds')
|
|
27
27
|
const isCdsRequire = node => Boolean(node?.type === 'CallExpression'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
&& node.callee.type === 'Identifier'
|
|
29
|
+
&& node.callee.name === 'require'
|
|
30
|
+
&& node.arguments.length === 1
|
|
31
|
+
&& node.arguments[0].type === 'Literal'
|
|
32
|
+
&& isSapCds(node.arguments[0].value))
|
|
33
33
|
|
|
34
34
|
// like: import ... from '@sap/cds'
|
|
35
35
|
const isCdsImport = node => isSapCds(node?.source?.value)
|
|
@@ -39,13 +39,13 @@ const isFunctionBody = node => ['FunctionExpression', 'FunctionDeclaration', 'Ar
|
|
|
39
39
|
|
|
40
40
|
class CdsHandlerRule {
|
|
41
41
|
get isInsideCapService() { return this.capServiceStack.length > 0 }
|
|
42
|
-
get isInsideCapHandlerRegistration() {
|
|
42
|
+
get isInsideCapHandlerRegistration() { return this.capHandlerRegistrationStack.length > 0 }
|
|
43
43
|
|
|
44
|
-
constructor
|
|
44
|
+
constructor(context) {
|
|
45
45
|
/** @type {import('eslint').Rule.RuleContext} */
|
|
46
46
|
this.context = context
|
|
47
47
|
/** @type {Scope[]} */
|
|
48
|
-
this.functionScopes = [
|
|
48
|
+
this.functionScopes = [produceScope('<global>')]
|
|
49
49
|
this.capServiceStack = [] // stack of class bodies. Should probably never be more than one...
|
|
50
50
|
this.capHandlerRegistrationStack = [] // stack of handler registration calls. Should probably never be more than one...
|
|
51
51
|
}
|
|
@@ -53,7 +53,7 @@ class CdsHandlerRule {
|
|
|
53
53
|
/**
|
|
54
54
|
* @param {string} varName - name of the variable
|
|
55
55
|
*/
|
|
56
|
-
findDefinitionScope
|
|
56
|
+
findDefinitionScope(varName) {
|
|
57
57
|
const scopes = this.functionScopes
|
|
58
58
|
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
59
59
|
const scope = scopes[i]
|
|
@@ -69,22 +69,22 @@ class CdsHandlerRule {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// ClassExpression or ClassDeclaration
|
|
72
|
-
isCdsServiceClass
|
|
72
|
+
isCdsServiceClass(node) {
|
|
73
73
|
const superClass = node.superClass
|
|
74
74
|
if (!superClass) return false // no extends clause
|
|
75
75
|
let name
|
|
76
76
|
switch (superClass.type) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
case 'MemberExpression': {
|
|
78
|
+
// like: class X extends cds.ApplicationService
|
|
79
|
+
name = superClass.object.name
|
|
80
|
+
// TODO: && is *Service?
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
case 'Identifier': {
|
|
84
|
+
// like: class X extends ApplicationService
|
|
85
|
+
name = superClass.name
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
88
|
}
|
|
89
89
|
const info = this.findDefinitionScope(name)
|
|
90
90
|
return info?.variable.isCdsVariable
|
|
@@ -93,29 +93,29 @@ class CdsHandlerRule {
|
|
|
93
93
|
/**
|
|
94
94
|
* @param {ReturnType<typeof produceHandlerRegistration>} registration - the handler registration to add
|
|
95
95
|
*/
|
|
96
|
-
addCapHandlerRegistration
|
|
96
|
+
addCapHandlerRegistration(registration) {
|
|
97
97
|
this.capHandlerRegistrationStack.push(registration)
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
removeCapHandlerRegistration
|
|
100
|
+
removeCapHandlerRegistration() {
|
|
101
101
|
this.capHandlerRegistrationStack.pop()
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* @param {Variable} variable
|
|
106
106
|
*/
|
|
107
|
-
addScopeVariable
|
|
107
|
+
addScopeVariable(variable) {
|
|
108
108
|
this.functionScopes.at(-1).variables.push(variable)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* @param {Scope} scope - the scope to add
|
|
113
113
|
*/
|
|
114
|
-
enterFunctionScope
|
|
114
|
+
enterFunctionScope(scope) {
|
|
115
115
|
this.functionScopes.push(scope)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
leaveFunctionScope
|
|
118
|
+
leaveFunctionScope() {
|
|
119
119
|
this.functionScopes.pop()
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -123,9 +123,9 @@ class CdsHandlerRule {
|
|
|
123
123
|
* @abstract
|
|
124
124
|
*/
|
|
125
125
|
// eslint-disable-next-line no-unused-vars
|
|
126
|
-
CAPHandlerRegistration
|
|
126
|
+
CAPHandlerRegistration(node) { /* abstract */ }
|
|
127
127
|
|
|
128
|
-
ClassBody
|
|
128
|
+
ClassBody(node) {
|
|
129
129
|
// by hooking into ClassBody and ascending to .parent, we capture declarations and expressions:
|
|
130
130
|
// like: module.exports = class X extends cds.ApplicationService (ClassExpression)
|
|
131
131
|
// like: class X extends cds.ApplicationService (ClassDeclaration)
|
|
@@ -164,21 +164,21 @@ class CdsHandlerRule {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
BlockStatement(node) {
|
|
167
|
-
if(isFunctionBody(node)) {
|
|
167
|
+
if (isFunctionBody(node)) {
|
|
168
168
|
this.enterFunctionScope(produceScope(
|
|
169
169
|
node.parent?.key?.name
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
?? node.parent?.id?.name
|
|
171
|
+
?? node.parent?.parent?.key?.name
|
|
172
|
+
// const f = function() { ... }
|
|
173
|
+
?? (node.parent?.parent?.type === 'VariableDeclarator'
|
|
174
|
+
? node.parent.parent.id.name
|
|
175
|
+
: undefined)
|
|
176
|
+
?? '<anonymous>'))
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
'BlockStatement:exit'(node) {
|
|
181
|
-
if(isFunctionBody(node)) {
|
|
181
|
+
if (isFunctionBody(node)) {
|
|
182
182
|
this.leaveFunctionScope()
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -199,60 +199,60 @@ class CdsHandlerRule {
|
|
|
199
199
|
const isCdsVariable = isCdsImport(node)
|
|
200
200
|
for (const specifier of specifiers) {
|
|
201
201
|
switch (specifier.type) {
|
|
202
|
-
|
|
202
|
+
case 'ImportNamespaceSpecifier':
|
|
203
203
|
// like: import * as x from y
|
|
204
204
|
// fallthrough
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
case 'ImportDefaultSpecifier':
|
|
206
|
+
// like: import x from y
|
|
207
|
+
this.addScopeVariable(produceVariable({
|
|
208
|
+
original: specifier.local.name,
|
|
209
|
+
name: specifier.local.name,
|
|
210
|
+
type: 'import',
|
|
211
|
+
isCdsVariable
|
|
212
|
+
}))
|
|
213
|
+
break
|
|
214
|
+
case 'ImportSpecifier':
|
|
215
|
+
// like: import { x, y as foo } from z
|
|
216
|
+
this.addScopeVariable(produceVariable({
|
|
217
|
+
original: specifier.imported.name,
|
|
218
|
+
name: specifier.local.name,
|
|
219
|
+
type: 'import',
|
|
220
|
+
isCdsVariable
|
|
221
|
+
}))
|
|
222
|
+
break
|
|
223
|
+
default:
|
|
224
|
+
throw new Error(`Unexpected specifier type: ${specifier.type}`)
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
VariableDeclarator({id, init, parent}) {
|
|
230
|
+
VariableDeclarator({ id, init, parent }) {
|
|
231
231
|
// like: const ... = require('@sap/cds')
|
|
232
232
|
const isCdsVariable = isCdsRequire(init)
|
|
233
233
|
switch (id.type) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
234
|
+
case 'Identifier':
|
|
235
|
+
// like: const x = y
|
|
236
|
+
this.addScopeVariable(produceVariable({
|
|
237
|
+
original: id.name,
|
|
238
|
+
name: id.name,
|
|
239
|
+
type: parent.kind,
|
|
240
|
+
isCdsVariable
|
|
241
|
+
}))
|
|
242
|
+
break
|
|
243
|
+
case 'ObjectPattern':
|
|
244
|
+
// like: const { x, y } = z
|
|
245
|
+
for (const { key, type, value } of id.properties) {
|
|
246
|
+
if (type === 'Property' && key.type === 'Identifier') {
|
|
247
|
+
this.addScopeVariable(produceVariable({
|
|
248
|
+
original: key.name,
|
|
249
|
+
type: parent.kind,
|
|
250
|
+
isCdsVariable,
|
|
251
|
+
name: value?.name
|
|
252
|
+
}))
|
|
253
|
+
}
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
-
break
|
|
255
|
+
break
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
@@ -272,7 +272,7 @@ class CdsHandlerRule {
|
|
|
272
272
|
* ```
|
|
273
273
|
* in your rule definition, or else the visitor methods will not be called by ESL.
|
|
274
274
|
*/
|
|
275
|
-
asESLintVisitor
|
|
275
|
+
asESLintVisitor() {
|
|
276
276
|
let proto = Object.getPrototypeOf(this)
|
|
277
277
|
const visitors = {}
|
|
278
278
|
while (proto && proto !== Object.prototype) {
|
|
@@ -32,7 +32,7 @@ class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
|
32
32
|
|
|
33
33
|
const [functionName, prefix] = node.callee.type === 'MemberExpression'
|
|
34
34
|
// for ….where`...` we need to use the full preceding expression in the following replacement
|
|
35
|
-
? [node.callee.property?.name, this.context.
|
|
35
|
+
? [node.callee.property?.name, this.context.sourceCode.getText(node.callee)]
|
|
36
36
|
// for SELECT`...` we can use the function name directly
|
|
37
37
|
: [node.callee.name, node.callee.name]
|
|
38
38
|
this.context.report({
|
|
@@ -42,7 +42,7 @@ class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
|
42
42
|
suggest: [{
|
|
43
43
|
desc: 'Use {{functionName}}`...` instead of {{functionName}}(`...`)',
|
|
44
44
|
data: { functionName },
|
|
45
|
-
fix: fixer => fixer.replaceText(node, `${prefix}${this.context.
|
|
45
|
+
fix: fixer => fixer.replaceText(node, `${prefix}${this.context.sourceCode.getText(arg)}`)
|
|
46
46
|
}]
|
|
47
47
|
})
|
|
48
48
|
}
|
|
@@ -12,7 +12,7 @@ const MAX_INPUT_STRING_LENGTH = 1_000
|
|
|
12
12
|
* @param {Node} node
|
|
13
13
|
*/
|
|
14
14
|
function compareImportAndFilename (importPath, context, node) {
|
|
15
|
-
const currentFile = context.
|
|
15
|
+
const currentFile = context.filename
|
|
16
16
|
// ignore stdin
|
|
17
17
|
if (currentFile === '<input>') return
|
|
18
18
|
// ignore excessively long strings
|
|
@@ -29,8 +29,8 @@ module.exports = {
|
|
|
29
29
|
return checkValidHeaders
|
|
30
30
|
|
|
31
31
|
function checkValidHeaders () {
|
|
32
|
-
const filePath = context.
|
|
33
|
-
const sourcecode = context.
|
|
32
|
+
const filePath = context.filename
|
|
33
|
+
const sourcecode = context.sourceCode
|
|
34
34
|
const code = sourcecode.getText()
|
|
35
35
|
|
|
36
36
|
let model = context.getModel()
|
package/lib/utils/createRule.js
CHANGED
|
@@ -38,64 +38,64 @@ module.exports = function createRule(spec) {
|
|
|
38
38
|
create: context => {
|
|
39
39
|
// do a fast check to exclude most cases, i.e. irrelevant files
|
|
40
40
|
const isRelevant =
|
|
41
|
-
context.
|
|
42
|
-
isConfiguredFileType(context.
|
|
41
|
+
context.sourceCode.lines[0] === '' || // env. rules
|
|
42
|
+
isConfiguredFileType(context.filename, 'FILES') // file rules
|
|
43
43
|
if (!isRelevant) {
|
|
44
44
|
return {}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
return {
|
|
48
48
|
Program: node => {
|
|
49
|
-
const file = context.
|
|
49
|
+
const file = context.filename
|
|
50
50
|
if (file !== filePrev) {
|
|
51
|
-
LOG?.(`File: ${context.
|
|
51
|
+
LOG?.(`File: ${context.filename}`)
|
|
52
52
|
}
|
|
53
53
|
const cdsContext = extendContext(node, context, meta)
|
|
54
54
|
globalCache.set('context', cdsContext)
|
|
55
55
|
const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdsContext)
|
|
56
56
|
switch (meta.model) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
case 'none':
|
|
58
|
+
if (doEnvironmentChecks) {
|
|
59
|
+
if (isTest || !globalCache.has(`rule:${cdsContext.id}`)) {
|
|
60
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
61
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
62
|
+
createReport(node, cdsContext, meta, create)
|
|
63
|
+
}
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
break
|
|
65
|
+
break
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (isTest || showInEditor || !globalCache.has(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)) {
|
|
75
|
-
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
76
|
-
if (!showInEditor) {
|
|
77
|
-
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
67
|
+
case 'inferred':
|
|
68
|
+
if (isValidFile && doRootModelChecks) {
|
|
69
|
+
if (showInEditor) {
|
|
70
|
+
globalCache.remove(`model:${globalCache.get('rootpath')}`)
|
|
71
|
+
globalCache.remove(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)
|
|
72
|
+
globalCache.remove(`report:${context.filename}:${context.id}`)
|
|
78
73
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
if (isTest || showInEditor || !globalCache.has(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)) {
|
|
75
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
76
|
+
if (!showInEditor) {
|
|
77
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
78
|
+
}
|
|
79
|
+
createReport(node, cdsContext, meta, create)
|
|
80
|
+
} else {
|
|
81
|
+
if (globalCache.has(`report:${context.filename}:${context.id}`)) {
|
|
82
|
+
const reports = globalCache.get(`report:${context.filename}:${context.id}`)
|
|
83
|
+
for (const r of Array.from(reports)) {
|
|
84
|
+
context.report(JSON.parse(r))
|
|
85
|
+
}
|
|
86
|
+
globalCache.remove(`report:${context.filename}:${context.id}`)
|
|
87
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
85
88
|
}
|
|
86
|
-
globalCache.remove(`report:${context.getFilename()}:${context.id}`)
|
|
87
|
-
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
break
|
|
91
|
+
break
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
default:
|
|
94
|
+
if (isValidFile) {
|
|
95
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
96
|
+
createReport(node, cdsContext, meta, create)
|
|
97
|
+
}
|
|
98
|
+
break
|
|
99
99
|
}
|
|
100
100
|
filePrev = file
|
|
101
101
|
}
|
|
@@ -104,26 +104,26 @@ module.exports = function createRule(spec) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function isRunningWithCDSLint
|
|
107
|
+
function isRunningWithCDSLint() {
|
|
108
108
|
return process.argv[1].match(/cds(\.js)?$/) && process.argv[2]?.toLowerCase() === 'lint'
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function isRunningWithESLint
|
|
111
|
+
function isRunningWithESLint() {
|
|
112
112
|
return process.argv[1].match(/eslint(\.js)?$/)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function checkEntryCriteria
|
|
115
|
+
function checkEntryCriteria(meta, cdsContext) {
|
|
116
116
|
const isTest = globalCache.has('test')
|
|
117
117
|
const showInEditor = cdsContext.options.includes('show')
|
|
118
|
-
const isValidFile = isConfiguredFileType(cdsContext.
|
|
118
|
+
const isValidFile = isConfiguredFileType(cdsContext.filename, 'FILES')
|
|
119
119
|
const doRootModelChecks = isTest || (hasProjectRoots() && (isRunningWithCDSLint() || isRunningWithESLint()) || showInEditor)
|
|
120
120
|
// Lint all env rules independent of any parsed file (i.e. 'cds lint' uses the lintText "" API)
|
|
121
121
|
const doEnvironmentChecks =
|
|
122
|
-
isTest || (isRunningWithCDSLint() && cdsContext.
|
|
122
|
+
isTest || (isRunningWithCDSLint() && cdsContext.filename === '<text>')
|
|
123
123
|
return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks, showInEditor }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function setMetaDefaults
|
|
126
|
+
function setMetaDefaults(meta) {
|
|
127
127
|
meta ??= {}
|
|
128
128
|
meta.severity ??= constants.DEFAULT_RULE_SEVERITY
|
|
129
129
|
meta.docs ??= {}
|
|
@@ -145,7 +145,7 @@ function setMetaDefaults (meta) {
|
|
|
145
145
|
* @param {Function} create
|
|
146
146
|
* @returns
|
|
147
147
|
*/
|
|
148
|
-
function createReport
|
|
148
|
+
function createReport(node, cdsContext, meta, create) {
|
|
149
149
|
const handlers = create(cdsContext)
|
|
150
150
|
/**
|
|
151
151
|
* TODO: Can these be rewritten to have a visitor? Note, that so far,
|
|
@@ -155,33 +155,33 @@ function createReport (node, cdsContext, meta, create) {
|
|
|
155
155
|
* - Environment rules
|
|
156
156
|
*/
|
|
157
157
|
switch (typeof handlers) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
case 'function':
|
|
159
|
+
handlers()
|
|
160
|
+
break
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
case 'object': {
|
|
163
|
+
if (meta.model !== 'none') {
|
|
164
|
+
const model = cdsContext.getModel()
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
if (model) {
|
|
167
|
+
model.forall(d => {
|
|
168
|
+
d = (meta.model === 'inferred') ? sanitizeFileLocation(d) : d
|
|
169
|
+
const isValidLocation = (meta.model === 'parsed' && d.$location) ||
|
|
170
170
|
(meta.model === 'inferred' && d.$location?.file)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
Object.entries(handlers)
|
|
172
|
+
.filter(([type,]) => d.is(type) && isValidLocation)
|
|
173
|
+
.forEach(([, handler]) => {
|
|
174
|
+
handler(d)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}
|
|
177
178
|
}
|
|
179
|
+
break
|
|
178
180
|
}
|
|
179
|
-
break
|
|
180
|
-
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function sanitizeFileLocation
|
|
184
|
+
function sanitizeFileLocation(d) {
|
|
185
185
|
let parent = d
|
|
186
186
|
while (!parent.$location && parent.parent && !parent.parent.definitions)
|
|
187
187
|
parent = d.parent
|
|
@@ -195,9 +195,9 @@ function sanitizeFileLocation (d) {
|
|
|
195
195
|
* @param {CDSRuleContext} context
|
|
196
196
|
* @param meta
|
|
197
197
|
*/
|
|
198
|
-
function extendContext
|
|
198
|
+
function extendContext(node, context, meta) {
|
|
199
199
|
if (!globalCache.has('test')) {
|
|
200
|
-
const filePath = context.
|
|
200
|
+
const filePath = context.filename
|
|
201
201
|
const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
|
|
202
202
|
if (rootPath) {
|
|
203
203
|
globalCache.set('rootpath', rootPath)
|
|
@@ -236,7 +236,7 @@ function extendContext (node, context, meta) {
|
|
|
236
236
|
throw new CdsLintAssertionError(`Rule ${context.id} must return a "file" property in the rule report!`)
|
|
237
237
|
}
|
|
238
238
|
const file = globalCache.get('rootpath') ? resolveFilePath(r.file) : r.file
|
|
239
|
-
if (cdscontext.
|
|
239
|
+
if (cdscontext.filename === file) {
|
|
240
240
|
delete r.file
|
|
241
241
|
context.report(r)
|
|
242
242
|
}
|
|
@@ -255,10 +255,10 @@ function extendContext (node, context, meta) {
|
|
|
255
255
|
* @param {number} line
|
|
256
256
|
* @param {CDSRuleContext} cdsContext
|
|
257
257
|
*/
|
|
258
|
-
function isRuleDisabled
|
|
258
|
+
function isRuleDisabled(line, cdsContext) {
|
|
259
259
|
let isDisabled = false
|
|
260
260
|
if (cdsContext) {
|
|
261
|
-
const sourcecode = cdsContext.
|
|
261
|
+
const sourcecode = cdsContext.sourceCode
|
|
262
262
|
const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line)
|
|
263
263
|
const id = cdsContext.id
|
|
264
264
|
isDisabled = line && id in rulesDisabled && rulesDisabled[id] === 'off'
|
|
@@ -272,7 +272,7 @@ function isRuleDisabled (line, cdsContext) {
|
|
|
272
272
|
* @param {CDSRuleContext} context
|
|
273
273
|
* @param meta
|
|
274
274
|
*/
|
|
275
|
-
function cacheReport
|
|
275
|
+
function cacheReport(r, filepath, context, meta) {
|
|
276
276
|
delete r.file
|
|
277
277
|
if (r.node && r.node.range) {
|
|
278
278
|
r.node.range = []
|
|
@@ -296,7 +296,7 @@ function cacheReport (r, filepath, context, meta) {
|
|
|
296
296
|
* @param {string} sourcecode
|
|
297
297
|
* @param {number} line
|
|
298
298
|
*/
|
|
299
|
-
function getDisabled
|
|
299
|
+
function getDisabled(code, sourcecode, line) {
|
|
300
300
|
const listDisabled = []
|
|
301
301
|
const rules = globalCache.get('rules')
|
|
302
302
|
const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
|
|
@@ -369,7 +369,7 @@ function getDisabled (code, sourcecode, line) {
|
|
|
369
369
|
/**
|
|
370
370
|
* @param {string} code
|
|
371
371
|
*/
|
|
372
|
-
function getLastLine
|
|
372
|
+
function getLastLine(code) {
|
|
373
373
|
const lines = typeof code === 'string' ? SourceCode.splitLines(code) : code
|
|
374
374
|
return lines.length - 1
|
|
375
375
|
}
|
|
@@ -377,6 +377,6 @@ function getLastLine (code) {
|
|
|
377
377
|
/**
|
|
378
378
|
* @param {string} file
|
|
379
379
|
*/
|
|
380
|
-
function resolveFilePath
|
|
380
|
+
function resolveFilePath(file) {
|
|
381
381
|
return path.isAbsolute(file) ? file : path.join(globalCache.get('rootpath'), file)
|
|
382
382
|
}
|
package/lib/utils/rules.js
CHANGED
|
@@ -92,7 +92,7 @@ module.exports = {
|
|
|
92
92
|
|
|
93
93
|
getReplacementsSuggestions: function (context, value, loc) {
|
|
94
94
|
let invalid
|
|
95
|
-
const lineToReplace = context.
|
|
95
|
+
const lineToReplace = context.sourceCode.lines[loc.line]
|
|
96
96
|
const regExp = /\[([^)]+)\]/
|
|
97
97
|
const matches = regExp.exec(lineToReplace)
|
|
98
98
|
if (matches && matches[0]) {
|
|
@@ -25,7 +25,7 @@ function testRuleWrapper(rule) {
|
|
|
25
25
|
function prepareAndRunRule(context) {
|
|
26
26
|
return {
|
|
27
27
|
Program: node => {
|
|
28
|
-
const filePath = context.
|
|
28
|
+
const filePath = context.filename
|
|
29
29
|
_initModelRuleTester(filePath, rule.meta.model)
|
|
30
30
|
const createValue = rule.create(context)
|
|
31
31
|
const result = createValue.Program(node)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "ESLint plugin including recommended SAP Cloud Application Programming model and environment rules",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@sap/cds": ">=9",
|
|
30
|
-
"eslint": "^9"
|
|
30
|
+
"eslint": "^9 || ^10"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=20"
|