@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/README.md +2 -1
  3. package/lib/api/index.js +13 -12
  4. package/lib/conf/all.js +22 -0
  5. package/lib/conf/index.js +22 -0
  6. package/lib/conf/recommended.js +19 -0
  7. package/lib/constants.js +19 -16
  8. package/lib/index.js +11 -33
  9. package/lib/parser.js +169 -10
  10. package/lib/rules/assoc2many-ambiguous-key.js +80 -96
  11. package/lib/rules/auth-no-empty-restrictions.js +23 -30
  12. package/lib/rules/auth-use-requires.js +25 -24
  13. package/lib/rules/auth-valid-restrict-grant.js +87 -49
  14. package/lib/rules/auth-valid-restrict-keys.js +30 -23
  15. package/lib/rules/auth-valid-restrict-to.js +97 -52
  16. package/lib/rules/auth-valid-restrict-where.js +52 -42
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +27 -0
  19. package/lib/rules/latest-cds-version.js +23 -21
  20. package/lib/rules/min-node-version.js +25 -24
  21. package/lib/rules/no-db-keywords.js +23 -31
  22. package/lib/rules/no-dollar-prefixed-names.js +17 -14
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +11 -16
  25. package/lib/rules/sql-cast-suggestion.js +16 -29
  26. package/lib/rules/start-elements-lowercase.js +42 -44
  27. package/lib/rules/start-entities-uppercase.js +29 -31
  28. package/lib/rules/valid-csv-header.js +65 -64
  29. package/lib/{api/lint.d.ts → types.d.ts} +5 -7
  30. package/lib/utils/Cache.js +33 -0
  31. package/lib/utils/Colors.js +9 -0
  32. package/lib/utils/createRule.js +317 -0
  33. package/lib/utils/findFuzzy.js +87 -0
  34. package/lib/utils/genDocs.js +345 -0
  35. package/lib/utils/getConfigPath.js +33 -0
  36. package/lib/utils/getConfiguredFileTypes.js +10 -0
  37. package/lib/utils/getFileExtensions.js +8 -0
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +20 -0
  40. package/lib/utils/rules.js +112 -1041
  41. package/lib/utils/runRuleTester.js +116 -0
  42. package/package.json +10 -4
  43. package/lib/processor.js +0 -50
  44. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
  45. package/lib/utils/fuzzySearch.js +0 -94
  46. package/lib/utils/helpers.js +0 -94
  47. package/lib/utils/jsonc.js +0 -1
  48. package/lib/utils/model.js +0 -393
  49. package/lib/utils/ruleHelpers.js +0 -199
  50. package/lib/utils/ruleTester.js +0 -78
  51. package/lib/utils/validate.js +0 -36
