@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.
@@ -9,10 +9,15 @@
9
9
  *
10
10
  * Since we want to lint cds models without an AST, we adapted the context
11
11
  * object to explicitly pass some additional information:
12
- * - 'code': code object based on ESLint getFromSourceCode()
13
- * - 'filePath': ESLint getFilename()
14
- * - 'model': Loaded cds model (CSN)
15
- * - 'cds': Proxy for cached cds calls
12
+ * - 'category' : Rule category
13
+ * - 'cds' : Proxy for cds object which also caches results
14
+ * - 'code' : Code object based on ESLint getFromSourceCode()
15
+ * - 'ruleID' : Rule ID ("@sap/cds/...")
16
+ * - 'filePath' : ESLint's 'physical' filename
17
+ * - 'options' : CDS Environment parameters
18
+ * - 'report' : Proxy for ESLint's context.report() for lint filtering
19
+ * - 'ruleID' : ESLint's rule ID
20
+ * - 'sourcecode': ESLint's SourceCode object
16
21
  *
17
22
  * @typedef { import('eslint').Rule.RuleModule } RuleModule
18
23
  * @typedef { import('eslint').Rule.RuleContext } RuleContext
@@ -23,297 +28,284 @@
23
28
  * @typedef { import("./types").CDSRuleTestOpts } CDSRuleTestOpts
24
29
  */
