@sap/eslint-plugin-cds 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/lib/api/index.js +9 -9
  4. package/lib/conf/all.js +20 -19
  5. package/lib/conf/index.js +10 -10
  6. package/lib/conf/recommended.js +17 -16
  7. package/lib/constants.js +16 -14
  8. package/lib/index.js +17 -11
  9. package/lib/parser.js +90 -82
  10. package/lib/rules/assoc2many-ambiguous-key.js +71 -70
  11. package/lib/rules/auth-no-empty-restrictions.js +16 -15
  12. package/lib/rules/auth-use-requires.js +19 -18
  13. package/lib/rules/auth-valid-restrict-grant.js +49 -46
  14. package/lib/rules/auth-valid-restrict-keys.js +19 -18
  15. package/lib/rules/auth-valid-restrict-to.js +68 -64
  16. package/lib/rules/auth-valid-restrict-where.js +44 -43
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +23 -22
  19. package/lib/rules/latest-cds-version.js +21 -20
  20. package/lib/rules/min-node-version.js +22 -22
  21. package/lib/rules/no-db-keywords.js +21 -27
  22. package/lib/rules/no-dollar-prefixed-names.js +12 -11
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +8 -8
  25. package/lib/rules/sql-cast-suggestion.js +13 -12
  26. package/lib/rules/start-elements-lowercase.js +42 -41
  27. package/lib/rules/start-entities-uppercase.js +26 -25
  28. package/lib/rules/valid-csv-header.js +58 -57
  29. package/lib/types.d.ts +1 -0
  30. package/lib/utils/Cache.js +17 -17
  31. package/lib/utils/Colors.js +8 -8
  32. package/lib/utils/createRule.js +172 -153
  33. package/lib/utils/findFuzzy.js +37 -38
  34. package/lib/utils/genDocs.js +224 -242
  35. package/lib/utils/getConfigPath.js +27 -27
  36. package/lib/utils/getConfiguredFileTypes.js +4 -4
  37. package/lib/utils/getFileExtensions.js +3 -3
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +11 -11
  40. package/lib/utils/rules.js +59 -59
  41. package/lib/utils/runRuleTester.js +76 -71
  42. package/package.json +7 -1
  43. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -25
  44. package/lib/utils/createRuleDocs.js +0 -361
  45. package/lib/utils/jsonc.js +0 -1
  46. package/lib/utils/jsoncParser.js +0 -1
@@ -1,23 +1,24 @@
1
1
  module.exports = {
2
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
4
  docs: {
4
- description: `Names must not start with $ to avoid possible shadowing of reserved variables.`,
5
+ description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
5
6
  recommended: true
6
7
  },
7
- type: "problem"
8
+ type: 'problem'
8
9
  },
