@sap/eslint-plugin-cds 2.2.2 → 2.3.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 +4 -54
- package/lib/impl/constants.js +1 -11
- package/lib/impl/index.js +6 -1
- package/lib/impl/ruleFactory.js +44 -14
- package/lib/impl/rules/assoc2many-ambiguous-key.js +4 -2
- package/lib/impl/rules/cds-compile-error.js +21 -22
- package/lib/impl/rules/latest-cds-version.js +6 -1
- package/lib/impl/rules/min-node-version.js +3 -1
- package/lib/impl/rules/no-db-keywords.js +3 -0
- package/lib/impl/rules/no-join-on-draft-enabled-entities.js +3 -1
- package/lib/impl/rules/require-2many-oncond.js +3 -1
- package/lib/impl/rules/sql-cast-suggestion.js +3 -1
- package/lib/impl/rules/start-elements-lowercase.js +3 -2
- package/lib/impl/rules/start-entities-uppercase.js +2 -2
- package/lib/impl/rules/valid-csv-header.js +92 -0
- package/lib/impl/utils/fuzzySearch.js +87 -0
- package/lib/impl/utils/model.js +52 -26
- package/lib/impl/utils/rules.js +269 -141
- package/lib/impl/utils/validate.js +6 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,67 +6,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
-
## [2.1.2] - 2021-10-05
|
|
10
|
-
|
|
11
|
-
## Changed
|
|
12
|
-
|
|
13
|
-
- Allow not only *.js but also other file types (i.e. *.ts, etc) to bypass plugin rules
|
|
14
|
-
|
|
15
|
-
## [2.1.1] - 2021-10-04
|
|
16
9
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
- Added preprocessor to avoid (other plugins) parsing errors on cds files
|
|
20
|
-
|
|
21
|
-
## [2.2.0] - 2021-10-29
|
|
10
|
+
## [2.3.0] - 201-12-03
|
|
22
11
|
|
|
23
12
|
## Added
|
|
24
13
|
|
|
25
|
-
- Added
|
|
14
|
+
- Added new rule 'valid-csv-header'
|
|
26
15
|
|
|
27
16
|
## Changed
|
|
28
17
|
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
## [2.1.2] - 2021-10-05
|
|
32
|
-
|
|
33
|
-
## Changed
|
|
34
|
-
|
|
35
|
-
- Allow not only *.js but also other file types (i.e. *.ts, etc) to bypass plugin rules
|
|
36
|
-
|
|
37
|
-
## [2.1.1] - 2021-10-04
|
|
38
|
-
|
|
39
|
-
## Changed
|
|
40
|
-
|
|
41
|
-
- Added preprocessor to avoid (other plugins) parsing errors on cds files
|
|
42
|
-
|
|
43
|
-
## [2.2.1] - 2021-10-29
|
|
44
|
-
|
|
45
|
-
## Changed
|
|
46
|
-
|
|
47
|
-
- Optimized model loading and fixed bug in loading of 'outsider' files
|
|
48
|
-
|
|
49
|
-
## [2.2.0] - 2021-10-29
|
|
50
|
-
|
|
51
|
-
## Added
|
|
52
|
-
|
|
53
|
-
- Added typings to javascript for all exposed apis
|
|
54
|
-
|
|
55
|
-
## Changed
|
|
56
|
-
|
|
57
|
-
- Aligned rule creation and tester api with ESLint
|
|
58
|
-
|
|
59
|
-
## [2.1.2] - 2021-10-05
|
|
60
|
-
|
|
61
|
-
## Changed
|
|
62
|
-
|
|
63
|
-
- Allow not only *.js but also other file types (i.e. *.ts, etc) to bypass plugin rules
|
|
64
|
-
|
|
65
|
-
## [2.1.1] - 2021-10-04
|
|
66
|
-
|
|
67
|
-
## Changed
|
|
68
|
-
|
|
69
|
-
- Added preprocessor to avoid (other plugins) parsing errors on cds files
|
|
18
|
+
- Fixed suggestion messages in editor option (and disabled auto-fix)
|
|
19
|
+
- Added rule properties 'docs.recommended', 'severity'
|
|
70
20
|
|
|
71
21
|
## [2.2.2] - 2021-11-08
|
|
72
22
|
|
package/lib/impl/constants.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* - categories: The category labels we use to for model and environment rules
|
|
4
4
|
* - customRulesDir: The custom rules directory name in the user's project home
|
|
5
5
|
* which contains the subdirs 'docs', 'rules' and 'tests'
|
|
6
|
-
* - recommended: The set of this plugin's recommended rules and their severities
|
|
7
6
|
* - globals: The globals which should be exposed to ESLint by this plugin
|
|
8
7
|
* - files: Any additional file extensions which ESLint should lint
|
|
9
8
|
*/
|
|
@@ -14,15 +13,6 @@ module.exports = {
|
|
|
14
13
|
model: "Model Validation",
|
|
15
14
|
},
|
|
16
15
|
customRulesDir: ".eslint",
|
|
17
|
-
recommended: {
|
|
18
|
-
"@sap/cds/assoc2many-ambiguous-key": 1,
|
|
19
|
-
"@sap/cds/cds-compile-error": 2,
|
|
20
|
-
"@sap/cds/min-node-version": 2,
|
|
21
|
-
"@sap/cds/no-join-on-draft-enabled-entities": 1,
|
|
22
|
-
"@sap/cds/no-db-keywords": 2,
|
|
23
|
-
"@sap/cds/require-2many-oncond": 2,
|
|
24
|
-
"@sap/cds/sql-cast-suggestion": 1,
|
|
25
|
-
},
|
|
26
16
|
globals: {
|
|
27
17
|
SELECT: true,
|
|
28
18
|
INSERT: true,
|
|
@@ -35,6 +25,6 @@ module.exports = {
|
|
|
35
25
|
CXL: true,
|
|
36
26
|
cds: true,
|
|
37
27
|
},
|
|
38
|
-
files: ["*.cds", "*.csn"],
|
|
28
|
+
files: ["*.cds", "*.csn", "*.csv", "undeploy.json"],
|
|
39
29
|
modelFiles: ["*.cds", "*.csn"],
|
|
40
30
|
};
|
package/lib/impl/index.js
CHANGED
|
@@ -22,7 +22,7 @@ const {
|
|
|
22
22
|
getLocation,
|
|
23
23
|
getRange,
|
|
24
24
|
} = require("../impl/utils/model");
|
|
25
|
-
const { files, globals
|
|
25
|
+
const { files, globals } = require("./constants");
|
|
26
26
|
|
|
27
27
|
const cds = require("@sap/cds");
|
|
28
28
|
cds.getLocation = getLocation;
|
|
@@ -36,6 +36,11 @@ if (!Cache.has("rulesInfo")) {
|
|
|
36
36
|
} else {
|
|
37
37
|
rulesInfo = Cache.get("rulesInfo");
|
|
38
38
|
}
|
|
39
|
+
const recommended = Object.assign({},
|
|
40
|
+
...Object.entries(rulesInfo.rules)
|
|
41
|
+
.filter(([k,v]) => (v.meta.docs.recommended))
|
|
42
|
+
.map(([k,v]) => ({ [`@sap/cds/${k}`]:v.meta.severity }))
|
|
43
|
+
);
|
|
39
44
|
|
|
40
45
|
module.exports = {
|
|
41
46
|
configs: {
|
package/lib/impl/ruleFactory.js
CHANGED
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
const fs = require("fs");
|
|
32
32
|
const path = require("path");
|
|
33
33
|
const { RuleTester, SourceCode } = require("eslint");
|
|
34
|
-
const { isEditor, isValidFile
|
|
34
|
+
const { isEditor, isValidFile } = require("./utils/helpers");
|
|
35
35
|
const { isValidEnv, isValidModel } = require("./utils/validate");
|
|
36
36
|
const {
|
|
37
37
|
Cache,
|
|
38
38
|
populateModelAndEnv,
|
|
39
39
|
hasCompilationError,
|
|
40
40
|
getAST,
|
|
41
|
+
initModelRuleTester,
|
|
41
42
|
loadConfigPath,
|
|
42
43
|
} = require("./utils/model");
|
|
43
44
|
const { isRuleDisabled, getRules, populateRules } = require("./utils/rules");
|
|
@@ -60,9 +61,6 @@ const { customRulesDir, categories } = require("./constants");
|
|
|
60
61
|
*/
|
|
61
62
|
function createRule(spec) {
|
|
62
63
|
const { meta, create } = spec;
|
|
63
|
-
if (!meta.type) meta.type = "problem";
|
|
64
|
-
if (meta.docs && !meta.docs.category)
|
|
65
|
-
meta.docs.category = categories["model"];
|
|
66
64
|
return {
|
|
67
65
|
meta,
|
|
68
66
|
create: function (context) {
|
|
@@ -84,11 +82,11 @@ function createRule(spec) {
|
|
|
84
82
|
try {
|
|
85
83
|
create(cdscontext);
|
|
86
84
|
} catch (err) {
|
|
87
|
-
// Do not throw to avoid ESLint VSCode editor pop-ups
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
if (isEditor()) { // Do not throw to avoid ESLint VSCode editor pop-ups
|
|
86
|
+
console.error(`An error occurred while linting. Rule: ${cdscontext.ruleID}\n`, err);
|
|
87
|
+
} else {
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
92
90
|
}
|
|
93
91
|
// Show compilation error only on console
|
|
94
92
|
} else if (hasCompilationError(cdscontext) && !isEditor()) {
|
|
@@ -210,7 +208,12 @@ function getProxyReport(obj) {
|
|
|
210
208
|
*/
|
|
211
209
|
function createCDSContext(context, node, meta) {
|
|
212
210
|
const filePath = context.getPhysicalFilename();
|
|
213
|
-
|
|
211
|
+
let configPath;
|
|
212
|
+
if (!Cache.has("pluginpath")) {
|
|
213
|
+
configPath = loadConfigPath(filePath);
|
|
214
|
+
} else {
|
|
215
|
+
configPath = Cache.get("configpath")
|
|
216
|
+
}
|
|
214
217
|
let category = "model";
|
|
215
218
|
if (meta.docs.category === categories["env"]) {
|
|
216
219
|
category = "env";
|
|
@@ -234,10 +237,32 @@ function createCDSContext(context, node, meta) {
|
|
|
234
237
|
options: context.options,
|
|
235
238
|
report: getProxyReport(context.report),
|
|
236
239
|
ruleID: context.id,
|
|
237
|
-
sourcecode
|
|
240
|
+
sourcecode
|
|
238
241
|
};
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
function getProxyRun(obj) {
|
|
245
|
+
const handler = {
|
|
246
|
+
get(target, prop, receiver) {
|
|
247
|
+
const value = Reflect.get(target, prop, receiver);
|
|
248
|
+
if (typeof value !== "object") {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
/* eslint no-extra-boolean-cast: "off" */
|
|
252
|
+
if (!!value) {
|
|
253
|
+
return new Proxy(value, handler);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
err: `Property ${prop} prop does not exist on object ${obj}!`,
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
apply(target, thisArg, argumentsList) {
|
|
260
|
+
return thisArg.run();
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
return new Proxy(obj, handler);
|
|
264
|
+
}
|
|
265
|
+
|
|
241
266
|
/**
|
|
242
267
|
* ESLint RuleTester (used by custom rule creator api)
|
|
243
268
|
* Calls ESLint's RuleTester with custom cds parser and input for
|
|
@@ -247,20 +272,22 @@ function createCDSContext(context, node, meta) {
|
|
|
247
272
|
* @param {CDSRuleTestOpts} options RuleTester input options
|
|
248
273
|
* @returns RuleTester results
|
|
249
274
|
*/
|
|
250
|
-
function runRuleTester(options) {
|
|
251
|
-
process.env['RULE_TESTER'] = true;
|
|
275
|
+
function runRuleTester(options, dryRun=false) {
|
|
252
276
|
let parser;
|
|
253
277
|
let rule = {};
|
|
278
|
+
process.env.LINT_FLAVOR = "inferred";
|
|
254
279
|
const rulename = path.basename(options.root);
|
|
255
280
|
const plugin = "eslint-plugin-cds";
|
|
256
281
|
if (options.root.includes(plugin)) {
|
|
257
282
|
// For plugin's internal tests, resolve parser from here
|
|
258
283
|
parser = require.resolve("./parser");
|
|
259
284
|
rule = require(`./rules/${path.basename(options.root)}`);
|
|
285
|
+
const pluginPath = path.join(path.dirname(options.root), "../..");
|
|
260
286
|
Cache.set(
|
|
261
287
|
"rulesInfo",
|
|
262
288
|
getRules(path.join(path.dirname(options.root), "../../lib/impl/rules"))
|
|
263
289
|
);
|
|
290
|
+
Cache.set("pluginpath", pluginPath);
|
|
264
291
|
} else {
|
|
265
292
|
// Otherwise from project root
|
|
266
293
|
const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
|
|
@@ -271,7 +298,9 @@ function runRuleTester(options) {
|
|
|
271
298
|
options.root,
|
|
272
299
|
`../../rules/${path.basename(options.root)}`
|
|
273
300
|
));
|
|
301
|
+
const pluginPath = path.join(path.dirname(options.root), "../../../..");
|
|
274
302
|
Cache.set("rulesInfo", getRules(path.join(options.root, "../../rules")));
|
|
303
|
+
Cache.set("pluginpath", pluginPath);
|
|
275
304
|
}
|
|
276
305
|
let category = categories["model"];
|
|
277
306
|
if (rule.meta) {
|
|
@@ -296,11 +325,12 @@ function runRuleTester(options) {
|
|
|
296
325
|
];
|
|
297
326
|
} else if (!category || category === categories.model) {
|
|
298
327
|
testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
|
|
328
|
+
initModelRuleTester(filePath);
|
|
299
329
|
}
|
|
300
330
|
if (type === "invalid") {
|
|
301
331
|
testerCases[type][0].errors = options.errors;
|
|
302
332
|
const fileFixed = path.join(options.root, `fixed/${options.filename}`);
|
|
303
|
-
if (fs.existsSync(fileFixed)) {
|
|
333
|
+
if (fs.existsSync(fileFixed) && rule.meta.type !== "suggestion") {
|
|
304
334
|
testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
|
|
305
335
|
}
|
|
306
336
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
module.exports = require("../../api").createRule({
|
|
2
2
|
meta: {
|
|
3
3
|
docs: {
|
|
4
|
-
description:
|
|
4
|
+
description: "Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.",
|
|
5
5
|
category: "Model Validation",
|
|
6
|
+
recommended: true,
|
|
6
7
|
version: "1.0.1",
|
|
7
8
|
},
|
|
8
|
-
|
|
9
|
+
severity: "warn",
|
|
10
|
+
type: "problem"
|
|
9
11
|
},
|
|
10
12
|
create(context) {
|
|
11
13
|
let csnOdata;
|
|
@@ -5,31 +5,30 @@ module.exports = require("../../api").createRule({
|
|
|
5
5
|
category: "Model Validation",
|
|
6
6
|
version: "1.0.0",
|
|
7
7
|
},
|
|
8
|
+
severity: "error",
|
|
8
9
|
type: "problem",
|
|
9
10
|
},
|
|
10
11
|
create: function (context) {
|
|
11
12
|
const m = context.cds.model;
|
|
12
|
-
if (m.err) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
}
|
|
13
|
+
if (m && 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
|
+
});
|
|
33
32
|
}
|
|
34
|
-
}
|
|
33
|
+
}
|
|
35
34
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
const os = require("os");
|
|
1
2
|
const cp = require("child_process");
|
|
2
3
|
const semver = require("semver");
|
|
3
4
|
|
|
5
|
+
const IS_WIN = os.platform() === "win32";
|
|
6
|
+
|
|
4
7
|
module.exports = require("../../api").createRule({
|
|
5
8
|
meta: {
|
|
6
9
|
docs: {
|
|
7
|
-
description: "Checks whether the latest cds version is being used.",
|
|
10
|
+
description: "Checks whether the latest `@sap/cds` version is being used.",
|
|
8
11
|
category: "Environment",
|
|
9
12
|
version: "1.0.4",
|
|
10
13
|
},
|
|
@@ -23,6 +26,8 @@ module.exports = require("../../api").createRule({
|
|
|
23
26
|
result = cp
|
|
24
27
|
.execSync(`npm outdated @sap/cds --json`, {
|
|
25
28
|
cwd: process.cwd(),
|
|
29
|
+
shell: IS_WIN,
|
|
30
|
+
stdio: "pipe",
|
|
26
31
|
})
|
|
27
32
|
.toString();
|
|
28
33
|
cdsVersions = JSON.parse(result)["@sap/cds"];
|
|
@@ -4,10 +4,12 @@ const semver = require("semver");
|
|
|
4
4
|
module.exports = require("../../api").createRule({
|
|
5
5
|
meta: {
|
|
6
6
|
docs: {
|
|
7
|
-
description: `Checks whether the minimum
|
|
7
|
+
description: `Checks whether the minimum Node.js version required by \`@sap/cds\` is achieved.`,
|
|
8
8
|
category: "Environment",
|
|
9
|
+
recommended: true,
|
|
9
10
|
version: "1.0.0",
|
|
10
11
|
},
|
|
12
|
+
severity: "error",
|
|
11
13
|
type: "problem",
|
|
12
14
|
},
|
|
13
15
|
create: function (context) {
|
|
@@ -3,7 +3,10 @@ module.exports = require("../../api").defineRule({
|
|
|
3
3
|
docs: {
|
|
4
4
|
description: `Avoid using reserved SQL keywords.`,
|
|
5
5
|
category: "Model Validation",
|
|
6
|
+
recommended: true,
|
|
7
|
+
version: "2.1.0",
|
|
6
8
|
},
|
|
9
|
+
severity: "error"
|
|
7
10
|
},
|
|
8
11
|
create(context) {
|
|
9
12
|
const { db = { kind: "sql" } } = context.cds.env.requires;
|
|
@@ -3,15 +3,17 @@ module.exports = require("../../api").createRule({
|
|
|
3
3
|
docs: {
|
|
4
4
|
description: `Draft-enabled entities shall not be used in views that make use of \`JOIN\`.`,
|
|
5
5
|
category: "Model Validation",
|
|
6
|
+
recommended: true,
|
|
6
7
|
version: "2.2.1",
|
|
7
8
|
},
|
|
9
|
+
severity: "warn",
|
|
8
10
|
type: "suggestion",
|
|
9
11
|
messages: {
|
|
10
12
|
noJoinOnDraftEnabledEntities: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
|
|
11
13
|
},
|
|
12
14
|
},
|
|
13
15
|
create: function (context) {
|
|
14
|
-
const m = context.cds.model;
|
|
16
|
+
const m = context.cds.model; if (!m) return
|
|
15
17
|
m.foreach("entity", (entity) => {
|
|
16
18
|
if (entity["@odata.draft.enabled"]) {
|
|
17
19
|
if (entity.query.SELECT.from.join) {
|
|
@@ -3,12 +3,14 @@ module.exports = require("../../api").createRule({
|
|
|
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
5
|
category: "Model Validation",
|
|
6
|
+
recommended: true,
|
|
6
7
|
version: "2.1.0",
|
|
7
8
|
},
|
|
9
|
+
severity: "error",
|
|
8
10
|
type: "problem",
|
|
9
11
|
},
|
|
10
12
|
create: function (context) {
|
|
11
|
-
const m = context.cds.model;
|
|
13
|
+
const m = context.cds.model; if (!m) return
|
|
12
14
|
m.forall((d) => {
|
|
13
15
|
if (d.name) {
|
|
14
16
|
if (!d.elements) return;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
module.exports = require("../../api").createRule({
|
|
2
2
|
meta: {
|
|
3
3
|
docs: {
|
|
4
|
-
description: "Should make suggestions for possible missing
|
|
4
|
+
description: "Should make suggestions for possible missing SQL casts.",
|
|
5
5
|
category: "Model Validation",
|
|
6
|
+
recommended: true,
|
|
6
7
|
version: "1.0.8",
|
|
7
8
|
},
|
|
9
|
+
severity: "warn",
|
|
8
10
|
type: "suggestion",
|
|
9
11
|
hasSuggestions: true,
|
|
10
12
|
messages: {
|
|
@@ -10,6 +10,8 @@ module.exports = require("../../api").createRule({
|
|
|
10
10
|
messages: {
|
|
11
11
|
startLowercase:
|
|
12
12
|
"Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
|
|
13
|
+
fixLowercase:
|
|
14
|
+
"Start element name with a lowercase letter."
|
|
13
15
|
},
|
|
14
16
|
fixable: "code",
|
|
15
17
|
},
|
|
@@ -52,14 +54,13 @@ module.exports = require("../../api").createRule({
|
|
|
52
54
|
messageId: "startLowercase",
|
|
53
55
|
loc,
|
|
54
56
|
file,
|
|
55
|
-
fix,
|
|
56
57
|
data: {
|
|
57
58
|
entityName,
|
|
58
59
|
elementName,
|
|
59
60
|
},
|
|
60
61
|
suggest: [
|
|
61
62
|
{
|
|
62
|
-
messageId: "
|
|
63
|
+
messageId: "fixLowercase",
|
|
63
64
|
fix,
|
|
64
65
|
},
|
|
65
66
|
],
|
|
@@ -10,6 +10,7 @@ module.exports = require("../../api").createRule({
|
|
|
10
10
|
messages: {
|
|
11
11
|
startUppercase:
|
|
12
12
|
"Entity name '{{entityName}}' should start with an uppercase letter.",
|
|
13
|
+
fixUppercase: "Start entity name with an uppercase letter.",
|
|
13
14
|
},
|
|
14
15
|
fixable: "code",
|
|
15
16
|
},
|
|
@@ -47,11 +48,10 @@ module.exports = require("../../api").createRule({
|
|
|
47
48
|
messageId: "startUppercase",
|
|
48
49
|
loc,
|
|
49
50
|
file,
|
|
50
|
-
fix,
|
|
51
51
|
data: { entityName },
|
|
52
52
|
suggest: [
|
|
53
53
|
{
|
|
54
|
-
messageId: "
|
|
54
|
+
messageId: "fixUppercase",
|
|
55
55
|
fix,
|
|
56
56
|
},
|
|
57
57
|
],
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const {basename, extname} = require('path')
|
|
2
|
+
const findFuzzy = require('../utils/fuzzySearch')
|
|
3
|
+
const SEP = '[,;\t]'
|
|
4
|
+
const EOL = '\\r?\\n'
|
|
5
|
+
|
|
6
|
+
module.exports = require("../../api").createRule({
|
|
7
|
+
meta: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: `CSV files for entities must refer to valid element names.`,
|
|
10
|
+
category: "Model Validation",
|
|
11
|
+
recommended: true,
|
|
12
|
+
version: "2.3.0",
|
|
13
|
+
},
|
|
14
|
+
severity: "warn",
|
|
15
|
+
type: "problem",
|
|
16
|
+
hasSuggestions: true,
|
|
17
|
+
messages: {
|
|
18
|
+
InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
|
|
19
|
+
ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
create: function (context) {
|
|
23
|
+
const {cds, code, filePath, sourcecode} = context
|
|
24
|
+
|
|
25
|
+
if (!filePath.endsWith('.csv')) return
|
|
26
|
+
if (!cds.model) return
|
|
27
|
+
let {env, model} = cds;
|
|
28
|
+
model = cds.compile.for.sql(model, {names:env.sql.names, messages: []} )
|
|
29
|
+
|
|
30
|
+
const filename = basename(filePath)
|
|
31
|
+
const entityName = filename.replace(/-/g,'.').slice(0, -extname(filename).length)
|
|
32
|
+
const entity = _entity4(entityName, model)
|
|
33
|
+
if (!entity) return
|
|
34
|
+
|
|
35
|
+
const elements = Object.values(entity.elements)
|
|
36
|
+
.filter (e => !!e['@cds.persistence.name'])
|
|
37
|
+
.map (e => e['@cds.persistence.name'].toUpperCase())
|
|
38
|
+
|
|
39
|
+
const [ cols ] = cds.parse.csv(code)
|
|
40
|
+
const missing = cols.filter (col => !elements.includes(col.toUpperCase()))
|
|
41
|
+
missing.forEach(miss => {
|
|
42
|
+
const index = _findInCode (miss, code)
|
|
43
|
+
const loc = sourcecode.getLocFromIndex(index)
|
|
44
|
+
const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
|
|
45
|
+
const suggest = candidates.map(cand => { return {
|
|
46
|
+
messageId: 'ReplaceColumnWith',
|
|
47
|
+
data: {column: miss, candidates:cand},
|
|
48
|
+
fix: (fixer) => fixer.replaceTextRange([index, index+miss.length], cand)
|
|
49
|
+
}})
|
|
50
|
+
context.report({
|
|
51
|
+
messageId: 'InvalidColumn',
|
|
52
|
+
data: {column: miss, candidates},
|
|
53
|
+
loc: {start: loc, end: {line: loc.line, column: loc.column+miss.length}},
|
|
54
|
+
file: filePath,
|
|
55
|
+
suggest
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function _findInCode (miss, code) {
|
|
63
|
+
// middle
|
|
64
|
+
let match = new RegExp(SEP+miss+SEP).exec(code)
|
|
65
|
+
if (match) return match.index+1
|
|
66
|
+
// end of line
|
|
67
|
+
match = new RegExp(SEP+miss+EOL).exec(code)
|
|
68
|
+
if (match) return match.index+1
|
|
69
|
+
// start of doc
|
|
70
|
+
match = new RegExp('^'+miss+SEP).exec(code)
|
|
71
|
+
if (match) return match.index
|
|
72
|
+
// somewhere (fallback)
|
|
73
|
+
return code.indexOf(miss)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _entity4 (name, csn) {
|
|
77
|
+
let entity = csn.definitions [name]
|
|
78
|
+
if (!entity) {
|
|
79
|
+
if (/(.+)[._]texts_?/.test (name)) { // 'Books.texts', 'Books.texts_de'
|
|
80
|
+
const base = csn.definitions [RegExp.$1]
|
|
81
|
+
return base && _entity4 (base.elements.texts.target, csn)
|
|
82
|
+
}
|
|
83
|
+
else return
|
|
84
|
+
}
|
|
85
|
+
// we also support simple views if they have no projection
|
|
86
|
+
const p = entity.query && entity.query.SELECT || entity.projection
|
|
87
|
+
if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
|
|
88
|
+
if (csn.definitions [p.from.ref[0]]) return entity
|
|
89
|
+
}
|
|
90
|
+
return entity.name ? entity : { name, __proto__:entity }
|
|
91
|
+
}
|
|
92
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levenshtein distance algorithm using recursive calls and cache
|
|
3
|
+
*
|
|
4
|
+
* @param input search the list for a best match for this string
|
|
5
|
+
* @param list a list of strings to match input against
|
|
6
|
+
* @param log logging method to use, might be null if no logging is wanted
|
|
7
|
+
* @returns array with best matches, is never null but might be empty in case no search was possible
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const cache = {};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
module.exports = (input, list, log) => {
|
|
15
|
+
let minDistWords = [];
|
|
16
|
+
|
|
17
|
+
if (input.length > 50 || list.length > 50) {
|
|
18
|
+
return minDistWords;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let minDist = Number.MAX_SAFE_INTEGER;
|
|
22
|
+
|
|
23
|
+
log && log('\nword\t\tlevDist\t\ttime(ms)');
|
|
24
|
+
|
|
25
|
+
let runtime = 0;
|
|
26
|
+
|
|
27
|
+
for (const word of list) {
|
|
28
|
+
const start = log && Date.now();
|
|
29
|
+
const levDist = levDistance(input, word);
|
|
30
|
+
|
|
31
|
+
if (log) {
|
|
32
|
+
const duration = Date.now() - start;
|
|
33
|
+
runtime = runtime + duration;
|
|
34
|
+
log(`${word}\t\t${levDist}\t\t${duration}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (levDist === minDist) {
|
|
38
|
+
minDistWords.push(word);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (levDist < minDist) {
|
|
42
|
+
minDist = levDist;
|
|
43
|
+
minDistWords = [word];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log && log(`runtime: ${runtime}ms`);
|
|
48
|
+
|
|
49
|
+
return minDistWords.sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
const levDistance = (a, b) => {
|
|
54
|
+
|
|
55
|
+
if (cache[a] && cache[a][b]) {
|
|
56
|
+
return cache[a][b];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (a.length === 0) {
|
|
60
|
+
return addToCache(a, b, b.length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (b.length === 0) {
|
|
64
|
+
return addToCache(a, b, a.length);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const tail_a = a.substring(1);
|
|
68
|
+
const tail_b = b.substring(1);
|
|
69
|
+
|
|
70
|
+
if (a[0] === b[0]) {
|
|
71
|
+
return levDistance(tail_a, tail_b);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lev1 = levDistance(tail_a, b);
|
|
75
|
+
const lev2 = levDistance(a, tail_b);
|
|
76
|
+
const lev3 = levDistance(tail_a, tail_b);
|
|
77
|
+
|
|
78
|
+
const levDist = Math.min(lev1, lev2, lev3) + 1;
|
|
79
|
+
return addToCache(a, b, levDist);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
const addToCache = (a, b, value) => {
|
|
84
|
+
cache[a] = cache[a] || {};
|
|
85
|
+
cache[a][b] = value;
|
|
86
|
+
return value;
|
|
87
|
+
}
|