@sap/eslint-plugin-cds 3.0.5 → 3.1.1

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/lib/api/index.js +4 -4
  4. package/lib/conf/all.js +17 -17
  5. package/lib/conf/experimental.js +12 -0
  6. package/lib/conf/index.js +12 -3
  7. package/lib/conf/recommended.js +13 -14
  8. package/lib/constants.js +2 -0
  9. package/lib/index.js +2 -1
  10. package/lib/parser.js +10 -1
  11. package/lib/rules/assoc2many-ambiguous-key.js +40 -11
  12. package/lib/rules/auth-no-empty-restrictions.js +38 -10
  13. package/lib/rules/auth-restrict-grant-service.js +29 -29
  14. package/lib/rules/auth-use-requires.js +27 -15
  15. package/lib/rules/auth-valid-restrict-grant.js +138 -82
  16. package/lib/rules/auth-valid-restrict-keys.js +34 -18
  17. package/lib/rules/auth-valid-restrict-to.js +57 -106
  18. package/lib/rules/auth-valid-restrict-where.js +44 -43
  19. package/lib/rules/extension-restrictions.js +11 -3
  20. package/lib/rules/index.js +5 -1
  21. package/lib/rules/latest-cds-version.js +5 -4
  22. package/lib/rules/no-db-keywords.js +14 -5
  23. package/lib/rules/no-dollar-prefixed-names.js +9 -2
  24. package/lib/rules/no-java-keywords.js +181 -0
  25. package/lib/rules/no-join-on-draft.js +9 -3
  26. package/lib/rules/sql-cast-suggestion.js +19 -15
  27. package/lib/rules/sql-null-comparison.js +60 -0
  28. package/lib/rules/start-elements-lowercase.js +6 -2
  29. package/lib/rules/start-entities-uppercase.js +12 -5
  30. package/lib/rules/valid-csv-header.js +33 -13
  31. package/lib/types.d.ts +4 -4
  32. package/lib/utils/Cache.js +4 -2
  33. package/lib/utils/Colors.js +2 -0
  34. package/lib/utils/LintError.js +17 -0
  35. package/lib/utils/createRule.js +160 -134
  36. package/lib/utils/csnTraversal.js +163 -0
  37. package/lib/utils/findFuzzy.js +21 -12
  38. package/lib/utils/getConfigPath.js +4 -2
  39. package/lib/utils/getConfiguredFileTypes.js +2 -0
  40. package/lib/utils/getFileExtensions.js +2 -0
  41. package/lib/utils/getProjectRootPath.js +53 -15
  42. package/lib/utils/isConfiguredFileType.js +8 -3
  43. package/lib/utils/rules.js +15 -9
  44. package/lib/utils/runRuleTester.js +69 -36
  45. package/package.json +1 -1
  46. package/lib/utils/genDocs.js +0 -346
