@sap/eslint-plugin-cds 3.1.2 → 4.0.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 +30 -2
- package/LICENSE +15 -21
- package/lib/conf/all.js +2 -0
- package/lib/conf/experimental.js +1 -2
- package/lib/conf/index.js +17 -22
- package/lib/conf/js/all.js +8 -0
- package/lib/conf/js/recommended.js +8 -0
- package/lib/constants.js +7 -0
- package/lib/parser.js +53 -35
- package/lib/rules/assoc2many-ambiguous-key.js +1 -1
- package/lib/rules/auth-no-empty-restrictions.js +1 -1
- package/lib/rules/auth-restrict-grant-service.js +1 -1
- package/lib/rules/auth-use-requires.js +1 -1
- package/lib/rules/auth-valid-restrict-grant.js +1 -1
- package/lib/rules/auth-valid-restrict-keys.js +1 -1
- package/lib/rules/auth-valid-restrict-to.js +1 -1
- package/lib/rules/auth-valid-restrict-where.js +1 -1
- package/lib/rules/extension-restrictions.js +1 -1
- package/lib/rules/index.js +19 -23
- package/lib/rules/js/CdsHandlerRule.js +265 -0
- package/lib/rules/js/no-cross-service-import.js +69 -0
- package/lib/rules/js/no-deep-sap-cds-import.js +56 -0
- package/lib/rules/js/no-shared-handler-variable.js +73 -0
- package/lib/rules/js/types.d.ts +15 -0
- package/lib/rules/js/use-cql-select-template-strings.js +35 -0
- package/lib/rules/latest-cds-version.js +2 -0
- package/lib/rules/no-db-keywords.js +3 -2
- package/lib/rules/no-dollar-prefixed-names.js +67 -9
- package/lib/rules/no-java-keywords.js +3 -1
- package/lib/rules/no-join-on-draft.js +4 -1
- package/lib/rules/sql-cast-suggestion.js +46 -22
- package/lib/rules/sql-null-comparison.js +39 -35
- package/lib/rules/start-elements-lowercase.js +43 -39
- package/lib/rules/start-entities-uppercase.js +24 -34
- package/lib/rules/valid-csv-header.js +3 -2
- package/lib/utils/Cache.js +21 -18
- package/lib/utils/createRule.js +47 -44
- package/lib/utils/getConfigPath.js +1 -12
- package/lib/utils/{getProjectRootPath.js → projectRootPath.js} +18 -6
- package/lib/utils/rules.js +10 -9
- package/lib/utils/runRuleTester.js +16 -24
- package/package.json +5 -5
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// TODO:
|
|
2
|
+
// - class extends require('@sap/cds').ApplicationService
|
|
3
|
+
// - class extends await import('@sap/cds').ApplicationService
|
|
4
|
+
/** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.Scope} Scope */
|
|
5
|
+
/** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.Variable} Variable */
|
|
6
|
+
/** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.VariableType} VariableType */
|
|
7
|
+
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} name - name of the scope
|
|
12
|
+
* @returns {Scope}
|
|
13
|
+
*/
|
|
14
|
+
const produceScope = name => ({ name, variables: [] })
|
|
15
|
+
|
|
16
|
+
const produceHandlerRegistration = ({call, handler}) => ({ call, handler })
|
|
17
|
+
|
|
18
|
+
// matches: @sap/cds, "@sap/cds", '@sap/cds', `@sap/cds`, but not @sap/cds-compiler etc
|
|
19
|
+
const isSapCds = name => Boolean(name?.match(/^\W*@sap\/cds\W*$/))
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {VariableType} type - type of the variable
|
|
23
|
+
*/
|
|
24
|
+
const produceVariable = ({name, type, isCdsVariable, original}) => ({ name, type, original, isCdsVariable })
|
|
25
|
+
|
|
26
|
+
// like: require('@sap/cds')
|
|
27
|
+
const isCdsRequire = node => Boolean(node?.type === 'CallExpression'
|
|
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
|
+
|
|
34
|
+
// like: import ... from '@sap/cds'
|
|
35
|
+
const isCdsImport = node => isSapCds(node?.source?.value)
|
|
36
|
+
|
|
37
|
+
const isFunctionBody = node => ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression']
|
|
38
|
+
.includes(node.parent.type)
|
|
39
|
+
|
|
40
|
+
class CdsHandlerRule {
|
|
41
|
+
get isInsideCapService() { return this.capServiceStack.length > 0 }
|
|
42
|
+
get isInsideCapHandlerRegistration() { return this.capHandlerRegistrationStack.length > 0 }
|
|
43
|
+
|
|
44
|
+
constructor (context) {
|
|
45
|
+
this.context = context
|
|
46
|
+
/** @type {Scope[]} */
|
|
47
|
+
this.functionScopes = [ produceScope('<global>') ]
|
|
48
|
+
this.capServiceStack = [] // stack of class bodies. Should probably never be more than one...
|
|
49
|
+
this.capHandlerRegistrationStack = [] // stack of handler registration calls. Should probably never be more than one...
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} varName - name of the variable
|
|
54
|
+
*/
|
|
55
|
+
findDefinitionScope (varName) {
|
|
56
|
+
const scopes = this.functionScopes
|
|
57
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
58
|
+
const scope = scopes[i]
|
|
59
|
+
const variable = scope.variables.find(variable => variable.name === varName)
|
|
60
|
+
if (variable) return {
|
|
61
|
+
scope,
|
|
62
|
+
variable,
|
|
63
|
+
isLocal: i === scopes.length - 1,
|
|
64
|
+
isGlobal: i === 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ClassExpression or ClassDeclaration
|
|
71
|
+
isCdsServiceClass (node) {
|
|
72
|
+
const superClass = node.superClass
|
|
73
|
+
if (!superClass) return false // no extends clause
|
|
74
|
+
let name
|
|
75
|
+
switch (superClass.type) {
|
|
76
|
+
case 'MemberExpression': {
|
|
77
|
+
// like: class X extends cds.ApplicationService
|
|
78
|
+
name = superClass.object.name
|
|
79
|
+
// TODO: && is *Service?
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
case 'Identifier': {
|
|
83
|
+
// like: class X extends ApplicationService
|
|
84
|
+
name = superClass.name
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const info = this.findDefinitionScope(name)
|
|
89
|
+
return info?.variable.isCdsVariable
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {Variable} variable
|
|
94
|
+
*/
|
|
95
|
+
addScopeVariable (variable) {
|
|
96
|
+
this.functionScopes.at(-1).variables.push(variable)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @abstract
|
|
101
|
+
*/
|
|
102
|
+
// eslint-disable-next-line no-unused-vars
|
|
103
|
+
CAPHandlerRegistration (node) { /* abstract */ }
|
|
104
|
+
|
|
105
|
+
ClassBody (node) {
|
|
106
|
+
// by hooking into ClassBody and ascending to .parent, we capture declarations and expressions:
|
|
107
|
+
// like: module.exports = class X extends cds.ApplicationService (ClassExpression)
|
|
108
|
+
// like: class X extends cds.ApplicationService (ClassDeclaration)
|
|
109
|
+
if (this.isCdsServiceClass(node.parent)) {
|
|
110
|
+
this.capServiceStack.push(node)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
'ClassBody:exit'(node) {
|
|
115
|
+
if (this.capServiceStack.at(-1) === node) {
|
|
116
|
+
this.capServiceStack.pop()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
CallExpression(node) {
|
|
121
|
+
if (!this.isInsideCapService) return
|
|
122
|
+
const { type, object, property } = node.callee
|
|
123
|
+
if (type === 'MemberExpression' && object.type === 'ThisExpression') {
|
|
124
|
+
// like: this.on('submitOrder', bar)
|
|
125
|
+
if (['before', 'on', 'after'].some(method => method === property.name)) {
|
|
126
|
+
const handler = node.arguments.at(-1)
|
|
127
|
+
this.capHandlerRegistrationStack.push(produceHandlerRegistration({
|
|
128
|
+
call: node,
|
|
129
|
+
handler
|
|
130
|
+
}))
|
|
131
|
+
// TODO: named references
|
|
132
|
+
this.CAPHandlerRegistration(handler)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
'CallExpression:exit'(node) {
|
|
138
|
+
if (this.capHandlerRegistrationStack.at(-1)?.call === node) {
|
|
139
|
+
this.capHandlerRegistrationStack.pop()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
BlockStatement(node) {
|
|
144
|
+
if(isFunctionBody(node)) {
|
|
145
|
+
this.functionScopes.push(produceScope(
|
|
146
|
+
node.parent?.key?.name
|
|
147
|
+
?? node.parent?.id?.name
|
|
148
|
+
?? node.parent?.parent?.key?.name
|
|
149
|
+
?? '<anonymous>'))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
'BlockStatement:exit'(node) {
|
|
154
|
+
if(isFunctionBody(node)) {
|
|
155
|
+
this.functionScopes.pop()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// the following visitors handle arrow functions, which can have a BlockStatement as body,
|
|
160
|
+
// OR just any single expression, like an Assignment, etc. This makes it very hard to
|
|
161
|
+
// determine when we are inside the body ArrowFunctionExpression from looking at the body,
|
|
162
|
+
// as we'd have to add a visitor for every expression type and check if it's a child of an ArrowFunctionExpression.
|
|
163
|
+
'ArrowFunctionExpression > :not(BlockStatement)'() {
|
|
164
|
+
this.functionScopes.push(produceScope('<anonymous>'))
|
|
165
|
+
}
|
|
166
|
+
'ArrowFunctionExpression > :not(BlockStatement):exit'() {
|
|
167
|
+
this.functionScopes.pop()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ImportDeclaration(node) {
|
|
171
|
+
const { specifiers } = node
|
|
172
|
+
const isCdsVariable = isCdsImport(node)
|
|
173
|
+
for (const specifier of specifiers) {
|
|
174
|
+
switch (specifier.type) {
|
|
175
|
+
case 'ImportNamespaceSpecifier':
|
|
176
|
+
// like: import * as x from y
|
|
177
|
+
// fallthrough
|
|
178
|
+
case 'ImportDefaultSpecifier':
|
|
179
|
+
// like: import x from y
|
|
180
|
+
this.addScopeVariable(produceVariable({
|
|
181
|
+
original: specifier.local.name,
|
|
182
|
+
name: specifier.local.name,
|
|
183
|
+
type: 'import',
|
|
184
|
+
isCdsVariable
|
|
185
|
+
}))
|
|
186
|
+
break
|
|
187
|
+
case 'ImportSpecifier':
|
|
188
|
+
// like: import { x, y as foo } from z
|
|
189
|
+
this.addScopeVariable(produceVariable({
|
|
190
|
+
original: specifier.imported.name,
|
|
191
|
+
name: specifier.local.name,
|
|
192
|
+
type: 'import',
|
|
193
|
+
isCdsVariable
|
|
194
|
+
}))
|
|
195
|
+
break
|
|
196
|
+
default:
|
|
197
|
+
throw new Error(`Unexpected specifier type: ${specifier.type}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
VariableDeclarator({id, init, parent}) {
|
|
204
|
+
// like: const ... = require('@sap/cds')
|
|
205
|
+
const isCdsVariable = isCdsRequire(init)
|
|
206
|
+
switch (id.type) {
|
|
207
|
+
case 'Identifier':
|
|
208
|
+
// like: const x = y
|
|
209
|
+
this.addScopeVariable(produceVariable({
|
|
210
|
+
original: id.name,
|
|
211
|
+
name: id.name,
|
|
212
|
+
type: parent.kind,
|
|
213
|
+
isCdsVariable
|
|
214
|
+
}))
|
|
215
|
+
break
|
|
216
|
+
case 'ObjectPattern':
|
|
217
|
+
// like: const { x, y } = z
|
|
218
|
+
for (const { key, type, value } of id.properties) {
|
|
219
|
+
if (type === 'Property' && key.type === 'Identifier') {
|
|
220
|
+
this.addScopeVariable(produceVariable({
|
|
221
|
+
original: key.name,
|
|
222
|
+
type: parent.kind,
|
|
223
|
+
isCdsVariable,
|
|
224
|
+
name: value?.name
|
|
225
|
+
}))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* ESLint expects an object literal with functions members when registering
|
|
234
|
+
* the visitor in create(context).
|
|
235
|
+
* This method transforms the class instance into such an object, retaining
|
|
236
|
+
* the base methods, as well as all methods defined in subclasses.
|
|
237
|
+
* Visitors are distinguished by their name starting with an uppercase letter.
|
|
238
|
+
* So you should only ever
|
|
239
|
+
* ```js
|
|
240
|
+
* return new MyCdsRule(context).asESLintVisitor()
|
|
241
|
+
* ```
|
|
242
|
+
* and not
|
|
243
|
+
* ```js
|
|
244
|
+
* return new MyCdsRule(context)
|
|
245
|
+
* ```
|
|
246
|
+
* in your rule definition, or else the visitor methods will not be called by ESL.
|
|
247
|
+
*/
|
|
248
|
+
asESLintVisitor () {
|
|
249
|
+
let proto = Object.getPrototypeOf(this)
|
|
250
|
+
const visitors = {}
|
|
251
|
+
while (proto && proto !== Object.prototype) {
|
|
252
|
+
Object.getOwnPropertyNames(proto)
|
|
253
|
+
.filter(key => typeof this[key] === 'function' && /^[A-Z]/.test(key))
|
|
254
|
+
.forEach(key => {
|
|
255
|
+
visitors[key] = this[key].bind(this)
|
|
256
|
+
})
|
|
257
|
+
proto = Object.getPrototypeOf(proto) // Move up the prototype chain
|
|
258
|
+
}
|
|
259
|
+
return visitors
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
CdsHandlerRule
|
|
265
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
|
+
|
|
5
|
+
// used as a pre-check to ensure the checks using RegExps
|
|
6
|
+
// with greedy matchers are not susceptible to ReDoS attacks
|
|
7
|
+
const MAX_INPUT_STRING_LENGTH = 1_000
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} importPath
|
|
11
|
+
* @param {RuleContext} context
|
|
12
|
+
* @param {Node} node
|
|
13
|
+
*/
|
|
14
|
+
function compareImportAndFilename (importPath, context, node) {
|
|
15
|
+
const currentFile = context.getFilename()
|
|
16
|
+
// ignore stdin
|
|
17
|
+
if (currentFile === '<input>') return
|
|
18
|
+
// ignore excessively long strings
|
|
19
|
+
if (importPath.length > MAX_INPUT_STRING_LENGTH || currentFile.length > MAX_INPUT_STRING_LENGTH) return
|
|
20
|
+
const [, typerModuleFq, typerModule] = /^#cds-models\/.*?((\w+)Service)$/.exec(importPath) ?? []
|
|
21
|
+
const [, fileNameFq, fileName] = /((\w+)-?[sS]ervice\.m?[jt]s)$/.exec(currentFile)
|
|
22
|
+
// typerModule === undefined -> not a service import (probably db-level-entity import)
|
|
23
|
+
if (typerModule && fileName && typerModule !== fileName) {
|
|
24
|
+
context.report({
|
|
25
|
+
node,
|
|
26
|
+
messageId: 'noCrossServiceImport',
|
|
27
|
+
data: {
|
|
28
|
+
from: typerModuleFq,
|
|
29
|
+
target: fileNameFq
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: 'problem',
|
|
38
|
+
docs: {
|
|
39
|
+
recommended: true,
|
|
40
|
+
category: RULE_CATEGORIES.javascript,
|
|
41
|
+
description: 'Warn about imports from another service.'
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
noCrossServiceImport: 'You are importing service-level entities from another service "{{from}}" inside the definition of service "{{target}}". This is likely an accidental cross-service import.',
|
|
46
|
+
},
|
|
47
|
+
hasSuggestions: false
|
|
48
|
+
},
|
|
49
|
+
create: context => ({
|
|
50
|
+
CallExpression(node) {
|
|
51
|
+
// look for: require('#cds-models/...')
|
|
52
|
+
if (node.callee.type !== 'Identifier') return
|
|
53
|
+
if (node.callee.name !== 'require') return
|
|
54
|
+
if (node.arguments.length !== 1) return
|
|
55
|
+
if (node.arguments[0].type !== 'Literal') return
|
|
56
|
+
compareImportAndFilename(node.arguments[0].value, context, node)
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
ImportDeclaration(node) {
|
|
60
|
+
// import ... from '#cds-models/...'
|
|
61
|
+
compareImportAndFilename(node.source.value, context, node)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
ImportExpression(node) {
|
|
65
|
+
// await import('#cds-models/...')
|
|
66
|
+
compareImportAndFilename(node.source.value, context, node)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const allowList = new Set([
|
|
3
|
+
// exception: not part of the facade and should be available to users this way
|
|
4
|
+
'@sap/cds/eslint.config.mjs'
|
|
5
|
+
])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} importPath
|
|
9
|
+
* @param {RuleContext} context
|
|
10
|
+
* @param {Node} node
|
|
11
|
+
*/
|
|
12
|
+
function checkImport (importPath, context, node) {
|
|
13
|
+
if (!importPath.startsWith('@sap/cds/')) return
|
|
14
|
+
if (allowList.has(importPath)) return
|
|
15
|
+
context.report({
|
|
16
|
+
node,
|
|
17
|
+
messageId: 'noDeepSapCdsImports',
|
|
18
|
+
data: { import: importPath }
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
recommended: true,
|
|
27
|
+
category: 'JavaScript Validation',
|
|
28
|
+
description: 'Warn about deep imports from @sap/cds.'
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: {
|
|
32
|
+
noDeepSapCdsImports: `"{{import}}" is a deep import. The API of @sap/cds is available only from its facade via 'require("@sap/cds")' or 'import ... from "@sap/cds"'.`,
|
|
33
|
+
},
|
|
34
|
+
hasSuggestions: false
|
|
35
|
+
},
|
|
36
|
+
create: context => ({
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
// look for: require('@sap/cds/...')
|
|
39
|
+
if (node.callee.type !== 'Identifier') return
|
|
40
|
+
if (node.callee.name !== 'require') return
|
|
41
|
+
if (node.arguments.length !== 1) return
|
|
42
|
+
if (node.arguments[0].type !== 'Literal') return
|
|
43
|
+
checkImport(node.arguments[0].value, context, node)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
ImportDeclaration(node) {
|
|
47
|
+
// import ... from '@sap/cds/...'
|
|
48
|
+
checkImport(node.source.value, context, node)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
ImportExpression(node) {
|
|
52
|
+
// await import('@sap/cds/...')
|
|
53
|
+
checkImport(node.source.value, context, node)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use cases not yet covered:
|
|
3
|
+
|
|
4
|
+
//---------
|
|
5
|
+
INLINE EXTENSION
|
|
6
|
+
class FooService extends require('@sap/cds').ApplicationService { ... }
|
|
7
|
+
|
|
8
|
+
//---------
|
|
9
|
+
REFERENCED FUNCTION
|
|
10
|
+
function bad() { ... }
|
|
11
|
+
|
|
12
|
+
class ... {
|
|
13
|
+
this.on('', bad)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//---------
|
|
17
|
+
METHOD
|
|
18
|
+
class ... {
|
|
19
|
+
bad () {}
|
|
20
|
+
|
|
21
|
+
this.on('', this.bad)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//---------
|
|
25
|
+
IMPORTED FUNCTION
|
|
26
|
+
const { bad } = require('./bad')
|
|
27
|
+
|
|
28
|
+
class ... {
|
|
29
|
+
this.on('', bad)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//---------
|
|
33
|
+
NON-CLASS-BASED CDS SERVICE
|
|
34
|
+
cds.services['myService'].on('READ', 'Books', () => {})
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict'
|
|
38
|
+
|
|
39
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
40
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
41
|
+
|
|
42
|
+
class NoSharedVariable extends CdsHandlerRule {
|
|
43
|
+
AssignmentExpression(node) {
|
|
44
|
+
if (!this.isInsideCapHandlerRegistration) return
|
|
45
|
+
const declaringScope = this.findDefinitionScope(node.left.name)
|
|
46
|
+
if (declaringScope?.isLocal === false) {
|
|
47
|
+
this.context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: 'noSharedHandlerVariable',
|
|
50
|
+
data: {
|
|
51
|
+
definitionScope: declaringScope.scope.name
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'problem',
|
|
61
|
+
docs: {
|
|
62
|
+
recommended: true,
|
|
63
|
+
category: RULE_CATEGORIES.javascript,
|
|
64
|
+
description: 'Enforce that variables can not be used to share state between handlers.'
|
|
65
|
+
},
|
|
66
|
+
schema: [],
|
|
67
|
+
messages: {
|
|
68
|
+
noSharedHandlerVariable: 'Assignment to a non-local variable inside a CDS event handler (was declared in scope "{{definitionScope}}").'
|
|
69
|
+
},
|
|
70
|
+
hasSuggestions: true
|
|
71
|
+
},
|
|
72
|
+
create: context => new NoSharedVariable(context).asESLintVisitor()
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare namespace CdsContextTracker {
|
|
2
|
+
type VariableType = 'let' | 'const' | 'var' | 'import'
|
|
3
|
+
|
|
4
|
+
type Variable = {
|
|
5
|
+
name: string,
|
|
6
|
+
original: string
|
|
7
|
+
type: VariableType,
|
|
8
|
+
isCdsVariable: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Scope = {
|
|
12
|
+
name: string,
|
|
13
|
+
variables: Variable[]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
5
|
+
|
|
6
|
+
class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
7
|
+
CallExpression(node) {
|
|
8
|
+
super.CallExpression(node)
|
|
9
|
+
if (node.callee?.name === 'SELECT' && node.arguments[0].type === 'TemplateLiteral') {
|
|
10
|
+
this.context.report({
|
|
11
|
+
node,
|
|
12
|
+
message: 'Do not use SELECT(`...`), which is prone to SQL injections.',
|
|
13
|
+
suggest: [{
|
|
14
|
+
desc: 'Use SELECT`...` instead',
|
|
15
|
+
fix: fixer => fixer.replaceText(node, `SELECT${this.context.getSourceCode().getText(node.arguments[0])}`)
|
|
16
|
+
}]
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
recommended: true,
|
|
27
|
+
category: RULE_CATEGORIES.javascript,
|
|
28
|
+
description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
|
|
29
|
+
},
|
|
30
|
+
fixable: 'code',
|
|
31
|
+
schema: [],
|
|
32
|
+
hasSuggestions: true
|
|
33
|
+
},
|
|
34
|
+
create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
|
|
35
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const cp = require('child_process')
|
|
4
4
|
const semver = require('semver')
|
|
5
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
meta: {
|
|
8
9
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
9
10
|
docs: {
|
|
11
|
+
category: RULE_CATEGORIES.environment,
|
|
10
12
|
description: 'Checks whether the latest `@sap/cds` version is being used.',
|
|
11
13
|
},
|
|
12
14
|
type: 'suggestion',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const cds = require('@sap/cds')
|
|
4
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
4
5
|
|
|
5
6
|
// REVISIT: Replace by compiler-provided check
|
|
6
7
|
const RESERVED = cds.compile.to.sql.sqlite
|
|
@@ -11,9 +12,9 @@ module.exports = {
|
|
|
11
12
|
meta: {
|
|
12
13
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
13
14
|
docs: {
|
|
15
|
+
category: RULE_CATEGORIES.model,
|
|
14
16
|
description: 'Avoid using reserved SQL keywords.',
|
|
15
|
-
|
|
16
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
|
|
17
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
|
|
17
18
|
},
|
|
18
19
|
messages: {
|
|
19
20
|
reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
|
|
@@ -1,29 +1,87 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Util to check if an entity is part of an external service.
|
|
7
|
+
*/
|
|
8
|
+
class ExternalServices {
|
|
9
|
+
hasExternalServices = false
|
|
10
|
+
externalServices = Object.create(null)
|
|
11
|
+
|
|
12
|
+
static create(model) {
|
|
13
|
+
return new ExternalServices(model)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(model) {
|
|
17
|
+
for (const defName in model.definitions) {
|
|
18
|
+
const def = model.definitions[defName]
|
|
19
|
+
if (def?.kind === 'service' && def['@cds.external']) {
|
|
20
|
+
this.externalServices[defName] = true
|
|
21
|
+
this.hasExternalServices = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isInExternalService(defName) {
|
|
27
|
+
if (!this.hasExternalServices)
|
|
28
|
+
return false // shortcut
|
|
29
|
+
const segments = defName.split('.')
|
|
30
|
+
for (let i = segments.length - 1; i >= 0; i--)
|
|
31
|
+
if (this.externalServices[segments.slice(0, i).join('.')])
|
|
32
|
+
return true
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
3
38
|
module.exports = {
|
|
4
39
|
meta: {
|
|
5
40
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
41
|
docs: {
|
|
42
|
+
category: RULE_CATEGORIES.model,
|
|
7
43
|
description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
|
|
8
44
|
recommended: true,
|
|
9
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
45
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
|
|
10
46
|
},
|
|
11
47
|
messages: {
|
|
12
48
|
dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
|
|
13
49
|
},
|
|
14
50
|
type: 'problem'
|
|
15
51
|
},
|
|
16
|
-
create (context) {
|
|
17
|
-
return { element: _check }
|
|
18
52
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
53
|
+
create(context) {
|
|
54
|
+
const model = context.getModel()
|
|
55
|
+
if (!model?.definitions)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
const externals = ExternalServices.create(model)
|
|
59
|
+
|
|
60
|
+
return function checkAllElementsForDollarPrefix() {
|
|
61
|
+
for (const defName in model.definitions) {
|
|
62
|
+
if (!externals.isInExternalService(defName))
|
|
63
|
+
checkElements(defName, model.definitions[defName])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkElements(defName, def) {
|
|
68
|
+
if (!Object.hasOwn(def,'elements') || !def.elements || typeof def.elements !== 'object')
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
for (const elementName in def.elements) {
|
|
72
|
+
const element = def.elements[elementName]
|
|
73
|
+
check(elementName, element)
|
|
74
|
+
if (element.elements)
|
|
75
|
+
checkElements(elementName, element)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function check(name, def) {
|
|
80
|
+
if (name.startsWith('$')) {
|
|
23
81
|
context.report({
|
|
24
82
|
messageId: 'dollarPrefix',
|
|
25
|
-
data: { name
|
|
26
|
-
|
|
83
|
+
data: { name },
|
|
84
|
+
loc: context.getLocation(name, def, model),
|
|
27
85
|
})
|
|
28
86
|
}
|
|
29
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
3
4
|
// Check that Java keywords are not used as identifiers unless they have
|
|
4
5
|
// a Java-specific annotation that renames/ignores them. This avoids issues
|
|
5
6
|
// later on in code-generation of CAP Java classes.
|
|
@@ -26,8 +27,9 @@ module.exports = {
|
|
|
26
27
|
meta: {
|
|
27
28
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
28
29
|
docs: {
|
|
30
|
+
category: RULE_CATEGORIES.model,
|
|
29
31
|
description: 'Reject reserved Java keywords as CDS identifiers.',
|
|
30
|
-
|
|
32
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
|
|
31
33
|
},
|
|
32
34
|
type: 'problem',
|
|
33
35
|
model: 'inferred',
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
4
|
+
|
|
3
5
|
module.exports = {
|
|
4
6
|
meta: {
|
|
5
7
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
8
|
docs: {
|
|
9
|
+
category: RULE_CATEGORIES.model,
|
|
7
10
|
description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
|
|
8
11
|
recommended: true,
|
|
9
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',
|
|
10
13
|
},
|
|
11
14
|
messages: {
|
|
12
15
|
draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
|