@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.
- package/CHANGELOG.md +31 -2
- package/README.md +2 -1
- package/lib/api/index.js +13 -12
- package/lib/conf/all.js +22 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +19 -0
- package/lib/constants.js +19 -16
- package/lib/index.js +11 -33
- package/lib/parser.js +169 -10
- package/lib/rules/assoc2many-ambiguous-key.js +80 -96
- package/lib/rules/auth-no-empty-restrictions.js +23 -30
- package/lib/rules/auth-use-requires.js +25 -24
- package/lib/rules/auth-valid-restrict-grant.js +87 -49
- package/lib/rules/auth-valid-restrict-keys.js +30 -23
- package/lib/rules/auth-valid-restrict-to.js +97 -52
- package/lib/rules/auth-valid-restrict-where.js +52 -42
- package/lib/rules/extension-restrictions.js +69 -0
- package/lib/rules/index.js +27 -0
- package/lib/rules/latest-cds-version.js +23 -21
- package/lib/rules/min-node-version.js +25 -24
- package/lib/rules/no-db-keywords.js +23 -31
- package/lib/rules/no-dollar-prefixed-names.js +17 -14
- package/lib/rules/no-join-on-draft.js +27 -0
- package/lib/rules/require-2many-oncond.js +11 -16
- package/lib/rules/sql-cast-suggestion.js +16 -29
- package/lib/rules/start-elements-lowercase.js +42 -44
- package/lib/rules/start-entities-uppercase.js +29 -31
- package/lib/rules/valid-csv-header.js +65 -64
- package/lib/{api/lint.d.ts → types.d.ts} +5 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +317 -0
- package/lib/utils/findFuzzy.js +87 -0
- package/lib/utils/genDocs.js +345 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/getProjectRootPath.js +25 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/rules.js +112 -1041
- package/lib/utils/runRuleTester.js +116 -0
- package/package.json +10 -4
- package/lib/processor.js +0 -50
- package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
- package/lib/utils/fuzzySearch.js +0 -94
- package/lib/utils/helpers.js +0 -94
- package/lib/utils/jsonc.js +0 -1
- package/lib/utils/model.js +0 -393
- package/lib/utils/ruleHelpers.js +0 -199
- package/lib/utils/ruleTester.js +0 -78
- 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
|
-
|
|
6
|
-
category:
|
|
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
|
-
|
|
11
|
-
|
|
14
|
+
type: 'problem',
|
|
15
|
+
model: 'inferred'
|
|
12
16
|
},
|
|
13
|
-
create(context) {
|
|
14
|
-
return
|
|
17
|
+
create (context) {
|
|
18
|
+
return checkAssocs
|
|
15
19
|
|
|
16
|
-
function
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
function checkAssocs () {
|
|
21
|
+
let csnOdata
|
|
22
|
+
const m = context.getModel()
|
|
23
|
+
if (!m) return
|
|
20
24
|
if (m && m.definitions) {
|
|
21
|
-
csnOdata =
|
|
22
|
-
const csnOdataLinked =
|
|
23
|
-
|
|
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 ===
|
|
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] ===
|
|
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.
|
|
66
|
-
const colName = column.as ? column.as : column.name
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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 ===
|
|
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
|
|
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:
|
|
10
|
-
category:
|
|
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:
|
|
18
|
-
ReplaceItemWith:
|
|
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:
|
|
24
|
-
service:
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function check_restrictions(e) {
|
|
28
|
-
const reports = [];
|
|
21
|
+
entity: checkRestrictions,
|
|
22
|
+
service: checkRestrictions
|
|
23
|
+
}
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
7
|
-
category:
|
|
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:
|
|
8
|
+
version: '2.4.1'
|
|
10
9
|
},
|
|
11
|
-
severity: SEVERITY,
|
|
12
10
|
hasSuggestions: true,
|
|
13
11
|
messages: {
|
|
14
|
-
InvalidItem:
|
|
15
|
-
ReplaceItemWith:
|
|
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:
|
|
21
|
-
action:
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function check_restrict(e) {
|
|
25
|
-
const reports = [];
|
|
20
|
+
service: checkRestrict,
|
|
21
|
+
action: checkRestrict
|
|
22
|
+
}
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
1
|
+
const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require('../utils/rules')
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
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:
|
|
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:
|
|
19
|
-
ReplaceItemWith:
|
|
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:
|
|
25
|
-
|
|
26
|
-
};
|
|
23
|
+
entity: checkRestrictGrant
|
|
24
|
+
}
|
|
27
25
|
|
|
28
|
-
function
|
|
29
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
context
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
context
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
1
|
+
const { isEmptyObject, findFuzzy } = require('../utils/rules')
|
|
2
2
|
|
|
3
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function check_restrict_keys(e) {
|
|
26
|
-
const reports = [];
|
|
21
|
+
entity: checkRestrictKeys
|
|
22
|
+
}
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
if (e[
|
|
30
|
-
for (const entry of e[
|
|
31
|
-
|
|
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
|
+
}
|