@sap/eslint-plugin-cds 2.4.1 → 2.5.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/lib/api/index.js +9 -8
  3. package/lib/conf/all.js +21 -0
  4. package/lib/conf/index.js +22 -0
  5. package/lib/conf/recommended.js +18 -0
  6. package/lib/constants.js +10 -8
  7. package/lib/index.js +11 -32
  8. package/lib/parser.js +158 -7
  9. package/lib/rules/assoc2many-ambiguous-key.js +21 -38
  10. package/lib/rules/auth-no-empty-restrictions.js +13 -21
  11. package/lib/rules/auth-use-requires.js +15 -15
  12. package/lib/rules/auth-valid-restrict-grant.js +71 -34
  13. package/lib/rules/auth-valid-restrict-keys.js +22 -16
  14. package/lib/rules/auth-valid-restrict-to.js +71 -27
  15. package/lib/rules/auth-valid-restrict-where.js +24 -15
  16. package/lib/rules/index.js +26 -0
  17. package/lib/rules/latest-cds-version.js +8 -7
  18. package/lib/rules/min-node-version.js +10 -9
  19. package/lib/rules/no-db-keywords.js +16 -15
  20. package/lib/rules/no-dollar-prefixed-names.js +9 -7
  21. package/lib/rules/no-join-on-draft-enabled-entities.js +9 -24
  22. package/lib/rules/require-2many-oncond.js +9 -14
  23. package/lib/rules/sql-cast-suggestion.js +6 -20
  24. package/lib/rules/start-elements-lowercase.js +5 -8
  25. package/lib/rules/start-entities-uppercase.js +8 -11
  26. package/lib/rules/valid-csv-header.js +66 -66
  27. package/lib/{api/lint.d.ts → types.d.ts} +4 -7
  28. package/lib/utils/Cache.js +33 -0
  29. package/lib/utils/Colors.js +9 -0
  30. package/lib/utils/createRule.js +304 -0
  31. package/lib/utils/createRuleDocs.js +361 -0
  32. package/lib/utils/{fuzzySearch.js → findFuzzy.js} +0 -2
  33. package/lib/utils/genDocs.js +363 -0
  34. package/lib/utils/getConfigPath.js +33 -0
  35. package/lib/utils/getConfiguredFileTypes.js +10 -0
  36. package/lib/utils/getFileExtensions.js +8 -0
  37. package/lib/utils/isConfiguredFileType.js +20 -0
  38. package/lib/utils/jsonc.js +1 -1
  39. package/lib/utils/jsoncParser.js +1 -0
  40. package/lib/utils/rules.js +112 -1041
  41. package/lib/utils/runRuleTester.js +111 -0
  42. package/package.json +4 -4
  43. package/lib/processor.js +0 -50
  44. package/lib/utils/helpers.js +0 -94
  45. package/lib/utils/model.js +0 -393
  46. package/lib/utils/ruleHelpers.js +0 -199
  47. package/lib/utils/ruleTester.js +0 -78
  48. package/lib/utils/validate.js +0 -36
@@ -2,39 +2,24 @@ module.exports = {
2
2
  meta: {
3
3
  docs: {
4
4
  description: `Draft-enabled entities shall not be used in views that make use of \`JOIN\`.`,
5
- category: "Model Validation",
6
- recommended: true,
7
- version: "2.2.1",
5
+ recommended: true
8
6
  },
9
- severity: "warn",
10
7
  type: "suggestion",
11
- messages: {
12
- noJoinOnDraftEnabledEntities: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
13
- },
8
+ model: "inferred"
14
9
  },
15
10
  create: function (context) {
16
-
17
- return { entity: check_nojoin_draftenabled }
11
+ return { entity: check_nojoin_draftenabled };
18
12
 
19
13
  function check_nojoin_draftenabled(e) {
20
14
  if (e["@odata.draft.enabled"]) {
21
15
  if (e.query.SELECT.from.join) {
22
- const location = e.query.$location;
23
- if (context.sourcecode.lines[location.line - 1]) {
24
- const endCol = context.sourcecode.lines[location.line - 1].length;
25
- const loc = {
26
- start: { line: location.line, column: location.col - 1 },
27
- end: { line: location.line, column: endCol },
28
- };
29
- return {
30
- messageId: "noJoinOnDraftEnabledEntities",
31
- loc,
32
- file: e.$location.file,
33
- };
34
- }
16
+ context.report({
17
+ message: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
18
+ node: context.getNode(e),
19
+ file: e.$location.file
20
+ });
35
21
  }
36
22
  }
37
23
  }
38
-
39
- },
24
+ }
40
25
  };
