@sap/eslint-plugin-cds 2.1.1 → 2.2.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.
@@ -3,367 +3,317 @@
3
3
  * https://eslint.org/docs/developer-guide/working-with-rules
4
4
  *
5
5
  * Each ESLint rule module must be composed of a 'meta', 'create' object:
6
- * - 'meta': meta data for rule (docs, fixable, etc.)
7
- * - 'create': ingests rule context and returns object with nodes to visit
8
- * while traversing the AST
6
+ * - 'meta': meta data for rule (docs, fixable, etc.)
7
+ * - 'create': ingests rule context and returns object with nodes to visit
8
+ * while traversing the AST
9
9
  *
10
- * Since for the cds we want to lint without an AST, we simplified the context
11
- * object to explicitly pass a subset of ESLint's context arguments:
12
- * - 'code': code object based on ESLint getFromSourceCode()
10
+ * Since we want to lint cds models without an AST, we adapted the context
11
+ * object to explicitly pass some additional information:
12
+ * - 'code': code object based on ESLint getFromSourceCode()
13
13
  * - 'filePath': ESLint getFilename()
14
- * We add two custom properties to aid in model rule creation:
15
- * - 'model': Loaded cds model (CSN)
16
- * - 'cds': Proxy for cached cds calls
17
- */
18
-
19
- const fs = require("fs");
20
- const path = require("path");
21
- const { RuleTester } = require("eslint");
22
- const { isEditor } = require("./utils/helpers");
23
- const { Cache, getConfigPath, getSourcecode, loadModel, updateModel, updateConfigPath } = require("./utils/model");
24
- const { getFileExtensions, getDisabledFromComments, getEnvRules, getModelRules } = require("./utils/rules");
25
- const envRules = getEnvRules(path.join(__dirname, "rules"));
26
- const modelRules = getModelRules(path.join(__dirname, "rules"));
27
- const rules = envRules.concat(modelRules).map((rule) => `@sap/cds/${rule}`);
28
-
29
- const CONSTANTS = require("./constants");
30
-
31
- // Types:
32
- // Interface CDSRuleMetaData extends Rule.RuleMetaData {
33
- // docs?: Rule.RuleMetaData['docs'] & {
34
- // version?: string;
35
- // }
36
- // }
37
- // Interface CDSRuleContext {
38
- // (cds: any,
39
- // context: Rule.RuleContext,
40
- // node: Rule.Node): Rule.ReportDescriptor[]
41
- // }
42
- // CDSReport = Rule.ReportDescriptor & { file?: string };
43
- // RuleModule = Rule.RuleModule;
44
-
45
- /**
46
- * ESLint Rule creator:
47
- * https://eslint.org/docs/developer-guide/working-with-rules
48
- * - Must follow the ESLint prescribed convention for all rule exports
49
- * - defineMeta:
50
- * - defineReport: Our wrapper for ESLint's 'create' rule function used by ESLint to traverse
51
- * its AST nodes. Since we do not work with an AST, we just access our dummy Programm node
52
- * and collect the required cds parameters for our rules
14
+ * - 'model': Loaded cds model (CSN)
15
+ * - 'cds': Proxy for cached cds calls
53
16
  *
54
- * @param defineMeta
55
- * @param defineReport
56
- * @returns
17
+ * @typedef { import('eslint').Rule.RuleModule } RuleModule
18
+ * @typedef { import('eslint').Rule.RuleContext } RuleContext
19
+ * @typedef { import('eslint').Rule.Node } RuleNode
20
+ * @typedef { import("./types").CDSRuleSpec } CDSRuleSpec
21
+ * @typedef { import("./types").CDSRuleMetaData } CDSRuleMetaData
22
+ * @typedef { import("./types").CDSRuleContext } CDSRuleContext
23
+ * @typedef { import("./types").CDSRuleTestOpts } CDSRuleTestOpts
57
24
  */
58
25
 
