@sap/eslint-plugin-cds 3.2.0 → 4.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 +28 -1
- package/LICENSE +15 -21
- package/lib/conf/index.js +17 -22
- package/lib/conf/js/all.js +9 -0
- package/lib/conf/js/recommended.js +9 -0
- package/lib/constants.js +7 -0
- package/lib/index.js +7 -4
- package/lib/parser.js +2 -2
- package/lib/rules/index.js +17 -21
- package/lib/rules/js/CdsHandlerRule.js +292 -0
- package/lib/rules/js/case-sensitive-well-known-events.js +53 -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 +211 -0
- package/lib/rules/js/types.d.ts +15 -0
- package/lib/rules/js/use-cql-select-template-strings.js +64 -0
- package/lib/rules/latest-cds-version.js +2 -0
- package/lib/rules/no-db-keywords.js +2 -0
- package/lib/rules/no-dollar-prefixed-names.js +66 -8
- package/lib/rules/no-java-keywords.js +2 -0
- package/lib/rules/no-join-on-draft.js +3 -0
- package/lib/rules/sql-cast-suggestion.js +45 -21
- package/lib/rules/sql-null-comparison.js +37 -33
- package/lib/rules/start-elements-lowercase.js +3 -0
- package/lib/rules/start-entities-uppercase.js +2 -0
- package/lib/rules/valid-csv-header.js +2 -1
- package/lib/utils/createRule.js +9 -5
- package/lib/utils/getConfigPath.js +1 -12
- package/lib/utils/rules.js +10 -9
- package/lib/utils/runRuleTester.js +4 -12
- package/package.json +4 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
5
|
+
|
|
6
|
+
const WELL_KNOWN_EVENTS = [
|
|
7
|
+
'CREATE', 'READ', 'UPDATE', 'UPSERT','DELETE',
|
|
8
|
+
'INSERT','SELECT',
|
|
9
|
+
'POST','GET','PUT','PATCH',
|
|
10
|
+
'NEW', 'CANCEL', 'EDIT', 'SAVE' // draft related
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
class CaseSensitiveWellKnownEvents extends CdsHandlerRule {
|
|
14
|
+
CAPHandlerRegistration(node) {
|
|
15
|
+
const first = node.parent?.arguments?.[0]
|
|
16
|
+
if (!first) return
|
|
17
|
+
const currentEventName = first.value ?? ''
|
|
18
|
+
const currentEventNameUpper = currentEventName.toUpperCase()
|
|
19
|
+
if (WELL_KNOWN_EVENTS.includes(currentEventNameUpper) && currentEventName !== currentEventNameUpper) {
|
|
20
|
+
const quoteType = ['"', '\'', '`'].includes(first.raw?.[0]) ? first.raw?.[0] : '"'
|
|
21
|
+
this.context.report({
|
|
22
|
+
node: first,
|
|
23
|
+
messageId: 'incorrectEventNameCase',
|
|
24
|
+
data: {
|
|
25
|
+
currentEventName: currentEventName,
|
|
26
|
+
properEventName: currentEventNameUpper
|
|
27
|
+
},
|
|
28
|
+
suggest: [{
|
|
29
|
+
desc: 'Change event casing to upper case',
|
|
30
|
+
fix: fixer => fixer.replaceText(first, `${quoteType}${currentEventNameUpper}${quoteType}`)
|
|
31
|
+
}]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
docs: {
|
|
41
|
+
recommended: true,
|
|
42
|
+
category: RULE_CATEGORIES.javascript,
|
|
43
|
+
description: 'Make sure well-known events are used with proper casing.'
|
|
44
|
+
},
|
|
45
|
+
fixable: 'code',
|
|
46
|
+
schema: [],
|
|
47
|
+
messages: {
|
|
48
|
+
incorrectEventNameCase: 'Found an event registration for event "{{currentEventName}}", which is likely supposed to be "{{properEventName}}".'
|
|
49
|
+
},
|
|
50
|
+
hasSuggestions: true
|
|
51
|
+
},
|
|
52
|
+
create: context => new CaseSensitiveWellKnownEvents(context).asESLintVisitor()
|
|
53
|
+
}
|
|
@@ -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,211 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use cases not yet covered:
|
|
3
|
+
|
|
4
|
+
//---------
|
|
5
|
+
INLINE EXTENSION
|
|
6
|
+
class FooService extends require('@sap/cds').ApplicationService { ... }
|
|
7
|
+
|
|
8
|
+
//---------
|
|
9
|
+
METHOD
|
|
10
|
+
class ... {
|
|
11
|
+
bad () {}
|
|
12
|
+
|
|
13
|
+
this.on('', this.bad)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//---------
|
|
17
|
+
IMPORTED FUNCTION
|
|
18
|
+
const { bad } = require('./bad')
|
|
19
|
+
|
|
20
|
+
class ... {
|
|
21
|
+
this.on('', bad)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//---------
|
|
25
|
+
NON-CLASS-BASED CDS SERVICE
|
|
26
|
+
cds.services['myService'].on('READ', 'Books', () => {})
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict'
|
|
30
|
+
|
|
31
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
32
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string | undefined} t
|
|
36
|
+
*/
|
|
37
|
+
function isHandlerType(t) {
|
|
38
|
+
// match "import('@sap/cds').CRUDEventHandler.Before" etc.
|
|
39
|
+
return ['Before', 'On', 'After'].some(handlerType => t?.match(new RegExp(`import\\s?\\(.@sap\\/cds.\\)\\.CRUDEventHandler.${handlerType}`)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class NoSharedVariable extends CdsHandlerRule {
|
|
43
|
+
/**
|
|
44
|
+
* Functions that modify variables that are not locally declared.
|
|
45
|
+
* They are stored by name.
|
|
46
|
+
* Note: this is not fully fail proof, as the functions are stored in a flat fashion,
|
|
47
|
+
* rather than maintaining the scope they are declared in.
|
|
48
|
+
* This could lead to false positives if a function with the same name is declared in multiple scopes.
|
|
49
|
+
* @type {Record<string, Scope>}
|
|
50
|
+
*/
|
|
51
|
+
#suspiciousFunctions = {}
|
|
52
|
+
/**
|
|
53
|
+
* nodes of handler registrations like:
|
|
54
|
+
* ```js
|
|
55
|
+
* this.on('READ', 'Books', handler)
|
|
56
|
+
* // ^^^^^^^
|
|
57
|
+
* ```
|
|
58
|
+
* as they reference the handler by name, its definition may come later in the code
|
|
59
|
+
* due to hoisting.
|
|
60
|
+
* We check them later in `Program:exit()`.
|
|
61
|
+
*/
|
|
62
|
+
#pendingInspections = []
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Typedef JSDoc to look up local aliases.
|
|
66
|
+
* ```js
|
|
67
|
+
* @typedef {Bar} Foo
|
|
68
|
+
* ```
|
|
69
|
+
* becomes
|
|
70
|
+
* ```js
|
|
71
|
+
* {Foo: 'Bar'}
|
|
72
|
+
* ```
|
|
73
|
+
* @type {Record<string, import('estree').Comment & { type: string }>}
|
|
74
|
+
*/
|
|
75
|
+
#typeDefinitions = {}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Type JSDoc that matches the estree definition of a Comment,
|
|
79
|
+
* plus an additional `type` property that contains the type of the variable.
|
|
80
|
+
* Local type aliases are resolved to the actual type using #typeDefinitions.
|
|
81
|
+
*
|
|
82
|
+
* @type {Array<import('estree').Comment & { type: string }>}
|
|
83
|
+
*/
|
|
84
|
+
#typeDeclarations = []
|
|
85
|
+
|
|
86
|
+
#handlerDefinitionDepth = 0
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* When we are inside a function that has explicitly been annotated as
|
|
90
|
+
* a handler function via a @type JSDoc annotation.
|
|
91
|
+
*/
|
|
92
|
+
get isInsideExplicitCapHandlerDefinition() {
|
|
93
|
+
return this.#handlerDefinitionDepth > 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
addCapHandlerRegistration(registration) {
|
|
97
|
+
super.addCapHandlerRegistration(registration)
|
|
98
|
+
|
|
99
|
+
if (registration.handler.type === 'Identifier') {
|
|
100
|
+
this.#pendingInspections.push(registration)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Program() {
|
|
105
|
+
const comments = this.context.sourceCode.getAllComments()
|
|
106
|
+
this.#typeDefinitions = Object.fromEntries(comments
|
|
107
|
+
.map(comment => {
|
|
108
|
+
const [, type, name] = comment.value.match(/^\*\s?@typedef\s?\{(.*)\}\s?(\w*)/) ?? []
|
|
109
|
+
return type && name ? [ name, {...comment, type} ] : null
|
|
110
|
+
})
|
|
111
|
+
.filter(Boolean))
|
|
112
|
+
this.#typeDeclarations = comments
|
|
113
|
+
.map(comment => {
|
|
114
|
+
const match = comment.value.match(/^\*\s?@type\s?\{(.*)\}/)?.[1]
|
|
115
|
+
if (!match) return null
|
|
116
|
+
const type = this.#typeDefinitions[match]?.type ?? match
|
|
117
|
+
return type ? { ...comment, type } : null
|
|
118
|
+
})
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
'Program:exit'() {
|
|
123
|
+
for (const node of this.#pendingInspections) {
|
|
124
|
+
const { scope } = this.#suspiciousFunctions[node.handler.name] ?? {}
|
|
125
|
+
if (scope) {
|
|
126
|
+
this.context.report({
|
|
127
|
+
node: node.handler,
|
|
128
|
+
messageId: 'noSharedHandlerVariable',
|
|
129
|
+
data: {
|
|
130
|
+
definitionScope: scope.name
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#enterFunctionDefinition(node) {
|
|
138
|
+
// find a JDoc type comment, that is either on the line before, or in the same line,
|
|
139
|
+
// but up to three columns to the left. The latter condition covers three cases:
|
|
140
|
+
// 1. TYPEDEFFUNC -- (no space between the comment and start of function) -> distance = 0
|
|
141
|
+
// 2. TYPEDEF FUNC-- (one space between the comment and start of function) -> distance = 1
|
|
142
|
+
// 3. TYPEDEF(FUNC) -- (no space after typedef, followed by an opening parenthesis) -> distance = 1
|
|
143
|
+
// 3. TYPEDEF (FUNC) -- (one space after typedef and an opening parenthesis) -> distance = 2
|
|
144
|
+
// Note: this will fail if we have empty lines between the function declaration and the JSDoc.
|
|
145
|
+
// Also when users have more spaces or other outlandish formatting styles.
|
|
146
|
+
const type = this.#typeDeclarations.find(({loc}) =>
|
|
147
|
+
loc.end.line === node.loc.start.line - 1
|
|
148
|
+
|| loc.end.line === node.loc.start.line && [0,1,2].includes(node.loc.start.column - loc.end.column)
|
|
149
|
+
)
|
|
150
|
+
// if the function is explicitly declared as handler, we check it.
|
|
151
|
+
// If the function is not explicitly declared as handler, but a surrounding function is (this.handlerDefinitionDepth > 0),
|
|
152
|
+
// we check it too.
|
|
153
|
+
if (isHandlerType(type?.type) || this.isInsideExplicitCapHandlerDefinition) this.#handlerDefinitionDepth++
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#exitFunctionDefinition() {
|
|
157
|
+
this.#handlerDefinitionDepth = Math.max(0, this.#handlerDefinitionDepth - 1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// () => ...
|
|
161
|
+
ArrowFunctionExpression(node) { this.#enterFunctionDefinition(node) }
|
|
162
|
+
'ArrowFunctionExpression:exit'() { this.#exitFunctionDefinition() }
|
|
163
|
+
// function() { ... }
|
|
164
|
+
FunctionExpression(node) { this.#enterFunctionDefinition(node) }
|
|
165
|
+
'FunctionExpression:exit'() { this.#exitFunctionDefinition() }
|
|
166
|
+
// function f () { ... }
|
|
167
|
+
FunctionDeclaration(node) { this.#enterFunctionDefinition(node) }
|
|
168
|
+
'FunctionDeclaration:exit'() { this.#exitFunctionDefinition()}
|
|
169
|
+
|
|
170
|
+
AssignmentExpression(node) {
|
|
171
|
+
if (this.isInsideCapHandlerRegistration || this.isInsideExplicitCapHandlerDefinition) {
|
|
172
|
+
// like: this.on('READ', 'Books', () => { variable = 42 })
|
|
173
|
+
const declaringScope = this.findDefinitionScope(node.left.name)
|
|
174
|
+
if (declaringScope?.isLocal === false) {
|
|
175
|
+
this.context.report({
|
|
176
|
+
node,
|
|
177
|
+
messageId: 'noSharedHandlerVariable',
|
|
178
|
+
data: {
|
|
179
|
+
definitionScope: declaringScope.scope.name
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
} else if (this.functionScopes.length > 0) {
|
|
184
|
+
// not inside a handler registration, but in a function that may be referenced in a handler registration
|
|
185
|
+
// like: this.on('READ', 'Books', handler)
|
|
186
|
+
// as functions are hoisted and can be referenced before their definition, we just collect the names of suspicious functions
|
|
187
|
+
// and check them in Program:exit when we have inspected all functions.
|
|
188
|
+
const declaringScope = this.findDefinitionScope(node.left.name)
|
|
189
|
+
if (declaringScope?.isLocal === false) {
|
|
190
|
+
this.#suspiciousFunctions[this.functionScopes.at(-1).name] = declaringScope
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
meta: {
|
|
198
|
+
type: 'problem',
|
|
199
|
+
docs: {
|
|
200
|
+
recommended: true,
|
|
201
|
+
category: RULE_CATEGORIES.javascript,
|
|
202
|
+
description: 'Enforce that variables can not be used to share state between handlers.'
|
|
203
|
+
},
|
|
204
|
+
schema: [],
|
|
205
|
+
messages: {
|
|
206
|
+
noSharedHandlerVariable: 'Assignment to a non-local variable inside a CDS event handler (was declared in scope "{{definitionScope}}").'
|
|
207
|
+
},
|
|
208
|
+
hasSuggestions: true
|
|
209
|
+
},
|
|
210
|
+
create: context => new NoSharedVariable(context).asESLintVisitor()
|
|
211
|
+
}
|
|
@@ -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,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
5
|
+
|
|
6
|
+
const isCqlClauseStart = node => ['SELECT', 'UPDATE', 'INSERT', 'DELETE', 'UPSERT'].includes(node.name)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ESLint goes through member functions in reverse order:
|
|
10
|
+
* x.y.z -> z, y, x
|
|
11
|
+
* so we can not track when we
|
|
12
|
+
* enter a CQL clause, but instead have to check the chain of ancestors for each member
|
|
13
|
+
* recursively. If we find the topmost function call is a CQL clause (SELECT, UPDATE, etc.),
|
|
14
|
+
* none of the member functions in the chain are allowed to use untagged template strings.
|
|
15
|
+
*/
|
|
16
|
+
const isInCqlClause = node => {
|
|
17
|
+
if (!node) return false
|
|
18
|
+
if (node.type === 'CallExpression' && isCqlClauseStart(node.callee)) return true
|
|
19
|
+
if (node.type === 'TaggedTemplateExpression' && isCqlClauseStart(node.tag)) return true
|
|
20
|
+
// f(...) and f`...` have slightly different structure, the former has .callee, the latter has .tag
|
|
21
|
+
// -> use the first that is available to ascend through the call chain
|
|
22
|
+
return isInCqlClause(node.callee?.object ?? node.tag?.object)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
super.CallExpression(node)
|
|
28
|
+
const [arg] = node.arguments ?? []
|
|
29
|
+
if (arg?.type !== 'TemplateLiteral') return
|
|
30
|
+
if (arg.expressions.length === 0) return // no expressions in the template string, so no SQL injection risk
|
|
31
|
+
if (!isInCqlClause(node)) return
|
|
32
|
+
|
|
33
|
+
const [functionName, prefix] = node.callee.type === 'MemberExpression'
|
|
34
|
+
// for ….where`...` we need to use the full preceding expression in the following replacement
|
|
35
|
+
? [node.callee.property?.name, this.context.getSourceCode().getText(node.callee)]
|
|
36
|
+
// for SELECT`...` we can use the function name directly
|
|
37
|
+
: [node.callee.name, node.callee.name]
|
|
38
|
+
this.context.report({
|
|
39
|
+
node,
|
|
40
|
+
message: 'Do not use {{functionName}}(`...`) inside CQL statements, which is prone to SQL injections.',
|
|
41
|
+
data: { functionName },
|
|
42
|
+
suggest: [{
|
|
43
|
+
desc: 'Use {{functionName}}`...` instead of {{functionName}}(`...`)',
|
|
44
|
+
data: { functionName },
|
|
45
|
+
fix: fixer => fixer.replaceText(node, `${prefix}${this.context.getSourceCode().getText(arg)}`)
|
|
46
|
+
}]
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
recommended: true,
|
|
56
|
+
category: RULE_CATEGORIES.javascript,
|
|
57
|
+
description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
|
|
58
|
+
},
|
|
59
|
+
fixable: 'code',
|
|
60
|
+
schema: [],
|
|
61
|
+
hasSuggestions: true
|
|
62
|
+
},
|
|
63
|
+
create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
|
|
64
|
+
}
|
|
@@ -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,6 +12,7 @@ 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
17
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
|
|
16
18
|
},
|
|
@@ -1,9 +1,45 @@
|
|
|
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
45
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
|
|
@@ -13,17 +49,39 @@ module.exports = {
|
|
|
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,6 +27,7 @@ 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
|
},
|
|
@@ -1,9 +1,12 @@
|
|
|
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
12
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',
|