@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.
@@ -1,40 +1,64 @@
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: 'Should make suggestions for possible missing SQL casts.',
8
11
  recommended: true,
9
12
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-cast-suggestion',
10
13
  },
11
14
  type: 'suggestion',
12
15
  messages: {
13
- missingSQLCast: 'Potential issue - Missing SQL cast for column expression?'
14
- }
16
+ missingSqlCast: 'Potential issue - Missing SQL cast for column expression?'
17
+ },
18
+ model: 'parsed',
15
19
  },
16
20
  create: function (context) {
17
- return { view: checkSqlCast }
21
+ const model = context.getModel()
22
+ if (!model?.definitions)
23
+ return
24
+
25
+ return function checkAllElementsStartWithLowercase() {
26
+ for (const defName in model.definitions) {
27
+ const def = model.definitions[defName]
28
+ checkSqlCastsInView(defName, def)
29
+ }
30
+ }
18
31
 
19
- function checkSqlCast (v) {
32
+ function checkSqlCastsInView(defName, def) {
20
33
  // TODO: restructure and make more robust (#507)
21
- if (v?.query?.SET?.args) {
22
- for (const arg of v.query.SET.args) {
23
- if (arg?.SELECT) {
24
- // Only in UNION cases?
25
- for (const each of arg.SELECT.columns || []) {
26
- if (each) {
27
- const { xpr, cast } = each
28
- if (cast && xpr) {
29
- if (!(xpr[0]?.xpr && xpr[0]?.cast)) {
30
- context.report({
31
- messageId: 'missingSQLCast',
32
- node: context.getNode(v)
33
- })
34
- }
35
- }
36
- }
37
- }
34
+ if (!def?.query?.SET?.args?.length)
35
+ return
36
+
37
+ for (const arg of def.query.SET.args) {
38
+ if (arg?.SELECT?.columns?.length) {
39
+ // Only in UNION cases?
40
+ for (const col of arg.SELECT.columns) {
41
+ if (col)
42
+ checkColumn(col)
43
+ }
44
+ }
45
+ }
46
+
47
+ function checkColumn(col) {
48
+ const { xpr, cast } = col
49
+ if (cast && xpr) {
50
+ if (!(xpr[0]?.xpr && xpr[0]?.cast)) {
51
+ // we don't pass a name for the column's location, as it would be used to calculate
52
+ // endColumn, which is not correct for this expression
53
+ const loc = col.$location ?
54
+ context.getLocation('', col, model) :
55
+ context.getLocation(defName, def, model)
56
+
57
+ context.report({
58
+ messageId: 'missingSqlCast',
59
+ loc,
60
+ file: def.$location.file,
61
+ })
38
62
  }
39
63
  }
40
64
  }
@@ -11,8 +11,7 @@ module.exports = {
11
11
  description: 'Ensure SQL comparisons with \'null\' are valid',
12
12
  category: 'Model Validation',
13
13
  recommended: false,
14
- // TODO: Add documentation
15
- // url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
14
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/sql-null-comparison',
16
15
  },
17
16
  type: 'problem',
18
17
  model: 'parsed',
@@ -21,40 +20,45 @@ module.exports = {
21
20
  }
22
21
  },
23
22
  create(context) {
24
- return {
25
- view(v) {
26
- forEachXprInDefinition(v, checkExpression)
27
-
28
- function checkExpression(xpr, ctx) {
29
- if (!xpr || !Array.isArray(xpr))
30
- return
31
-
32
- for (let i = 0; i < xpr.length; i++) {
33
- if (typeof xpr[i] !== 'object')
34
- continue // scalar value, etc.
35
-
36
- if (xpr[i]?.val === null) {
37
- const prev = i > 0 && typeof xpr[i-1] === 'string' ? xpr[i-1] : null
38
- if (prev && invalidComparisonOperators.includes(prev)) {
39
- reportComparison(xpr, ctx)
40
- continue
41
- }
42
- const next = i+1 < xpr.length && typeof xpr[i+1] === 'string' ? xpr[i+1] : null
43
- if (next && invalidComparisonOperators.includes(next))
44
- reportComparison(xpr, ctx)
45
- }
46
- }
47
- }
23
+ const model = context.getModel()
24
+ if (!model?.definitions)
25
+ return
48
26
 
49
- function reportComparison(xpr, ctx) {
50
- context.report({
51
- messageId: 'nullComparison',
52
- loc: context.getLocation(null, ctx),
53
- file: ctx.$location?.file,
54
- })
55
- }
27
+ return function checkSqlNullComparisonsInModel() {
28
+ for (const defName in model.definitions) {
29
+ const def = model.definitions[defName]
30
+ if (def.query || def.projection)
31
+ forEachXprInDefinition(def, checkExpression)
32
+ }
33
+ }
34
+
35
+ function checkExpression(xpr, ctx) {
36
+ if (!xpr || !Array.isArray(xpr))
37
+ return
56
38
 
39
+ for (let i = 0; i < xpr.length; i++) {
40
+ if (typeof xpr[i] !== 'object')
41
+ continue // scalar value, etc.
42
+
43
+ if (xpr[i]?.val === null) {
44
+ const prev = i > 0 && typeof xpr[i-1] === 'string' ? xpr[i-1] : null
45
+ if (prev && invalidComparisonOperators.includes(prev)) {
46
+ reportComparison(xpr, ctx)
47
+ continue
48
+ }
49
+ const next = i+1 < xpr.length && typeof xpr[i+1] === 'string' ? xpr[i+1] : null
50
+ if (next && invalidComparisonOperators.includes(next))
51
+ reportComparison(xpr, ctx)
52
+ }
57
53
  }
58
54
  }
55
+
56
+ function reportComparison(xpr, ctx) {
57
+ context.report({
58
+ messageId: 'nullComparison',
59
+ loc: context.getLocation('', ctx),
60
+ file: ctx.$location?.file,
61
+ })
62
+ }
59
63
  }
60
64
  }
@@ -1,11 +1,14 @@
1
1
  'use strict'
2
2
 
3
+ const { RULE_CATEGORIES } = require('../constants')
4
+
3
5
  const allowedUpperCaseElements = ['ID']
4
6
 
5
7
  module.exports = {
6
8
  meta: {
7
9
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
8
10
  docs: {
11
+ category: RULE_CATEGORIES.model,
9
12
  description: 'Regular element names should start with lowercase letters.',
10
13
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-elements-lowercase',
11
14
  },
@@ -1,11 +1,13 @@
1
1
  'use strict'
2
2
 
3
+ const { RULE_CATEGORIES } = require('../constants')
3
4
  const { splitDefName } = require('../utils/rules')
4
5
 
5
6
  module.exports = {
6
7
  meta: {
7
8
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
8
9
  docs: {
10
+ category: RULE_CATEGORIES.model,
9
11
  description: 'Regular entity names should start with uppercase letters.',
10
12
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/start-entities-uppercase',
11
13
  },
@@ -3,6 +3,7 @@
3
3
  const cds = require('@sap/cds')
4
4
  const { basename, extname } = require('node:path')
5
5
  const findFuzzy = require('../utils/findFuzzy')
6
+ const { RULE_CATEGORIES } = require('../constants')
6
7
  const SEP = '[,;\t]'
7
8
  const EOL = '\\r?\\n'
8
9
 
@@ -11,7 +12,7 @@ module.exports = {
11
12
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
12
13
  docs: {
13
14
  description: 'CSV files for entities must refer to valid element names.',
14
- category: 'Model Validation',
15
+ category: RULE_CATEGORIES.csv,
15
16
  recommended: true,
16
17
  url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/valid-csv-header',
17
18
  },
@@ -127,7 +127,6 @@ function setMetaDefaults (meta) {
127
127
  meta ??= {}
128
128
  meta.severity ??= constants.DEFAULT_RULE_SEVERITY
129
129
  meta.docs ??= {}
130
- meta.docs.category ??= constants.DEFAULT_RULE_CATEGORY
131
130
  meta.model ??= 'parsed'
132
131
  return meta
133
132
  }
@@ -184,8 +183,10 @@ function createReport (node, cdsContext, meta, create) {
184
183
 
185
184
  function sanitizeFileLocation (d) {
186
185
  let parent = d
187
- while (!parent.$location && parent.parent && !parent.parent.definitions) parent = d.parent
188
- if (parent.$location) d.$location = parent.$location
186
+ while (!parent.$location && parent.parent && !parent.parent.definitions)
187
+ parent = d.parent
188
+ if (parent.$location)
189
+ d.$location = parent.$location
189
190
  return d
190
191
  }
191
192
 
@@ -213,8 +214,11 @@ function extendContext (node, context, meta) {
213
214
 
214
215
  const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
215
216
  const { parserServices } = context.sourceCode || context
216
- cdscontext.getModel =
217
- meta.model === 'inferred' ? parserServices.getInferredCsn : parserServices.getParsedCsn
217
+
218
+ cdscontext.getModel = meta.model === 'inferred'
219
+ ? parserServices.getInferredCsn
220
+ : parserServices.getParsedCsn
221
+
218
222
  cdscontext.getEnvironment = () => {
219
223
  const options = context.options
220
224
  return options && options[0] && options[0].environment ? options[0].environment : undefined
@@ -11,23 +11,12 @@
11
11
  const fs = require('node:fs')
12
12
  const path = require('node:path')
13
13
 
14
- module.exports = (currentDir = '.', legacy=false) => {
14
+ module.exports = (currentDir = '.') => {
15
15
  let configFiles = [
16
16
  'eslint.config.js',
17
17
  'eslint.config.cjs',
18
18
  'eslint.config.mjs'
19
19
  ]
20
- if (legacy) {
21
- configFiles = [
22
- '.eslintrc.js',
23
- '.eslintrc.cjs',
24
- '.eslintrc.yaml',
25
- '.eslintrc.yml',
26
- '.eslintrc.json',
27
- '.eslintrc',
28
- 'package.json',
29
- ]
30
- }
31
20
  let configDir = path.resolve(currentDir)
32
21
  while (configDir !== path.resolve(configDir, '..')) {
33
22
  for (const configFile of configFiles) {
@@ -8,23 +8,24 @@ const findFuzzy = require('./findFuzzy')
8
8
  module.exports = {
9
9
  findFuzzy,
10
10
  /**
11
- *
12
- * @param {*} e
11
+ * @param {object} definition CSN definition object.
12
+ * @param {string} [name] The definition's name. Inferred for "linked CSN".
13
13
  */
14
- splitDefName(e) {
14
+ splitDefName(definition, name = definition.name) {
15
+ if (!name)
16
+ return null
15
17
  // Entity names from CSN are of the form:
16
18
  // <namespace>.<service>.<def>.<'texts'|'localized'>|<composition value>
17
19
  let prefix = ''
18
20
  let suffix = ''
19
- let defName = e.name
20
- const names = defName.split('.')
21
- defName = names[names.length - 1]
21
+ const names = name.split('.')
22
+ let defName = names[names.length - 1]
22
23
 
23
24
  if (defName) {
24
25
  // Managed composition get compiler tag `_up`
25
26
  let isManagedComposition = false
26
- if (e.elements) {
27
- isManagedComposition = Object.keys(e.elements).some(k => k === 'up_')
27
+ if (definition.elements) {
28
+ isManagedComposition = Object.keys(definition.elements).some(k => k === 'up_')
28
29
  }
29
30
  // Check for compiler tags
30
31
  const compilerTagsToExclude = ['texts', 'localized']
@@ -34,7 +35,7 @@ module.exports = {
34
35
  suffix = names[names.length - 1]
35
36
  defName = names[names.length - 2]
36
37
  }
37
- prefix = e.name.split(`.${defName}`)[0]
38
+ prefix = name.split(`.${defName}`)[0]
38
39
  }
39
40
  return { prefix, name: defName, suffix }
40
41
  },
@@ -5,7 +5,7 @@
5
5
  const fs = require('node:fs')
6
6
  const path = require('node:path')
7
7
 
8
- const { Linter, RuleTester } = require('eslint')
8
+ const { RuleTester } = require('eslint')
9
9
  const { globalCache } = require('./Cache')
10
10
  const isConfiguredFileType = require('./isConfiguredFileType')
11
11
  const { compileModelFromDict } = require('../parser')
@@ -56,6 +56,7 @@ module.exports = function runRuleTester(options) {
56
56
  rule = testRuleWrapper(rules[path.basename(options.root)]())
57
57
  } else {
58
58
  // Otherwise from project root
59
+
59
60
  // eslint-disable-next-line
60
61
  const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
61
62
  paths: [options.root]
@@ -66,9 +67,7 @@ module.exports = function runRuleTester(options) {
66
67
 
67
68
  let tester
68
69
  if (parserPath) {
69
- const options = _isEslint9OrLater()
70
- ? { languageOptions: { parser: require(parserPath) } }
71
- : { parser: parserPath }
70
+ const options = { languageOptions: { parser: require(parserPath) } }
72
71
  tester = new RuleTester(options)
73
72
  } else {
74
73
  tester = new RuleTester()
@@ -82,10 +81,7 @@ module.exports = function runRuleTester(options) {
82
81
  filename: filePath,
83
82
  }
84
83
  ]
85
- if (_isEslint9OrLater()) {
86
- // property not supported for ESLint 8
87
- testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
88
- }
84
+ testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
89
85
  if (!isConfiguredFileType(options.filename, 'FILES')) {
90
86
  const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
91
87
  testerCases[type][0].code = ''
@@ -150,7 +146,3 @@ function _getDictFiles(input, filenames) {
150
146
  }
151
147
  return dictFiles
152
148
  }
153
-
154
- function _isEslint9OrLater() {
155
- return Number(Linter.version.split('.')[0]) >= 9
156
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "3.2.0",
3
+ "version": "4.1.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": [
@@ -20,13 +20,13 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@sap/cds": ">=7",
24
23
  "semver": "^7.7.1"
25
24
  },
26
25
  "peerDependencies": {
27
- "eslint": ">=8"
26
+ "@sap/cds": ">=9",
27
+ "eslint": "^9"
28
28
  },
29
29
  "engines": {
30
- "node": ">=18"
30
+ "node": ">=20"
31
31
  }
32
32
  }