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