@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 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
- [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
3
- [![Sync Files Workflow](https://github.tools.sap/cap/eslint-plugin-cds/actions/workflows/sync.yml/badge.svg)](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,
@@ -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
- cxn = cds.parse.expr(entry.where)
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
  }
@@ -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
- const { db = { kind: 'sql' } } = cds.env.requires
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 ${db.kind.toUpperCase()}`,
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.query.SELECT.from.join) {
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),
@@ -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' ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn
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 = context.parserServices.getLocation
211
- cdscontext.getNode = Object.keys(context.parserServices).length > 0 ? context.parserServices.getNode : () => node
221
+ cdscontext.getLocation = parserServices.getLocation
222
+ cdscontext.getNode = Object.keys(parserServices).length > 0 ? parserServices.getNode : () => node
212
223
  return cdscontext
213
224
  }
214
225
 
@@ -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
- console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`)
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
- console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`)
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
- console.log('Done!')
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
- console.log(`Failed to connect to ${registry} - check your connection and try again.`)
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
- console.log(`Failed to get latest plugin version from ${registry} - check your connection and try again.`)
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
- console.log(`${fileNumber}> preparing docs for ${ruleTestPath}`)
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
- console.log(` > 🚧 Rule '${rule}' still under construction.\n`)
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.1",
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": ">=14"
36
+ "node": ">=18"
37
37
  }
38
- }
38
+ }