@@ -1,393 +0,0 @@
1
- /**
2
- * @typedef { import("eslint").AST.SourceLocation } SourceLocation
3
- */
4
-
5
-
6
- const fs = require("fs");
7
- const path = require("path");
8
- const cds = require("@sap/cds");
9
- const { SourceCode } = require("eslint");
10
- const { isValidFile } = require("./helpers");
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
- return cache.get(key) ? cache.get(key)[0] : undefined
28
- },
29
- dump() {
30
- const dump = {};
31
- for (const [key, value] of cache.entries()) {
32
- const timestamp = new Date(value[1]);
33
- dump[key] = { key, value: JSON.stringify(value[0]), timestamp };
34
- }
35
- return dump;
36
- },
37
- remove(key) {
38
- if (cache.has(key)) {
39
- cache.delete(key);
40
- }
41
- },
42
- clear() {
43
- cache.clear();
44
- },
45
- },
46
-
47
- /**
48
- * Checks whether the path where the nearest ESLint configuration
49
- * file has changed
50
- * @param {*} configPath
51
- * @returns boolean
52
- */
53
- isNewConfigPath: function (configPath) {
54
- return !module.exports.Cache.has("configpath")
55
- && (configPath !== module.exports.Cache.get("configpath"))
56
- },
57
-
58
- /**
59
- * Gets directory of the nearest ESLint config files associated
60
- * Within this plugin, this is equivalent to the cds project's directory
61
- * @param filePath
62
- * @returns Directory of ESLint config file
63
- */
64
- loadConfigPath: function (filePath) {
65
- let configPath = path.dirname(module.exports.getConfigPath(filePath));
66
- if (configPath) {
67
- module.exports.Cache.set("configpath", configPath);
68
- } else {
69
- throw new Error("Failed to find an ESLint configuration file!");
70
- }
71
- return configPath;
72
- },
73
-
74
- /**
75
- * Generates dummy AST with just single Program node
76
- * @param code Parse file contents
77
- * @returns AST
78
- */
79
- getAST: function (code) {
80
- return {
81
- type: "Program",
82
- body: [],
83
- sourceType: "module",
84
- tokens: [],
85
- comments: [],
86
- range: [0, code.length],
87
- loc: {
88
- start: {
89
- line: 1,
90
- column: 0,
91
- },
92
- end: {
93
- line: 1,
94
- column: 0,
95
- },
96
- },
97
- };
98
- },
99
-
100
- /**
101
- * Converts code with {line, column} to ESLint's 'range' property:
102
- * https://eslint.org/docs/developer-guide/working-with-custom-parsers#all-nodes
103
- * code.slice(node.range[0], node.range[1]) must be the text of the node!
104
- * @param code source code
105
- * @param line line number
106
- * @param column column number
107
- * @returns ESLint range
108
- */
109
- getRange: function (code, line, column) {
110
- const lines = typeof code === "string" ? SourceCode.splitLines(code) : code
111
- const ranges = [0];
112
- lines.forEach((line, i) => {
113
- ranges[i + 1] = i === 0 ? line.length + 1 : ranges[i] + line.length + 1
114
- });
115
- return line > 1 ? ranges[line - 1] + column : column
116
- },
117
-
118
- /**
119
- * Uses ESLint's static function splitLines() to split the source code text
120
- * into an array of lines:
121
- * https://eslint.org/docs/developer-guide/nodejs-api#sourcecodesplitlines
122
- * Returns the index of the last line
123
- * @param code
124
- * @returns Last line index
125
- */
126
- getLastLine: function (code) {
127
- const lines = typeof code === "string" ? SourceCode.splitLines(code) : code
128
- return lines.length - 1;
129
- },
130
-
131
- /**
132
- * Generates ESlint's 'loc' from artifact string and cds $location property:
133
- * https://eslint.org/docs/developer-guide/working-with-rules-deprecated#contextreport
134
- * @param name
135
- * @param {SoureLocation} obj
136
- * @returns ESLint's 'loc' object
137
- */
138
- getLocation: function (name, obj, model) {
139
- let loc;
140
- const defaultLoc = {
141
- start: { line: 0, column: 0 },
142
- end: { line: 1, column: 0 },
143
- };
144
- if (obj.$location) {
145
- const nameloc = obj.$location;
146
- if (nameloc) {
147
- // CSN entry with column 0 is equivalent to 'undefined'
148
- // It means that the column in that line cannot be determined,
149
- // so we assign a value 1 to get a column location of 0
150
- if (nameloc.col === 0) {
151
- nameloc.col = 1;
152
- }
153
- loc = defaultLoc;
154
- loc.start.column = nameloc.col - 1;
155
- loc.start.line = nameloc.line;
156
- loc.end.column = nameloc.col - 1 + name.length;
157
- loc.end.line = nameloc.line;
158
- } else if (obj.parent) {
159
- this.getLocation(name, obj.parent, model);
160
- }
161
- }
162
- // Empty locations default to line 0, column 0
163
- if (!loc) {
164
- loc = defaultLoc;
165
- }
166
- return loc;
167
- },
168
-
169
- /**
170
- * Searches for ESLint config file types (in order or precedence)
171
- * and returns corresponding directory (usually project's root dir)
172
- * https://eslint.org/docs/user-guide/configuring#configuration-file-formats
173
- * @param {string} currentDir start here and search until root dir
174
- * @returns {string} dir containing ESLint config file (empty if not exists)
175
- */
176
- getConfigPath: function (currentDir = ".") {
177
- const configFiles = [
178
- ".eslintrc.js",
179
- ".eslintrc.cjs",
180
- ".eslintrc.yaml",
181
- ".eslintrc.yml",
182
- ".eslintrc.json",
183
- ".eslintrc",
184
- "package.json",
185
- ];
186
- let configDir = path.resolve(currentDir);
187
- while (configDir !== path.resolve(configDir, "..")) {
188
- for (let i = 0; i < configFiles.length; i++) {
189
- const configPath = path.join(configDir, configFiles[i]);
190
- if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
191
- return configPath;
192
- }
193
- }
194
- configDir = path.join(configDir, "..");
195
- }
196
- return "";
197
- },
198
-
199
- /**
200
- * Compiles reflected model for a given project directory
201
- * Note, that to support monorepos, the cache (in @sap/cds) must be cleared
202
- * to also change the roots with every changed configPath.
203
- * @param configPath
204
- * @returns reflected model
205
- */
206
- compileModelFromPath: function (configPath) {
207
- let compiledModel;
208
- let reflectedModel;
209
- cds.resolve.cache = {};
210
- const roots = cds.resolve("*", { root: configPath });
211
- const messages = [];
212
- if (roots) {
213
- try {
214
- compiledModel = cds.load(roots, {
215
- cwd: configPath,
216
- sync: true,
217
- locations: true,
218
- messages,
219
- });
220
- module.exports.Cache.remove('errRootModel');
221
- } catch (err) {
222
- module.exports.Cache.set('errRootModel', err);
223
- }
224
- if (compiledModel) {
225
- reflectedModel = cds.linked(compiledModel);
226
- if (messages) {
227
- reflectedModel.messages = messages;
228
- }
229
- }
230
- }
231
- return reflectedModel;
232
- },
233
-
234
- /**
235
- * Compiles reflected model for a dictionary of files/file contents
236
- * Note, that this method is used to account for editor type events
237
- * and hence, model updates.
238
- * WARNING: Only use if cds roots are defined prior to this step
239
- * and the dictionary is complete (the compiler will not resolve
240
- * any missing files)!
241
- * @param dictFiles
242
- * @returns reflected model
243
- */
244
- compileModelFromDict: function (dictFiles, options) {
245
- let reflectedModel;
246
- const messages = [];
247
- const compiledModel = cds.compile(dictFiles, {
248
- sync: true,
249
- locations: true,
250
- messages,
251
- ...options,
252
- });
253
- if (compiledModel) {
254
- reflectedModel = cds.linked(compiledModel);
255
- if (messages) {
256
- reflectedModel.messages = messages;
257
- }
258
- }
259
- return reflectedModel;
260
- },
261
-
262
- compileModelFromFile: function (code, filePath) {
263
- let compiledModel;
264
- let reflectedModel;
265
- const dictFiles = {};
266
- dictFiles[filePath] = code;
267
- let flavor = "inferred";
268
- try {
269
- compiledModel = module.exports.compileModelFromDict(dictFiles, {
270
- flavor,
271
- });
272
- } catch (err) {
273
- // Supress errors from parked files
274
- }
275
- if (compiledModel) {
276
- reflectedModel = cds.linked(compiledModel);
277
- }
278
- return reflectedModel;
279
- },
280
-
281
- /**
282
- * Initiates and stores new reflected model, it's corresponding project path,
283
- * as well as a list and dictionary of files comprising the model.
284
- * @param configPath
285
- * @param filePath
286
- */
287
- initRootModel: function (configPath) {
288
- module.exports.Cache.set("configpath", configPath);
289
- const reflectedModel = module.exports.compileModelFromPath(configPath);
290
- module.exports.Cache.set(`model:${configPath}`, reflectedModel);
291
- let files;
292
- if (reflectedModel && reflectedModel.$sources) {
293
- files = reflectedModel.$sources;
294
- if (files) {
295
- module.exports.Cache.set(`modelfiles:${configPath}`, files);
296
- } else {
297
- files = [];
298
- }
299
- const dictFiles = module.exports.getDictFiles(configPath, files);
300
- module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
301
- }
302
- return reflectedModel;
303
- },
304
-
305
- /**
306
- * Creates a model for ESLint unit tests
307
- */
308
- initModelRuleTester: function (filePath) {
309
- module.exports.Cache.set("test", true);
310
- const configPath = path.dirname(filePath);
311
- module.exports.Cache.set('configpath', configPath);
312
- let files = fs.readdirSync(configPath);
313
- const modelfiles = files.map(f => path.join(configPath, f))
314
- .filter(fp => isValidFile(fp, 'MODEL_FILES'))
315
- module.exports.Cache.set(`modelfiles:${configPath}`, modelfiles);
316
- const dictFiles = module.exports.getDictFiles(configPath, modelfiles);
317
- module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
318
- const reflectedModel = module.exports.compileModelFromDict(dictFiles);
319
- module.exports.Cache.set(`model:${configPath}`, reflectedModel);
320
- },
321
-
322
- /**
323
- * Creates or updates a dictionary of files/file contents for a given
324
- * project path.
325
- * @param configPath
326
- * @param files
327
- * @returns dictFiles
328
- */
329
- getDictFiles: function (input, files = []) {
330
- let dictFiles = {};
331
- if (module.exports.Cache.has(`dictfiles:${input}`)) {
332
- dictFiles = module.exports.Cache.get(`dictfiles:${input}`);
333
- } else {
334
- files.forEach((file) => {
335
- dictFiles[file] = module.exports.Cache.has(`file:${file}`)
336
- ? module.exports.Cache.get(`file:${file}`)
337
- : fs.readFileSync(file, "utf8")
338
- });
339
- }
340
- return dictFiles;
341
- },
342
-
343
- /**
344
- * Determines whether an incoming file has changed contents
345
- * @param context cds context object
346
- * @returns boolean
347
- */
348
- hasFileChanged: function (code, filePath, configPath) {
349
- let result = false
350
- const files = module.exports.Cache.get(`modelfiles:${configPath}`);
351
- const dictFiles = module.exports.getDictFiles(configPath, files);
352
- const isFileInModel = module.exports.isFileInModel(filePath, configPath);
353
- // If incoming file is a 'model' file
354
- if (isFileInModel) {
355
- // Only update on detected changes
356
- if (dictFiles[filePath] !== code) {
357
- result = true
358
- }
359
- } else if (dictFiles[filePath] !== code) {
360
- result = true
361
- }
362
- return result;
363
- },
364
-
365
- /**
366
- * Checks whether a file is part of the model for a given project
367
- * @param context
368
- * @param files
369
- * @returns boolean
370
- */
371
- isFileInModel(filePath, configPath) {
372
- let files = module.exports.Cache.get(`modelfiles:${configPath}`) || [];
373
- return files && files.length > 0 && files.includes(filePath)
374
- },
375
-
376
- /**
377
- * Updates and stores reflected model on file changes. Model compilation
378
- * us handled separately for 'model' files (part of model) and 'outsider'
379
- * files.
380
- * @param context cds context object
381
- */
382
- updateModel: function (code, filePath, configPath) {
383
- let reflectedModel;
384
- const dictFiles = module.exports.Cache.get(`dictfiles:${configPath}`);
385
- dictFiles[filePath] = code;
386
- module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
387
- reflectedModel = module.exports.compileModelFromDict(dictFiles, { flavor: "inferred" });
388
- if (reflectedModel) { module.exports.Cache.remove('errRootModel') }
389
- module.exports.Cache.set(`model:${configPath}`, reflectedModel);
390
- return reflectedModel;
391
- }
392
-
393
- };
@@ -1,199 +0,0 @@
1
- const SEP = "[,;\t]";
2
- const EOL = "\\r?\\n";
3
-
4
- const findFuzzy = require("./fuzzySearch");
5
-
6
- module.exports = {
7
- /**
8
- *
9
- * @param {*} e
10
- */
11
- splitEntityName: function (e) {
12
- // Entity names from CSN are of the form:
13
- // <namespace>.<service>.<entity>.<'texts'|'localized'>|<composition value>
14
- let prefix = "";
15
- let suffix = "";
16
- let entityName = e.name;
17
- const names = entityName.split(".");
18
- entityName = names[names.length - 1];
19
-
20
- if (entityName) {
21
- // Managed composition get compiler tag `_up`
22
- let isManagedComposition = false;
23
- if (e.elements) {
24
- isManagedComposition = Object.keys(e.elements).some((k) => k === "up_");
25
- }
26
- // Check for compiler tags
27
- let compilerTagsToExclude = ["texts", "localized"];
28
- const isCompilerTag = compilerTagsToExclude.includes(entityName);
29
-
30
- if (isManagedComposition || isCompilerTag) {
31
- suffix = names[names.length - 1];
32
- entityName = names[names.length - 2];
33
- }
34
- prefix = e.name.split(`.${entityName}`)[0];
35
- }
36
- return { prefix, entity: entityName, suffix };
37
- },
38
-
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
- },
82
-
83
- _findInCode: function (miss, code) {
84
- // middle
85
- let match = new RegExp(SEP + miss + SEP).exec(code);
86
- if (match) return match.index + 1;
87
- // end of line
88
- match = new RegExp(SEP + miss + EOL).exec(code);
89
- if (match) return match.index + 1;
90
- // start of doc
91
- match = new RegExp("^" + miss + SEP).exec(code);
92
- if (match) return match.index;
93
- // somewhere (fallback)
94
- return code.indexOf(miss);
95
- },
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
- },
170
-
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
- };
@@ -1,78 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- const { RuleTester } = require("eslint");
5
- const { Cache, initModelRuleTester } = require("./model");
6
- const { createRule, getRules } = require("./rules");
7
- const { isValidFile } = require("./helpers");
8
-
9
- module.exports = {
10
- /**
11
- * ESLint RuleTester (used by custom rule creator api)
12
- * Calls ESLint's RuleTester with custom cds parser and input for
13
- * valid/invalid checks:
14
- * Model checks require input 'code' entries
15
- * Env checks require input 'options' with selected parameters
16
- * @param {CDSRuleTestOpts} options RuleTester input options
17
- * @returns RuleTester results
18
- */
19
- runRuleTester: function(options) {
20
- let parser;
21
- let rule = {};
22
- const rulename = path.basename(options.root);
23
- if (options.root.startsWith(path.resolve(__dirname,'../..'))) {
24
- // For plugin's internal tests, resolve parser from here
25
- parser = require.resolve("../parser");
26
- const pluginPath = path.join(path.dirname(options.root), "../..");
27
- rule = createRule(require(`../rules/${path.basename(options.root)}`));
28
- Cache.set(
29
- "rulesInfo",
30
- getRules(path.join(path.dirname(options.root), "../../lib/rules"), rulename)
31
- );
32
- Cache.set("pluginpath", pluginPath);
33
- } else {
34
- // Otherwise from project root
35
- const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
36
- paths: [options.root],
37
- });
38
- parser = path.join(path.dirname(resolvedPlugin), "parser");
39
- rule = require(path.join(
40
- options.root,
41
- `../../rules/${path.basename(options.root)}`
42
- ));
43
- const pluginPath = path.join(path.dirname(options.root), "../../../..");
44
- Cache.set("rulesInfo", getRules(path.join(options.root, "../../rules", rulename)));
45
- Cache.set("pluginpath", pluginPath);
46
- }
47
- let tester = new RuleTester({});
48
- if (parser) {
49
- tester = new RuleTester({ parser });
50
- }
51
- const testerCases = {};
52
- ["valid", "invalid"].forEach((type) => {
53
- const filePath = path.join(options.root, `${type}/${options.filename}`);
54
- const model = initModelRuleTester(filePath);
55
- testerCases[type] = [
56
- {
57
- filename: filePath,
58
- },
59
- ];
60
- if (!isValidFile(options.filename, 'FILES')) {
61
- const fileContents = JSON.parse(fs.readFileSync(filePath, "utf8"));
62
- testerCases[type][0].code = "";
63
- testerCases[type][0].options = [{ environment: fileContents }];
64
- } else {
65
- testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
66
- testerCases[type][0].options = [{ model }];
67
- }
68
- if (type === "invalid") {
69
- testerCases[type][0].errors = options.errors;
70
- const fileFixed = path.join(options.root, `fixed/${options.filename}`);
71
- if (fs.existsSync(fileFixed) && rule.meta.type !== "suggestion") {
72
- testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
73
- }
74
- }
75
- });
76
- return tester.run(rulename, rule, testerCases);
77
- }
78
- }
@@ -1,36 +0,0 @@
1
- /**
2
- * @typedef { import("eslint").Rule.RuleContext.options } ContextOptions
3
- */
4
-
5
- module.exports = {
6
- /**
7
- * Checks whether the rule name is valid (i.e. is contained within
8
- * the plugin's or user's *custom* rules).
9
- * @param context
10
- * @param pluginRules
11
- * @param customRules
12
- * @returns
13
- */
14
- isValidRule: function (context, rules) {
15
- if (rules.includes(context.id.replace("@sap/cds/", ""))) {
16
- return true;
17
- }
18
- return false;
19
- },
20
-
21
- /**
22
- * Checks whether a cds model is error-free
23
- * @param context cds context object
24
- * @returns
25
- */
26
- isValidModel: function (context) {
27
- const cds = context.cds;
28
- if (cds) {
29
- if (cds.model) {
30
- return !cds.model.err;
31
- }
32
- return true; // allow no model
33
- }
34
- return false;
35
- }
36
- };