@sap/eslint-plugin-cds 2.4.1 → 2.5.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.
- package/CHANGELOG.md +16 -2
- package/lib/api/index.js +9 -8
- package/lib/conf/all.js +21 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +18 -0
- package/lib/constants.js +10 -8
- package/lib/index.js +11 -32
- package/lib/parser.js +158 -7
- package/lib/rules/assoc2many-ambiguous-key.js +21 -38
- package/lib/rules/auth-no-empty-restrictions.js +13 -21
- package/lib/rules/auth-use-requires.js +15 -15
- package/lib/rules/auth-valid-restrict-grant.js +71 -34
- package/lib/rules/auth-valid-restrict-keys.js +22 -16
- package/lib/rules/auth-valid-restrict-to.js +71 -27
- package/lib/rules/auth-valid-restrict-where.js +24 -15
- package/lib/rules/index.js +26 -0
- package/lib/rules/latest-cds-version.js +8 -7
- package/lib/rules/min-node-version.js +10 -9
- package/lib/rules/no-db-keywords.js +16 -15
- package/lib/rules/no-dollar-prefixed-names.js +9 -7
- package/lib/rules/no-join-on-draft-enabled-entities.js +9 -24
- package/lib/rules/require-2many-oncond.js +9 -14
- package/lib/rules/sql-cast-suggestion.js +6 -20
- package/lib/rules/start-elements-lowercase.js +5 -8
- package/lib/rules/start-entities-uppercase.js +8 -11
- package/lib/rules/valid-csv-header.js +66 -66
- package/lib/{api/lint.d.ts → types.d.ts} +4 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +304 -0
- package/lib/utils/createRuleDocs.js +361 -0
- package/lib/utils/{fuzzySearch.js → findFuzzy.js} +0 -2
- package/lib/utils/genDocs.js +363 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/jsonc.js +1 -1
- package/lib/utils/jsoncParser.js +1 -0
- package/lib/utils/rules.js +112 -1041
- package/lib/utils/runRuleTester.js +111 -0
- package/package.json +4 -4
- package/lib/processor.js +0 -50
- package/lib/utils/helpers.js +0 -94
- package/lib/utils/model.js +0 -393
- package/lib/utils/ruleHelpers.js +0 -199
- package/lib/utils/ruleTester.js +0 -78
- package/lib/utils/validate.js +0 -36
package/lib/utils/ruleHelpers.js
DELETED
|
@@ -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
|
-
};
|
package/lib/utils/ruleTester.js
DELETED
|
@@ -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
|
-
}
|
package/lib/utils/validate.js
DELETED
|
@@ -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
|
-
};
|