package/CHANGELOG.md CHANGED
@@ -6,6 +6,55 @@ 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
+ ## [3.1.1] - 2024-10-08
10
+
11
+ ### Changed
12
+
13
+ - `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.
15
+
16
+ ### Fixed
17
+
18
+ - `auth-restrict-grant-service` can now handle invalid values for `@restrict`
19
+ - `auth-use-requires` now handles `null` values for `@restrict.grant.to`
20
+ - `auth-valid-restrict-to` is now more robust against invalid properties such as `__proto__`
21
+ and reduces the number of false positives
22
+ - `auth-valid-restrict-where` now handles and reports invalid value `@restrict: [{where: null}]`
23
+ - `auth-no-empty-restrictions` now handles invalid value `@restrict: [null]`
24
+
25
+
26
+ ## [3.1.0] - 2024-09-26
27
+
28
+ ### Added
29
+
30
+ - api:
31
+ + rules now have a `name` property, containing the rule name
32
+ + new exported property `parser`
33
+ - rules: there is now an `experimental` rule group, containing new rules that can be tested
34
+ - new experimental rules were added:
35
+ + `@sap/cds/sql-null-comparison`
36
+ + `@sap/cds/no-java-keywords`
37
+ - `auth-valid-restrict-grant` now proposes '*' when incorrect `@restrict.grant` value 'any' is used
38
+
39
+ ### Removed
40
+
41
+ - api: `genDocs` was removed
42
+
43
+ ### Fixed
44
+
45
+ - cli: Running `eslint` on the command line now runs `inferred` rules again
46
+ - `start-entities-uppercase` no longer reports false positives for elements
47
+ - Typescript errors in `lib/types.d.ts` were fixed
48
+ - Rule property `hasSuggestions: true` was removed from rules that did not have suggestions
49
+ - Custom rule tests using `runRuleTester` did not catch errors in files inside `valid/`.
50
+ Tests can now also be run with other test runners such as `mocha` and `node --test` instead of just `jest`
51
+ - `auth-` lint rules have been reworked to reduce the number of false positives and negatives
52
+ + `auth-valid-restrict-where` no longer runs in quadratic time and now handles "expressions as annotation values"
53
+ + `auth-no-empty-restrictions` now runs for actions and functions, too
54
+ + `auth-use-requires` will not propose `@requires` anymore, if the `@restrict` has a `where` condition
55
+ + `auth-valid-restrict-grants` incorrectly proposed to use `WRITE` instead of other events; it no longer crashes for invalid value types and now runs on all CSN artifacts
56
+ + `auth-valid-restrict-keys` has improved reporting about misspelled vs unknown properties; it now runs on all CSN artifacts
57
+
9
58
  ## [3.0.5] - 2024-09-11
10
59
 
11
60
  ### Fixed
package/README.md CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
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.
5
5
 
6
- See the [CDS Linting documentation](https://cap.cloud.sap/docs/tools/#cds-lint) for more details, or jump directly to a complete [list of rules](https://cap.cloud.sap/docs/tools/#cds-lint-rules). CAP provides a set of recommended rules. On top, you can create your own, application-specific rules.
6
+ See the [CDS Linting documentation](https://cap.cloud.sap/docs/tools/cds-lint/) for more details, or jump directly to a complete [list of rules](https://cap.cloud.sap/docs/tools/cds-lint/#cds-lint-rules). CAP provides a set of recommended rules. On top, you can create your own, application-specific rules.
package/lib/api/index.js CHANGED
@@ -4,23 +4,23 @@
4
4
  * Our custom ESLint plugin API should:
5
5
  * - Expose 'createRule' and 'runRuleTester' to
6
6
  * support the addition of *custom* CDS Lint rules at runtime
7
- * - Expose 'getFileExtensions', and 'genDocs' for usage in
7
+ * - Expose 'getFileExtensions' for usage in
8
8
  * 'cds lint' client (@sap/cds-dk)
9
9
  * - Expose 'parserPath' for CDS Lint rule unit tests with ESLint's ruleTester
10
10
  */
11
11
 
12
12
  const runRuleTester = require('../utils/runRuleTester')
13
13
  const createRule = require('../utils/createRule')
14
- const genDocs = require('../utils/genDocs')
15
14
  const getConfigPath = require('../utils/getConfigPath')
16
15
  const getConfiguredFileTypes = require('../utils/getConfiguredFileTypes')
17
16
  const parserPath = require.resolve('../parser')
17
+ const parser = require('../parser')
18
18
 
19
19
  module.exports = {
20
20
  runRuleTester,
21
21
  createRule,
22
- genDocs,
23
22
  getConfigPath,
24
23
  getFileExtensions: getConfiguredFileTypes,
25
- parserPath
24
+ parserPath,
25
+ parser,
26
26
  }
package/lib/conf/all.js CHANGED
@@ -1,21 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
- '@sap/cds/assoc2many-ambiguous-key': 2,
5
- '@sap/cds/auth-no-empty-restrictions': 2,
6
- '@sap/cds/auth-use-requires': 2,
7
- '@sap/cds/auth-restrict-grant-service': 2,
8
- '@sap/cds/auth-valid-restrict-grant': 2,
9
- '@sap/cds/auth-valid-restrict-keys': 2,
10
- '@sap/cds/auth-valid-restrict-to': 2,
11
- '@sap/cds/auth-valid-restrict-where': 2,
12
- '@sap/cds/latest-cds-version': 2,
13
- '@sap/cds/no-db-keywords': 2,
14
- '@sap/cds/no-dollar-prefixed-names': 2,
15
- '@sap/cds/no-join-on-draft': 2,
16
- '@sap/cds/sql-cast-suggestion': 2,
17
- '@sap/cds/start-elements-lowercase': 2,
18
- '@sap/cds/start-entities-uppercase': 2,
19
- '@sap/cds/valid-csv-header': 2,
20
- '@sap/cds/extension-restrictions': 2
4
+ '@sap/cds/assoc2many-ambiguous-key': 'error',
5
+ '@sap/cds/auth-no-empty-restrictions': 'error',
6
+ '@sap/cds/auth-use-requires': 'error',
7
+ '@sap/cds/auth-restrict-grant-service': 'error',
8
+ '@sap/cds/auth-valid-restrict-grant': 'error',
9
+ '@sap/cds/auth-valid-restrict-keys': 'error',
10
+ '@sap/cds/auth-valid-restrict-to': 'error',
11
+ '@sap/cds/auth-valid-restrict-where': 'error',
12
+ '@sap/cds/latest-cds-version': 'error',
13
+ '@sap/cds/no-db-keywords': 'error',
14
+ '@sap/cds/no-dollar-prefixed-names': 'error',
15
+ '@sap/cds/no-join-on-draft': 'error',
16
+ '@sap/cds/sql-cast-suggestion': 'error',
17
+ '@sap/cds/start-elements-lowercase': 'error',
18
+ '@sap/cds/start-entities-uppercase': 'error',
19
+ '@sap/cds/valid-csv-header': 'error',
20
+ '@sap/cds/extension-restrictions': 'error',
21
21
  }
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ // Experimental Rules
4
+ // ------------------
5
+ // New rules that we want to publish, but don't activate per default.
6
+ // We want to give users the chance to test them, before moving them
7
+ // to "all" or even "recommended".
8
+
9
+ module.exports = {
10
+ '@sap/cds/sql-null-comparison': 'warn',
11
+ '@sap/cds/no-java-keywords': 'error',
12
+ }
package/lib/conf/index.js CHANGED
@@ -1,10 +1,17 @@
1
- const path = require('path')
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
2
4
  const { FILES, GLOBALS, PLUGIN_NAME } = require('../constants')
3
5
  const { parserPath } = require('../api')
4
6
 
5
- function _createConfig (plugin, configName, legacy = false) {
7
+ /**
8
+ * @param {object} plugin Plugin implementation used for new configuration layout.
9
+ * @param {string} configName
10
+ * @param {boolean} [isLegacy]
11
+ */
12
+ function _createConfig (plugin, configName, isLegacy = false) {
6
13
  const config = require(path.join(__dirname, configName))
7
- if (legacy) {
14
+ if (isLegacy) {
8
15
  return {
9
16
  root: true,
10
17
  globals: GLOBALS,
@@ -19,6 +26,7 @@ function _createConfig (plugin, configName, legacy = false) {
19
26
  }
20
27
  }
21
28
  return {
29
+ name: `@sap/cds/${configName}`,
22
30
  languageOptions: {
23
31
  globals: GLOBALS,
24
32
  parser: require(parserPath)
@@ -37,6 +45,7 @@ module.exports = function (plugin) {
37
45
  return {
38
46
  all: _createConfig(plugin, 'all'),
39
47
  recommended: _createConfig(plugin, 'recommended'),
48
+ experimental: _createConfig(plugin, 'experimental'),
40
49
  // Legacy configs (for backwards compatibility)
41
50
  'all-legacy': _createConfig(plugin, 'all', true),
42
51
  'recommended-legacy': _createConfig(plugin, 'recommended', true)
@@ -1,18 +1,17 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
- '@sap/cds/assoc2many-ambiguous-key': 2,
5
- '@sap/cds/auth-no-empty-restrictions': 2,
6
- '@sap/cds/auth-use-requires': 1,
7
- '@sap/cds/auth-restrict-grant-service': 2,
8
- '@sap/cds/auth-valid-restrict-grant': 1,
9
- '@sap/cds/auth-valid-restrict-keys': 1,
10
- '@sap/cds/auth-valid-restrict-to': 1,
11
- '@sap/cds/auth-valid-restrict-where': 1,
12
- '@sap/cds/no-db-keywords': 1,
13
- '@sap/cds/no-dollar-prefixed-names': 1,
14
- '@sap/cds/no-join-on-draft': 1,
15
- '@sap/cds/sql-cast-suggestion': 1,
16
- '@sap/cds/valid-csv-header': 1,
17
- '@sap/cds/extension-restrictions': 2
4
+ '@sap/cds/assoc2many-ambiguous-key': 'error',
5
+ '@sap/cds/auth-no-empty-restrictions': 'error',
6
+ '@sap/cds/auth-use-requires': 'warn',
7
+ '@sap/cds/auth-restrict-grant-service': 'error',
8
+ '@sap/cds/auth-valid-restrict-grant': 'warn',
9
+ '@sap/cds/auth-valid-restrict-keys': 'warn',
10
+ '@sap/cds/auth-valid-restrict-to': 'warn',
11
+ '@sap/cds/auth-valid-restrict-where': 'warn',
12
+ '@sap/cds/no-dollar-prefixed-names': 'warn',
13
+ '@sap/cds/no-join-on-draft': 'warn',
14
+ '@sap/cds/sql-cast-suggestion': 'warn',
15
+ '@sap/cds/valid-csv-header': 'warn',
16
+ '@sap/cds/extension-restrictions': 'error',
18
17
  }
package/lib/constants.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  /**
2
4
  * This file is used to store/share any constants for this plugin:
3
5
  * - DEFAULT_RULE_CATEGORY: Default rule category (must be one of plugin's rule categories)
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
- // index.js
1
+ 'use strict'
2
+
2
3
  /**
3
4
  * ## Plugin structure
4
5
  * This is the main entry point of our [custom ESLint plugin](https://eslint.org/docs/developer-guide/working-with-plugins),
package/lib/parser.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  /**
2
4
  * Custom ESLint parser:
3
5
  * https://eslint.org/docs/developer-guide/working-with-custom-parsers
@@ -28,7 +30,7 @@ module.exports = {
28
30
  const messages = []
29
31
  try {
30
32
  compiledModel = cds.parse(code)
31
- } catch (_err) {
33
+ } catch {
32
34
  // Do nothing
33
35
  }
34
36
  if (compiledModel) {
@@ -124,6 +126,7 @@ module.exports = {
124
126
  let loc
125
127
  if (obj) {
126
128
  let name = obj.name
129
+ // TODO: 'action'/'function' not correct for bound action/function
127
130
  if (['action', 'entity', 'function', 'service'].includes(obj.kind)) {
128
131
  name = splitDefName(obj).name
129
132
  }
@@ -143,7 +146,9 @@ module.exports = {
143
146
 
144
147
  /**
145
148
  * Generates dummy AST with just single Program node
149
+ *
146
150
  * @param code Parse file contents
151
+ * @param {object} [loc]
147
152
  * @returns AST
148
153
  */
149
154
  function createProgramAST (code, loc) {
@@ -168,6 +173,10 @@ function createProgramAST (code, loc) {
168
173
  }
169
174
  }
170
175
 
176
+ /**
177
+ * @param {object} dictFiles
178
+ * @param options
179
+ */
171
180
  function compileModelFromDict (dictFiles, options) {
172
181
  let reflectedModel
173
182
  const messages = []
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const cds = require('@sap/cds')
2
4
 
3
5
  /** @type {import('../types').Rule} */
@@ -9,7 +11,11 @@ module.exports = {
9
11
  description:
10
12
  'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
11
13
  category: 'Model Validation',
12
- recommended: true
14
+ recommended: true,
15
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/assoc2many-ambiguous-key',
16
+ },
17
+ messages: {
18
+ ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`,
13
19
  },
