@sap/eslint-plugin-cds 2.4.1 → 2.6.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/README.md +2 -1
  3. package/lib/api/index.js +13 -12
  4. package/lib/conf/all.js +22 -0
  5. package/lib/conf/index.js +22 -0
  6. package/lib/conf/recommended.js +19 -0
  7. package/lib/constants.js +19 -16
  8. package/lib/index.js +11 -33
  9. package/lib/parser.js +169 -10
  10. package/lib/rules/assoc2many-ambiguous-key.js +80 -96
  11. package/lib/rules/auth-no-empty-restrictions.js +23 -30
  12. package/lib/rules/auth-use-requires.js +25 -24
  13. package/lib/rules/auth-valid-restrict-grant.js +87 -49
  14. package/lib/rules/auth-valid-restrict-keys.js +30 -23
  15. package/lib/rules/auth-valid-restrict-to.js +97 -52
  16. package/lib/rules/auth-valid-restrict-where.js +52 -42
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +27 -0
  19. package/lib/rules/latest-cds-version.js +23 -21
  20. package/lib/rules/min-node-version.js +25 -24
  21. package/lib/rules/no-db-keywords.js +23 -31
  22. package/lib/rules/no-dollar-prefixed-names.js +17 -14
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +11 -16
  25. package/lib/rules/sql-cast-suggestion.js +16 -29
  26. package/lib/rules/start-elements-lowercase.js +42 -44
  27. package/lib/rules/start-entities-uppercase.js +29 -31
  28. package/lib/rules/valid-csv-header.js +65 -64
  29. package/lib/{api/lint.d.ts → types.d.ts} +5 -7
  30. package/lib/utils/Cache.js +33 -0
  31. package/lib/utils/Colors.js +9 -0
  32. package/lib/utils/createRule.js +317 -0
  33. package/lib/utils/findFuzzy.js +87 -0
  34. package/lib/utils/genDocs.js +345 -0
  35. package/lib/utils/getConfigPath.js +33 -0
  36. package/lib/utils/getConfiguredFileTypes.js +10 -0
  37. package/lib/utils/getFileExtensions.js +8 -0
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +20 -0
  40. package/lib/utils/rules.js +112 -1041
  41. package/lib/utils/runRuleTester.js +116 -0
  42. package/package.json +10 -4
  43. package/lib/processor.js +0 -50
  44. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
  45. package/lib/utils/fuzzySearch.js +0 -94
  46. package/lib/utils/helpers.js +0 -94
  47. package/lib/utils/jsonc.js +0 -1
  48. package/lib/utils/model.js +0 -393
  49. package/lib/utils/ruleHelpers.js +0 -199
  50. package/lib/utils/ruleTester.js +0 -78
  51. package/lib/utils/validate.js +0 -36
@@ -1,170 +1,154 @@
1
+ const cds = require('@sap/cds')
2
+
3
+ /** @type {import('../types').Rule} */
4
+
1
5
  module.exports = {
2
6
  meta: {
7
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
8
  docs: {
4
9
  description:
5
- "Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.",
6
- category: "Model Validation",
7
- recommended: true,
8
- version: "1.0.1",
10
+ 'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
11
+ category: 'Model Validation',
12
+ recommended: true
9
13
  },
10
- severity: "warn",
11
- type: "problem",
14
+ type: 'problem',
15
+ model: 'inferred'
12
16
  },
13
- create(context) {
14
- return { all: check_assoc2many_ambiguous_key };
17
+ create (context) {
18
+ return checkAssocs
15
19
 
16
- function check_assoc2many_ambiguous_key() {
17
- let reports = [];
18
- let csnOdata;
19
- const m = context.cds.model;
20
+ function checkAssocs () {
21
+ let csnOdata
22
+ const m = context.getModel()
23
+ if (!m) return
20
24
  if (m && m.definitions) {
21
- csnOdata = context.cds.compile.for.odata(m);
22
- const csnOdataLinked = context.cds.linked(csnOdata);
23
- reports = associationCardinalityFlaw(csnOdataLinked, context);
24
- }
25
- if (reports.length > 0) {
26
- return reports;
25
+ csnOdata = cds.compile.for.odata(m)
26
+ const csnOdataLinked = cds.linked(csnOdata)
27
+ associationCardinalityFlaw(csnOdataLinked, context)
27
28
  }
28
29
  }
29
- },
30
- };
30
+ }
31
+ }
31
32
 
