@sap/eslint-plugin-cds 2.2.2 → 2.3.3

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 (44) hide show
  1. package/CHANGELOG.md +32 -106
  2. package/lib/api/index.js +11 -13
  3. package/lib/api/lint.d.ts +48 -0
  4. package/lib/constants.js +54 -0
  5. package/lib/index.js +44 -0
  6. package/lib/{impl/parser.js → parser.js} +2 -13
  7. package/lib/processor.js +47 -0
  8. package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +51 -52
  9. package/lib/rules/latest-cds-version.js +42 -0
  10. package/lib/rules/min-node-version.js +47 -0
  11. package/lib/rules/no-db-keywords.js +46 -0
  12. package/lib/rules/no-dollar-prefixed-names.js +47 -0
  13. package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +16 -11
  14. package/lib/rules/require-2many-oncond.js +27 -0
  15. package/lib/rules/sql-cast-suggestion.js +52 -0
  16. package/lib/rules/start-elements-lowercase.js +61 -0
  17. package/lib/rules/start-entities-uppercase.js +55 -0
  18. package/lib/rules/valid-csv-header.js +100 -0
  19. package/lib/utils/fuzzySearch.js +87 -0
  20. package/lib/utils/helpers.js +55 -0
  21. package/lib/{impl/utils → utils}/jsonc.js +0 -0
  22. package/lib/{impl/utils → utils}/model.js +122 -216
  23. package/lib/utils/ruleHelpers.js +56 -0
  24. package/lib/utils/ruleTester.js +79 -0
  25. package/lib/utils/rules.js +1033 -0
  26. package/lib/{impl/utils → utils}/validate.js +8 -21
  27. package/package.json +3 -3
  28. package/lib/api/formatter.js +0 -182
  29. package/lib/impl/constants.js +0 -40
  30. package/lib/impl/index.js +0 -58
  31. package/lib/impl/processor.js +0 -23
  32. package/lib/impl/ruleFactory.js +0 -311
  33. package/lib/impl/rules/cds-compile-error.js +0 -35
  34. package/lib/impl/rules/latest-cds-version.js +0 -46
  35. package/lib/impl/rules/min-node-version.js +0 -42
  36. package/lib/impl/rules/no-db-keywords.js +0 -35
  37. package/lib/impl/rules/require-2many-oncond.js +0 -29
  38. package/lib/impl/rules/rule.hbs +0 -20
  39. package/lib/impl/rules/sql-cast-suggestion.js +0 -50
  40. package/lib/impl/rules/start-elements-lowercase.js +0 -74
  41. package/lib/impl/rules/start-entities-uppercase.js +0 -65
  42. package/lib/impl/types.d.ts +0 -48
  43. package/lib/impl/utils/helpers.js +0 -68
  44. package/lib/impl/utils/rules.js +0 -550
