@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,10 +1,10 @@
1
- const { FILES } = require("../constants");
1
+ const { FILES } = require('../constants')
2
2
 
3
3
  /**
4
4
  * Returns an array of allowed file extensions
5
5
  * the plugin can parse of the form "*.ext"
6
- * @returns {ConfigOverrideFiles} Array of file extensions
6
+ * @returns { ConfigOverrideFiles } Array of file extensions
7
7
  */
8
8
  module.exports = () => {
9
- return FILES;
10
- };
9
+ return FILES
10
+ }
@@ -1,8 +1,8 @@
1
- const constants = require("../constants");
1
+ const constants = require('../constants')
2
2
 
3
3
  /**
4
4
  * Returns an array of allowed file extensions
5
5
  * the plugin can parse of the form "*.ext"
6
- * @returns {ConfigOverrideFiles} Array of file extensions
6
+ * @returns { ConfigOverrideFiles } Array of file extensions
7
7
  */
8
- module.exports = () => constants.FILES;
8
+ module.exports = () => constants.FILES
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Searches for directory containing cds roots
3
+ * @param {string} currentDir start here and search until root dir
4
+ * @returns {string} dir containing cds roots (empty if not exists)
5
+ */
6
+
7
+ const path = require('path')
8
+ const cds = require('@sap/cds')
9
+ const Cache = require('./Cache')
10
+
11
+ module.exports = (currentDir = '.') => {
12
+ let dir = path.resolve(currentDir)
13
+ while (dir !== path.resolve(dir, '..')) {
14
+ // @ts-ignore
15
+ cds.resolve.cache = {}
16
+ // @ts-ignore
17
+ const roots = cds.resolve('*', { root: dir })
18
+ if (roots && roots.length > 0) {
19
+ Cache.set(`roots:${dir}`, roots)
20
+ return dir
21
+ }
22
+ dir = path.join(dir, '..')
23
+ }
24
+ return ''
25
+ }
@@ -1,4 +1,4 @@
1
- const { FILES, MODEL_FILES } = require("../constants");
1
+ const { FILES, MODEL_FILES } = require('../constants')
2
2
 
3
3
  /**
4
4
  * Checks whether the given filePath matches a regex `files`
@@ -6,15 +6,15 @@ const { FILES, MODEL_FILES } = require("../constants");
6
6
  * @returns boolean
7
7
  */
8
8
  module.exports = (filePath, fileType) => {
9
- const genRegex = (key) => new RegExp(`${key.map((file) => file.replace("*", "")).join("$|")}$`);
10
- let isValid = false;
9
+ const genRegex = (key) => new RegExp(`${key.map((file) => file.replace('*', '')).join('$|')}$`)
10
+ let isValid = false
11
11
  switch (fileType) {
12
- case "MODEL_FILES":
13
- isValid = genRegex(MODEL_FILES).test(filePath);
14
- break;
15
- case "FILES":
16
- isValid = genRegex(FILES).test(filePath);
17
- break;
12
+ case 'MODEL_FILES':
13
+ isValid = genRegex(MODEL_FILES).test(filePath)
14
+ break
15
+ case 'FILES':
16
+ isValid = genRegex(FILES).test(filePath)
17
+ break
18
18
  }
19
- return isValid;
20
- }
19
+ return isValid
20
+ }
@@ -1,7 +1,7 @@
1
- const SEP = "[,;\t]";
2
- const EOL = "\\r?\\n";
1
+ const SEP = '[,;\t]'
2
+ const EOL = '\\r?\\n'
3
3
 
4
- const findFuzzy = require("./findFuzzy");
4
+ const findFuzzy = require('./findFuzzy')
5
5
 
