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