]> git.proxmox.com Git - pve-eslint.git/blob - eslint/tests/lib/cli-engine/_utils.js
first commit
[pve-eslint.git] / eslint / tests / lib / cli-engine / _utils.js
1 /**
2 * @fileoverview Define classes what use the in-memory file system.
3 *
4 * This provides utilities to test `ConfigArrayFactory`,
5 * `CascadingConfigArrayFactory`, `FileEnumerator`, and `CLIEngine`.
6 *
7 * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
8 * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
9 * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })`
10 * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })`
11 *
12 * Those functions define correspond classes with the in-memory file system.
13 * Those search config files, parsers, and plugins in the `files` option via the
14 * in-memory file system.
15 *
16 * For each test case, it makes more readable if we define minimal files the
17 * test case requires.
18 *
19 * For example:
20 *
21 * ```js
22 * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({
23 * files: {
24 * "node_modules/eslint-config-foo/index.js": `
25 * module.exports = {
26 * parser: "./parser",
27 * rules: {
28 * "no-undef": "error"
29 * }
30 * }
31 * `,
32 * "node_modules/eslint-config-foo/parser.js": `
33 * module.exports = {
34 * parse() {}
35 * }
36 * `,
37 * ".eslintrc.json": JSON.stringify({ root: true, extends: "foo" })
38 * }
39 * });
40 * const factory = new ConfigArrayFactory();
41 * const config = factory.loadFile(".eslintrc.json");
42 *
43 * assert(config[0].name === ".eslintrc.json ยป eslint-config-foo");
44 * assert(config[0].filePath === path.resolve("node_modules/eslint-config-foo/index.js"));
45 * assert(config[0].parser.filePath === path.resolve("node_modules/eslint-config-foo/parser.js"));
46 *
47 * assert(config[1].name === ".eslintrc.json");
48 * assert(config[1].filePath === path.resolve(".eslintrc.json"));
49 * assert(config[1].root === true);
50 * ```
51 *
52 * @author Toru Nagashima <https://github.com/mysticatea>
53 */
54 "use strict";
55
56 const path = require("path");
57 const vm = require("vm");
58 const Proxyquire = require("proxyquire/lib/proxyquire");
59 const { defineInMemoryFs } = require("../_utils");
60
61 const CascadingConfigArrayFactoryPath =
62 require.resolve("../../../lib/cli-engine/cascading-config-array-factory");
63 const CLIEnginePath =
64 require.resolve("../../../lib/cli-engine/cli-engine");
65 const ConfigArrayFactoryPath =
66 require.resolve("../../../lib/cli-engine/config-array-factory");
67 const FileEnumeratorPath =
68 require.resolve("../../../lib/cli-engine/file-enumerator");
69 const LoadRulesPath =
70 require.resolve("../../../lib/cli-engine/load-rules");
71 const ESLintAllPath =
72 require.resolve("../../../conf/eslint-all");
73 const ESLintRecommendedPath =
74 require.resolve("../../../conf/eslint-recommended");
75
76 // Ensure the needed files has been loaded and cached.
77 require(CascadingConfigArrayFactoryPath);
78 require(CLIEnginePath);
79 require(ConfigArrayFactoryPath);
80 require(FileEnumeratorPath);
81 require(LoadRulesPath);
82 require("js-yaml");
83 require("espree");
84
85 // Override `_require` in order to throw runtime errors in stubs.
86 const ERRORED = Symbol("errored");
87 const proxyquire = new class extends Proxyquire {
88 _require(...args) {
89 const retv = super._require(...args); // eslint-disable-line no-underscore-dangle
90
91 if (retv[ERRORED]) {
92 throw retv[ERRORED];
93 }
94 return retv;
95 }
96 }(module).noCallThru().noPreserveCache();
97
98 // Separated (sandbox) context to compile fixture files.
99 const context = vm.createContext();
100
101 /**
102 * Check if a given path is an existing file.
103 * @param {import("fs")} fs The file system.
104 * @param {string} filePath Tha path to a file to check.
105 * @returns {boolean} `true` if the file existed.
106 */
107 function isExistingFile(fs, filePath) {
108 try {
109 return fs.statSync(filePath).isFile();
110 } catch (error) {
111 return false;
112 }
113 }
114
115 /**
116 * Get some paths to test.
117 * @param {string} prefix The prefix to try.
118 * @returns {string[]} The paths to test.
119 */
120 function getTestPaths(prefix) {
121 return [
122 path.join(prefix),
123 path.join(`${prefix}.js`),
124 path.join(prefix, "index.js")
125 ];
126 }
127
128 /**
129 * Iterate the candidate paths of a given request to resolve.
130 * @param {string} request Tha package name or file path to resolve.
131 * @param {string} relativeTo Tha path to the file what called this resolving.
132 * @returns {IterableIterator<string>} The candidate paths.
133 */
134 function *iterateCandidatePaths(request, relativeTo) {
135 if (path.isAbsolute(request)) {
136 yield* getTestPaths(request);
137 return;
138 }
139 if (/^\.{1,2}[/\\]/u.test(request)) {
140 yield* getTestPaths(path.resolve(path.dirname(relativeTo), request));
141 return;
142 }
143
144 let prevPath = path.resolve(relativeTo);
145 let dirPath = path.dirname(prevPath);
146
147 while (dirPath && dirPath !== prevPath) {
148 yield* getTestPaths(path.join(dirPath, "node_modules", request));
149 prevPath = dirPath;
150 dirPath = path.dirname(dirPath);
151 }
152 }
153
154 /**
155 * Resolve a given module name or file path relatively in the given file system.
156 * @param {import("fs")} fs The file system.
157 * @param {string} request Tha package name or file path to resolve.
158 * @param {string} relativeTo Tha path to the file what called this resolving.
159 * @returns {void}
160 */
161 function fsResolve(fs, request, relativeTo) {
162 for (const filePath of iterateCandidatePaths(request, relativeTo)) {
163 if (isExistingFile(fs, filePath)) {
164 return filePath;
165 }
166 }
167
168 throw Object.assign(
169 new Error(`Cannot find module '${request}'`),
170 { code: "MODULE_NOT_FOUND" }
171 );
172 }
173
174 /**
175 * Compile a JavaScript file.
176 * This is used to compile only fixture files, so this is minimam.
177 * @param {import("fs")} fs The file system.
178 * @param {Object} stubs The stubs.
179 * @param {string} filePath The path to a JavaScript file to compile.
180 * @param {string} content The source code to compile.
181 * @returns {any} The exported value.
182 */
183 function compile(fs, stubs, filePath, content) {
184 const code = `(function(exports, require, module, __filename, __dirname) { ${content} })`;
185 const f = vm.runInContext(code, context);
186 const exports = {};
187 const module = { exports };
188
189 f.call(
190 exports,
191 exports,
192 request => {
193 const modulePath = fsResolve(fs, request, filePath);
194 const stub = stubs[modulePath];
195
196 if (stub[ERRORED]) {
197 throw stub[ERRORED];
198 }
199 return stub;
200 },
201 module,
202 filePath,
203 path.dirname(filePath)
204 );
205
206 return module.exports;
207 }
208
209 /**
210 * Import a given file path in the given file system.
211 * @param {import("fs")} fs The file system.
212 * @param {Object} stubs The stubs.
213 * @param {string} absolutePath Tha file path to import.
214 * @returns {void}
215 */
216 function fsImportFresh(fs, stubs, absolutePath) {
217 if (absolutePath === ESLintAllPath) {
218 return require(ESLintAllPath);
219 }
220 if (absolutePath === ESLintRecommendedPath) {
221 return require(ESLintRecommendedPath);
222 }
223
224 if (fs.existsSync(absolutePath)) {
225 return compile(
226 fs,
227 stubs,
228 absolutePath,
229 fs.readFileSync(absolutePath, "utf8")
230 );
231 }
232
233 throw Object.assign(
234 new Error(`Cannot find module '${absolutePath}'`),
235 { code: "MODULE_NOT_FOUND" }
236 );
237 }
238
239 /**
240 * Define stubbed `ConfigArrayFactory` class what uses the in-memory file system.
241 * @param {Object} options The options.
242 * @param {() => string} [options.cwd] The current working directory.
243 * @param {Object} [options.files] The initial files definition in the in-memory file system.
244 * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class.
245 */
246 function defineConfigArrayFactoryWithInMemoryFileSystem({
247 cwd = process.cwd,
248 files = {}
249 } = {}) {
250 const fs = defineInMemoryFs({ cwd, files });
251 const RelativeModuleResolver = { resolve: fsResolve.bind(null, fs) };
252
253 /*
254 * Stubs for proxyquire.
255 * This contains the JavaScript files in `options.files`.
256 */
257 const stubs = {};
258
259 stubs.fs = fs;
260 stubs["import-fresh"] = fsImportFresh.bind(null, fs, stubs);
261 stubs["../shared/relative-module-resolver"] = RelativeModuleResolver;
262
263 /*
264 * Write all files to the in-memory file system and compile all JavaScript
265 * files then set to `stubs`.
266 */
267 (function initFiles(directoryPath, definition) {
268 for (const [filename, content] of Object.entries(definition)) {
269 const filePath = path.resolve(directoryPath, filename);
270
271 if (typeof content === "object") {
272 initFiles(filePath, content);
273 continue;
274 }
275
276 /*
277 * Compile then stub if this file is a JavaScript file.
278 * For parsers and plugins that `require()` will import.
279 */
280 if (path.extname(filePath) === ".js") {
281 Object.defineProperty(stubs, filePath, {
282 configurable: true,
283 enumerable: true,
284 get() {
285 let stub;
286
287 try {
288 stub = compile(fs, stubs, filePath, content);
289 } catch (error) {
290 stub = { [ERRORED]: error };
291 }
292 Object.defineProperty(stubs, filePath, {
293 configurable: true,
294 enumerable: true,
295 value: stub
296 });
297
298 return stub;
299 }
300 });
301 }
302 }
303 }(cwd(), files));
304
305 // Load the stubbed one.
306 const { ConfigArrayFactory } = proxyquire(ConfigArrayFactoryPath, stubs);
307
308 // Override the default cwd.
309 return {
310 fs,
311 stubs,
312 RelativeModuleResolver,
313 ConfigArrayFactory: cwd === process.cwd
314 ? ConfigArrayFactory
315 : class extends ConfigArrayFactory {
316 constructor(options) {
317 super({ cwd: cwd(), ...options });
318 }
319 }
320 };
321 }
322
323 /**
324 * Define stubbed `CascadingConfigArrayFactory` class what uses the in-memory file system.
325 * @param {Object} options The options.
326 * @param {() => string} [options.cwd] The current working directory.
327 * @param {Object} [options.files] The initial files definition in the in-memory file system.
328 * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"] }} The stubbed `CascadingConfigArrayFactory` class.
329 */
330 function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({
331 cwd = process.cwd,
332 files = {}
333 } = {}) {
334 const { fs, stubs, RelativeModuleResolver, ConfigArrayFactory } =
335 defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files });
336 const loadRules = proxyquire(LoadRulesPath, stubs);
337 const { CascadingConfigArrayFactory } =
338 proxyquire(CascadingConfigArrayFactoryPath, {
339 "./config-array-factory": { ConfigArrayFactory },
340 "./load-rules": loadRules
341 });
342
343 // Override the default cwd.
344 return {
345 fs,
346 RelativeModuleResolver,
347 ConfigArrayFactory,
348 CascadingConfigArrayFactory: cwd === process.cwd
349 ? CascadingConfigArrayFactory
350 : class extends CascadingConfigArrayFactory {
351 constructor(options) {
352 super({ cwd: cwd(), ...options });
353 }
354 }
355 };
356 }
357
358 /**
359 * Define stubbed `FileEnumerator` class what uses the in-memory file system.
360 * @param {Object} options The options.
361 * @param {() => string} [options.cwd] The current working directory.
362 * @param {Object} [options.files] The initial files definition in the in-memory file system.
363 * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"] }} The stubbed `FileEnumerator` class.
364 */
365 function defineFileEnumeratorWithInMemoryFileSystem({
366 cwd = process.cwd,
367 files = {}
368 } = {}) {
369 const {
370 fs,
371 RelativeModuleResolver,
372 ConfigArrayFactory,
373 CascadingConfigArrayFactory
374 } =
375 defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files });
376 const { FileEnumerator } = proxyquire(FileEnumeratorPath, {
377 fs,
378 "./cascading-config-array-factory": { CascadingConfigArrayFactory }
379 });
380
381 // Override the default cwd.
382 return {
383 fs,
384 RelativeModuleResolver,
385 ConfigArrayFactory,
386 CascadingConfigArrayFactory,
387 FileEnumerator: cwd === process.cwd
388 ? FileEnumerator
389 : class extends FileEnumerator {
390 constructor(options) {
391 super({ cwd: cwd(), ...options });
392 }
393 }
394 };
395 }
396
397 /**
398 * Define stubbed `CLIEngine` class what uses the in-memory file system.
399 * @param {Object} options The options.
400 * @param {() => string} [options.cwd] The current working directory.
401 * @param {Object} [options.files] The initial files definition in the in-memory file system.
402 * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"], CLIEngine: import("../../../lib/cli-engine/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class.
403 */
404 function defineCLIEngineWithInMemoryFileSystem({
405 cwd = process.cwd,
406 files = {}
407 } = {}) {
408 const {
409 fs,
410 RelativeModuleResolver,
411 ConfigArrayFactory,
412 CascadingConfigArrayFactory,
413 FileEnumerator
414 } =
415 defineFileEnumeratorWithInMemoryFileSystem({ cwd, files });
416 const { CLIEngine, getCLIEngineInternalSlots } = proxyquire(CLIEnginePath, {
417 fs,
418 "./cascading-config-array-factory": { CascadingConfigArrayFactory },
419 "./file-enumerator": { FileEnumerator },
420 "../shared/relative-module-resolver": RelativeModuleResolver
421 });
422
423 // Override the default cwd.
424 return {
425 fs,
426 RelativeModuleResolver,
427 ConfigArrayFactory,
428 CascadingConfigArrayFactory,
429 FileEnumerator,
430 CLIEngine: cwd === process.cwd
431 ? CLIEngine
432 : class extends CLIEngine {
433 constructor(options) {
434 super({ cwd: cwd(), ...options });
435 }
436 },
437 getCLIEngineInternalSlots
438 };
439 }
440
441 module.exports = {
442 defineConfigArrayFactoryWithInMemoryFileSystem,
443 defineCascadingConfigArrayFactoryWithInMemoryFileSystem,
444 defineFileEnumeratorWithInMemoryFileSystem,
445 defineCLIEngineWithInMemoryFileSystem
446 };