59
- // Types: { defineMeta: CDSRuleMetaData, defineReport: CDSRuleContext): RuleModule }
60
- function createRule(defineMeta, defineReport) {
61
- return {
62
- meta: {
63
- ...defineMeta,
64
- },
65
- // Types: { context: Rule.RuleContext }
66
- create(context) {
67
- {
68
- return cbRuleFunctions(context, defineReport);
69
- }
70
- },
71
- };
72
- }
73
-
74
- /**
75
- * More eslint-like API with more convenience re error reports
76
- */
77
- function defineRule (spec) {
78
- const { meta, create } = spec
79
- if (!meta.type) meta.type = 'problem'
80
- if (meta.docs && !meta.docs.category) meta.docs.category = "Model Validation"
81
- return createRule (meta, (report, cds) => {
82
- const handlers = create({ cds, model:cds.model, report:(r)=>report.push(r) })
83
- cds.model.forall (d => {
84
- for (let each in handlers) if (d.is(each)) {
85
- let r = handlers[each](d)
86
- if (r) {
87
- if (typeof r === 'string') r = { message: r }
88
- if (!r.loc) r.loc = cds.getLocation(d.name,d)
89
- if (!r.file) r.file = d.$location && d.$location.file || 'unknown.cds'
90
- }
91
- report.push (r)
92
- }
93
- })
94
- return report
95
- })
96
- }
97
-
98
-
99
- /**
100
- * ESLint Rule creator callback object
101
- * - Must return an object with nodes ESLint can visit while traversing the AST
102
- * (here, an empty Program node)
103
- * - Must ingest 'context' object which contains info in each rule's context
104
- * https://eslint.org/docs/developer-guide/working-with-rules#the-context-object
105
- *
106
- * @param context rule context info
107
- * @param defineReport rule report
108
- * @returns rule callback function
109
- */
110
- // Types: { context: Rule.RuleContext, defineReport: CDSRuleContext }
111
- function cbRuleFunctions(context, defineReport) {
112
- return {
113
- Program(node) {
114
- let report = [];
115
- const ruleID = context.id;
116
- let cds = context.parserServices.cdsProxy || Cache.get('cds');
117
- let sourcecode = context.getSourceCode()
118
- let filePath = context.getFilename();
119
-
120
- if (!filePath.endsWith('.js')) {
121
- filePath = `${filePath.split('.cds')[0]}.cds`;
122
- if (filePath.endsWith('.cds')) {
123
- const code = sourcecode.getText(node) || Cache.get(`file:${filePath}`);
124
- if (code) sourcecode = getSourcecode(code);
125
-
126
- let category = CONSTANTS.categories.model;
127
- if (envRules.includes(ruleID.replace("@sap/cds/", ""))) {
128
- category = CONSTANTS.categories.env;
129
- }
130
-
131
- // Set configPath according to filePath
132
- let configPath = path.dirname(getConfigPath(filePath));
133
- Cache.set("configpath", configPath);
134
-
135
- // Get cds model for current project
136
- loadModel(code, configPath, filePath);
137
-
138
- // Update config path (any files not part of the above model)
139
- // Can only do this if model.$sources are known, otherwise no
140
- // way to distinguish between 'model' vs 'outsider' files
141
- if (cds && cds.model && !cds.model.err) {
142
- updateConfigPath(code, configPath, filePath);
143
- configPath = Cache.get("configpath");
144
- }
145
-
146
- // Update cds model on every 'type' event
147
- if (
148
- Cache.has(`file:${filePath}`) &&
149
- code !== Cache.get(`file:${filePath}`)
150
- ) {
151
- // Update file contents in Cache
152
- Cache.set(`file:${filePath}`, code);
153
- updateModel(code, configPath, filePath);
154
- }
155
-
156
- // Get cds environment (when called from ruleTester)
157
- if (context.options[0] && context.options[0].environment) {
158
- Cache.set(`environment`, context.options[0].environment);
159
- }
160
-
161
- // Get report for triggered cds 'environment' and 'model' rules
162
- const properties = { filePath, configPath, ruleID, code, sourcecode };
163
- if (cds && isValidModel(cds, ruleID)) {
164
- if (
165
- (category === CONSTANTS.categories.model && cds.model &&
166
- getFileExtensions().includes(`*${path.extname(filePath)}`)) ||
167
- (category === CONSTANTS.categories.env && cds.environment)
168
- ) {
169
- report = defineReport(report, cds, sourcecode, node, filePath);
170
- }
171
- if (report.length > 0) {
172
- reportErrors(report, context, properties);
173
- }
174
- } else {
175
- // Add any compilation errors to ESLint report, except when plugin
176
- // is used within VS Code ESLint extension editor as it would duplicate
177
- // CDS compile messages
178
- if (hasCompilationError(cds, ruleID) && !isEditor()) {
179
- report = defineReport(report, cds, sourcecode, node);
180
- if (report.length > 0) {
181
- reportErrors(report, context, properties);
182
- }
183
- }
184
- }
185
- }
186
- }
187
- },
188
- };
189
- }
190
-
191
- /**
192
- * Checks whether the compiled cds model or environment
193
- * is eligible for performing the plugin's rule checks
194
- * @param cds cds object
195
- * @param ruleID rule name
196
- * @returns
197
- */
198
- function isValidModel(cds, ruleID) {
199
- if (
200
- (cds && cds.model && !cds.model.err) ||
201
- (cds.environment &&
202
- !(
203
- ruleID === "@sap/cds/cds-compile-error" ||
204
- ruleID === "cds-compile-error"
205
- ))
206
- ) {
207
- return true;
208
- }
209
- return false;
210
- }
211
-
212
- /**
213
- * Checks whether the compiled cds model contains compilation errors
214
- * which should only be reported via the 'cds-compile-error' rule
215
- * @param cds cds object
216
- * @param ruleID rule name
217
- * @returns
218
- */
219
- function hasCompilationError(cds, ruleID) {
220
- if (
221
- cds &&
222
- cds.model &&
223
- cds.model.err &&
224
- cds.model.err.message.startsWith("CDS compilation failed")
225
- ) {
226
- if (
227
- ruleID === "@sap/cds/cds-compile-error" ||
228
- ruleID === "cds-compile-error"
229
- ) {
230
- cds.model.err
231
- return true;
232
- }
233
- }
234
- return false;
235
- }
236
-
237
- /**
238
- * Allows to pass on every ESLint error report only for its corresponding file
239
- * @param report rule report
240
- * @param context ESLint rule context
241
- * @param properties Current file/path/rule association
242
- * Only send report if:
243
- * - Rule is not disabled via comment
244
- * - Rule is an environment check
245
- * - Rule is a model check and:
246
- * - Error/warning can be allocated to an actual file
247
- * - File is <stdin>.cds from model rule test
248
- */
249
- // Types: { report: any[], context: Rule.RuleContext, properties: any }
250
- function reportErrors(report, context, properties) {
251
- // Types: { entry: CDSReport, i: number }
252
- report.forEach((entry) => {
253
- if (entry) {
254
- const ruleID = properties.ruleID;
255
- const configPath = properties.configPath;
256
- const fileRel = entry.file;
257
- let fileAbs = entry.file || "";
258
- if (!path.isAbsolute(fileAbs)) {
259
- fileAbs = fileRel ? path.join(Cache.get("configpath"), fileRel) : "";
260
- }
261
- let isDisabled = false;
262
- if (entry.loc && entry.loc.start) {
263
- const line = entry.loc.start.line;
264
- const rulesDisabled = getDisabledFromComments(
265
- rules,
266
- properties.code,
267
- properties.sourcecode,
268
- line
269
- );
270
- if (
271
- line &&
272
- ruleID in rulesDisabled &&
273
- rulesDisabled[ruleID] === "off"
274
- ) {
275
- isDisabled = true;
276
- }
277
- }
278
- if (!isDisabled) {
279
- if (
280
- envRules.includes(ruleID) ||
281
- envRules.includes(ruleID.replace("@sap/cds/", ""))
282
- ) {
283
- if (!isEditor()) {
284
- if (!Cache.has(`envChecks:${configPath}`)) {
285
- context.report(entry);
286
- Cache.set(`envChecks:${configPath}`, [entry.message]);
287
- } else if (
288
- Cache.has(`envChecks:${configPath}`) &&
289
- !Cache.get(`envChecks:${configPath}`).includes(entry.message)
290
- ) {
291
- context.report(entry);
292
- Cache.set(`envChecks:${configPath}`, [entry.message]);
293
- }
294
- }
295
- } else if (
296
- fileRel &&
297
- (fileAbs === properties.filePath || fileRel === "<stdin>.cds")
298
- ) {
299
- context.report(entry);
300
- }
301
- }
302
- }
303
- });
304
- }
305
-
306
- /**
307
- * ESLint RuleTester (used by custom rule creator api)
308
- * Calls ESLint's RuleTester with custom cds parser and input for
309
- * valid/invalid checks:
310
- * Model checks require input 'code' entries
311
- * Env checks require input 'options' with selected parameters
312
- * @param options RuleTester input options
313
- * @returns RuleTester results
314
- */
315
- function runRuleTester(options) {
316
- let parser;
317
- let rule = {};
318
- const rulename = path.basename(options.root);
319
- const plugin = "eslint-plugin-cds";
320
- if (options.root.includes(plugin)) {
321
- // For internal plugin tests, resolve parser from here
322
- try {
323
- parser = options.parser;
324
- rule = options.rule;
325
- } catch (err) {
326
- throw new Error(err);
327
- }
328
- } else {
329
- // Otherwise from project root
330
- const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
331
- paths: [options.root],
332
- });
333
- parser = path.join(path.dirname(resolvedPlugin), "parser");
334
- rule = require(path.join(
335
- path.dirname(resolvedPlugin),
336
- `rules/${rulename}`
337
- ));
338
- }
339
-
340
- const category = rule.meta.docs.category;
341
- const tester = new RuleTester({ parser });
342
- const testerCases = {};
343
- ["valid", "invalid"].forEach((type) => {
344
- const filePath = path.join(options.root, `${type}/${options.filename}`);
345
- testerCases[type] = [
346
- {
347
- filename: filePath,
348
- },
349
- ];
350
- if (category === "Environment") {
351
- testerCases[type][0].code = "";
352
- testerCases[type][0].options = [
353
- { environment: JSON.parse(fs.readFileSync(filePath, "utf8")) },
354
- ];
355
- } else if (category === CONSTANTS.categories.model) {
356
- testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
357
- }
358
- if (type === "invalid") {
359
- testerCases[type][0].errors = options.errors;
360
- const fileFixed = path.join(options.root, `fixed/${options.filename}`);
361
- if (fs.existsSync(fileFixed)) {
362
- testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
363
- }
364
- }
365
- });
366
- return tester.run(rulename, rule, testerCases);
367
- }
368
-
369
- module.exports = { defineRule, createRule, runRuleTester };
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+ const { RuleTester, SourceCode } = require("eslint");
29
+ const { isEditor, isValidFile, hasCompilationError, isTest } = require("./utils/helpers");
30
+ const { isValidModel, isValidEnv, isValidRule } = require("./utils/validate");
31
+ const { Cache, getAST, updateCache } = require("./utils/model");
32
+ const { checkDisabled, getRules } = require("./utils/rules");
33
+ const { customRulesDir, categories } = require("./constants");
34
+
35
+ /**
36
+ * Wrapper for ESLint's Rule creator:
37
+ * https://eslint.org/docs/developer-guide/working-with-rules
38
+ * - Must follow the ESLint prescribed convention for all rule exports
39
+ * - ESLint uses 'create' function to traverse its AST nodes
40
+ * - Since we do not work with an AST for cds models, a dummy 'Programm'
41
+ * node is used as an entry point
42
+ * - For all ESLint rules, we have two entry points for additional checks:
43
+ * 1. Before ESLint's rule creation via create()
44
+ * (see below)
45
+ * 2. Before ESLint's report via context.report()
46
+ * (see getProxyReport())
47
+ * @param {CDSRuleSpec} spec
48
+ * @returns {RuleModule}
49
+ */
50
+ function createRule(spec) {
51
+ const { meta, create } = spec;
52
+ if (!meta.type) meta.type = "problem";
53
+ if (meta.docs && !meta.docs.category) meta.docs.category = categories["model"];
54
+ return {
55
+ meta,
56
+ create: function (context) {
57
+ return {
58
+ Program: function (node) {
59
+ // --- Checks before create() ---
60
+ // If plugin rules have been loaded
61
+ if (Cache.has("rulesInfo")) {
62
+ let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
63
+ let cdscontext = createCDSContext(context, node, listEnvRules, listModelRules, listRules);
64
+
65
+ // If file has plugin's allowed extension or a valid environment
66
+ if (isValidFile(cdscontext.filePath) || isValidEnv(cdscontext.options)) {
67
+ cdscontext = updateCache(cdscontext);
68
+
69
+ // If rule has not already been evaluated for this model
70
+ if (!Cache.get(`done:${cdscontext.configPath}:${cdscontext.ruleID}` === cdscontext.filePath)) {
71
+ let customRulesPath = path.join(Cache.get(`configpath`), customRulesDir, "rules");
72
+ const stats = fs.statSync(Cache.get("configpath"));
73
+ if (stats.isFile()) {
74
+ customRulesPath = path.join(Cache.get('projectpath'), customRulesDir, "rules");
75
+ }
76
+ let customRulesInfo;
77
+ if (fs.existsSync(customRulesPath)) {
78
+ customRulesInfo = getRules(customRulesPath);
79
+ listEnvRules = listEnvRules.concat(customRulesInfo.listEnvRules);
80
+ listModelRules = listModelRules.concat(customRulesInfo.listModelRules);
81
+ }
82
+
83
+ // If valid rule and model/env
84
+ if (
85
+ isValidRule(cdscontext, listEnvRules) ||
86
+ (isValidRule(cdscontext, listModelRules) &&
87
+ isValidModel(cdscontext) &&
88
+ !hasCompilationError(cdscontext))
89
+ ) {
90
+ create(cdscontext);
91
+ Cache.set(`done:${cdscontext.configPath}:${cdscontext.ruleID}`, cdscontext.filePath);
92
+
93
+ // If compilation error (only shown on console, not in editor)
94
+ } else if (hasCompilationError(cdscontext) && !isEditor()) {
95
+ create(cdscontext);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ },
101
+ };
102
+ },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Generates proxy for ESLint's context object which adds caching
108
+ * @param obj ESLint's context object
109
+ * @returns Proxy for cds
110
+ */
111
+ function getProxyReport(obj) {
112
+ const handler = {
113
+ get(target, prop, receiver) {
114
+ const value = Reflect.get(target, prop, receiver);
115
+ if (typeof value !== "object") {
116
+ return value;
117
+ }
118
+ /* eslint no-extra-boolean-cast: "off" */
119
+ if (!!value) {
120
+ return new Proxy(value, handler);
121
+ }
122
+ return {
123
+ err: `Property ${prop} prop does not exist on object ${obj}!`,
124
+ };
125
+ },
126
+ apply(target, thisArg, argumentsList) {
127
+ let report = false;
128
+ if (argumentsList.length > 0) {
129
+ argumentsList.forEach((lint) => {
130
+ if (lint) {
131
+ // --- Checks before context.report() ---
132
+ let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
133
+
134
+ // If rule/loc not disabled by ESLint disable comment
135
+ if (!checkDisabled(lint, thisArg, listRules, listEnvRules, listModelRules)) {
136
+ const fileRel = lint.file;
137
+ let fileAbs = lint.file || "";
138
+ if (!path.isAbsolute(fileAbs)) {
139
+ fileAbs = fileRel ? path.join(Cache.get("configpath"), fileRel) : "";
140
+ }
141
+ const stats = fs.statSync(Cache.get("configpath"));
142
+ if (stats.isFile()) {
143
+ fileAbs = Cache.get("configpath");
144
+ }
145
+
146
+ // If environment rule, only report once per configPath
147
+ if (
148
+ listEnvRules.includes(thisArg.ruleID) ||
149
+ listEnvRules.includes(thisArg.ruleID.replace("@sap/cds/", ""))
150
+ ) {
151
+ if (!isEditor()) {
152
+ if (!Cache.has(`envChecks:${thisArg.configPath}`)) {
153
+ report = true;
154
+ Cache.set(`envChecks:${thisArg.configPath}`, [lint.message]);
155
+ } else if (
156
+ Cache.has(`envChecks:${thisArg.configPath}`) &&
157
+ !Cache.get(`envChecks:${thisArg.configPath}`).includes(lint.message)
158
+ ) {
159
+ report = true;
160
+ Cache.set(`envChecks:${thisArg.configPath}`, [lint.message]);
161
+ }
162
+ }
163
+
164
+ // If model rules, only report if file matches that of report
165
+ } else if (
166
+ fileAbs === thisArg.filePath ||
167
+ fileAbs === thisArg.configPath ||
168
+ fileRel === "<stdin>.cds" ||
169
+ isTest()
170
+ ) {
171
+ report = true;
172
+ }
173
+ if (report) {
174
+ return thisArg._context.report(lint);
175
+ }
176
+ }
177
+ }
178
+ });
179
+ }
180
+ },
181
+ };
182
+ return new Proxy(obj, handler);
183
+ }
184
+
185
+ /**
186
+ * Experimental wrapper for 'createRule' to yield:
187
+ * - More eslint-like API
188
+ * - More convenience re error reports
189
+ * @param {CDSRuleSpec} spec
190
+ * @returns {RuleModule}
191
+ */
192
+ function defineRule(spec) {
193
+ const { meta, create } = spec;
194
+ if (!meta.type) meta.type = "problem";
195
+ if (meta.docs && !meta.docs.category) meta.docs.category = "Model Validation";
196
+ return createRule({
197
+ meta,
198
+ create: (context) => {
199
+ const { cds, report } = context;
200
+ const handlers = create({
201
+ cds,
202
+ model: cds.model,
203
+ report: (r) => report(r),
204
+ });
205
+ if (cds.model) {
206
+ cds.model.forall((d) => {
207
+ for (let each in handlers)
208
+ if (d.is(each)) {
209
+ let r = handlers[each](d);
210
+ if (r) {
211
+ if (typeof r === "string") r = { message: r };
212
+ if (!r.loc) r.loc = cds.getLocation(d.name, d);
213
+ if (!r.file) r.file = (d.$location && d.$location.file) || "unknown.cds";
214
+ context.report(r);
215
+ }
216
+ }
217
+ });
218
+ }
219
+ },
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Expands CDS context object with some CDS properties
225
+ * We also retrieve the file contents cached by the preprocessor
226
+ * @param {RuleContext} context
227
+ * @param {RuleNode} node
228
+ * @returns cdscontext
229
+ */
230
+ function createCDSContext(context, node, envRules, modelRules, rules) {
231
+ let sourcecode = context.getSourceCode();
232
+ let code = sourcecode.getText(node);
233
+ if (!code) {
234
+ code = Cache.get(`processed:${context.getPhysicalFilename()}`);
235
+ }
236
+ if (code) {
237
+ sourcecode = new SourceCode(code, getAST(code));
238
+ }
239
+ return {
240
+ _context: context,
241
+ ...context,
242
+ cds: context.parserServices.cdsProxy || Cache.get("cds"),
243
+ configPath: Cache.get("configpath"),
244
+ code,
245
+ filePath: context.getPhysicalFilename(),
246
+ options: context.options,
247
+ ruleID: context.id,
248
+ sourcecode,
249
+ envRules,
250
+ modelRules,
251
+ node,
252
+ rules,
253
+ report: getProxyReport(context.report),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * ESLint RuleTester (used by custom rule creator api)
259
+ * Calls ESLint's RuleTester with custom cds parser and input for
260
+ * valid/invalid checks:
261
+ * Model checks require input 'code' entries
262
+ * Env checks require input 'options' with selected parameters
263
+ * @param {CDSRuleTestOpts} options RuleTester input options
264
+ * @returns RuleTester results
265
+ */
266
+ function runRuleTester(options) {
267
+ let parser;
268
+ let rule = {};
269
+ const rulename = path.basename(options.root);
270
+ const plugin = "eslint-plugin-cds";
271
+ if (options.root.includes(plugin)) {
272
+ // For plugin's internal tests, resolve parser from here
273
+ parser = require.resolve("./parser");
274
+ rule = require(`./rules/${path.basename(options.root)}`);
275
+ Cache.set("rulesInfo", getRules(path.join(path.dirname(options.root), "../../lib/impl/rules")));
276
+ } else {
277
+ // Otherwise from project root
278
+ const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
279
+ paths: [options.root],
280
+ });
281
+ parser = path.join(path.dirname(resolvedPlugin), "parser");
282
+ rule = require(path.join(options.root, `../../rules/${path.basename(options.root)}`));
283
+ Cache.set("rulesInfo", getRules(path.join(options.root, "../../rules")));
284
+ }
285
+ let category = categories["model"];
286
+ if (rule.meta) {
287
+ category = rule.meta.docs.category;
288
+ }
289
+ let tester = new RuleTester({});
290
+ if (parser) {
291
+ tester = new RuleTester({ parser });
292
+ }
293
+ const testerCases = {};
294
+ ["valid", "invalid"].forEach((type) => {
295
+ const filePath = path.join(options.root, `${type}/${options.filename}`);
296
+ testerCases[type] = [
297
+ {
298
+ filename: filePath,
299
+ },
300
+ ];
301
+ if (category === "Environment") {
302
+ testerCases[type][0].code = "";
303
+ testerCases[type][0].options = [{ environment: JSON.parse(fs.readFileSync(filePath, "utf8")) }];
304
+ } else if (!category || category === categories.model) {
305
+ testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
306
+ }
307
+ if (type === "invalid") {
308
+ testerCases[type][0].errors = options.errors;
309
+ const fileFixed = path.join(options.root, `fixed/${options.filename}`);
310
+ if (fs.existsSync(fileFixed)) {
311
+ testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
312
+ }
313
+ }
314
+ });
315
+ return tester.run(rulename, rule, testerCases);
316
+ }
317
+
318
+ module.exports = { createRule, defineRule, runRuleTester };
319
+