@@ -2,26 +2,21 @@ module.exports = {
2
2
  meta: {
3
3
  docs: {
4
4
  description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
5
- category: "Model Validation",
6
- recommended: true,
7
- version: "2.1.0",
5
+ recommended: true
8
6
  },
9
- severity: "error",
10
- type: "problem",
7
+ type: "problem"
11
8
  },
12
9
  create: function (context) {
13
-
14
- return { element: check_2many_oncond }
10
+ return { element: check_2many_oncond };
15
11
 
16
12
  function check_2many_oncond(e) {
17
- if (e.is2many && !e.on && typeof e.target === 'string') {
18
- const loc = context.cds.getLocation(e.name, e);
19
- return {
13
+ if (e.is2many && !e.on && typeof e.target === "string") {
14
+ context.report({
20
15
  message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${e.name}'.`,
21
- loc,
22
- file: e.parent.$location.file,
23
- };
16
+ node: context.getNode(e)
17
+ });
24
18
  }
25
19
  }
26
- },
20
+
21
+ }
27
22
  };
@@ -3,11 +3,8 @@ module.exports = {
3
3
  meta: {
4
4
  docs: {
5
5
  description: "Should make suggestions for possible missing SQL casts.",
6
- category: "Model Validation",
7
- recommended: true,
8
- version: "1.0.8",
6
+ recommended: true
9
7
  },
10
- severity: "warn",
11
8
  type: "suggestion",
12
9
  hasSuggestions: true,
13
10
  messages: {
@@ -18,35 +15,24 @@ module.exports = {
18
15
  return { view: check_sql_cast };
19
16
 
20
17
  function check_sql_cast(v) {
21
- const reports = [];
22
18
  if (v.query && v.query.SET) {
23
19
  for (const { SELECT } of v.query.SET.args) {
24
20
  // Only in UNION cases?
25
21
  for (const each of SELECT.columns || []) {
26
- const { xpr, cast, $location: location } = each;
22
+ const { xpr, cast } = each;
27
23
  if (cast && xpr) {
28
24
  if (xpr[0].xpr && xpr[0].cast) {
29
25
  continue;
30
26
  } else {
31
- if (context.sourcecode.lines[location.line - 1]) {
32
- const endCol = context.sourcecode.lines[location.line - 1].length;
33
- const loc = {
34
- start: { line: location.line, column: location.col - 1 },
35
- end: { line: location.line, column: endCol },
36
- };
37
- reports.push({
38
- messageId: "missingSQLCast",
39
- loc,
40
- file: location.file,
41
- });
42
- }
27
+ context.report({
28
+ messageId: "missingSQLCast",
29
+ node: context.getNode(v)
30
+ });
43
31
  }
44
32
  }
45
33
  }
46
34
  }
47
35
  }
48
- if (reports.length > 0 ) return reports;
49
36
  }
50
-
51
37
  },
52
38
  };
@@ -1,9 +1,7 @@
1
1
  module.exports = {
2
2
  meta: {
3
3
  docs: {
4
- description: "Regular element names should start with lowercase letters.",
5
- category: "Model Validation",
6
- version: "1.0.4",
4
+ description: "Regular element names should start with lowercase letters."
7
5
  },
8
6
  type: "suggestion",
9
7
  hasSuggestions: true,
@@ -14,21 +12,20 @@ module.exports = {
14
12
  fixable: "code",
15
13
  },
16
14
  create: function (context) {
17
- const { cds, filePath, sourcecode } = context;
15
+ const sourcecode = context.getSourceCode();
18
16
 
19
17
  return {
20
18
  element: check_start_lowercase,
21
19
  };
22
20
 
23
21
  function check_start_lowercase(e) {
24
- if (!filePath.endsWith(".cds")) return;
25
22
  const elementName = e.name;
26
23
  const entityName = e.parent.name;
27
24
  if (elementName && !(entityName.startsWith("localized") || entityName.endsWith("texts"))) {
28
25
  if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !["ID"].includes(elementName)) {
29
26
  if (e.$location && e.$location.file) {
30
27
  const file = e.$location.file;
31
- const loc = cds.getLocation(elementName, e);
28
+ const loc = context.getLocation(elementName, e);
32
29
  const fix = (fixer, source = sourcecode) => {
33
30
  const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
34
31
  const rangeEnd = source.getIndexFromLoc({
@@ -38,7 +35,7 @@ module.exports = {
38
35
  const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0;
39
36
  return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
40
37
  };
41
- return {
38
+ context.report({
42
39
  messageId: "startLowercase",
43
40
  loc,
44
41
  file,
@@ -52,7 +49,7 @@ module.exports = {
52
49
  fix,
53
50
  },
54
51
  ],
55
- };
52
+ });
56
53
  }
57
54
  }
58
55
  }
@@ -1,11 +1,9 @@
1
- const { splitEntityName } = require("../utils/ruleHelpers");
1
+ const { splitEntityName } = require("../utils/rules");
2
2
 
3
3
  module.exports = {
4
4
  meta: {
5
5
  docs: {
6
- description: "Regular entity names should start with uppercase letters.",
7
- category: "Model Validation",
8
- version: "1.0.4",
6
+ description: "Regular entity names should start with uppercase letters."
9
7
  },
10
8
  type: "suggestion",
11
9
  hasSuggestions: true,
@@ -16,27 +14,26 @@ module.exports = {
16
14
  fixable: "code",
17
15
  },
18
16
  create: function (context) {
19
- const { cds, filePath, sourcecode } = context;
17
+ const sourcecode = context.getSourceCode();
20
18
 
21
19
  return { entity: check_starts_uppercase };
22
20
 
23
21
  function check_starts_uppercase(e) {
24
- if (!filePath.endsWith(".cds")) return;
25
22
  const entityName = splitEntityName(e).entity;
26
23
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
27
24
  if (e.$location && e.$location.file) {
28
25
  const file = e.$location.file;
29
- const loc = cds.getLocation(entityName, e);
30
- const fix = (fixer, source = sourcecode) => {
26
+ const loc = context.getLocation(entityName, e);
27
+ const fix = (fixer) => {
31
28
  const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
32
- const rangeEnd = source.getIndexFromLoc({
29
+ const rangeEnd = sourcecode.getIndexFromLoc({
33
30
  line: loc.end.line,
34
31
  column: loc.end.column,
35
32
  });
36
33
  const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0;
37
34
  return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
38
35
  };
39
- return {
36
+ context.report({
40
37
  messageId: "startUppercase",
41
38
  loc,
42
39
  file,
@@ -47,7 +44,7 @@ module.exports = {
47
44
  fix,
48
45
  },
49
46
  ],
50
- };
47
+ });
51
48
  }
52
49
  }
53
50
  }
@@ -1,100 +1,100 @@
1
- const {basename, extname} = require('path')
2
- const findFuzzy = require('../utils/fuzzySearch')
3
- const SEP = '[,;\t]'
4
- 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";
5
6
 
6
7
  module.exports = {
7
8
  meta: {
8
9
  docs: {
9
10
  description: `CSV files for entities must refer to valid element names.`,
10
11
  category: "Model Validation",
11
- recommended: true,
12
- version: "2.3.0",
12
+ recommended: true
13
13
  },
14
14
  severity: "warn",
15
15
  type: "problem",
16
16
  hasSuggestions: true,
17
17
  messages: {
18
18
  InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
19
- ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`
20
- }
19
+ ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`,
20
+ },
21
+ model: "inferred",
21
22
  },
22
23
  create: function (context) {
23
-
24
- return { all: check_valid_headers }
24
+ return check_valid_headers;
25
25
 
26
26
  function check_valid_headers() {
27
- const reports = [];
28
- const {cds, code, filePath, sourcecode} = context;
27
+ const filePath = context.getFilename();
28
+ const sourcecode = context.getSourceCode();
29
+ const code = sourcecode.getText();
29
30
 
30
- if (!filePath.endsWith('.csv')) return
31
- if (!cds.model) return
32
- let { env, model } = cds;
33
- model = cds.compile.for.sql(model, { names:env.sql.names, messages: [] } )
31
+ let model = context.getModel();
32
+ if (!filePath.endsWith(".csv")) return;
33
+ if (!model) return;
34
34
 
35
- const filename = basename(filePath)
36
- const entityName = filename.replace(/-/g,'.').slice(0, -extname(filename).length)
37
- const entity = _entity4(entityName, model);
38
- if (!entity) return
35
+ model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] });
39
36
 
40
- const elements = Object.values(entity.elements)
41
- .filter (e => !!e['@cds.persistence.name'])
42
- .map (e => e['@cds.persistence.name'].toUpperCase())
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;
43
41
 
44
- const [ cols ] = cds.parse.csv(code);
45
- const missing = cols.filter (col => !elements.includes(col.toUpperCase()))
46
- missing.forEach(miss => {
47
- const index = _findInCode (miss, code)
48
- const loc = sourcecode.getLocFromIndex(index)
49
- const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
50
- const suggest = candidates.map(cand => { return {
51
- messageId: 'ReplaceColumnWith',
52
- data: {column: miss, candidates:cand},
53
- fix: (fixer) => fixer.replaceTextRange([index, index+miss.length], cand)
54
- }})
55
- reports.push({
56
- messageId: 'InvalidColumn',
57
- data: {column: miss, candidates},
58
- loc: {start: loc, end: {line: loc.line, column: loc.column+miss.length}},
59
- file: filePath,
60
- suggest
61
- })
62
- })
63
- return reports
64
- }
65
-
66
- }
42
+ const elements = Object.values(entity.elements)
43
+ .filter((e) => !!e["@cds.persistence.name"])
44
+ .map((e) => e["@cds.persistence.name"].toUpperCase());
67
45
 
68
- }
46
+ const [cols] = cds.parse.csv(code);
47
+ const missing = cols.filter((col) => !elements.includes(col.toUpperCase()));
48
+ 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());
52
+ const suggest = candidates.map((cand) => {
53
+ return {
54
+ messageId: "ReplaceColumnWith",
55
+ data: { column: miss, candidates: cand },
56
+ fix: (fixer) => fixer.replaceTextRange([index, index + miss.length], cand),
57
+ };
58
+ });
59
+ context.report({
60
+ messageId: "InvalidColumn",
61
+ data: { column: miss, candidates },
62
+ loc: { start: loc, end: { line: loc.line, column: loc.column + miss.length } },
63
+ file: filePath,
64
+ suggest,
65
+ });
66
+ }
67
+ }
68
+ },
69
+ };
69
70
 
70
- function _findInCode (miss, code) {
71
+ function _findInCode(miss, code) {
71
72
  // middle
72
- let match = new RegExp(SEP+miss+SEP).exec(code)
73
- if (match) return match.index+1
73
+ let match = new RegExp(SEP + miss + SEP).exec(code);
74
+ if (match) return match.index + 1;
74
75
  // end of line
75
- match = new RegExp(SEP+miss+EOL).exec(code)
76
- if (match) return match.index+1
76
+ match = new RegExp(SEP + miss + EOL).exec(code);
77
+ if (match) return match.index + 1;
77
78
  // start of doc
78
- match = new RegExp('^'+miss+SEP).exec(code)
79
- if (match) return match.index
79
+ match = new RegExp("^" + miss + SEP).exec(code);
80
+ if (match) return match.index;
80
81
  // somewhere (fallback)
81
- return code.indexOf(miss)
82
+ return code.indexOf(miss);
82
83
  }
83
84
 
84
- function _entity4 (name, csn) {
85
- let entity = csn.definitions [name]
85
+ function _entity4(name, csn) {
86
+ let entity = csn.definitions[name];
86
87
  if (!entity) {
87
- if (/(.+)[._]texts_?/.test (name)) { // 'Books.texts', 'Books.texts_de'
88
- const base = csn.definitions [RegExp.$1]
89
- return base && _entity4 (base.elements.texts.target, csn)
90
- }
91
- else return
88
+ if (/(.+)[._]texts_?/.test(name)) {
89
+ // 'Books.texts', 'Books.texts_de'
90
+ const base = csn.definitions[RegExp.$1];
91
+ return base && _entity4(base.elements.texts.target, csn);
92
+ } else return;
92
93
  }
93
94
  // we also support simple views if they have no projection
94
- const p = entity.query && entity.query.SELECT || entity.projection
95
+ const p = (entity.query && entity.query.SELECT) || entity.projection;
95
96
  if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
96
- if (csn.definitions [p.from.ref[0]]) return entity
97
+ if (csn.definitions[p.from.ref[0]]) return entity;
97
98
  }
98
- return entity.name ? entity : { name, __proto__:entity }
99
+ return entity.name ? entity : { name, __proto__: entity };
99
100
  }
100
-
@@ -3,7 +3,7 @@ import { Linter, Rule, RuleTester, SourceCode } from "eslint";
3
3
 
4
4
  export interface CDSRuleContext extends Rule.RuleContext {
5
5
  cds: any;
6
- configPath: string;
6
+ rootPath: string;
7
7
  code: string;
8
8
  filePath: string;
9
9
  options: [];
@@ -13,16 +13,13 @@ export interface CDSRuleContext extends Rule.RuleContext {
13
13
  err: Error;
14
14
  }
15
15
 
16
- export interface CDSRuleSpec {
17
- meta: CDSRuleMetaData,
16
+ export interface Rule {
17
+ meta: RuleMetaData,
18
18
  create: (context: CDSRuleContext) => void;
19
19
  }
20
20
 
21
21
  export interface CDSRuleMetaData extends Rule.RuleMetaData {
22
- docs: Rule.RuleMetaData['docs'] & {
23
- version: string;
24
- };
25
- severity?: Linter.RuleLevel;
22
+ model?: "parsed" | "inferred" | "none";
26
23
  }
27
24
 
28
25
  export type CDSRuleReport = Rule.ReportDescriptor & {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Simple cache to store model and any cds calls made in the rule creation
3
+ * api to modify the model
4
+ */
5
+ const cache = new Map();
6
+
7
+ module.exports = {
8
+ has(key) {
9
+ return cache.has(key);
10
+ },
11
+ set(key, value) {
12
+ return cache.set(key, [value, Date.now()]);
13
+ },
14
+ get(key) {
15
+ return cache.get(key) ? cache.get(key)[0] : undefined;
16
+ },
17
+ dump() {
18
+ const dump = {};
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 };
22
+ }
23
+ return dump;
24
+ },
25
+ remove(key) {
26
+ if (cache.has(key)) {
27
+ cache.delete(key);
28
+ }
29
+ },
30
+ clear() {
31
+ cache.clear();
32
+ },
33
+ }
@@ -0,0 +1,9 @@
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
+ }