@sap/eslint-plugin-cds 2.3.4 → 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.
- package/CHANGELOG.md +40 -0
- package/lib/api/index.js +9 -8
- package/lib/conf/all.js +21 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +18 -0
- package/lib/constants.js +10 -8
- package/lib/index.js +11 -32
- package/lib/parser.js +158 -7
- package/lib/rules/assoc2many-ambiguous-key.js +21 -38
- package/lib/rules/auth-no-empty-restrictions.js +37 -0
- package/lib/rules/auth-use-requires.js +38 -0
- package/lib/rules/auth-valid-restrict-grant.js +105 -0
- package/lib/rules/auth-valid-restrict-keys.js +43 -0
- package/lib/rules/auth-valid-restrict-to.js +130 -0
- package/lib/rules/auth-valid-restrict-where.js +89 -0
- package/lib/rules/index.js +26 -0
- package/lib/rules/latest-cds-version.js +8 -7
- package/lib/rules/min-node-version.js +10 -9
- package/lib/rules/no-db-keywords.js +16 -17
- package/lib/rules/no-dollar-prefixed-names.js +7 -33
- package/lib/rules/no-join-on-draft-enabled-entities.js +9 -24
- package/lib/rules/require-2many-oncond.js +9 -14
- package/lib/rules/sql-cast-suggestion.js +6 -20
- package/lib/rules/start-elements-lowercase.js +7 -10
- package/lib/rules/start-entities-uppercase.js +6 -9
- package/lib/rules/valid-csv-header.js +66 -66
- package/lib/{api/lint.d.ts → types.d.ts} +4 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +304 -0
- package/lib/utils/createRuleDocs.js +361 -0
- package/lib/utils/{fuzzySearch.js → findFuzzy.js} +9 -4
- package/lib/utils/genDocs.js +363 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/jsonc.js +1 -1
- package/lib/utils/jsoncParser.js +1 -0
- package/lib/utils/rules.js +85 -945
- package/lib/utils/runRuleTester.js +111 -0
- package/package.json +8 -5
- package/lib/processor.js +0 -47
- package/lib/utils/helpers.js +0 -47
- package/lib/utils/model.js +0 -387
- package/lib/utils/ruleHelpers.js +0 -56
- package/lib/utils/ruleTester.js +0 -79
- package/lib/utils/validate.js +0 -36
|
@@ -3,11 +3,8 @@ module.exports = {
|
|
|
3
3
|
meta: {
|
|
4
4
|
docs: {
|
|
5
5
|
description: "Should make suggestions for possible missing SQL casts.",
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,31 +12,30 @@ module.exports = {
|
|
|
14
12
|
fixable: "code",
|
|
15
13
|
},
|
|
16
14
|
create: function (context) {
|
|
17
|
-
const
|
|
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 =
|
|
32
|
-
const fix = (fixer) => {
|
|
28
|
+
const loc = context.getLocation(elementName, e);
|
|
29
|
+
const fix = (fixer, source = sourcecode) => {
|
|
33
30
|
const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
|
|
34
|
-
const rangeEnd =
|
|
31
|
+
const rangeEnd = source.getIndexFromLoc({
|
|
35
32
|
line: loc.end.line,
|
|
36
33
|
column: loc.end.column,
|
|
37
34
|
});
|
|
38
35
|
const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0;
|
|
39
36
|
return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
|
|
40
37
|
};
|
|
41
|
-
|
|
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/
|
|
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,17 +14,16 @@ module.exports = {
|
|
|
16
14
|
fixable: "code",
|
|
17
15
|
},
|
|
18
16
|
create: function (context) {
|
|
19
|
-
const
|
|
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 =
|
|
26
|
+
const loc = context.getLocation(entityName, e);
|
|
30
27
|
const fix = (fixer) => {
|
|
31
28
|
const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
|
|
32
29
|
const rangeEnd = sourcecode.getIndexFromLoc({
|
|
@@ -36,7 +33,7 @@ module.exports = {
|
|
|
36
33
|
const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0;
|
|
37
34
|
return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
|
|
38
35
|
};
|
|
39
|
-
|
|
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
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
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
|
-
|
|
28
|
-
|
|
27
|
+
const filePath = context.getFilename();
|
|
28
|
+
const sourcecode = context.getSourceCode();
|
|
29
|
+
const code = sourcecode.getText();
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
71
|
+
function _findInCode(miss, code) {
|
|
71
72
|
// middle
|
|
72
|
-
let match = new RegExp(SEP+miss+SEP).exec(code)
|
|
73
|
-
if (match)
|
|
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)
|
|
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(
|
|
79
|
-
if (match)
|
|
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
|
|
85
|
-
let entity = csn.definitions
|
|
85
|
+
function _entity4(name, csn) {
|
|
86
|
+
let entity = csn.definitions[name];
|
|
86
87
|
if (!entity) {
|
|
87
|
-
if (/(.+)[._]texts_?/.test
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
17
|
-
meta:
|
|
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
|
-
|
|
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
|
+
}
|