9
- create(context) {
10
- return { element: _check };
10
+ create (context) {
11
+ return { element: _check }
11
12
 
12
- function _check(d) {
13
- let srv = d._service || (d.parent && d.parent._service);
14
- if (srv && srv["@cds.external"]) return;
15
- if (d.name.startsWith("$")) {
13
+ function _check (d) {
14
+ const srv = d._service || (d.parent && d.parent._service)
15
+ if (srv && srv['@cds.external']) return
16
+ if (d.name.startsWith('$')) {
16
17
  context.report({
17
- message: `“${d.name} is prefixed with a dollar sign ($)`,
18
+ message: `'${d.name}' is prefixed with a dollar sign ($)`,
18
19
  node: context.getNode(d)
19
- });
20
+ })
20
21
  }
21
22
  }
22
23
  }
23
- };
24
+ }
@@ -0,0 +1,27 @@
1
+ module.exports = {
2
+ meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
+ docs: {
5
+ // eslint-disable-next-line quotes
6
+ description: "Draft-enabled entities shall not be used in views that make use of `JOIN`.",
7
+ recommended: true
8
+ },
9
+ type: 'suggestion',
10
+ model: 'inferred'
11
+ },
12
+ create: function (context) {
13
+ return { entity: checkNojoinDraftenabled }
14
+
15
+ function checkNojoinDraftenabled (e) {
16
+ if (e['@odata.draft.enabled']) {
17
+ if (e.query.SELECT.from.join) {
18
+ context.report({
19
+ message: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
20
+ node: context.getNode(e),
21
+ file: e.$location.file
22
+ })
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -1,22 +1,22 @@
1
1
  module.exports = {
2
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
4
  docs: {
4
- description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
5
+ description: 'Foreign key information of a `TO MANY` relationship must be defined within the target and specified in an `ON` condition.',
5
6
  recommended: true
6
7
  },
7
- type: "problem"
8
+ type: 'problem'
8
9
  },
9
10
  create: function (context) {
10
- return { element: check_2many_oncond };
11
+ return { element: check2manyOncond }
11
12
 
12
- function check_2many_oncond(e) {
13
- if (e.is2many && !e.on && typeof e.target === "string") {
13
+ function check2manyOncond (e) {
14
+ if (e.is2many && !e.on && typeof e.target === 'string') {
14
15
  context.report({
15
16
  message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${e.name}'.`,
16
17
  node: context.getNode(e)
17
- });
18
+ })
18
19
  }
19
20
  }
20
-
21
21
  }
22
- };
22
+ }
@@ -1,38 +1,39 @@
1
1
  /* eslint-disable no-undef */
2
2
  module.exports = {
3
3
  meta: {
4
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
5
  docs: {
5
- description: "Should make suggestions for possible missing SQL casts.",
6
+ description: 'Should make suggestions for possible missing SQL casts.',
6
7
  recommended: true
7
8
  },
8
- type: "suggestion",
9
+ type: 'suggestion',
9
10
  hasSuggestions: true,
10
11
  messages: {
11
- missingSQLCast: "Potential issue - Missing SQL cast for column expression?",
12
- },
12
+ missingSQLCast: 'Potential issue - Missing SQL cast for column expression?'
13
+ }
13
14
  },
14
15
  create: function (context) {
15
- return { view: check_sql_cast };
16
+ return { view: checkSqlCast }
16
17
 
17
- function check_sql_cast(v) {
18
+ function checkSqlCast (v) {
18
19
  if (v.query && v.query.SET) {
19
20
  for (const { SELECT } of v.query.SET.args) {
20
21
  // Only in UNION cases?
21
22
  for (const each of SELECT.columns || []) {
22
- const { xpr, cast } = each;
23
+ const { xpr, cast } = each
23
24
  if (cast && xpr) {
24
25
  if (xpr[0].xpr && xpr[0].cast) {
25
- continue;
26
+ continue
26
27
  } else {
27
28
  context.report({
28
- messageId: "missingSQLCast",
29
+ messageId: 'missingSQLCast',
29
30
  node: context.getNode(v)
30
- });
31
+ })
31
32
  }
32
33
  }
33
34
  }
34
35
  }
35
36
  }
36
37
  }
37
- },
38
- };
38
+ }
39
+ }
@@ -1,58 +1,59 @@
1
1
  module.exports = {
2
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
4
  docs: {
4
- description: "Regular element names should start with lowercase letters."
5
+ description: 'Regular element names should start with lowercase letters.'
5
6
  },
6
- type: "suggestion",
7
+ type: 'suggestion',
7
8
  hasSuggestions: true,
8
9
  messages: {
9
10
  startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
10
- fixLowercase: "Start element name with a lowercase letter.",
11
+ fixLowercase: 'Start element name with a lowercase letter.'
11
12
  },
12
- fixable: "code",
13
+ fixable: 'code'
13
14
  },
14
15
  create: function (context) {
15
- const sourcecode = context.getSourceCode();
16
+ const sourcecode = context.getSourceCode()
16
17
 
17
18
  return {
18
- element: check_start_lowercase,
19
- };
19
+ element: checkStartLowercase
20
+ }
20
21
 