25
30
 
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
-
31
+ const fs = require("fs");
32
+ const path = require("path");
33
+ const { RuleTester, SourceCode } = require("eslint");
34
+ const { isEditor, isValidFile, styleText } = require("./utils/helpers");
35
+ const { isValidEnv, isValidModel } = require("./utils/validate");
36
+ const {
37
+ Cache,
38
+ populateModelAndEnv,
39
+ hasCompilationError,
40
+ getAST,
41
+ loadConfigPath,
42
+ } = require("./utils/model");
43
+ const { isRuleDisabled, getRules, populateRules } = require("./utils/rules");
44
+ const { customRulesDir, categories } = require("./constants");
45
+
46
+ /**
47
+ * Wrapper for ESLint's Rule creator:
48
+ * https://eslint.org/docs/developer-guide/working-with-rules
49
+ * - Must follow the ESLint prescribed convention for all rule exports
50
+ * - ESLint uses 'create' function to traverse its AST nodes
51
+ * - Since we do not work with an AST for cds models, a dummy 'Programm'
52
+ * node is used as an entry point
53
+ * - For all ESLint rules, we have two entry points for additional checks:
54
+ * 1. Before ESLint's rule creation via create()
55
+ * (see below)
56
+ * 2. Before ESLint's report via context.report()
57
+ * (see getProxyReport())
58
+ * @param {CDSRuleSpec} spec
59
+ * @returns {RuleModule}
60
+ */
61
+ function createRule(spec) {
62
+ const { meta, create } = spec;
63
+ if (!meta.type) meta.type = "problem";
64
+ if (meta.docs && !meta.docs.category)
65
+ meta.docs.category = categories["model"];
66
+ return {
67
+ meta,
68
+ create: function (context) {
69
+ return {
70
+ Program: function (node) {
71
+ // --- Checks before create() ---
72
+ let cdscontext = createCDSContext(context, node, meta);
73
+ // 1. Is file exension allowed?
74
+ // (i.e. when using globs + other plugins)
75
+ if (
76
+ isValidFile(cdscontext.filePath) ||
77
+ meta.docs.category === categories["env"]
78
+ ) {
79
+ // Update model/env contents and rules (at runtime)
80
+ populateModelAndEnv(cdscontext);
81
+ populateRules(cdscontext, customRulesDir);
82
+ // 2. Is rule allowed and model/env valid?
83
+ if (isValidEnv(cdscontext) || isValidModel(cdscontext)) {
84
+ try {
85
+ create(cdscontext);
86
+ } catch (err) {
87
+ // Do not throw to avoid ESLint VSCode editor pop-ups
88
+ styleText(
89
+ `Rule ${cdscontext.ruleID} has failed unexpectedly - please report this error!\n`,
90
+ ["bold", "red"]
91
+ );
92
+ }
93
+ // Show compilation error only on console
94
+ } else if (hasCompilationError(cdscontext) && !isEditor()) {
95
+ create(cdscontext);
96
+ }
97
+ }
98
+ },
99
+ };
100
+ },
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Experimental wrapper for 'createRule' to yield:
106
+ * - More eslint-like API
107
+ * - More convenience re error reports
108
+ * @param {CDSRuleSpec} spec
109
+ * @returns {RuleModule}
110
+ */
111
+ function defineRule(spec) {
112
+ const { meta, create } = spec;
113
+ if (!meta.type) meta.type = "problem";
114
+ if (meta.docs && !meta.docs.category) meta.docs.category = "Model Validation";
115
+ return createRule({
116
+ meta,
117
+ create: (context) => {
118
+ const { cds, report } = context;
119
+ const handlers = create({
120
+ cds,
121
+ model: cds.model,
122
+ report: (r) => report(r),
123
+ });
124
+ if (cds.model) {
125
+ cds.model.forall((d) => {
126
+ for (let each in handlers)
127
+ if (d.is(each)) {
128
+ let r = handlers[each](d);
129
+ if (r) {
130
+ if (typeof r === "string") r = { message: r };
131
+ if (!r.loc) r.loc = cds.getLocation(d.name, d);
132
+ if (!r.file)
133
+ r.file = (d.$location && d.$location.file) || "unknown.cds";
134
+ context.report(r);
135
+ }
136
+ }
137
+ });
138
+ }
139
+ },
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Generates proxy for ESLint's context object which adds caching
145
+ * @param obj ESLint's context object
146
+ * @returns Proxy for cds
147
+ */
148
+ function getProxyReport(obj) {
149
+ const handler = {
150
+ get(target, prop, receiver) {
151
+ const value = Reflect.get(target, prop, receiver);
152
+ if (typeof value !== "object") {
153
+ return value;
154
+ }
155
+ /* eslint no-extra-boolean-cast: "off" */
156
+ if (!!value) {
157
+ return new Proxy(value, handler);
158
+ }
159
+ return {
160
+ err: `Property ${prop} prop does not exist on object ${obj}!`,
161
+ };
162
+ },
163
+ apply(target, thisArg, argumentsList) {
164
+ let report = false;
165
+ if (argumentsList.length > 0) {
166
+ argumentsList.forEach((lint) => {
167
+ if (lint) {
168
+ // --- Checks before context.report() ---
169
+ // 1. Is lint (loc) not disabled by ESLint disable comments?
170
+ if (!isRuleDisabled(lint, thisArg)) {
171
+ const fileRel = lint.file;
172
+ let fileAbs = lint.file || "";
173
+ if (!path.isAbsolute(fileAbs)) {
174
+ fileAbs = fileRel
175
+ ? path.join(Cache.get("configpath"), fileRel)
176
+ : "";
177
+ }
178
+ // Only show 'env' lints on console
179
+ if (thisArg.category === "env" && !isEditor()) {
180
+ lint["loc"] = {
181
+ start: { line: 0, column: -1 },
182
+ end: { line: 0, column: -1 },
183
+ };
184
+ report = true;
185
+ // Only show 'model' lints at corrsponding file
186
+ } else if (
187
+ fileAbs === thisArg.filePath ||
188
+ fileRel === "<stdin>.cds"
189
+ ) {
190
+ report = true;
191
+ }
192
+ if (report) {
193
+ return thisArg._context.report(lint);
194
+ }
195
+ }
196
+ }
197
+ });
198
+ }
199
+ },
200
+ };
201
+ return new Proxy(obj, handler);
202
+ }
203
+
204
+ /**
205
+ * Expands CDS context object with some CDS properties
206
+ * We also retrieve the file contents cached by the preprocessor
207
+ * @param {RuleContext} context
208
+ * @param {RuleNode} node
209
+ * @returns cdscontext
210
+ */
211
+ function createCDSContext(context, node, meta) {
212
+ const filePath = context.getPhysicalFilename();
213
+ const configPath = loadConfigPath(filePath);
214
+ let category = "model";
215
+ if (meta.docs.category === categories["env"]) {
216
+ category = "env";
217
+ }
218
+ let sourcecode = context.getSourceCode();
219
+ let code = sourcecode.getText(node);
220
+ if (!code) {
221
+ code = Cache.get(`file:${context.getPhysicalFilename()}`);
222
+ }
223
+ if (code) {
224
+ sourcecode = new SourceCode(code, getAST(code));
225
+ }
226
+ return {
227
+ _context: context,
228
+ ...context,
229
+ category,
230
+ cds: context.parserServices.cdsProxy || Cache.get("cds"),
231
+ configPath,
232
+ code,
233
+ filePath,
234
+ options: context.options,
235
+ report: getProxyReport(context.report),
236
+ ruleID: context.id,
237
+ sourcecode,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * ESLint RuleTester (used by custom rule creator api)
243
+ * Calls ESLint's RuleTester with custom cds parser and input for
244
+ * valid/invalid checks:
245
+ * Model checks require input 'code' entries
246
+ * Env checks require input 'options' with selected parameters
247
+ * @param {CDSRuleTestOpts} options RuleTester input options
248
+ * @returns RuleTester results
249
+ */
250
+ function runRuleTester(options) {
251
+ process.env['RULE_TESTER'] = true;
252
+ let parser;
253
+ let rule = {};
254
+ const rulename = path.basename(options.root);
255
+ const plugin = "eslint-plugin-cds";
256
+ if (options.root.includes(plugin)) {
257
+ // For plugin's internal tests, resolve parser from here
258
+ parser = require.resolve("./parser");
259
+ rule = require(`./rules/${path.basename(options.root)}`);
260
+ Cache.set(
261
+ "rulesInfo",
262
+ getRules(path.join(path.dirname(options.root), "../../lib/impl/rules"))
263
+ );
264
+ } else {
265
+ // Otherwise from project root
266
+ const resolvedPlugin = require.resolve("@sap/eslint-plugin-cds", {
267
+ paths: [options.root],
268
+ });
269
+ parser = path.join(path.dirname(resolvedPlugin), "parser");
270
+ rule = require(path.join(
271
+ options.root,
272
+ `../../rules/${path.basename(options.root)}`
273
+ ));
274
+ Cache.set("rulesInfo", getRules(path.join(options.root, "../../rules")));
275
+ }
276
+ let category = categories["model"];
277
+ if (rule.meta) {
278
+ category = rule.meta.docs.category;
279
+ }
280
+ let tester = new RuleTester({});
281
+ if (parser) {
282
+ tester = new RuleTester({ parser });
283
+ }
284
+ const testerCases = {};
285
+ ["valid", "invalid"].forEach((type) => {
286
+ const filePath = path.join(options.root, `${type}/${options.filename}`);
287
+ testerCases[type] = [
288
+ {
289
+ filename: filePath,
290
+ },
291
+ ];
292
+ if (category === categories["env"]) {
293
+ testerCases[type][0].code = "";
294
+ testerCases[type][0].options = [
295
+ { environment: JSON.parse(fs.readFileSync(filePath, "utf8")) },
296
+ ];
297
+ } else if (!category || category === categories.model) {
298
+ testerCases[type][0].code = fs.readFileSync(filePath, "utf8");
299
+ }
300
+ if (type === "invalid") {
301
+ testerCases[type][0].errors = options.errors;
302
+ const fileFixed = path.join(options.root, `fixed/${options.filename}`);
303
+ if (fs.existsSync(fileFixed)) {
304
+ testerCases[type][0].output = fs.readFileSync(fileFixed, "utf8");
305
+ }
306
+ }
307
+ });
308
+ return tester.run(rulename, rule, testerCases);
309
+ }
310
+
311
+ module.exports = { createRule, defineRule, runRuleTester };