@sap/eslint-plugin-cds 2.3.5 → 2.4.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 +19 -0
- package/lib/processor.js +4 -1
- package/lib/rules/auth-no-empty-restrictions.js +45 -0
- package/lib/rules/auth-use-requires.js +38 -0
- package/lib/rules/auth-valid-restrict-grant.js +68 -0
- package/lib/rules/auth-valid-restrict-keys.js +37 -0
- package/lib/rules/auth-valid-restrict-to.js +86 -0
- package/lib/rules/auth-valid-restrict-where.js +80 -0
- package/lib/rules/no-db-keywords.js +0 -2
- package/lib/rules/no-dollar-prefixed-names.js +1 -29
- package/lib/rules/start-elements-lowercase.js +2 -2
- package/lib/rules/start-entities-uppercase.js +2 -2
- package/lib/utils/fuzzySearch.js +9 -2
- package/lib/utils/helpers.js +47 -0
- package/lib/utils/model.js +1 -2
- package/lib/utils/ruleHelpers.js +145 -2
- package/lib/utils/ruleTester.js +1 -2
- package/lib/utils/rules.js +1038 -975
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.4.1] - 2022-06-17
|
|
10
|
+
|
|
11
|
+
- Added authorization rules 'auth-*'.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Node.js 14 is now the minimum required Node.js version. Version 12 is no longer supported.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [2.4.0] - 2022-04-14
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Rule report recycling ensures that rules are created/run only once for the root model
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Rule `no-dollar-prefixed-names` no longer acts on compiler warning messages
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
9
28
|
## [2.3.5] - 2022-04-05
|
|
10
29
|
|
|
11
30
|
### Changed
|
package/lib/processor.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = {
|
|
|
31
31
|
messages.forEach(fileMessages => {
|
|
32
32
|
const fileMessagesSanitized = [];
|
|
33
33
|
fileMessages.forEach(r => {
|
|
34
|
-
if (r.message.startsWith(`CompilationError:`)) {
|
|
34
|
+
if (r && r.message && r.message.startsWith(`CompilationError:`)) {
|
|
35
35
|
r.message = r.message.replace(`CompilationError: `,
|
|
36
36
|
'CDS model could not be compiled!\n');
|
|
37
37
|
r.ruleId = `❗${r.ruleId}`;
|
|
@@ -41,6 +41,9 @@ module.exports = {
|
|
|
41
41
|
})
|
|
42
42
|
messagesSanitized.push(fileMessagesSanitized);
|
|
43
43
|
})
|
|
44
|
+
if (Cache.has("test")) {
|
|
45
|
+
Cache.clear();
|
|
46
|
+
}
|
|
44
47
|
return [].concat(...messagesSanitized);
|
|
45
48
|
},
|
|
46
49
|
supportsAutofix: true,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { splitEntityName } = require("../utils/ruleHelpers");
|
|
2
|
+
|
|
3
|
+
const SEVERITY = "warn";
|
|
4
|
+
const LABELS = ["@restrict", "@requires"];
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: "`@restrict` and `@requires` must not be empty",
|
|
10
|
+
category: "Model Validation",
|
|
11
|
+
recommended: true,
|
|
12
|
+
version: "2.4.1",
|
|
13
|
+
},
|
|
14
|
+
severity: SEVERITY,
|
|
15
|
+
hasSuggestions: true,
|
|
16
|
+
messages: {
|
|
17
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
18
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
entity: check_restrictions,
|
|
24
|
+
service: check_restrictions,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function check_restrictions(e) {
|
|
28
|
+
const reports = [];
|
|
29
|
+
|
|
30
|
+
LABELS.forEach((l) => {
|
|
31
|
+
const invalid = (e[l] && typeof e[l] === "object" && e[l].length === 0) || (typeof e[l] === "string" && !e[l]);
|
|
32
|
+
if (invalid) {
|
|
33
|
+
const entityName = splitEntityName(e).entity;
|
|
34
|
+
const loc = context.cds.getLocation(entityName, e);
|
|
35
|
+
reports.push({
|
|
36
|
+
message: `No explicit restrictions provided on ${e.kind} \`${e.name}\` at \`${l}\`.`,
|
|
37
|
+
loc,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return reports.length > 0 ? reports : undefined;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const SEVERITY = "warn";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events",
|
|
7
|
+
category: "Model Validation",
|
|
8
|
+
recommended: true,
|
|
9
|
+
version: "2.4.1",
|
|
10
|
+
},
|
|
11
|
+
severity: SEVERITY,
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
messages: {
|
|
14
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
15
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create() {
|
|
19
|
+
return {
|
|
20
|
+
service: check_restrict,
|
|
21
|
+
action: check_restrict,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function check_restrict(e) {
|
|
25
|
+
const reports = [];
|
|
26
|
+
|
|
27
|
+
if (e && e["@restrict"] && typeof e["@restrict"] === "object") {
|
|
28
|
+
e["@restrict"].forEach((entry) => {
|
|
29
|
+
const keys = Object.keys(entry);
|
|
30
|
+
if (keys.includes("to") && keys.includes("grant") && entry.grant === "*") {
|
|
31
|
+
reports.push(`Use \`@requires\` instead of \`@restrict.to\` at ${e.kind} \`${e.name}\`.`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return reports.length > 0 ? reports : undefined;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { validateObject, validateString } = require("../utils/ruleHelpers");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SEVERITY = "error";
|
|
4
|
+
const REPLACE_AS_WRITE_EVENTS = ["READ", "CREATE", "UPDATE", "DELETE"];
|
|
5
|
+
const VALID_EVENTS = REPLACE_AS_WRITE_EVENTS.concat(["INSERT", "UPSERT", "WRITE", "*"]);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: '`@restrict.grant` must have valid values',
|
|
11
|
+
category: "Model Validation",
|
|
12
|
+
recommended: true,
|
|
13
|
+
version: "2.4.1",
|
|
14
|
+
},
|
|
15
|
+
severity: DEFAULT_SEVERITY,
|
|
16
|
+
hasSuggestions: true,
|
|
17
|
+
messages: {
|
|
18
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
19
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
entity: check_restrict_grant,
|
|
25
|
+
//service: check_hierarchy_action
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function check_restrict_grant(e) {
|
|
29
|
+
const reports = [];
|
|
30
|
+
|
|
31
|
+
if (e["@restrict"]) {
|
|
32
|
+
const actions = e.actions;
|
|
33
|
+
const actionNames = actions ? Object.keys(actions).map((s) => actions[s].name) : [];
|
|
34
|
+
const validEventsAndActions = VALID_EVENTS.concat(actionNames);
|
|
35
|
+
for (const entry of e["@restrict"]) {
|
|
36
|
+
const grantValues = entry.grant;
|
|
37
|
+
if (Object.keys(entry).includes('grant')) {
|
|
38
|
+
switch (typeof grantValues) {
|
|
39
|
+
case "string": {
|
|
40
|
+
validateString(
|
|
41
|
+
reports,
|
|
42
|
+
context,
|
|
43
|
+
e,
|
|
44
|
+
{ key: "grant", name: "event/action" },
|
|
45
|
+
grantValues,
|
|
46
|
+
validEventsAndActions
|
|
47
|
+
);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "object":
|
|
51
|
+
validateObject(
|
|
52
|
+
reports,
|
|
53
|
+
context,
|
|
54
|
+
e,
|
|
55
|
+
{ key: "grant", name: "events/actions" },
|
|
56
|
+
grantValues,
|
|
57
|
+
validEventsAndActions
|
|
58
|
+
);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return reports.length > 0 ? reports : undefined;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { suggestItems } = require("../utils/ruleHelpers");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SEVERITY = "error";
|
|
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
|
+
version: "2.4.1",
|
|
12
|
+
},
|
|
13
|
+
severity: DEFAULT_SEVERITY,
|
|
14
|
+
hasSuggestions: true,
|
|
15
|
+
messages: {
|
|
16
|
+
InvalidItem: `Misspelled key '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
17
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
entity: check_restrict_keys,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function check_restrict_keys(e) {
|
|
26
|
+
const reports = [];
|
|
27
|
+
|
|
28
|
+
const validRestrictKeys = ["grant", "to", "where"];
|
|
29
|
+
if (e["@restrict"]) {
|
|
30
|
+
for (const entry of e["@restrict"]) {
|
|
31
|
+
suggestItems(reports, context, Object.keys(entry), validRestrictKeys, DEFAULT_SEVERITY);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return reports.length > 0 ? reports : undefined;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { splitEntityName, validateObject, validateString } = require("../utils/ruleHelpers");
|
|
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
|
+
version: "2.4.1",
|
|
12
|
+
},
|
|
13
|
+
severity: "warn",
|
|
14
|
+
hasSuggestions: true,
|
|
15
|
+
messages: {
|
|
16
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
17
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const { model } = context.cds;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
entity: check_restrict_to,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function check_restrict_to(e) {
|
|
28
|
+
const reports = [];
|
|
29
|
+
|
|
30
|
+
const USER_ROLES = [];
|
|
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 { prefix } = splitEntityName(e);
|
|
56
|
+
const prefixSplit = prefix.split(".");
|
|
57
|
+
const serviceName = prefixSplit[prefixSplit.length - 1];
|
|
58
|
+
let services = model.services;
|
|
59
|
+
|
|
60
|
+
// 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
|
+
switch (typeof entry.to) {
|
|
71
|
+
case "string": {
|
|
72
|
+
const isPseudoRole = entry.to && (entry.to === entry.to.toLowerCase());
|
|
73
|
+
validateString(reports, context, e, {key: 'to', name: 'role'}, entry.to, ROLES, isPseudoRole);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "object":
|
|
77
|
+
validateObject(reports, context, e, {key: 'to', name: 'roles'}, entry.to, ROLES);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return reports.length > 0 ? reports : undefined;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { splitEntityName, validateObject, validateString } = require("../utils/ruleHelpers");
|
|
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
|
+
version: "2.4.1",
|
|
12
|
+
},
|
|
13
|
+
severity: "error",
|
|
14
|
+
hasSuggestions: true,
|
|
15
|
+
messages: {
|
|
16
|
+
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
17
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const { model } = context.cds;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
entity: check_restrict_grant,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function check_restrict_grant(e) {
|
|
28
|
+
const reports = [];
|
|
29
|
+
|
|
30
|
+
const USER_ROLES = [];
|
|
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
|
+
for (const entry of e["@restrict"]) {
|
|
56
|
+
const whereValues = entry.where;
|
|
57
|
+
if (whereValues && typeof whereValues === "string") {
|
|
58
|
+
let cxn;
|
|
59
|
+
try {
|
|
60
|
+
cxn = context.cds.parse.expr(entry.where);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
reports.push(`Invalid \`where\` expression, CDS compilation failed.`);
|
|
63
|
+
}
|
|
64
|
+
if (cxn && cxn.xpr) {
|
|
65
|
+
const operator = cxn.xpr[1];
|
|
66
|
+
const role = cxn.xpr[2].ref;
|
|
67
|
+
if (operator === "=") {
|
|
68
|
+
const isValidRole = (role == '$user') || ROLES.includes(role);
|
|
69
|
+
if (!isValidRole) {
|
|
70
|
+
reports.push(`Invalid \`where\` expression, role \`${role}\` not found.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return reports.length > 0 ? reports : undefined;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const { isVSCodeEditor } = require("../utils/helpers");
|
|
2
|
-
|
|
3
1
|
module.exports = {
|
|
4
2
|
meta: {
|
|
5
3
|
docs: {
|
|
@@ -14,36 +12,10 @@ module.exports = {
|
|
|
14
12
|
return { element: _check };
|
|
15
13
|
|
|
16
14
|
function _check(d) {
|
|
17
|
-
|
|
18
15
|
// Do not blame in case of external services
|
|
19
16
|
let srv = d._service || (d.parent && d.parent._service);
|
|
20
17
|
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
|
-
if (d.name.startsWith("$")) {
|
|
43
|
-
// Do blame
|
|
44
|
-
results.push(`“${d.name}” is prefixed with a dollar sign ($)`);
|
|
45
|
-
}
|
|
46
|
-
return results.length > 0 ? results : undefined;
|
|
18
|
+
return d.name.startsWith("$") ? [`“${d.name}” is prefixed with a dollar sign ($)`] : undefined;
|
|
47
19
|
}
|
|
48
20
|
},
|
|
49
21
|
};
|
|
@@ -29,9 +29,9 @@ module.exports = {
|
|
|
29
29
|
if (e.$location && e.$location.file) {
|
|
30
30
|
const file = e.$location.file;
|
|
31
31
|
const loc = cds.getLocation(elementName, e);
|
|
32
|
-
const fix = (fixer) => {
|
|
32
|
+
const fix = (fixer, source = sourcecode) => {
|
|
33
33
|
const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
|
|
34
|
-
const rangeEnd =
|
|
34
|
+
const rangeEnd = source.getIndexFromLoc({
|
|
35
35
|
line: loc.end.line,
|
|
36
36
|
column: loc.end.column,
|
|
37
37
|
});
|
|
@@ -27,9 +27,9 @@ module.exports = {
|
|
|
27
27
|
if (e.$location && e.$location.file) {
|
|
28
28
|
const file = e.$location.file;
|
|
29
29
|
const loc = cds.getLocation(entityName, e);
|
|
30
|
-
const fix = (fixer) => {
|
|
30
|
+
const fix = (fixer, source = sourcecode) => {
|
|
31
31
|
const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
|
|
32
|
-
const rangeEnd =
|
|
32
|
+
const rangeEnd = source.getIndexFromLoc({
|
|
33
33
|
line: loc.end.line,
|
|
34
34
|
column: loc.end.column,
|
|
35
35
|
});
|
package/lib/utils/fuzzySearch.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
const cache = {};
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
module.exports = (input, list, log) => {
|
|
14
|
+
module.exports = (input, list, log, keepCase=false) => {
|
|
15
15
|
let minDistWords = [];
|
|
16
16
|
|
|
17
17
|
if (input.length > 50 || list.length > 50) {
|
|
@@ -25,8 +25,15 @@ module.exports = (input, list, log) => {
|
|
|
25
25
|
let runtime = 0;
|
|
26
26
|
|
|
27
27
|
for (const word of list) {
|
|
28
|
+
|
|
29
|
+
|
|
28
30
|
const start = log && Date.now();
|
|
29
|
-
|
|
31
|
+
let levDist;
|
|
32
|
+
if (word === word.toUpperCase() && !keepCase) {
|
|
33
|
+
levDist = levDistance(input.toUpperCase(), word);
|
|
34
|
+
} else {
|
|
35
|
+
levDist = levDistance(input, word);
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
if (log) {
|
|
32
39
|
const duration = Date.now() - start;
|
package/lib/utils/helpers.js
CHANGED
|
@@ -44,4 +44,51 @@ module.exports = {
|
|
|
44
44
|
return FILES;
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Attempts to extract JSON from a text by looking for the longst substring
|
|
49
|
+
* enclosed by braces or brackets. Input string can therefore either contain
|
|
50
|
+
* a JSON object enclosed by braces, or a JSON array enclosed by brackets.
|
|
51
|
+
* No semantic parsing is done whatsoever (aside from the final JSON.parse)
|
|
52
|
+
* so if the input string is not sane, you will only notice from the
|
|
53
|
+
* final parse step failing.
|
|
54
|
+
* @param text text from which to extract JSON.
|
|
55
|
+
* @param index the start index of the first opening "{" or "[" within text.
|
|
56
|
+
* @returns a parsed JSON object or an empty object if not called with proper parameters.
|
|
57
|
+
* @throws Errors when JSON contained within string is not valid.
|
|
58
|
+
*/
|
|
59
|
+
extractJSON: function(text, index) {
|
|
60
|
+
const [opening, closing] = {
|
|
61
|
+
"{": ["{", "}"],
|
|
62
|
+
"[": ["[", "]"],
|
|
63
|
+
}[text[index]] || [undefined, undefined];
|
|
64
|
+
|
|
65
|
+
// neither "[" nor "{" at beginning -> fail fast
|
|
66
|
+
if (opening === undefined) return {};
|
|
67
|
+
|
|
68
|
+
// we expect caller to call with an index of the first opening brace.
|
|
69
|
+
// So add that brace and increment index at start.
|
|
70
|
+
index++;
|
|
71
|
+
let result = opening;
|
|
72
|
+
let open = 1;
|
|
73
|
+
while (open > 0 && index < text.length) {
|
|
74
|
+
const char = text[index];
|
|
75
|
+
if (char === closing) {
|
|
76
|
+
open--;
|
|
77
|
+
} else if (char === opening) {
|
|
78
|
+
open++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result += char;
|
|
82
|
+
index++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (open !== 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"text does not contain proper JSON (unmatched opening or closing brace)"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return JSON.parse(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
47
94
|
};
|
package/lib/utils/model.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @typedef { import("eslint").AST.SourceLocation } SourceLocation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
const fs = require("fs");
|
|
6
7
|
const path = require("path");
|
|
7
8
|
const cds = require("@sap/cds");
|
|
@@ -353,8 +354,6 @@ module.exports = {
|
|
|
353
354
|
if (isFileInModel) {
|
|
354
355
|
// Only update on detected changes
|
|
355
356
|
if (dictFiles[filePath] !== code) {
|
|
356
|
-
dictFiles[filePath] = code;
|
|
357
|
-
module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
|
|
358
357
|
result = true
|
|
359
358
|
}
|
|
360
359
|
} else if (dictFiles[filePath] !== code) {
|