@@ -0,0 +1,100 @@
1
+ const {basename, extname} = require('path')
2
+ const findFuzzy = require('../utils/fuzzySearch')
3
+ const SEP = '[,;\t]'
4
+ const EOL = '\\r?\\n'
5
+
6
+ module.exports = {
7
+ meta: {
8
+ docs: {
9
+ description: `CSV files for entities must refer to valid element names.`,
10
+ category: "Model Validation",
11
+ recommended: true,
12
+ version: "2.3.0",
13
+ },
14
+ severity: "warn",
15
+ type: "problem",
16
+ hasSuggestions: true,
17
+ messages: {
18
+ InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
19
+ ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`
20
+ }
21
+ },
22
+ create: function (context) {
23
+
24
+ return { all: check_valid_headers }
25
+
26
+ function check_valid_headers() {
27
+ const reports = [];
28
+ const {cds, code, filePath, sourcecode} = context;
29
+
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: [] } )
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
39
+
40
+ const elements = Object.values(entity.elements)
41
+ .filter (e => !!e['@cds.persistence.name'])
42
+ .map (e => e['@cds.persistence.name'].toUpperCase())
43
+
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
+ }
67
+
68
+ }
69
+
70
+ function _findInCode (miss, code) {
71
+ // middle
72
+ let match = new RegExp(SEP+miss+SEP).exec(code)
73
+ if (match) return match.index+1
74
+ // end of line
75
+ match = new RegExp(SEP+miss+EOL).exec(code)
76
+ if (match) return match.index+1
77
+ // start of doc
78
+ match = new RegExp('^'+miss+SEP).exec(code)
79
+ if (match) return match.index
80
+ // somewhere (fallback)
81
+ return code.indexOf(miss)
82
+ }
83
+
84
+ function _entity4 (name, csn) {
85
+ let entity = csn.definitions [name]
86
+ 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
92
+ }
93
+ // we also support simple views if they have no projection
94
+ const p = entity.query && entity.query.SELECT || entity.projection
95
+ if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
96
+ if (csn.definitions [p.from.ref[0]]) return entity
97
+ }
98
+ return entity.name ? entity : { name, __proto__:entity }
99
+ }
100
+
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Levenshtein distance algorithm using recursive calls and cache
3
+ *
4
+ * @param input search the list for a best match for this string
5
+ * @param list a list of strings to match input against
6
+ * @param log logging method to use, might be null if no logging is wanted
7
+ * @returns array with best matches, is never null but might be empty in case no search was possible
8
+ */
9
+
10
+
11
+ const cache = {};
12
+
13
+
14
+ module.exports = (input, list, log) => {
15
+ let minDistWords = [];
16
+
17
+ if (input.length > 50 || list.length > 50) {
18
+ return minDistWords;
19
+ }
20
+
21
+ let minDist = Number.MAX_SAFE_INTEGER;
22
+
23
+ log && log('\nword\t\tlevDist\t\ttime(ms)');
24
+
25
+ let runtime = 0;
26
+
27
+ for (const word of list) {
28
+ const start = log && Date.now();
29
+ const levDist = levDistance(input, word);
30
+
31
+ if (log) {
32
+ const duration = Date.now() - start;
33
+ runtime = runtime + duration;
34
+ log(`${word}\t\t${levDist}\t\t${duration}`);
35
+ }
36
+
37
+ if (levDist === minDist) {
38
+ minDistWords.push(word);
39
+ }
40
+
41
+ if (levDist < minDist) {
42
+ minDist = levDist;
43
+ minDistWords = [word];
44
+ }
45
+ }
46
+
47
+ log && log(`runtime: ${runtime}ms`);
48
+
49
+ return minDistWords.sort();
50
+ }
51
+
52
+
53
+ const levDistance = (a, b) => {
54
+
55
+ if (cache[a] && cache[a][b]) {
56
+ return cache[a][b];
57
+ }
58
+
59
+ if (a.length === 0) {
60
+ return addToCache(a, b, b.length);
61
+ }
62
+
63
+ if (b.length === 0) {
64
+ return addToCache(a, b, a.length);
65
+ }
66
+
67
+ const tail_a = a.substring(1);
68
+ const tail_b = b.substring(1);
69
+
70
+ if (a[0] === b[0]) {
71
+ return levDistance(tail_a, tail_b);
72
+ }
73
+
74
+ const lev1 = levDistance(tail_a, b);
75
+ const lev2 = levDistance(a, tail_b);
76
+ const lev3 = levDistance(tail_a, tail_b);
77
+
78
+ const levDist = Math.min(lev1, lev2, lev3) + 1;
79
+ return addToCache(a, b, levDist);
80
+ }
81
+
82
+
83
+ const addToCache = (a, b, value) => {
84
+ cache[a] = cache[a] || {};
85
+ cache[a][b] = value;
86
+ return value;
87
+ }
@@ -0,0 +1,55 @@
1
+ const { FILES, MODEL_FILES } = require("../constants");
2
+
3
+ module.exports = {
4
+ /**
5
+ * Checks whether the given filePath matches a regex `files`
6
+ * @param {string} filePath
7
+ * @returns boolean
8
+ */
9
+ isValidFile: function (filePath, fileType) {
10
+ function genRegex(key) {
11
+ return new RegExp(
12
+ `${key
13
+ .map((file) => {
14
+ return file.replace("*", "");
15
+ })
16
+ .join("$|")}$`
17
+ );
18
+ }
19
+ let isValid = false;
20
+ switch(fileType) {
21
+ case 'MODEL_FILES':
22
+ isValid = genRegex(MODEL_FILES).test(filePath);
23
+ break;
24
+ case 'FILES':
25
+ isValid = genRegex(FILES).test(filePath);
26
+ break;
27
+ }
28
+ return isValid;
29
+ },
30
+ /**
31
+ * Checks whether the plugin is run via VS Code ESLint extension
32
+ * @returns boolean
33
+ */
34
+ isVSCodeEditor() {
35
+ return process.argv.join(" ").includes("dbaeumer.vscode-eslint");
36
+ },
37
+
38
+ /**
39
+ * Checks whether ESLint is running in debug mode
40
+ * @returns boolean
41
+ */
42
+ hasDebugFlag() {
43
+ return process.argv.includes("--debug");
44
+ },
45
+
46
+ /**
47
+ * Returns an array of allowed file extensions
48
+ * the plugin can parse of the form "*.ext"
49
+ * @returns {ConfigOverrideFiles} Array of file extensions
50
+ */
51
+ getFileExtensions: function() {
52
+ return FILES;
53
+ },
54
+
55
+ };
File without changes