@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require("../utils/rules");
|
|
2
|
+
|
|
3
|
+
const REPLACE_AS_WRITE_EVENTS = ["READ", "CREATE", "UPDATE", "DELETE"];
|
|
4
|
+
const VALID_EVENTS = REPLACE_AS_WRITE_EVENTS.concat(["INSERT", "UPSERT", "WRITE", "*"]);
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: "`@restrict.grant` must have valid values.",
|
|
10
|
+
category: "Model Validation",
|
|
11
|
+
recommended: true
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
15
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`
|
|
16
|
+
},
|
|
17
|
+
type: "problem",
|
|
18
|
+
model: "inferred"
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
entity: check_restrict_grant
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function check_restrict_grant(e) {
|
|
26
|
+
const node = context.getNode(e);
|
|
27
|
+
const file = e.$location.file;
|
|
28
|
+
if (e["@restrict"]) {
|
|
29
|
+
const actions = e.actions;
|
|
30
|
+
const actionNames = actions ? Object.keys(actions).map((s) => actions[s].name) : [];
|
|
31
|
+
const validEventsAndActions = VALID_EVENTS.concat(actionNames);
|
|
32
|
+
|
|
33
|
+
for (const entry of e["@restrict"]) {
|
|
34
|
+
if (Object.keys(entry).includes("grant")) {
|
|
35
|
+
const grantValue = entry.grant;
|
|
36
|
+
switch (typeof grantValue) {
|
|
37
|
+
case "string": {
|
|
38
|
+
if (isEmptyString(grantValue)) {
|
|
39
|
+
context.report({
|
|
40
|
+
message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
|
|
41
|
+
node,
|
|
42
|
+
file
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
if (!isStringInArray(grantValue, validEventsAndActions, true)) {
|
|
46
|
+
const candidates = findFuzzy(grantValue, validEventsAndActions.sort());
|
|
47
|
+
context.report({
|
|
48
|
+
messageId: "InvalidItem",
|
|
49
|
+
data: { invalid: grantValue, candidates },
|
|
50
|
+
node,
|
|
51
|
+
file
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "object":
|
|
59
|
+
if (isEmptyObject(grantValue)) {
|
|
60
|
+
context.report({
|
|
61
|
+
message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
|
|
62
|
+
node,
|
|
63
|
+
file
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
let valuesForWrite = grantValue.filter(function (item) {
|
|
67
|
+
return item !== "READ" && item !== "WRITE" && item !== "*";
|
|
68
|
+
});
|
|
69
|
+
for (const value of grantValue) {
|
|
70
|
+
if (!validEventsAndActions.includes(value)) {
|
|
71
|
+
const candidates = findFuzzy(value, validEventsAndActions.sort());
|
|
72
|
+
context.report({
|
|
73
|
+
messageId: "InvalidItem",
|
|
74
|
+
data: { invalid: value, candidates },
|
|
75
|
+
node,
|
|
76
|
+
file
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let allValuesIncluded = grantValue.every((v) => valuesForWrite.includes(v));
|
|
81
|
+
if (allValuesIncluded) {
|
|
82
|
+
context.report({
|
|
83
|
+
messageId: "InvalidItem",
|
|
84
|
+
data: { invalid: [`[${grantValue}]`], candidates: [`["WRITE"]`] },
|
|
85
|
+
node,
|
|
86
|
+
file
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (grantValue.includes("*")) {
|
|
90
|
+
context.report({
|
|
91
|
+
messageId: "InvalidItem",
|
|
92
|
+
data: { invalid: `[${grantValue}]`, candidates: [`["*"]`] },
|
|
93
|
+
node,
|
|
94
|
+
file
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { isEmptyObject, findFuzzy } = require("../utils/rules");
|
|
2
|
+
|
|
3
|
+
const VALID_RESTRICT_KEYS = ["grant", "to", "where"];
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "`@restrict` must have properly spelled `to`, `grant`, and `where` keys.",
|
|
9
|
+
category: "Model Validation",
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
InvalidItem: `Misspelled key '{{invalid}}'. Did you mean '{{candidates}}'?`
|
|
14
|
+
},
|
|
15
|
+
type: "problem",
|
|
16
|
+
model: "inferred"
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
entity: check_restrict_keys
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function check_restrict_keys(e) {
|
|
24
|
+
if (e["@restrict"]) {
|
|
25
|
+
for (const entry of e["@restrict"]) {
|
|
26
|
+
if (typeof entry === "object" && !isEmptyObject(entry)) {
|
|
27
|
+
for (const key of Object.keys(entry)) {
|
|
28
|
+
if (!VALID_RESTRICT_KEYS.includes(key)) {
|
|
29
|
+
const candidates = findFuzzy(key, VALID_RESTRICT_KEYS.sort());
|
|
30
|
+
context.report({
|
|
31
|
+
messageId: "InvalidItem",
|
|
32
|
+
data: { invalid: key, candidates },
|
|
33
|
+
node: context.getNode(e),
|
|
34
|
+
file: e.$location.file
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const { isEmptyString, isStringInArray, findFuzzy, splitEntityName, isEmptyObject } = require("../utils/rules");
|
|
2
|
+
|
|
3
|
+
const VALID_PSEUDO_ROLES = ["authenticated-user", "system-user", "any"];
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "`@restrict.to` must have valid values.",
|
|
9
|
+
category: "Model Validation",
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
messages: {
|
|
14
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
15
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`
|
|
16
|
+
},
|
|
17
|
+
type: "problem",
|
|
18
|
+
model: "inferred"
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
entity: check_restrict_to
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function check_restrict_to(e) {
|
|
26
|
+
const USER_ROLES = [];
|
|
27
|
+
const model = context.getModel();
|
|
28
|
+
|
|
29
|
+
model.foreach("entity", (e) => {
|
|
30
|
+
if (e["@restrict"]) {
|
|
31
|
+
e["@restrict"].forEach((p) => {
|
|
32
|
+
if (p.to) {
|
|
33
|
+
switch (typeof p.to) {
|
|
34
|
+
case "string":
|
|
35
|
+
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
36
|
+
USER_ROLES.push(p.to);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case "object":
|
|
40
|
+
for (const r in p.to) {
|
|
41
|
+
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
42
|
+
USER_ROLES.push(r);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES);
|
|
51
|
+
|
|
52
|
+
if (e["@restrict"]) {
|
|
53
|
+
let node = context.getNode(e);
|
|
54
|
+
let file = e.$location.file;
|
|
55
|
+
const { prefix } = splitEntityName(e);
|
|
56
|
+
const prefixSplit = prefix.split(".");
|
|
57
|
+
const serviceName = prefixSplit[prefixSplit.length - 1];
|
|
58
|
+
let services = model.services;
|
|
59
|
+
|
|
60
|
+
// TODO: For hierachies, check whether service restriction exists
|
|
61
|
+
//let grantAllTo;
|
|
62
|
+
Object.values(services).map((s) => {
|
|
63
|
+
if (s.name === serviceName && s["@requires"]) {
|
|
64
|
+
//grantAllTo = s['@requires'];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (const entry of e["@restrict"]) {
|
|
69
|
+
if (Object.keys(entry).includes("to")) {
|
|
70
|
+
const toValue = entry.to;
|
|
71
|
+
|
|
72
|
+
switch (typeof toValue) {
|
|
73
|
+
case "string": {
|
|
74
|
+
if (isEmptyString(toValue)) {
|
|
75
|
+
context.report({
|
|
76
|
+
message: `Missing role on ${e.name} for \`@restrict.to\`.`,
|
|
77
|
+
node,
|
|
78
|
+
file
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const isPseudoRole = entry.to && entry.to === entry.to.toLowerCase();
|
|
82
|
+
if (!isStringInArray(toValue, ROLES, isPseudoRole)) {
|
|
83
|
+
const candidates = findFuzzy(toValue, ROLES.sort());
|
|
84
|
+
context.report({
|
|
85
|
+
messageId: "InvalidItem",
|
|
86
|
+
data: { invalid: toValue, candidates },
|
|
87
|
+
node,
|
|
88
|
+
file
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "object":
|
|
96
|
+
if (isEmptyObject(toValue)) {
|
|
97
|
+
context.report({
|
|
98
|
+
message: `Missing roles on ${e.name} for \`@restrict.to\`.`,
|
|
99
|
+
node,
|
|
100
|
+
file
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
if (toValue.includes("any")) {
|
|
104
|
+
context.report({
|
|
105
|
+
messageId: "InvalidItem",
|
|
106
|
+
data: { invalid: `[${toValue}]`, candidates: [`["any"]`] },
|
|
107
|
+
node,
|
|
108
|
+
file
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
toValue.forEach((value) => {
|
|
112
|
+
if (!ROLES.includes(value)) {
|
|
113
|
+
const candidates = findFuzzy(value, ROLES.sort());
|
|
114
|
+
context.report({
|
|
115
|
+
messageId: "InvalidItem",
|
|
116
|
+
data: { invalid: value, candidates },
|
|
117
|
+
node,
|
|
118
|
+
file
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
|
|
3
|
+
const VALID_PSEUDO_ROLES = ["authenticated-user", "system-user", "any"];
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "`@restrict.where` must have valid values.",
|
|
9
|
+
category: "Model Validation",
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
severity: "error",
|
|
13
|
+
hasSuggestions: true,
|
|
14
|
+
messages: {
|
|
15
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
16
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`
|
|
17
|
+
},
|
|
18
|
+
type: "problem",
|
|
19
|
+
model: "inferred"
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const model = context.getModel();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
entity: check_restrict_grant
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function check_restrict_grant(e) {
|
|
29
|
+
const USER_ROLES = [];
|
|
30
|
+
|
|
31
|
+
model.foreach("entity", (e) => {
|
|
32
|
+
if (e["@restrict"]) {
|
|
33
|
+
e["@restrict"].forEach((p) => {
|
|
34
|
+
if (p.to) {
|
|
35
|
+
switch (typeof p.to) {
|
|
36
|
+
case "string":
|
|
37
|
+
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
38
|
+
USER_ROLES.push(p.to);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case "object":
|
|
42
|
+
for (const r in p.to) {
|
|
43
|
+
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
44
|
+
USER_ROLES.push(r);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES);
|
|
53
|
+
|
|
54
|
+
if (e["@restrict"]) {
|
|
55
|
+
const node = context.getNode(e);
|
|
56
|
+
const file = e.$location.file;
|
|
57
|
+
for (const entry of e["@restrict"]) {
|
|
58
|
+
const whereValues = entry.where;
|
|
59
|
+
if (whereValues && typeof whereValues === "string") {
|
|
60
|
+
let cxn;
|
|
61
|
+
try {
|
|
62
|
+
cxn = cds.parse.expr(entry.where);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
context.report({
|
|
65
|
+
message: `Invalid \`where\` expression, CDS compilation failed.`,
|
|
66
|
+
node,
|
|
67
|
+
file
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (cxn && cxn.xpr) {
|
|
71
|
+
const operator = cxn.xpr[1];
|
|
72
|
+
const role = cxn.xpr[2].ref;
|
|
73
|
+
if (operator === "=") {
|
|
74
|
+
const isValidRole = role == "$user" || ROLES.includes(role);
|
|
75
|
+
if (!isValidRole) {
|
|
76
|
+
context.report({
|
|
77
|
+
message: `Invalid \`where\` expression, role \`${role}\` not found.`,
|
|
78
|
+
node,
|
|
79
|
+
file
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const Cache = require("../utils/Cache");
|
|
2
|
+
const createRule = require("../utils/createRule");
|
|
3
|
+
|
|
4
|
+
const rules = {
|
|
5
|
+
"assoc2many-ambiguous-key": () => createRule(require("./assoc2many-ambiguous-key")),
|
|
6
|
+
"auth-no-empty-restrictions": () => createRule(require("./auth-no-empty-restrictions")),
|
|
7
|
+
"auth-use-requires": () => createRule(require("./auth-use-requires")),
|
|
8
|
+
"auth-valid-restrict-grant": () => createRule(require("./auth-valid-restrict-grant")),
|
|
9
|
+
"auth-valid-restrict-keys": () => createRule(require("./auth-valid-restrict-keys")),
|
|
10
|
+
"auth-valid-restrict-to": () => createRule(require("./auth-valid-restrict-to")),
|
|
11
|
+
"auth-valid-restrict-where": () => createRule(require("./auth-valid-restrict-where")),
|
|
12
|
+
"latest-cds-version": () => createRule(require("./latest-cds-version")),
|
|
13
|
+
"min-node-version": () => createRule(require("./min-node-version")),
|
|
14
|
+
"no-db-keywords": () => createRule(require("./no-db-keywords")),
|
|
15
|
+
"no-dollar-prefixed-names": () => createRule(require("./no-dollar-prefixed-names")),
|
|
16
|
+
"no-join-on-draft-enabled-entities": () => createRule(require("./no-join-on-draft-enabled-entities")),
|
|
17
|
+
"require-2many-oncond": () => createRule(require("./require-2many-oncond")),
|
|
18
|
+
"sql-cast-suggestion": () => createRule(require("./sql-cast-suggestion")),
|
|
19
|
+
"start-elements-lowercase": () => createRule(require("./start-elements-lowercase")),
|
|
20
|
+
"start-entities-uppercase": () => createRule(require("./start-entities-uppercase")),
|
|
21
|
+
"valid-csv-header": () => createRule(require("./valid-csv-header"))
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Cache.set("rules", rules);
|
|
25
|
+
|
|
26
|
+
module.exports = rules;
|
|
@@ -4,22 +4,22 @@ const semver = require("semver");
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
meta: {
|
|
6
6
|
docs: {
|
|
7
|
-
description: "Checks whether the latest `@sap/cds` version is being used."
|
|
8
|
-
category: "Environment",
|
|
9
|
-
version: "1.0.4",
|
|
7
|
+
description: "Checks whether the latest `@sap/cds` version is being used."
|
|
10
8
|
},
|
|
11
9
|
type: "suggestion",
|
|
12
10
|
hasSuggestions: true,
|
|
13
11
|
messages: {
|
|
14
12
|
latestCDSVersion: `A newer CDS version is available!`,
|
|
15
13
|
},
|
|
14
|
+
severity: "off",
|
|
15
|
+
model: "none"
|
|
16
16
|
},
|
|
17
17
|
create: function (context) {
|
|
18
|
-
return
|
|
18
|
+
return check_latest_cds_version;
|
|
19
19
|
|
|
20
20
|
function check_latest_cds_version() {
|
|
21
21
|
let cdsVersions;
|
|
22
|
-
let e = context.
|
|
22
|
+
let e = context.getEnvironment();
|
|
23
23
|
if (!e) {
|
|
24
24
|
e = cp
|
|
25
25
|
.execSync(`npm outdated @sap/cds --json`, {
|
|
@@ -32,9 +32,10 @@ module.exports = {
|
|
|
32
32
|
cdsVersions = e["@sap/cds"];
|
|
33
33
|
// If current cds version is not the latest
|
|
34
34
|
if (Object.keys(cdsVersions).length !== 0 && !semver.satisfies(cdsVersions.latest, cdsVersions.current)) {
|
|
35
|
-
|
|
35
|
+
context.report({
|
|
36
36
|
messageId: "latestCDSVersion",
|
|
37
|
-
|
|
37
|
+
node: context.getNode()
|
|
38
|
+
});
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -4,25 +4,23 @@ const semver = require("semver");
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
meta: {
|
|
6
6
|
docs: {
|
|
7
|
-
description: `Checks whether the minimum Node.js version required by \`@sap/cds\` is achieved
|
|
8
|
-
category: "Environment",
|
|
9
|
-
recommended: true,
|
|
10
|
-
version: "1.0.0",
|
|
7
|
+
description: `Checks whether the minimum Node.js version required by \`@sap/cds\` is achieved.`
|
|
11
8
|
},
|
|
12
|
-
severity: "
|
|
9
|
+
severity: "off",
|
|
13
10
|
type: "problem",
|
|
11
|
+
model: "none"
|
|
14
12
|
},
|
|
15
13
|
create: function (context) {
|
|
16
|
-
return
|
|
14
|
+
return check_min_node_version;
|
|
17
15
|
|
|
18
16
|
function check_min_node_version() {
|
|
19
|
-
const e = context.
|
|
17
|
+
const e = context.getEnvironment();
|
|
20
18
|
let nodeVersion, nodeVersionCDS;
|
|
21
19
|
if (!e) {
|
|
22
20
|
// Get current and required node versions
|
|
23
21
|
try {
|
|
24
22
|
const CDSPath = require.resolve("@sap/cds/package.json", {
|
|
25
|
-
paths: [path.dirname(
|
|
23
|
+
paths: [path.dirname(".")],
|
|
26
24
|
});
|
|
27
25
|
const jsonCDS = require(CDSPath);
|
|
28
26
|
nodeVersion = process.version;
|
|
@@ -39,7 +37,10 @@ module.exports = {
|
|
|
39
37
|
nodeVersionCDS &&
|
|
40
38
|
!semver.satisfies(nodeVersion, nodeVersionCDS, { loose: true })
|
|
41
39
|
) {
|
|
42
|
-
|
|
40
|
+
context.report({
|
|
41
|
+
message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
|
|
42
|
+
node: context.getNode()
|
|
43
|
+
});
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
|
|
3
3
|
module.exports = {
|
|
4
4
|
meta: {
|
|
5
5
|
docs: {
|
|
6
6
|
description: `Avoid using reserved SQL keywords.`,
|
|
7
|
-
|
|
8
|
-
recommended: true,
|
|
9
|
-
version: "2.1.0",
|
|
7
|
+
recommended: true
|
|
10
8
|
},
|
|
11
|
-
|
|
9
|
+
type: "problem"
|
|
12
10
|
},
|
|
13
11
|
create(context) {
|
|
14
|
-
const {
|
|
15
|
-
const { db = { kind: "sql" } } = cds.env.requires
|
|
12
|
+
const { db = { kind: "sql" } } = cds.env.requires;
|
|
16
13
|
|
|
17
|
-
return {
|
|
14
|
+
return {
|
|
15
|
+
//> return standard eslint visitor callbacks registered to CSN kinds, types, ...
|
|
18
16
|
entity: check_name_is_not_reserved,
|
|
19
17
|
element: check_name_is_not_reserved
|
|
20
|
-
}
|
|
18
|
+
};
|
|
21
19
|
|
|
22
20
|
function check_name_is_not_reserved(d) {
|
|
23
21
|
if (d.name in RESERVED) {
|
|
24
22
|
// Do not blame in case of external services
|
|
25
|
-
let srv = d._service || (d.parent && d.parent._service)
|
|
26
|
-
if (srv && srv["@cds.external"]) return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
let srv = d._service || (d.parent && d.parent._service);
|
|
24
|
+
if (srv && srv["@cds.external"]) return;
|
|
25
|
+
context.report({
|
|
26
|
+
message: `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`,
|
|
27
|
+
node: context.getNode(d)
|
|
28
|
+
});
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
|
-
}
|
|
31
|
+
}
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
// REVISIT: Replace by compiler-provided check
|
|
@@ -42,5 +41,5 @@ const RESERVED = {
|
|
|
42
41
|
group: 1,
|
|
43
42
|
LIMIT: 1,
|
|
44
43
|
Limit: 1,
|
|
45
|
-
limit: 1
|
|
44
|
+
limit: 1
|
|
46
45
|
};
|
|
@@ -1,49 +1,23 @@
|
|
|
1
|
-
const { isVSCodeEditor } = require("../utils/helpers");
|
|
2
|
-
|
|
3
1
|
module.exports = {
|
|
4
2
|
meta: {
|
|
5
3
|
docs: {
|
|
6
4
|
description: `Names must not start with $ to avoid possible shadowing of reserved variables.`,
|
|
7
|
-
|
|
8
|
-
recommended: true,
|
|
9
|
-
version: "2.3.3",
|
|
5
|
+
recommended: true
|
|
10
6
|
},
|
|
11
|
-
|
|
7
|
+
type: "problem"
|
|
12
8
|
},
|
|
13
9
|
create(context) {
|
|
14
10
|
return { element: _check };
|
|
15
11
|
|
|
16
12
|
function _check(d) {
|
|
17
|
-
|
|
18
|
-
// Do not blame in case of external services
|
|
19
13
|
let srv = d._service || (d.parent && d.parent._service);
|
|
20
14
|
if (srv && srv["@cds.external"]) return;
|
|
21
|
-
|
|
22
|
-
const results = [];
|
|
23
|
-
for (const m in context.cds.model.messages) {
|
|
24
|
-
const msg = context.cds.model.messages[m];
|
|
25
|
-
if (msg.messageId === "syntax-dollar-ident" && !isVSCodeEditor()) {
|
|
26
|
-
results.push({
|
|
27
|
-
message: msg.message,
|
|
28
|
-
loc: {
|
|
29
|
-
start: { line: msg.location.line, column: msg.location.col - 1 },
|
|
30
|
-
end: {
|
|
31
|
-
line: msg.location.endLine,
|
|
32
|
-
column: !msg.location.endCol
|
|
33
|
-
? context.sourcecode.lines[msg.location.line - 1].length
|
|
34
|
-
: msg.location.endCol - 1,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
file: msg.location.file,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
15
|
if (d.name.startsWith("$")) {
|
|
43
|
-
|
|
44
|
-
|
|
16
|
+
context.report({
|
|
17
|
+
message: `“${d.name}” is prefixed with a dollar sign ($)`,
|
|
18
|
+
node: context.getNode(d)
|
|
19
|
+
});
|
|
45
20
|
}
|
|
46
|
-
return results.length > 0 ? results : undefined;
|
|
47
21
|
}
|
|
48
|
-
}
|
|
22
|
+
}
|
|
49
23
|
};
|
|
@@ -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
|
-
|
|
6
|
-
recommended: true,
|
|
7
|
-
version: "2.2.1",
|
|
5
|
+
recommended: true
|
|
8
6
|
},
|
|
9
|
-
severity: "warn",
|
|
10
7
|
type: "suggestion",
|
|
11
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
6
|
-
recommended: true,
|
|
7
|
-
version: "2.1.0",
|
|
5
|
+
recommended: true
|
|
8
6
|
},
|
|
9
|
-
|
|
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 ===
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
16
|
+
node: context.getNode(e)
|
|
17
|
+
});
|
|
24
18
|
}
|
|
25
19
|
}
|
|
26
|
-
|
|
20
|
+
|
|
21
|
+
}
|
|
27
22
|
};
|