6
6
  module.exports = {
7
7
  findFuzzy,
@@ -9,105 +9,105 @@ module.exports = {
9
9
  *
10
10
  * @param {*} e
11
11
  */
12
- splitEntityName: function (e) {
12
+ splitDefName: function (e) {
13
13
  // Entity names from CSN are of the form:
14
- // <namespace>.<service>.<entity>.<'texts'|'localized'>|<composition value>
15
- let prefix = "";
16
- let suffix = "";
17
- let entityName = e.name;
18
- const names = entityName.split(".");
19
- entityName = names[names.length - 1];
14
+ // <namespace>.<service>.<def>.<'texts'|'localized'>|<composition value>
15
+ let prefix = ''
16
+ let suffix = ''
17
+ let defName = e.name
18
+ const names = defName.split('.')
19
+ defName = names[names.length - 1]
20
20
 
21
- if (entityName) {
21
+ if (defName) {
22
22
  // Managed composition get compiler tag `_up`
23
- let isManagedComposition = false;
23
+ let isManagedComposition = false
24
24
  if (e.elements) {
25
- isManagedComposition = Object.keys(e.elements).some((k) => k === "up_");
25
+ isManagedComposition = Object.keys(e.elements).some((k) => k === 'up_')
26
26
  }
27
27
  // Check for compiler tags
28
- let compilerTagsToExclude = ["texts", "localized"];
29
- const isCompilerTag = compilerTagsToExclude.includes(entityName);
28
+ const compilerTagsToExclude = ['texts', 'localized']
29
+ const isCompilerTag = compilerTagsToExclude.includes(defName)
30
30
 
31
31
  if (isManagedComposition || isCompilerTag) {
32
- suffix = names[names.length - 1];
33
- entityName = names[names.length - 2];
32
+ suffix = names[names.length - 1]
33
+ defName = names[names.length - 2]
34
34
  }
35
- prefix = e.name.split(`.${entityName}`)[0];
35
+ prefix = e.name.split(`.${defName}`)[0]
36
36
  }
37
- return { prefix, entity: entityName, suffix };
37
+ return { prefix, name: defName, suffix }
38
38
  },
39
39
 
40
40
  _findInCode: function (miss, code) {
41
41
  // middle
42
- let match = new RegExp(SEP + miss + SEP).exec(code);
43
- if (match) return match.index + 1;
42
+ let match = new RegExp(SEP + miss + SEP).exec(code)
43
+ if (match) return match.index + 1
44
44
  // end of line
45
- match = new RegExp(SEP + miss + EOL).exec(code);
46
- if (match) return match.index + 1;
45
+ match = new RegExp(SEP + miss + EOL).exec(code)
46
+ if (match) return match.index + 1
47
47
  // start of doc
48
- match = new RegExp("^" + miss + SEP).exec(code);
49
- if (match) return match.index;
48
+ match = new RegExp('^' + miss + SEP).exec(code)
49
+ if (match) return match.index
50
50
  // somewhere (fallback)
51
- return code.indexOf(miss);
51
+ return code.indexOf(miss)
52
52
  },
53
53
 
54
54
  isEmptyString: function (value) {
55
- if (typeof value !== "string" || (typeof value === "string" && value && value.length > 0)) {
56
- return false;
55
+ if (typeof value !== 'string' || (typeof value === 'string' && value && value.length > 0)) {
56
+ return false
57
57
  }
58
- return true;
58
+ return true
59
59
  },
60
60
 
61
61
  isEmptyObject: function (value) {
62
- function isEmpty(object) {
63
- for (const property in object) {
64
- return false;
62
+ function isEmpty (object) {
63
+ if (Object.keys(object).length) {
64
+ return false
65
65
  }
66
- return true;
66
+ return true
67
67
  }
68
- if (typeof value !== "object" || (typeof value === "object" && !isEmpty(value)) ||
69
- (typeof value === "object" && value && value.length > 0)) {
70
- return false;
68
+ if (typeof value !== 'object' || (typeof value === 'object' && !isEmpty(value)) ||
69
+ (typeof value === 'object' && value && value.length > 0)) {
70
+ return false
71
71
  }
72
- return true;
72
+ return true
73
73
  },
74
74
 
75
- isStringInArray(str, arr, caps=false) {
76
- const notIncluded = !arr.includes(str);
75
+ isStringInArray (str, arr, caps = false) {
76
+ const notIncluded = !arr.includes(str)
77
77
  if (!caps && notIncluded) {
78
- return false;
78
+ return false
79
79
  }
80
- if (caps && notIncluded && !arr.includes(str.toLowerCase()) && !arr.includes(str.toUpperCase())) {
81
- return false;
80
+ if (caps && notIncluded && !arr.includes(str.toLowerCase()) && !arr.includes(str.toUpperCase())) {
81
+ return false
82
82
  }
83
- return true;
83
+ return true
84
84
  },
85
85
 
86
86
  getReplacementsSuggestions: function (context, value, loc) {
87
- let invalid;
88
- const lineToReplace = context.sourcecode.lines[loc.line];
89
- var regExp = /\[([^)]+)\]/;
90
- var matches = regExp.exec(lineToReplace);
87
+ let invalid
88
+ const lineToReplace = context.sourcecode.lines[loc.line]
89
+ const regExp = /\[([^)]+)\]/
90
+ const matches = regExp.exec(lineToReplace)
91
91
  if (matches && matches[0]) {
92
- invalid = matches[0];
92
+ invalid = matches[0]
93
93
  }
94
- const startIndex = lineToReplace.indexOf(invalid);
95
- const candidates = `['${value}']`;
94
+ const startIndex = lineToReplace.indexOf(invalid)
95
+ const candidates = `['${value}']`
96
96
  const suggest = {
97
- messageId: "ReplaceItemWith",
97
+ messageId: 'ReplaceItemWith',
98
98
  data: { invalid, candidates },
99
- fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates),
100
- };
99
+ fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates)
100
+ }
101
101
  return ({
102
- messageId: "InvalidItem",
102
+ messageId: 'InvalidItem',
103
103
  data: { invalid, candidates },
104
104
  loc: {
105
105
  start: { line: loc.line + 1, column: startIndex },
106
- end: { line: loc.line + 1, column: startIndex + invalid.length },
106
+ end: { line: loc.line + 1, column: startIndex + invalid.length }
107
107
  },
108
108
  file: loc.file,
109
109
  suggest,
110
- severity: "warn",
111
- });
112
- },
113
- };
110
+ severity: 'warn'
111
+ })
112
+ }
113
+ }
@@ -1,11 +1,11 @@
1
- const fs = require("fs");
2
- const path = require("path");
1
+ const fs = require('fs')
2
+ const path = require('path')
3
3
 