14
20
  type: 'problem',
15
21
  model: 'inferred'
@@ -22,14 +28,23 @@ module.exports = {
22
28
  const m = context.getModel()
23
29
  if (!m) return
24
30
  if (m && m.definitions) {
25
- csnOdata = cds.compile.for.odata(m)
26
- const csnOdataLinked = cds.linked(csnOdata)
27
- associationCardinalityFlaw(csnOdataLinked, context)
31
+ try {
32
+ csnOdata = cds.compile.for.odata(m)
33
+ const csnOdataLinked = cds.linked(csnOdata)
34
+ associationCardinalityFlaw(csnOdataLinked, context)
35
+ } catch {
36
+ // FIXME: there are currently too many issues with this rule, e.g.
37
+ // assumptions that properties exist, etc.
38
+ }
28
39
  }
29
40
  }
30
41
  }
31
42
  }
32
43
 
44
+ /**
45
+ * @param {object} csn
46
+ * @param {CDSRuleContext} context
47
+ */
33
48
  function associationCardinalityFlaw (csn, context) {
34
49
  processEntity(csn, (definition, sourceEntity, sourceAlias) => {
35
50
  let refCardinalityMult = false
@@ -52,7 +67,7 @@ function associationCardinalityFlaw (csn, context) {
52
67
  refPlainElement = true
53
68
  }
54
69
  },
55
- (column) => {
70
+ column => {
56
71
  if (
57
72
  definition.keys &&
58
73
  Object.keys(definition.keys).length === 1 &&
@@ -65,7 +80,8 @@ function associationCardinalityFlaw (csn, context) {
65
80
  const keyLoc = context.getLocation(keyName, key, csn)
66
81
  const colName = column.as ? column.as : column.name
67
82
  context.report({
68
- message: `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`,
83
+ messageId: 'ambiguous',
84
+ data: { name: definition.name, 'column-name': colName, 'key-name': keyName },
69
85
  loc: keyLoc,
70
86
  file: key.$location.file
71
87
  })
@@ -75,8 +91,12 @@ function associationCardinalityFlaw (csn, context) {
75
91
  })
76
92
  }
77
93
 
94
+ /**
95
+ * @param {object} csn
96
+ * @param {Function} eachCallback
97
+ */
78
98
  function processEntity (csn, eachCallback) {
79
- Object.keys(csn.definitions).forEach((name) => {
99
+ Object.keys(csn.definitions).forEach(name => {
80
100
  if (name.startsWith('localized.')) {
81
101
  return
82
102
  }
@@ -99,7 +119,7 @@ function processEntity (csn, eachCallback) {
99
119
  } else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
100
120
  // Join
101
121
  sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
102
- definition.query.SELECT.from.args.forEach((arg) => {
122
+ definition.query.SELECT.from.args.forEach(arg => {
103
123
  sourceAlias.push({
104
124
  from: arg.ref.join('_'),
105
125
  as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
@@ -114,16 +134,25 @@ function processEntity (csn, eachCallback) {
114
134
  })
115
135
  }
116
136
 
137
+ /**
138
+ * @param {object} csn
139
+ * @param {object} definition
140
+ * @param {object} sourceEntity
141
+ * @param {string} sourceAlias
142
+ * @param {Function} beforeCallback
143
+ * @param {Function} eachCallback
144
+ * @param {Function} afterCallback
145
+ */
117
146
  function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
118
- definition.query.SELECT.columns.forEach((column) => {
147
+ definition.query.SELECT.columns.forEach(column => {
119
148
  if (column.ref && column.ref.length > 1) {
120
149
  let refEntity = sourceEntity
121
150
  let refAlias = sourceAlias
122
151
  beforeCallback()
123
- column.ref.forEach((ref) => {
152
+ column.ref.forEach(ref => {
124
153
  ref = ref.id || ref
125
154
  // Alias
126
- const matchAlias = refAlias.find((alias) => {
155
+ const matchAlias = refAlias.find(alias => {
127
156
  return alias.as === ref
128
157
  })
129
158
  let refElement
@@ -1,4 +1,6 @@
1
- const LABELS = ['@restrict', '@requires']
1
+ 'use strict'
2
+
3
+ const AUTH_ANNOTATIONS = [ '@restrict', '@requires' ]
2
4
 
3
5
  module.exports = {
4
6
  meta: {
@@ -6,12 +8,11 @@ module.exports = {
6
8
  docs: {
7
9
  description: '`@restrict` and `@requires` must not be empty.',
8
10
  category: 'Model Validation',
9
- recommended: true
11
+ recommended: true,
12
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-no-empty-restrictions',
10
13
  },
11
- hasSuggestions: true,
12
14
  messages: {
13
- InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
14
- ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
15
+ missingRestriction: 'No explicit restrictions provided on {{kind}} `{{name}}` at `{{label}}`.',
15
16
  },
16
17
  type: 'problem',
17
18
  model: 'inferred'
@@ -19,15 +20,17 @@ module.exports = {
19
20
  create (context) {
20
21
  return {
21
22
  entity: checkRestrictions,
22
- service: checkRestrictions
23
+ service: checkRestrictions,
24
+ action: checkRestrictions,
25
+ function: checkRestrictions,
23
26
  }
24
27
 
25
28
  function checkRestrictions (e) {
26
- for (const l of LABELS) {
27
- const invalid = (typeof e[l] === 'object' && e[l].length === 0) || (typeof e[l] === 'string' && e[l] === '')
28
- if (invalid) {
29
+ for (const anno of AUTH_ANNOTATIONS) {
30
+ if (isEmptyRestriction(e[anno])) {
29
31
  context.report({
30
- message: `No explicit restrictions provided on ${e.kind} \`${e.name}\` at \`${l}\`.`,
32
+ messageId: 'missingRestriction',
33
+ data: { kind: e.kind, name: e.name, label: anno },
31
34
  node: context.getNode(e),
32
35
  file: e.$location.file
33
36
  })
@@ -36,3 +39,28 @@ module.exports = {
36
39
  }
37
40
  }
38
41
  }
42
+
43
+ /**
44
+ * Checks if the given annotation value is an empty restriction.
45
+ * Examples which would return `true`:
46
+ * ```
47
+ * @requires: ''
48
+ * @requires: ['']
49
+ * @restrict: [ { to: '' } ]
50
+ * @restrict: [ { to: [''] } ]
51
+ * ```
52
+ *
53
+ * @param {*} obj
54
+ * @returns {boolean}
55
+ */
56
+ function isEmptyRestriction(obj) {
57
+ if (typeof obj === 'string')
58
+ return obj === ''
59
+ if (Array.isArray(obj))
60
+ return obj.length === 0 || obj.some(isEmptyRestriction)
61
+ if (typeof obj === 'object') {
62
+ // handle `null` as non-empty (i.e. ignore)
63
+ return obj && isEmptyRestriction(obj.to)
64
+ }
65
+ return false
66
+ }
@@ -1,10 +1,16 @@
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: '`@restrict.grant` on service level and for bound/unbound actions and functions is limited to grant: \'*\'',
6
8
  category: 'Model Validation',
7
- recommended: true
9
+ recommended: true,
10
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-restrict-grant-service',
11
+ },
12
+ messages: {
13
+ limitedGrant: `The grant value provided in @restrict is limited to '*' for {{kind}} '{{name}}'`,
8
14
  },
9
15
  type: 'problem',
10
16
  model: 'inferred'
@@ -16,36 +22,30 @@ module.exports = {
16
22
  service: checkRestrictGrant
17
23
  }
18
24
 
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
- }
25
+ function checkRestrictGrant(def) {
26
+ if (!Array.isArray(def['@restrict']))
27
+ return
28
+
29
+ const node = context.getNode(def)
30
+ const file = def.$location.file
31
+ const data = { kind: def.kind, name: def.name }
32
+
33
+ for (const entry of def['@restrict']) {
34
+ if (entry?.grant !== undefined) {
35
+
36
+ if (typeof entry.grant === 'string') {
37
+ if (entry.grant !== '*')
38
+ context.report({ messageId: 'limitedGrant', data, node, file })
39
+
40
+ } else if (Array.isArray(entry.grant)) {
41
+ if (entry.grant.length === 0 || !entry.grant.some(val => val === '*'))
42
+ context.report({ messageId: 'limitedGrant', data, node, file })
43
+
44
+ } else {
45
+ // invalid grant value; ignored by this rule
47
46
  }
48
47
  }
48
+
49
49
  }
50
50
  }
51
51
  }
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  module.exports = {
2
4
  meta: {
3
5
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
@@ -5,12 +7,11 @@ module.exports = {
5
7
  description: 'Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events.',
6
8
  category: 'Model Validation',
7
9
  recommended: true,
8
- version: '2.4.1'
10
+ version: '2.4.1',
11
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-use-requires',
9
12
  },
10
- hasSuggestions: true,
11
13
  messages: {
12
- InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
13
- ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
14
+ useRequires: 'Use `@requires` instead of `@restrict.to` at {{kind}} `{{name}}`.'
14
15
  },
15
16
  type: 'problem',
16
17
  model: 'inferred'
@@ -18,22 +19,33 @@ module.exports = {
18
19
  create (context) {
19
20
  return {
20
21
  service: checkRestrict,
21
- action: checkRestrict
22
+ action: checkRestrict,
23
+ function: checkRestrict,
22
24
  }
23
25
 
24
26
  function checkRestrict (e) {
25
- if (e && e['@restrict'] && typeof e['@restrict'] === 'object') {
26
- for (const entry of e['@restrict']) {
27
- const keys = Object.keys(entry)
28
- if (keys.includes('to') && keys.includes('grant') && entry.grant === '*') {
29
- context.report({
30
- message: `Use \`@requires\` instead of \`@restrict.to\` at ${e.kind} \`${e.name}\`.`,
31
- node: context.getNode(e),
32
- file: e.$location.file
33
- })
34
- }
27
+ if (!Array.isArray(e?.['@restrict']))
28
+ return
29
+
30
+ for (const entry of e['@restrict']) {
31
+ // Scenario: `@restrict: [ { to: 'Foo', grant: '*' } ]`
32
+ // There must be no `where` condition, as otherwise it wouldn't be equivalent
33
+ // to `@requires`. `to` must not be `null`, as `@requires: null` is not the
34
+ // same as `@restrict: [{to:null}]`.
35
+ // See https://cap.cloud.sap/docs/guides/security/authorization#supported-combinations-with-cds-resources
36
+ // for documentation.
37
+ if (entry?.to !== undefined && entry.to !== null &&
38
+ (entry.grant === '*' || !entry.grant) && entry.where === undefined) {
39
+ context.report({
40
+ messageId: 'useRequires',
41
+ data: { kind: e.kind, name: e.name },
42
+ node: context.getNode(e),
43
+ file: e.$location.file
44
+ })
45
+ break // max one report per annotation
35
46
  }
36
47
  }
37
48
  }
49
+
38
50
  }
39
51
  }