@sap/eslint-plugin-cds 4.0.2 → 4.1.1
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 +16 -1
- package/lib/conf/index.js +17 -0
- package/lib/conf/java/all.js +5 -0
- package/lib/conf/java/experimental.js +5 -0
- package/lib/conf/java/recommended.js +3 -0
- package/lib/conf/js/all.js +2 -1
- package/lib/conf/js/recommended.js +2 -1
- package/lib/constants.js +1 -0
- package/lib/index.js +14 -9
- package/lib/languages/java/java-language.js +69 -0
- package/lib/languages/java/java-source-code.js +179 -0
- package/lib/rules/index.js +21 -3
- package/lib/rules/java/cql-class-targets.js +59 -0
- package/lib/rules/js/CdsHandlerRule.js +36 -9
- package/lib/rules/js/case-sensitive-well-known-events.js +53 -0
- package/lib/rules/js/cql-template-strings.js +64 -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/utils/runRuleTester.js +1 -1
- package/package.json +5 -2
- package/lib/rules/js/use-cql-select-template-strings.js +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,22 @@ 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.
|
|
9
|
+
## [4.1.1] - 2025-12-04
|
|
10
|
+
### Added
|
|
11
|
+
- Top level type definitions for package export
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [4.1.0] - 2025-08-18
|
|
15
|
+
### Added
|
|
16
|
+
- Add new rule `case-sensitive-well-known-events` to detect when a well known event is not cased correctly.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Adjust `no-shared-handler-variables` to also detect shared states when handler refers to a locally defined function, rather than an inline declaration.
|
|
20
|
+
- Rule `use-cql-select-template-strings` now also catches offending template strings in other query parts than just `SELECT`.
|
|
21
|
+
- 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}`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `no-cross-service-import` rule no longer crashes when some file has an unexpected name.
|
|
10
25
|
|
|
11
26
|
|
|
12
27
|
## [4.0.2] - 2025-05-27
|
package/lib/conf/index.js
CHANGED
|
@@ -4,6 +4,18 @@ const path = require('node:path')
|
|
|
4
4
|
const { FILES, GLOBALS } = require('../constants')
|
|
5
5
|
const { parserPath } = require('../api')
|
|
6
6
|
|
|
7
|
+
function _createJavaConfig (plugin, configName) {
|
|
8
|
+
return {
|
|
9
|
+
name: '@sap/cds/java',
|
|
10
|
+
plugins: {
|
|
11
|
+
'@sap/cds': plugin,
|
|
12
|
+
},
|
|
13
|
+
files: ['**/*.java'],
|
|
14
|
+
language: '@sap/cds/java',
|
|
15
|
+
rules: require(path.join(__dirname, 'java', configName)),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
function _createJsConfig (plugin, configName) {
|
|
8
20
|
return {
|
|
9
21
|
name: '@sap/cds/js',
|
|
@@ -43,6 +55,11 @@ module.exports = function (plugin) {
|
|
|
43
55
|
js: {
|
|
44
56
|
all: _createJsConfig(plugin, 'all'),
|
|
45
57
|
recommended: _createJsConfig(plugin, 'recommended')
|
|
58
|
+
},
|
|
59
|
+
java: {
|
|
60
|
+
all: _createJavaConfig(plugin, 'all'),
|
|
61
|
+
recommended: _createJavaConfig(plugin, 'recommended'),
|
|
62
|
+
experimental: _createJavaConfig(plugin, 'experimental')
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
}
|
package/lib/conf/js/all.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
'@sap/cds/no-shared-handler-variable': 'error',
|
|
5
|
-
'@sap/cds/
|
|
5
|
+
'@sap/cds/cql-template-strings': 'error',
|
|
6
6
|
'@sap/cds/no-cross-service-import': 'warn',
|
|
7
7
|
'@sap/cds/no-deep-sap-cds-import': 'warn',
|
|
8
|
+
'@sap/cds/case-sensitive-well-known-events': 'warn',
|
|
8
9
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
'@sap/cds/no-shared-handler-variable': 'error',
|
|
5
|
-
'@sap/cds/
|
|
5
|
+
'@sap/cds/cql-template-strings': 'error',
|
|
6
6
|
'@sap/cds/no-cross-service-import': 'warn',
|
|
7
7
|
'@sap/cds/no-deep-sap-cds-import': 'warn',
|
|
8
|
+
'@sap/cds/case-sensitive-well-known-events': 'warn',
|
|
8
9
|
}
|
package/lib/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ const RULE_CATEGORIES = {
|
|
|
21
21
|
javascript: 'JavaScript Validation',
|
|
22
22
|
environment: 'Environment Validation',
|
|
23
23
|
csv: 'CSV Validation',
|
|
24
|
+
java: 'Java Validation'
|
|
24
25
|
}
|
|
25
26
|
const DEFAULT_RULE_CATEGORY = 'Model Validation'
|
|
26
27
|
const DEFAULT_RULE_FLAVOR = RULE_FLAVORS[0]
|
package/lib/index.js
CHANGED
|
@@ -18,26 +18,31 @@
|
|
|
18
18
|
|
|
19
19
|
const api = require('./api')
|
|
20
20
|
const getConfigs = require('./conf')
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
...Object.entries(require('./rules')).map(([k, v]) => ({ [k]: v() }))
|
|
24
|
-
)
|
|
25
|
-
|
|
21
|
+
const { allRules, initialiseRules } = require('./rules')
|
|
22
|
+
const { javaLanguage } = require('./languages/java/java-language')
|
|
26
23
|
const packageJson = require('../package.json')
|
|
27
24
|
|
|
25
|
+
const rules = initialiseRules(allRules)
|
|
26
|
+
|
|
28
27
|
const plugin = {
|
|
29
28
|
meta: {
|
|
30
29
|
name: packageJson.name,
|
|
31
30
|
version: packageJson.version
|
|
32
31
|
},
|
|
32
|
+
languages: {
|
|
33
|
+
java: javaLanguage
|
|
34
|
+
},
|
|
33
35
|
configs: {},
|
|
34
36
|
rules,
|
|
35
37
|
...api
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Assign configs here so we can reference `plugin`
|
|
39
|
-
Object.assign(plugin.configs, getConfigs(plugin))
|
|
40
|
-
|
|
41
40
|
// Use commonJS entry point to ensure backwards compatibility (<eslint@v9):
|
|
42
41
|
// https://eslint.org/docs/latest/extend/plugin-migration-flat-config#backwards-compatibility
|
|
43
|
-
|
|
42
|
+
// spread and mix object to be able to reference plugin in getConfigs
|
|
43
|
+
module.exports = {
|
|
44
|
+
...plugin,
|
|
45
|
+
...{
|
|
46
|
+
configs: getConfigs(plugin)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TreeSitterParser = require('tree-sitter')
|
|
4
|
+
const Java = require('tree-sitter-java')
|
|
5
|
+
const { JavaSourceCode } = require('./java-source-code')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} JavaLanguageOptions
|
|
9
|
+
* @typedef {{languageOptions: JavaLanguageOptions}} JavaLanguageContext
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const jp = new TreeSitterParser()
|
|
13
|
+
jp.setLanguage(Java)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('@eslint/core').File} file
|
|
17
|
+
* @param {JavaLanguageContext} context
|
|
18
|
+
*/
|
|
19
|
+
function parseJavaCode (file /*, context*/) {
|
|
20
|
+
const code = file.body
|
|
21
|
+
const treeSitterAst = jp.parse(code)
|
|
22
|
+
|
|
23
|
+
if (!treeSitterAst) {
|
|
24
|
+
return { ast: null, ok: false }
|
|
25
|
+
}
|
|
26
|
+
if (!treeSitterAst.rootNode) {
|
|
27
|
+
return { ast: treeSitterAst, ok: false }
|
|
28
|
+
}
|
|
29
|
+
return { ast: treeSitterAst, ok: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('@eslint/core').File} file
|
|
34
|
+
* @param {ReturnType<typeof parseJavaCode>} parseResult
|
|
35
|
+
* @param {JavaLanguageContext} context
|
|
36
|
+
*/
|
|
37
|
+
function createJavaSourceCode(file, parseResult /*, context*/) {
|
|
38
|
+
if (!parseResult.ok || !parseResult.ast) {
|
|
39
|
+
throw new Error('createJavaSourceCode: parseResult is not ok or has no AST', {
|
|
40
|
+
ok: parseResult.ok,
|
|
41
|
+
hasAst: !!parseResult.ast
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return new JavaSourceCode({ text: file.body, ast: parseResult.ast })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {JavaLanguageOptions} languageOptions
|
|
49
|
+
*/
|
|
50
|
+
function validateJavaLanguageOptions(/*languageOptions*/) {
|
|
51
|
+
// No specific options to validate for Java currently
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @link https://eslint.org/docs/latest/extend/languages#the-language-object
|
|
56
|
+
*/
|
|
57
|
+
const javaLanguage = /** @type {const}*/({
|
|
58
|
+
fileType: 'text',
|
|
59
|
+
lineStart: 0,
|
|
60
|
+
columnStart: 0,
|
|
61
|
+
nodeTypeKey: 'type',
|
|
62
|
+
validateLanguageOptions: validateJavaLanguageOptions,
|
|
63
|
+
parse: parseJavaCode,
|
|
64
|
+
createSourceCode: createJavaSourceCode
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
javaLanguage,
|
|
69
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// not 100% accurate, as it matches _any_ comment containing 'eslint-disable' etc.
|
|
4
|
+
// but in Java sources, users will hopefully only mention eslint-* when they
|
|
5
|
+
// actually mean to configure ESLint.
|
|
6
|
+
const INLINE_CONFIG = /eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @link see https://github.com/eslint/json/blob/main/src/languages/json-source-code.js
|
|
10
|
+
*/
|
|
11
|
+
const {
|
|
12
|
+
VisitNodeStep,
|
|
13
|
+
TextSourceCodeBase,
|
|
14
|
+
Directive
|
|
15
|
+
} = require('@eslint/plugin-kit')
|
|
16
|
+
|
|
17
|
+
/** @typedef {'disable' | 'enable' | 'disable-next-line' | 'disable-line'} DirectiveType */
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('tree-sitter').SyntaxNode} comment
|
|
22
|
+
* @returns {{
|
|
23
|
+
* label: 'eslint-disable' | 'eslint-enable' | 'eslint-disable-next-line' | 'eslint-disable-line',
|
|
24
|
+
* value: string,
|
|
25
|
+
* justification: string | undefined
|
|
26
|
+
* }}
|
|
27
|
+
*/
|
|
28
|
+
function parseDirective(comment) {
|
|
29
|
+
// label, value, justification
|
|
30
|
+
// * but before the "--" that indicates the justification.
|
|
31
|
+
const [, label, value, justification] = comment.text.match(/^\W*(eslint[\w-]*) (\S+)(?: -- (.+))?/)
|
|
32
|
+
return { label, value, justification}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class JavaTraversalStep extends VisitNodeStep {
|
|
36
|
+
/** @param {{ target: unknown, phase: 1|2, args: Array<any> }} arg0 */
|
|
37
|
+
constructor({ target, phase, args }) {
|
|
38
|
+
super({ target, phase, args })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {import('tree-sitter').SyntaxNode} node
|
|
44
|
+
* @param {(node: import('tree-sitter').SyntaxNode) => void} enterAction
|
|
45
|
+
* @param {undefined | (node: import('tree-sitter').SyntaxNode) => void} exitAction
|
|
46
|
+
*/
|
|
47
|
+
function traverse(node, enterAction, exitAction) {
|
|
48
|
+
enterAction?.(node)
|
|
49
|
+
//steps.push(new JavaTraversalStep({ target: node, phase: 1, args: [] }))
|
|
50
|
+
for (const child of node.children) {
|
|
51
|
+
traverse(child, enterAction, exitAction)
|
|
52
|
+
}
|
|
53
|
+
exitAction?.(node)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
58
|
+
const getAncestors = node => !node
|
|
59
|
+
? []
|
|
60
|
+
: [...getAncestors(node.parent), node]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @link https://eslint.org/docs/latest/extend/languages#the-sourcecode-object
|
|
64
|
+
*/
|
|
65
|
+
class JavaSourceCode extends TextSourceCodeBase {
|
|
66
|
+
/** @type {undefined | import('tree-sitter').SyntaxNode[]} */
|
|
67
|
+
#comments
|
|
68
|
+
/** @type {undefined | import('tree-sitter').SyntaxNode[]} */
|
|
69
|
+
#inlineConfigComments
|
|
70
|
+
|
|
71
|
+
get comments() {
|
|
72
|
+
if (!this.#comments) {
|
|
73
|
+
this.#comments = []
|
|
74
|
+
traverse(this.ast.rootNode, node => {
|
|
75
|
+
if (['line_comment', 'block_comment'].includes(node.type)) {
|
|
76
|
+
this.#comments.push(node)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
return this.#comments
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get inlineConfigNodes() {
|
|
84
|
+
if (!this.#inlineConfigComments) {
|
|
85
|
+
this.#inlineConfigComments = this
|
|
86
|
+
.comments
|
|
87
|
+
.filter(comment =>
|
|
88
|
+
INLINE_CONFIG.test(comment.text),
|
|
89
|
+
) ?? []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.#inlineConfigComments
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get lines () { return this.text.split(/\r\n|\r|\n/) }
|
|
96
|
+
|
|
97
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
98
|
+
getLoc(node) {
|
|
99
|
+
return {
|
|
100
|
+
start: { line: node.startPosition.row, column: node.startPosition.column },
|
|
101
|
+
end: { line: node.endPosition.row, column: node.endPosition.column }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
106
|
+
getRange(node) {
|
|
107
|
+
return [node.startIndex, node.endIndex]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
111
|
+
getParent(node) {
|
|
112
|
+
return node.parent ?? undefined // .parent is null if root, we need undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
116
|
+
getAncestors(node) {
|
|
117
|
+
// ESLint wants ancestors excluding the node itself
|
|
118
|
+
return getAncestors(node.parent)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @param {import('tree-sitter').SyntaxNode} node */
|
|
122
|
+
getText(node) {
|
|
123
|
+
return node.text
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getDisableDirectives() {
|
|
127
|
+
/** @type {FileProblem[]} */
|
|
128
|
+
const problems = []
|
|
129
|
+
/** @type {Directive[]} */
|
|
130
|
+
const directives = []
|
|
131
|
+
|
|
132
|
+
for (const comment of this.inlineConfigNodes) {
|
|
133
|
+
const { label, value, justification } = parseDirective(comment)
|
|
134
|
+
|
|
135
|
+
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
|
|
136
|
+
if (label === 'eslint-disable-line' && comment.loc.start.line !== comment.loc.end.line) {
|
|
137
|
+
problems.push({
|
|
138
|
+
ruleId: null,
|
|
139
|
+
message: `${label} comment should not span multiple lines.`,
|
|
140
|
+
loc: comment.loc,
|
|
141
|
+
})
|
|
142
|
+
} else if (['eslint-disable', 'eslint-enable', 'eslint-disable-next-line', 'eslint-disable-line'].includes(label)) {
|
|
143
|
+
directives.push(
|
|
144
|
+
new Directive({
|
|
145
|
+
type: /** @type {DirectiveType} */ (label.slice('eslint-'.length)),
|
|
146
|
+
node: comment,
|
|
147
|
+
value,
|
|
148
|
+
justification,
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { problems, directives }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
traverse() {
|
|
157
|
+
const steps = []
|
|
158
|
+
if (!this.ast || !this.ast.rootNode) {
|
|
159
|
+
return steps
|
|
160
|
+
}
|
|
161
|
+
traverse(this.ast.rootNode, node => {
|
|
162
|
+
steps.push(new JavaTraversalStep({ target: node, phase: 1 }))
|
|
163
|
+
}, node => {
|
|
164
|
+
steps.push(new JavaTraversalStep({ target: node, phase: 2 }))
|
|
165
|
+
})
|
|
166
|
+
return steps
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @param {{text: string, ast: import('tree-sitter').Tree}} param0 */
|
|
170
|
+
constructor({text, ast}) {
|
|
171
|
+
super({text, ast})
|
|
172
|
+
this.text = text
|
|
173
|
+
this.ast = ast
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
JavaSourceCode
|
|
179
|
+
}
|
package/lib/rules/index.js
CHANGED
|
@@ -17,10 +17,28 @@ const readRulesFromDir = (dir, post) => Object.fromEntries(fs.readdirSync(path.j
|
|
|
17
17
|
.filter(([,module]) => Object.hasOwn(module, 'create')) // create() is required to exist on top level by eslint -> good check to find actual rules
|
|
18
18
|
.map(([file, module]) => [file, () => post(module)]))
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Calls the initialisation function for each rule in the given rule set.
|
|
22
|
+
* @param {Record<string, () => unknown>} ruleSet
|
|
23
|
+
* @returns {Record<string, unknown>}
|
|
24
|
+
*/
|
|
25
|
+
const initialiseRules = ruleSet => Object.fromEntries(
|
|
26
|
+
Object.entries(ruleSet)
|
|
27
|
+
.map(([k, v]) => [[k], v()]))
|
|
28
|
+
|
|
20
29
|
const cdsRules = readRulesFromDir('.', createRule)
|
|
21
30
|
const jsRules = readRulesFromDir('js', module => module)
|
|
22
|
-
|
|
31
|
+
// backwards compat, remove in next minor
|
|
32
|
+
jsRules['use-cql-select-template-strings'] = jsRules['cql-template-strings']
|
|
33
|
+
const javaRules = readRulesFromDir('java', module => module)
|
|
34
|
+
const allRules = {...cdsRules, ...jsRules, ...javaRules}
|
|
23
35
|
|
|
24
|
-
globalCache.set('rules',
|
|
36
|
+
globalCache.set('rules', allRules)
|
|
25
37
|
|
|
26
|
-
module.exports =
|
|
38
|
+
module.exports = {
|
|
39
|
+
allRules,
|
|
40
|
+
jsRules,
|
|
41
|
+
javaRules,
|
|
42
|
+
cdsRules,
|
|
43
|
+
initialiseRules
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In Select.from(x), x should be a class literal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
recommended: true,
|
|
14
|
+
category: RULE_CATEGORIES.java,
|
|
15
|
+
description: 'java.'
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
selectOnNonClass: `'{{target}}' is not a class. Prefer to pass class literals to Select.from!`,
|
|
21
|
+
},
|
|
22
|
+
hasSuggestions: true
|
|
23
|
+
},
|
|
24
|
+
create: context => {
|
|
25
|
+
const knownClasses = new Set()
|
|
26
|
+
|
|
27
|
+
/** @param {Node} node */
|
|
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
|
+
|
|
36
|
+
return {
|
|
37
|
+
'import_declaration > scoped_identifier': function(node) {
|
|
38
|
+
if (!node) return
|
|
39
|
+
if (node.nameNode) {
|
|
40
|
+
knownClasses.add(node.nameNode.text)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"method_invocation > identifier[text='from']": function(node) {
|
|
45
|
+
if (!node?.parent?.argumentsNode) return
|
|
46
|
+
const args = node.parent.argumentsNode.children
|
|
47
|
+
.filter(c => c.constructor.name !== 'SyntaxNode') // filter out commas, parens, etc.
|
|
48
|
+
?? []
|
|
49
|
+
if (!refersToClass(args[0])) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: args[0],
|
|
52
|
+
messageId: 'selectOnNonClass',
|
|
53
|
+
data: { target: args[0].text }
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -9,7 +9,7 @@ const { RuleTester } = require('eslint')
|
|
|
9
9
|
const { globalCache } = require('./Cache')
|
|
10
10
|
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
11
11
|
const { compileModelFromDict } = require('../parser')
|
|
12
|
-
const rules = require('../rules')
|
|
12
|
+
const { allRules: rules } = require('../rules')
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* A wrapper around the return value of `createRule()` that initializes the global
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
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": [
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"
|
|
23
|
+
"@eslint/plugin-kit": "^0.4.1",
|
|
24
|
+
"semver": "^7.7.1",
|
|
25
|
+
"tree-sitter": "^0.21.1",
|
|
26
|
+
"tree-sitter-java": "^0.23.5"
|
|
24
27
|
},
|
|
25
28
|
"peerDependencies": {
|
|
26
29
|
"@sap/cds": ">=9",
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|