@sap/eslint-plugin-cds 2.0.4 → 2.2.1
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 +82 -1
- package/README.md +2 -3
- package/lib/api/formatter.js +170 -119
- package/lib/api/index.js +22 -9
- package/lib/impl/constants.js +42 -48
- package/lib/impl/index.js +56 -49
- package/lib/impl/parser.js +37 -25
- package/lib/impl/processor.js +23 -0
- package/lib/impl/ruleFactory.js +319 -117
- package/lib/impl/rules/assoc2many-ambiguous-key.js +153 -0
- package/lib/impl/rules/cds-compile-error.js +35 -0
- package/lib/impl/rules/latest-cds-version.js +41 -34
- package/lib/impl/rules/min-node-version.js +37 -34
- package/lib/impl/rules/no-db-keywords.js +25 -0
- package/lib/impl/rules/require-2many-oncond.js +29 -0
- package/lib/impl/rules/rule.hbs +13 -0
- package/lib/impl/rules/sql-cast-suggestion.js +43 -39
- package/lib/impl/rules/start-elements-lowercase.js +66 -0
- package/lib/impl/rules/start-entities-uppercase.js +56 -0
- package/lib/impl/types.d.ts +48 -0
- package/lib/impl/utils/helpers.js +89 -0
- package/lib/impl/utils/jsonc.js +1 -0
- package/lib/impl/utils/model.js +485 -0
- package/lib/impl/utils/rules.js +541 -0
- package/lib/impl/utils/validate.js +56 -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 -370
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: `Ambiguous key with a \`TO MANY\` relationship since entries could appear multiple times with the same key.`,
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "1.0.1",
|
|
7
|
+
},
|
|
8
|
+
type: "problem",
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
let csnOdata;
|
|
12
|
+
const m = context.cds.model;
|
|
13
|
+
if (m && m.definitions) {
|
|
14
|
+
csnOdata = context.cds.compile.for.odata(m);
|
|
15
|
+
const csnOdataLinked = context.cds.linked(csnOdata);
|
|
16
|
+
associationCardinalityFlaw(csnOdataLinked, context);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function associationCardinalityFlaw(csn, context) {
|
|
22
|
+
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
|
|
23
|
+
let refCardinalityMult = false;
|
|
24
|
+
let refPlainElement = false;
|
|
25
|
+
processElement(
|
|
26
|
+
csn,
|
|
27
|
+
definition,
|
|
28
|
+
sourceEntity,
|
|
29
|
+
sourceAlias,
|
|
30
|
+
() => {
|
|
31
|
+
refCardinalityMult = false;
|
|
32
|
+
refPlainElement = false;
|
|
33
|
+
},
|
|
34
|
+
(refEntity, refElement) => {
|
|
35
|
+
if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
|
|
36
|
+
if (refElement.cardinality && refElement.cardinality.max === "*") {
|
|
37
|
+
refCardinalityMult = true;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
refPlainElement = true;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
(column) => {
|
|
44
|
+
if (
|
|
45
|
+
definition.keys &&
|
|
46
|
+
Object.keys(definition.keys).length === 1 &&
|
|
47
|
+
Object.keys(definition.keys)[0] === "ID" &&
|
|
48
|
+
refCardinalityMult &&
|
|
49
|
+
refPlainElement
|
|
50
|
+
) {
|
|
51
|
+
const loc = context.cds.getLocation(definition.name, definition);
|
|
52
|
+
context.report({
|
|
53
|
+
message: `Ambiguous key in '${definition.name}'. Element '${
|
|
54
|
+
column.as ? column.as : column.name
|
|
55
|
+
}' leads to multiple entries so that key '${
|
|
56
|
+
Object.keys(definition.keys)[0]
|
|
57
|
+
}' is not unique.`,
|
|
58
|
+
loc,
|
|
59
|
+
file: definition.$location.file,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function processEntity(csn, eachCallback) {
|
|
68
|
+
Object.keys(csn.definitions).forEach((name) => {
|
|
69
|
+
if (name.startsWith("localized.")) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const definition = csn.definitions[name];
|
|
73
|
+
if (
|
|
74
|
+
definition.kind === "entity" &&
|
|
75
|
+
definition.query &&
|
|
76
|
+
definition.query.SELECT &&
|
|
77
|
+
definition.query.SELECT.columns
|
|
78
|
+
) {
|
|
79
|
+
let sourceEntity;
|
|
80
|
+
const sourceAlias = [];
|
|
81
|
+
if (definition.query.SELECT.from.ref) {
|
|
82
|
+
// From
|
|
83
|
+
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join("_")];
|
|
84
|
+
sourceAlias.push({
|
|
85
|
+
from: sourceEntity.name,
|
|
86
|
+
as:
|
|
87
|
+
definition.query.SELECT.from.as ||
|
|
88
|
+
definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
|
|
89
|
+
});
|
|
90
|
+
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
91
|
+
// Join
|
|
92
|
+
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
|
|
93
|
+
definition.query.SELECT.from.args.forEach((arg) => {
|
|
94
|
+
sourceAlias.push({
|
|
95
|
+
from: arg.ref.join("_"),
|
|
96
|
+
as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!sourceEntity) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
eachCallback(definition, sourceEntity, sourceAlias);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function processElement(
|
|
109
|
+
csn,
|
|
110
|
+
definition,
|
|
111
|
+
sourceEntity,
|
|
112
|
+
sourceAlias,
|
|
113
|
+
beforeCallback,
|
|
114
|
+
eachCallback,
|
|
115
|
+
afterCallback
|
|
116
|
+
) {
|
|
117
|
+
definition.query.SELECT.columns.forEach((column) => {
|
|
118
|
+
if (column.ref && column.ref.length > 1) {
|
|
119
|
+
let refEntity = sourceEntity;
|
|
120
|
+
let refAlias = sourceAlias;
|
|
121
|
+
beforeCallback();
|
|
122
|
+
column.ref.forEach((ref) => {
|
|
123
|
+
ref = ref.id || ref;
|
|
124
|
+
// Alias
|
|
125
|
+
const matchAlias = refAlias.find((alias) => {
|
|
126
|
+
return alias.as === ref;
|
|
127
|
+
});
|
|
128
|
+
let refElement;
|
|
129
|
+
if (matchAlias) {
|
|
130
|
+
refEntity = csn.definitions[matchAlias.from];
|
|
131
|
+
} else {
|
|
132
|
+
refElement = refEntity.elements[ref];
|
|
133
|
+
// Mixin
|
|
134
|
+
if (!refElement) {
|
|
135
|
+
refElement = definition.elements[ref];
|
|
136
|
+
if (!refElement && definition.query.SELECT.mixin) {
|
|
137
|
+
refElement = definition.query.SELECT.mixin[ref];
|
|
138
|
+
if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
|
|
139
|
+
refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
eachCallback(refEntity, refElement);
|
|
144
|
+
if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
|
|
145
|
+
refEntity = csn.definitions[refElement.target];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
refAlias = [];
|
|
149
|
+
});
|
|
150
|
+
afterCallback(column);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: `Checks whether the CDS model file can be compiled by the @sap/cds wihtout errors.`,
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "1.0.0",
|
|
7
|
+
},
|
|
8
|
+
type: "problem",
|
|
9
|
+
},
|
|
10
|
+
create: function (context) {
|
|
11
|
+
const m = context.cds.model;
|
|
12
|
+
if (m.err) {
|
|
13
|
+
if (m.err) {
|
|
14
|
+
// If any csn compile errors occur
|
|
15
|
+
m.err.messages.forEach((err) => {
|
|
16
|
+
const msg = err.message;
|
|
17
|
+
let file = "";
|
|
18
|
+
const loc = {
|
|
19
|
+
start: { line: 0, column: 0 },
|
|
20
|
+
end: { line: 1, column: 0 },
|
|
21
|
+
};
|
|
22
|
+
// Get its location if it exists
|
|
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
|
+
context.report({ message: `${msg}`, loc, file });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -1,40 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const child_process_1 = __importDefault(require("child_process"));
|
|
7
|
-
const semver_1 = __importDefault(require("semver"));
|
|
8
|
-
module.exports = ruleFactory_1.createRule({
|
|
9
|
-
type: 'suggestion',
|
|
1
|
+
const cp = require("child_process");
|
|
2
|
+
const semver = require("semver");
|
|
3
|
+
|
|
4
|
+
module.exports = require("../../api").createRule({
|
|
5
|
+
meta: {
|
|
10
6
|
docs: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
description: "Checks whether the latest cds version is being used.",
|
|
8
|
+
category: "Environment",
|
|
9
|
+
version: "1.0.4",
|
|
14
10
|
},
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
messages: {
|
|
14
|
+
latestCDSVersion: `A newer CDS version is available!`,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create: function(context) {
|
|
17
18
|
let result;
|
|
18
19
|
let cdsVersions;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const e = context.cds.environment;
|
|
21
|
+
if (!e) {
|
|
22
|
+
try {
|
|
23
|
+
result = cp
|
|
24
|
+
.execSync(`npm outdated @sap/cds --json`, {
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
})
|
|
27
|
+
.toString();
|
|
28
|
+
cdsVersions = JSON.parse(result)["@sap/cds"];
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Do not throw
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
cdsVersions = context.cds.environment["@sap/cds"];
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// If current cds version is not the latest
|
|
36
|
+
if (
|
|
37
|
+
Object.keys(cdsVersions).length !== 0 &&
|
|
38
|
+
!semver.satisfies(cdsVersions.latest, cdsVersions.current)
|
|
39
|
+
) {
|
|
40
|
+
// Add to ESLint report
|
|
41
|
+
context.report({
|
|
42
|
+
messageId: 'latestCDSVersion',
|
|
43
|
+
loc: { line: 0, column: 0 },
|
|
44
|
+
});
|
|
37
45
|
}
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
//# sourceMappingURL=latest-cds-version.js.map
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -1,40 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const semver_1 = __importDefault(require("semver"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
module.exports = ruleFactory_1.createRule({
|
|
10
|
-
type: 'problem',
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const semver = require("semver");
|
|
3
|
+
|
|
4
|
+
module.exports = require("../../api").createRule({
|
|
5
|
+
meta: {
|
|
11
6
|
docs: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
description: `Checks whether the minimum node version required by the \`@sap/cds\` is achieved.`,
|
|
8
|
+
category: "Environment",
|
|
9
|
+
version: "1.0.0",
|
|
15
10
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
type: "problem",
|
|
12
|
+
},
|
|
13
|
+
create: function (context) {
|
|
14
|
+
const e = context.cds.environment;
|
|
19
15
|
let nodeVersion, nodeVersionCDS;
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
nodeVersionCDS = (_a = jsonCDS.engines) === null || _a === void 0 ? void 0 : _a.node;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
nodeVersion = cds.environment.nodeVersion;
|
|
30
|
-
nodeVersionCDS = cds.environment.nodeVersionCDS;
|
|
31
|
-
}
|
|
32
|
-
if (nodeVersion && nodeVersionCDS && !semver_1.default.satisfies(nodeVersion, nodeVersionCDS, { loose: true })) {
|
|
33
|
-
report.push({
|
|
34
|
-
message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
|
|
35
|
-
loc: { line: 0, column: 1 }
|
|
16
|
+
if (!e) {
|
|
17
|
+
// Get current and required node versions
|
|
18
|
+
try {
|
|
19
|
+
const CDSPath = require.resolve("@sap/cds/package.json", {
|
|
20
|
+
paths: [path.dirname(context.filePath)],
|
|
36
21
|
});
|
|
22
|
+
const jsonCDS = require(CDSPath);
|
|
23
|
+
nodeVersion = process.version;
|
|
24
|
+
nodeVersionCDS = jsonCDS.engines.node;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// Do not throw
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
nodeVersion = context.cds.environment.nodeVersion;
|
|
30
|
+
nodeVersionCDS = context.cds.environment.nodeVersionCDS;
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
nodeVersion &&
|
|
34
|
+
nodeVersionCDS &&
|
|
35
|
+
!semver.satisfies(nodeVersion, nodeVersionCDS, { loose: true })
|
|
36
|
+
) {
|
|
37
|
+
context.report({
|
|
38
|
+
message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
|
|
39
|
+
loc: { line: 0, column: 1 },
|
|
40
|
+
});
|
|
37
41
|
}
|
|
38
|
-
|
|
42
|
+
},
|
|
39
43
|
});
|
|
40
|
-
//# sourceMappingURL=min-node-version.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = require("../../api").defineRule({
|
|
2
|
+
meta: { docs: { description: `Avoid using reserved SQL keywords.` }},
|
|
3
|
+
create (context) {
|
|
4
|
+
const { db = { kind: 'sql' } } = context.cds.env.requires
|
|
5
|
+
function _check (d) {
|
|
6
|
+
if (d.name in RESERVED) {
|
|
7
|
+
|
|
8
|
+
// Do not blame in case of external services
|
|
9
|
+
let srv = d._service || d.parent && d.parent._service
|
|
10
|
+
if (srv && srv['@cds.external']) return
|
|
11
|
+
|
|
12
|
+
// Do blame
|
|
13
|
+
return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { entity: _check, element: _check }
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// REVISIT: Replace by compiler-provided check
|
|
21
|
+
const RESERVED = {
|
|
22
|
+
ORDER: 1, Order: 1, order: 1,
|
|
23
|
+
GROUP: 1, Group: 1, group: 1,
|
|
24
|
+
LIMIT: 1, Limit: 1, limit: 1,
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "2.1.0",
|
|
7
|
+
},
|
|
8
|
+
type: "problem",
|
|
9
|
+
},
|
|
10
|
+
create: function(context) {
|
|
11
|
+
const m = context.cds.model;
|
|
12
|
+
m.forall((d) => {
|
|
13
|
+
if (d.name) {
|
|
14
|
+
if (!d.elements) return;
|
|
15
|
+
for (const elementName in d.elements) {
|
|
16
|
+
const element = d.elements[elementName];
|
|
17
|
+
if (element.is2many && !element.on) {
|
|
18
|
+
const loc = context.cds.getLocation(elementName, element);
|
|
19
|
+
context.report({
|
|
20
|
+
message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${element.name}'.`,
|
|
21
|
+
loc,
|
|
22
|
+
file: d.$location.file,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: "Should make suggestions for possible missing sql casts.",
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "1.0.8",
|
|
7
|
+
},
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
hasSuggestions: true,
|
|
10
|
+
messages: {
|
|
11
|
+
missingSQLCast: "Potential issue - Missing SQL cast for column expression?",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
create: function (context) {
|
|
15
|
+
const m = context.cds.model;
|
|
16
|
+
if (m) {
|
|
17
|
+
const view = (d) => d.query;
|
|
18
|
+
m.foreach(view, (v) => {
|
|
7
19
|
if (v.query.SET)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
for (const { SELECT } of v.query.SET.args) {
|
|
21
|
+
// Only in UNION cases?
|
|
22
|
+
for (const each of SELECT.columns || []) {
|
|
23
|
+
const { xpr, cast, $location: location } = each;
|
|
24
|
+
if (cast && xpr) {
|
|
25
|
+
if (xpr[0].xpr && xpr[0].xpr && xpr[0].cast) {
|
|
26
|
+
continue;
|
|
27
|
+
} else {
|
|
28
|
+
if (context.sourcecode.lines[location.line - 1]) {
|
|
29
|
+
const endCol = context.sourcecode.lines[location.line - 1].length;
|
|
30
|
+
const loc = {
|
|
31
|
+
start: { line: location.line, column: location.col - 1 },
|
|
32
|
+
end: { line: location.line, column: endCol },
|
|
33
|
+
};
|
|
34
|
+
context.report({
|
|
35
|
+
messageId: "missingSQLCast",
|
|
36
|
+
loc,
|
|
37
|
+
file: location.file,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
19
40
|
}
|
|
41
|
+
}
|
|
20
42
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return results;
|
|
24
|
-
}
|
|
25
|
-
module.exports = ruleFactory_1.createRule({
|
|
26
|
-
type: 'problem',
|
|
27
|
-
docs: {
|
|
28
|
-
description: `Should make suggestions for possible missing sql casts.`,
|
|
29
|
-
category: 'Model Validation',
|
|
30
|
-
version: '1.0.8'
|
|
31
|
-
},
|
|
32
|
-
fixable: 'code'
|
|
33
|
-
}, (cds, context) => {
|
|
34
|
-
let report = [];
|
|
35
|
-
const filepath = context.getFilename();
|
|
36
|
-
if (filepath.endsWith('.cds')) {
|
|
37
|
-
const m = cds.linked(cds.model);
|
|
38
|
-
if (m) {
|
|
39
|
-
report = suggestSQLCast(m);
|
|
40
|
-
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
41
45
|
}
|
|
42
|
-
return report;
|
|
46
|
+
return context.report;
|
|
47
|
+
},
|
|
43
48
|
});
|
|
44
|
-
//# sourceMappingURL=sql-cast-suggestion.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: "Regular element names should start with lowercase letters.",
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "1.0.4",
|
|
7
|
+
},
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
hasSuggestions: true,
|
|
10
|
+
messages: {
|
|
11
|
+
startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
|
|
12
|
+
},
|
|
13
|
+
fixable: "code",
|
|
14
|
+
},
|
|
15
|
+
create: function (context) {
|
|
16
|
+
const m = context.cds.model;
|
|
17
|
+
if (m && m.definitions) {
|
|
18
|
+
m.forall((d) => {
|
|
19
|
+
const entityName = d.name;
|
|
20
|
+
for (const elementName in d.elements) {
|
|
21
|
+
const element = d.elements[elementName];
|
|
22
|
+
if (
|
|
23
|
+
elementName &&
|
|
24
|
+
!(entityName.startsWith("localized") || entityName.endsWith("texts"))
|
|
25
|
+
) {
|
|
26
|
+
if (
|
|
27
|
+
elementName.charAt(0) !== elementName.charAt(0).toLowerCase() &&
|
|
28
|
+
!["ID"].includes(elementName)
|
|
29
|
+
) {
|
|
30
|
+
if (element.$location && element.$location.file) {
|
|
31
|
+
const file = element.$location.file;
|
|
32
|
+
const loc = context.cds.getLocation(elementName, element);
|
|
33
|
+
const fix = (fixer) => {
|
|
34
|
+
const elementNameSanitized =
|
|
35
|
+
elementName.charAt(0).toLowerCase() + elementName.slice(1);
|
|
36
|
+
const rangeEnd = context.sourcecode.getIndexFromLoc({
|
|
37
|
+
line: loc.end.line,
|
|
38
|
+
column: loc.end.column,
|
|
39
|
+
});
|
|
40
|
+
const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0;
|
|
41
|
+
return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
|
|
42
|
+
};
|
|
43
|
+
context.report({
|
|
44
|
+
messageId: "startLowercase",
|
|
45
|
+
loc,
|
|
46
|
+
file,
|
|
47
|
+
fix,
|
|
48
|
+
data: {
|
|
49
|
+
entityName,
|
|
50
|
+
elementName
|
|
51
|
+
},
|
|
52
|
+
suggest: [
|
|
53
|
+
{
|
|
54
|
+
messageId: "startLowercase",
|
|
55
|
+
fix,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module.exports = require("../../api").createRule({
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
description: "Regular entity names should start with uppercase letters.",
|
|
5
|
+
category: "Model Validation",
|
|
6
|
+
version: "1.0.4",
|
|
7
|
+
},
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
hasSuggestions: true,
|
|
10
|
+
messages: {
|
|
11
|
+
startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
|
|
12
|
+
},
|
|
13
|
+
fixable: "code",
|
|
14
|
+
},
|
|
15
|
+
create: function (context) {
|
|
16
|
+
const m = context.cds.model;
|
|
17
|
+
if (m) {
|
|
18
|
+
m.foreach("entity", (e) => {
|
|
19
|
+
let entityName = e.name;
|
|
20
|
+
const names = entityName.split(".");
|
|
21
|
+
entityName = names[names.length - 1];
|
|
22
|
+
if (entityName && !(entityName.startsWith("localized") || entityName.endsWith("texts"))) {
|
|
23
|
+
if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
|
|
24
|
+
if (e.$location && e.$location.file) {
|
|
25
|
+
const file = e.$location.file;
|
|
26
|
+
const loc = context.cds.getLocation(entityName, e);
|
|
27
|
+
const fix = (fixer) => {
|
|
28
|
+
const entityNameSanitized =
|
|
29
|
+
entityName.charAt(0).toUpperCase() + entityName.slice(1);
|
|
30
|
+
const rangeEnd = context.sourcecode.getIndexFromLoc({
|
|
31
|
+
line: loc.end.line,
|
|
32
|
+
column: loc.end.column,
|
|
33
|
+
});
|
|
34
|
+
const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0;
|
|
35
|
+
return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
|
|
36
|
+
};
|
|
37
|
+
context.report({
|
|
38
|
+
messageId: "startUppercase",
|
|
39
|
+
loc,
|
|
40
|
+
file,
|
|
41
|
+
fix,
|
|
42
|
+
data: { entityName },
|
|
43
|
+
suggest: [
|
|
44
|
+
{
|
|
45
|
+
messageId: "startUppercase",
|
|
46
|
+
fix,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Rule, RuleTester, SourceCode } from "eslint";
|
|
2
|
+
|
|
3
|
+
export interface CDSRuleMetaData extends Rule.RuleMetaData {
|
|
4
|
+
docs: {
|
|
5
|
+
/** provides the short description of the rule in the [rules index](https://eslint.org/docs/rules/) */
|
|
6
|
+
description: Rule.RuleMetaData['docs']['description'];
|
|
7
|
+
/** specifies version of @sap/eslint-plugin-cds at which the rule was first implemented */
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
messages?: Rule.RuleMetaData['messages'];
|
|
11
|
+
fixable?: Rule.RuleMetaData['fixable'];
|
|
12
|
+
schema?: Rule.RuleMetaData['schema'];
|
|
13
|
+
deprecated?: Rule.RuleMetaData['deprecated'];
|
|
14
|
+
type?: Rule.RuleMetaData['type'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CDSReport = Rule.ReportDescriptor & { file?: string };
|
|
18
|
+
|
|
19
|
+
export interface CDSRuleContext extends Rule.RuleContext {
|
|
20
|
+
cds: any;
|
|
21
|
+
configPath: string;
|
|
22
|
+
code: string;
|
|
23
|
+
filePath: string;
|
|
24
|
+
options: [];
|
|
25
|
+
ruleID: string;
|
|
26
|
+
sourcecode: SourceCode
|
|
27
|
+
}
|
|
28
|
+
export interface CDSRuleSpec {
|
|
29
|
+
meta: CDSRuleMetaData,
|
|
30
|
+
create: (arg0: CDSRuleContext) => Rule.ReportDescriptor;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CDSTestCaseError extends RuleTester.TestCaseError {
|
|
34
|
+
message: string | RegExp;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CDSRuleTestOpts {
|
|
38
|
+
/** specifies __dirname */
|
|
39
|
+
root: string;
|
|
40
|
+
/** requires your rule .js here */
|
|
41
|
+
rule?: string;
|
|
42
|
+
/** filename ('schema.cds' for model, 'package.json' for env) */
|
|
43
|
+
filename: string;
|
|
44
|
+
/** resolves cds parser path */
|
|
45
|
+
parser?: string;
|
|
46
|
+
/** List of warnings/errors from ESLint's [ruleTester](https://eslint.org/docs/developer-guide/nodejs-api#ruletester) */
|
|
47
|
+
errors: CDSTestCaseError[]
|
|
48
|
+
}
|