@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
|
@@ -1,40 +1,64 @@
|
|
|
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: 'Should make suggestions for possible missing SQL casts.',
|
|
8
11
|
recommended: true,
|
|
9
12
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-cast-suggestion',
|
|
10
13
|
},
|
|
11
14
|
type: 'suggestion',
|
|
12
15
|
messages: {
|
|
13
|
-
|
|
14
|
-
}
|
|
16
|
+
missingSqlCast: 'Potential issue - Missing SQL cast for column expression?'
|
|
17
|
+
},
|
|
18
|
+
model: 'parsed',
|
|
15
19
|
},
|
|
16
20
|
create: function (context) {
|
|
17
|
-
|
|
21
|
+
const model = context.getModel()
|
|
22
|
+
if (!model?.definitions)
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
return function checkAllElementsStartWithLowercase() {
|
|
26
|
+
for (const defName in model.definitions) {
|
|
27
|
+
const def = model.definitions[defName]
|
|
28
|
+
checkSqlCastsInView(defName, def)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
|
|
19
|
-
function
|
|
32
|
+
function checkSqlCastsInView(defName, def) {
|
|
20
33
|
// TODO: restructure and make more robust (#507)
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (!def?.query?.SET?.args?.length)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
for (const arg of def.query.SET.args) {
|
|
38
|
+
if (arg?.SELECT?.columns?.length) {
|
|
39
|
+
// Only in UNION cases?
|
|
40
|
+
for (const col of arg.SELECT.columns) {
|
|
41
|
+
if (col)
|
|
42
|
+
checkColumn(col)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkColumn(col) {
|
|
48
|
+
const { xpr, cast } = col
|
|
49
|
+
if (cast && xpr) {
|
|
50
|
+
if (!(xpr[0]?.xpr && xpr[0]?.cast)) {
|
|
51
|
+
// we don't pass a name for the column's location, as it would be used to calculate
|
|
52
|
+
// endColumn, which is not correct for this expression
|
|
53
|
+
const loc = col.$location ?
|
|
54
|
+
context.getLocation('', col, model) :
|
|
55
|
+
context.getLocation(defName, def, model)
|
|
56
|
+
|
|
57
|
+
context.report({
|
|
58
|
+
messageId: 'missingSqlCast',
|
|
59
|
+
loc,
|
|
60
|
+
file: def.$location.file,
|
|
61
|
+
})
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
64
|
}
|
|
@@ -11,8 +11,7 @@ module.exports = {
|
|
|
11
11
|
description: 'Ensure SQL comparisons with \'null\' are valid',
|
|
12
12
|
category: 'Model Validation',
|
|
13
13
|
recommended: false,
|
|
14
|
-
|
|
15
|
-
// url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
|
|
14
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
|
|
16
15
|
},
|
|
17
16
|
type: 'problem',
|
|
18
17
|
model: 'parsed',
|
|
@@ -21,40 +20,45 @@ module.exports = {
|
|
|
21
20
|
}
|
|
22
21
|
},
|
|
23
22
|
create(context) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function checkExpression(xpr, ctx) {
|
|
29
|
-
if (!xpr || !Array.isArray(xpr))
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < xpr.length; i++) {
|
|
33
|
-
if (typeof xpr[i] !== 'object')
|
|
34
|
-
continue // scalar value, etc.
|
|
35
|
-
|
|
36
|
-
if (xpr[i]?.val === null) {
|
|
37
|
-
const prev = i > 0 && typeof xpr[i-1] === 'string' ? xpr[i-1] : null
|
|
38
|
-
if (prev && invalidComparisonOperators.includes(prev)) {
|
|
39
|
-
reportComparison(xpr, ctx)
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
const next = i+1 < xpr.length && typeof xpr[i+1] === 'string' ? xpr[i+1] : null
|
|
43
|
-
if (next && invalidComparisonOperators.includes(next))
|
|
44
|
-
reportComparison(xpr, ctx)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
23
|
+
const model = context.getModel()
|
|
24
|
+
if (!model?.definitions)
|
|
25
|
+
return
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
27
|
+
return function checkSqlNullComparisonsInModel() {
|
|
28
|
+
for (const defName in model.definitions) {
|
|
29
|
+
const def = model.definitions[defName]
|
|
30
|
+
if (def.query || def.projection)
|
|
31
|
+
forEachXprInDefinition(def, checkExpression)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkExpression(xpr, ctx) {
|
|
36
|
+
if (!xpr || !Array.isArray(xpr))
|
|
37
|
+
return
|
|
56
38
|
|
|
39
|
+
for (let i = 0; i < xpr.length; i++) {
|
|
40
|
+
if (typeof xpr[i] !== 'object')
|
|
41
|
+
continue // scalar value, etc.
|
|
42
|
+
|
|
43
|
+
if (xpr[i]?.val === null) {
|
|
44
|
+
const prev = i > 0 && typeof xpr[i-1] === 'string' ? xpr[i-1] : null
|
|
45
|
+
if (prev && invalidComparisonOperators.includes(prev)) {
|
|
46
|
+
reportComparison(xpr, ctx)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
const next = i+1 < xpr.length && typeof xpr[i+1] === 'string' ? xpr[i+1] : null
|
|
50
|
+
if (next && invalidComparisonOperators.includes(next))
|
|
51
|
+
reportComparison(xpr, ctx)
|
|
52
|
+
}
|
|
57
53
|
}
|
|
58
54
|
}
|
|
55
|
+
|
|
56
|
+
function reportComparison(xpr, ctx) {
|
|
57
|
+
context.report({
|
|
58
|
+
messageId: 'nullComparison',
|
|
59
|
+
loc: context.getLocation('', ctx),
|
|
60
|
+
file: ctx.$location?.file,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
4
|
+
|
|
3
5
|
const allowedUpperCaseElements = ['ID']
|
|
4
6
|
|
|
5
7
|
module.exports = {
|
|
6
8
|
meta: {
|
|
7
9
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
8
10
|
docs: {
|
|
11
|
+
category: RULE_CATEGORIES.model,
|
|
9
12
|
description: 'Regular element names should start with lowercase letters.',
|
|
10
13
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-elements-lowercase',
|
|
11
14
|
},
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
3
4
|
const { splitDefName } = require('../utils/rules')
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
meta: {
|
|
7
8
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
8
9
|
docs: {
|
|
10
|
+
category: RULE_CATEGORIES.model,
|
|
9
11
|
description: 'Regular entity names should start with uppercase letters.',
|
|
10
12
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-entities-uppercase',
|
|
11
13
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const cds = require('@sap/cds')
|
|
4
4
|
const { basename, extname } = require('node:path')
|
|
5
5
|
const findFuzzy = require('../utils/findFuzzy')
|
|
6
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
6
7
|
const SEP = '[,;\t]'
|
|
7
8
|
const EOL = '\\r?\\n'
|
|
8
9
|
|
|
@@ -11,7 +12,7 @@ module.exports = {
|
|
|
11
12
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
12
13
|
docs: {
|
|
13
14
|
description: 'CSV files for entities must refer to valid element names.',
|
|
14
|
-
category:
|
|
15
|
+
category: RULE_CATEGORIES.csv,
|
|
15
16
|
recommended: true,
|
|
16
17
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/valid-csv-header',
|
|
17
18
|
},
|
package/lib/utils/createRule.js
CHANGED
|
@@ -127,7 +127,6 @@ function setMetaDefaults (meta) {
|
|
|
127
127
|
meta ??= {}
|
|
128
128
|
meta.severity ??= constants.DEFAULT_RULE_SEVERITY
|
|
129
129
|
meta.docs ??= {}
|
|
130
|
-
meta.docs.category ??= constants.DEFAULT_RULE_CATEGORY
|
|
131
130
|
meta.model ??= 'parsed'
|
|
132
131
|
return meta
|
|
133
132
|
}
|
|
@@ -184,8 +183,10 @@ function createReport (node, cdsContext, meta, create) {
|
|
|
184
183
|
|
|
185
184
|
function sanitizeFileLocation (d) {
|
|
186
185
|
let parent = d
|
|
187
|
-
while (!parent.$location && parent.parent && !parent.parent.definitions)
|
|
188
|
-
|
|
186
|
+
while (!parent.$location && parent.parent && !parent.parent.definitions)
|
|
187
|
+
parent = d.parent
|
|
188
|
+
if (parent.$location)
|
|
189
|
+
d.$location = parent.$location
|
|
189
190
|
return d
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -213,8 +214,11 @@ function extendContext (node, context, meta) {
|
|
|
213
214
|
|
|
214
215
|
const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
|
|
215
216
|
const { parserServices } = context.sourceCode || context
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
|
|
218
|
+
cdscontext.getModel = meta.model === 'inferred'
|
|
219
|
+
? parserServices.getInferredCsn
|
|
220
|
+
: parserServices.getParsedCsn
|
|
221
|
+
|
|
218
222
|
cdscontext.getEnvironment = () => {
|
|
219
223
|
const options = context.options
|
|
220
224
|
return options && options[0] && options[0].environment ? options[0].environment : undefined
|
|
@@ -11,23 +11,12 @@
|
|
|
11
11
|
const fs = require('node:fs')
|
|
12
12
|
const path = require('node:path')
|
|
13
13
|
|
|
14
|
-
module.exports = (currentDir = '.'
|
|
14
|
+
module.exports = (currentDir = '.') => {
|
|
15
15
|
let configFiles = [
|
|
16
16
|
'eslint.config.js',
|
|
17
17
|
'eslint.config.cjs',
|
|
18
18
|
'eslint.config.mjs'
|
|
19
19
|
]
|
|
20
|
-
if (legacy) {
|
|
21
|
-
configFiles = [
|
|
22
|
-
'.eslintrc.js',
|
|
23
|
-
'.eslintrc.cjs',
|
|
24
|
-
'.eslintrc.yaml',
|
|
25
|
-
'.eslintrc.yml',
|
|
26
|
-
'.eslintrc.json',
|
|
27
|
-
'.eslintrc',
|
|
28
|
-
'package.json',
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
20
|
let configDir = path.resolve(currentDir)
|
|
32
21
|
while (configDir !== path.resolve(configDir, '..')) {
|
|
33
22
|
for (const configFile of configFiles) {
|
package/lib/utils/rules.js
CHANGED
|
@@ -8,23 +8,24 @@ const findFuzzy = require('./findFuzzy')
|
|
|
8
8
|
module.exports = {
|
|
9
9
|
findFuzzy,
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {object} definition CSN definition object.
|
|
12
|
+
* @param {string} [name] The definition's name. Inferred for "linked CSN".
|
|
13
13
|
*/
|
|
14
|
-
splitDefName(
|
|
14
|
+
splitDefName(definition, name = definition.name) {
|
|
15
|
+
if (!name)
|
|
16
|
+
return null
|
|
15
17
|
// Entity names from CSN are of the form:
|
|
16
18
|
// <namespace>.<service>.<def>.<'texts'|'localized'>|<composition value>
|
|
17
19
|
let prefix = ''
|
|
18
20
|
let suffix = ''
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
defName = names[names.length - 1]
|
|
21
|
+
const names = name.split('.')
|
|
22
|
+
let defName = names[names.length - 1]
|
|
22
23
|
|
|
23
24
|
if (defName) {
|
|
24
25
|
// Managed composition get compiler tag `_up`
|
|
25
26
|
let isManagedComposition = false
|
|
26
|
-
if (
|
|
27
|
-
isManagedComposition = Object.keys(
|
|
27
|
+
if (definition.elements) {
|
|
28
|
+
isManagedComposition = Object.keys(definition.elements).some(k => k === 'up_')
|
|
28
29
|
}
|
|
29
30
|
// Check for compiler tags
|
|
30
31
|
const compilerTagsToExclude = ['texts', 'localized']
|
|
@@ -34,7 +35,7 @@ module.exports = {
|
|
|
34
35
|
suffix = names[names.length - 1]
|
|
35
36
|
defName = names[names.length - 2]
|
|
36
37
|
}
|
|
37
|
-
prefix =
|
|
38
|
+
prefix = name.split(`.${defName}`)[0]
|
|
38
39
|
}
|
|
39
40
|
return { prefix, name: defName, suffix }
|
|
40
41
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('node:fs')
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { RuleTester } = require('eslint')
|
|
9
9
|
const { globalCache } = require('./Cache')
|
|
10
10
|
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
11
11
|
const { compileModelFromDict } = require('../parser')
|
|
@@ -56,6 +56,7 @@ module.exports = function runRuleTester(options) {
|
|
|
56
56
|
rule = testRuleWrapper(rules[path.basename(options.root)]())
|
|
57
57
|
} else {
|
|
58
58
|
// Otherwise from project root
|
|
59
|
+
|
|
59
60
|
// eslint-disable-next-line
|
|
60
61
|
const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
|
|
61
62
|
paths: [options.root]
|
|
@@ -66,9 +67,7 @@ module.exports = function runRuleTester(options) {
|
|
|
66
67
|
|
|
67
68
|
let tester
|
|
68
69
|
if (parserPath) {
|
|
69
|
-
const options =
|
|
70
|
-
? { languageOptions: { parser: require(parserPath) } }
|
|
71
|
-
: { parser: parserPath }
|
|
70
|
+
const options = { languageOptions: { parser: require(parserPath) } }
|
|
72
71
|
tester = new RuleTester(options)
|
|
73
72
|
} else {
|
|
74
73
|
tester = new RuleTester()
|
|
@@ -82,10 +81,7 @@ module.exports = function runRuleTester(options) {
|
|
|
82
81
|
filename: filePath,
|
|
83
82
|
}
|
|
84
83
|
]
|
|
85
|
-
|
|
86
|
-
// property not supported for ESLint 8
|
|
87
|
-
testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
|
|
88
|
-
}
|
|
84
|
+
testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
|
|
89
85
|
if (!isConfiguredFileType(options.filename, 'FILES')) {
|
|
90
86
|
const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
91
87
|
testerCases[type][0].code = ''
|
|
@@ -150,7 +146,3 @@ function _getDictFiles(input, filenames) {
|
|
|
150
146
|
}
|
|
151
147
|
return dictFiles
|
|
152
148
|
}
|
|
153
|
-
|
|
154
|
-
function _isEslint9OrLater() {
|
|
155
|
-
return Number(Linter.version.split('.')[0]) >= 9
|
|
156
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
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,13 +20,13 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@sap/cds": ">=7",
|
|
24
23
|
"semver": "^7.7.1"
|
|
25
24
|
},
|
|
26
25
|
"peerDependencies": {
|
|
27
|
-
"
|
|
26
|
+
"@sap/cds": ">=9",
|
|
27
|
+
"eslint": "^9"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
30
|
+
"node": ">=20"
|
|
31
31
|
}
|
|
32
32
|
}
|