@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 +33 -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 -27
- 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 +48 -9
- package/lib/utils/model.js +29 -70
- package/lib/utils/ruleHelpers.js +145 -2
- package/lib/utils/ruleTester.js +1 -2
- package/lib/utils/rules.js +1038 -1029
- package/package.json +5 -2
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");
|
|
@@ -23,11 +24,7 @@ module.exports = {
|
|
|
23
24
|
return cache.set(key, [value, Date.now()]);
|
|
24
25
|
},
|
|
25
26
|
get(key) {
|
|
26
|
-
|
|
27
|
-
return cache.get(key)[0];
|
|
28
|
-
} else {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
27
|
+
return cache.get(key) ? cache.get(key)[0] : undefined
|
|
31
28
|
},
|
|
32
29
|
dump() {
|
|
33
30
|
const dump = {};
|
|
@@ -41,11 +38,9 @@ module.exports = {
|
|
|
41
38
|
if (cache.has(key)) {
|
|
42
39
|
cache.delete(key);
|
|
43
40
|
}
|
|
44
|
-
return;
|
|
45
41
|
},
|
|
46
42
|
clear() {
|
|
47
43
|
cache.clear();
|
|
48
|
-
return;
|
|
49
44
|
},
|
|
50
45
|
},
|
|
51
46
|
|
|
@@ -56,11 +51,8 @@ module.exports = {
|
|
|
56
51
|
* @returns boolean
|
|
57
52
|
*/
|
|
58
53
|
isNewConfigPath: function (configPath) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
54
|
+
return !module.exports.Cache.has("configpath")
|
|
55
|
+
&& (configPath !== module.exports.Cache.get("configpath"))
|
|
64
56
|
},
|
|
65
57
|
|
|
66
58
|
/**
|
|
@@ -115,25 +107,12 @@ module.exports = {
|
|
|
115
107
|
* @returns ESLint range
|
|
116
108
|
*/
|
|
117
109
|
getRange: function (code, line, column) {
|
|
118
|
-
|
|
119
|
-
if (typeof code === "string") {
|
|
120
|
-
lines = SourceCode.splitLines(code);
|
|
121
|
-
} else {
|
|
122
|
-
lines = code;
|
|
123
|
-
}
|
|
110
|
+
const lines = typeof code === "string" ? SourceCode.splitLines(code) : code
|
|
124
111
|
const ranges = [0];
|
|
125
112
|
lines.forEach((line, i) => {
|
|
126
|
-
|
|
127
|
-
ranges[i + 1] = line.length + 1;
|
|
128
|
-
} else {
|
|
129
|
-
ranges[i + 1] = ranges[i] + line.length + 1;
|
|
130
|
-
}
|
|
113
|
+
ranges[i + 1] = i === 0 ? line.length + 1 : ranges[i] + line.length + 1
|
|
131
114
|
});
|
|
132
|
-
|
|
133
|
-
return ranges[line - 1] + column;
|
|
134
|
-
} else {
|
|
135
|
-
return column;
|
|
136
|
-
}
|
|
115
|
+
return line > 1 ? ranges[line - 1] + column : column
|
|
137
116
|
},
|
|
138
117
|
|
|
139
118
|
/**
|
|
@@ -145,12 +124,7 @@ module.exports = {
|
|
|
145
124
|
* @returns Last line index
|
|
146
125
|
*/
|
|
147
126
|
getLastLine: function (code) {
|
|
148
|
-
|
|
149
|
-
if (typeof code === "string") {
|
|
150
|
-
lines = SourceCode.splitLines(code);
|
|
151
|
-
} else {
|
|
152
|
-
lines = code;
|
|
153
|
-
}
|
|
127
|
+
const lines = typeof code === "string" ? SourceCode.splitLines(code) : code
|
|
154
128
|
return lines.length - 1;
|
|
155
129
|
},
|
|
156
130
|
|
|
@@ -181,10 +155,8 @@ module.exports = {
|
|
|
181
155
|
loc.start.line = nameloc.line;
|
|
182
156
|
loc.end.column = nameloc.col - 1 + name.length;
|
|
183
157
|
loc.end.line = nameloc.line;
|
|
184
|
-
} else {
|
|
185
|
-
if (obj.parent) {
|
|
158
|
+
} else if (obj.parent) {
|
|
186
159
|
this.getLocation(name, obj.parent, model);
|
|
187
|
-
}
|
|
188
160
|
}
|
|
189
161
|
}
|
|
190
162
|
// Empty locations default to line 0, column 0
|
|
@@ -232,17 +204,23 @@ module.exports = {
|
|
|
232
204
|
* @returns reflected model
|
|
233
205
|
*/
|
|
234
206
|
compileModelFromPath: function (configPath) {
|
|
207
|
+
let compiledModel;
|
|
235
208
|
let reflectedModel;
|
|
236
209
|
cds.resolve.cache = {};
|
|
237
210
|
const roots = cds.resolve("*", { root: configPath });
|
|
238
211
|
const messages = [];
|
|
239
212
|
if (roots) {
|
|
240
|
-
|
|
213
|
+
try {
|
|
214
|
+
compiledModel = cds.load(roots, {
|
|
241
215
|
cwd: configPath,
|
|
242
216
|
sync: true,
|
|
243
217
|
locations: true,
|
|
244
218
|
messages,
|
|
245
219
|
});
|
|
220
|
+
module.exports.Cache.remove('errRootModel');
|
|
221
|
+
} catch (err) {
|
|
222
|
+
module.exports.Cache.set('errRootModel', err);
|
|
223
|
+
}
|
|
246
224
|
if (compiledModel) {
|
|
247
225
|
reflectedModel = cds.linked(compiledModel);
|
|
248
226
|
if (messages) {
|
|
@@ -332,13 +310,8 @@ module.exports = {
|
|
|
332
310
|
const configPath = path.dirname(filePath);
|
|
333
311
|
module.exports.Cache.set('configpath', configPath);
|
|
334
312
|
let files = fs.readdirSync(configPath);
|
|
335
|
-
const modelfiles =
|
|
336
|
-
|
|
337
|
-
const filePath = path.join(configPath, file);
|
|
338
|
-
if (isValidFile(filePath, 'MODEL_FILES')) {
|
|
339
|
-
modelfiles.push(filePath);
|
|
340
|
-
}
|
|
341
|
-
});
|
|
313
|
+
const modelfiles = files.map(f => path.join(configPath, f))
|
|
314
|
+
.filter(fp => isValidFile(fp, 'MODEL_FILES'))
|
|
342
315
|
module.exports.Cache.set(`modelfiles:${configPath}`, modelfiles);
|
|
343
316
|
const dictFiles = module.exports.getDictFiles(configPath, modelfiles);
|
|
344
317
|
module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
|
|
@@ -359,11 +332,9 @@ module.exports = {
|
|
|
359
332
|
dictFiles = module.exports.Cache.get(`dictfiles:${input}`);
|
|
360
333
|
} else {
|
|
361
334
|
files.forEach((file) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
dictFiles[file] = fs.readFileSync(file, "utf8");
|
|
366
|
-
}
|
|
335
|
+
dictFiles[file] = module.exports.Cache.has(`file:${file}`)
|
|
336
|
+
? module.exports.Cache.get(`file:${file}`)
|
|
337
|
+
: fs.readFileSync(file, "utf8")
|
|
367
338
|
});
|
|
368
339
|
}
|
|
369
340
|
return dictFiles;
|
|
@@ -375,6 +346,7 @@ module.exports = {
|
|
|
375
346
|
* @returns boolean
|
|
376
347
|
*/
|
|
377
348
|
hasFileChanged: function (code, filePath, configPath) {
|
|
349
|
+
let result = false
|
|
378
350
|
const files = module.exports.Cache.get(`modelfiles:${configPath}`);
|
|
379
351
|
const dictFiles = module.exports.getDictFiles(configPath, files);
|
|
380
352
|
const isFileInModel = module.exports.isFileInModel(filePath, configPath);
|
|
@@ -382,16 +354,12 @@ module.exports = {
|
|
|
382
354
|
if (isFileInModel) {
|
|
383
355
|
// Only update on detected changes
|
|
384
356
|
if (dictFiles[filePath] !== code) {
|
|
385
|
-
|
|
386
|
-
module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
if (dictFiles[filePath] !== code) {
|
|
391
|
-
return true;
|
|
357
|
+
result = true
|
|
392
358
|
}
|
|
359
|
+
} else if (dictFiles[filePath] !== code) {
|
|
360
|
+
result = true
|
|
393
361
|
}
|
|
394
|
-
return
|
|
362
|
+
return result;
|
|
395
363
|
},
|
|
396
364
|
|
|
397
365
|
/**
|
|
@@ -401,14 +369,8 @@ module.exports = {
|
|
|
401
369
|
* @returns boolean
|
|
402
370
|
*/
|
|
403
371
|
isFileInModel(filePath, configPath) {
|
|
404
|
-
let files = module.exports.Cache.get(`modelfiles:${configPath}`);
|
|
405
|
-
|
|
406
|
-
files = [];
|
|
407
|
-
}
|
|
408
|
-
if (files && files.length > 0 && files.includes(filePath)) {
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
return false;
|
|
372
|
+
let files = module.exports.Cache.get(`modelfiles:${configPath}`) || [];
|
|
373
|
+
return files && files.length > 0 && files.includes(filePath)
|
|
412
374
|
},
|
|
413
375
|
|
|
414
376
|
/**
|
|
@@ -419,14 +381,11 @@ module.exports = {
|
|
|
419
381
|
*/
|
|
420
382
|
updateModel: function (code, filePath, configPath) {
|
|
421
383
|
let reflectedModel;
|
|
422
|
-
let files = module.exports.Cache.get(`modelfiles:${configPath}`);
|
|
423
|
-
if (!files) {
|
|
424
|
-
files = [];
|
|
425
|
-
}
|
|
426
384
|
const dictFiles = module.exports.Cache.get(`dictfiles:${configPath}`);
|
|
427
385
|
dictFiles[filePath] = code;
|
|
428
386
|
module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
|
|
429
387
|
reflectedModel = module.exports.compileModelFromDict(dictFiles, { flavor: "inferred" });
|
|
388
|
+
if (reflectedModel) { module.exports.Cache.remove('errRootModel') }
|
|
430
389
|
module.exports.Cache.set(`model:${configPath}`, reflectedModel);
|
|
431
390
|
return reflectedModel;
|
|
432
391
|
}
|
package/lib/utils/ruleHelpers.js
CHANGED
|
@@ -36,7 +36,49 @@ module.exports = {
|
|
|
36
36
|
return { prefix, entity: entityName, suffix };
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {*} reports
|
|
42
|
+
* @param {*} context
|
|
43
|
+
* @param {*} items
|
|
44
|
+
* @param {*} validItems
|
|
45
|
+
* @param {*} severity
|
|
46
|
+
*/
|
|
47
|
+
suggestItems: function (reports, context, items, validItems, severity, keepCase) {
|
|
48
|
+
const { code, sourcecode, filePath } = context;
|
|
49
|
+
let invalidItems = items.filter(
|
|
50
|
+
(e) => !validItems.includes(e) && !validItems.includes(e.toUpperCase() && !validItems.includes(e.toLowerCase()))
|
|
51
|
+
);
|
|
52
|
+
invalidItems.forEach((invalid) => {
|
|
53
|
+
const index = module.exports._findInCode(invalid, code);
|
|
54
|
+
// Safetey check that string exists in source code
|
|
55
|
+
if (index > 0) {
|
|
56
|
+
const loc = sourcecode.getLocFromIndex(index);
|
|
57
|
+
const candidates = findFuzzy(invalid, validItems.sort(), keepCase);
|
|
58
|
+
const suggest = candidates.map((cand) => {
|
|
59
|
+
return {
|
|
60
|
+
messageId: "ReplaceItemWith",
|
|
61
|
+
data: { invalid, candidates: cand },
|
|
62
|
+
fix: (fixer) => fixer.replaceTextRange([index, index + invalid.length], cand),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
reports.push({
|
|
66
|
+
messageId: "InvalidItem",
|
|
67
|
+
data: { invalid, candidates },
|
|
68
|
+
loc: {
|
|
69
|
+
start: loc,
|
|
70
|
+
end: { line: loc.line, column: loc.column + invalid.length },
|
|
71
|
+
},
|
|
72
|
+
file: filePath,
|
|
73
|
+
suggest,
|
|
74
|
+
severity,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (!invalidItems) {
|
|
79
|
+
reports.push(`Missing values!`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
40
82
|
|
|
41
83
|
_findInCode: function (miss, code) {
|
|
42
84
|
// middle
|
|
@@ -52,5 +94,106 @@ module.exports = {
|
|
|
52
94
|
return code.indexOf(miss);
|
|
53
95
|
},
|
|
54
96
|
|
|
97
|
+
validateString: function (reports, context, e, text, value, allowedValues, fuzzySearch = true, keepCase = true) {
|
|
98
|
+
const { key, name } = text;
|
|
99
|
+
if (typeof value !== "string") {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!value) {
|
|
103
|
+
reports.push(`Missing ${name} on ${e.name} for @restrict \`${key}\`.`);
|
|
104
|
+
} else if (fuzzySearch) {
|
|
105
|
+
module.exports.suggestItems(reports, context, [value], allowedValues, keepCase);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
validateObject: function (reports, context, e, text, values, allowedValues, fuzzySearch = true) {
|
|
110
|
+
const { key, name } = text;
|
|
111
|
+
if (typeof values !== "object") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (values.length === 0) {
|
|
115
|
+
reports.push(`Missing ${name} on ${e.name} for @restrict \`${key}\`.`);
|
|
116
|
+
} else if (fuzzySearch) {
|
|
117
|
+
switch (key) {
|
|
118
|
+
case "to": {
|
|
119
|
+
if (values.includes("any")) {
|
|
120
|
+
module.exports.suggestRedundantItems(reports, context, "any", e);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "grant": {
|
|
125
|
+
let valuesForWrite = allowedValues.filter(function (item) {
|
|
126
|
+
return item !== "READ" && item !== "WRITE" && item !== "*";
|
|
127
|
+
});
|
|
128
|
+
let allValuesIncluded = (arr, target) => target.every((v) => arr.includes(v));
|
|
129
|
+
if (allValuesIncluded(values, valuesForWrite)) {
|
|
130
|
+
module.exports.suggestRedundantItems(reports, context, "WRITE", e);
|
|
131
|
+
}
|
|
132
|
+
if (values.includes("*")) {
|
|
133
|
+
module.exports.suggestRedundantItems(reports, context, "*", e);
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
module.exports.suggestItems(reports, context, values, allowedValues);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
suggestRedundantItems: function (reports, context, value, e) {
|
|
143
|
+
let invalid;
|
|
144
|
+
const loc = e.$location;
|
|
145
|
+
const lineToReplace = context.sourcecode.lines[loc.line];
|
|
146
|
+
var regExp = /\[([^)]+)\]/;
|
|
147
|
+
var matches = regExp.exec(lineToReplace);
|
|
148
|
+
if (matches && matches[0]) {
|
|
149
|
+
invalid = matches[0];
|
|
150
|
+
}
|
|
151
|
+
const startIndex = lineToReplace.indexOf(invalid);
|
|
152
|
+
const candidates = `['${value}']`;
|
|
153
|
+
const suggest = {
|
|
154
|
+
messageId: "ReplaceItemWith",
|
|
155
|
+
data: { invalid, candidates },
|
|
156
|
+
fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates),
|
|
157
|
+
};
|
|
158
|
+
reports.push({
|
|
159
|
+
messageId: "InvalidItem",
|
|
160
|
+
data: { invalid, candidates },
|
|
161
|
+
loc: {
|
|
162
|
+
start: { line: loc.line + 1, column: startIndex },
|
|
163
|
+
end: { line: loc.line + 1, column: startIndex + invalid.length },
|
|
164
|
+
},
|
|
165
|
+
file: context.filePath,
|
|
166
|
+
suggest,
|
|
167
|
+
severity: "warn",
|
|
168
|
+
});
|
|
169
|
+
},
|
|
55
170
|
|
|
56
|
-
|
|
171
|
+
suggestReplacementsItems: function (reports, context, value, e) {
|
|
172
|
+
let invalid;
|
|
173
|
+
const loc = e.$location;
|
|
174
|
+
const lineToReplace = context.sourcecode.lines[loc.line];
|
|
175
|
+
var regExp = /\[([^)]+)\]/;
|
|
176
|
+
var matches = regExp.exec(lineToReplace);
|
|
177
|
+
if (matches && matches[0]) {
|
|
178
|
+
invalid = matches[0];
|
|
179
|
+
}
|
|
180
|
+
const startIndex = lineToReplace.indexOf(invalid);
|
|
181
|
+
const candidates = `['${value}']`;
|
|
182
|
+
const suggest = {
|
|
183
|
+
messageId: "ReplaceItemWith",
|
|
184
|
+
data: { invalid, candidates },
|
|
185
|
+
fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates),
|
|
186
|
+
};
|
|
187
|
+
reports.push({
|
|
188
|
+
messageId: "InvalidItem",
|
|
189
|
+
data: { invalid, candidates },
|
|
190
|
+
loc: {
|
|
191
|
+
start: { line: loc.line + 1, column: startIndex },
|
|
192
|
+
end: { line: loc.line + 1, column: startIndex + invalid.length },
|
|
193
|
+
},
|
|
194
|
+
file: context.filePath,
|
|
195
|
+
suggest,
|
|
196
|
+
severity: "warn",
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
};
|
package/lib/utils/ruleTester.js
CHANGED
|
@@ -20,8 +20,7 @@ runRuleTester: function(options) {
|
|
|
20
20
|
let parser;
|
|
21
21
|
let rule = {};
|
|
22
22
|
const rulename = path.basename(options.root);
|
|
23
|
-
|
|
24
|
-
if (options.root.includes(plugin)) {
|
|
23
|
+
if (options.root.startsWith(path.resolve(__dirname,'../..'))) {
|
|
25
24
|
// For plugin's internal tests, resolve parser from here
|
|
26
25
|
parser = require.resolve("../parser");
|
|
27
26
|
const pluginPath = path.join(path.dirname(options.root), "../..");
|