@sap/eslint-plugin-cds 3.2.0 → 4.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.
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ const { RULE_CATEGORIES } = require('../../constants')
4
+ const { CdsHandlerRule } = require('./CdsHandlerRule')
5
+
6
+ const WELL_KNOWN_EVENTS = [
7
+ 'CREATE', 'READ', 'UPDATE', 'UPSERT','DELETE',
8
+ 'INSERT','SELECT',
9
+ 'POST','GET','PUT','PATCH',
10
+ 'NEW', 'CANCEL', 'EDIT', 'SAVE' // draft related
11
+ ]
12
+
13
+ class CaseSensitiveWellKnownEvents extends CdsHandlerRule {
14
+ CAPHandlerRegistration(node) {
15
+ const first = node.parent?.arguments?.[0]
16
+ if (!first) return
17
+ const currentEventName = first.value ?? ''
18
+ const currentEventNameUpper = currentEventName.toUpperCase()
19
+ if (WELL_KNOWN_EVENTS.includes(currentEventNameUpper) && currentEventName !== currentEventNameUpper) {
20
+ const quoteType = ['"', '\'', '`'].includes(first.raw?.[0]) ? first.raw?.[0] : '"'
21
+ this.context.report({
22
+ node: first,
23
+ messageId: 'incorrectEventNameCase',
24
+ data: {
25
+ currentEventName: currentEventName,
26
+ properEventName: currentEventNameUpper
27
+ },
28
+ suggest: [{
29
+ desc: 'Change event casing to upper case',
30
+ fix: fixer => fixer.replaceText(first, `${quoteType}${currentEventNameUpper}${quoteType}`)
31
+ }]
32
+ })
33
+ }
34
+ }
35
+ }
36
+
37
+ module.exports = {
38
+ meta: {
39
+ type: 'problem',
40
+ docs: {
41
+ recommended: true,
42
+ category: RULE_CATEGORIES.javascript,
43
+ description: 'Make sure well-known events are used with proper casing.'
44
+ },
45
+ fixable: 'code',
46
+ schema: [],
47
+ messages: {
48
+ incorrectEventNameCase: 'Found an event registration for event "{{currentEventName}}", which is likely supposed to be "{{properEventName}}".'
49
+ },
50
+ hasSuggestions: true
51
+ },
52
+ create: context => new CaseSensitiveWellKnownEvents(context).asESLintVisitor()
53
+ }
@@ -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,211 @@
1
+ /*
2
+ Use cases not yet covered:
3
+
4
+ //---------
5
+ INLINE EXTENSION
6
+ class FooService extends require('@sap/cds').ApplicationService { ... }
7
+
8
+ //---------
9
+ METHOD
10
+ class ... {
11
+ bad () {}
12
+
13
+ this.on('', this.bad)
14
+ }
15
+
16
+ //---------
17
+ IMPORTED FUNCTION
18
+ const { bad } = require('./bad')
19
+
20
+ class ... {
21
+ this.on('', bad)
22
+ }
23
+
24
+ //---------
25
+ NON-CLASS-BASED CDS SERVICE
26
+ cds.services['myService'].on('READ', 'Books', () => {})
27
+ */
28
+
29
+ 'use strict'
30
+
31
+ const { RULE_CATEGORIES } = require('../../constants')
32
+ const { CdsHandlerRule } = require('./CdsHandlerRule')
33
+
34
+ /**
35
+ * @param {string | undefined} t
36
+ */
37
+ function isHandlerType(t) {
38
+ // match "import('@sap/cds').CRUDEventHandler.Before" etc.
39
+ return ['Before', 'On', 'After'].some(handlerType => t?.match(new RegExp(`import\\s?\\(.@sap\\/cds.\\)\\.CRUDEventHandler.${handlerType}`)))
40
+ }
41
+
42
+ class NoSharedVariable extends CdsHandlerRule {
43
+ /**
44
+ * Functions that modify variables that are not locally declared.
45
+ * They are stored by name.
46
+ * Note: this is not fully fail proof, as the functions are stored in a flat fashion,
47
+ * rather than maintaining the scope they are declared in.
48
+ * This could lead to false positives if a function with the same name is declared in multiple scopes.
49
+ * @type {Record<string, Scope>}
50
+ */
51
+ #suspiciousFunctions = {}
52
+ /**
53
+ * nodes of handler registrations like:
54
+ * ```js
55
+ * this.on('READ', 'Books', handler)
56
+ * // ^^^^^^^
57
+ * ```
58
+ * as they reference the handler by name, its definition may come later in the code
59
+ * due to hoisting.
60
+ * We check them later in `Program:exit()`.
61
+ */
62
+ #pendingInspections = []
63
+
64
+ /**
65
+ * Typedef JSDoc to look up local aliases.
66
+ * ```js
67
+ * @typedef {Bar} Foo
68
+ * ```
69
+ * becomes
70
+ * ```js
71
+ * {Foo: 'Bar'}
72
+ * ```
73
+ * @type {Record<string, import('estree').Comment & { type: string }>}
74
+ */
75
+ #typeDefinitions = {}
76
+
77
+ /**
78
+ * Type JSDoc that matches the estree definition of a Comment,
79
+ * plus an additional `type` property that contains the type of the variable.
80
+ * Local type aliases are resolved to the actual type using #typeDefinitions.
81
+ *
82
+ * @type {Array<import('estree').Comment & { type: string }>}
83
+ */
84
+ #typeDeclarations = []
85
+
86
+ #handlerDefinitionDepth = 0
87
+
88
+ /**
89
+ * When we are inside a function that has explicitly been annotated as
90
+ * a handler function via a @type JSDoc annotation.
91
+ */
92
+ get isInsideExplicitCapHandlerDefinition() {
93
+ return this.#handlerDefinitionDepth > 0
94
+ }
95
+
96
+ addCapHandlerRegistration(registration) {
97
+ super.addCapHandlerRegistration(registration)
98
+
99
+ if (registration.handler.type === 'Identifier') {
100
+ this.#pendingInspections.push(registration)
101
+ }
102
+ }
103
+
104
+ Program() {
105
+ const comments = this.context.sourceCode.getAllComments()
106
+ this.#typeDefinitions = Object.fromEntries(comments
107
+ .map(comment => {
108
+ const [, type, name] = comment.value.match(/^\*\s?@typedef\s?\{(.*)\}\s?(\w*)/) ?? []
109
+ return type && name ? [ name, {...comment, type} ] : null
110
+ })
111
+ .filter(Boolean))
112
+ this.#typeDeclarations = comments
113
+ .map(comment => {
114
+ const match = comment.value.match(/^\*\s?@type\s?\{(.*)\}/)?.[1]
115
+ if (!match) return null
116
+ const type = this.#typeDefinitions[match]?.type ?? match
117
+ return type ? { ...comment, type } : null
118
+ })
119
+ .filter(Boolean)
120
+ }
121
+
122
+ 'Program:exit'() {
123
+ for (const node of this.#pendingInspections) {
124
+ const { scope } = this.#suspiciousFunctions[node.handler.name] ?? {}
125
+ if (scope) {
126
+ this.context.report({
127
+ node: node.handler,
128
+ messageId: 'noSharedHandlerVariable',
129
+ data: {
130
+ definitionScope: scope.name
131
+ }
132
+ })
133
+ }
134
+ }
135
+ }
136
+
137
+ #enterFunctionDefinition(node) {
138
+ // find a JDoc type comment, that is either on the line before, or in the same line,
139
+ // but up to three columns to the left. The latter condition covers three cases:
140
+ // 1. TYPEDEFFUNC -- (no space between the comment and start of function) -> distance = 0
141
+ // 2. TYPEDEF FUNC-- (one space between the comment and start of function) -> distance = 1
142
+ // 3. TYPEDEF(FUNC) -- (no space after typedef, followed by an opening parenthesis) -> distance = 1
143
+ // 3. TYPEDEF (FUNC) -- (one space after typedef and an opening parenthesis) -> distance = 2
144
+ // Note: this will fail if we have empty lines between the function declaration and the JSDoc.
145
+ // Also when users have more spaces or other outlandish formatting styles.
146
+ const type = this.#typeDeclarations.find(({loc}) =>
147
+ loc.end.line === node.loc.start.line - 1
148
+ || loc.end.line === node.loc.start.line && [0,1,2].includes(node.loc.start.column - loc.end.column)
149
+ )
150
+ // if the function is explicitly declared as handler, we check it.
151
+ // If the function is not explicitly declared as handler, but a surrounding function is (this.handlerDefinitionDepth > 0),
152
+ // we check it too.
153
+ if (isHandlerType(type?.type) || this.isInsideExplicitCapHandlerDefinition) this.#handlerDefinitionDepth++
154
+ }
155
+
156
+ #exitFunctionDefinition() {
157
+ this.#handlerDefinitionDepth = Math.max(0, this.#handlerDefinitionDepth - 1)
158
+ }
159
+
160
+ // () => ...
161
+ ArrowFunctionExpression(node) { this.#enterFunctionDefinition(node) }
162
+ 'ArrowFunctionExpression:exit'() { this.#exitFunctionDefinition() }
163
+ // function() { ... }
164
+ FunctionExpression(node) { this.#enterFunctionDefinition(node) }
165
+ 'FunctionExpression:exit'() { this.#exitFunctionDefinition() }
166
+ // function f () { ... }
167
+ FunctionDeclaration(node) { this.#enterFunctionDefinition(node) }
168
+ 'FunctionDeclaration:exit'() { this.#exitFunctionDefinition()}
169
+
170
+ AssignmentExpression(node) {
171
+ if (this.isInsideCapHandlerRegistration || this.isInsideExplicitCapHandlerDefinition) {
172
+ // like: this.on('READ', 'Books', () => { variable = 42 })
173
+ const declaringScope = this.findDefinitionScope(node.left.name)
174
+ if (declaringScope?.isLocal === false) {
175
+ this.context.report({
176
+ node,
177
+ messageId: 'noSharedHandlerVariable',
178
+ data: {
179
+ definitionScope: declaringScope.scope.name
180
+ }
181
+ })
182
+ }
183
+ } else if (this.functionScopes.length > 0) {
184
+ // not inside a handler registration, but in a function that may be referenced in a handler registration
185
+ // like: this.on('READ', 'Books', handler)
186
+ // as functions are hoisted and can be referenced before their definition, we just collect the names of suspicious functions
187
+ // and check them in Program:exit when we have inspected all functions.
188
+ const declaringScope = this.findDefinitionScope(node.left.name)
189
+ if (declaringScope?.isLocal === false) {
190
+ this.#suspiciousFunctions[this.functionScopes.at(-1).name] = declaringScope
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ module.exports = {
197
+ meta: {
198
+ type: 'problem',
199
+ docs: {
200
+ recommended: true,
201
+ category: RULE_CATEGORIES.javascript,
202
+ description: 'Enforce that variables can not be used to share state between handlers.'
203
+ },
204
+ schema: [],
205
+ messages: {
206
+ noSharedHandlerVariable: 'Assignment to a non-local variable inside a CDS event handler (was declared in scope "{{definitionScope}}").'
207
+ },
208
+ hasSuggestions: true
209
+ },
210
+ create: context => new NoSharedVariable(context).asESLintVisitor()
211
+ }
@@ -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,64 @@
1
+ 'use strict'
2
+
3
+ const { RULE_CATEGORIES } = require('../../constants')
4
+ const { CdsHandlerRule } = require('./CdsHandlerRule')
5
+
6
+ const isCqlClauseStart = node => ['SELECT', 'UPDATE', 'INSERT', 'DELETE', 'UPSERT'].includes(node.name)
7
+
8
+ /**
9
+ * ESLint goes through member functions in reverse order:
10
+ * x.y.z -> z, y, x
11
+ * so we can not track when we
12
+ * enter a CQL clause, but instead have to check the chain of ancestors for each member
13
+ * recursively. If we find the topmost function call is a CQL clause (SELECT, UPDATE, etc.),
14
+ * none of the member functions in the chain are allowed to use untagged template strings.
15
+ */
16
+ const isInCqlClause = node => {
17
+ if (!node) return false
18
+ if (node.type === 'CallExpression' && isCqlClauseStart(node.callee)) return true
19
+ if (node.type === 'TaggedTemplateExpression' && isCqlClauseStart(node.tag)) return true
20
+ // f(...) and f`...` have slightly different structure, the former has .callee, the latter has .tag
21
+ // -> use the first that is available to ascend through the call chain
22
+ return isInCqlClause(node.callee?.object ?? node.tag?.object)
23
+ }
24
+
25
+ class CqlSelectUseTemplateStrings extends CdsHandlerRule {
26
+ CallExpression(node) {
27
+ super.CallExpression(node)
28
+ const [arg] = node.arguments ?? []
29
+ if (arg?.type !== 'TemplateLiteral') return
30
+ if (arg.expressions.length === 0) return // no expressions in the template string, so no SQL injection risk
31
+ if (!isInCqlClause(node)) return
32
+
33
+ const [functionName, prefix] = node.callee.type === 'MemberExpression'
34
+ // for ….where`...` we need to use the full preceding expression in the following replacement
35
+ ? [node.callee.property?.name, this.context.getSourceCode().getText(node.callee)]
36
+ // for SELECT`...` we can use the function name directly
37
+ : [node.callee.name, node.callee.name]
38
+ this.context.report({
39
+ node,
40
+ message: 'Do not use {{functionName}}(`...`) inside CQL statements, which is prone to SQL injections.',
41
+ data: { functionName },
42
+ suggest: [{
43
+ desc: 'Use {{functionName}}`...` instead of {{functionName}}(`...`)',
44
+ data: { functionName },
45
+ fix: fixer => fixer.replaceText(node, `${prefix}${this.context.getSourceCode().getText(arg)}`)
46
+ }]
47
+ })
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ meta: {
53
+ type: 'problem',
54
+ docs: {
55
+ recommended: true,
56
+ category: RULE_CATEGORIES.javascript,
57
+ description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
58
+ },
59
+ fixable: 'code',
60
+ schema: [],
61
+ hasSuggestions: true
62
+ },
63
+ create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
64
+ }
@@ -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,6 +12,7 @@ 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
17
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
16
18
  },
@@ -1,9 +1,45 @@
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
45
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
@@ -13,17 +49,39 @@ module.exports = {
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,6 +27,7 @@ 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
32
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
31
33
  },
@@ -1,9 +1,12 @@
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
12
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',