@sap/eslint-plugin-cds 4.0.2 → 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 +12 -1
- package/lib/conf/js/all.js +1 -0
- package/lib/conf/js/recommended.js +1 -0
- package/lib/index.js +7 -4
- package/lib/rules/js/CdsHandlerRule.js +36 -9
- package/lib/rules/js/case-sensitive-well-known-events.js +53 -0
- package/lib/rules/js/no-cross-service-import.js +1 -1
- package/lib/rules/js/no-shared-handler-variable.js +156 -18
- package/lib/rules/js/use-cql-select-template-strings.js +39 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +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.1.0] - 2025-
|
|
9
|
+
## [4.1.0] - 2025-08-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Add new rule `case-sensitive-well-known-events` to detect when a well known event is not cased correctly.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Adjust `no-shared-handler-variables` to also detect shared states when handler refers to a locally defined function, rather than an inline declaration.
|
|
16
|
+
- Rule `use-cql-select-template-strings` now also catches offending template strings in other query parts than just `SELECT`.
|
|
17
|
+
- Rule `no-shared-handler-variables` now also checks functions that are not part of a class extending `cds.ApplicationService`, if the function has an explicit type annotation `@type {import('@sap/cds').CRUDEventHandler.Before}`, `@type {import('@sap/cds').CRUDEventHandler.On}`, or `@type {import('@sap/cds').CRUDEventHandler.After}`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- `no-cross-service-import` rule no longer crashes when some file has an unexpected name.
|
|
10
21
|
|
|
11
22
|
|
|
12
23
|
## [4.0.2] - 2025-05-27
|
package/lib/conf/js/all.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -35,9 +35,12 @@ const plugin = {
|
|
|
35
35
|
...api
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Assign configs here so we can reference `plugin`
|
|
39
|
-
Object.assign(plugin.configs, getConfigs(plugin))
|
|
40
|
-
|
|
41
38
|
// Use commonJS entry point to ensure backwards compatibility (<eslint@v9):
|
|
42
39
|
// https://eslint.org/docs/latest/extend/plugin-migration-flat-config#backwards-compatibility
|
|
43
|
-
|
|
40
|
+
// spread and mix object to be able to reference plugin in getConfigs
|
|
41
|
+
module.exports = {
|
|
42
|
+
...plugin,
|
|
43
|
+
...{
|
|
44
|
+
configs: getConfigs(plugin)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// TODO:
|
|
2
2
|
// - class extends require('@sap/cds').ApplicationService
|
|
3
3
|
// - class extends await import('@sap/cds').ApplicationService
|
|
4
|
-
/** @typedef {import('
|
|
5
|
-
/** @typedef {import('
|
|
6
|
-
/** @typedef {import('
|
|
4
|
+
/** @typedef {import('./types').CdsContextTracker.Scope} Scope */
|
|
5
|
+
/** @typedef {import('./types').CdsContextTracker.Variable} Variable */
|
|
6
|
+
/** @typedef {import('./types^').CdsContextTracker.VariableType} VariableType */
|
|
7
7
|
|
|
8
8
|
'use strict'
|
|
9
9
|
|
|
@@ -42,6 +42,7 @@ class CdsHandlerRule {
|
|
|
42
42
|
get isInsideCapHandlerRegistration() { return this.capHandlerRegistrationStack.length > 0 }
|
|
43
43
|
|
|
44
44
|
constructor (context) {
|
|
45
|
+
/** @type {import('eslint').Rule.RuleContext} */
|
|
45
46
|
this.context = context
|
|
46
47
|
/** @type {Scope[]} */
|
|
47
48
|
this.functionScopes = [ produceScope('<global>') ]
|
|
@@ -89,6 +90,17 @@ class CdsHandlerRule {
|
|
|
89
90
|
return info?.variable.isCdsVariable
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {ReturnType<typeof produceHandlerRegistration>} registration - the handler registration to add
|
|
95
|
+
*/
|
|
96
|
+
addCapHandlerRegistration (registration) {
|
|
97
|
+
this.capHandlerRegistrationStack.push(registration)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeCapHandlerRegistration () {
|
|
101
|
+
this.capHandlerRegistrationStack.pop()
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
/**
|
|
93
105
|
* @param {Variable} variable
|
|
94
106
|
*/
|
|
@@ -96,6 +108,17 @@ class CdsHandlerRule {
|
|
|
96
108
|
this.functionScopes.at(-1).variables.push(variable)
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @param {Scope} scope - the scope to add
|
|
113
|
+
*/
|
|
114
|
+
enterFunctionScope (scope) {
|
|
115
|
+
this.functionScopes.push(scope)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
leaveFunctionScope () {
|
|
119
|
+
this.functionScopes.pop()
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
/**
|
|
100
123
|
* @abstract
|
|
101
124
|
*/
|
|
@@ -124,7 +147,7 @@ class CdsHandlerRule {
|
|
|
124
147
|
// like: this.on('submitOrder', bar)
|
|
125
148
|
if (['before', 'on', 'after'].some(method => method === property.name)) {
|
|
126
149
|
const handler = node.arguments.at(-1)
|
|
127
|
-
this.
|
|
150
|
+
this.addCapHandlerRegistration(produceHandlerRegistration({
|
|
128
151
|
call: node,
|
|
129
152
|
handler
|
|
130
153
|
}))
|
|
@@ -136,23 +159,27 @@ class CdsHandlerRule {
|
|
|
136
159
|
|
|
137
160
|
'CallExpression:exit'(node) {
|
|
138
161
|
if (this.capHandlerRegistrationStack.at(-1)?.call === node) {
|
|
139
|
-
this.
|
|
162
|
+
this.removeCapHandlerRegistration()
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
BlockStatement(node) {
|
|
144
167
|
if(isFunctionBody(node)) {
|
|
145
|
-
this.
|
|
168
|
+
this.enterFunctionScope(produceScope(
|
|
146
169
|
node.parent?.key?.name
|
|
147
170
|
?? node.parent?.id?.name
|
|
148
171
|
?? node.parent?.parent?.key?.name
|
|
172
|
+
// const f = function() { ... }
|
|
173
|
+
?? (node.parent?.parent?.type === 'VariableDeclarator'
|
|
174
|
+
? node.parent.parent.id.name
|
|
175
|
+
: undefined)
|
|
149
176
|
?? '<anonymous>'))
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
'BlockStatement:exit'(node) {
|
|
154
181
|
if(isFunctionBody(node)) {
|
|
155
|
-
this.
|
|
182
|
+
this.leaveFunctionScope()
|
|
156
183
|
}
|
|
157
184
|
}
|
|
158
185
|
|
|
@@ -161,10 +188,10 @@ class CdsHandlerRule {
|
|
|
161
188
|
// determine when we are inside the body ArrowFunctionExpression from looking at the body,
|
|
162
189
|
// as we'd have to add a visitor for every expression type and check if it's a child of an ArrowFunctionExpression.
|
|
163
190
|
'ArrowFunctionExpression > :not(BlockStatement)'() {
|
|
164
|
-
this.
|
|
191
|
+
this.enterFunctionScope(produceScope('<anonymous>'))
|
|
165
192
|
}
|
|
166
193
|
'ArrowFunctionExpression > :not(BlockStatement):exit'() {
|
|
167
|
-
this.
|
|
194
|
+
this.leaveFunctionScope()
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
ImportDeclaration(node) {
|
|
@@ -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
|
+
}
|
|
@@ -18,7 +18,7 @@ function compareImportAndFilename (importPath, context, node) {
|
|
|
18
18
|
// ignore excessively long strings
|
|
19
19
|
if (importPath.length > MAX_INPUT_STRING_LENGTH || currentFile.length > MAX_INPUT_STRING_LENGTH) return
|
|
20
20
|
const [, typerModuleFq, typerModule] = /^#cds-models\/.*?((\w+)Service)$/.exec(importPath) ?? []
|
|
21
|
-
const [, fileNameFq, fileName] = /((\w+)-?[sS]ervice\.m?[jt]s)$/.exec(currentFile)
|
|
21
|
+
const [, fileNameFq, fileName] = /((\w+)-?[sS]ervice\.m?[jt]s)$/.exec(currentFile) ?? []
|
|
22
22
|
// typerModule === undefined -> not a service import (probably db-level-entity import)
|
|
23
23
|
if (typerModule && fileName && typerModule !== fileName) {
|
|
24
24
|
context.report({
|
|
@@ -5,14 +5,6 @@ Use cases not yet covered:
|
|
|
5
5
|
INLINE EXTENSION
|
|
6
6
|
class FooService extends require('@sap/cds').ApplicationService { ... }
|
|
7
7
|
|
|
8
|
-
//---------
|
|
9
|
-
REFERENCED FUNCTION
|
|
10
|
-
function bad() { ... }
|
|
11
|
-
|
|
12
|
-
class ... {
|
|
13
|
-
this.on('', bad)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
//---------
|
|
17
9
|
METHOD
|
|
18
10
|
class ... {
|
|
@@ -39,18 +31,164 @@ cds.services['myService'].on('READ', 'Books', () => {})
|
|
|
39
31
|
const { RULE_CATEGORIES } = require('../../constants')
|
|
40
32
|
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
41
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
42
|
class NoSharedVariable extends CdsHandlerRule {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
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
|
+
}
|
|
54
192
|
}
|
|
55
193
|
}
|
|
56
194
|
}
|
|
@@ -3,19 +3,48 @@
|
|
|
3
3
|
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
4
|
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
5
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
|
+
|
|
6
25
|
class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
7
26
|
CallExpression(node) {
|
|
8
27
|
super.CallExpression(node)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
})
|
|
19
48
|
}
|
|
20
49
|
}
|
|
21
50
|
|
package/package.json
CHANGED