32
- function associationCardinalityFlaw(csn, context) {
33
- const reports = [];
33
+ function associationCardinalityFlaw (csn, context) {
34
34
  processEntity(csn, (definition, sourceEntity, sourceAlias) => {
35
- let refCardinalityMult = false;
36
- let refPlainElement = false;
35
+ let refCardinalityMult = false
36
+ let refPlainElement = false
37
37
  processElement(
38
38
  csn,
39
39
  definition,
40
40
  sourceEntity,
41
41
  sourceAlias,
42
42
  () => {
43
- refCardinalityMult = false;
44
- refPlainElement = false;
43
+ refCardinalityMult = false
44
+ refPlainElement = false
45
45
  },
46
46
  (refEntity, refElement) => {
47
- if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
48
- if (refElement.cardinality && refElement.cardinality.max === "*") {
49
- refCardinalityMult = true;
47
+ if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
48
+ if (refElement.cardinality && refElement.cardinality.max === '*') {
49
+ refCardinalityMult = true
50
50
  }
51
51
  } else {
52
- refPlainElement = true;
52
+ refPlainElement = true
53
53
  }
54
54
  },
55
55
  (column) => {
56
56
  if (
57
57
  definition.keys &&
58
58
  Object.keys(definition.keys).length === 1 &&
59
- Object.keys(definition.keys)[0] === "ID" &&
59
+ Object.keys(definition.keys)[0] === 'ID' &&
60
60
  refCardinalityMult &&
61
61
  refPlainElement
62
62
  ) {
63
- const keyName = Object.keys(definition.keys)[0];
64
- const key = definition.keys[keyName];
65
- const keyLoc = context.cds.getLocation(keyName, key, csn);
66
- const colName = column.as ? column.as : column.name;
67
- if (context.sourcecode.lines[column.$location.line - 1]) {
68
- const endCol = context.sourcecode.lines[column.$location.col - 1].length;
69
- const colLoc = {
70
- start: { line: column.$location.line, column: column.$location.col - 1 },
71
- end: { line: column.$location.line, column: endCol },
72
- };
73
- const message = `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`;
74
- reports.push(
75
- {
76
- message,
77
- loc: keyLoc,
78
- file: key.$location.file,
79
- },
80
- {
81
- message,
82
- loc: colLoc,
83
- file: column.$location.file,
84
- }
85
- );
86
- }
63
+ const keyName = Object.keys(definition.keys)[0]
64
+ const key = definition.keys[keyName]
65
+ const keyLoc = context.getLocation(keyName, key, csn)
66
+ const colName = column.as ? column.as : column.name
67
+ context.report({
68
+ message: `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`,
69
+ loc: keyLoc,
70
+ file: key.$location.file
71
+ })
87
72
  }
88
73
  }
89
- );
90
- });
91
- return reports;
74
+ )
75
+ })
92
76
  }
93
77
 
94
- function processEntity(csn, eachCallback) {
78
+ function processEntity (csn, eachCallback) {
95
79
  Object.keys(csn.definitions).forEach((name) => {
96
- if (name.startsWith("localized.")) {
97
- return;
80
+ if (name.startsWith('localized.')) {
81
+ return
98
82
  }
99
- const definition = csn.definitions[name];
83
+ const definition = csn.definitions[name]
100
84
  if (
101
- definition.kind === "entity" &&
85
+ definition.kind === 'entity' &&
102
86
  definition.query &&
103
87
  definition.query.SELECT &&
104
88
  definition.query.SELECT.columns
105
89
  ) {
106
- let sourceEntity;
107
- const sourceAlias = [];
90
+ let sourceEntity
91
+ const sourceAlias = []
108
92
  if (definition.query.SELECT.from.ref) {
109
93
  // From
110
- sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join("_")];
94
+ sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join('_')]
111
95
  sourceAlias.push({
112
96
  from: sourceEntity.name,
113
- as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
114
- });
97
+ as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split('.').pop()
98
+ })
115
99
  } else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
116
100
  // Join
117
- sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
101
+ sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
118
102
  definition.query.SELECT.from.args.forEach((arg) => {
119
103
  sourceAlias.push({
120
- from: arg.ref.join("_"),
121
- as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
122
- });
123
- });
104
+ from: arg.ref.join('_'),
105
+ as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
106
+ })
107
+ })
124
108
  }
