@sap/eslint-plugin-cds 2.0.5 → 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 +111 -1
- package/README.md +2 -3
- package/lib/api/formatter.js +182 -132
- package/lib/api/index.js +27 -9
- package/lib/impl/constants.js +38 -44
- package/lib/impl/index.js +57 -34
- package/lib/impl/parser.js +37 -25
- package/lib/impl/processor.js +23 -0
- package/lib/impl/ruleFactory.js +301 -149
- package/lib/impl/rules/assoc2many-ambiguous-key.js +171 -0
- package/lib/impl/rules/cds-compile-error.js +35 -0
- package/lib/impl/rules/latest-cds-version.js +39 -33
- package/lib/impl/rules/min-node-version.js +36 -36
- package/lib/impl/rules/no-db-keywords.js +35 -0
- package/lib/impl/rules/no-join-on-draft-enabled-entities.js +35 -0
- package/lib/impl/rules/require-2many-oncond.js +29 -0
- package/lib/impl/rules/rule.hbs +20 -0
- package/lib/impl/rules/sql-cast-suggestion.js +45 -39
- package/lib/impl/rules/start-elements-lowercase.js +74 -0
- package/lib/impl/rules/start-entities-uppercase.js +65 -0
- package/lib/impl/types.d.ts +48 -0
- package/lib/impl/utils/helpers.js +68 -0
- package/lib/impl/utils/jsonc.js +1 -0
- package/lib/impl/utils/model.js +528 -0
- package/lib/impl/utils/rules.js +550 -0
- package/lib/impl/utils/validate.js +49 -0
- package/package.json +3 -3
- package/lib/impl/rules/assocs-card-flaw.js +0 -215
- package/lib/impl/rules/csn-compile-error.js +0 -37
- package/lib/impl/rules/lower-camelcase-elements.js +0 -42
- package/lib/impl/rules/upper-camelcase-entities.js +0 -50
- package/lib/impl/utils.js +0 -395
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const ruleFactory_1 = require("../ruleFactory");
|
|
3
|
-
const Annotations = {
|
|
4
|
-
ModelCheckIgnore: "@AFC.Model.Check.Ignore",
|
|
5
|
-
Source: "@AFC.Source",
|
|
6
|
-
MasterDataSource: "@AFC.MasterData.Source",
|
|
7
|
-
ConfigSource: "@AFC.Config.Source",
|
|
8
|
-
ChangelogExternalEntityKey: "@AFC.Changelog.ExternalEntity.Key",
|
|
9
|
-
ChangelogExternalEntityText: "@AFC.Changelog.ExternalEntity.Text",
|
|
10
|
-
ChangelogExternalEntityTextKey: "@AFC.Changelog.ExternalEntity.TextKey",
|
|
11
|
-
ChangelogIgnoredCompositions: "@AFC.Changelog.IgnoredCompositions",
|
|
12
|
-
PopulatedBy: "@AFC.PopulatedBy",
|
|
13
|
-
PopulatedByParent: "@AFC.PopulatedBy.Parent",
|
|
14
|
-
Enum: "@AFC.Enum",
|
|
15
|
-
EnumText: "@AFC.Enum.Text",
|
|
16
|
-
};
|
|
17
|
-
const Severity = {
|
|
18
|
-
Error: "E",
|
|
19
|
-
Warning: "W",
|
|
20
|
-
Info: "I",
|
|
21
|
-
};
|
|
22
|
-
function getInfoFromCSN(cds, csn, result) {
|
|
23
|
-
let loc = {
|
|
24
|
-
start: { line: 0, column: 0 },
|
|
25
|
-
end: { line: 1, column: 0 }
|
|
26
|
-
};
|
|
27
|
-
let file;
|
|
28
|
-
const resultSplit = result.toString().split(':');
|
|
29
|
-
const resultCombSplit = `${resultSplit[1]}`.split('~>');
|
|
30
|
-
const entity = resultCombSplit[0].trim();
|
|
31
|
-
if (csn && csn.definitions && csn.definitions[entity]) {
|
|
32
|
-
const entityObject = csn.definitions[entity];
|
|
33
|
-
if (entityObject.$location) {
|
|
34
|
-
loc = cds.getLocation(entity, entityObject);
|
|
35
|
-
file = entityObject.$location.file;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return { file, loc };
|
|
39
|
-
}
|
|
40
|
-
function associationCardinalityFlaw(csn) {
|
|
41
|
-
const messages = [];
|
|
42
|
-
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
|
|
43
|
-
let refCardinalityMult = false;
|
|
44
|
-
let refPlainElement = false;
|
|
45
|
-
processElement(csn, definition, sourceEntity, sourceAlias, () => {
|
|
46
|
-
refCardinalityMult = false;
|
|
47
|
-
refPlainElement = false;
|
|
48
|
-
}, (refEntity, refElement) => {
|
|
49
|
-
if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
|
|
50
|
-
if (refElement.cardinality && refElement.cardinality.max === "*") {
|
|
51
|
-
refCardinalityMult = true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
refPlainElement = true;
|
|
56
|
-
}
|
|
57
|
-
}, (column) => {
|
|
58
|
-
if (definition.keys &&
|
|
59
|
-
Object.keys(definition.keys).length === 1 &&
|
|
60
|
-
Object.keys(definition.keys)[0] === "ID" &&
|
|
61
|
-
refCardinalityMult &&
|
|
62
|
-
refPlainElement) {
|
|
63
|
-
report(messages, Severity.Error, "InvalidAssociationCardinality", definition, column, '');
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
return [messages];
|
|
68
|
-
}
|
|
69
|
-
function processEntity(csn, eachCallback) {
|
|
70
|
-
Object.keys(csn.definitions).forEach((name) => {
|
|
71
|
-
if (name.startsWith("localized.")) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const definition = csn.definitions[name];
|
|
75
|
-
if (definition.kind === "entity" &&
|
|
76
|
-
definition.query &&
|
|
77
|
-
definition.query.SELECT &&
|
|
78
|
-
definition.query.SELECT.columns) {
|
|
79
|
-
let sourceEntity;
|
|
80
|
-
const sourceAlias = [];
|
|
81
|
-
if (definition.query.SELECT.from.ref) {
|
|
82
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join("_")];
|
|
83
|
-
sourceAlias.push({
|
|
84
|
-
from: sourceEntity.name,
|
|
85
|
-
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
89
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
|
|
90
|
-
definition.query.SELECT.from.args.forEach((arg) => {
|
|
91
|
-
sourceAlias.push({
|
|
92
|
-
from: arg.ref.join("_"),
|
|
93
|
-
as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (!sourceEntity) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
eachCallback(definition, sourceEntity, sourceAlias);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function processElement(csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
|
|
105
|
-
definition.query.SELECT.columns.forEach((column) => {
|
|
106
|
-
if (column.ref && column.ref.length > 1) {
|
|
107
|
-
let refEntity = sourceEntity;
|
|
108
|
-
let refAlias = sourceAlias;
|
|
109
|
-
beforeCallback();
|
|
110
|
-
column.ref.forEach((ref) => {
|
|
111
|
-
ref = ref.id || ref;
|
|
112
|
-
const matchAlias = refAlias.find((alias) => {
|
|
113
|
-
return alias.as === ref;
|
|
114
|
-
});
|
|
115
|
-
let refElement;
|
|
116
|
-
if (matchAlias) {
|
|
117
|
-
refEntity = csn.definitions[matchAlias.from];
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
refElement = refEntity.elements[ref];
|
|
121
|
-
if (!refElement) {
|
|
122
|
-
refElement = definition.elements[ref];
|
|
123
|
-
if (!refElement && definition.query.SELECT.mixin) {
|
|
124
|
-
refElement = definition.query.SELECT.mixin[ref];
|
|
125
|
-
if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
|
|
126
|
-
refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
eachCallback(refEntity, refElement);
|
|
131
|
-
if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
|
|
132
|
-
refEntity = csn.definitions[refElement.target];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
refAlias = [];
|
|
136
|
-
});
|
|
137
|
-
afterCallback(column);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
function report(messages, severity, code, entity, column, info) {
|
|
142
|
-
if (entity && entity[Annotations.ModelCheckIgnore] && entity[Annotations.ModelCheckIgnore].includes(code)) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (column) {
|
|
146
|
-
if (column.as &&
|
|
147
|
-
entity.elements[column.as] &&
|
|
148
|
-
entity.elements[column.as][Annotations.ModelCheckIgnore] &&
|
|
149
|
-
entity.elements[column.as][Annotations.ModelCheckIgnore].includes(code)) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (column[Annotations.ModelCheckIgnore] && column[Annotations.ModelCheckIgnore].includes(code)) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const message = {
|
|
157
|
-
severity: severity || Severity.Error,
|
|
158
|
-
code,
|
|
159
|
-
entity,
|
|
160
|
-
column,
|
|
161
|
-
info,
|
|
162
|
-
toString: () => {
|
|
163
|
-
let columnPath = "";
|
|
164
|
-
if (message.column) {
|
|
165
|
-
if (message.column.ref) {
|
|
166
|
-
columnPath = `${message.column.ref.map((ref) => ref.id || ref).join(".")} ${message.column.as ? ": " + message.column.as : ""}`;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
columnPath = message.column.name;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return `[${message.severity}] ${message.code}: ${message.entity.name} ${columnPath ? "~> " + columnPath : ""}${message.info ? ` (${message.info})` : ""}`;
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
messages.push(message);
|
|
176
|
-
return message;
|
|
177
|
-
}
|
|
178
|
-
module.exports = ruleFactory_1.createRule({
|
|
179
|
-
type: 'problem',
|
|
180
|
-
docs: {
|
|
181
|
-
description: `Checks for association cardinality flaw.`,
|
|
182
|
-
category: 'Model Validation',
|
|
183
|
-
version: '1.0.1'
|
|
184
|
-
}
|
|
185
|
-
}, (cds, context) => {
|
|
186
|
-
var _a, _b;
|
|
187
|
-
const report = [];
|
|
188
|
-
const filepath = context.getFilename();
|
|
189
|
-
if (filepath.endsWith('.cds')) {
|
|
190
|
-
let csnOdata;
|
|
191
|
-
if ((_a = cds.model) === null || _a === void 0 ? void 0 : _a.definitions) {
|
|
192
|
-
try {
|
|
193
|
-
csnOdata = cds.compile.for.odata(cds.model);
|
|
194
|
-
if (cds.model && csnOdata && ((_b = cds.model) === null || _b === void 0 ? void 0 : _b.definitions)) {
|
|
195
|
-
const csnOdataLinked = cds.linked(csnOdata);
|
|
196
|
-
const results = associationCardinalityFlaw(csnOdataLinked);
|
|
197
|
-
results.forEach((result) => {
|
|
198
|
-
if (result.toString()) {
|
|
199
|
-
const location = getInfoFromCSN(cds, cds.model, result);
|
|
200
|
-
report.push({
|
|
201
|
-
message: result.toString(),
|
|
202
|
-
loc: location.loc,
|
|
203
|
-
file: location.file
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return report;
|
|
214
|
-
});
|
|
215
|
-
//# sourceMappingURL=assocs-card-flaw.js.map
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const ruleFactory_1 = require("../ruleFactory");
|
|
3
|
-
module.exports = ruleFactory_1.createRule({
|
|
4
|
-
type: 'problem',
|
|
5
|
-
docs: {
|
|
6
|
-
description: `Checks whether the CDS model file can be compiled by the @sap/cds wihtout errors.`,
|
|
7
|
-
category: 'Model Validation',
|
|
8
|
-
version: '1.0.0'
|
|
9
|
-
}
|
|
10
|
-
}, (cds, context) => {
|
|
11
|
-
const report = [];
|
|
12
|
-
const filepath = context.getFilename();
|
|
13
|
-
if (filepath.endsWith('.cds')) {
|
|
14
|
-
if (cds.model.err) {
|
|
15
|
-
if (cds.model.err) {
|
|
16
|
-
cds.model.err.errors.forEach((err) => {
|
|
17
|
-
const msg = err.message;
|
|
18
|
-
let file = '';
|
|
19
|
-
const loc = {
|
|
20
|
-
start: { line: 0, column: 0 },
|
|
21
|
-
end: { line: 1, column: 0 }
|
|
22
|
-
};
|
|
23
|
-
if (err.$location) {
|
|
24
|
-
loc.start.column = err.$location.col;
|
|
25
|
-
loc.start.line = err.$location.line;
|
|
26
|
-
loc.end.column = err.$location.endCol;
|
|
27
|
-
loc.end.line = err.$location.endLine;
|
|
28
|
-
file = err.$location.file;
|
|
29
|
-
}
|
|
30
|
-
report.push({ message: `${msg}`, loc, file });
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return report;
|
|
36
|
-
});
|
|
37
|
-
//# sourceMappingURL=csn-compile-error.js.map
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const ruleFactory_1 = require("../ruleFactory");
|
|
3
|
-
module.exports = ruleFactory_1.createRule({
|
|
4
|
-
type: 'suggestion',
|
|
5
|
-
docs: {
|
|
6
|
-
description: `Regular element names should all be in lower camel case.`,
|
|
7
|
-
category: 'Model Validation',
|
|
8
|
-
version: '1.0.4'
|
|
9
|
-
},
|
|
10
|
-
fixable: 'code'
|
|
11
|
-
}, (cds, context) => {
|
|
12
|
-
const report = [];
|
|
13
|
-
const filepath = context.getFilename();
|
|
14
|
-
if (filepath.endsWith('.cds')) {
|
|
15
|
-
const m = cds.reflect(cds.model);
|
|
16
|
-
if (m) {
|
|
17
|
-
m.forall((d) => {
|
|
18
|
-
var _a;
|
|
19
|
-
const entityName = d.name;
|
|
20
|
-
for (const elementName in d.elements) {
|
|
21
|
-
const element = d.elements[elementName];
|
|
22
|
-
if (elementName && !(entityName.startsWith('localized') || entityName.endsWith('texts'))) {
|
|
23
|
-
if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !['ID'].includes(elementName)) {
|
|
24
|
-
const file = (_a = element === null || element === void 0 ? void 0 : element.$location) === null || _a === void 0 ? void 0 : _a.file;
|
|
25
|
-
const loc = cds.getLocation(elementName, element);
|
|
26
|
-
const sourcecode = context.getSourceCode();
|
|
27
|
-
const fix = (fixer) => {
|
|
28
|
-
const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
|
|
29
|
-
const rangeEnd = sourcecode === null || sourcecode === void 0 ? void 0 : sourcecode.getIndexFromLoc({ line: loc.end.line, column: loc.end.column });
|
|
30
|
-
const rangeBeg = (rangeEnd) ? rangeEnd - elementNameSanitized.length : 0;
|
|
31
|
-
return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
|
|
32
|
-
};
|
|
33
|
-
report.push({ message: `Element '${elementName}' from entity '${entityName}' is not lower camel case!`, loc, fix, file });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return report;
|
|
41
|
-
});
|
|
42
|
-
//# sourceMappingURL=lower-camelcase-elements.js.map
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const ruleFactory_1 = require("../ruleFactory");
|
|
3
|
-
module.exports = ruleFactory_1.createRule({
|
|
4
|
-
type: 'suggestion',
|
|
5
|
-
docs: {
|
|
6
|
-
description: `Entity names should all be in upper camel case.`,
|
|
7
|
-
category: 'Model Validation',
|
|
8
|
-
version: '1.0.4'
|
|
9
|
-
},
|
|
10
|
-
messages: {
|
|
11
|
-
'startUppercase': 'Start entity and type names with capital letters'
|
|
12
|
-
},
|
|
13
|
-
fixable: 'code'
|
|
14
|
-
}, (cds, context) => {
|
|
15
|
-
var _a;
|
|
16
|
-
const report = [];
|
|
17
|
-
const filepath = context.getFilename();
|
|
18
|
-
if (filepath.endsWith('.cds')) {
|
|
19
|
-
const m = cds.reflect(cds.model);
|
|
20
|
-
if (m) {
|
|
21
|
-
for (const entity of m.each('entity')) {
|
|
22
|
-
let entityName = entity.name;
|
|
23
|
-
const names = entityName.split('.');
|
|
24
|
-
entityName = names[names.length - 1];
|
|
25
|
-
if (entityName && !(entityName.startsWith('localized') || entityName.endsWith('texts'))) {
|
|
26
|
-
if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
|
|
27
|
-
const file = (_a = entity === null || entity === void 0 ? void 0 : entity.$location) === null || _a === void 0 ? void 0 : _a.file;
|
|
28
|
-
const loc = cds.getLocation(entityName, entity);
|
|
29
|
-
const sourcecode = context.getSourceCode();
|
|
30
|
-
const fix = (fixer) => {
|
|
31
|
-
const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
|
|
32
|
-
const rangeEnd = sourcecode === null || sourcecode === void 0 ? void 0 : sourcecode.getIndexFromLoc({ line: loc.end.line, column: loc.end.column });
|
|
33
|
-
const rangeBeg = (rangeEnd) ? rangeEnd - entityNameSanitized.length : 0;
|
|
34
|
-
return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
|
|
35
|
-
};
|
|
36
|
-
const suggest = [
|
|
37
|
-
{
|
|
38
|
-
messageId: 'startUppercase',
|
|
39
|
-
fix
|
|
40
|
-
}
|
|
41
|
-
];
|
|
42
|
-
report.push({ message: `Entity '${entityName}' is not upper camel case!`, loc, file, fix, suggest });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return report;
|
|
49
|
-
});
|
|
50
|
-
//# sourceMappingURL=upper-camelcase-entities.js.map
|