4
- const { RuleTester } = require("eslint");
5
- const Cache = require("./Cache");
6
- const createRule = require("./createRule");
7
- const isConfiguredFileType = require("./isConfiguredFileType");
8
- const { compileModelFromDict } = require("../parser");
4
+ const { RuleTester } = require('eslint')
5
+ const Cache = require('./Cache')
6
+ const createRule = require('./createRule')
7
+ const isConfiguredFileType = require('./isConfiguredFileType')
8
+ const { compileModelFromDict } = require('../parser')
9
9
 
10
10
  /**
11
11
  * ESLint RuleTester (used by custom rule creator api)
@@ -13,99 +13,104 @@ const { compileModelFromDict } = require("../parser");
13
13
  * valid/invalid checks:
14
14
  * Model checks require input 'code' entries
15
15
  * Env checks require input 'options' with selected parameters
16
- * @param {CDSRuleTestOpts} options RuleTester input options
16
+ * @param { CDSRuleTestOpts } options RuleTester input options
17
17
  * @returns RuleTester results
18
18
  */
19
19
  module.exports = (options) => {
20
- let parser;
21
- let rule = {};
22
- Cache.set("rules", require(path.join(__dirname, "../rules")));
23
- const rulename = path.basename(options.root);
24
- if (options.root.startsWith(path.resolve(__dirname, "../.."))) {
20
+ let parser
21
+ let rule = {}
22
+ Cache.set('rules', require(path.join(__dirname, '../rules')))
23
+ const rulename = path.basename(options.root)
24
+ if (options.root.startsWith(path.resolve(__dirname, '../..'))) {
25
25
  // For plugin's internal tests, resolve parser from here
26
- parser = require.resolve("../parser");
27
- const pluginPath = path.join(path.dirname(options.root), "../..");
28
- rule = createRule(require(`../rules/${path.basename(options.root)}`));
29
- Cache.set("pluginpath", pluginPath);
26
+ parser = require.resolve('../parser')
27
+ const pluginPath = path.join(path.dirname(options.root), '../..')
28
+ rule = createRule(require(`../rules/${path.basename(options.root)}`))
29
+ Cache.set('pluginpath', pluginPath)
30
30
  } else {
31
31
  // Otherwise from project root
32
- const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
33
- paths: [options.root],
34
- });
35
- parser = path.join(path.dirname(resolvedPlugin), "parser");
36
- rule = require(path.join(options.root, `../rules/${path.basename(options.root)}`));
37
- const pluginPath = path.join(path.dirname(options.root), "../../..");
38
- Cache.set("pluginpath", pluginPath);
32
+ const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
33
+ paths: [options.root]
34
+ })
35
+ parser = path.join(path.dirname(resolvedPlugin), 'parser')
36
+ rule = require(path.join(options.root, `../../rules/${path.basename(options.root)}`))
37
+ const pluginPath = path.join(path.dirname(options.root), '../../..')
38
+ Cache.set('pluginpath', pluginPath)
39
39
  }
