@sap/eslint-plugin-cds 3.0.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +39 -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 +14 -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 +36 -10
  13. package/lib/rules/auth-restrict-grant-service.js +19 -20
  14. package/lib/rules/auth-use-requires.js +25 -15
  15. package/lib/rules/auth-valid-restrict-grant.js +137 -81
  16. package/lib/rules/auth-valid-restrict-keys.js +34 -18
  17. package/lib/rules/auth-valid-restrict-to.js +67 -60
  18. package/lib/rules/auth-valid-restrict-where.js +31 -44
  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 +15 -7
  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 +13 -7
  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,9 +6,48 @@ 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.0] - 2024-09-26
10
+
11
+ ### Added
12
+
13
+ - api:
14
+ + rules now have a `name` property, containing the rule name
15
+ + new exported property `parser`
16
+ - rules: there is now an `experimental` rule group, containing new rules that can be tested
17
+ - new experimental rules were added:
18
+ + `@sap/cds/sql-null-comparison`
19
+ + `@sap/cds/no-java-keywords`
20
+ - `auth-valid-restrict-grant` now proposes '*' when incorrect `@restrict.grant` value 'any' is used
21
+
22
+ ### Removed
23
+
24
+ - api: `genDocs` was removed
25
+
26
+ ### Fixed
27
+
28
+ - cli: Running `eslint` on the command line now runs `inferred` rules again
29
+ - `start-entities-uppercase` no longer reports false positives for elements
30
+ - Typescript errors in `lib/types.d.ts` were fixed
31
+ - Rule property `hasSuggestions: true` was removed from rules that did not have suggestions
32
+ - Custom rule tests using `runRuleTester` did not catch errors in files inside `valid/`.
33
+ Tests can now also be run with other test runners such as `mocha` and `node --test` instead of just `jest`
34
+ - `auth-` lint rules have been reworked to reduce the number of false positives and negatives
35
+ + `auth-valid-restrict-where` no longer runs in quadratic time and now handles "expressions as annotation values"
36
+ + `auth-no-empty-restrictions` now runs for actions and functions, too
37
+ + `auth-use-requires` will not propose `@requires` anymore, if the `@restrict` has a `where` condition
38
+ + `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
39
+ + `auth-valid-restrict-keys` has improved reporting about misspelled vs unknown properties; it now runs on all CSN artifacts
40
+
41
+ ## [3.0.5] - 2024-09-11
42
+
43
+ ### Fixed
44
+
45
+ - Inferred rules did not run when executed via `eslint` or `cds lint`.
46
+
9
47
  ## [3.0.4] - 2024-06-19
10
48
 
11
49
  ### Changed
50
+
12
51
  - Internal refactorings
13
52
 
14
53
  ## [3.0.3] - 2024-05-08
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,18 @@
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-db-keywords': 'warn',
13
+ '@sap/cds/no-dollar-prefixed-names': 'warn',
14
+ '@sap/cds/no-join-on-draft': 'warn',
15
+ '@sap/cds/sql-cast-suggestion': 'warn',
16
+ '@sap/cds/valid-csv-header': 'warn',
17
+ '@sap/cds/extension-restrictions': 'error',
18
18
  }
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,26 @@ 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
+ return isEmptyRestriction(obj.to)
63
+ return false
64
+ }
@@ -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'
@@ -23,26 +29,19 @@ module.exports = {
23
29
  for (const entry of d['@restrict']) {
24
30
  if (Object.keys(entry).includes('grant')) {
25
31
  const grantValue = entry.grant
26
- const message = `The grant value provided in @restrict is limited to '*' for ${d.kind} '${d.name}'`
32
+ const messageId = 'limitedGrant'
33
+ const data = { kind: d.kind, name: d.name }
27
34
  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
35
+ case 'string':
36
+ if (grantValue !== '*') {
37
+ context.report({ messageId, data, node, file })
38
+ }
39
+ break
40
+ case 'object':
41
+ if (grantValue.length > 1 || grantValue[0] !== '*') {
42
+ context.report({ messageId, data, node, file })
43
+ }
44
+ break
46
45
  }
47
46
  }
48
47
  }
@@ -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,31 @@ 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 (!e?.['@restrict'] || !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`.
34
+ // See https://cap.cloud.sap/docs/guides/security/authorization#supported-combinations-with-cds-resources
35
+ // for documentation.
36
+ if (entry?.to !== undefined && (entry.grant === '*' || !entry.grant) && entry.where === undefined) {
37
+ context.report({
38
+ messageId: 'useRequires',
39
+ data: { kind: e.kind, name: e.name },
40
+ node: context.getNode(e),
41
+ file: e.$location.file
42
+ })
43
+ return // max one report per annotation
35
44
  }
36
45
  }
37
46
  }
47
+
38
48
  }
39
49
  }