@sap/eslint-plugin-cds 3.2.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/LICENSE +15 -21
- package/lib/conf/index.js +17 -22
- package/lib/conf/js/all.js +8 -0
- package/lib/conf/js/recommended.js +8 -0
- package/lib/constants.js +7 -0
- package/lib/parser.js +2 -2
- package/lib/rules/index.js +17 -21
- package/lib/rules/js/CdsHandlerRule.js +265 -0
- package/lib/rules/js/no-cross-service-import.js +69 -0
- package/lib/rules/js/no-deep-sap-cds-import.js +56 -0
- package/lib/rules/js/no-shared-handler-variable.js +73 -0
- package/lib/rules/js/types.d.ts +15 -0
- package/lib/rules/js/use-cql-select-template-strings.js +35 -0
- package/lib/rules/latest-cds-version.js +2 -0
- package/lib/rules/no-db-keywords.js +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,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const allowList = new Set([
|
|
3
|
+
// exception: not part of the facade and should be available to users this way
|
|
4
|
+
'@sap/cds/eslint.config.mjs'
|
|
5
|
+
])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} importPath
|
|
9
|
+
* @param {RuleContext} context
|
|
10
|
+
* @param {Node} node
|
|
11
|
+
*/
|
|
12
|
+
function checkImport (importPath, context, node) {
|
|
13
|
+
if (!importPath.startsWith('@sap/cds/')) return
|
|
14
|
+
if (allowList.has(importPath)) return
|
|
15
|
+
context.report({
|
|
16
|
+
node,
|
|
17
|
+
messageId: 'noDeepSapCdsImports',
|
|
18
|
+
data: { import: importPath }
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
recommended: true,
|
|
27
|
+
category: 'JavaScript Validation',
|
|
28
|
+
description: 'Warn about deep imports from @sap/cds.'
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: {
|
|
32
|
+
noDeepSapCdsImports: `"{{import}}" is a deep import. The API of @sap/cds is available only from its facade via 'require("@sap/cds")' or 'import ... from "@sap/cds"'.`,
|
|
33
|
+
},
|
|
34
|
+
hasSuggestions: false
|
|
35
|
+
},
|
|
36
|
+
create: context => ({
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
// look for: require('@sap/cds/...')
|
|
39
|
+
if (node.callee.type !== 'Identifier') return
|
|
40
|
+
if (node.callee.name !== 'require') return
|
|
41
|
+
if (node.arguments.length !== 1) return
|
|
42
|
+
if (node.arguments[0].type !== 'Literal') return
|
|
43
|
+
checkImport(node.arguments[0].value, context, node)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
ImportDeclaration(node) {
|
|
47
|
+
// import ... from '@sap/cds/...'
|
|
48
|
+
checkImport(node.source.value, context, node)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
ImportExpression(node) {
|
|
52
|
+
// await import('@sap/cds/...')
|
|
53
|
+
checkImport(node.source.value, context, node)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use cases not yet covered:
|
|
3
|
+
|
|
4
|
+
//---------
|
|
5
|
+
INLINE EXTENSION
|
|
6
|
+
class FooService extends require('@sap/cds').ApplicationService { ... }
|
|
7
|
+
|
|
8
|
+
//---------
|
|
9
|
+
REFERENCED FUNCTION
|
|
10
|
+
function bad() { ... }
|
|
11
|
+
|
|
12
|
+
class ... {
|
|
13
|
+
this.on('', bad)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//---------
|
|
17
|
+
METHOD
|
|
18
|
+
class ... {
|
|
19
|
+
bad () {}
|
|
20
|
+
|
|
21
|
+
this.on('', this.bad)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//---------
|
|
25
|
+
IMPORTED FUNCTION
|
|
26
|
+
const { bad } = require('./bad')
|
|
27
|
+
|
|
28
|
+
class ... {
|
|
29
|
+
this.on('', bad)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//---------
|
|
33
|
+
NON-CLASS-BASED CDS SERVICE
|
|
34
|
+
cds.services['myService'].on('READ', 'Books', () => {})
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict'
|
|
38
|
+
|
|
39
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
40
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
41
|
+
|
|
42
|
+
class NoSharedVariable extends CdsHandlerRule {
|
|
43
|
+
AssignmentExpression(node) {
|
|
44
|
+
if (!this.isInsideCapHandlerRegistration) return
|
|
45
|
+
const declaringScope = this.findDefinitionScope(node.left.name)
|
|
46
|
+
if (declaringScope?.isLocal === false) {
|
|
47
|
+
this.context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: 'noSharedHandlerVariable',
|
|
50
|
+
data: {
|
|
51
|
+
definitionScope: declaringScope.scope.name
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'problem',
|
|
61
|
+
docs: {
|
|
62
|
+
recommended: true,
|
|
63
|
+
category: RULE_CATEGORIES.javascript,
|
|
64
|
+
description: 'Enforce that variables can not be used to share state between handlers.'
|
|
65
|
+
},
|
|
66
|
+
schema: [],
|
|
67
|
+
messages: {
|
|
68
|
+
noSharedHandlerVariable: 'Assignment to a non-local variable inside a CDS event handler (was declared in scope "{{definitionScope}}").'
|
|
69
|
+
},
|
|
70
|
+
hasSuggestions: true
|
|
71
|
+
},
|
|
72
|
+
create: context => new NoSharedVariable(context).asESLintVisitor()
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare namespace CdsContextTracker {
|
|
2
|
+
type VariableType = 'let' | 'const' | 'var' | 'import'
|
|
3
|
+
|
|
4
|
+
type Variable = {
|
|
5
|
+
name: string,
|
|
6
|
+
original: string
|
|
7
|
+
type: VariableType,
|
|
8
|
+
isCdsVariable: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Scope = {
|
|
12
|
+
name: string,
|
|
13
|
+
variables: Variable[]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { RULE_CATEGORIES } = require('../../constants')
|
|
4
|
+
const { CdsHandlerRule } = require('./CdsHandlerRule')
|
|
5
|
+
|
|
6
|
+
class CqlSelectUseTemplateStrings extends CdsHandlerRule {
|
|
7
|
+
CallExpression(node) {
|
|
8
|
+
super.CallExpression(node)
|
|
9
|
+
if (node.callee?.name === 'SELECT' && node.arguments[0].type === 'TemplateLiteral') {
|
|
10
|
+
this.context.report({
|
|
11
|
+
node,
|
|
12
|
+
message: 'Do not use SELECT(`...`), which is prone to SQL injections.',
|
|
13
|
+
suggest: [{
|
|
14
|
+
desc: 'Use SELECT`...` instead',
|
|
15
|
+
fix: fixer => fixer.replaceText(node, `SELECT${this.context.getSourceCode().getText(node.arguments[0])}`)
|
|
16
|
+
}]
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
recommended: true,
|
|
27
|
+
category: RULE_CATEGORIES.javascript,
|
|
28
|
+
description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
|
|
29
|
+
},
|
|
30
|
+
fixable: 'code',
|
|
31
|
+
schema: [],
|
|
32
|
+
hasSuggestions: true
|
|
33
|
+
},
|
|
34
|
+
create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
|
|
35
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const cp = require('child_process')
|
|
4
4
|
const semver = require('semver')
|
|
5
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
meta: {
|
|
8
9
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
9
10
|
docs: {
|
|
11
|
+
category: RULE_CATEGORIES.environment,
|
|
10
12
|
description: 'Checks whether the latest `@sap/cds` version is being used.',
|
|
11
13
|
},
|
|
12
14
|
type: 'suggestion',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const cds = require('@sap/cds')
|
|
4
|
+
const { RULE_CATEGORIES } = require('../constants')
|
|
4
5
|
|
|
5
6
|
// REVISIT: Replace by compiler-provided check
|
|
6
7
|
const RESERVED = cds.compile.to.sql.sqlite
|
|
@@ -11,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',
|
|
@@ -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
|
},
|