@sap/eslint-plugin-cds 2.3.3 → 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 CHANGED
@@ -6,6 +6,39 @@ 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
28
+ ## [2.3.5] - 2022-04-05
29
+
30
+ ### Changed
31
+
32
+ - Catch root model compilation errors and do not try again on every file (-> long lint times for broken models)
33
+ - Add to lint reports with rules marked with '!'
34
+
35
+ ## [2.3.4] - 2022-03-31
36
+
37
+ ### Changed
38
+
39
+ - Only deduplicate model error messages when working within VS Code Editor
40
+ - Hide `no-dollar-prefixed-names` compiler warning message in VS Code Editor (already passed by lsp)
41
+
9
42
  ## [2.3.3] - 2022-03-24
10
43
 
11
44
  ### Added
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
- // TODO: TEST API
2
- //module.exports = require("../../api").createRule({ // TODO: eliminate that and allow std eslint style module.export = { ...
3
1
  module.exports = {
4
2
  meta: {
5
3
  docs: {
@@ -12,36 +12,10 @@ module.exports = {
12
12
  return { element: _check };
13
13
 
14
14
  function _check(d) {
15
-
16
15
  // Do not blame in case of external services
17
16
  let srv = d._service || (d.parent && d.parent._service);
18
17
  if (srv && srv["@cds.external"]) return;
19
-
20
- const results = [];
21
- for (const m in context.cds.model.messages) {
22
- const msg = context.cds.model.messages[m];
23
- if (msg.messageId === "syntax-dollar-ident") {
24
- results.push({
25
- message: msg.message,
26
- loc: {
27
- start: { line: msg.location.line, column: msg.location.col - 1 },
28
- end: {
29
- line: msg.location.endLine,
30
- column: !msg.location.endCol
31
- ? context.sourcecode.lines[msg.location.line - 1].length
32
- : msg.location.endCol - 1,
33
- },
34
- },
35
- file: msg.location.file,
36
- });
37
- }
38
- }
39
-
40
- if (d.name.startsWith("$")) {
41
- // Do blame
42
- results.push(`“${d.name}” is prefixed with a dollar sign ($)`);
43
- }
44
- return results.length > 0 ? results : undefined;
18
+ return d.name.startsWith("$") ? [`“${d.name}” is prefixed with a dollar sign ($)`] : undefined;
45
19
  }
46
20
  },
47
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 = sourcecode.getIndexFromLoc({
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 = sourcecode.getIndexFromLoc({
32
+ const rangeEnd = source.getIndexFromLoc({
33
33
  line: loc.end.line,
34
34
  column: loc.end.column,
35
35
  });
@@ -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
- const levDist = levDistance(input, word);
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;
@@ -7,15 +7,7 @@ module.exports = {
7
7
  * @returns boolean
8
8
  */
9
9
  isValidFile: function (filePath, fileType) {
10
- function genRegex(key) {
11
- return new RegExp(
12
- `${key
13
- .map((file) => {
14
- return file.replace("*", "");
15
- })
16
- .join("$|")}$`
17
- );
18
- }
10
+ const genRegex = (key) => new RegExp(`${key.map((file) => file.replace("*", "")).join("$|")}$`);
19
11
  let isValid = false;
20
12
  switch(fileType) {
21
13
  case 'MODEL_FILES':
@@ -52,4 +44,51 @@ module.exports = {
52
44
  return FILES;
53
45
  },
54
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
+
55
94
  };