21
- function check_start_lowercase(e) {
22
- const elementName = e.name;
23
- const entityName = e.parent.name;
24
- if (elementName && !(entityName.startsWith("localized") || entityName.endsWith("texts"))) {
25
- if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !["ID"].includes(elementName)) {
26
- if (e.$location && e.$location.file) {
27
- const file = e.$location.file;
28
- const loc = context.getLocation(elementName, e);
29
- const fix = (fixer, source = sourcecode) => {
30
- const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
31
- const rangeEnd = source.getIndexFromLoc({
32
- line: loc.end.line,
33
- column: loc.end.column,
34
- });
35
- const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0;
36
- return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
37
- };
38
- context.report({
39
- messageId: "startLowercase",
40
- loc,
41
- file,
42
- data: {
43
- entityName,
44
- elementName,
45
- },
46
- suggest: [
47
- {
48
- messageId: "fixLowercase",
49
- fix,
50
- },
51
- ],
52
- });
22
+ function checkStartLowercase (e) {
23
+ const elementName = e.name
24
+ const entityName = e.parent.name
25
+ if (elementName && !(entityName.startsWith('localized') || entityName.endsWith('texts'))) {
26
+ if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !['ID'].includes(elementName)) {
27
+ if (e.$location && e.$location.file) {
28
+ const file = e.$location.file
29
+ const loc = context.getLocation(elementName, e)
30
+ const fix = (fixer, source = sourcecode) => {
31
+ const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1)
32
+ const rangeEnd = source.getIndexFromLoc({
33
+ line: loc.end.line,
34
+ column: loc.end.column
35
+ })
36
+ const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0
37
+ return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized)
53
38
  }
39
+ context.report({
40
+ messageId: 'startLowercase',
41
+ loc,
42
+ file,
43
+ data: {
44
+ entityName,
45
+ elementName
46
+ },
47
+ suggest: [
48
+ {
49
+ messageId: 'fixLowercase',
50
+ fix
51
+ }
52
+ ]
53
+ })
54
+ }
54
55
  }
55
56
  }
56
57
  }
57
- },
58
- };
58
+ }
59
+ }
@@ -1,52 +1,53 @@
1
- const { splitEntityName } = require("../utils/rules");
1
+ const { splitDefName } = require('../utils/rules')
2
2
 
3
3
  module.exports = {
4
4
  meta: {
5
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
5
6
  docs: {
6
- description: "Regular entity names should start with uppercase letters."
7
+ description: 'Regular entity names should start with uppercase letters.'
7
8
  },
8
- type: "suggestion",
9
+ type: 'suggestion',
9
10
  hasSuggestions: true,
10
11
  messages: {
11
12
  startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
12
- fixUppercase: "Start entity name with an uppercase letter.",
13
+ fixUppercase: 'Start entity name with an uppercase letter.'
13
14
  },
14
- fixable: "code",
15
+ fixable: 'code'
15
16
  },
16
17
  create: function (context) {
17
- const sourcecode = context.getSourceCode();
18
+ const sourcecode = context.getSourceCode()
18
19
 
19
- return { entity: check_starts_uppercase };
20
+ return { entity: checkStartsUppercase }
20
21
 
21
- function check_starts_uppercase(e) {
22
- const entityName = splitEntityName(e).entity;
22
+ function checkStartsUppercase (e) {
23
+ const entityName = splitDefName(e).name
23
24
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
24
25
  if (e.$location && e.$location.file) {
25
- const file = e.$location.file;
26
- const loc = context.getLocation(entityName, e);
26
+ const file = e.$location.file
27
+ const loc = context.getLocation(entityName, e)
27
28
  const fix = (fixer) => {
28
- const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
29
+ const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1)
29
30
  const rangeEnd = sourcecode.getIndexFromLoc({
30
31
  line: loc.end.line,
31
- column: loc.end.column,
32
- });
33
- const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0;
34
- return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
35
- };
32
+ column: loc.end.column
33
+ })
34
+ const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0
35
+ return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized)
36
+ }
36
37
  context.report({
37
- messageId: "startUppercase",
38
+ messageId: 'startUppercase',
38
39
  loc,
39
40
  file,
40
41
  data: { entityName },
41
42
  suggest: [
42
43
  {
43
- messageId: "fixUppercase",
44
- fix,
45
- },
46
- ],
47
- });
44
+ messageId: 'fixUppercase',
45
+ fix
46
+ }
47
+ ]
48
+ })
48
49
  }
49
50
  }
50
51
  }
51
- },
52
- };
52
+ }
53
+ }
@@ -1,100 +1,101 @@
1
- const cds = require("@sap/cds");
2
- const { basename, extname } = require("path");
3
- const findFuzzy = require("../utils/findFuzzy");
4
- const SEP = "[,;\t]";
5
- const EOL = "\\r?\\n";
1
+ const cds = require('@sap/cds')
2
+ const { basename, extname } = require('path')
3
+ const findFuzzy = require('../utils/findFuzzy')
4
+ const SEP = '[,;\t]'
5
+ const EOL = '\\r?\\n'
6
6
 