40
- let tester = new RuleTester({});
40
+ let tester = new RuleTester({})
41
41
  if (parser) {
42
- tester = new RuleTester({ parser });
42
+ tester = new RuleTester({ parser })
43
43
  }
44
44
  const testerCases = {};
45
- ["valid", "invalid"].forEach((type) => {
46
- const filePath = path.join(options.root, `${type}/${options.filename}`);
47
- Cache.set("rootpath", path.dirname(filePath));
48
- _initModelRuleTester(filePath);
45
+ ['valid', 'invalid'].forEach((type) => {
46
+ const filePath = path.join(options.root, `${type}/${options.filename}`)
47
+ Cache.set('rootpath', path.dirname(filePath))
48
+ _initModelRuleTester(filePath, rule.meta?.model)
49
49
  testerCases[type] = [
50
50
  {
51
- filename: filePath,
52
- },
53
- ];
54
- if (!isConfiguredFileType(options.filename, "FILES")) {
55
- const fileContents = JSON.parse(fs.readFileSync(filePath, "utf8"));
56
- testerCases[type][0].code = "";
57
- testerCases[type][0].filename = "<text>";
58
- testerCases[type][0].options = [{ environment: fileContents }];
51
+ filename: filePath
52
+ }
53
+ ]
54
+ if (!isConfiguredFileType(options.filename, 'FILES')) {
55
+ const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
56
+ testerCases[type][0].code = ''
57
+ testerCases[type][0].filename = '<text>'
58
+ testerCases[type][0].options = [{ environment: fileContents }]
59
59
  } else {
60
- testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
60
+ testerCases[type][0].code = fs.readFileSync(filePath, 'utf8')
61
61
  if (options.options) {
62
- testerCases[type][0].options = options.options;
62
+ testerCases[type][0].options = options.options
63
63
  }
64
64
  }
65
- if (type === "invalid") {
66
- testerCases[type][0].errors = options.errors;
67
- const fileFixed = path.join(options.root, `fixed/${options.filename}`);
68
- if (fs.existsSync(fileFixed) && rule.meta.type !== "suggestion") {
69
- testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
65
+ if (type === 'invalid') {
66
+ testerCases[type][0].errors = options.errors
67
+ const fileFixed = path.join(options.root, `fixed/${options.filename}`)
68
+ if (fs.existsSync(fileFixed) && rule.meta.type !== 'suggestion') {
69
+ testerCases[type][0].output = fs.readFileSync(fileFixed, 'utf8')
70
70
  }
71
71
  }
72
- });
73
- return tester.run(rulename, rule, testerCases);
72
+ })
73
+ if (Cache.get('testerCases')) {
74
+ Cache.set(`testerCases:${rulename}`, testerCases)
75
+ }
76
+ return tester.run(rulename, rule, testerCases)
74
77
  }
75
78
 
