@sap/eslint-plugin-cds 2.2.2 → 2.3.3
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 +32 -106
- package/lib/api/index.js +11 -13
- package/lib/api/lint.d.ts +48 -0
- package/lib/constants.js +54 -0
- package/lib/index.js +44 -0
- package/lib/{impl/parser.js → parser.js} +2 -13
- package/lib/processor.js +47 -0
- package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +51 -52
- package/lib/rules/latest-cds-version.js +42 -0
- package/lib/rules/min-node-version.js +47 -0
- package/lib/rules/no-db-keywords.js +46 -0
- package/lib/rules/no-dollar-prefixed-names.js +47 -0
- package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +16 -11
- package/lib/rules/require-2many-oncond.js +27 -0
- package/lib/rules/sql-cast-suggestion.js +52 -0
- package/lib/rules/start-elements-lowercase.js +61 -0
- package/lib/rules/start-entities-uppercase.js +55 -0
- package/lib/rules/valid-csv-header.js +100 -0
- package/lib/utils/fuzzySearch.js +87 -0
- package/lib/utils/helpers.js +55 -0
- package/lib/{impl/utils → utils}/jsonc.js +0 -0
- package/lib/{impl/utils → utils}/model.js +122 -216
- package/lib/utils/ruleHelpers.js +56 -0
- package/lib/utils/ruleTester.js +79 -0
- package/lib/utils/rules.js +1033 -0
- package/lib/{impl/utils → utils}/validate.js +8 -21
- package/package.json +3 -3
- package/lib/api/formatter.js +0 -182
- package/lib/impl/constants.js +0 -40
- package/lib/impl/index.js +0 -58
- package/lib/impl/processor.js +0 -23
- package/lib/impl/ruleFactory.js +0 -311
- package/lib/impl/rules/cds-compile-error.js +0 -35
- package/lib/impl/rules/latest-cds-version.js +0 -46
- package/lib/impl/rules/min-node-version.js +0 -42
- package/lib/impl/rules/no-db-keywords.js +0 -35
- package/lib/impl/rules/require-2many-oncond.js +0 -29
- package/lib/impl/rules/rule.hbs +0 -20
- package/lib/impl/rules/sql-cast-suggestion.js +0 -50
- package/lib/impl/rules/start-elements-lowercase.js +0 -74
- package/lib/impl/rules/start-entities-uppercase.js +0 -65
- package/lib/impl/types.d.ts +0 -48
- package/lib/impl/utils/helpers.js +0 -68
- package/lib/impl/utils/rules.js +0 -550
|
@@ -12,38 +12,25 @@ module.exports = {
|
|
|
12
12
|
* @returns
|
|
13
13
|
*/
|
|
14
14
|
isValidRule: function (context, rules) {
|
|
15
|
-
if (rules.includes(context.
|
|
15
|
+
if (rules.includes(context.id.replace("@sap/cds/", ""))) {
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
return false;
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Checks whether a
|
|
22
|
+
* Checks whether a cds model is error-free
|
|
23
23
|
* @param context cds context object
|
|
24
24
|
* @returns
|
|
25
25
|
*/
|
|
26
26
|
isValidModel: function (context) {
|
|
27
27
|
const cds = context.cds;
|
|
28
|
-
if (cds
|
|
29
|
-
|
|
28
|
+
if (cds) {
|
|
29
|
+
if (cds.model) {
|
|
30
|
+
return !cds.model.err;
|
|
31
|
+
}
|
|
32
|
+
return true; // allow no model
|
|
30
33
|
}
|
|
31
34
|
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Checks whether a well-defined cds environment exists
|
|
36
|
-
* @param context cds context object
|
|
37
|
-
* @returns
|
|
38
|
-
*/
|
|
39
|
-
isValidEnv: function (context) {
|
|
40
|
-
if (
|
|
41
|
-
context.options &&
|
|
42
|
-
context.options[0] &&
|
|
43
|
-
context.options[0].environment
|
|
44
|
-
) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
},
|
|
35
|
+
}
|
|
49
36
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "ESLint plugin including recommended SAP Cloud Application Programming model and environment rules",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"author": "SAP SE (https://www.sap.com)",
|
|
14
14
|
"license": "See LICENSE file",
|
|
15
|
-
"main": "lib/
|
|
15
|
+
"main": "lib/index.js",
|
|
16
16
|
"files": [
|
|
17
17
|
"lib/",
|
|
18
18
|
"CHANGELOG.md",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@sap/cds": "^5.
|
|
23
|
+
"@sap/cds": "^5.6.0",
|
|
24
24
|
"semver": "^7.3.4"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
package/lib/api/formatter.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom ESLint formatter:
|
|
3
|
-
* https://eslint.org/docs/developer-guide/working-with-custom-formatters
|
|
4
|
-
* Here, we take the ESLint.LintResult[] and format it into ESLint's standard (stylish) error report.
|
|
5
|
-
* Then, we adapt it to report environment errors:
|
|
6
|
-
* - Separately from model errors
|
|
7
|
-
* - Report them within a project scope but independent of a specific file within that scope
|
|
8
|
-
*/
|
|
9
|
-
const fs = require("fs");
|
|
10
|
-
const path = require("path");
|
|
11
|
-
const formatter = require("eslint/lib/cli-engine/formatters/stylish");
|
|
12
|
-
|
|
13
|
-
const { Cache } = require("../impl/utils/model");
|
|
14
|
-
const { styleText, isEditor } = require("../impl/utils/helpers");
|
|
15
|
-
|
|
16
|
-
const CONSTANTS = require("../impl/constants");
|
|
17
|
-
/* eslint-disable-next-line no-control-regex */
|
|
18
|
-
const REGEX_STRIP_ANSI = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
19
|
-
const REGEX_NEWLINE = /\r?\n/g;
|
|
20
|
-
|
|
21
|
-
let projects = Cache.get('configpaths') || [];
|
|
22
|
-
|
|
23
|
-
module.exports = function (results, data) {
|
|
24
|
-
let output = "";
|
|
25
|
-
// Get plugin ruleIDs
|
|
26
|
-
const rules = getPluginRules(data);
|
|
27
|
-
// Get ruleIDs from custom rules
|
|
28
|
-
const customRules = [];
|
|
29
|
-
projects.forEach((project) => {
|
|
30
|
-
const customRulesPath = path.resolve(project, CONSTANTS.customRulesDir, "rules");
|
|
31
|
-
if (fs.existsSync(customRulesPath)) {
|
|
32
|
-
fs.readdirSync(customRulesPath).forEach((customRule) => {
|
|
33
|
-
const ruleID = path.basename(customRule, ".js");
|
|
34
|
-
if (!customRules.includes(ruleID)) {
|
|
35
|
-
customRules.push(ruleID);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
if (!Cache.has("err")) {
|
|
41
|
-
// Filter out error messages not from plugin or custom rules
|
|
42
|
-
if (!isEditor()) {
|
|
43
|
-
results.forEach((result) => {
|
|
44
|
-
result.messages = result.messages.filter(
|
|
45
|
-
(msg) =>
|
|
46
|
-
rules.model.includes(msg.ruleId) ||
|
|
47
|
-
rules.environment.includes(msg.ruleId) ||
|
|
48
|
-
customRules.includes(msg.ruleId)
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
// Get standard ESLint 'stylish' output
|
|
53
|
-
const outputStylish = formatter(results);
|
|
54
|
-
|
|
55
|
-
// Split according to category
|
|
56
|
-
const msgs = splitOutput(outputStylish, rules);
|
|
57
|
-
|
|
58
|
-
// Format new CDSLint output
|
|
59
|
-
output = formatOutput(output, msgs);
|
|
60
|
-
}
|
|
61
|
-
return output;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Splits rules by config and category
|
|
66
|
-
* @param data Rules meta data
|
|
67
|
-
* @returns rules object based on category
|
|
68
|
-
*/
|
|
69
|
-
function getPluginRules(data) {
|
|
70
|
-
const rules = { environment: [], model: [], recommended: [] };
|
|
71
|
-
Object.keys(data.rulesMeta).forEach((rule) => {
|
|
72
|
-
if (data.rulesMeta[rule]) {
|
|
73
|
-
const category = data.rulesMeta[rule].docs.category;
|
|
74
|
-
if (category === CONSTANTS.categories.model) {
|
|
75
|
-
rules.model.push(rule);
|
|
76
|
-
rules.recommended.push(rule);
|
|
77
|
-
} else if (category === CONSTANTS.categories.env) {
|
|
78
|
-
rules.environment.push(rule);
|
|
79
|
-
rules.recommended.push(rule);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
return rules;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Obtains ESLint's standard (*stylish*) output, then splits and reccollects error messages
|
|
88
|
-
* based on rule category and content type (msg vs. error count/report)
|
|
89
|
-
* @param outputStylish ESLint's standard output
|
|
90
|
-
* @param rules Plugin rules based on category
|
|
91
|
-
* @returns Collected msgs
|
|
92
|
-
*/
|
|
93
|
-
function splitOutput(outputStylish, rules) {
|
|
94
|
-
const msgs = { environment: {}, model: {}, report: [] };
|
|
95
|
-
let file = "";
|
|
96
|
-
outputStylish.split(REGEX_NEWLINE).forEach((line) => {
|
|
97
|
-
const lineStripped = line.replace(REGEX_STRIP_ANSI, "");
|
|
98
|
-
// Collect model file (default)
|
|
99
|
-
const lineStrippedSplit = lineStripped.split(" ");
|
|
100
|
-
const rule = lineStrippedSplit[lineStrippedSplit.length - 1];
|
|
101
|
-
if (
|
|
102
|
-
!lineStripped.startsWith(" ") &&
|
|
103
|
-
!rules.recommended.includes(rule) &&
|
|
104
|
-
!lineStripped.startsWith("✖")
|
|
105
|
-
) {
|
|
106
|
-
file = line;
|
|
107
|
-
if (process.argv[1].includes("jest") || process.argv[1].includes("mocha")) {
|
|
108
|
-
projects = [path.dirname(lineStripped)];
|
|
109
|
-
}
|
|
110
|
-
} else if (rules.environment.includes(rule)) {
|
|
111
|
-
// Collect error/warning messages from @sap/cds/ rules
|
|
112
|
-
const delimiter = line.split(/ +/, 2)[1];
|
|
113
|
-
const lineWithoutLocation = line.split(delimiter)[1];
|
|
114
|
-
let project = "";
|
|
115
|
-
for (let i = 0; i < projects.length; i++) {
|
|
116
|
-
project = projects[i];
|
|
117
|
-
if (file.includes(project)) {
|
|
118
|
-
project = styleText(project, ["link"]);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (project) {
|
|
123
|
-
if (!Object.keys(msgs.environment).includes(project)) {
|
|
124
|
-
msgs.environment[project] = [lineWithoutLocation];
|
|
125
|
-
} else if (!msgs.environment[project].includes(lineWithoutLocation)) {
|
|
126
|
-
msgs.environment[project].push(lineWithoutLocation);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} else if (
|
|
130
|
-
lineStripped &&
|
|
131
|
-
!rules.environment.includes(rule) &&
|
|
132
|
-
!lineStripped.includes("potentially fixable") &&
|
|
133
|
-
!lineStripped.startsWith("✖")
|
|
134
|
-
) {
|
|
135
|
-
if (lineStripped) {
|
|
136
|
-
if (!msgs.model[file]) {
|
|
137
|
-
msgs.model[file] = [line];
|
|
138
|
-
} else {
|
|
139
|
-
msgs.model[file].push(line);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} else if (lineStripped.startsWith("✖") || lineStripped.includes("potentially fixable")) {
|
|
143
|
-
msgs.report.push(line);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
return msgs;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Takes the collected error messages and collects them
|
|
151
|
-
* in a final string for ESLint to output
|
|
152
|
-
* @param output final string to output
|
|
153
|
-
* @param msgs error messages based on category
|
|
154
|
-
* @returns
|
|
155
|
-
*/
|
|
156
|
-
function formatOutput(output, msgs) {
|
|
157
|
-
// First, output model msgs per file (from ESLint 'stylish' output)
|
|
158
|
-
for (const fileModel in msgs.model) {
|
|
159
|
-
if (msgs.model[fileModel].length > 0) {
|
|
160
|
-
output += `${fileModel}\n`;
|
|
161
|
-
output += `${msgs.model[fileModel].join("\n")}\n\n`;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Next, output env msgs (file-independent but associated to project)
|
|
165
|
-
for (const fileModel in msgs.environment) {
|
|
166
|
-
if (msgs.environment[fileModel].length > 0) {
|
|
167
|
-
output += `${fileModel}\n`;
|
|
168
|
-
output += `${msgs.environment[fileModel].join("\n")}\n\n`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// Finally output error/warning report count
|
|
172
|
-
if (output && msgs.report) {
|
|
173
|
-
output += `${msgs.report.join("\n")}`;
|
|
174
|
-
}
|
|
175
|
-
if (output) {
|
|
176
|
-
output = `\n${output}`;
|
|
177
|
-
if (Cache.has('linted')) {
|
|
178
|
-
output += `\n\nTotal files linted: ${Cache.get('linted').files.length}\n`;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return output;
|
|
182
|
-
}
|
package/lib/impl/constants.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file contains all constants for:
|
|
3
|
-
* - categories: The category labels we use to for model and environment rules
|
|
4
|
-
* - customRulesDir: The custom rules directory name in the user's project home
|
|
5
|
-
* which contains the subdirs 'docs', 'rules' and 'tests'
|
|
6
|
-
* - recommended: The set of this plugin's recommended rules and their severities
|
|
7
|
-
* - globals: The globals which should be exposed to ESLint by this plugin
|
|
8
|
-
* - files: Any additional file extensions which ESLint should lint
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
module.exports = {
|
|
12
|
-
categories: {
|
|
13
|
-
env: "Environment",
|
|
14
|
-
model: "Model Validation",
|
|
15
|
-
},
|
|
16
|
-
customRulesDir: ".eslint",
|
|
17
|
-
recommended: {
|
|
18
|
-
"@sap/cds/assoc2many-ambiguous-key": 1,
|
|
19
|
-
"@sap/cds/cds-compile-error": 2,
|
|
20
|
-
"@sap/cds/min-node-version": 2,
|
|
21
|
-
"@sap/cds/no-join-on-draft-enabled-entities": 1,
|
|
22
|
-
"@sap/cds/no-db-keywords": 2,
|
|
23
|
-
"@sap/cds/require-2many-oncond": 2,
|
|
24
|
-
"@sap/cds/sql-cast-suggestion": 1,
|
|
25
|
-
},
|
|
26
|
-
globals: {
|
|
27
|
-
SELECT: true,
|
|
28
|
-
INSERT: true,
|
|
29
|
-
UPDATE: true,
|
|
30
|
-
DELETE: true,
|
|
31
|
-
CREATE: true,
|
|
32
|
-
DROP: true,
|
|
33
|
-
CDL: true,
|
|
34
|
-
CQL: true,
|
|
35
|
-
CXL: true,
|
|
36
|
-
cds: true,
|
|
37
|
-
},
|
|
38
|
-
files: ["*.cds", "*.csn"],
|
|
39
|
-
modelFiles: ["*.cds", "*.csn"],
|
|
40
|
-
};
|
package/lib/impl/index.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom ESLint plugin:
|
|
3
|
-
* https://eslint.org/docs/developer-guide/working-with-plugins
|
|
4
|
-
* This file exposes our plugins ESLint configuration, which must:
|
|
5
|
-
* - Expose any 'configs' for prescribed rule configuration bundles
|
|
6
|
-
* (i.e. "recommended"). See shareable configs:
|
|
7
|
-
* https://eslint.org/docs/developer-guide/shareable-configs
|
|
8
|
-
* - Expose any 'globals' for use in ESLint
|
|
9
|
-
* - Expose any 'processors' for use in ESLint
|
|
10
|
-
* - Expose any 'rules' for use in ESLint
|
|
11
|
-
* We also initiate and cache the objects 'rulesInfo' and 'cds' for later use.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const path = require("path");
|
|
15
|
-
const processor = require("./processor");
|
|
16
|
-
const plugin = require("../../package.json").name;
|
|
17
|
-
|
|
18
|
-
const { getRules } = require("../impl/utils/rules");
|
|
19
|
-
const {
|
|
20
|
-
Cache,
|
|
21
|
-
getCDSProxy,
|
|
22
|
-
getLocation,
|
|
23
|
-
getRange,
|
|
24
|
-
} = require("../impl/utils/model");
|
|
25
|
-
const { files, globals, recommended } = require("./constants");
|
|
26
|
-
|
|
27
|
-
const cds = require("@sap/cds");
|
|
28
|
-
cds.getLocation = getLocation;
|
|
29
|
-
cds.getRange = getRange;
|
|
30
|
-
Cache.set("cds", getCDSProxy(cds));
|
|
31
|
-
|
|
32
|
-
let rulesInfo;
|
|
33
|
-
if (!Cache.has("rulesInfo")) {
|
|
34
|
-
rulesInfo = getRules(path.join(__dirname, "rules"));
|
|
35
|
-
Cache.set("rulesInfo", rulesInfo);
|
|
36
|
-
} else {
|
|
37
|
-
rulesInfo = Cache.get("rulesInfo");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
module.exports = {
|
|
41
|
-
configs: {
|
|
42
|
-
recommended: {
|
|
43
|
-
globals,
|
|
44
|
-
plugins: [plugin],
|
|
45
|
-
overrides: [
|
|
46
|
-
{
|
|
47
|
-
files: files,
|
|
48
|
-
processor: "@sap/cds/cds",
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
rules: recommended,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
processors: {
|
|
55
|
-
cds: processor,
|
|
56
|
-
},
|
|
57
|
-
rules: rulesInfo.rules,
|
|
58
|
-
};
|
package/lib/impl/processor.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint custom processor:
|
|
3
|
-
* https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins
|
|
4
|
-
* This processor is used to avoid parsing errors when this plugin is extended
|
|
5
|
-
* in ESLint alongside other plugins, such as prettier which then also try to
|
|
6
|
-
* read the new file types exposed via globs.
|
|
7
|
-
* Note, that because we cache the file contents and return empty files, the
|
|
8
|
-
* plugin's parser is not actually used and we must retrieve the file contents
|
|
9
|
-
* later on (ruleFactory).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { Cache } = require("./utils/model");
|
|
13
|
-
const { isValidFile } = require("./utils/helpers");
|
|
14
|
-
|
|
15
|
-
module.exports = {
|
|
16
|
-
preprocess: function (text, filename) {
|
|
17
|
-
if (isValidFile(filename)) {
|
|
18
|
-
Cache.set(`file:${filename}`, text);
|
|
19
|
-
}
|
|
20
|
-
return [{ text: "", filename }];
|
|
21
|
-
},
|
|
22
|
-
supportsAutofix: true,
|
|
23
|
-
};
|
package/lib/impl/ruleFactory.js
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rule report object (as prescribed by ESLint):
|
|
3
|
-
* https://eslint.org/docs/developer-guide/working-with-rules
|
|
4
|
-
*
|
|
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
|
|
9
|
-
*
|
|
10
|
-
* Since we want to lint cds models without an AST, we adapted the context
|
|
11
|
-
* object to explicitly pass some additional information:
|
|
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
|
|
21
|
-
*
|
|
22
|
-
* @typedef { import('eslint').Rule.RuleModule } RuleModule
|
|
23
|
-
* @typedef { import('eslint').Rule.RuleContext } RuleContext
|
|
24
|
-
* @typedef { import('eslint').Rule.Node } RuleNode
|
|
25
|
-
* @typedef { import("./types").CDSRuleSpec } CDSRuleSpec
|
|
26
|
-
* @typedef { import("./types").CDSRuleMetaData } CDSRuleMetaData
|
|
27
|
-
* @typedef { import("./types").CDSRuleContext } CDSRuleContext
|
|
28
|
-
* @typedef { import("./types").CDSRuleTestOpts } CDSRuleTestOpts
|
|
29
|
-
*/
|
|
30
|
-
|
|
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 };
|