7
7
  module.exports = {
8
8
  meta: {
9
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
9
10
  docs: {
10
- description: `CSV files for entities must refer to valid element names.`,
11
- category: "Model Validation",
11
+ description: 'CSV files for entities must refer to valid element names.',
12
+ category: 'Model Validation',
12
13
  recommended: true
13
14
  },
14
- severity: "warn",
15
- type: "problem",
15
+ severity: 'warn',
16
+ type: 'problem',
16
17
  hasSuggestions: true,
17
18
  messages: {
18
- InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
19
- ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`,
19
+ InvalidColumn: "Invalid column '{{column}}'. Did you mean '{{candidates}}'?",
20
+ ReplaceColumnWith: "Replace '{{column}}' with '{{candidates}}'"
20
21
  },
21
- model: "inferred",
22
+ model: 'inferred'
22
23
  },
23
24
  create: function (context) {
24
- return check_valid_headers;
25
+ return checkValidHeaders
25
26
 
26
- function check_valid_headers() {
27
- const filePath = context.getFilename();
28
- const sourcecode = context.getSourceCode();
29
- const code = sourcecode.getText();
27
+ function checkValidHeaders () {
28
+ const filePath = context.getFilename()
29
+ const sourcecode = context.getSourceCode()
30
+ const code = sourcecode.getText()
30
31
 
31
- let model = context.getModel();
32
- if (!filePath.endsWith(".csv")) return;
33
- if (!model) return;
32
+ let model = context.getModel()
33
+ if (!filePath.endsWith('.csv')) return
34
+ if (!model) return
34
35
 
35
- model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] });
36
+ model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] })
36
37
 
37
- const filename = basename(filePath);
38
- const entityName = filename.replace(/-/g, ".").slice(0, -extname(filename).length);
39
- const entity = _entity4(entityName, model);
40
- if (!entity) return;
38
+ const filename = basename(filePath)
39
+ const entityName = filename.replace(/-/g, '.').slice(0, -extname(filename).length)
40
+ const entity = _entity4(entityName, model)
41
+ if (!entity) return
41
42
 
42
43
  const elements = Object.values(entity.elements)
43
- .filter((e) => !!e["@cds.persistence.name"])
44
- .map((e) => e["@cds.persistence.name"].toUpperCase());
44
+ .filter((e) => !!e['@cds.persistence.name'])
45
+ .map((e) => e['@cds.persistence.name'].toUpperCase())
45
46
 
46
- const [cols] = cds.parse.csv(code);
47
- const missing = cols.filter((col) => !elements.includes(col.toUpperCase()));
47
+ const [cols] = cds.parse.csv(code)
48
+ const missing = cols.filter((col) => !elements.includes(col.toUpperCase()))
48
49
  for (const miss of missing) {
49
- const index = _findInCode(miss, code);
50
- const loc = sourcecode.getLocFromIndex(index);
51
- const candidates = findFuzzy(miss, Object.keys(entity.elements).sort());
50
+ const index = _findInCode(miss, code)
51
+ const loc = sourcecode.getLocFromIndex(index)
52
+ const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
52
53
  const suggest = candidates.map((cand) => {
53
54
  return {
54
- messageId: "ReplaceColumnWith",
55
+ messageId: 'ReplaceColumnWith',
55
56
  data: { column: miss, candidates: cand },
56
- fix: (fixer) => fixer.replaceTextRange([index, index + miss.length], cand),
57
- };
58
- });
57
+ fix: (fixer) => fixer.replaceTextRange([index, index + miss.length], cand)
58
+ }
59
+ })
59
60
  context.report({
60
- messageId: "InvalidColumn",
61
+ messageId: 'InvalidColumn',
61
62
  data: { column: miss, candidates },
62
63
  loc: { start: loc, end: { line: loc.line, column: loc.column + miss.length } },
63
64
  file: filePath,
64
- suggest,
65
- });
65
+ suggest
66
+ })
66
67
  }
67
68
  }
68
- },
69
- };
69
+ }
70
+ }
70
71
 
71
- function _findInCode(miss, code) {
72
+ function _findInCode (miss, code) {
72
73
  // middle
73
- let match = new RegExp(SEP + miss + SEP).exec(code);
74
- if (match) return match.index + 1;
74
+ let match = new RegExp(SEP + miss + SEP).exec(code)
75
+ if (match) return match.index + 1
75
76
  // end of line
76
- match = new RegExp(SEP + miss + EOL).exec(code);
77
- if (match) return match.index + 1;
77
+ match = new RegExp(SEP + miss + EOL).exec(code)
78
+ if (match) return match.index + 1
78
79
  // start of doc
79
- match = new RegExp("^" + miss + SEP).exec(code);
80
- if (match) return match.index;
80
+ match = new RegExp('^' + miss + SEP).exec(code)
81
+ if (match) return match.index
81
82
  // somewhere (fallback)
82
- return code.indexOf(miss);
83
+ return code.indexOf(miss)
83
84
  }
84
85
 
85
- function _entity4(name, csn) {
86
- let entity = csn.definitions[name];
86
+ function _entity4 (name, csn) {
87
+ const entity = csn.definitions[name]
87
88
  if (!entity) {
88
89
  if (/(.+)[._]texts_?/.test(name)) {
89
90
  // 'Books.texts', 'Books.texts_de'
90
- const base = csn.definitions[RegExp.$1];
91
- return base && _entity4(base.elements.texts.target, csn);
92
- } else return;
91
+ const base = csn.definitions[RegExp.$1]
92
+ return base && _entity4(base.elements.texts.target, csn)
93
+ } else return
93
94
  }
94
95
  // we also support simple views if they have no projection
95
- const p = (entity.query && entity.query.SELECT) || entity.projection;
96
+ const p = (entity.query && entity.query.SELECT) || entity.projection
96
97
  if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
97
- if (csn.definitions[p.from.ref[0]]) return entity;
98
+ if (csn.definitions[p.from.ref[0]]) return entity
98
99
  }
99
- return entity.name ? entity : { name, __proto__: entity };
100
+ return entity.name ? entity : { name, __proto__: entity }
100
101
  }
package/lib/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface CDSRuleContext extends Rule.RuleContext {
9
9
  options: [];
10
10
  id: string;
11
11
  sourcecode: SourceCode;
12
+ getModel: function;
12
13
  report: (CDSRuleReport) => void;
13
14
  err: Error;
14
15
  }
@@ -2,32 +2,32 @@
2
2
  * Simple cache to store model and any cds calls made in the rule creation
3
3
  * api to modify the model
4
4
  */
5
- const cache = new Map();
5
+ const cache = new Map()
6
6
 
7
7
  module.exports = {
8
- has(key) {
9
- return cache.has(key);
8
+ has (key) {
9
+ return cache.has(key)
10
10
  },
11
- set(key, value) {
12
- return cache.set(key, [value, Date.now()]);
11
+ set (key, value) {
12
+ return cache.set(key, [value, Date.now()])
13
13
  },
14
- get(key) {
15
- return cache.get(key) ? cache.get(key)[0] : undefined;
14
+ get (key) {
15
+ return cache.get(key) ? cache.get(key)[0] : undefined
16
16
  },
17
- dump() {
18
- const dump = {};
17
+ dump () {
18
+ const dump = {}
19
19
  for (const [key, value] of cache.entries()) {
20
- const timestamp = new Date(value[1]);
21
- dump[key] = { key, value: JSON.stringify(value[0]), timestamp };
20
+ const timestamp = new Date(value[1])
21
+ dump[key] = { key, value: JSON.stringify(value[0]), timestamp }
22
22
  }
23
- return dump;
23
+ return dump
24
24
  },
25
- remove(key) {
25
+ remove (key) {
26
26
  if (cache.has(key)) {
27
- cache.delete(key);
27
+ cache.delete(key)
28
28
  }
29
29
  },
30
- clear() {
31
- cache.clear();
32
- },
30
+ clear () {
31
+ cache.clear()
32
+ }
33
33
  }
@@ -1,9 +1,9 @@
1
1
  module.exports = {
2
- reset: '\x1b[0m', // Default
3
- bold: '\x1b[1m', // Bold/Bright
4
- link: '\x1b[4m', // underline
5
- red: '\x1b[91m', // Bright Foreground Red
6
- green: '\x1b[32m', // Foreground Green
7
- blue: '\x1b[34m', // Foreground Blue
8
- orange: '\x1b[38;2;255;140;0m' // darker orange, works with bright and dark background
9
- }
2
+ reset: '\x1b[0m', // Default
3
+ bold: '\x1b[1m', // Bold/Bright
4
+ link: '\x1b[4m', // underline
5
+ red: '\x1b[91m', // Bright Foreground Red
6
+ green: '\x1b[32m', // Foreground Green
7
+ blue: '\x1b[34m', // Foreground Blue
8
+ orange: '\x1b[38;2;255;140;0m' // darker orange, works with bright and dark background
9
+ }