@sap/eslint-plugin-cds 2.6.1 → 2.6.4
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 +24 -0
- package/README.md +1 -2
- package/lib/conf/all.js +1 -0
- package/lib/conf/recommended.js +1 -0
- package/lib/parser.js +1 -1
- package/lib/rules/auth-restrict-grant-service.js +52 -0
- package/lib/rules/auth-valid-restrict-where.js +1 -19
- package/lib/rules/index.js +1 -0
- package/lib/rules/no-db-keywords.js +7 -2
- package/lib/rules/no-join-on-draft.js +1 -1
- package/lib/utils/createRule.js +15 -4
- package/lib/utils/genDocs.js +8 -7
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
## [2.6.4] - 2023-11-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- New `auth-restrict-grant-service` rule that validates events on restricted services.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- In _no-join-on-draft_, do not run check if there is no valid query.
|
|
19
|
+
- In _auth-valid-restrict-where_, do not consider when missing expression references.
|
|
20
|
+
|
|
21
|
+
## [2.6.3] - 2023-02-13
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Filter rule reports using *inferred* models on $location.
|
|
26
|
+
|
|
27
|
+
## [2.6.2] - 2023-02-13
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Fixed rule reports using *inferred* models to always receive valid _file_ $locations.
|
|
32
|
+
|
|
9
33
|
## [2.6.1] - 2023-01-26
|
|
10
34
|
|
|
11
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# @sap/eslint-plugin-cds
|
|
2
|
-
|
|
3
|
-
[](https://github.tools.sap/cap/eslint-plugin-cds/actions/workflows/sync.yml)
|
|
2
|
+
|
|
4
3
|
|
|
5
4
|
The [ESLint](https://eslint.org) plugin includes a set of recommended [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap) model and environment rules. The aim of CDS linting is to catch issues with CDS models and with the environment early. To use this module we recommend to install [@sap/cds-dk](https://www.npmjs.com/package/@sap/cds-dk) globally.
|
|
6
5
|
|
package/lib/conf/all.js
CHANGED
|
@@ -4,6 +4,7 @@ module.exports = {
|
|
|
4
4
|
'@sap/cds/assoc2many-ambiguous-key': 2,
|
|
5
5
|
'@sap/cds/auth-no-empty-restrictions': 2,
|
|
6
6
|
'@sap/cds/auth-use-requires': 2,
|
|
7
|
+
'@sap/cds/auth-restrict-grant-service': 2,
|
|
7
8
|
'@sap/cds/auth-valid-restrict-grant': 2,
|
|
8
9
|
'@sap/cds/auth-valid-restrict-keys': 2,
|
|
9
10
|
'@sap/cds/auth-valid-restrict-to': 2,
|
package/lib/conf/recommended.js
CHANGED
|
@@ -4,6 +4,7 @@ module.exports = {
|
|
|
4
4
|
'@sap/cds/assoc2many-ambiguous-key': 2,
|
|
5
5
|
'@sap/cds/auth-no-empty-restrictions': 2,
|
|
6
6
|
'@sap/cds/auth-use-requires': 1,
|
|
7
|
+
'@sap/cds/auth-restrict-grant-service': 2,
|
|
7
8
|
'@sap/cds/auth-valid-restrict-grant': 1,
|
|
8
9
|
'@sap/cds/auth-valid-restrict-keys': 1,
|
|
9
10
|
'@sap/cds/auth-valid-restrict-to': 1,
|
package/lib/parser.js
CHANGED
|
@@ -124,7 +124,7 @@ module.exports = {
|
|
|
124
124
|
let loc
|
|
125
125
|
if (obj) {
|
|
126
126
|
let name = obj.name
|
|
127
|
-
if (['entity', 'service'].includes(obj.kind)) {
|
|
127
|
+
if (['action', 'entity', 'function', 'service'].includes(obj.kind)) {
|
|
128
128
|
name = splitDefName(obj).name
|
|
129
129
|
}
|
|
130
130
|
loc = this.getLocation(name, obj)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */ }],
|
|
4
|
+
docs: {
|
|
5
|
+
description: '`@restrict.grant` on service level and for bound/unbound actions and functions is limited to grant: \'*\'',
|
|
6
|
+
category: 'Model Validation',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
type: 'problem',
|
|
10
|
+
model: 'inferred'
|
|
11
|
+
},
|
|
12
|
+
create (context) {
|
|
13
|
+
return {
|
|
14
|
+
action: checkRestrictGrant,
|
|
15
|
+
function: checkRestrictGrant,
|
|
16
|
+
service: checkRestrictGrant
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkRestrictGrant (d) {
|
|
20
|
+
const node = context.getNode(d)
|
|
21
|
+
const file = d.$location.file
|
|
22
|
+
if (d['@restrict']) {
|
|
23
|
+
for (const entry of d['@restrict']) {
|
|
24
|
+
if (Object.keys(entry).includes('grant')) {
|
|
25
|
+
const grantValue = entry.grant
|
|
26
|
+
const message = `The grant value provided in @restrict is limited to '*' for ${d.kind} '${d.name}'`
|
|
27
|
+
switch (typeof grantValue) {
|
|
28
|
+
case 'string':
|
|
29
|
+
if (grantValue !== '*') {
|
|
30
|
+
context.report({
|
|
31
|
+
message,
|
|
32
|
+
node,
|
|
33
|
+
file
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
break
|
|
37
|
+
case 'object':
|
|
38
|
+
if (grantValue.length > 1 || grantValue[0] !== '*') {
|
|
39
|
+
context.report({
|
|
40
|
+
message,
|
|
41
|
+
node,
|
|
42
|
+
file
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
|
-
const VALID_PSEUDO_ROLES = ['authenticated-user', 'system-user', 'any']
|
|
4
|
-
|
|
5
3
|
module.exports = {
|
|
6
4
|
meta: {
|
|
7
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
@@ -50,7 +48,6 @@ module.exports = {
|
|
|
50
48
|
})
|
|
51
49
|
}
|
|
52
50
|
})
|
|
53
|
-
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
54
51
|
|
|
55
52
|
if (e['@restrict']) {
|
|
56
53
|
const node = context.getNode(e)
|
|
@@ -58,9 +55,8 @@ module.exports = {
|
|
|
58
55
|
for (const entry of e['@restrict']) {
|
|
59
56
|
const whereValues = entry.where
|
|
60
57
|
if (whereValues && typeof whereValues === 'string') {
|
|
61
|
-
let cxn
|
|
62
58
|
try {
|
|
63
|
-
|
|
59
|
+
cds.parse.expr(entry.where)
|
|
64
60
|
} catch (err) {
|
|
65
61
|
context.report({
|
|
66
62
|
message: 'Invalid `where` expression, CDS compilation failed.',
|
|
@@ -68,20 +64,6 @@ module.exports = {
|
|
|
68
64
|
file
|
|
69
65
|
})
|
|
70
66
|
}
|
|
71
|
-
if (cxn && cxn.xpr) {
|
|
72
|
-
const operator = cxn.xpr[1]
|
|
73
|
-
const role = cxn.xpr[2].ref
|
|
74
|
-
if (operator === '=') {
|
|
75
|
-
const isValidRole = role === '$user' || ROLES.includes(role)
|
|
76
|
-
if (!isValidRole) {
|
|
77
|
-
context.report({
|
|
78
|
-
message: `Invalid \`where\` expression, role ${role} not found.`,
|
|
79
|
-
node,
|
|
80
|
-
file
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
67
|
}
|
|
86
68
|
}
|
|
87
69
|
}
|
package/lib/rules/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const rules = {
|
|
|
5
5
|
'assoc2many-ambiguous-key': () => createRule(require('./assoc2many-ambiguous-key')),
|
|
6
6
|
'auth-no-empty-restrictions': () => createRule(require('./auth-no-empty-restrictions')),
|
|
7
7
|
'auth-use-requires': () => createRule(require('./auth-use-requires')),
|
|
8
|
+
'auth-restrict-grant-service': () => createRule(require('./auth-restrict-grant-service')),
|
|
8
9
|
'auth-valid-restrict-grant': () => createRule(require('./auth-valid-restrict-grant')),
|
|
9
10
|
'auth-valid-restrict-keys': () => createRule(require('./auth-valid-restrict-keys')),
|
|
10
11
|
'auth-valid-restrict-to': () => createRule(require('./auth-valid-restrict-to')),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { dirname } = require('path')
|
|
2
|
+
|
|
1
3
|
const cds = require('@sap/cds')
|
|
2
4
|
|
|
3
5
|
module.exports = {
|
|
@@ -11,7 +13,10 @@ module.exports = {
|
|
|
11
13
|
model: 'inferred'
|
|
12
14
|
},
|
|
13
15
|
create (context) {
|
|
14
|
-
|
|
16
|
+
let dir = context.getFilename()
|
|
17
|
+
dir = dirname(dir)
|
|
18
|
+
const { requires } = cds.env.for('cds', dir)
|
|
19
|
+
if (requires.db?.kind !== 'sqlite') return
|
|
15
20
|
|
|
16
21
|
return {
|
|
17
22
|
// > return standard eslint visitor callbacks registered to CSN kinds, types, ...
|
|
@@ -26,7 +31,7 @@ module.exports = {
|
|
|
26
31
|
if (srv && srv['@cds.external']) return
|
|
27
32
|
if (d.kind === 'entity' && d['@cds.persistence.skip'] === true) return
|
|
28
33
|
context.report({
|
|
29
|
-
message: `'${d.name}' is a reserved keyword in
|
|
34
|
+
message: `'${d.name}' is a reserved keyword in SQLite`,
|
|
30
35
|
node: context.getNode(d),
|
|
31
36
|
file: d.$location.file
|
|
32
37
|
})
|
|
@@ -14,7 +14,7 @@ module.exports = {
|
|
|
14
14
|
|
|
15
15
|
function checkNojoinDraftenabled (e) {
|
|
16
16
|
if (e['@odata.draft.enabled']) {
|
|
17
|
-
if (e
|
|
17
|
+
if (e?.query?.SELECT?.from?.join) {
|
|
18
18
|
context.report({
|
|
19
19
|
message: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
|
|
20
20
|
node: context.getNode(e),
|
package/lib/utils/createRule.js
CHANGED
|
@@ -144,8 +144,11 @@ function createReport (node, cdscontext, meta, create) {
|
|
|
144
144
|
|
|
145
145
|
if (model) {
|
|
146
146
|
model.forall((d) => {
|
|
147
|
+
d = (meta.model === 'inferred') ? sanitizeFileLocation(d) : d
|
|
148
|
+
const isValidLocation = (meta.model === 'parsed' && d.$location) ||
|
|
149
|
+
(meta.model === 'inferred' && d.$location?.file)
|
|
147
150
|
Object.entries(handlers)
|
|
148
|
-
.filter(([type, lazy]) => d.is(type))
|
|
151
|
+
.filter(([type, lazy]) => d.is(type) && isValidLocation)
|
|
149
152
|
.forEach(([lazy, handler]) => {
|
|
150
153
|
try {
|
|
151
154
|
handler(d)
|
|
@@ -161,6 +164,13 @@ function createReport (node, cdscontext, meta, create) {
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function sanitizeFileLocation (d) {
|
|
168
|
+
let parent = d
|
|
169
|
+
while (!parent.$location && parent.parent && !parent.parent.definitions) parent = d.parent
|
|
170
|
+
if (parent.$location) d.$location = parent.$location
|
|
171
|
+
return d
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
function extendContext (node, context, meta) {
|
|
165
175
|
if (!Cache.has('test')) {
|
|
166
176
|
const filePath = context.getFilename()
|
|
@@ -201,14 +211,15 @@ function extendContext (node, context, meta) {
|
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
|
|
214
|
+
const { parserServices } = context.sourceCode || context
|
|
204
215
|
cdscontext.getModel =
|
|
205
|
-
meta.model === 'inferred' ?
|
|
216
|
+
meta.model === 'inferred' ? parserServices.getInferredCsn : parserServices.getParsedCsn
|
|
206
217
|
cdscontext.getEnvironment = () => {
|
|
207
218
|
const options = context.options
|
|
208
219
|
return options && options[0] && options[0].environment ? options[0].environment : undefined
|
|
209
220
|
}
|
|
210
|
-
cdscontext.getLocation =
|
|
211
|
-
cdscontext.getNode = Object.keys(
|
|
221
|
+
cdscontext.getLocation = parserServices.getLocation
|
|
222
|
+
cdscontext.getNode = Object.keys(parserServices).length > 0 ? parserServices.getNode : () => node
|
|
212
223
|
return cdscontext
|
|
213
224
|
}
|
|
214
225
|
|
package/lib/utils/genDocs.js
CHANGED
|
@@ -13,6 +13,7 @@ const IS_WIN = os.platform() === 'win32'
|
|
|
13
13
|
const { exit } = require('process')
|
|
14
14
|
|
|
15
15
|
const constants = require('../constants')
|
|
16
|
+
const LOG = process.env.SILENT ? undefined : constants.log
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Generates custom rules documentation (markdown files)
|
|
@@ -45,7 +46,7 @@ module.exports = async (projectPath, customRulesDir, registry, prepareRelease =
|
|
|
45
46
|
? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
46
47
|
: getPackageVersion(registry)
|
|
47
48
|
if (versionInternal) {
|
|
48
|
-
|
|
49
|
+
LOG?.(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`)
|
|
49
50
|
const rulesInternal = getRules(docsPath, rulePath, testPath, versionInternal)
|
|
50
51
|
genDocFiles(rulesInternal, docsPath)
|
|
51
52
|
}
|
|
@@ -55,7 +56,7 @@ module.exports = async (projectPath, customRulesDir, registry, prepareRelease =
|
|
|
55
56
|
? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
56
57
|
: getPackageVersion(npmRegistry)
|
|
57
58
|
if (versionExternal) {
|
|
58
|
-
|
|
59
|
+
LOG?.(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`)
|
|
59
60
|
const rulesExternal = getRules(docsPath, rulePath, testPath, versionExternal, release)
|
|
60
61
|
genDocFiles(rulesExternal, docsPath, release)
|
|
61
62
|
}
|
|
@@ -64,7 +65,7 @@ module.exports = async (projectPath, customRulesDir, registry, prepareRelease =
|
|
|
64
65
|
const rules = getRules(docsPath, rulePath, testPath)
|
|
65
66
|
genDocFiles(rules, docsPath)
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
LOG?.('Done!')
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -166,12 +167,12 @@ function getPackageVersion (registry) {
|
|
|
166
167
|
})
|
|
167
168
|
.toString()
|
|
168
169
|
} catch (err) {
|
|
169
|
-
|
|
170
|
+
LOG?.(`Failed to connect to ${registry} - check your connection and try again.`)
|
|
170
171
|
exit(0)
|
|
171
172
|
}
|
|
172
173
|
const version = JSON.parse(result).version
|
|
173
174
|
if (!version) {
|
|
174
|
-
|
|
175
|
+
LOG?.(`Failed to get latest plugin version from ${registry} - check your connection and try again.`)
|
|
175
176
|
exit(0)
|
|
176
177
|
}
|
|
177
178
|
return version
|
|
@@ -198,7 +199,7 @@ function getRules (docsPath, rulePath, testPath, versionRequired = '0.0.0', rele
|
|
|
198
199
|
fs.writeFileSync(ruleVersionsPath, JSON.stringify(ruleVersions, null, 4), 'utf8')
|
|
199
200
|
}
|
|
200
201
|
if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
|
|
201
|
-
|
|
202
|
+
LOG?.(`${fileNumber}> preparing docs for ${ruleTestPath}`)
|
|
202
203
|
|
|
203
204
|
const details = ruleMeta.docs.description
|
|
204
205
|
const flavor = ruleMeta.model ? ruleMeta.model : constants.DEFAULT_RULE_FLAVOR
|
|
@@ -211,7 +212,7 @@ function getRules (docsPath, rulePath, testPath, versionRequired = '0.0.0', rele
|
|
|
211
212
|
let underConstruction = ''
|
|
212
213
|
if (!release && (version === 'TBD' || semver.satisfies(version, `>${versionRequired}`))) {
|
|
213
214
|
underConstruction = '🚧'
|
|
214
|
-
|
|
215
|
+
LOG?.(` > 🚧 Rule '${rule}' still under construction.\n`)
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
const isFixable = ['code', 'whitespace'].includes(fixable) ? '🔧' : ''
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.4",
|
|
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": [
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"eslint": ">=7"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
36
|
+
"node": ">=18"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|