@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.
@@ -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
- if (cache.get(key)) {
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
- if (!module.exports.Cache.has("configpath") &&
60
- (configPath !== module.exports.Cache.get("configpath"))) {
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
- let lines;
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
- if (i === 0) {
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
- if (line > 1) {
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
- let lines;
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
- const compiledModel = cds.load(roots, {
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
- files.forEach((file) => {
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
- if (module.exports.Cache.has(`file:${file}`)) {
363
- dictFiles[file] = module.exports.Cache.get(`file:${file}`);
364
- } else {
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
- dictFiles[filePath] = code;
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 false;
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
- if (!files) {
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
  }
@@ -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
+ };
@@ -20,8 +20,7 @@ runRuleTester: function(options) {
20
20
  let parser;
21
21
  let rule = {};
22
22
  const rulename = path.basename(options.root);
23
- const plugin = "eslint-plugin-cds";
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), "../..");