125
109
  if (!sourceEntity) {
126
- return;
110
+ return
127
111
  }
128
- eachCallback(definition, sourceEntity, sourceAlias);
112
+ eachCallback(definition, sourceEntity, sourceAlias)
129
113
  }
130
- });
114
+ })
131
115
  }
132
116
 
133
- function processElement(csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
117
+ function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
134
118
  definition.query.SELECT.columns.forEach((column) => {
135
119
  if (column.ref && column.ref.length > 1) {
136
- let refEntity = sourceEntity;
137
- let refAlias = sourceAlias;
138
- beforeCallback();
120
+ let refEntity = sourceEntity
121
+ let refAlias = sourceAlias
122
+ beforeCallback()
139
123
  column.ref.forEach((ref) => {
140
- ref = ref.id || ref;
124
+ ref = ref.id || ref
141
125
  // Alias
142
126
  const matchAlias = refAlias.find((alias) => {
143
- return alias.as === ref;
144
- });
145
- let refElement;
127
+ return alias.as === ref
128
+ })
129
+ let refElement
146
130
  if (matchAlias) {
147
- refEntity = csn.definitions[matchAlias.from];
131
+ refEntity = csn.definitions[matchAlias.from]
148
132
  } else {
149
- refElement = refEntity.elements[ref];
133
+ refElement = refEntity.elements[ref]
150
134
  // Mixin
151
135
  if (!refElement) {
152
- refElement = definition.elements[ref];
136
+ refElement = definition.elements[ref]
153
137
  if (!refElement && definition.query.SELECT.mixin) {
154
- refElement = definition.query.SELECT.mixin[ref];
138
+ refElement = definition.query.SELECT.mixin[ref]
155
139
  if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
156
- refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref];
140
+ refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref]
157
141
  }
158
142
  }
159
143
  }
160
- eachCallback(refEntity, refElement);
161
- if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
162
- refEntity = csn.definitions[refElement.target];
144
+ eachCallback(refEntity, refElement)
145
+ if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
146
+ refEntity = csn.definitions[refElement.target]
163
147
  }
164
148
  }
165
- refAlias = [];
166
- });
167
- afterCallback(column);
149
+ refAlias = []
150
+ })
151
+ afterCallback(column)
168
152
  }
169
- });
153
+ })
170
154
  }
@@ -1,45 +1,38 @@
1
- const { splitEntityName } = require("../utils/ruleHelpers");
2
-
3
- const SEVERITY = "warn";
4
- const LABELS = ["@restrict", "@requires"];
1
+ const LABELS = ['@restrict', '@requires']
5
2
 
6
3
  module.exports = {
7
4
  meta: {
5
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
8
6
  docs: {
9
- description: "`@restrict` and `@requires` must not be empty",
10
- category: "Model Validation",
11
- recommended: true,
12
- version: "2.4.1",
7
+ description: '`@restrict` and `@requires` must not be empty.',
8
+ category: 'Model Validation',
9
+ recommended: true
13
10
  },
14
- severity: SEVERITY,
15
11
  hasSuggestions: true,
16
12
  messages: {
17
- InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
18
- ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
13
+ InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
14
+ ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
19
15
  },
16
+ type: 'problem',
17
+ model: 'inferred'
20
18
  },
21
- create(context) {
19
+ create (context) {
22
20
  return {
23
- entity: check_restrictions,
24
- service: check_restrictions,
25
- };
26
-
27
- function check_restrictions(e) {
28
- const reports = [];
21
+ entity: checkRestrictions,
22
+ service: checkRestrictions
23
+ }
29
24
 
30
- LABELS.forEach((l) => {
31
- const invalid = (e[l] && typeof e[l] === "object" && e[l].length === 0) || (typeof e[l] === "string" && !e[l]);
25
+ function checkRestrictions (e) {
26
+ for (const l of LABELS) {
27
+ const invalid = (typeof e[l] === 'object' && e[l].length === 0) || (typeof e[l] === 'string' && e[l] === '')
32
28
  if (invalid) {
33
- const entityName = splitEntityName(e).entity;
34
- const loc = context.cds.getLocation(entityName, e);
35
- reports.push({
29
+ context.report({
36
30
  message: `No explicit restrictions provided on ${e.kind} \`${e.name}\` at \`${l}\`.`,
37
- loc,
38
- });
31
+ node: context.getNode(e),
32
+ file: e.$location.file
33
+ })
39
34
  }
40
- });
41
-
42
- return reports.length > 0 ? reports : undefined;
35
+ }
43
36
  }
44
- },
45
- };
37
+ }
38
+ }
@@ -1,38 +1,39 @@
1
- const SEVERITY = "warn";
2
-
3
1
  module.exports = {
4
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
5
4
  docs: {
6
- description: "Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events",
7
- category: "Model Validation",
5
+ description: 'Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events.',
6
+ category: 'Model Validation',
8
7
  recommended: true,
9
- version: "2.4.1",
8
+ version: '2.4.1'
10
9
  },
11
- severity: SEVERITY,
12
10
  hasSuggestions: true,
13
11
  messages: {
14
- InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
15
- ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
12
+ InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
13
+ ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
16
14
  },
15
+ type: 'problem',
16
+ model: 'inferred'
17
17
  },
