@sap/eslint-plugin-cds 3.0.4 → 3.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 +39 -0
- package/README.md +1 -1
- package/lib/api/index.js +4 -4
- package/lib/conf/all.js +17 -17
- package/lib/conf/experimental.js +12 -0
- package/lib/conf/index.js +12 -3
- package/lib/conf/recommended.js +14 -14
- package/lib/constants.js +2 -0
- package/lib/index.js +2 -1
- package/lib/parser.js +10 -1
- package/lib/rules/assoc2many-ambiguous-key.js +40 -11
- package/lib/rules/auth-no-empty-restrictions.js +36 -10
- package/lib/rules/auth-restrict-grant-service.js +19 -20
- package/lib/rules/auth-use-requires.js +25 -15
- package/lib/rules/auth-valid-restrict-grant.js +137 -81
- package/lib/rules/auth-valid-restrict-keys.js +34 -18
- package/lib/rules/auth-valid-restrict-to.js +67 -60
- package/lib/rules/auth-valid-restrict-where.js +31 -44
- package/lib/rules/extension-restrictions.js +11 -3
- package/lib/rules/index.js +5 -1
- package/lib/rules/latest-cds-version.js +5 -4
- package/lib/rules/no-db-keywords.js +14 -5
- package/lib/rules/no-dollar-prefixed-names.js +9 -2
- package/lib/rules/no-java-keywords.js +181 -0
- package/lib/rules/no-join-on-draft.js +9 -3
- package/lib/rules/sql-cast-suggestion.js +19 -15
- package/lib/rules/sql-null-comparison.js +60 -0
- package/lib/rules/start-elements-lowercase.js +6 -2
- package/lib/rules/start-entities-uppercase.js +12 -5
- package/lib/rules/valid-csv-header.js +33 -13
- package/lib/types.d.ts +4 -4
- package/lib/utils/Cache.js +4 -2
- package/lib/utils/Colors.js +2 -0
- package/lib/utils/LintError.js +17 -0
- package/lib/utils/createRule.js +160 -134
- package/lib/utils/csnTraversal.js +163 -0
- package/lib/utils/findFuzzy.js +15 -7
- package/lib/utils/getConfigPath.js +4 -2
- package/lib/utils/getConfiguredFileTypes.js +2 -0
- package/lib/utils/getFileExtensions.js +2 -0
- package/lib/utils/getProjectRootPath.js +53 -15
- package/lib/utils/isConfiguredFileType.js +8 -3
- package/lib/utils/rules.js +13 -7
- package/lib/utils/runRuleTester.js +69 -36
- package/package.json +1 -1
- package/lib/utils/genDocs.js +0 -346
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cp = require('child_process')
|
|
2
4
|
const semver = require('semver')
|
|
3
5
|
|
|
@@ -5,12 +7,11 @@ module.exports = {
|
|
|
5
7
|
meta: {
|
|
6
8
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
7
9
|
docs: {
|
|
8
|
-
description: 'Checks whether the latest `@sap/cds` version is being used.'
|
|
10
|
+
description: 'Checks whether the latest `@sap/cds` version is being used.',
|
|
9
11
|
},
|
|
10
12
|
type: 'suggestion',
|
|
11
|
-
hasSuggestions: true,
|
|
12
13
|
messages: {
|
|
13
|
-
|
|
14
|
+
latestCdsVersion: 'A newer CDS version is available!'
|
|
14
15
|
},
|
|
15
16
|
severity: 'off',
|
|
16
17
|
model: 'none'
|
|
@@ -36,7 +37,7 @@ module.exports = {
|
|
|
36
37
|
// If current cds version is not the latest
|
|
37
38
|
if (Object.keys(cdsVersions).length !== 0 && !semver.satisfies(cdsVersions.latest, cdsVersions.current)) {
|
|
38
39
|
context.report({
|
|
39
|
-
messageId: '
|
|
40
|
+
messageId: 'latestCdsVersion',
|
|
40
41
|
node: context.getNode()
|
|
41
42
|
})
|
|
42
43
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cds = require('@sap/cds')
|
|
2
4
|
|
|
5
|
+
// REVISIT: Replace by compiler-provided check
|
|
6
|
+
const RESERVED = cds.compile.to.sql.sqlite
|
|
7
|
+
? cds.compile.to.sql.sqlite.keywords
|
|
8
|
+
: [ 'ORDER', 'GROUP', 'LIMIT' ]
|
|
9
|
+
|
|
3
10
|
module.exports = {
|
|
4
11
|
meta: {
|
|
5
12
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
13
|
docs: {
|
|
7
14
|
description: 'Avoid using reserved SQL keywords.',
|
|
8
|
-
recommended: true
|
|
15
|
+
recommended: true,
|
|
16
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
|
|
9
20
|
},
|
|
10
21
|
type: 'problem',
|
|
11
22
|
model: 'inferred'
|
|
@@ -29,7 +40,8 @@ module.exports = {
|
|
|
29
40
|
if (srv && srv['@cds.external']) return
|
|
30
41
|
if (d.kind === 'entity' && d['@cds.persistence.skip'] === true) return
|
|
31
42
|
context.report({
|
|
32
|
-
|
|
43
|
+
messageId: 'reservedKeyword',
|
|
44
|
+
data: { name: d.name },
|
|
33
45
|
node: context.getNode(d),
|
|
34
46
|
file: d.$location.file
|
|
35
47
|
})
|
|
@@ -37,6 +49,3 @@ module.exports = {
|
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
// REVISIT: Replace by compiler-provided check
|
|
42
|
-
const RESERVED = cds.compile.to.sql.sqlite ? cds.compile.to.sql.sqlite.keywords : ['ORDER', 'GROUP', 'LIMIT']
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
4
6
|
docs: {
|
|
5
7
|
description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
|
|
6
|
-
recommended: true
|
|
8
|
+
recommended: true,
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-dollar-prefixed-names',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
|
|
7
13
|
},
|
|
8
14
|
type: 'problem'
|
|
9
15
|
},
|
|
@@ -15,7 +21,8 @@ module.exports = {
|
|
|
15
21
|
if (srv && srv['@cds.external']) return
|
|
16
22
|
if (d.name.startsWith('$')) {
|
|
17
23
|
context.report({
|
|
18
|
-
|
|
24
|
+
messageId: 'dollarPrefix',
|
|
25
|
+
data: { name: d.name },
|
|
19
26
|
node: context.getNode(d)
|
|
20
27
|
})
|
|
21
28
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Check that Java keywords are not used as identifiers unless they have
|
|
4
|
+
// a Java-specific annotation that renames/ignores them. This avoids issues
|
|
5
|
+
// later on in code-generation of CAP Java classes.
|
|
6
|
+
// Test Java code via godbolt.org: https://godbolt.org/z/1c5s49qjo
|
|
7
|
+
|
|
8
|
+
const { splitDefName } = require('../utils/rules')
|
|
9
|
+
|
|
10
|
+
// There is also `@cds.java.this.name`, which is not relevant for this check.
|
|
11
|
+
const ANNO_JAVA_NAME = '@cds.java.name'
|
|
12
|
+
const ANNO_JAVA_IGNORE = '@cds.java.ignore'
|
|
13
|
+
|
|
14
|
+
// CSN kinds that are relevant for code generation with possible keyword
|
|
15
|
+
// conflicts. For example, types are not relevant, because they use
|
|
16
|
+
// PascalCase, i.e. it can never be a keyword conflict, since all keywords
|
|
17
|
+
// are lowercase.
|
|
18
|
+
const relevantKinds = [
|
|
19
|
+
'element',
|
|
20
|
+
'param',
|
|
21
|
+
'action',
|
|
22
|
+
'function',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
meta: {
|
|
27
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
28
|
+
docs: {
|
|
29
|
+
description: 'Reject reserved Java keywords as CDS identifiers.',
|
|
30
|
+
recommended: true
|
|
31
|
+
},
|
|
32
|
+
type: 'problem',
|
|
33
|
+
model: 'inferred',
|
|
34
|
+
messages: {
|
|
35
|
+
keywordJava: `'{{name}}' is a reserved keyword in Java. Use '@cds.java.name' to override the name for Java code generation.`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
create (context) {
|
|
39
|
+
const rootPath = context.getRootPath()
|
|
40
|
+
if (!rootPath)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
return function checkForJavaKeywords(){
|
|
44
|
+
const model = context.getModel()
|
|
45
|
+
if (!model)
|
|
46
|
+
return
|
|
47
|
+
for (const name in model.definitions)
|
|
48
|
+
checkDefinition(model.definitions[name])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkDefinition(def) {
|
|
52
|
+
checkNameIsNotReserved(def)
|
|
53
|
+
if (def.elements) {
|
|
54
|
+
for (const name in def.elements)
|
|
55
|
+
checkDefinition(def.elements[name])
|
|
56
|
+
}
|
|
57
|
+
if (def.actions) {
|
|
58
|
+
for (const name in def.actions)
|
|
59
|
+
checkDefinition(def.actions[name])
|
|
60
|
+
}
|
|
61
|
+
if (def.kind === 'action' || def.kind === 'function') {
|
|
62
|
+
for (const name in def.params)
|
|
63
|
+
checkDefinition(def.params[name])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkNameIsNotReserved(artifact) {
|
|
68
|
+
if (!artifact.$location?.file || !relevantKinds.includes(artifact.kind))
|
|
69
|
+
return
|
|
70
|
+
if (artifact[ANNO_JAVA_IGNORE])
|
|
71
|
+
return // ignored; no Java code generated
|
|
72
|
+
if (artifact[ANNO_JAVA_NAME])
|
|
73
|
+
return // explicitly renamed; assume the user uses a valid name
|
|
74
|
+
|
|
75
|
+
const name = artifact.is('element')
|
|
76
|
+
? artifact.name
|
|
77
|
+
: splitDefName(artifact).name
|
|
78
|
+
|
|
79
|
+
if (isValueReservedJavaKeyword(name)) {
|
|
80
|
+
context.report({
|
|
81
|
+
messageId: 'keywordJava',
|
|
82
|
+
data: { name },
|
|
83
|
+
node: context.getNode(artifact),
|
|
84
|
+
file: artifact.$location.file,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// List from https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html
|
|
92
|
+
// Also available at https://github.com/openjdk/jdk/blob/f92c60e1a9968620cbc92b52aa546b57c09da487/src/java.compiler/share/classes/javax/lang/model/SourceVersion.java#L651
|
|
93
|
+
// though that list includes fewer items.
|
|
94
|
+
const JAVA_RESERVED = [
|
|
95
|
+
'_',
|
|
96
|
+
'abstract',
|
|
97
|
+
'assert',
|
|
98
|
+
'boolean',
|
|
99
|
+
'break',
|
|
100
|
+
'byte',
|
|
101
|
+
'case',
|
|
102
|
+
'catch',
|
|
103
|
+
'char',
|
|
104
|
+
'class',
|
|
105
|
+
'const',
|
|
106
|
+
'continue',
|
|
107
|
+
'default',
|
|
108
|
+
'do',
|
|
109
|
+
'double',
|
|
110
|
+
'else',
|
|
111
|
+
'enum',
|
|
112
|
+
'extends',
|
|
113
|
+
'final',
|
|
114
|
+
'finally',
|
|
115
|
+
'float',
|
|
116
|
+
'for',
|
|
117
|
+
'goto',
|
|
118
|
+
'if',
|
|
119
|
+
'implements',
|
|
120
|
+
'import',
|
|
121
|
+
'instanceof',
|
|
122
|
+
'int',
|
|
123
|
+
'interface',
|
|
124
|
+
'long',
|
|
125
|
+
'native',
|
|
126
|
+
'new',
|
|
127
|
+
'package',
|
|
128
|
+
'private',
|
|
129
|
+
'protected',
|
|
130
|
+
'public',
|
|
131
|
+
'return',
|
|
132
|
+
'short',
|
|
133
|
+
'static',
|
|
134
|
+
'strictfp',
|
|
135
|
+
'super',
|
|
136
|
+
'switch',
|
|
137
|
+
'synchronized',
|
|
138
|
+
'this',
|
|
139
|
+
'throw',
|
|
140
|
+
'throws',
|
|
141
|
+
'transient',
|
|
142
|
+
'try',
|
|
143
|
+
'void',
|
|
144
|
+
'volatile',
|
|
145
|
+
'while',
|
|
146
|
+
// literals
|
|
147
|
+
'true',
|
|
148
|
+
'false',
|
|
149
|
+
'null',
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if the given value is a reserved keyword.
|
|
154
|
+
*
|
|
155
|
+
* @param {any} name
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function isValueReservedJavaKeyword(name) {
|
|
159
|
+
if (!name || typeof name !== 'string')
|
|
160
|
+
return false
|
|
161
|
+
const normalized = identifierForJava(name)
|
|
162
|
+
return JAVA_RESERVED.includes(normalized)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns the check-relevant identifier for Java.
|
|
167
|
+
* CAP Java does not use lowercase for the full identifier, but instead
|
|
168
|
+
* uses lowerCamelCase, i.e. it is enough to change the first character
|
|
169
|
+
* of the identifier.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} name
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function identifierForJava(name) {
|
|
175
|
+
if (!name)
|
|
176
|
+
return name
|
|
177
|
+
const firstChar = name.charAt(0)
|
|
178
|
+
if (firstChar === firstChar.toLowerCase())
|
|
179
|
+
return name
|
|
180
|
+
return `${firstChar.toLowerCase()}${name.slice(1)}`
|
|
181
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
4
6
|
docs: {
|
|
5
|
-
description:
|
|
6
|
-
recommended: true
|
|
7
|
+
description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
|
|
8
|
+
recommended: true,
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-join-on-draft',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
|
|
7
13
|
},
|
|
8
14
|
type: 'suggestion',
|
|
9
15
|
model: 'inferred'
|
|
@@ -15,7 +21,7 @@ module.exports = {
|
|
|
15
21
|
if (e['@odata.draft.enabled']) {
|
|
16
22
|
if (e?.query?.SELECT?.from?.join) {
|
|
17
23
|
context.report({
|
|
18
|
-
|
|
24
|
+
messageId: 'draftJoin',
|
|
19
25
|
node: context.getNode(e),
|
|
20
26
|
file: e.$location.file
|
|
21
27
|
})
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
1
2
|
|
|
2
3
|
module.exports = {
|
|
3
4
|
meta: {
|
|
4
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
5
6
|
docs: {
|
|
6
7
|
description: 'Should make suggestions for possible missing SQL casts.',
|
|
7
|
-
recommended: true
|
|
8
|
+
recommended: true,
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-cast-suggestion',
|
|
8
10
|
},
|
|
9
11
|
type: 'suggestion',
|
|
10
|
-
hasSuggestions: true,
|
|
11
12
|
messages: {
|
|
12
13
|
missingSQLCast: 'Potential issue - Missing SQL cast for column expression?'
|
|
13
14
|
}
|
|
@@ -16,19 +17,22 @@ module.exports = {
|
|
|
16
17
|
return { view: checkSqlCast }
|
|
17
18
|
|
|
18
19
|
function checkSqlCast (v) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
// TODO: restructure and make more robust (#507)
|
|
21
|
+
if (v?.query?.SET?.args) {
|
|
22
|
+
for (const arg of v.query.SET.args) {
|
|
23
|
+
if (arg?.SELECT) {
|
|
24
|
+
// Only in UNION cases?
|
|
25
|
+
for (const each of arg.SELECT.columns || []) {
|
|
26
|
+
if (each) {
|
|
27
|
+
const { xpr, cast } = each
|
|
28
|
+
if (cast && xpr) {
|
|
29
|
+
if (!(xpr[0]?.xpr && xpr[0]?.cast)) {
|
|
30
|
+
context.report({
|
|
31
|
+
messageId: 'missingSQLCast',
|
|
32
|
+
node: context.getNode(v)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { forEachXprInDefinition } = require('../utils/csnTraversal')
|
|
4
|
+
|
|
5
|
+
const invalidComparisonOperators = [ '=', '!=', '<>' ]
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Ensure SQL comparisons with \'null\' are valid',
|
|
12
|
+
category: 'Model Validation',
|
|
13
|
+
recommended: false,
|
|
14
|
+
// TODO: Add documentation
|
|
15
|
+
// url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-null-comparison',
|
|
16
|
+
},
|
|
17
|
+
type: 'problem',
|
|
18
|
+
model: 'parsed',
|
|
19
|
+
messages: {
|
|
20
|
+
nullComparison: `Comparisons against 'null' are always false. Did you mean 'is not null'?`,
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
view(v) {
|
|
26
|
+
forEachXprInDefinition(v, checkExpression)
|
|
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
|
+
}
|
|
48
|
+
|
|
49
|
+
function reportComparison(xpr, ctx) {
|
|
50
|
+
context.report({
|
|
51
|
+
messageId: 'nullComparison',
|
|
52
|
+
loc: context.getLocation(null, ctx),
|
|
53
|
+
file: ctx.$location?.file,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
4
6
|
docs: {
|
|
5
|
-
description: 'Regular element names should start with lowercase letters.'
|
|
7
|
+
description: 'Regular element names should start with lowercase letters.',
|
|
8
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-elements-lowercase',
|
|
6
9
|
},
|
|
7
10
|
type: 'suggestion',
|
|
8
11
|
hasSuggestions: true,
|
|
@@ -10,7 +13,8 @@ module.exports = {
|
|
|
10
13
|
startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
|
|
11
14
|
fixLowercase: 'Start element name with a lowercase letter.'
|
|
12
15
|
},
|
|
13
|
-
fixable: 'code'
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
model: 'parsed',
|
|
14
18
|
},
|
|
15
19
|
create: function (context) {
|
|
16
20
|
const sourcecode = context.getSourceCode()
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { splitDefName } = require('../utils/rules')
|
|
2
4
|
|
|
3
5
|
module.exports = {
|
|
4
6
|
meta: {
|
|
5
7
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
8
|
docs: {
|
|
7
|
-
description: 'Regular entity names should start with uppercase letters.'
|
|
9
|
+
description: 'Regular entity names should start with uppercase letters.',
|
|
10
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-entities-uppercase',
|
|
8
11
|
},
|
|
9
12
|
type: 'suggestion',
|
|
10
13
|
hasSuggestions: true,
|
|
@@ -12,20 +15,24 @@ module.exports = {
|
|
|
12
15
|
startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
|
|
13
16
|
fixUppercase: 'Start entity name with an uppercase letter.'
|
|
14
17
|
},
|
|
15
|
-
fixable: 'code'
|
|
18
|
+
fixable: 'code',
|
|
19
|
+
model: 'parsed',
|
|
16
20
|
},
|
|
17
|
-
create
|
|
21
|
+
create(context) {
|
|
18
22
|
const sourcecode = context.getSourceCode()
|
|
19
23
|
|
|
20
24
|
return { entity: checkStartsUppercase }
|
|
21
25
|
|
|
22
26
|
function checkStartsUppercase (e) {
|
|
27
|
+
if (e.kind !== 'entity')
|
|
28
|
+
return // workaround for #424
|
|
29
|
+
|
|
23
30
|
const entityName = splitDefName(e).name
|
|
24
31
|
if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
|
|
25
|
-
if (e.$location
|
|
32
|
+
if (e.$location?.file) {
|
|
26
33
|
const file = e.$location.file
|
|
27
34
|
const loc = context.getLocation(entityName, e)
|
|
28
|
-
const fix =
|
|
35
|
+
const fix = fixer => {
|
|
29
36
|
const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1)
|
|
30
37
|
const rangeEnd = sourcecode.getIndexFromLoc({
|
|
31
38
|
line: loc.end.line,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cds = require('@sap/cds')
|
|
2
|
-
const { basename, extname } = require('path')
|
|
4
|
+
const { basename, extname } = require('node:path')
|
|
3
5
|
const findFuzzy = require('../utils/findFuzzy')
|
|
4
6
|
const SEP = '[,;\t]'
|
|
5
7
|
const EOL = '\\r?\\n'
|
|
@@ -10,7 +12,8 @@ module.exports = {
|
|
|
10
12
|
docs: {
|
|
11
13
|
description: 'CSV files for entities must refer to valid element names.',
|
|
12
14
|
category: 'Model Validation',
|
|
13
|
-
recommended: true
|
|
15
|
+
recommended: true,
|
|
16
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/valid-csv-header',
|
|
14
17
|
},
|
|
15
18
|
severity: 'warn',
|
|
16
19
|
type: 'problem',
|
|
@@ -33,7 +36,15 @@ module.exports = {
|
|
|
33
36
|
if (!filePath.endsWith('.csv')) return
|
|
34
37
|
if (!model) return
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
try {
|
|
40
|
+
model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] })
|
|
41
|
+
} catch(e) {
|
|
42
|
+
// ignore invalid models; the compiler emits errors already
|
|
43
|
+
if (e.code !== 'ERR_CDS_COMPILATION_FAILURE')
|
|
44
|
+
throw e
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!model) return
|
|
37
48
|
|
|
38
49
|
const filename = basename(filePath)
|
|
39
50
|
const entityName = filename.replace(/-/g, '.').slice(0, -extname(filename).length)
|
|
@@ -41,20 +52,20 @@ module.exports = {
|
|
|
41
52
|
if (!entity) return
|
|
42
53
|
|
|
43
54
|
const elements = Object.values(entity.elements)
|
|
44
|
-
.filter(
|
|
45
|
-
.map(
|
|
55
|
+
.filter(e => !!e['@cds.persistence.name'])
|
|
56
|
+
.map(e => e['@cds.persistence.name'].toUpperCase())
|
|
46
57
|
|
|
47
58
|
const [cols] = cds.parse.csv(code)
|
|
48
|
-
const missing = cols.filter(
|
|
59
|
+
const missing = cols.filter(col => !elements.includes(col.toUpperCase()))
|
|
49
60
|
for (const miss of missing) {
|
|
50
61
|
const index = _findInCode(miss, code)
|
|
51
62
|
const loc = sourcecode.getLocFromIndex(index)
|
|
52
63
|
const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
|
|
53
|
-
const suggest = candidates.map(
|
|
64
|
+
const suggest = candidates.map(cand => {
|
|
54
65
|
return {
|
|
55
66
|
messageId: 'ReplaceColumnWith',
|
|
56
67
|
data: { column: miss, candidates: cand },
|
|
57
|
-
fix:
|
|
68
|
+
fix: fixer => fixer.replaceTextRange([index, index + miss.length], cand)
|
|
58
69
|
}
|
|
59
70
|
})
|
|
60
71
|
context.report({
|
|
@@ -69,20 +80,29 @@ module.exports = {
|
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} needle
|
|
85
|
+
* @param {string} code
|
|
86
|
+
* @returns {number} -1 if not found
|
|
87
|
+
*/
|
|
88
|
+
function _findInCode(needle, code) {
|
|
73
89
|
// middle
|
|
74
|
-
let match = new RegExp(SEP +
|
|
90
|
+
let match = new RegExp(SEP + needle + SEP).exec(code)
|
|
75
91
|
if (match) return match.index + 1
|
|
76
92
|
// end of line
|
|
77
|
-
match = new RegExp(SEP +
|
|
93
|
+
match = new RegExp(SEP + needle + EOL).exec(code)
|
|
78
94
|
if (match) return match.index + 1
|
|
79
95
|
// start of doc
|
|
80
|
-
match = new RegExp('^' +
|
|
96
|
+
match = new RegExp('^' + needle + SEP).exec(code)
|
|
81
97
|
if (match) return match.index
|
|
82
98
|
// somewhere (fallback)
|
|
83
|
-
return code.indexOf(
|
|
99
|
+
return code.indexOf(needle)
|
|
84
100
|
}
|
|
85
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} name
|
|
104
|
+
* @param {object} csn
|
|
105
|
+
*/
|
|
86
106
|
function _entity4 (name, csn) {
|
|
87
107
|
const entity = csn.definitions[name]
|
|
88
108
|
if (!entity) {
|
package/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
2
|
+
import { Rule, RuleTester, SourceCode } from "eslint";
|
|
3
3
|
|
|
4
4
|
export interface CDSRuleContext extends Rule.RuleContext {
|
|
5
5
|
cds: any;
|
|
@@ -9,13 +9,13 @@ export interface CDSRuleContext extends Rule.RuleContext {
|
|
|
9
9
|
options: [];
|
|
10
10
|
id: string;
|
|
11
11
|
sourcecode: SourceCode;
|
|
12
|
-
getModel:
|
|
13
|
-
report: (CDSRuleReport) => void;
|
|
12
|
+
getModel: Function;
|
|
13
|
+
report: (descriptor: CDSRuleReport) => void;
|
|
14
14
|
err: Error;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface Rule {
|
|
18
|
-
meta: RuleMetaData,
|
|
18
|
+
meta: Rule.RuleMetaData,
|
|
19
19
|
create: (context: CDSRuleContext) => void;
|
|
20
20
|
}
|
|
21
21
|
|
package/lib/utils/Cache.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Simple cache to store model and any cds calls made in the rule creation
|
|
3
5
|
* api to modify the model
|
|
@@ -18,9 +20,9 @@ module.exports = {
|
|
|
18
20
|
const dump = {}
|
|
19
21
|
for (const [key, value] of cache.entries()) {
|
|
20
22
|
const timestamp = new Date(value[1])
|
|
21
|
-
dump[key] = { key, value:
|
|
23
|
+
dump[key] = { key, value: value[0], timestamp }
|
|
22
24
|
}
|
|
23
|
-
return dump
|
|
25
|
+
return JSON.stringify(dump, null, 2)
|
|
24
26
|
},
|
|
25
27
|
remove (key) {
|
|
26
28
|
if (cache.has(key)) {
|
package/lib/utils/Colors.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error class used to indicate assertion errors in this plugin.
|
|
5
|
+
* For example, if we need to validate some object structure at
|
|
6
|
+
* runtime and find issues that we need to report.
|
|
7
|
+
*/
|
|
8
|
+
class CdsLintAssertionError extends Error {
|
|
9
|
+
constructor(msg) {
|
|
10
|
+
super(msg)
|
|
11
|
+
this.code = 'CDS_LINT_ASSERTION_ERROR'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
CdsLintAssertionError,
|
|
17
|
+
}
|