@sap/eslint-plugin-cds 3.1.1 → 3.2.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 +22 -3
- package/lib/conf/all.js +2 -0
- package/lib/conf/experimental.js +1 -2
- package/lib/index.js +1 -3
- package/lib/parser.js +56 -33
- package/lib/rules/assoc2many-ambiguous-key.js +1 -1
- package/lib/rules/auth-no-empty-restrictions.js +1 -1
- package/lib/rules/auth-restrict-grant-service.js +1 -1
- package/lib/rules/auth-use-requires.js +1 -1
- package/lib/rules/auth-valid-restrict-grant.js +1 -1
- package/lib/rules/auth-valid-restrict-keys.js +1 -1
- package/lib/rules/auth-valid-restrict-to.js +1 -1
- package/lib/rules/auth-valid-restrict-where.js +1 -1
- package/lib/rules/extension-restrictions.js +1 -1
- package/lib/rules/index.js +2 -2
- package/lib/rules/no-db-keywords.js +1 -2
- package/lib/rules/no-dollar-prefixed-names.js +1 -1
- package/lib/rules/no-java-keywords.js +1 -1
- package/lib/rules/no-join-on-draft.js +1 -1
- package/lib/rules/sql-cast-suggestion.js +1 -1
- package/lib/rules/sql-null-comparison.js +3 -3
- package/lib/rules/start-elements-lowercase.js +40 -39
- package/lib/rules/start-entities-uppercase.js +22 -34
- package/lib/rules/valid-csv-header.js +1 -1
- package/lib/utils/Cache.js +21 -18
- package/lib/utils/createRule.js +40 -41
- package/lib/utils/{getProjectRootPath.js → projectRootPath.js} +18 -6
- package/lib/utils/runRuleTester.js +12 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,16 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
This project adheres to [Semantic Versioning](
|
|
5
|
+
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
8
|
+
|
|
9
|
+
## [3.2.0] - 2025-03-03
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Rules `@sap/cds/sql-null-comparison` and `@sap/cds/no-java-keywords` are moved from the `experimental` rule set to `all`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Rules `@sap/cds/sql-null-comparison` will not warn about `!= null`, as it may be supported by future CDS compiler versions.
|
|
18
|
+
- Some rules had `docs` meta property `recommended: true`, but were not part of the recommended rules list.
|
|
19
|
+
- When determining a CDS project's root directory, we now consider package.json's with `@sap/cds` as `devDependency` or `peerDependency`
|
|
20
|
+
|
|
21
|
+
## [3.1.2] - 2024-10-31
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- run inferred rules correctly on Microsoft Windows
|
|
6
26
|
|
|
7
|
-
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
27
|
|
|
9
28
|
## [3.1.1] - 2024-10-08
|
|
10
29
|
|
|
11
30
|
### Changed
|
|
12
31
|
|
|
13
32
|
- `no-db-keywords` is no longer part of the 'recommended' rules,
|
|
14
|
-
as the cds-compiler takes care of quoting SQL keywords, if they are used as identifiers.
|
|
33
|
+
as the cds-compiler takes care of quoting SQL keywords, if they are used as identifiers.
|
|
15
34
|
|
|
16
35
|
### Fixed
|
|
17
36
|
|
package/lib/conf/all.js
CHANGED
package/lib/conf/experimental.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
* - Expose any 'rules' for use in ESLint
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const path = require('node:path')
|
|
20
|
-
|
|
21
19
|
const api = require('./api')
|
|
22
20
|
const getConfigs = require('./conf')
|
|
23
21
|
const rules = Object.assign(
|
|
@@ -25,7 +23,7 @@ const rules = Object.assign(
|
|
|
25
23
|
...Object.entries(require('./rules')).map(([k, v]) => ({ [k]: v() }))
|
|
26
24
|
)
|
|
27
25
|
|
|
28
|
-
const packageJson = require(
|
|
26
|
+
const packageJson = require('../package.json')
|
|
29
27
|
|
|
30
28
|
const plugin = {
|
|
31
29
|
meta: {
|
package/lib/parser.js
CHANGED
|
@@ -11,53 +11,65 @@
|
|
|
11
11
|
* (parserOptions).
|
|
12
12
|
*/
|
|
13
13
|
const cds = require('@sap/cds')
|
|
14
|
-
const
|
|
14
|
+
const { globalCache } = require('./utils/Cache')
|
|
15
15
|
const LOG = cds.debug('lint:plugin')
|
|
16
16
|
const colors = require('./utils/Colors')
|
|
17
17
|
const { splitDefName } = require('./utils/rules')
|
|
18
|
+
const packageJson = require('../package.json')
|
|
19
|
+
|
|
20
|
+
const newLineRegEx = /\r\n?|\n/g
|
|
18
21
|
|
|
19
22
|
module.exports = {
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
meta: {
|
|
24
|
+
name: packageJson.name,
|
|
25
|
+
version: packageJson.version
|
|
26
|
+
},
|
|
27
|
+
parse(code, parserOptions) {
|
|
28
|
+
return module.exports.parseForESLint(code, parserOptions).ast
|
|
22
29
|
},
|
|
23
|
-
|
|
30
|
+
// See https://eslint.org/docs/latest/extend/custom-parsers#parseforeslint-return-object
|
|
31
|
+
// eslint-disable-next-line no-unused-vars
|
|
32
|
+
parseForESLint(code, parserOptions) {
|
|
24
33
|
return {
|
|
25
34
|
ast: createProgramAST(code),
|
|
26
35
|
services: {
|
|
27
36
|
getParsedCsn: function () {
|
|
37
|
+
const compileOptions = {
|
|
38
|
+
messages: [],
|
|
39
|
+
}
|
|
28
40
|
let compiledModel
|
|
29
41
|
let reflectedModel
|
|
30
|
-
const messages = []
|
|
31
42
|
try {
|
|
32
|
-
compiledModel = cds.parse(code)
|
|
43
|
+
compiledModel = cds.parse(code, compileOptions)
|
|
33
44
|
} catch {
|
|
34
45
|
// Do nothing
|
|
35
46
|
}
|
|
36
47
|
if (compiledModel) {
|
|
37
48
|
try {
|
|
38
49
|
reflectedModel = cds.linked(compiledModel)
|
|
39
|
-
if (messages) {
|
|
40
|
-
reflectedModel.messages = messages
|
|
50
|
+
if (compileOptions.messages) {
|
|
51
|
+
reflectedModel.messages = compileOptions.messages
|
|
41
52
|
}
|
|
42
53
|
} catch (err) {
|
|
43
|
-
LOG
|
|
44
|
-
LOG
|
|
45
|
-
LOG
|
|
54
|
+
LOG?.(colors.red + 'ERROR:' + colors.reset, err)
|
|
55
|
+
LOG?.('COMPILED', compiledModel)
|
|
56
|
+
LOG?.('REFLECTED', reflectedModel)
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
return reflectedModel
|
|
49
60
|
},
|
|
61
|
+
|
|
50
62
|
getInferredCsn: function () {
|
|
51
|
-
const rootPath =
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
63
|
+
const rootPath = globalCache.get('rootpath')
|
|
64
|
+
if (globalCache.has('test')) {
|
|
65
|
+
return globalCache.get(`model:${rootPath}`)
|
|
54
66
|
}
|
|
55
67
|
let compiledModel
|
|
56
68
|
let reflectedModel
|
|
57
69
|
cds.resolve.cache = {}
|
|
58
70
|
|
|
59
|
-
if (!
|
|
60
|
-
const roots =
|
|
71
|
+
if (!globalCache.has(`model:${rootPath}`) && rootPath) {
|
|
72
|
+
const roots = globalCache.get(`roots:${rootPath}`)
|
|
61
73
|
const messages = []
|
|
62
74
|
if (roots) {
|
|
63
75
|
try {
|
|
@@ -67,27 +79,28 @@ module.exports = {
|
|
|
67
79
|
locations: true,
|
|
68
80
|
messages
|
|
69
81
|
})
|
|
70
|
-
|
|
82
|
+
globalCache.remove('errRootModel')
|
|
71
83
|
} catch (err) {
|
|
72
|
-
|
|
84
|
+
// TODO: Only catch Compile Errors?
|
|
85
|
+
globalCache.set('errRootModel', err)
|
|
73
86
|
}
|
|
74
87
|
if (compiledModel) {
|
|
75
88
|
reflectedModel = cds.linked(compiledModel)
|
|
76
|
-
|
|
89
|
+
globalCache.set(`model:${globalCache.get('rootpath')}`, reflectedModel)
|
|
77
90
|
if (messages) {
|
|
78
91
|
reflectedModel.messages = messages
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
} else {
|
|
83
|
-
reflectedModel =
|
|
96
|
+
reflectedModel = globalCache.get(`model:${rootPath}`)
|
|
84
97
|
}
|
|
85
98
|
return reflectedModel
|
|
86
99
|
},
|
|
87
100
|
updateInferredCsn: compileModelFromDict,
|
|
88
101
|
getEnvironment: function () {
|
|
89
|
-
const options =
|
|
90
|
-
return
|
|
102
|
+
const options = globalCache.get('options')
|
|
103
|
+
return options?.[0]?.environment
|
|
91
104
|
},
|
|
92
105
|
getLocation: function (name, obj, model) {
|
|
93
106
|
let loc
|
|
@@ -141,27 +154,37 @@ module.exports = {
|
|
|
141
154
|
}
|
|
142
155
|
},
|
|
143
156
|
createProgramAST,
|
|
144
|
-
compileModelFromDict
|
|
157
|
+
compileModelFromDict,
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/**
|
|
148
|
-
* Generates dummy AST with just single Program node
|
|
161
|
+
* Generates dummy AST with just single Program node.
|
|
149
162
|
*
|
|
150
163
|
* @param code Parse file contents
|
|
151
164
|
* @param {object} [loc]
|
|
152
|
-
* @returns AST
|
|
165
|
+
* @returns ESLint AST
|
|
153
166
|
*/
|
|
154
167
|
function createProgramAST (code, loc) {
|
|
155
|
-
loc
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
if (!loc && code.length) {
|
|
169
|
+
const newLines = [...code.matchAll(newLineRegEx)]
|
|
170
|
+
const endColumn = newLines.length ? (code.length - newLines.at(-1).index) : code.length
|
|
171
|
+
loc = {
|
|
172
|
+
start: {
|
|
173
|
+
line: 1,
|
|
174
|
+
column: 1
|
|
175
|
+
},
|
|
176
|
+
end: {
|
|
177
|
+
line: newLines.length + 1,
|
|
178
|
+
column: endColumn
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
} else if (!loc) {
|
|
182
|
+
loc = {
|
|
183
|
+
start: { line: 0, column: 0 },
|
|
184
|
+
end: { line: 0, column: 0 },
|
|
163
185
|
}
|
|
164
186
|
}
|
|
187
|
+
|
|
165
188
|
return {
|
|
166
189
|
type: 'Program',
|
|
167
190
|
body: [],
|
|
@@ -12,7 +12,7 @@ module.exports = {
|
|
|
12
12
|
'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
|
|
13
13
|
category: 'Model Validation',
|
|
14
14
|
recommended: true,
|
|
15
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
15
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/assoc2many-ambiguous-key',
|
|
16
16
|
},
|
|
17
17
|
messages: {
|
|
18
18
|
ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`,
|
|
@@ -9,7 +9,7 @@ module.exports = {
|
|
|
9
9
|
description: '`@restrict` and `@requires` must not be empty.',
|
|
10
10
|
category: 'Model Validation',
|
|
11
11
|
recommended: true,
|
|
12
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-no-empty-restrictions',
|
|
13
13
|
},
|
|
14
14
|
messages: {
|
|
15
15
|
missingRestriction: 'No explicit restrictions provided on {{kind}} `{{name}}` at `{{label}}`.',
|
|
@@ -7,7 +7,7 @@ module.exports = {
|
|
|
7
7
|
description: '`@restrict.grant` on service level and for bound/unbound actions and functions is limited to grant: \'*\'',
|
|
8
8
|
category: 'Model Validation',
|
|
9
9
|
recommended: true,
|
|
10
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
10
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-restrict-grant-service',
|
|
11
11
|
},
|
|
12
12
|
messages: {
|
|
13
13
|
limitedGrant: `The grant value provided in @restrict is limited to '*' for {{kind}} '{{name}}'`,
|
|
@@ -8,7 +8,7 @@ module.exports = {
|
|
|
8
8
|
category: 'Model Validation',
|
|
9
9
|
recommended: true,
|
|
10
10
|
version: '2.4.1',
|
|
11
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
11
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-use-requires',
|
|
12
12
|
},
|
|
13
13
|
messages: {
|
|
14
14
|
useRequires: 'Use `@requires` instead of `@restrict.to` at {{kind}} `{{name}}`.'
|
|
@@ -20,7 +20,7 @@ module.exports = {
|
|
|
20
20
|
description: '`@restrict.grant` must have valid values.',
|
|
21
21
|
category: 'Model Validation',
|
|
22
22
|
recommended: true,
|
|
23
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
23
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-grant',
|
|
24
24
|
},
|
|
25
25
|
messages: {
|
|
26
26
|
invalidType: 'Invalid type for grant value. Must either be string or array of strings.',
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
description: '`@restrict` must not have properties besides `to`, `grant`, and `where`.',
|
|
14
14
|
category: 'Model Validation',
|
|
15
15
|
recommended: true,
|
|
16
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
16
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-keys',
|
|
17
17
|
},
|
|
18
18
|
messages: {
|
|
19
19
|
misspelledProperty: "Misspelled or unknown property '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
@@ -9,7 +9,7 @@ module.exports = {
|
|
|
9
9
|
description: '`@restrict.to` must have valid values.',
|
|
10
10
|
category: 'Model Validation',
|
|
11
11
|
recommended: true,
|
|
12
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-to',
|
|
13
13
|
},
|
|
14
14
|
messages: {
|
|
15
15
|
invalidType: 'Invalid type for value of `@restrict.to`. Must either be string or array of strings.',
|
|
@@ -9,7 +9,7 @@ module.exports = {
|
|
|
9
9
|
description: '`@restrict.where` must have valid values.',
|
|
10
10
|
category: 'Model Validation',
|
|
11
11
|
recommended: true,
|
|
12
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-where',
|
|
13
13
|
},
|
|
14
14
|
severity: 'error',
|
|
15
15
|
messages: {
|
|
@@ -10,7 +10,7 @@ const rule = module.exports = {
|
|
|
10
10
|
description: 'Extensions must not violate restrictions set by the extended SaaS app.',
|
|
11
11
|
category: 'Model Validation',
|
|
12
12
|
recommended: true,
|
|
13
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
13
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/extension-restrictions',
|
|
14
14
|
},
|
|
15
15
|
hasSuggestions: false,
|
|
16
16
|
type: 'problem',
|
package/lib/rules/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const { globalCache } = require('../utils/Cache')
|
|
4
4
|
const createRule = require('../utils/createRule')
|
|
5
5
|
|
|
6
6
|
const rules = {
|
|
@@ -25,6 +25,6 @@ const rules = {
|
|
|
25
25
|
'extension-restrictions': () => createRule(require('./extension-restrictions'))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
globalCache.set('rules', rules)
|
|
29
29
|
|
|
30
30
|
module.exports = rules
|
|
@@ -12,8 +12,7 @@ module.exports = {
|
|
|
12
12
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
13
13
|
docs: {
|
|
14
14
|
description: 'Avoid using reserved SQL keywords.',
|
|
15
|
-
|
|
16
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
|
|
15
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
|
|
17
16
|
},
|
|
18
17
|
messages: {
|
|
19
18
|
reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
|
|
@@ -6,7 +6,7 @@ module.exports = {
|
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
|
|
8
8
|
recommended: true,
|
|
9
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
|
|
10
10
|
},
|
|
11
11
|
messages: {
|
|
12
12
|
dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
|
|
@@ -27,7 +27,7 @@ module.exports = {
|
|
|
27
27
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
28
28
|
docs: {
|
|
29
29
|
description: 'Reject reserved Java keywords as CDS identifiers.',
|
|
30
|
-
|
|
30
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
|
|
31
31
|
},
|
|
32
32
|
type: 'problem',
|
|
33
33
|
model: 'inferred',
|
|
@@ -6,7 +6,7 @@ module.exports = {
|
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
|
|
8
8
|
recommended: true,
|
|
9
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',
|
|
10
10
|
},
|
|
11
11
|
messages: {
|
|
12
12
|
draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
|
|
@@ -6,7 +6,7 @@ module.exports = {
|
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Should make suggestions for possible missing SQL casts.',
|
|
8
8
|
recommended: true,
|
|
9
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
9
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-cast-suggestion',
|
|
10
10
|
},
|
|
11
11
|
type: 'suggestion',
|
|
12
12
|
messages: {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { forEachXprInDefinition } = require('../utils/csnTraversal')
|
|
4
4
|
|
|
5
|
-
const invalidComparisonOperators = [ '=', '
|
|
5
|
+
const invalidComparisonOperators = [ '=', '<>' ]
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
meta: {
|
|
@@ -12,12 +12,12 @@ module.exports = {
|
|
|
12
12
|
category: 'Model Validation',
|
|
13
13
|
recommended: false,
|
|
14
14
|
// TODO: Add documentation
|
|
15
|
-
// url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
15
|
+
// url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
|
|
16
16
|
},
|
|
17
17
|
type: 'problem',
|
|
18
18
|
model: 'parsed',
|
|
19
19
|
messages: {
|
|
20
|
-
nullComparison: `Comparisons against 'null' are always null. Did you mean 'is not null'?`,
|
|
20
|
+
nullComparison: `Comparisons against 'null' using '=' and '<>' are always null. Did you mean 'is null'/'is not null'?`,
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
create(context) {
|
|
@@ -1,63 +1,64 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const allowedUpperCaseElements = ['ID']
|
|
4
|
+
|
|
3
5
|
module.exports = {
|
|
4
6
|
meta: {
|
|
5
7
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
8
|
docs: {
|
|
7
9
|
description: 'Regular element names should start with lowercase letters.',
|
|
8
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
10
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-elements-lowercase',
|
|
9
11
|
},
|
|
10
12
|
type: 'suggestion',
|
|
11
|
-
hasSuggestions: true,
|
|
12
13
|
messages: {
|
|
13
|
-
startLowercase: "Element name '{{
|
|
14
|
-
fixLowercase: 'Start element name with a lowercase letter.'
|
|
14
|
+
startLowercase: "Element name '{{defName}}:{{elementName}}' should start with a lowercase letter.",
|
|
15
15
|
},
|
|
16
16
|
fixable: 'code',
|
|
17
17
|
model: 'parsed',
|
|
18
18
|
},
|
|
19
19
|
create: function (context) {
|
|
20
|
-
const
|
|
20
|
+
const model = context.getModel()
|
|
21
|
+
if (!model?.definitions)
|
|
22
|
+
return
|
|
21
23
|
|
|
22
|
-
return {
|
|
23
|
-
|
|
24
|
+
return function checkAllElementsStartWithLowercase() {
|
|
25
|
+
for (const defName in model.definitions)
|
|
26
|
+
checkDefinition(defName, model.definitions[defName])
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
column: loc.end.column
|
|
39
|
-
})
|
|
40
|
-
const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0
|
|
41
|
-
return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized)
|
|
42
|
-
}
|
|
43
|
-
context.report({
|
|
44
|
-
messageId: 'startLowercase',
|
|
45
|
-
loc,
|
|
46
|
-
file,
|
|
47
|
-
data: {
|
|
48
|
-
entityName,
|
|
49
|
-
elementName
|
|
50
|
-
},
|
|
51
|
-
suggest: [
|
|
52
|
-
{
|
|
53
|
-
messageId: 'fixLowercase',
|
|
54
|
-
fix
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
})
|
|
29
|
+
function checkDefinition(defName, def) {
|
|
30
|
+
if (defName.startsWith('localized') || defName.endsWith('texts'))
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
checkElements(def)
|
|
34
|
+
|
|
35
|
+
function checkElements(art) {
|
|
36
|
+
if (art.elements) {
|
|
37
|
+
for (const elementName in art.elements) {
|
|
38
|
+
const element = art.elements[elementName]
|
|
39
|
+
checkStartLowercase(element, elementName)
|
|
40
|
+
checkElements(element)
|
|
58
41
|
}
|
|
59
42
|
}
|
|
60
43
|
}
|
|
44
|
+
|
|
45
|
+
function checkStartLowercase (element, elementName) {
|
|
46
|
+
if (!element.$location?.file)
|
|
47
|
+
return // without location, we can't report anything properly
|
|
48
|
+
|
|
49
|
+
if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase()
|
|
50
|
+
&& !allowedUpperCaseElements.includes(elementName)) {
|
|
51
|
+
context.report({
|
|
52
|
+
messageId: 'startLowercase',
|
|
53
|
+
loc: context.getLocation(elementName, element),
|
|
54
|
+
file: element.$location.file,
|
|
55
|
+
data: {
|
|
56
|
+
defName,
|
|
57
|
+
elementName
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -7,53 +7,41 @@ module.exports = {
|
|
|
7
7
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
8
8
|
docs: {
|
|
9
9
|
description: 'Regular entity names should start with uppercase letters.',
|
|
10
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
10
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-entities-uppercase',
|
|
11
11
|
},
|
|
12
12
|
type: 'suggestion',
|
|
13
|
-
hasSuggestions: true,
|
|
14
13
|
messages: {
|
|
15
14
|
startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
|
|
16
|
-
fixUppercase: 'Start entity name with an uppercase letter.'
|
|
17
15
|
},
|
|
18
16
|
fixable: 'code',
|
|
19
17
|
model: 'parsed',
|
|
20
18
|
},
|
|
21
19
|
create(context) {
|
|
22
|
-
const
|
|
20
|
+
const model = context.getModel()
|
|
21
|
+
if (!model?.definitions)
|
|
22
|
+
return
|
|
23
23
|
|
|
24
|
-
return
|
|
24
|
+
return function checkAllEntitiesStartWithUppercase() {
|
|
25
|
+
for (const defName in model.definitions) {
|
|
26
|
+
const def = model.definitions[defName]
|
|
27
|
+
if (def.kind === 'entity') {
|
|
28
|
+
checkEntityStartsUppercase(defName, def)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
25
32
|
|
|
26
|
-
function
|
|
27
|
-
if (
|
|
28
|
-
return //
|
|
33
|
+
function checkEntityStartsUppercase(name, entity) {
|
|
34
|
+
if (!entity.$location?.file)
|
|
35
|
+
return // without location, we can't report anything properly
|
|
29
36
|
|
|
30
|
-
const entityName = splitDefName(
|
|
37
|
+
const entityName = splitDefName(entity, name).name
|
|
31
38
|
if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
line: loc.end.line,
|
|
39
|
-
column: loc.end.column
|
|
40
|
-
})
|
|
41
|
-
const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0
|
|
42
|
-
return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized)
|
|
43
|
-
}
|
|
44
|
-
context.report({
|
|
45
|
-
messageId: 'startUppercase',
|
|
46
|
-
loc,
|
|
47
|
-
file,
|
|
48
|
-
data: { entityName },
|
|
49
|
-
suggest: [
|
|
50
|
-
{
|
|
51
|
-
messageId: 'fixUppercase',
|
|
52
|
-
fix
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
})
|
|
56
|
-
}
|
|
39
|
+
context.report({
|
|
40
|
+
messageId: 'startUppercase',
|
|
41
|
+
loc: context.getLocation(entityName, entity),
|
|
42
|
+
file: entity.$location.file,
|
|
43
|
+
data: { entityName },
|
|
44
|
+
})
|
|
57
45
|
}
|
|
58
46
|
}
|
|
59
47
|
}
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
description: 'CSV files for entities must refer to valid element names.',
|
|
14
14
|
category: 'Model Validation',
|
|
15
15
|
recommended: true,
|
|
16
|
-
url: 'https://cap.cloud.sap/docs/tools/cds-lint/
|
|
16
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/valid-csv-header',
|
|
17
17
|
},
|
|
18
18
|
severity: 'warn',
|
|
19
19
|
type: 'problem',
|
package/lib/utils/Cache.js
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Simple cache to store model and any cds calls made in the rule creation
|
|
5
|
-
* api to modify the model
|
|
6
|
-
*/
|
|
7
|
-
const cache = new Map()
|
|
8
3
|
|
|
9
|
-
|
|
4
|
+
class Cache {
|
|
5
|
+
#entries = new Map()
|
|
6
|
+
|
|
10
7
|
has (key) {
|
|
11
|
-
return
|
|
12
|
-
}
|
|
8
|
+
return this.#entries.has(key)
|
|
9
|
+
}
|
|
13
10
|
set (key, value) {
|
|
14
|
-
return
|
|
15
|
-
}
|
|
11
|
+
return this.#entries.set(key, [value, Date.now()])
|
|
12
|
+
}
|
|
16
13
|
get (key) {
|
|
17
|
-
return
|
|
18
|
-
}
|
|
14
|
+
return this.#entries.has(key) ? this.#entries.get(key)[0] : undefined
|
|
15
|
+
}
|
|
19
16
|
dump () {
|
|
20
17
|
const dump = {}
|
|
21
|
-
for (const [key, value] of
|
|
18
|
+
for (const [key, value] of this.#entries.entries()) {
|
|
22
19
|
const timestamp = new Date(value[1])
|
|
23
20
|
dump[key] = { key, value: value[0], timestamp }
|
|
24
21
|
}
|
|
25
22
|
return JSON.stringify(dump, null, 2)
|
|
26
|
-
}
|
|
23
|
+
}
|
|
27
24
|
remove (key) {
|
|
28
|
-
if (
|
|
29
|
-
|
|
25
|
+
if (this.#entries.has(key)) {
|
|
26
|
+
this.#entries.delete(key)
|
|
30
27
|
}
|
|
31
|
-
}
|
|
28
|
+
}
|
|
32
29
|
clear () {
|
|
33
|
-
|
|
30
|
+
this.#entries.clear()
|
|
34
31
|
}
|
|
35
32
|
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
Cache,
|
|
37
|
+
globalCache: new Cache(),
|
|
38
|
+
}
|
package/lib/utils/createRule.js
CHANGED
|
@@ -17,10 +17,10 @@ const fs = require('fs')
|
|
|
17
17
|
const path = require('path')
|
|
18
18
|
const cds = require('@sap/cds')
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const { globalCache } = require('./Cache')
|
|
21
21
|
const constants = require('../constants')
|
|
22
22
|
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
23
|
-
const getProjectRootPath = require('./
|
|
23
|
+
const { getProjectRootPath, hasProjectRoots } = require('./projectRootPath')
|
|
24
24
|
const { CdsLintAssertionError } = require('./LintError')
|
|
25
25
|
|
|
26
26
|
const LOG = cds.debug('lint:plugin')
|
|
@@ -48,18 +48,18 @@ module.exports = function createRule(spec) {
|
|
|
48
48
|
Program: node => {
|
|
49
49
|
const file = context.getFilename()
|
|
50
50
|
if (file !== filePrev) {
|
|
51
|
-
LOG
|
|
51
|
+
LOG?.(`File: ${context.getFilename()}`)
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta,
|
|
53
|
+
const cdsContext = extendContext(node, context, meta)
|
|
54
|
+
globalCache.set('context', cdsContext)
|
|
55
|
+
const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdsContext)
|
|
56
56
|
switch (meta.model) {
|
|
57
57
|
case 'none':
|
|
58
58
|
if (doEnvironmentChecks) {
|
|
59
|
-
if (isTest || !
|
|
60
|
-
LOG
|
|
61
|
-
|
|
62
|
-
createReport(node,
|
|
59
|
+
if (isTest || !globalCache.has(`rule:${cdsContext.id}`)) {
|
|
60
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
61
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
62
|
+
createReport(node, cdsContext, meta, create)
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
break
|
|
@@ -67,24 +67,24 @@ module.exports = function createRule(spec) {
|
|
|
67
67
|
case 'inferred':
|
|
68
68
|
if (isValidFile && doRootModelChecks) {
|
|
69
69
|
if (showInEditor) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
globalCache.remove(`model:${globalCache.get('rootpath')}`)
|
|
71
|
+
globalCache.remove(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)
|
|
72
|
+
globalCache.remove(`report:${context.getFilename()}:${context.id}`)
|
|
73
73
|
}
|
|
74
|
-
if (isTest || showInEditor || !
|
|
75
|
-
LOG
|
|
74
|
+
if (isTest || showInEditor || !globalCache.has(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)) {
|
|
75
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
76
76
|
if (!showInEditor) {
|
|
77
|
-
|
|
77
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
78
78
|
}
|
|
79
|
-
createReport(node,
|
|
79
|
+
createReport(node, cdsContext, meta, create)
|
|
80
80
|
} else {
|
|
81
|
-
if (
|
|
82
|
-
const reports =
|
|
81
|
+
if (globalCache.has(`report:${context.getFilename()}:${context.id}`)) {
|
|
82
|
+
const reports = globalCache.get(`report:${context.getFilename()}:${context.id}`)
|
|
83
83
|
for (const r of Array.from(reports)) {
|
|
84
84
|
context.report(JSON.parse(r))
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
globalCache.remove(`report:${context.getFilename()}:${context.id}`)
|
|
87
|
+
globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -92,8 +92,8 @@ module.exports = function createRule(spec) {
|
|
|
92
92
|
|
|
93
93
|
default:
|
|
94
94
|
if (isValidFile) {
|
|
95
|
-
LOG
|
|
96
|
-
createReport(node,
|
|
95
|
+
LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
|
|
96
|
+
createReport(node, cdsContext, meta, create)
|
|
97
97
|
}
|
|
98
98
|
break
|
|
99
99
|
}
|
|
@@ -105,22 +105,21 @@ module.exports = function createRule(spec) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function isRunningWithCDSLint () {
|
|
108
|
-
return process.argv[
|
|
108
|
+
return process.argv[1].match(/cds(\.js)?$/) && process.argv[2]?.toLowerCase() === 'lint'
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function isRunningWithESLint () {
|
|
112
|
-
return process.argv[
|
|
112
|
+
return process.argv[1].match(/eslint(\.js)?$/)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function checkEntryCriteria (meta,
|
|
116
|
-
const isTest =
|
|
117
|
-
const showInEditor =
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const doRootModelChecks = isTest || (hasProjectRoots && (isRunningWithCDSLint() || isRunningWithESLint()) || showInEditor)
|
|
115
|
+
function checkEntryCriteria (meta, cdsContext) {
|
|
116
|
+
const isTest = globalCache.has('test')
|
|
117
|
+
const showInEditor = cdsContext.options.includes('show')
|
|
118
|
+
const isValidFile = isConfiguredFileType(cdsContext.getFilename(), 'FILES')
|
|
119
|
+
const doRootModelChecks = isTest || (hasProjectRoots() && (isRunningWithCDSLint() || isRunningWithESLint()) || showInEditor)
|
|
121
120
|
// Lint all env rules independent of any parsed file (i.e. 'cds lint' uses the lintText "" API)
|
|
122
121
|
const doEnvironmentChecks =
|
|
123
|
-
isTest || (isRunningWithCDSLint() &&
|
|
122
|
+
isTest || (isRunningWithCDSLint() && cdsContext.getFilename() === '<text>')
|
|
124
123
|
return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks, showInEditor }
|
|
125
124
|
}
|
|
126
125
|
|
|
@@ -196,11 +195,11 @@ function sanitizeFileLocation (d) {
|
|
|
196
195
|
* @param meta
|
|
197
196
|
*/
|
|
198
197
|
function extendContext (node, context, meta) {
|
|
199
|
-
if (!
|
|
198
|
+
if (!globalCache.has('test')) {
|
|
200
199
|
const filePath = context.getFilename()
|
|
201
200
|
const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
|
|
202
201
|
if (rootPath) {
|
|
203
|
-
|
|
202
|
+
globalCache.set('rootpath', rootPath)
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
|
|
@@ -222,7 +221,7 @@ function extendContext (node, context, meta) {
|
|
|
222
221
|
}
|
|
223
222
|
cdscontext.getLocation = parserServices.getLocation
|
|
224
223
|
cdscontext.getNode = Object.keys(parserServices).length > 0 ? parserServices.getNode : () => node
|
|
225
|
-
cdscontext.getRootPath = () =>
|
|
224
|
+
cdscontext.getRootPath = () => globalCache.get('rootpath')
|
|
226
225
|
return cdscontext
|
|
227
226
|
|
|
228
227
|
function reportWrapper(r) {
|
|
@@ -232,7 +231,7 @@ function extendContext (node, context, meta) {
|
|
|
232
231
|
if (!r.file) {
|
|
233
232
|
throw new CdsLintAssertionError(`Rule ${context.id} must return a "file" property in the rule report!`)
|
|
234
233
|
}
|
|
235
|
-
const file =
|
|
234
|
+
const file = globalCache.get('rootpath') ? resolveFilePath(r.file) : r.file
|
|
236
235
|
if (cdscontext.getFilename() === file) {
|
|
237
236
|
delete r.file
|
|
238
237
|
context.report(r)
|
|
@@ -280,11 +279,11 @@ function cacheReport (r, filepath, context, meta) {
|
|
|
280
279
|
}
|
|
281
280
|
if (r) {
|
|
282
281
|
let reports = new Set()
|
|
283
|
-
if (
|
|
284
|
-
reports =
|
|
282
|
+
if (globalCache.has(`report:${filepath}:${context.id}`)) {
|
|
283
|
+
reports = globalCache.get(`report:${filepath}:${context.id}`)
|
|
285
284
|
}
|
|
286
285
|
reports.add(JSON.stringify(r))
|
|
287
|
-
|
|
286
|
+
globalCache.set(`report:${filepath}:${context.id}`, reports)
|
|
288
287
|
}
|
|
289
288
|
}
|
|
290
289
|
|
|
@@ -295,7 +294,7 @@ function cacheReport (r, filepath, context, meta) {
|
|
|
295
294
|
*/
|
|
296
295
|
function getDisabled (code, sourcecode, line) {
|
|
297
296
|
const listDisabled = []
|
|
298
|
-
const rules =
|
|
297
|
+
const rules = globalCache.get('rules')
|
|
299
298
|
const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
|
|
300
299
|
if (code) {
|
|
301
300
|
const matches = [...code.matchAll(REGEX_COMMENTS)]
|
|
@@ -375,5 +374,5 @@ function getLastLine (code) {
|
|
|
375
374
|
* @param {string} file
|
|
376
375
|
*/
|
|
377
376
|
function resolveFilePath (file) {
|
|
378
|
-
return path.isAbsolute(file) ? file : path.join(
|
|
377
|
+
return path.isAbsolute(file) ? file : path.join(globalCache.get('rootpath'), file)
|
|
379
378
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const cds = require('@sap/cds')
|
|
5
|
-
const
|
|
5
|
+
const { globalCache } = require('./Cache')
|
|
6
6
|
const fs = require('node:fs')
|
|
7
7
|
|
|
8
8
|
const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Searches for directory containing cds roots
|
|
11
|
+
* Searches for a directory containing cds roots.
|
|
12
12
|
*
|
|
13
13
|
* As of today, there is no unified way to find the root directory for a CDS project.
|
|
14
14
|
* ("The root is wherever the user typed `cds init`")
|
|
@@ -20,9 +20,8 @@ const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
|
|
|
20
20
|
* @param {string} currentDir start here and search until root dir
|
|
21
21
|
* @returns {string} dir containing cds roots (empty if not exists)
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
function getProjectRootPath(currentDir = '.') {
|
|
24
24
|
let dir = path.resolve(currentDir)
|
|
25
|
-
|
|
26
25
|
while (!couldBeProjectRoot(dir)) {
|
|
27
26
|
if (dir === path.resolve(dir, '..'))
|
|
28
27
|
return '' // we reached the file system root -> abort
|
|
@@ -32,7 +31,7 @@ module.exports = function getProjectRootPath(currentDir = '.') {
|
|
|
32
31
|
cds.resolve.cache = {}
|
|
33
32
|
const roots = cds.resolve('*', { root: dir })
|
|
34
33
|
if (roots?.length > 0) {
|
|
35
|
-
|
|
34
|
+
globalCache.set(`roots:${dir}`, roots)
|
|
36
35
|
return dir
|
|
37
36
|
}
|
|
38
37
|
return ''
|
|
@@ -56,8 +55,21 @@ function isRootPackageJson(filepath) {
|
|
|
56
55
|
|
|
57
56
|
try {
|
|
58
57
|
const config = JSON.parse(fs.readFileSync(filepath, 'utf8'))
|
|
59
|
-
return
|
|
58
|
+
return config && (config.cds ||
|
|
59
|
+
hasCdsPackage(config.dependencies) ||
|
|
60
|
+
hasCdsPackage(config.peerDependencies) ||
|
|
61
|
+
hasCdsPackage(config.devDependencies))
|
|
62
|
+
|
|
60
63
|
} catch {
|
|
61
64
|
return false
|
|
62
65
|
}
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
function hasCdsPackage(deps) {
|
|
69
|
+
return deps && (('@sap/cds' in deps) || ('@sap/cds-dk' in deps))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
getProjectRootPath,
|
|
74
|
+
hasProjectRoots: () => globalCache.has(`roots:${globalCache.get('rootpath')}`),
|
|
75
|
+
}
|
|
@@ -6,7 +6,7 @@ const fs = require('node:fs')
|
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
|
|
8
8
|
const { Linter, RuleTester } = require('eslint')
|
|
9
|
-
const
|
|
9
|
+
const { globalCache } = require('./Cache')
|
|
10
10
|
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
11
11
|
const { compileModelFromDict } = require('../parser')
|
|
12
12
|
const rules = require('../rules')
|
|
@@ -29,7 +29,7 @@ function testRuleWrapper(rule) {
|
|
|
29
29
|
_initModelRuleTester(filePath, rule.meta.model)
|
|
30
30
|
const createValue = rule.create(context)
|
|
31
31
|
const result = createValue.Program(node)
|
|
32
|
-
|
|
32
|
+
globalCache.clear()
|
|
33
33
|
return result
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -114,18 +114,18 @@ module.exports = function runRuleTester(options) {
|
|
|
114
114
|
* @param {string} flavor
|
|
115
115
|
*/
|
|
116
116
|
function _initModelRuleTester(filePath, flavor) {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
globalCache.set('rules', rules)
|
|
118
|
+
globalCache.set('test', true)
|
|
119
119
|
const rootPath = path.dirname(filePath)
|
|
120
|
-
|
|
120
|
+
globalCache.set('rootpath', rootPath)
|
|
121
121
|
if (flavor !== 'none') { // not for env rules
|
|
122
122
|
const files = fs.readdirSync(rootPath)
|
|
123
123
|
const modelfiles = files.map(f => path.join(rootPath, f)).filter(fp => isConfiguredFileType(fp, 'MODEL_FILES'))
|
|
124
|
-
|
|
124
|
+
globalCache.set(`modelfiles:${rootPath}`, modelfiles)
|
|
125
125
|
const dictFiles = _getDictFiles(rootPath, modelfiles)
|
|
126
|
-
|
|
126
|
+
globalCache.set(`dictfiles:${rootPath}`, dictFiles)
|
|
127
127
|
const reflectedModel = compileModelFromDict(dictFiles, { flavor })
|
|
128
|
-
|
|
128
|
+
globalCache.set(`model:${rootPath}`, reflectedModel)
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -139,12 +139,12 @@ function _initModelRuleTester(filePath, flavor) {
|
|
|
139
139
|
*/
|
|
140
140
|
function _getDictFiles(input, filenames) {
|
|
141
141
|
let dictFiles = {}
|
|
142
|
-
if (
|
|
143
|
-
dictFiles =
|
|
142
|
+
if (globalCache.has(`dictfiles:${input}`)) {
|
|
143
|
+
dictFiles = globalCache.get(`dictfiles:${input}`)
|
|
144
144
|
} else {
|
|
145
145
|
filenames.forEach(file => {
|
|
146
|
-
dictFiles[file] =
|
|
147
|
-
?
|
|
146
|
+
dictFiles[file] = globalCache.has(`file:${file}`)
|
|
147
|
+
? globalCache.get(`file:${file}`)
|
|
148
148
|
: fs.readFileSync(file, 'utf8')
|
|
149
149
|
})
|
|
150
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@sap/cds": ">=7",
|
|
24
|
-
"semver": "^7.
|
|
24
|
+
"semver": "^7.7.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"eslint": ">=8"
|