18
- create() {
18
+ create (context) {
19
19
  return {
20
- service: check_restrict,
21
- action: check_restrict,
22
- };
23
-
24
- function check_restrict(e) {
25
- const reports = [];
20
+ service: checkRestrict,
21
+ action: checkRestrict
22
+ }
26
23
 
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}\`.`);
24
+ function checkRestrict (e) {
25
+ if (e && e['@restrict'] && typeof e['@restrict'] === 'object') {
26
+ for (const entry of e['@restrict']) {
27
+ const keys = Object.keys(entry)
28
+ if (keys.includes('to') && keys.includes('grant') && entry.grant === '*') {
29
+ context.report({
30
+ message: `Use \`@requires\` instead of \`@restrict.to\` at ${e.kind} \`${e.name}\`.`,
31
+ node: context.getNode(e),
32
+ file: e.$location.file
33
+ })
32
34
  }
33
- });
35
+ }
34
36
  }
35
- return reports.length > 0 ? reports : undefined;
36
37
  }
37
- },
38
- };
38
+ }
39
+ }
@@ -1,68 +1,106 @@
1
- const { validateObject, validateString } = require("../utils/ruleHelpers");
1
+ const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require('../utils/rules')
2
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", "*"]);
3
+ const REPLACE_AS_WRITE_EVENTS = ['READ', 'CREATE', 'UPDATE', 'DELETE']
4
+ const VALID_EVENTS = REPLACE_AS_WRITE_EVENTS.concat(['INSERT', 'UPSERT', 'WRITE', '*'])
6
5
 
