@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.
- package/CHANGELOG.md +32 -106
- package/lib/api/index.js +11 -13
- package/lib/api/lint.d.ts +48 -0
- package/lib/constants.js +54 -0
- package/lib/index.js +44 -0
- package/lib/{impl/parser.js → parser.js} +2 -13
- package/lib/processor.js +47 -0
- package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +51 -52
- package/lib/rules/latest-cds-version.js +42 -0
- package/lib/rules/min-node-version.js +47 -0
- package/lib/rules/no-db-keywords.js +46 -0
- package/lib/rules/no-dollar-prefixed-names.js +47 -0
- package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +16 -11
- package/lib/rules/require-2many-oncond.js +27 -0
- package/lib/rules/sql-cast-suggestion.js +52 -0
- package/lib/rules/start-elements-lowercase.js +61 -0
- package/lib/rules/start-entities-uppercase.js +55 -0
- package/lib/rules/valid-csv-header.js +100 -0
- package/lib/utils/fuzzySearch.js +87 -0
- package/lib/utils/helpers.js +55 -0
- package/lib/{impl/utils → utils}/jsonc.js +0 -0
- package/lib/{impl/utils → utils}/model.js +122 -216
- package/lib/utils/ruleHelpers.js +56 -0
- package/lib/utils/ruleTester.js +79 -0
- package/lib/utils/rules.js +1033 -0
- package/lib/{impl/utils → utils}/validate.js +8 -21
- package/package.json +3 -3
- package/lib/api/formatter.js +0 -182
- package/lib/impl/constants.js +0 -40
- package/lib/impl/index.js +0 -58
- package/lib/impl/processor.js +0 -23
- package/lib/impl/ruleFactory.js +0 -311
- package/lib/impl/rules/cds-compile-error.js +0 -35
- package/lib/impl/rules/latest-cds-version.js +0 -46
- package/lib/impl/rules/min-node-version.js +0 -42
- package/lib/impl/rules/no-db-keywords.js +0 -35
- package/lib/impl/rules/require-2many-oncond.js +0 -29
- package/lib/impl/rules/rule.hbs +0 -20
- package/lib/impl/rules/sql-cast-suggestion.js +0 -50
- package/lib/impl/rules/start-elements-lowercase.js +0 -74
- package/lib/impl/rules/start-entities-uppercase.js +0 -65
- package/lib/impl/types.d.ts +0 -48
- package/lib/impl/utils/helpers.js +0 -68
- 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
|