@sap/eslint-plugin-cds 3.1.2 → 4.0.2

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 (42) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/LICENSE +15 -21
  3. package/lib/conf/all.js +2 -0
  4. package/lib/conf/experimental.js +1 -2
  5. package/lib/conf/index.js +17 -22
  6. package/lib/conf/js/all.js +8 -0
  7. package/lib/conf/js/recommended.js +8 -0
  8. package/lib/constants.js +7 -0
  9. package/lib/parser.js +53 -35
  10. package/lib/rules/assoc2many-ambiguous-key.js +1 -1
  11. package/lib/rules/auth-no-empty-restrictions.js +1 -1
  12. package/lib/rules/auth-restrict-grant-service.js +1 -1
  13. package/lib/rules/auth-use-requires.js +1 -1
  14. package/lib/rules/auth-valid-restrict-grant.js +1 -1
  15. package/lib/rules/auth-valid-restrict-keys.js +1 -1
  16. package/lib/rules/auth-valid-restrict-to.js +1 -1
  17. package/lib/rules/auth-valid-restrict-where.js +1 -1
  18. package/lib/rules/extension-restrictions.js +1 -1
  19. package/lib/rules/index.js +19 -23
  20. package/lib/rules/js/CdsHandlerRule.js +265 -0
  21. package/lib/rules/js/no-cross-service-import.js +69 -0
  22. package/lib/rules/js/no-deep-sap-cds-import.js +56 -0
  23. package/lib/rules/js/no-shared-handler-variable.js +73 -0
  24. package/lib/rules/js/types.d.ts +15 -0
  25. package/lib/rules/js/use-cql-select-template-strings.js +35 -0
  26. package/lib/rules/latest-cds-version.js +2 -0
  27. package/lib/rules/no-db-keywords.js +3 -2
  28. package/lib/rules/no-dollar-prefixed-names.js +67 -9
  29. package/lib/rules/no-java-keywords.js +3 -1
  30. package/lib/rules/no-join-on-draft.js +4 -1
  31. package/lib/rules/sql-cast-suggestion.js +46 -22
  32. package/lib/rules/sql-null-comparison.js +39 -35
  33. package/lib/rules/start-elements-lowercase.js +43 -39
  34. package/lib/rules/start-entities-uppercase.js +24 -34
  35. package/lib/rules/valid-csv-header.js +3 -2
  36. package/lib/utils/Cache.js +21 -18
  37. package/lib/utils/createRule.js +47 -44
  38. package/lib/utils/getConfigPath.js +1 -12
  39. package/lib/utils/{getProjectRootPath.js → projectRootPath.js} +18 -6
  40. package/lib/utils/rules.js +10 -9
  41. package/lib/utils/runRuleTester.js +16 -24
  42. package/package.json +5 -5
