@sap/eslint-plugin-cds 3.1.2 → 3.2.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.
package/CHANGELOG.md CHANGED
@@ -2,9 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- This project adheres to [Semantic Versioning](http://semver.org/).
5
+ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
7
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
8
+
9
+ ## [3.2.0] - 2025-03-03
10
+
11
+ ### Changed
12
+
13
+ - Rules `@sap/cds/sql-null-comparison` and `@sap/cds/no-java-keywords` are moved from the `experimental` rule set to `all`.
14
+
15
+ ### Fixed
16
+
17
+ - Rules `@sap/cds/sql-null-comparison` will not warn about `!= null`, as it may be supported by future CDS compiler versions.
18
+ - Some rules had `docs` meta property `recommended: true`, but were not part of the recommended rules list.
19
+ - When determining a CDS project's root directory, we now consider package.json's with `@sap/cds` as `devDependency` or `peerDependency`
8
20
 
9
21
  ## [3.1.2] - 2024-10-31
10
22
 
package/lib/conf/all.js CHANGED
@@ -18,4 +18,6 @@ module.exports = {
18
18
  '@sap/cds/start-entities-uppercase': 'error',
19
19
  '@sap/cds/valid-csv-header': 'error',
20
20
  '@sap/cds/extension-restrictions': 'error',
21
+ '@sap/cds/sql-null-comparison': 'warn',
22
+ '@sap/cds/no-java-keywords': 'warn',
21
23
  }
@@ -7,6 +7,5 @@
7
7
  // to "all" or even "recommended".
8
8
 
9
9
  module.exports = {
10
- '@sap/cds/sql-null-comparison': 'warn',
11
- '@sap/cds/no-java-keywords': 'error',
10
+ // '@sap/cds/my-rule': 'warn',
12
11
  }
package/lib/parser.js CHANGED
@@ -11,58 +11,65 @@
11
11
  * (parserOptions).
12
12
  */
13
13
  const cds = require('@sap/cds')
14
- const Cache = require('./utils/Cache')
14
+ const { globalCache } = require('./utils/Cache')
15
15
  const LOG = cds.debug('lint:plugin')
16
16
  const colors = require('./utils/Colors')
17
17
  const { splitDefName } = require('./utils/rules')
18
18
  const packageJson = require('../package.json')
19
19
 
20
+ const newLineRegEx = /\r\n?|\n/g
21
+
20
22
  module.exports = {
21
23
  meta: {
22
24
  name: packageJson.name,
23
25
  version: packageJson.version
24
26
  },
25
- parse: function (code, options) {
26
- return module.exports.parseForESLint(code, options).ast
27
+ parse(code, parserOptions) {
28
+ return module.exports.parseForESLint(code, parserOptions).ast
27
29
  },
28
- parseForESLint: function (code) {
30
+ // See https://eslint.org/docs/latest/extend/custom-parsers#parseforeslint-return-object
31
+ // eslint-disable-next-line no-unused-vars
32
+ parseForESLint(code, parserOptions) {
29
33
  return {
30
34
  ast: createProgramAST(code),
31
35
  services: {
32
36
  getParsedCsn: function () {
37
+ const compileOptions = {
38
+ messages: [],
39
+ }
33
40
  let compiledModel
34
41
  let reflectedModel
35
- const messages = []
36
42
  try {
37
- compiledModel = cds.parse(code)
43
+ compiledModel = cds.parse(code, compileOptions)
38
44
  } catch {
39
45
  // Do nothing
40
46
  }
41
47
  if (compiledModel) {
42
48
  try {
43
49
  reflectedModel = cds.linked(compiledModel)
44
- if (messages) {
45
- reflectedModel.messages = messages
50
+ if (compileOptions.messages) {
51
+ reflectedModel.messages = compileOptions.messages
46
52
  }
47
53
  } catch (err) {
48
- LOG && LOG(colors.red + 'ERROR:' + colors.reset, err)
49
- LOG && LOG('COMPILED', compiledModel)
50
- LOG && LOG('REFLECTED', reflectedModel)
54
+ LOG?.(colors.red + 'ERROR:' + colors.reset, err)
55
+ LOG?.('COMPILED', compiledModel)
56
+ LOG?.('REFLECTED', reflectedModel)
51
57
  }
52
58
  }
53
59
  return reflectedModel
54
60
  },
61
+
55
62
  getInferredCsn: function () {
56
- const rootPath = Cache.get('rootpath')
57
- if (Cache.has('test')) {
58
- return Cache.get(`model:${rootPath}`)
63
+ const rootPath = globalCache.get('rootpath')
64
+ if (globalCache.has('test')) {
65
+ return globalCache.get(`model:${rootPath}`)
59
66
  }
60
67
  let compiledModel
61
68
  let reflectedModel
62
69
  cds.resolve.cache = {}
63
70
 
64
- if (!Cache.has(`model:${rootPath}`) && rootPath) {
65
- const roots = Cache.get(`roots:${rootPath}`)
71
+ if (!globalCache.has(`model:${rootPath}`) && rootPath) {
72
+ const roots = globalCache.get(`roots:${rootPath}`)
66
73
  const messages = []
67
74
  if (roots) {
68
75
  try {
@@ -72,27 +79,28 @@ module.exports = {
72
79
  locations: true,
73
80
  messages
74
81
  })
75
- Cache.remove('errRootModel')
82
+ globalCache.remove('errRootModel')
76
83
  } catch (err) {
77
- Cache.set('errRootModel', err)
84
+ // TODO: Only catch Compile Errors?
85
+ globalCache.set('errRootModel', err)
78
86
  }
79
87
  if (compiledModel) {
80
88
  reflectedModel = cds.linked(compiledModel)
81
- Cache.set(`model:${Cache.get('rootpath')}`, reflectedModel)
89
+ globalCache.set(`model:${globalCache.get('rootpath')}`, reflectedModel)
82
90
  if (messages) {
83
91
  reflectedModel.messages = messages
84
92
  }
85
93
  }
86
94
  }
87
95
  } else {
88
- reflectedModel = Cache.get(`model:${rootPath}`)
96
+ reflectedModel = globalCache.get(`model:${rootPath}`)
89
97
  }
90
98
  return reflectedModel
91
99
  },
92
100
  updateInferredCsn: compileModelFromDict,
93
101
  getEnvironment: function () {
94
- const options = Cache.get('options')
95
- return (options && options[0] && options[0].environment) ? options[0].environment : undefined
102
+ const options = globalCache.get('options')
103
+ return options?.[0]?.environment
96
104
  },
97
105
  getLocation: function (name, obj, model) {
98
106
  let loc
@@ -146,27 +154,37 @@ module.exports = {
146
154
  }
147
155
  },
148
156
  createProgramAST,
149
- compileModelFromDict
157
+ compileModelFromDict,
150
158
  }
151
159
 
152
160
  /**
153
- * Generates dummy AST with just single Program node
161
+ * Generates dummy AST with just single Program node.
154
162
  *
155
163
  * @param code Parse file contents
156
164
  * @param {object} [loc]
157
- * @returns AST
165
+ * @returns ESLint AST
158
166
  */
159
167
  function createProgramAST (code, loc) {
160
- loc = loc || {
161
- start: {
162
- line: 1,
163
- column: 0
164
- },
165
- end: {
166
- line: 1,
167
- column: 0
168
+ if (!loc && code.length) {
169
+ const newLines = [...code.matchAll(newLineRegEx)]
170
+ const endColumn = newLines.length ? (code.length - newLines.at(-1).index) : code.length
171
+ loc = {
172
+ start: {
173
+ line: 1,
174
+ column: 1
175
+ },
176
+ end: {
177
+ line: newLines.length + 1,
178
+ column: endColumn
179
+ },
180
+ }
181
+ } else if (!loc) {
182
+ loc = {
183
+ start: { line: 0, column: 0 },
184
+ end: { line: 0, column: 0 },
168
185
  }
169
186
  }
187
+
170
188
  return {
171
189
  type: 'Program',
172
190
  body: [],
@@ -12,7 +12,7 @@ module.exports = {
12
12
  'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
13
13
  category: 'Model Validation',
14
14
  recommended: true,
15
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/assoc2many-ambiguous-key',
15
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/assoc2many-ambiguous-key',
16
16
  },
17
17
  messages: {
18
18
  ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`,
@@ -9,7 +9,7 @@ module.exports = {
9
9
  description: '`@restrict` and `@requires` must not be empty.',
10
10
  category: 'Model Validation',
11
11
  recommended: true,
12
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-no-empty-restrictions',
12
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-no-empty-restrictions',
13
13
  },
14
14
  messages: {
15
15
  missingRestriction: 'No explicit restrictions provided on {{kind}} `{{name}}` at `{{label}}`.',
@@ -7,7 +7,7 @@ module.exports = {
7
7
  description: '`@restrict.grant` on service level and for bound/unbound actions and functions is limited to grant: \'*\'',
8
8
  category: 'Model Validation',
9
9
  recommended: true,
10
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-restrict-grant-service',
10
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-restrict-grant-service',
11
11
  },
12
12
  messages: {
13
13
  limitedGrant: `The grant value provided in @restrict is limited to '*' for {{kind}} '{{name}}'`,
@@ -8,7 +8,7 @@ module.exports = {
8
8
  category: 'Model Validation',
9
9
  recommended: true,
10
10
  version: '2.4.1',
11
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-use-requires',
11
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-use-requires',
12
12
  },
13
13
  messages: {
14
14
  useRequires: 'Use `@requires` instead of `@restrict.to` at {{kind}} `{{name}}`.'
@@ -20,7 +20,7 @@ module.exports = {
20
20
  description: '`@restrict.grant` must have valid values.',
21
21
  category: 'Model Validation',
22
22
  recommended: true,
23
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-grant',
23
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-grant',
24
24
  },
25
25
  messages: {
26
26
  invalidType: 'Invalid type for grant value. Must either be string or array of strings.',
@@ -13,7 +13,7 @@ module.exports = {
13
13
  description: '`@restrict` must not have properties besides `to`, `grant`, and `where`.',
14
14
  category: 'Model Validation',
15
15
  recommended: true,
16
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-keys',
16
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-keys',
17
17
  },
18
18
  messages: {
19
19
  misspelledProperty: "Misspelled or unknown property '{{invalid}}'. Did you mean '{{candidates}}'?",
@@ -9,7 +9,7 @@ module.exports = {
9
9
  description: '`@restrict.to` must have valid values.',
10
10
  category: 'Model Validation',
11
11
  recommended: true,
12
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-to',
12
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-to',
13
13
  },
14
14
  messages: {
15
15
  invalidType: 'Invalid type for value of `@restrict.to`. Must either be string or array of strings.',
@@ -9,7 +9,7 @@ module.exports = {
9
9
  description: '`@restrict.where` must have valid values.',
10
10
  category: 'Model Validation',
11
11
  recommended: true,
12
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-where',
12
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-where',
13
13
  },
14
14
  severity: 'error',
15
15
  messages: {
@@ -10,7 +10,7 @@ const rule = module.exports = {
10
10
  description: 'Extensions must not violate restrictions set by the extended SaaS app.',
11
11
  category: 'Model Validation',
12
12
  recommended: true,
13
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/extension-restrictions',
13
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/extension-restrictions',
14
14
  },
15
15
  hasSuggestions: false,
16
16
  type: 'problem',
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const Cache = require('../utils/Cache')
3
+ const { globalCache } = require('../utils/Cache')
4
4
  const createRule = require('../utils/createRule')
5
5
 
6
6
  const rules = {
@@ -25,6 +25,6 @@ const rules = {
25
25
  'extension-restrictions': () => createRule(require('./extension-restrictions'))
26
26
  }
27
27
 
28
- Cache.set('rules', rules)
28
+ globalCache.set('rules', rules)
29
29
 
30
30
  module.exports = rules
@@ -12,8 +12,7 @@ module.exports = {
12
12
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
13
13
  docs: {
14
14
  description: 'Avoid using reserved SQL keywords.',
15
- recommended: true,
16
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
15
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-db-keywords',
17
16
  },
18
17
  messages: {
19
18
  reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
@@ -6,7 +6,7 @@ module.exports = {
6
6
  docs: {
7
7
  description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
8
8
  recommended: true,
9
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-dollar-prefixed-names',
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-dollar-prefixed-names',
10
10
  },
11
11
  messages: {
12
12
  dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
@@ -27,7 +27,7 @@ module.exports = {
27
27
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
28
28
  docs: {
29
29
  description: 'Reject reserved Java keywords as CDS identifiers.',
30
- recommended: true
30
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
31
31
  },
32
32
  type: 'problem',
33
33
  model: 'inferred',
@@ -6,7 +6,7 @@ module.exports = {
6
6
  docs: {
7
7
  description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
8
8
  recommended: true,
9
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-join-on-draft',
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-join-on-draft',
10
10
  },
11
11
  messages: {
12
12
  draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
@@ -6,7 +6,7 @@ module.exports = {
6
6
  docs: {
7
7
  description: 'Should make suggestions for possible missing SQL casts.',
8
8
  recommended: true,
9
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-cast-suggestion',
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-cast-suggestion',
10
10
  },
11
11
  type: 'suggestion',
12
12
  messages: {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { forEachXprInDefinition } = require('../utils/csnTraversal')
4
4
 
5
- const invalidComparisonOperators = [ '=', '!=', '<>' ]
5
+ const invalidComparisonOperators = [ '=', '<>' ]
6
6
 
7
7
  module.exports = {
8
8
  meta: {
@@ -12,12 +12,12 @@ module.exports = {
12
12
  category: 'Model Validation',
13
13
  recommended: false,
14
14
  // TODO: Add documentation
15
- // url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-null-comparison',
15
+ // url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
16
16
  },
17
17
  type: 'problem',
18
18
  model: 'parsed',
19
19
  messages: {
20
- nullComparison: `Comparisons against 'null' are always null. Did you mean 'is not null'?`,
20
+ nullComparison: `Comparisons against 'null' using '=' and '<>' are always null. Did you mean 'is null'/'is not null'?`,
21
21
  }
22
22
  },
23
23
  create(context) {
@@ -1,63 +1,64 @@
1
1
  'use strict'
2
2
 
3
+ const allowedUpperCaseElements = ['ID']
4
+
3
5
  module.exports = {
4
6
  meta: {
5
7
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
6
8
  docs: {
7
9
  description: 'Regular element names should start with lowercase letters.',
8
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-elements-lowercase',
10
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-elements-lowercase',
9
11
  },
10
12
  type: 'suggestion',
11
- hasSuggestions: true,
12
13
  messages: {
13
- startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
14
- fixLowercase: 'Start element name with a lowercase letter.'
14
+ startLowercase: "Element name '{{defName}}:{{elementName}}' should start with a lowercase letter.",
15
15
  },
16
16
  fixable: 'code',
17
17
  model: 'parsed',
18
18
  },
19
19
  create: function (context) {
20
- const sourcecode = context.getSourceCode()
20
+ const model = context.getModel()
21
+ if (!model?.definitions)
22
+ return
21
23
 
22
- return {
23
- element: checkStartLowercase
24
+ return function checkAllElementsStartWithLowercase() {
25
+ for (const defName in model.definitions)
26
+ checkDefinition(defName, model.definitions[defName])
24
27
  }
25
28
 
26
- function checkStartLowercase (e) {
27
- const elementName = e.name
28
- const entityName = e.parent.name
29
- if (elementName && !(entityName.startsWith('localized') || entityName.endsWith('texts'))) {
30
- if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !['ID'].includes(elementName)) {
31
- if (e.$location && e.$location.file) {
32
- const file = e.$location.file
33
- const loc = context.getLocation(elementName, e)
34
- const fix = (fixer, source = sourcecode) => {
35
- const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1)
36
- const rangeEnd = source.getIndexFromLoc({
37
- line: loc.end.line,
38
- column: loc.end.column
39
- })
40
- const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0
41
- return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized)
42
- }
43
- context.report({
44
- messageId: 'startLowercase',
45
- loc,
46
- file,
47
- data: {
48
- entityName,
49
- elementName
50
- },
51
- suggest: [
52
- {
53
- messageId: 'fixLowercase',
54
- fix
55
- }
56
- ]
57
- })
29
+ function checkDefinition(defName, def) {
30
+ if (defName.startsWith('localized') || defName.endsWith('texts'))
31
+ return
32
+
33
+ checkElements(def)
34
+
35
+ function checkElements(art) {
36
+ if (art.elements) {
37
+ for (const elementName in art.elements) {
38
+ const element = art.elements[elementName]
39
+ checkStartLowercase(element, elementName)
40
+ checkElements(element)
58
41
  }
59
42
  }
60
43
  }
44
+
45
+ function checkStartLowercase (element, elementName) {
46
+ if (!element.$location?.file)
47
+ return // without location, we can't report anything properly
48
+
49
+ if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase()
50
+ && !allowedUpperCaseElements.includes(elementName)) {
51
+ context.report({
52
+ messageId: 'startLowercase',
53
+ loc: context.getLocation(elementName, element),
54
+ file: element.$location.file,
55
+ data: {
56
+ defName,
57
+ elementName
58
+ }
59
+ })
60
+ }
61
+ }
61
62
  }
62
63
  }
63
64
  }
@@ -7,53 +7,41 @@ module.exports = {
7
7
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
8
8
  docs: {
9
9
  description: 'Regular entity names should start with uppercase letters.',
10
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-entities-uppercase',
10
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-entities-uppercase',
11
11
  },
12
12
  type: 'suggestion',
13
- hasSuggestions: true,
14
13
  messages: {
15
14
  startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
16
- fixUppercase: 'Start entity name with an uppercase letter.'
17
15
  },
18
16
  fixable: 'code',
19
17
  model: 'parsed',
20
18
  },
21
19
  create(context) {
22
- const sourcecode = context.getSourceCode()
20
+ const model = context.getModel()
21
+ if (!model?.definitions)
22
+ return
23
23
 
24
- return { entity: checkStartsUppercase }
24
+ return function checkAllEntitiesStartWithUppercase() {
25
+ for (const defName in model.definitions) {
26
+ const def = model.definitions[defName]
27
+ if (def.kind === 'entity') {
28
+ checkEntityStartsUppercase(defName, def)
29
+ }
30
+ }
31
+ }
25
32
 
26
- function checkStartsUppercase (e) {
27
- if (e.kind !== 'entity')
28
- return // workaround for #424
33
+ function checkEntityStartsUppercase(name, entity) {
34
+ if (!entity.$location?.file)
35
+ return // without location, we can't report anything properly
29
36
 
30
- const entityName = splitDefName(e).name
37
+ const entityName = splitDefName(entity, name).name
31
38
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
32
- if (e.$location?.file) {
33
- const file = e.$location.file
34
- const loc = context.getLocation(entityName, e)
35
- const fix = fixer => {
36
- const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1)
37
- const rangeEnd = sourcecode.getIndexFromLoc({
38
- line: loc.end.line,
39
- column: loc.end.column
40
- })
41
- const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0
42
- return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized)
43
- }
44
- context.report({
45
- messageId: 'startUppercase',
46
- loc,
47
- file,
48
- data: { entityName },
49
- suggest: [
50
- {
51
- messageId: 'fixUppercase',
52
- fix
53
- }
54
- ]
55
- })
56
- }
39
+ context.report({
40
+ messageId: 'startUppercase',
41
+ loc: context.getLocation(entityName, entity),
42
+ file: entity.$location.file,
43
+ data: { entityName },
44
+ })
57
45
  }
58
46
  }
59
47
  }
@@ -13,7 +13,7 @@ module.exports = {
13
13
  description: 'CSV files for entities must refer to valid element names.',
14
14
  category: 'Model Validation',
15
15
  recommended: true,
16
- url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/valid-csv-header',
16
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/valid-csv-header',
17
17
  },
18
18
  severity: 'warn',
19
19
  type: 'problem',
@@ -1,35 +1,38 @@
1
1
  'use strict'
2
2
 
3
- /**
4
- * Simple cache to store model and any cds calls made in the rule creation
5
- * api to modify the model
6
- */
7
- const cache = new Map()
8
3
 
9
- module.exports = {
4
+ class Cache {
5
+ #entries = new Map()
6
+
10
7
  has (key) {
11
- return cache.has(key)
12
- },
8
+ return this.#entries.has(key)
9
+ }
13
10
  set (key, value) {
14
- return cache.set(key, [value, Date.now()])
15
- },
11
+ return this.#entries.set(key, [value, Date.now()])
12
+ }
16
13
  get (key) {
17
- return cache.get(key) ? cache.get(key)[0] : undefined
18
- },
14
+ return this.#entries.has(key) ? this.#entries.get(key)[0] : undefined
15
+ }
19
16
  dump () {
20
17
  const dump = {}
21
- for (const [key, value] of cache.entries()) {
18
+ for (const [key, value] of this.#entries.entries()) {
22
19
  const timestamp = new Date(value[1])
23
20
  dump[key] = { key, value: value[0], timestamp }
24
21
  }
25
22
  return JSON.stringify(dump, null, 2)
26
- },
23
+ }
27
24
  remove (key) {
28
- if (cache.has(key)) {
29
- cache.delete(key)
25
+ if (this.#entries.has(key)) {
26
+ this.#entries.delete(key)
30
27
  }
31
- },
28
+ }
32
29
  clear () {
33
- cache.clear()
30
+ this.#entries.clear()
34
31
  }
35
32
  }
33
+
34
+
35
+ module.exports = {
36
+ Cache,
37
+ globalCache: new Cache(),
38
+ }
@@ -17,10 +17,10 @@ const fs = require('fs')
17
17
  const path = require('path')
18
18
  const cds = require('@sap/cds')
19
19
 
20
- const Cache = require('./Cache')
20
+ const { globalCache } = require('./Cache')
21
21
  const constants = require('../constants')
22
22
  const isConfiguredFileType = require('./isConfiguredFileType')
23
- const getProjectRootPath = require('./getProjectRootPath')
23
+ const { getProjectRootPath, hasProjectRoots } = require('./projectRootPath')
24
24
  const { CdsLintAssertionError } = require('./LintError')
25
25
 
26
26
  const LOG = cds.debug('lint:plugin')
@@ -48,18 +48,18 @@ module.exports = function createRule(spec) {
48
48
  Program: node => {
49
49
  const file = context.getFilename()
50
50
  if (file !== filePrev) {
51
- LOG && LOG(`File: ${context.getFilename()}`)
51
+ LOG?.(`File: ${context.getFilename()}`)
52
52
  }
53
- const cdscontext = extendContext(node, context, meta)
54
- Cache.set('context', cdscontext)
55
- const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdscontext)
53
+ const cdsContext = extendContext(node, context, meta)
54
+ globalCache.set('context', cdsContext)
55
+ const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks, showInEditor } = checkEntryCriteria(meta, cdsContext)
56
56
  switch (meta.model) {
57
57
  case 'none':
58
58
  if (doEnvironmentChecks) {
59
- if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
60
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
61
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
62
- createReport(node, cdscontext, meta, create)
59
+ if (isTest || !globalCache.has(`rule:${cdsContext.id}`)) {
60
+ LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
61
+ globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
62
+ createReport(node, cdsContext, meta, create)
63
63
  }
64
64
  }
65
65
  break
@@ -67,24 +67,24 @@ module.exports = function createRule(spec) {
67
67
  case 'inferred':
68
68
  if (isValidFile && doRootModelChecks) {
69
69
  if (showInEditor) {
70
- Cache.remove(`model:${Cache.get('rootpath')}`)
71
- Cache.remove(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)
72
- Cache.remove(`report:${context.getFilename()}:${context.id}`)
70
+ globalCache.remove(`model:${globalCache.get('rootpath')}`)
71
+ globalCache.remove(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)
72
+ globalCache.remove(`report:${context.getFilename()}:${context.id}`)
73
73
  }
74
- if (isTest || showInEditor || !Cache.has(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)) {
75
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
74
+ if (isTest || showInEditor || !globalCache.has(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`)) {
75
+ LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
76
76
  if (!showInEditor) {
77
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
77
+ globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
78
78
  }
79
- createReport(node, cdscontext, meta, create)
79
+ createReport(node, cdsContext, meta, create)
80
80
  } else {
81
- if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
82
- const reports = Cache.get(`report:${context.getFilename()}:${context.id}`)
81
+ if (globalCache.has(`report:${context.getFilename()}:${context.id}`)) {
82
+ const reports = globalCache.get(`report:${context.getFilename()}:${context.id}`)
83
83
  for (const r of Array.from(reports)) {
84
84
  context.report(JSON.parse(r))
85
85
  }
86
- Cache.remove(`report:${context.getFilename()}:${context.id}`)
87
- Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
86
+ globalCache.remove(`report:${context.getFilename()}:${context.id}`)
87
+ globalCache.set(`rule:${cdsContext.id}:${globalCache.get('rootpath')}`, 'done')
88
88
  }
89
89
  }
90
90
  }
@@ -92,8 +92,8 @@ module.exports = function createRule(spec) {
92
92
 
93
93
  default:
94
94
  if (isValidFile) {
95
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
96
- createReport(node, cdscontext, meta, create)
95
+ LOG?.(` Model: "${meta.model}" Rule: ${context.id}`)
96
+ createReport(node, cdsContext, meta, create)
97
97
  }
98
98
  break
99
99
  }
@@ -112,15 +112,14 @@ function isRunningWithESLint () {
112
112
  return process.argv[1].match(/eslint(\.js)?$/)
113
113
  }
114
114
 
115
- function checkEntryCriteria (meta, cdscontext) {
116
- const isTest = Cache.has('test')
117
- const showInEditor = cdscontext.options.includes('show')
118
- const hasProjectRoots = Cache.has(`roots:${Cache.get('rootpath')}`)
119
- const isValidFile = isConfiguredFileType(cdscontext.getFilename(), 'FILES')
120
- const doRootModelChecks = isTest || (hasProjectRoots && (isRunningWithCDSLint() || isRunningWithESLint()) || showInEditor)
115
+ function checkEntryCriteria (meta, cdsContext) {
116
+ const isTest = globalCache.has('test')
117
+ const showInEditor = cdsContext.options.includes('show')
118
+ const isValidFile = isConfiguredFileType(cdsContext.getFilename(), 'FILES')
119
+ const doRootModelChecks = isTest || (hasProjectRoots() && (isRunningWithCDSLint() || isRunningWithESLint()) || showInEditor)
121
120
  // Lint all env rules independent of any parsed file (i.e. 'cds lint' uses the lintText "" API)
122
121
  const doEnvironmentChecks =
123
- isTest || (isRunningWithCDSLint() && cdscontext.getFilename() === '<text>')
122
+ isTest || (isRunningWithCDSLint() && cdsContext.getFilename() === '<text>')
124
123
  return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks, showInEditor }
125
124
  }
126
125
 
@@ -196,11 +195,11 @@ function sanitizeFileLocation (d) {
196
195
  * @param meta
197
196
  */
198
197
  function extendContext (node, context, meta) {
199
- if (!Cache.has('test')) {
198
+ if (!globalCache.has('test')) {
200
199
  const filePath = context.getFilename()
201
200
  const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
202
201
  if (rootPath) {
203
- Cache.set('rootpath', rootPath)
202
+ globalCache.set('rootpath', rootPath)
204
203
  }
205
204
  }
206
205
 
@@ -222,7 +221,7 @@ function extendContext (node, context, meta) {
222
221
  }
223
222
  cdscontext.getLocation = parserServices.getLocation
224
223
  cdscontext.getNode = Object.keys(parserServices).length > 0 ? parserServices.getNode : () => node
225
- cdscontext.getRootPath = () => Cache.get('rootpath')
224
+ cdscontext.getRootPath = () => globalCache.get('rootpath')
226
225
  return cdscontext
227
226
 
228
227
  function reportWrapper(r) {
@@ -232,7 +231,7 @@ function extendContext (node, context, meta) {
232
231
  if (!r.file) {
233
232
  throw new CdsLintAssertionError(`Rule ${context.id} must return a "file" property in the rule report!`)
234
233
  }
235
- const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
234
+ const file = globalCache.get('rootpath') ? resolveFilePath(r.file) : r.file
236
235
  if (cdscontext.getFilename() === file) {
237
236
  delete r.file
238
237
  context.report(r)
@@ -280,11 +279,11 @@ function cacheReport (r, filepath, context, meta) {
280
279
  }
281
280
  if (r) {
282
281
  let reports = new Set()
283
- if (Cache.has(`report:${filepath}:${context.id}`)) {
284
- reports = Cache.get(`report:${filepath}:${context.id}`)
282
+ if (globalCache.has(`report:${filepath}:${context.id}`)) {
283
+ reports = globalCache.get(`report:${filepath}:${context.id}`)
285
284
  }
286
285
  reports.add(JSON.stringify(r))
287
- Cache.set(`report:${filepath}:${context.id}`, reports)
286
+ globalCache.set(`report:${filepath}:${context.id}`, reports)
288
287
  }
289
288
  }
290
289
 
@@ -295,7 +294,7 @@ function cacheReport (r, filepath, context, meta) {
295
294
  */
296
295
  function getDisabled (code, sourcecode, line) {
297
296
  const listDisabled = []
298
- const rules = Cache.get('rules')
297
+ const rules = globalCache.get('rules')
299
298
  const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
300
299
  if (code) {
301
300
  const matches = [...code.matchAll(REGEX_COMMENTS)]
@@ -375,5 +374,5 @@ function getLastLine (code) {
375
374
  * @param {string} file
376
375
  */
377
376
  function resolveFilePath (file) {
378
- return path.isAbsolute(file) ? file : path.join(Cache.get('rootpath'), file)
377
+ return path.isAbsolute(file) ? file : path.join(globalCache.get('rootpath'), file)
379
378
  }
@@ -2,13 +2,13 @@
2
2
 
3
3
  const path = require('node:path')
4
4
  const cds = require('@sap/cds')
5
- const Cache = require('./Cache')
5
+ const { globalCache } = require('./Cache')
6
6
  const fs = require('node:fs')
7
7
 
8
8
  const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
9
9
 
10
10
  /**
11
- * Searches for directory containing cds roots
11
+ * Searches for a directory containing cds roots.
12
12
  *
13
13
  * As of today, there is no unified way to find the root directory for a CDS project.
14
14
  * ("The root is wherever the user typed `cds init`")
@@ -20,9 +20,8 @@ const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
20
20
  * @param {string} currentDir start here and search until root dir
21
21
  * @returns {string} dir containing cds roots (empty if not exists)
22
22
  */
23
- module.exports = function getProjectRootPath(currentDir = '.') {
23
+ function getProjectRootPath(currentDir = '.') {
24
24
  let dir = path.resolve(currentDir)
25
-
26
25
  while (!couldBeProjectRoot(dir)) {
27
26
  if (dir === path.resolve(dir, '..'))
28
27
  return '' // we reached the file system root -> abort
@@ -32,7 +31,7 @@ module.exports = function getProjectRootPath(currentDir = '.') {
32
31
  cds.resolve.cache = {}
33
32
  const roots = cds.resolve('*', { root: dir })
34
33
  if (roots?.length > 0) {
35
- Cache.set(`roots:${dir}`, roots)
34
+ globalCache.set(`roots:${dir}`, roots)
36
35
  return dir
37
36
  }
38
37
  return ''
@@ -56,8 +55,21 @@ function isRootPackageJson(filepath) {
56
55
 
57
56
  try {
58
57
  const config = JSON.parse(fs.readFileSync(filepath, 'utf8'))
59
- return Object.keys(config?.dependencies ?? {}).some(dep => dep === '@sap/cds')
58
+ return config && (config.cds ||
59
+ hasCdsPackage(config.dependencies) ||
60
+ hasCdsPackage(config.peerDependencies) ||
61
+ hasCdsPackage(config.devDependencies))
62
+
60
63
  } catch {
61
64
  return false
62
65
  }
63
66
  }
67
+
68
+ function hasCdsPackage(deps) {
69
+ return deps && (('@sap/cds' in deps) || ('@sap/cds-dk' in deps))
70
+ }
71
+
72
+ module.exports = {
73
+ getProjectRootPath,
74
+ hasProjectRoots: () => globalCache.has(`roots:${globalCache.get('rootpath')}`),
75
+ }
@@ -6,7 +6,7 @@ const fs = require('node:fs')
6
6
  const path = require('node:path')
7
7
 
8
8
  const { Linter, RuleTester } = require('eslint')
9
- const Cache = require('./Cache')
9
+ const { globalCache } = require('./Cache')
10
10
  const isConfiguredFileType = require('./isConfiguredFileType')
11
11
  const { compileModelFromDict } = require('../parser')
12
12
  const rules = require('../rules')
@@ -29,7 +29,7 @@ function testRuleWrapper(rule) {
29
29
  _initModelRuleTester(filePath, rule.meta.model)
30
30
  const createValue = rule.create(context)
31
31
  const result = createValue.Program(node)
32
- Cache.clear()
32
+ globalCache.clear()
33
33
  return result
34
34
  }
35
35
  }
@@ -114,18 +114,18 @@ module.exports = function runRuleTester(options) {
114
114
  * @param {string} flavor
115
115
  */
116
116
  function _initModelRuleTester(filePath, flavor) {
117
- Cache.set('rules', rules)
118
- Cache.set('test', true)
117
+ globalCache.set('rules', rules)
118
+ globalCache.set('test', true)
119
119
  const rootPath = path.dirname(filePath)
120
- Cache.set('rootpath', rootPath)
120
+ globalCache.set('rootpath', rootPath)
121
121
  if (flavor !== 'none') { // not for env rules
122
122
  const files = fs.readdirSync(rootPath)
123
123
  const modelfiles = files.map(f => path.join(rootPath, f)).filter(fp => isConfiguredFileType(fp, 'MODEL_FILES'))
124
- Cache.set(`modelfiles:${rootPath}`, modelfiles)
124
+ globalCache.set(`modelfiles:${rootPath}`, modelfiles)
125
125
  const dictFiles = _getDictFiles(rootPath, modelfiles)
126
- Cache.set(`dictfiles:${rootPath}`, dictFiles)
126
+ globalCache.set(`dictfiles:${rootPath}`, dictFiles)
127
127
  const reflectedModel = compileModelFromDict(dictFiles, { flavor })
128
- Cache.set(`model:${rootPath}`, reflectedModel)
128
+ globalCache.set(`model:${rootPath}`, reflectedModel)
129
129
  }
130
130
  }
131
131
 
@@ -139,12 +139,12 @@ function _initModelRuleTester(filePath, flavor) {
139
139
  */
140
140
  function _getDictFiles(input, filenames) {
141
141
  let dictFiles = {}
142
- if (Cache.has(`dictfiles:${input}`)) {
143
- dictFiles = Cache.get(`dictfiles:${input}`)
142
+ if (globalCache.has(`dictfiles:${input}`)) {
143
+ dictFiles = globalCache.get(`dictfiles:${input}`)
144
144
  } else {
145
145
  filenames.forEach(file => {
146
- dictFiles[file] = Cache.has(`file:${file}`)
147
- ? Cache.get(`file:${file}`)
146
+ dictFiles[file] = globalCache.has(`file:${file}`)
147
+ ? globalCache.get(`file:${file}`)
148
148
  : fs.readFileSync(file, 'utf8')
149
149
  })
150
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "ESLint plugin including recommended SAP Cloud Application Programming model and environment rules",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "@sap/cds": ">=7",
24
- "semver": "^7.3.4"
24
+ "semver": "^7.7.1"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "eslint": ">=8"