@sap/eslint-plugin-cds 2.3.0 → 2.3.4

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 (45) hide show
  1. package/CHANGELOG.md +44 -61
  2. package/lib/api/index.js +11 -13
  3. package/lib/api/lint.d.ts +48 -0
  4. package/lib/constants.js +54 -0
  5. package/lib/index.js +44 -0
  6. package/lib/{impl/parser.js → parser.js} +2 -13
  7. package/lib/processor.js +47 -0
  8. package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +50 -53
  9. package/lib/rules/latest-cds-version.js +42 -0
  10. package/lib/rules/min-node-version.js +47 -0
  11. package/lib/rules/no-db-keywords.js +46 -0
  12. package/lib/rules/no-dollar-prefixed-names.js +49 -0
  13. package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +14 -11
  14. package/lib/rules/require-2many-oncond.js +27 -0
  15. package/lib/rules/sql-cast-suggestion.js +52 -0
  16. package/lib/rules/start-elements-lowercase.js +61 -0
  17. package/lib/rules/start-entities-uppercase.js +55 -0
  18. package/lib/{impl/rules → rules}/valid-csv-header.js +17 -9
  19. package/lib/{impl/utils → utils}/fuzzySearch.js +0 -0
  20. package/lib/utils/helpers.js +47 -0
  21. package/lib/{impl/utils → utils}/jsonc.js +0 -0
  22. package/lib/utils/model.js +387 -0
  23. package/lib/utils/ruleHelpers.js +56 -0
  24. package/lib/utils/ruleTester.js +79 -0
  25. package/lib/utils/rules.js +973 -0
  26. package/lib/{impl/utils → utils}/validate.js +2 -18
  27. package/package.json +2 -2
  28. package/lib/api/formatter.js +0 -182
  29. package/lib/impl/constants.js +0 -30
  30. package/lib/impl/index.js +0 -63
  31. package/lib/impl/processor.js +0 -23
  32. package/lib/impl/ruleFactory.js +0 -341
  33. package/lib/impl/rules/cds-compile-error.js +0 -34
  34. package/lib/impl/rules/latest-cds-version.js +0 -51
  35. package/lib/impl/rules/min-node-version.js +0 -44
  36. package/lib/impl/rules/no-db-keywords.js +0 -38
  37. package/lib/impl/rules/require-2many-oncond.js +0 -31
  38. package/lib/impl/rules/rule.hbs +0 -20
  39. package/lib/impl/rules/sql-cast-suggestion.js +0 -52
  40. package/lib/impl/rules/start-elements-lowercase.js +0 -75
  41. package/lib/impl/rules/start-entities-uppercase.js +0 -65
  42. package/lib/impl/types.d.ts +0 -48
  43. package/lib/impl/utils/helpers.js +0 -68
  44. package/lib/impl/utils/model.js +0 -554
  45. package/lib/impl/utils/rules.js +0 -678