76
79
  /**
77
80
  * Creates a model for ESLint unit tests
78
81
  */
79
- function _initModelRuleTester(filePath) {
80
- Cache.set("test", true);
81
- const rootPath = path.dirname(filePath);
82
- Cache.set("rootpath", rootPath);
83
- let files = fs.readdirSync(rootPath);
84
- const modelfiles = files.map((f) => path.join(rootPath, f)).filter((fp) => isConfiguredFileType(fp, "MODEL_FILES"));
85
- Cache.set(`modelfiles:${rootPath}`, modelfiles);
86
- const dictFiles = _getDictFiles(rootPath, modelfiles);
87
- Cache.set(`dictfiles:${rootPath}`, dictFiles);
88
- const reflectedModel = compileModelFromDict(dictFiles);
89
- Cache.set(`model:${rootPath}`, reflectedModel);
82
+ function _initModelRuleTester (filePath, flavor) {
83
+ Cache.set('test', true)
84
+ const rootPath = path.dirname(filePath)
85
+ Cache.set('rootpath', rootPath)
86
+ if (flavor !== 'none') { // not for env rules
87
+ const files = fs.readdirSync(rootPath)
88
+ const modelfiles = files.map((f) => path.join(rootPath, f)).filter((fp) => isConfiguredFileType(fp, 'MODEL_FILES'))
89
+ Cache.set(`modelfiles:${rootPath}`, modelfiles)
90
+ const dictFiles = _getDictFiles(rootPath, modelfiles)
91
+ Cache.set(`dictfiles:${rootPath}`, dictFiles)
92
+ const reflectedModel = compileModelFromDict(dictFiles, { flavor })
93
+ Cache.set(`model:${rootPath}`, reflectedModel)
94
+ }
90
95
  }
91
96
 
92
- /**
97
+ /**
93
98
  * Creates or updates a dictionary of files/file contents for a given
94
99
  * project path.
95
100
  * @param input
96
101
  * @param files
97
102
  * @returns dictFiles
98
103
  */
99
- function _getDictFiles(input, files) {
100
- let dictFiles = {};
101
- if (Cache.has(`dictfiles:${input}`)) {
102
- dictFiles = Cache.get(`dictfiles:${input}`);
103
- } else {
104
- files.forEach((file) => {
105
- dictFiles[file] = Cache.has(`file:${file}`)
106
- ? Cache.get(`file:${file}`)
107
- : fs.readFileSync(file, "utf8")
108
- });
109
- }
110
- return dictFiles;
104
+ function _getDictFiles (input, files) {
105
+ let dictFiles = {}
106
+ if (Cache.has(`dictfiles:${input}`)) {
107
+ dictFiles = Cache.get(`dictfiles:${input}`)
108
+ } else {
109
+ files.forEach((file) => {
110
+ dictFiles[file] = Cache.has(`file:${file}`)
111
+ ? Cache.get(`file:${file}`)
112
+ : fs.readFileSync(file, 'utf8')
113
+ })
111
114
  }
115
+ return dictFiles
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
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": [
@@ -23,6 +23,12 @@
23
23
  "@sap/cds": ">=5.6.0",
24
24
  "semver": "^7.3.4"
25
25
  },
26
+ "eslintConfig": {
27
+ "extends": [
28
+ "eslint:recommended",
29
+ "standard"
30
+ ]
31
+ },
26
32
  "peerDependencies": {
27
33
  "eslint": ">=7"
28
34
  },
@@ -1,25 +0,0 @@
1
- module.exports = {
2
- meta: {
3
- docs: {
4
- description: `Draft-enabled entities shall not be used in views that make use of \`JOIN\`.`,
5
- recommended: true
6
- },
7
- type: "suggestion",
8
- model: "inferred"
9
- },
10
- create: function (context) {
11
- return { entity: check_nojoin_draftenabled };
12
-
13
- function check_nojoin_draftenabled(e) {
14
- if (e["@odata.draft.enabled"]) {
15
- if (e.query.SELECT.from.join) {
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
- });
21
- }
22
- }
23
- }
24
- }
25
- };