7
6
  module.exports = {
8
7
  meta: {
8
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
9
9
  docs: {
10
- description: '`@restrict.grant` must have valid values',
11
- category: "Model Validation",
12
- recommended: true,
13
- version: "2.4.1",
10
+ description: '`@restrict.grant` must have valid values.',
11
+ category: 'Model Validation',
12
+ recommended: true
14
13
  },
15
- severity: DEFAULT_SEVERITY,
16
- hasSuggestions: true,
17
14
  messages: {
18
- InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
19
- ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
15
+ InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
16
+ ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
20
17
  },
18
+ type: 'problem',
19
+ model: 'inferred'
21
20
  },
22
- create(context) {
21
+ create (context) {
23
22
  return {
24
- entity: check_restrict_grant,
25
- //service: check_hierarchy_action
26
- };
23
+ entity: checkRestrictGrant
24
+ }
27
25
 
28
- function check_restrict_grant(e) {
29
- const reports = [];
26
+ function checkRestrictGrant (e) {
27
+ const node = context.getNode(e)
28
+ const file = e.$location.file
29
+ if (e['@restrict']) {
30
+ const actions = e.actions
31
+ const actionNames = actions ? Object.keys(actions).map((s) => actions[s].name) : []
32
+ const validEventsAndActions = VALID_EVENTS.concat(actionNames)
30
33
 
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;
34
+ for (const entry of e['@restrict']) {
37
35
  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;
36
+ const grantValue = entry.grant
37
+ switch (typeof grantValue) {
38
+ case 'string': {
39
+ if (isEmptyString(grantValue)) {
40
+ context.report({
41
+ message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
42
+ node,
43
+ file
44
+ })
45
+ } else {
46
+ if (!isStringInArray(grantValue, validEventsAndActions, true)) {
47
+ const candidates = findFuzzy(grantValue, validEventsAndActions.sort())
48
+ context.report({
49
+ messageId: 'InvalidItem',
50
+ data: { invalid: grantValue, candidates },
51
+ node,
52
+ file
53
+ })
54
+ }
55
+ }
56
+ break
49
57
  }
50
- case "object":
51
- validateObject(
52
- reports,
53
- context,
54
- e,
55
- { key: "grant", name: "events/actions" },
56
- grantValues,
57
- validEventsAndActions
58
- );
59
- break;
58
+
59
+ case 'object':
60
+ if (isEmptyObject(grantValue)) {
61
+ context.report({
62
+ message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
63
+ node,
64
+ file
65
+ })
66
+ } else {
67
+ const valuesForWrite = grantValue.filter(function (item) {
68
+ return item !== 'READ' && item !== 'WRITE' && item !== '*'
69
+ })
70
+ for (const value of grantValue) {
71
+ if (!validEventsAndActions.includes(value)) {
72
+ const candidates = findFuzzy(value, validEventsAndActions.sort())
73
+ context.report({
74
+ messageId: 'InvalidItem',
75
+ data: { invalid: value, candidates },
76
+ node,
77
+ file
78
+ })
79
+ }
80
+ }
81
+ const allValuesIncluded = grantValue.every((v) => valuesForWrite.includes(v))
82
+ if (allValuesIncluded) {
83
+ context.report({
84
+ messageId: 'InvalidItem',
85
+ data: { invalid: [`[${grantValue}]`], candidates: ['["WRITE"]'] },
86
+ node,
87
+ file
88
+ })
89
+ }
90
+ if (grantValue.includes('*')) {
91
+ context.report({
92
+ messageId: 'InvalidItem',
93
+ data: { invalid: `[${grantValue}]`, candidates: ['["*"]'] },
94
+ node,
95
+ file
96
+ })
97
+ }
98
+ }
99
+ break
60
100
  }
61
101
  }
62
102
  }
63
103
  }
64
-
65
- return reports.length > 0 ? reports : undefined;
66
104
  }
67
- },
68
- };
105
+ }
106
+ }
@@ -1,37 +1,44 @@
1
- const { suggestItems } = require("../utils/ruleHelpers");
1
+ const { isEmptyObject, findFuzzy } = require('../utils/rules')
2
2
 
3
- const DEFAULT_SEVERITY = "error";
3
+ const VALID_RESTRICT_KEYS = ['grant', 'to', 'where']
4
4
 
5
5
  module.exports = {
6
6
  meta: {
7
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
7
8
  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",
9
+ description: '`@restrict` must have properly spelled `to`, `grant`, and `where` keys.',
10
+ category: 'Model Validation',
11
+ recommended: true
12
12
  },
13
- severity: DEFAULT_SEVERITY,
14
- hasSuggestions: true,
15
13
  messages: {
16
- InvalidItem: `Misspelled key '{{invalid}}'. Did you mean '{{candidates}}'?`,
17
- ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`,
14
+ InvalidItem: "Misspelled key '{{invalid}}'. Did you mean '{{candidates}}'?"
18
15
  },
16
+ type: 'problem',
17
+ model: 'inferred'
19
18
  },
20
- create(context) {
19
+ create (context) {
21
20
  return {
22
- entity: check_restrict_keys,
23
- };
24
-
25
- function check_restrict_keys(e) {
26
- const reports = [];
21
+ entity: checkRestrictKeys
22
+ }
27
23
 
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);
24
+ function checkRestrictKeys (e) {
25
+ if (e['@restrict']) {
26
+ for (const entry of e['@restrict']) {
27
+ if (typeof entry === 'object' && !isEmptyObject(entry)) {
28
+ for (const key of Object.keys(entry)) {
29
+ if (!VALID_RESTRICT_KEYS.includes(key)) {
30
+ const candidates = findFuzzy(key, VALID_RESTRICT_KEYS.sort())
31
+ context.report({
32
+ messageId: 'InvalidItem',
33
+ data: { invalid: key, candidates },
34
+ node: context.getNode(e),
35
+ file: e.$location.file
36
+ })
37
+ }
38
+ }
39
+ }
32
40
  }
33
41
  }
34
- return reports.length > 0 ? reports : undefined;
35
42
  }
36
- },
37
- };
43
+ }
44
+ }