@sap/eslint-plugin-cds 2.2.1 → 2.2.2
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 +39 -0
- package/lib/api/formatter.js +165 -166
- package/lib/api/index.js +10 -5
- package/lib/impl/constants.js +3 -6
- package/lib/impl/processor.js +3 -3
- package/lib/impl/ruleFactory.js +290 -298
- package/lib/impl/rules/assoc2many-ambiguous-key.js +27 -9
- package/lib/impl/rules/latest-cds-version.js +4 -5
- package/lib/impl/rules/min-node-version.js +0 -1
- package/lib/impl/rules/no-db-keywords.js +25 -15
- package/lib/impl/rules/no-join-on-draft-enabled-entities.js +35 -0
- package/lib/impl/rules/require-2many-oncond.js +1 -1
- package/lib/impl/rules/rule.hbs +15 -8
- package/lib/impl/rules/sql-cast-suggestion.js +4 -2
- package/lib/impl/rules/start-elements-lowercase.js +13 -5
- package/lib/impl/rules/start-entities-uppercase.js +13 -4
- package/lib/impl/utils/helpers.js +27 -48
- package/lib/impl/utils/model.js +524 -481
- package/lib/impl/utils/rules.js +44 -35
- package/lib/impl/utils/validate.js +14 -21
- package/package.json +1 -1
|
@@ -11,9 +11,13 @@ module.exports = require("../../api").createRule({
|
|
|
11
11
|
let csnOdata;
|
|
12
12
|
const m = context.cds.model;
|
|
13
13
|
if (m && m.definitions) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
try {
|
|
15
|
+
csnOdata = context.cds.compile.for.odata(m);
|
|
16
|
+
const csnOdataLinked = context.cds.linked(csnOdata);
|
|
17
|
+
associationCardinalityFlaw(csnOdataLinked, context);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// Don't continue with rule if model fails to compile
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
});
|
|
@@ -32,7 +36,10 @@ function associationCardinalityFlaw(csn, context) {
|
|
|
32
36
|
refPlainElement = false;
|
|
33
37
|
},
|
|
34
38
|
(refEntity, refElement) => {
|
|
35
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
refElement.type === "cds.Association" ||
|
|
41
|
+
refElement.type === "cds.Composition"
|
|
42
|
+
) {
|
|
36
43
|
if (refElement.cardinality && refElement.cardinality.max === "*") {
|
|
37
44
|
refCardinalityMult = true;
|
|
38
45
|
}
|
|
@@ -80,16 +87,21 @@ function processEntity(csn, eachCallback) {
|
|
|
80
87
|
const sourceAlias = [];
|
|
81
88
|
if (definition.query.SELECT.from.ref) {
|
|
82
89
|
// From
|
|
83
|
-
sourceEntity =
|
|
90
|
+
sourceEntity =
|
|
91
|
+
csn.definitions[definition.query.SELECT.from.ref.join("_")];
|
|
84
92
|
sourceAlias.push({
|
|
85
93
|
from: sourceEntity.name,
|
|
86
94
|
as:
|
|
87
95
|
definition.query.SELECT.from.as ||
|
|
88
96
|
definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
|
|
89
97
|
});
|
|
90
|
-
} else if (
|
|
98
|
+
} else if (
|
|
99
|
+
definition.query.SELECT.from.args &&
|
|
100
|
+
definition.query.SELECT.from.args[0].ref
|
|
101
|
+
) {
|
|
91
102
|
// Join
|
|
92
|
-
sourceEntity =
|
|
103
|
+
sourceEntity =
|
|
104
|
+
csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
|
|
93
105
|
definition.query.SELECT.from.args.forEach((arg) => {
|
|
94
106
|
sourceAlias.push({
|
|
95
107
|
from: arg.ref.join("_"),
|
|
@@ -136,12 +148,18 @@ function processElement(
|
|
|
136
148
|
if (!refElement && definition.query.SELECT.mixin) {
|
|
137
149
|
refElement = definition.query.SELECT.mixin[ref];
|
|
138
150
|
if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
|
|
139
|
-
refElement =
|
|
151
|
+
refElement =
|
|
152
|
+
definition.query.SELECT.mixin[column.ref[0]]._target.elements[
|
|
153
|
+
ref
|
|
154
|
+
];
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
eachCallback(refEntity, refElement);
|
|
144
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
refElement.type === "cds.Association" ||
|
|
161
|
+
refElement.type === "cds.Composition"
|
|
162
|
+
) {
|
|
145
163
|
refEntity = csn.definitions[refElement.target];
|
|
146
164
|
}
|
|
147
165
|
}
|
|
@@ -14,7 +14,7 @@ module.exports = require("../../api").createRule({
|
|
|
14
14
|
latestCDSVersion: `A newer CDS version is available!`,
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
|
-
create: function(context) {
|
|
17
|
+
create: function (context) {
|
|
18
18
|
let result;
|
|
19
19
|
let cdsVersions;
|
|
20
20
|
const e = context.cds.environment;
|
|
@@ -39,9 +39,8 @@ module.exports = require("../../api").createRule({
|
|
|
39
39
|
) {
|
|
40
40
|
// Add to ESLint report
|
|
41
41
|
context.report({
|
|
42
|
-
messageId:
|
|
43
|
-
loc: { line: 0, column: 0 },
|
|
42
|
+
messageId: "latestCDSVersion",
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
module.exports = require("../../api").defineRule({
|
|
2
|
-
meta: {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: `Avoid using reserved SQL keywords.`,
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
},
|
|
7
|
+
},
|
|
8
|
+
create(context) {
|
|
9
|
+
const { db = { kind: "sql" } } = context.cds.env.requires;
|
|
10
|
+
function _check(d) {
|
|
6
11
|
if (d.name in RESERVED) {
|
|
7
|
-
|
|
8
12
|
// Do not blame in case of external services
|
|
9
|
-
let srv = d._service || d.parent && d.parent._service
|
|
10
|
-
if (srv && srv[
|
|
13
|
+
let srv = d._service || (d.parent && d.parent._service);
|
|
14
|
+
if (srv && srv["@cds.external"]) return;
|
|
11
15
|
|
|
12
16
|
// Do blame
|
|
13
|
-
return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}
|
|
17
|
+
return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`;
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
|
-
return { entity: _check, element: _check }
|
|
17
|
-
}
|
|
18
|
-
})
|
|
20
|
+
return { entity: _check, element: _check };
|
|
21
|
+
},
|
|
22
|
+
});
|
|
19
23
|
|
|
20
24
|
// REVISIT: Replace by compiler-provided check
|
|
21
25
|
const RESERVED = {
|
|
22
|
-
ORDER: 1,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
ORDER: 1,
|
|
27
|
+
Order: 1,
|
|
28
|
+
order: 1,
|
|
29
|
+
GROUP: 1,
|
|
30
|
+
Group: 1,
|
|
31
|
+
group: 1,
|
|
32
|
+
LIMIT: 1,
|
|
33
|
+
Limit: 1,
|
|
34
|
+
limit: 1,
|
|
35
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: `Draft-enabled entities shall not be used in views that make use of \`JOIN\`.`,
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "2.2.1",
|
|
7
|
+
},
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
messages: {
|
|
10
|
+
noJoinOnDraftEnabledEntities: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create: function (context) {
|
|
14
|
+
const m = context.cds.model;
|
|
15
|
+
m.foreach("entity", (entity) => {
|
|
16
|
+
if (entity["@odata.draft.enabled"]) {
|
|
17
|
+
if (entity.query.SELECT.from.join) {
|
|
18
|
+
const location = entity.query.$location;
|
|
19
|
+
if (context.sourcecode.lines[location.line - 1]) {
|
|
20
|
+
const endCol = context.sourcecode.lines[location.line - 1].length;
|
|
21
|
+
const loc = {
|
|
22
|
+
start: { line: location.line, column: location.col - 1 },
|
|
23
|
+
end: { line: location.line, column: endCol },
|
|
24
|
+
};
|
|
25
|
+
context.report({
|
|
26
|
+
messageId: "noJoinOnDraftEnabledEntities",
|
|
27
|
+
loc,
|
|
28
|
+
file: entity.$location.file,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
package/lib/impl/rules/rule.hbs
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
module.exports = require("../../api").createRule({
|
|
3
|
-
meta: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
meta: {
|
|
4
|
+
docs: {
|
|
5
|
+
description: "{{description}}",
|
|
6
|
+
version: "{{version}}"
|
|
7
|
+
},
|
|
8
|
+
type:"{{type}}",
|
|
9
|
+
},
|
|
7
10
|
create: function(context) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const m = cotext.cds.model;
|
|
12
|
+
m.forall((d)) => {
|
|
13
|
+
// Add cds logic here, for example
|
|
14
|
+
return [{
|
|
15
|
+
message: "{{messages}}",
|
|
16
|
+
loc: {{loc}}
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
12
19
|
}
|
|
13
20
|
})
|
|
@@ -8,7 +8,8 @@ module.exports = require("../../api").createRule({
|
|
|
8
8
|
type: "suggestion",
|
|
9
9
|
hasSuggestions: true,
|
|
10
10
|
messages: {
|
|
11
|
-
missingSQLCast:
|
|
11
|
+
missingSQLCast:
|
|
12
|
+
"Potential issue - Missing SQL cast for column expression?",
|
|
12
13
|
},
|
|
13
14
|
},
|
|
14
15
|
create: function (context) {
|
|
@@ -26,7 +27,8 @@ module.exports = require("../../api").createRule({
|
|
|
26
27
|
continue;
|
|
27
28
|
} else {
|
|
28
29
|
if (context.sourcecode.lines[location.line - 1]) {
|
|
29
|
-
const endCol =
|
|
30
|
+
const endCol =
|
|
31
|
+
context.sourcecode.lines[location.line - 1].length;
|
|
30
32
|
const loc = {
|
|
31
33
|
start: { line: location.line, column: location.col - 1 },
|
|
32
34
|
end: { line: location.line, column: endCol },
|
|
@@ -8,7 +8,8 @@ module.exports = require("../../api").createRule({
|
|
|
8
8
|
type: "suggestion",
|
|
9
9
|
hasSuggestions: true,
|
|
10
10
|
messages: {
|
|
11
|
-
startLowercase:
|
|
11
|
+
startLowercase:
|
|
12
|
+
"Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
|
|
12
13
|
},
|
|
13
14
|
fixable: "code",
|
|
14
15
|
},
|
|
@@ -21,7 +22,9 @@ module.exports = require("../../api").createRule({
|
|
|
21
22
|
const element = d.elements[elementName];
|
|
22
23
|
if (
|
|
23
24
|
elementName &&
|
|
24
|
-
!(
|
|
25
|
+
!(
|
|
26
|
+
entityName.startsWith("localized") || entityName.endsWith("texts")
|
|
27
|
+
)
|
|
25
28
|
) {
|
|
26
29
|
if (
|
|
27
30
|
elementName.charAt(0) !== elementName.charAt(0).toLowerCase() &&
|
|
@@ -37,8 +40,13 @@ module.exports = require("../../api").createRule({
|
|
|
37
40
|
line: loc.end.line,
|
|
38
41
|
column: loc.end.column,
|
|
39
42
|
});
|
|
40
|
-
const rangeBeg = rangeEnd
|
|
41
|
-
|
|
43
|
+
const rangeBeg = rangeEnd
|
|
44
|
+
? rangeEnd - elementNameSanitized.length
|
|
45
|
+
: 0;
|
|
46
|
+
return fixer.replaceTextRange(
|
|
47
|
+
[rangeBeg, rangeEnd],
|
|
48
|
+
elementNameSanitized
|
|
49
|
+
);
|
|
42
50
|
};
|
|
43
51
|
context.report({
|
|
44
52
|
messageId: "startLowercase",
|
|
@@ -47,7 +55,7 @@ module.exports = require("../../api").createRule({
|
|
|
47
55
|
fix,
|
|
48
56
|
data: {
|
|
49
57
|
entityName,
|
|
50
|
-
elementName
|
|
58
|
+
elementName,
|
|
51
59
|
},
|
|
52
60
|
suggest: [
|
|
53
61
|
{
|
|
@@ -8,7 +8,8 @@ module.exports = require("../../api").createRule({
|
|
|
8
8
|
type: "suggestion",
|
|
9
9
|
hasSuggestions: true,
|
|
10
10
|
messages: {
|
|
11
|
-
startUppercase:
|
|
11
|
+
startUppercase:
|
|
12
|
+
"Entity name '{{entityName}}' should start with an uppercase letter.",
|
|
12
13
|
},
|
|
13
14
|
fixable: "code",
|
|
14
15
|
},
|
|
@@ -19,7 +20,10 @@ module.exports = require("../../api").createRule({
|
|
|
19
20
|
let entityName = e.name;
|
|
20
21
|
const names = entityName.split(".");
|
|
21
22
|
entityName = names[names.length - 1];
|
|
22
|
-
if (
|
|
23
|
+
if (
|
|
24
|
+
entityName &&
|
|
25
|
+
!(entityName.startsWith("localized") || entityName.endsWith("texts"))
|
|
26
|
+
) {
|
|
23
27
|
if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
|
|
24
28
|
if (e.$location && e.$location.file) {
|
|
25
29
|
const file = e.$location.file;
|
|
@@ -31,8 +35,13 @@ module.exports = require("../../api").createRule({
|
|
|
31
35
|
line: loc.end.line,
|
|
32
36
|
column: loc.end.column,
|
|
33
37
|
});
|
|
34
|
-
const rangeBeg = rangeEnd
|
|
35
|
-
|
|
38
|
+
const rangeBeg = rangeEnd
|
|
39
|
+
? rangeEnd - entityNameSanitized.length
|
|
40
|
+
: 0;
|
|
41
|
+
return fixer.replaceTextRange(
|
|
42
|
+
[rangeBeg, rangeEnd],
|
|
43
|
+
entityNameSanitized
|
|
44
|
+
);
|
|
36
45
|
};
|
|
37
46
|
context.report({
|
|
38
47
|
messageId: "startUppercase",
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
const { files } = require("../constants");
|
|
1
|
+
const { files, envFiles, modelFiles } = require("../constants");
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
/**
|
|
5
|
-
* Checks whether plugin is used in 'test' mode
|
|
6
|
-
* @returns boolean
|
|
7
|
-
*/
|
|
8
|
-
isTest: function () {
|
|
9
|
-
let isTest = false;
|
|
10
|
-
if (process.argv[1].includes("jest") || process.argv[1].includes("mocha")) {
|
|
11
|
-
isTest = true;
|
|
12
|
-
}
|
|
13
|
-
return isTest;
|
|
14
|
-
},
|
|
15
4
|
|
|
16
5
|
/**
|
|
17
6
|
* Checks whether the given file path contains a file extension allowed by
|
|
@@ -19,15 +8,23 @@ module.exports = {
|
|
|
19
8
|
* @param {string} filePath
|
|
20
9
|
* @returns boolean
|
|
21
10
|
*/
|
|
22
|
-
isValidFile: function (filePath) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
isValidFile: function (filePath, key = "") {
|
|
12
|
+
function genRegex(key) {
|
|
13
|
+
return new RegExp(
|
|
14
|
+
`${key
|
|
15
|
+
.map((file) => {
|
|
16
|
+
return file.replace("*", "");
|
|
17
|
+
})
|
|
18
|
+
.join("$|")}$`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (key === "model") {
|
|
22
|
+
return genRegex(modelFiles).test(filePath);
|
|
23
|
+
} else if (key === "env") {
|
|
24
|
+
return genRegex(envFiles).test(filePath);
|
|
25
|
+
} else {
|
|
26
|
+
return genRegex(files).test(filePath);
|
|
27
|
+
}
|
|
31
28
|
},
|
|
32
29
|
|
|
33
30
|
/**
|
|
@@ -38,6 +35,15 @@ module.exports = {
|
|
|
38
35
|
return process.argv.join(" ").includes("dbaeumer.vscode-eslint");
|
|
39
36
|
},
|
|
40
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Returns an array of allowed file extensions
|
|
40
|
+
* the plugin can parse of the form "*.ext"
|
|
41
|
+
* @returns {ConfigOverrideFiles} Array of file extensions
|
|
42
|
+
*/
|
|
43
|
+
getFileExtensions: function () {
|
|
44
|
+
return files;
|
|
45
|
+
},
|
|
46
|
+
|
|
41
47
|
/**
|
|
42
48
|
* Prints a formatted message string according to the styles provided
|
|
43
49
|
* @param msg message to print
|
|
@@ -59,31 +65,4 @@ module.exports = {
|
|
|
59
65
|
});
|
|
60
66
|
return `${msgStyle}${msg}${types.reset}`;
|
|
61
67
|
},
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Checks whether the compiled cds model contains compilation errors which
|
|
65
|
-
* should only be reported via the 'cds-compile-error' rule
|
|
66
|
-
* @param cds cds object
|
|
67
|
-
* @param ruleID rule name
|
|
68
|
-
* @returns
|
|
69
|
-
*/
|
|
70
|
-
hasCompilationError: function (context) {
|
|
71
|
-
const cds = context.cds;
|
|
72
|
-
const ruleID = context.ruleID;
|
|
73
|
-
if (
|
|
74
|
-
cds &&
|
|
75
|
-
cds.model &&
|
|
76
|
-
cds.model.err &&
|
|
77
|
-
cds.model.err.message.startsWith("CDS compilation failed")
|
|
78
|
-
) {
|
|
79
|
-
if (
|
|
80
|
-
ruleID === "@sap/cds/cds-compile-error" ||
|
|
81
|
-
ruleID === "cds-compile-error"
|
|
82
|
-
) {
|
|
83
|
-
cds.model.err;
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
},
|
|
89
68
|
};
|