@@ -1,554 +0,0 @@
1
- /**
2
- * @typedef { import("eslint").AST.SourceLocation } SourceLocation
3
- */
4
-
5
- const fs = require("fs");
6
- const path = require("path");
7
- const cds = require("@sap/cds");
8
- const { SourceCode } = require("eslint");
9
- const { isValidFile } = require("./helpers");
10
- const { isValidEnv } = require("./validate");
11
-
12
- const cache = new Map();
13
-
14
- module.exports = {
15
- /**
16
- * Simple cache to store model and any cds calls made in the rule creation
17
- * api to modify the model
18
- */
19
- Cache: {
20
- has(key) {
21
- return cache.has(key);
22
- },
23
- set(key, value) {
24
- return cache.set(key, [value, Date.now()]);
25
- },
26
- get(key) {
27
- if (cache.get(key)) {
28
- return cache.get(key)[0];
29
- } else {
30
- return;
31
- }
32
- },
33
- dump() {
34
- const dump = {};
35
- for (const [key, value] of cache.entries()) {
36
- const timestamp = new Date(value[1]);
37
- dump[key] = { key, value: JSON.stringify(value[0]), timestamp };
38
- }
39
- return dump;
40
- },
41
- remove(key) {
42
- if (cache.has(key)) {
43
- cache.delete(key);
44
- }
45
- return;
46
- },
47
- clear() {
48
- cache.clear();
49
- return;
50
- },
51
- },
52
-
53
- hasModelError: function (filePath) {
54
- if (module.exports.Cache.has(`model:${filePath}`)) {
55
- const model = module.exports.Cache.get(`model:${filePath}`);
56
- if (model.err) {
57
- return true;
58
- }
59
- }
60
- return false;
61
- },
62
-
63
- /**
64
- * Checks whether the compiled cds model contains compilation errors which
65
- * should only be reported via the 'cds-compile-error' rule
66
- * @param cds cds object
67
- * @param ruleID rule name
68
- * @returns
69
- */
70
- hasCompilationError: function (context) {
71
- const cds = context.cds;
72
- const ruleID = context.ruleID;
73
- if (
74
- cds &&
75
- cds.model &&
76
- cds.model.err &&
77
- cds.model.err.message.startsWith("CDS compilation failed")
78
- ) {
79
- if (
80
- ruleID === "@sap/cds/cds-compile-error" ||
81
- ruleID === "cds-compile-error"
82
- ) {
83
- cds.model.err;
84
- return true;
85
- }
86
- }
87
- return false;
88
- },
89
-
90
- /**
91
- * Takes care of all of the cds modeling:
92
- * - Loads the model and assigns relevant files to it
93
- * - Updates the model (according to 'type' events in the editor)
94
- * - Updates the ESLint configuration file path (i.e. mono-repo with
95
- * multiple models)
96
- * @param context
97
- * @returns
98
- */
99
- populateModelAndEnv: function (context) {
100
- // Update file and config paths
101
- module.exports.Cache.set("filepath", context.filePath);
102
-
103
- // Get CDS reflected model
104
-
105
- if (
106
- process.env["LINT_FLAVOR"] !== "parsed" &&
107
- (module.exports.isNewFile(context.filePath) ||
108
- module.exports.isNewConfigPath(context.configPath))
109
- ) {
110
- module.exports.initModel(context.configPath, context.filePath);
111
- }
112
- // Trigger model updates for:
113
- // - Changed 'model' files
114
- // - Any 'outsider' files
115
- if (
116
- isValidFile(context.filePath, "model") &&
117
- module.exports.hasFileChanged(context)
118
- ) {
119
- module.exports.updateModel(context);
120
- }
121
-
122
- // Get cds environment (for internal ruleTester)
123
- if (isValidEnv(context)) {
124
- module.exports.Cache.set("environment", context.options[0].environment);
125
- }
126
- },
127
-
128
- /**
129
- * Checks whether a file is new or has already been
130
- * part of an existing cds model
131
- * @param {*} filePath
132
- * @returns boolean
133
- */
134
- isNewFile: function (filePath) {
135
- if (!module.exports.Cache.has(`model:${filePath}`)) {
136
- return true;
137
- } else {
138
- return false;
139
- }
140
- },
141
-
142
- /**
143
- * Checks whether the path where the nearest ESLint configuration
144
- * file has changed
145
- * @param {*} configPath
146
- * @returns boolean
147
- */
148
- isNewConfigPath: function (configPath) {
149
- let update = false;
150
- if (
151
- !module.exports.Cache.has("pluginpath") ||
152
- !module.exports.Cache.has("configpath") ||
153
- configPath !== module.exports.Cache.get("configpath")
154
- ) {
155
- update = true;
156
- }
157
- // Keep track of all config paths visited
158
- // - Used in formatter to group lint reports
159
- // - Dirnames are used to assign any 'env' lints
160
- if (!module.exports.Cache.has("configpaths")) {
161
- module.exports.Cache.set("configpaths", [configPath]);
162
- } else {
163
- const configPaths = module.exports.Cache.get("configpaths");
164
- configPaths.push(configPath);
165
- module.exports.Cache.set("configpaths", configPaths);
166
- }
167
- return update;
168
- },
169
-
170
- /**
171
- * Gets directory of the nearest ESLint config files associated
172
- * Within this plugin, this is equivalent to the cds project's directory
173
- * @param filePath
174
- * @returns Directory of ESLint config file
175
- */
176
- loadConfigPath: function (filePath) {
177
- let configPath = path.dirname(module.exports.getConfigPath(filePath));
178
- if (configPath) {
179
- module.exports.Cache.set("projectpath", configPath);
180
- } else {
181
- throw new Error("Failed to find an ESLint configuration file!");
182
- }
183
- return configPath;
184
- },
185
-
186
- /**
187
- * Generates dummy AST with just single Program node
188
- * @param code Parse file contents
189
- * @returns AST
190
- */
191
- getAST: function (code) {
192
- return {
193
- type: "Program",
194
- body: [],
195
- sourceType: "module",
196
- tokens: [],
197
- comments: [],
198
- range: [0, code.length],
199
- loc: {
200
- start: {
201
- line: 1,
202
- column: 0,
203
- },
204
- end: {
205
- line: 1,
206
- column: 0,
207
- },
208
- },
209
- };
210
- },
211
-
212
- /**
213
- * Generates proxy for cds object which adds caching
214
- * @param obj cds object
215
- * @returns Proxy for cds
216
- */
217
- getCDSProxy: function (obj) {
218
- const handler = {
219
- get(target, prop, receiver) {
220
- const value = Reflect.get(target, prop, receiver);
221
- if (["model", "environment"].includes(prop)) {
222
- if (prop === "model") {
223
- prop = `model:${module.exports.Cache.get("filepath")}`;
224
- }
225
- return module.exports.Cache.get(prop);
226
- }
227
- if (typeof value !== "object") {
228
- return value;
229
- }
230
- /*eslint no-extra-boolean-cast: "off"*/
231
- if (!!value) {
232
- return new Proxy(value, handler);
233
- }
234
- return {
235
- err: `Property ${prop} prop does not exist on object ${obj}!`,
236
- };
237
- },
238
- apply(target, thisArg, argumentsList) {
239
- const result = Reflect.apply(target, this, argumentsList);
240
- return result;
241
- },
242
- };
243
- return new Proxy(obj, handler);
244
- },
245
-
246
- /**
247
- * Converts code with {line, column} to ESLint's 'range' property:
248
- * https://eslint.org/docs/developer-guide/working-with-custom-parsers#all-nodes
249
- * code.slice(node.range[0], node.range[1]) must be the text of the node!
250
- * @param code source code
251
- * @param line line number
252
- * @param column column number
253
- * @returns ESLint range
254
- */
255
- getRange: function (code, line, column) {
256
- let lines;
257
- if (typeof code === "string") {
258
- lines = SourceCode.splitLines(code);
259
- } else {
260
- lines = code;
261
- }
262
- const ranges = [0];
263
- lines.forEach((line, i) => {
264
- if (i === 0) {
265
- ranges[i + 1] = line.length + 1;
266
- } else {
267
- ranges[i + 1] = ranges[i] + line.length + 1;
268
- }
269
- });
270
- if (line > 1) {
271
- return ranges[line - 1] + column;
272
- } else {
273
- return column;
274
- }
275
- },
276
-
277
- /**
278
- * Uses ESLint's static function splitLines() to split the source code text
279
- * into an array of lines:
280
- * https://eslint.org/docs/developer-guide/nodejs-api#sourcecodesplitlines
281
- * Returns the index of the last line
282
- * @param code
283
- * @returns Last line index
284
- */
285
- getLastLine: function (code) {
286
- let lines;
287
- if (typeof code === "string") {
288
- lines = SourceCode.splitLines(code);
289
- } else {
290
- lines = code;
291
- }
292
- return lines.length - 1;
293
- },
294
-
295
- /**
296
- * Generates ESlint's 'loc' from artifact string and cds $location property:
297
- * https://eslint.org/docs/developer-guide/working-with-rules-deprecated#contextreport
298
- * @param name
299
- * @param {SoureLocation} obj
300
- * @returns ESLint's 'loc' object
301
- */
302
- getLocation: function (name, obj) {
303
- const loc = {
304
- start: { line: 0, column: 0 },
305
- end: { line: 1, column: 0 },
306
- };
307
- if (obj.$location) {
308
- const nameloc = obj.$location;
309
- // CSN entry with column 0 is equivalent to 'undefined'
310
- // It means that the column in that line cannot be determined,
311
- // so we assign a value 1 so as not to get a negative value
312
- if (nameloc.col === 0) {
313
- nameloc.col = 1;
314
- }
315
- loc.start.column = nameloc.col - 1;
316
- loc.start.line = nameloc.line;
317
- loc.end.column = nameloc.col - 1 + name.length;
318
- loc.end.line = nameloc.line;
319
- }
320
- return loc;
321
- },
322
-
323
- /**
324
- * Searches for ESLint config file types (in order or precedence)
325
- * and returns corresponding directory (usually project's root dir)
326
- * https://eslint.org/docs/user-guide/configuring#configuration-file-formats
327
- * @param {string} currentDir start here and search until root dir
328
- * @returns {string} dir containing ESLint config file (empty if not exists)
329
- */
330
- getConfigPath: function (currentDir = ".") {
331
- const configFiles = [
332
- ".eslintrc.js",
333
- ".eslintrc.cjs",
334
- ".eslintrc.yaml",
335
- ".eslintrc.yml",
336
- ".eslintrc.json",
337
- ".eslintrc",
338
- "package.json",
339
- ];
340
- let configDir = path.resolve(currentDir);
341
- while (configDir !== path.resolve(configDir, "..")) {
342
- for (let i = 0; i < configFiles.length; i++) {
343
- const configPath = path.join(configDir, configFiles[i]);
344
- if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
345
- return configPath;
346
- }
347
- }
348
- configDir = path.join(configDir, "..");
349
- }
350
- return "";
351
- },
352
-
353
- /**
354
- * Compiles reflected model for a given project directory
355
- * Note, that to support monorepos, the cache (in @sap/cds) must be cleared
356
- * to also change the roots with every changed configPath.
357
- * @param configPath
358
- * @returns reflected model
359
- */
360
- compileModelFromPath: function (configPath) {
361
- let compiledModel;
362
- let reflectedModel;
363
- cds.resolve.cache = {};
364
- const roots = cds.resolve("*", { root: configPath });
365
- if (roots) {
366
- try {
367
- compiledModel = cds.load(roots, {
368
- cwd: configPath,
369
- sync: true,
370
- locations: true,
371
- });
372
- if (compiledModel) {
373
- reflectedModel = cds.linked(compiledModel);
374
- }
375
- } catch (err) {
376
- reflectedModel = { err };
377
- }
378
- }
379
- return reflectedModel;
380
- },
381
-
382
- /**
383
- * Compiles reflected model for a dictionary of files/file contents
384
- * Note, that this method is used to account for editor type events
385
- * and hence, model updates.
386
- * WARNING: Only use if cds roots are defined prior to this step
387
- * and the dictionary is complete (the compiler will not resolve
388
- * any missing files)!
389
- * @param dictFiles
390
- * @returns reflected model
391
- */
392
- compileModelFromDict: function (dictFiles, options) {
393
- let reflectedModel;
394
- try {
395
- const compiledModel = cds.compile(dictFiles, {
396
- sync: true,
397
- locations: true,
398
- ...options,
399
- });
400
- if (compiledModel) {
401
- reflectedModel = cds.linked(compiledModel);
402
- }
403
- } catch (err) {
404
- reflectedModel = { err };
405
- }
406
- return reflectedModel;
407
- },
408
-
409
- /**
410
- * Initiates and stores new reflected model, it's corresponding project path,
411
- * as well as a list and dictionary of files comprising the model.
412
- * @param configPath
413
- * @param filePath
414
- */
415
- initModel: function (configPath, filePath) {
416
- module.exports.Cache.set("configpath", configPath);
417
- const reflectedModel = module.exports.compileModelFromPath(configPath);
418
- let files;
419
- if (reflectedModel && !reflectedModel.err && reflectedModel.$sources) {
420
- module.exports.Cache.set(`model:${filePath}`, reflectedModel);
421
- files = reflectedModel.$sources;
422
- if (files) {
423
- module.exports.Cache.set(`modelfiles:${configPath}`, files);
424
- } else {
425
- files = [];
426
- }
427
- const dictFiles = module.exports.getDictFiles(configPath, files);
428
- module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
429
- }
430
- },
431
-
432
- initModelRuleTester: function (filePath) {
433
- const configPath = path.dirname(filePath);
434
- module.exports.Cache.set("configpath", configPath);
435
- let files = fs.readdirSync(configPath);
436
- const modelfiles = [];
437
- files.forEach((file) => {
438
- const filePath = path.join(configPath, file);
439
- if (isValidFile(filePath, "model")) {
440
- modelfiles.push(filePath);
441
- }
442
- });
443
- module.exports.Cache.set(`modelfiles:${configPath}`, files);
444
- const dictFiles = module.exports.getDictFiles(configPath, modelfiles);
445
- module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
446
- const compiledModel = module.exports.compileModelFromDict(dictFiles);
447
- let reflectedModel;
448
- if (compiledModel) {
449
- reflectedModel = cds.linked(compiledModel);
450
- }
451
- if (reflectedModel) {
452
- module.exports.Cache.set(`model:${filePath}`, reflectedModel);
453
- }
454
- },
455
-
456
- /**
457
- * Creates or updates a dictionary of files/file contents for a given
458
- * project path.
459
- * @param configPath
460
- * @param files
461
- * @returns dictFiles
462
- */
463
- getDictFiles: function (configPath, files=[]) {
464
- let dictFiles = {};
465
- if (module.exports.Cache.has(`dictfiles:${configPath}`)) {
466
- dictFiles = module.exports.Cache.get(`dictfiles:${configPath}`);
467
- } else {
468
- files.forEach((file) => {
469
- if (module.exports.Cache.has(`file:${file}`)) {
470
- dictFiles[file] = module.exports.Cache.get(`file:${file}`);
471
- } else {
472
- dictFiles[file] = fs.readFileSync(file, "utf8");
473
- }
474
- });
475
- }
476
- return dictFiles;
477
- },
478
-
479
- /**
480
- * Determines whether an incoming file has changed contents
481
- * @param context cds context object
482
- * @returns boolean
483
- */
484
- hasFileChanged: function (context) {
485
- const files = module.exports.Cache.get(`modelfiles:${context.configPath}`);
486
- const dictFiles = module.exports.getDictFiles(context.configPath, files);
487
- // If incoming file is a 'model' file
488
- if (module.exports.isFileInModel(context, files)) {
489
- // Only update on detected changes
490
- if (dictFiles[context.filePath] !== context.code) {
491
- dictFiles[context.filePath] = context.code;
492
- module.exports.Cache.set(`dictfiles:${context.configPath}`, dictFiles);
493
- return true;
494
- }
495
- } else {
496
- if (dictFiles[context.filePath] !== context.code) {
497
- return true;
498
- }
499
- }
500
- return false;
501
- },
502
-
503
- /**
504
- * Checks whether a file is part of the model for a given project
505
- * @param context
506
- * @param files
507
- * @returns boolean
508
- */
509
- isFileInModel(filePath, files) {
510
- if (files && files.length > 0 && files.includes(filePath)) {
511
- return true;
512
- }
513
- return false;
514
- },
515
-
516
- /**
517
- * Updates and stores reflected model on file changes. Model compilation
518
- * us handled separately for 'model' files (part of model) and 'outsider'
519
- * files.
520
- * @param context cds context object
521
- */
522
- updateModel: function (context) {
523
- let reflectedModel;
524
- let files = module.exports.Cache.get(`modelfiles:${context.configPath}`);
525
- if (!files) {
526
- files = [];
527
- }
528
- // If incoming file is a 'model' file
529
- if (
530
- !process.env["LINT_FLAVOR"] === "parsed" ||
531
- module.exports.isFileInModel(context, files)
532
- ) {
533
- const dictFiles = module.exports.Cache.get(
534
- `dictfiles:${context.configPath}`
535
- );
536
- dictFiles[context.filePath] = context.code;
537
- reflectedModel = module.exports.compileModelFromDict(dictFiles, {
538
- flavor: "inferred",
539
- });
540
- files.forEach((file) => {
541
- module.exports.Cache.set(`model:${file}`, reflectedModel);
542
- });
543
- } else {
544
- // If incoming file is an 'outsider' file
545
- const dictFiles = {};
546
- dictFiles[context.filePath] = context.code;
547
- let flavor = "parsed";
548
- reflectedModel = module.exports.compileModelFromDict(dictFiles, {
549
- flavor,
550
- });
551
- module.exports.Cache.set(`model:${context.filePath}`, reflectedModel);
552
- }
553
- },
554
- };