@@ -0,0 +1,265 @@
1
+ // TODO:
2
+ // - class extends require('@sap/cds').ApplicationService
3
+ // - class extends await import('@sap/cds').ApplicationService
4
+ /** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.Scope} Scope */
5
+ /** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.Variable} Variable */
6
+ /** @typedef {import('@sap/eslint-plugin-cds/lib/rules/js/types').CdsContextTracker.VariableType} VariableType */
7
+
8
+ 'use strict'
9
+
10
+ /**
11
+ * @param {string} name - name of the scope
12
+ * @returns {Scope}
13
+ */
14
+ const produceScope = name => ({ name, variables: [] })
15
+
16
+ const produceHandlerRegistration = ({call, handler}) => ({ call, handler })
17
+
18
+ // matches: @sap/cds, "@sap/cds", '@sap/cds', `@sap/cds`, but not @sap/cds-compiler etc
19
+ const isSapCds = name => Boolean(name?.match(/^\W*@sap\/cds\W*$/))
20
+
21
+ /**
22
+ * @param {VariableType} type - type of the variable
23
+ */
24
+ const produceVariable = ({name, type, isCdsVariable, original}) => ({ name, type, original, isCdsVariable })
25
+
26
+ // like: require('@sap/cds')
27
+ const isCdsRequire = node => Boolean(node?.type === 'CallExpression'
28
+ && node.callee.type === 'Identifier'
29
+ && node.callee.name === 'require'
30
+ && node.arguments.length === 1
31
+ && node.arguments[0].type === 'Literal'
32
+ && isSapCds(node.arguments[0].value))
33
+
34
+ // like: import ... from '@sap/cds'
35
+ const isCdsImport = node => isSapCds(node?.source?.value)
36
+
37
+ const isFunctionBody = node => ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression']
38
+ .includes(node.parent.type)
39
+
40
+ class CdsHandlerRule {
41
+ get isInsideCapService() { return this.capServiceStack.length > 0 }
42
+ get isInsideCapHandlerRegistration() { return this.capHandlerRegistrationStack.length > 0 }
43
+
44
+ constructor (context) {
45
+ this.context = context
46
+ /** @type {Scope[]} */
47
+ this.functionScopes = [ produceScope('<global>') ]
48
+ this.capServiceStack = [] // stack of class bodies. Should probably never be more than one...
49
+ this.capHandlerRegistrationStack = [] // stack of handler registration calls. Should probably never be more than one...
50
+ }
51
+
52
+ /**
53
+ * @param {string} varName - name of the variable
54
+ */
55
+ findDefinitionScope (varName) {
56
+ const scopes = this.functionScopes
57
+ for (let i = scopes.length - 1; i >= 0; i--) {
58
+ const scope = scopes[i]
59
+ const variable = scope.variables.find(variable => variable.name === varName)
60
+ if (variable) return {
61
+ scope,
62
+ variable,
63
+ isLocal: i === scopes.length - 1,
64
+ isGlobal: i === 0
65
+ }
66
+ }
67
+ return undefined
68
+ }
69
+
70
+ // ClassExpression or ClassDeclaration
71
+ isCdsServiceClass (node) {
72
+ const superClass = node.superClass
73
+ if (!superClass) return false // no extends clause
74
+ let name
75
+ switch (superClass.type) {
76
+ case 'MemberExpression': {
77
+ // like: class X extends cds.ApplicationService
78
+ name = superClass.object.name
79
+ // TODO: && is *Service?
80
+ break
81
+ }
82
+ case 'Identifier': {
83
+ // like: class X extends ApplicationService
84
+ name = superClass.name
85
+ break
86
+ }
87
+ }
88
+ const info = this.findDefinitionScope(name)
89
+ return info?.variable.isCdsVariable
90
+ }
91
+
92
+ /**
93
+ * @param {Variable} variable
94
+ */
95
+ addScopeVariable (variable) {
96
+ this.functionScopes.at(-1).variables.push(variable)
97
+ }
98
+
99
+ /**
100
+ * @abstract
101
+ */
102
+ // eslint-disable-next-line no-unused-vars
103
+ CAPHandlerRegistration (node) { /* abstract */ }
104
+
105
+ ClassBody (node) {
106
+ // by hooking into ClassBody and ascending to .parent, we capture declarations and expressions:
107
+ // like: module.exports = class X extends cds.ApplicationService (ClassExpression)
108
+ // like: class X extends cds.ApplicationService (ClassDeclaration)
109
+ if (this.isCdsServiceClass(node.parent)) {
110
+ this.capServiceStack.push(node)
111
+ }
112
+ }
113
+
114
+ 'ClassBody:exit'(node) {
115
+ if (this.capServiceStack.at(-1) === node) {
116
+ this.capServiceStack.pop()
117
+ }
118
+ }
119
+
120
+ CallExpression(node) {
121
+ if (!this.isInsideCapService) return
122
+ const { type, object, property } = node.callee
123
+ if (type === 'MemberExpression' && object.type === 'ThisExpression') {
124
+ // like: this.on('submitOrder', bar)
125
+ if (['before', 'on', 'after'].some(method => method === property.name)) {
126
+ const handler = node.arguments.at(-1)
127
+ this.capHandlerRegistrationStack.push(produceHandlerRegistration({
128
+ call: node,
129
+ handler
130
+ }))
131
+ // TODO: named references
132
+ this.CAPHandlerRegistration(handler)
133
+ }
134
+ }
135
+ }
136
+
137
+ 'CallExpression:exit'(node) {
138
+ if (this.capHandlerRegistrationStack.at(-1)?.call === node) {
139
+ this.capHandlerRegistrationStack.pop()
140
+ }
141
+ }
142
+
143
+ BlockStatement(node) {
144
+ if(isFunctionBody(node)) {
145
+ this.functionScopes.push(produceScope(
146
+ node.parent?.key?.name
147
+ ?? node.parent?.id?.name
148
+ ?? node.parent?.parent?.key?.name
149
+ ?? '<anonymous>'))
150
+ }
151
+ }
152
+
153
+ 'BlockStatement:exit'(node) {
154
+ if(isFunctionBody(node)) {
155
+ this.functionScopes.pop()
156
+ }
157
+ }
158
+
159
+ // the following visitors handle arrow functions, which can have a BlockStatement as body,
160
+ // OR just any single expression, like an Assignment, etc. This makes it very hard to
161
+ // determine when we are inside the body ArrowFunctionExpression from looking at the body,
162
+ // as we'd have to add a visitor for every expression type and check if it's a child of an ArrowFunctionExpression.
163
+ 'ArrowFunctionExpression > :not(BlockStatement)'() {
164
+ this.functionScopes.push(produceScope('<anonymous>'))
165
+ }
166
+ 'ArrowFunctionExpression > :not(BlockStatement):exit'() {
167
+ this.functionScopes.pop()
168
+ }
169
+
170
+ ImportDeclaration(node) {
171
+ const { specifiers } = node
172
+ const isCdsVariable = isCdsImport(node)
173
+ for (const specifier of specifiers) {
174
+ switch (specifier.type) {
175
+ case 'ImportNamespaceSpecifier':
176
+ // like: import * as x from y
177
+ // fallthrough
178
+ case 'ImportDefaultSpecifier':
179
+ // like: import x from y
180
+ this.addScopeVariable(produceVariable({
181
+ original: specifier.local.name,
182
+ name: specifier.local.name,
183
+ type: 'import',
184
+ isCdsVariable
185
+ }))
186
+ break
187
+ case 'ImportSpecifier':
188
+ // like: import { x, y as foo } from z
189
+ this.addScopeVariable(produceVariable({
190
+ original: specifier.imported.name,
191
+ name: specifier.local.name,
192
+ type: 'import',
193
+ isCdsVariable
194
+ }))
195
+ break
196
+ default:
197
+ throw new Error(`Unexpected specifier type: ${specifier.type}`)
198
+ }
199
+ }
200
+
201
+ }
202
+
203
+ VariableDeclarator({id, init, parent}) {
204
+ // like: const ... = require('@sap/cds')
205
+ const isCdsVariable = isCdsRequire(init)
206
+ switch (id.type) {
207
+ case 'Identifier':
208
+ // like: const x = y
209
+ this.addScopeVariable(produceVariable({
210
+ original: id.name,
211
+ name: id.name,
212
+ type: parent.kind,
213
+ isCdsVariable
214
+ }))
215
+ break
216
+ case 'ObjectPattern':
217
+ // like: const { x, y } = z
218
+ for (const { key, type, value } of id.properties) {
219
+ if (type === 'Property' && key.type === 'Identifier') {
220
+ this.addScopeVariable(produceVariable({
221
+ original: key.name,
222
+ type: parent.kind,
223
+ isCdsVariable,
224
+ name: value?.name
225
+ }))
226
+ }
227
+ }
228
+ break
229
+ }
230
+ }
231
+
232
+ /**
233
+ * ESLint expects an object literal with functions members when registering
234
+ * the visitor in create(context).
235
+ * This method transforms the class instance into such an object, retaining
236
+ * the base methods, as well as all methods defined in subclasses.
237
+ * Visitors are distinguished by their name starting with an uppercase letter.
238
+ * So you should only ever
239
+ * ```js
240
+ * return new MyCdsRule(context).asESLintVisitor()
241
+ * ```
242
+ * and not
243
+ * ```js
244
+ * return new MyCdsRule(context)
245
+ * ```
246
+ * in your rule definition, or else the visitor methods will not be called by ESL.
247
+ */
248
+ asESLintVisitor () {
249
+ let proto = Object.getPrototypeOf(this)
250
+ const visitors = {}
251
+ while (proto && proto !== Object.prototype) {
252
+ Object.getOwnPropertyNames(proto)
253
+ .filter(key => typeof this[key] === 'function' && /^[A-Z]/.test(key))
254
+ .forEach(key => {
255
+ visitors[key] = this[key].bind(this)
256
+ })
257
+ proto = Object.getPrototypeOf(proto) // Move up the prototype chain
258
+ }
259
+ return visitors
260
+ }
261
+ }
262
+
263
+ module.exports = {
264
+ CdsHandlerRule
265
+ }
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const { RULE_CATEGORIES } = require('../../constants')
4
+
5
+ // used as a pre-check to ensure the checks using RegExps
6
+ // with greedy matchers are not susceptible to ReDoS attacks
7
+ const MAX_INPUT_STRING_LENGTH = 1_000
8
+
9
+ /**
10
+ * @param {string} importPath
11
+ * @param {RuleContext} context
12
+ * @param {Node} node
13
+ */
14
+ function compareImportAndFilename (importPath, context, node) {
15
+ const currentFile = context.getFilename()
16
+ // ignore stdin
17
+ if (currentFile === '<input>') return
18
+ // ignore excessively long strings
19
+ if (importPath.length > MAX_INPUT_STRING_LENGTH || currentFile.length > MAX_INPUT_STRING_LENGTH) return
20
+ const [, typerModuleFq, typerModule] = /^#cds-models\/.*?((\w+)Service)$/.exec(importPath) ?? []
21
+ const [, fileNameFq, fileName] = /((\w+)-?[sS]ervice\.m?[jt]s)$/.exec(currentFile)
22
+ // typerModule === undefined -> not a service import (probably db-level-entity import)
23
+ if (typerModule && fileName && typerModule !== fileName) {
24
+ context.report({
25
+ node,
26
+ messageId: 'noCrossServiceImport',
27
+ data: {
28
+ from: typerModuleFq,
29
+ target: fileNameFq
30
+ }
31
+ })
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ meta: {
37
+ type: 'problem',
38
+ docs: {
39
+ recommended: true,
40
+ category: RULE_CATEGORIES.javascript,
41
+ description: 'Warn about imports from another service.'
42
+ },
43
+ schema: [],
44
+ messages: {
45
+ noCrossServiceImport: 'You are importing service-level entities from another service "{{from}}" inside the definition of service "{{target}}". This is likely an accidental cross-service import.',
46
+ },
47
+ hasSuggestions: false
48
+ },
49
+ create: context => ({
50
+ CallExpression(node) {
51
+ // look for: require('#cds-models/...')
52
+ if (node.callee.type !== 'Identifier') return
53
+ if (node.callee.name !== 'require') return
54
+ if (node.arguments.length !== 1) return
55
+ if (node.arguments[0].type !== 'Literal') return
56
+ compareImportAndFilename(node.arguments[0].value, context, node)
57
+ },
58
+
59
+ ImportDeclaration(node) {
60
+ // import ... from '#cds-models/...'
61
+ compareImportAndFilename(node.source.value, context, node)
62
+ },
63
+
64
+ ImportExpression(node) {
65
+ // await import('#cds-models/...')
66
+ compareImportAndFilename(node.source.value, context, node)
67
+ }
68
+ })
69
+ }
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+ const allowList = new Set([
3
+ // exception: not part of the facade and should be available to users this way
4
+ '@sap/cds/eslint.config.mjs'
5
+ ])
6
+
7
+ /**
8
+ * @param {string} importPath
9
+ * @param {RuleContext} context
10
+ * @param {Node} node
11
+ */
12
+ function checkImport (importPath, context, node) {
13
+ if (!importPath.startsWith('@sap/cds/')) return
14
+ if (allowList.has(importPath)) return
15
+ context.report({
16
+ node,
17
+ messageId: 'noDeepSapCdsImports',
18
+ data: { import: importPath }
19
+ })
20
+ }
21
+
22
+ module.exports = {
23
+ meta: {
24
+ type: 'problem',
25
+ docs: {
26
+ recommended: true,
27
+ category: 'JavaScript Validation',
28
+ description: 'Warn about deep imports from @sap/cds.'
29
+ },
30
+ schema: [],
31
+ messages: {
32
+ noDeepSapCdsImports: `"{{import}}" is a deep import. The API of @sap/cds is available only from its facade via 'require("@sap/cds")' or 'import ... from "@sap/cds"'.`,
33
+ },
34
+ hasSuggestions: false
35
+ },
36
+ create: context => ({
37
+ CallExpression(node) {
38
+ // look for: require('@sap/cds/...')
39
+ if (node.callee.type !== 'Identifier') return
40
+ if (node.callee.name !== 'require') return
41
+ if (node.arguments.length !== 1) return
42
+ if (node.arguments[0].type !== 'Literal') return
43
+ checkImport(node.arguments[0].value, context, node)
44
+ },
45
+
46
+ ImportDeclaration(node) {
47
+ // import ... from '@sap/cds/...'
48
+ checkImport(node.source.value, context, node)
49
+ },
50
+
51
+ ImportExpression(node) {
52
+ // await import('@sap/cds/...')
53
+ checkImport(node.source.value, context, node)
54
+ }
55
+ })
56
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ Use cases not yet covered:
3
+
4
+ //---------
5
+ INLINE EXTENSION
6
+ class FooService extends require('@sap/cds').ApplicationService { ... }
7
+
8
+ //---------
9
+ REFERENCED FUNCTION
10
+ function bad() { ... }
11
+
12
+ class ... {
13
+ this.on('', bad)
14
+ }
15
+
16
+ //---------
17
+ METHOD
18
+ class ... {
19
+ bad () {}
20
+
21
+ this.on('', this.bad)
22
+ }
23
+
24
+ //---------
25
+ IMPORTED FUNCTION
26
+ const { bad } = require('./bad')
27
+
28
+ class ... {
29
+ this.on('', bad)
30
+ }
31
+
32
+ //---------
33
+ NON-CLASS-BASED CDS SERVICE
34
+ cds.services['myService'].on('READ', 'Books', () => {})
35
+ */
36
+
37
+ 'use strict'
38
+
39
+ const { RULE_CATEGORIES } = require('../../constants')
40
+ const { CdsHandlerRule } = require('./CdsHandlerRule')
41
+
42
+ class NoSharedVariable extends CdsHandlerRule {
43
+ AssignmentExpression(node) {
44
+ if (!this.isInsideCapHandlerRegistration) return
45
+ const declaringScope = this.findDefinitionScope(node.left.name)
46
+ if (declaringScope?.isLocal === false) {
47
+ this.context.report({
48
+ node,
49
+ messageId: 'noSharedHandlerVariable',
50
+ data: {
51
+ definitionScope: declaringScope.scope.name
52
+ }
53
+ })
54
+ }
55
+ }
56
+ }
57
+
58
+ module.exports = {
59
+ meta: {
60
+ type: 'problem',
61
+ docs: {
62
+ recommended: true,
63
+ category: RULE_CATEGORIES.javascript,
64
+ description: 'Enforce that variables can not be used to share state between handlers.'
65
+ },
66
+ schema: [],
67
+ messages: {
68
+ noSharedHandlerVariable: 'Assignment to a non-local variable inside a CDS event handler (was declared in scope "{{definitionScope}}").'
69
+ },
70
+ hasSuggestions: true
71
+ },
72
+ create: context => new NoSharedVariable(context).asESLintVisitor()
73
+ }
@@ -0,0 +1,15 @@
1
+ export declare namespace CdsContextTracker {
2
+ type VariableType = 'let' | 'const' | 'var' | 'import'
3
+
4
+ type Variable = {
5
+ name: string,
6
+ original: string
7
+ type: VariableType,
8
+ isCdsVariable: boolean
9
+ }
10
+
11
+ type Scope = {
12
+ name: string,
13
+ variables: Variable[]
14
+ }
15
+ }
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const { RULE_CATEGORIES } = require('../../constants')
4
+ const { CdsHandlerRule } = require('./CdsHandlerRule')
5
+
6
+ class CqlSelectUseTemplateStrings extends CdsHandlerRule {
7
+ CallExpression(node) {
8
+ super.CallExpression(node)
9
+ if (node.callee?.name === 'SELECT' && node.arguments[0].type === 'TemplateLiteral') {
10
+ this.context.report({
11
+ node,
12
+ message: 'Do not use SELECT(`...`), which is prone to SQL injections.',
13
+ suggest: [{
14
+ desc: 'Use SELECT`...` instead',
15
+ fix: fixer => fixer.replaceText(node, `SELECT${this.context.getSourceCode().getText(node.arguments[0])}`)
16
+ }]
17
+ })
18
+ }
19
+ }
20
+ }
21
+
22
+ module.exports = {
23
+ meta: {
24
+ type: 'problem',
25
+ docs: {
26
+ recommended: true,
27
+ category: RULE_CATEGORIES.javascript,
28
+ description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
29
+ },
30
+ fixable: 'code',
31
+ schema: [],
32
+ hasSuggestions: true
33
+ },
34
+ create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
35
+ }
@@ -2,11 +2,13 @@
2
2
 
3
3
  const cp = require('child_process')
4
4
  const semver = require('semver')
5
+ const { RULE_CATEGORIES } = require('../constants')
5
6
 
6
7
  module.exports = {
7
8
  meta: {
8
9
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
9
10
  docs: {
11
+ category: RULE_CATEGORIES.environment,
10
12
  description: 'Checks whether the latest `@sap/cds` version is being used.',
11
13
  },
12
14
  type: 'suggestion',
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const cds = require('@sap/cds')
4
+ const { RULE_CATEGORIES } = require('../constants')
4
5
 
5
6
  // REVISIT: Replace by compiler-provided check
6
7
  const RESERVED = cds.compile.to.sql.sqlite
@@ -11,9 +12,9 @@ module.exports = {
11
12
  meta: {
12
13
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
13
14
  docs: {
15
+ category: RULE_CATEGORIES.model,
14
16
  description: 'Avoid using reserved SQL keywords.',
15
- recommended: true,
16
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
17
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
17
18
  },
18
19
  messages: {
19
20
  reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
@@ -1,29 +1,87 @@
1
1
  'use strict'
2
2
 
3
+ const { RULE_CATEGORIES } = require('../constants')
4
+
5
+ /**
6
+ * Util to check if an entity is part of an external service.
7
+ */
8
+ class ExternalServices {
9
+ hasExternalServices = false
10
+ externalServices = Object.create(null)
11
+
12
+ static create(model) {
13
+ return new ExternalServices(model)
14
+ }
15
+
16
+ constructor(model) {
17
+ for (const defName in model.definitions) {
18
+ const def = model.definitions[defName]
19
+ if (def?.kind === 'service' && def['@cds.external']) {
20
+ this.externalServices[defName] = true
21
+ this.hasExternalServices = true
22
+ }
23
+ }
24
+ }
25
+
26
+ isInExternalService(defName) {
27
+ if (!this.hasExternalServices)
28
+ return false // shortcut
29
+ const segments = defName.split('.')
30
+ for (let i = segments.length - 1; i >= 0; i--)
31
+ if (this.externalServices[segments.slice(0, i).join('.')])
32
+ return true
33
+ return false
34
+ }
35
+
36
+ }
37
+
3
38
  module.exports = {
4
39
  meta: {
5
40
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
6
41
  docs: {
42
+ category: RULE_CATEGORIES.model,
7
43
  description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
8
44
  recommended: true,
9
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-dollar-prefixed-names',
45
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
10
46
  },
11
47
  messages: {
12
48
  dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
13
49
  },
14
50
  type: 'problem'
15
51
  },
16
- create (context) {
17
- return { element: _check }
18
52
 
19
- function _check (d) {
20
- const srv = d._service || (d.parent && d.parent._service)
21
- if (srv && srv['@cds.external']) return
22
- if (d.name.startsWith('$')) {
53
+ create(context) {
54
+ const model = context.getModel()
55
+ if (!model?.definitions)
56
+ return
57
+
58
+ const externals = ExternalServices.create(model)
59
+
60
+ return function checkAllElementsForDollarPrefix() {
61
+ for (const defName in model.definitions) {
62
+ if (!externals.isInExternalService(defName))
63
+ checkElements(defName, model.definitions[defName])
64
+ }
65
+ }
66
+
67
+ function checkElements(defName, def) {
68
+ if (!Object.hasOwn(def,'elements') || !def.elements || typeof def.elements !== 'object')
69
+ return
70
+
71
+ for (const elementName in def.elements) {
72
+ const element = def.elements[elementName]
73
+ check(elementName, element)
74
+ if (element.elements)
75
+ checkElements(elementName, element)
76
+ }
77
+ }
78
+
79
+ function check(name, def) {
80
+ if (name.startsWith('$')) {
23
81
  context.report({
24
82
  messageId: 'dollarPrefix',
25
- data: { name: d.name },
26
- node: context.getNode(d)
83
+ data: { name },
84
+ loc: context.getLocation(name, def, model),
27
85
  })
28
86
  }
29
87
  }
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { RULE_CATEGORIES } = require('../constants')
3
4
  // Check that Java keywords are not used as identifiers unless they have
4
5
  // a Java-specific annotation that renames/ignores them. This avoids issues
5
6
  // later on in code-generation of CAP Java classes.
@@ -26,8 +27,9 @@ module.exports = {
26
27
  meta: {
27
28
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
28
29
  docs: {
30
+ category: RULE_CATEGORIES.model,
29
31
  description: 'Reject reserved Java keywords as CDS identifiers.',
30
- recommended: true
32
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
31
33
  },
32
34
  type: 'problem',
33
35
  model: 'inferred',
@@ -1,12 +1,15 @@
1
1
  'use strict'
2
2
 
3
+ const { RULE_CATEGORIES } = require('../constants')
4
+
3
5
  module.exports = {
4
6
  meta: {
5
7
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
6
8
  docs: {
9
+ category: RULE_CATEGORIES.model,
7
10
  description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
8
11
  recommended: true,
9
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-join-on-draft',
12
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',
10
13
  },
11
14
  messages: {
12
15
  draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',