.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / init / config-initializer.js
1 /**
2  * @fileoverview Config initialization wizard.
3  * @author Ilya Volodin
4  */
5
6
7 "use strict";
8
9 //------------------------------------------------------------------------------
10 // Requirements
11 //------------------------------------------------------------------------------
12
13 const util = require("util"),
14     path = require("path"),
15     enquirer = require("enquirer"),
16     ProgressBar = require("progress"),
17     semver = require("semver"),
18     espree = require("espree"),
19     recConfig = require("../../conf/eslint-recommended"),
20     ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
21     log = require("../shared/logging"),
22     naming = require("@eslint/eslintrc/lib/shared/naming"),
23     ModuleResolver = require("../shared/relative-module-resolver"),
24     autoconfig = require("./autoconfig.js"),
25     ConfigFile = require("./config-file"),
26     npmUtils = require("./npm-utils"),
27     { getSourceCodeOfFiles } = require("./source-code-utils");
28
29 const debug = require("debug")("eslint:config-initializer");
30
31 //------------------------------------------------------------------------------
32 // Private
33 //------------------------------------------------------------------------------
34
35 /* istanbul ignore next: hard to test fs function */
36 /**
37  * Create .eslintrc file in the current working directory
38  * @param {Object} config object that contains user's answers
39  * @param {string} format The file format to write to.
40  * @returns {void}
41  */
42 function writeFile(config, format) {
43
44     // default is .js
45     let extname = ".js";
46
47     if (format === "YAML") {
48         extname = ".yml";
49     } else if (format === "JSON") {
50         extname = ".json";
51     }
52
53     const installedESLint = config.installedESLint;
54
55     delete config.installedESLint;
56
57     ConfigFile.write(config, `./.eslintrc${extname}`);
58     log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
59
60     if (installedESLint) {
61         log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
62     }
63 }
64
65 /**
66  * Get the peer dependencies of the given module.
67  * This adds the gotten value to cache at the first time, then reuses it.
68  * In a process, this function is called twice, but `npmUtils.fetchPeerDependencies` needs to access network which is relatively slow.
69  * @param {string} moduleName The module name to get.
70  * @returns {Object} The peer dependencies of the given module.
71  * This object is the object of `peerDependencies` field of `package.json`.
72  * Returns null if npm was not found.
73  */
74 function getPeerDependencies(moduleName) {
75     let result = getPeerDependencies.cache.get(moduleName);
76
77     if (!result) {
78         log.info(`Checking peerDependencies of ${moduleName}`);
79
80         result = npmUtils.fetchPeerDependencies(moduleName);
81         getPeerDependencies.cache.set(moduleName, result);
82     }
83
84     return result;
85 }
86 getPeerDependencies.cache = new Map();
87
88 /**
89  * Return necessary plugins, configs, parsers, etc. based on the config
90  * @param   {Object} config  config object
91  * @param   {boolean} [installESLint=true]  If `false` is given, it does not install eslint.
92  * @returns {string[]} An array of modules to be installed.
93  */
94 function getModulesList(config, installESLint) {
95     const modules = {};
96
97     // Create a list of modules which should be installed based on config
98     if (config.plugins) {
99         for (const plugin of config.plugins) {
100             const moduleName = naming.normalizePackageName(plugin, "eslint-plugin");
101
102             modules[moduleName] = "latest";
103         }
104     }
105     if (config.extends) {
106         const extendList = Array.isArray(config.extends) ? config.extends : [config.extends];
107
108         for (const extend of extendList) {
109             if (extend.startsWith("eslint:") || extend.startsWith("plugin:")) {
110                 continue;
111             }
112             const moduleName = naming.normalizePackageName(extend, "eslint-config");
113
114             modules[moduleName] = "latest";
115             Object.assign(
116                 modules,
117                 getPeerDependencies(`${moduleName}@latest`)
118             );
119         }
120     }
121
122     const parser = config.parser || (config.parserOptions && config.parserOptions.parser);
123
124     if (parser) {
125         modules[parser] = "latest";
126     }
127
128     if (installESLint === false) {
129         delete modules.eslint;
130     } else {
131         const installStatus = npmUtils.checkDevDeps(["eslint"]);
132
133         // Mark to show messages if it's new installation of eslint.
134         if (installStatus.eslint === false) {
135             log.info("Local ESLint installation not found.");
136             modules.eslint = modules.eslint || "latest";
137             config.installedESLint = true;
138         }
139     }
140
141     return Object.keys(modules).map(name => `${name}@${modules[name]}`);
142 }
143
144 /**
145  * Set the `rules` of a config by examining a user's source code
146  *
147  * Note: This clones the config object and returns a new config to avoid mutating
148  * the original config parameter.
149  * @param   {Object} answers  answers received from enquirer
150  * @param   {Object} config   config object
151  * @returns {Object}          config object with configured rules
152  */
153 function configureRules(answers, config) {
154     const BAR_TOTAL = 20,
155         BAR_SOURCE_CODE_TOTAL = 4,
156         newConfig = Object.assign({}, config),
157         disabledConfigs = {};
158     let sourceCodes,
159         registry;
160
161     // Set up a progress bar, as this process can take a long time
162     const bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
163         width: 30,
164         total: BAR_TOTAL
165     });
166
167     bar.tick(0); // Shows the progress bar
168
169     // Get the SourceCode of all chosen files
170     const patterns = answers.patterns.split(/[\s]+/u);
171
172     try {
173         sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => {
174             bar.tick((BAR_SOURCE_CODE_TOTAL / total));
175         });
176     } catch (e) {
177         log.info("\n");
178         throw e;
179     }
180     const fileQty = Object.keys(sourceCodes).length;
181
182     if (fileQty === 0) {
183         log.info("\n");
184         throw new Error("Automatic Configuration failed.  No files were able to be parsed.");
185     }
186
187     // Create a registry of rule configs
188     registry = new autoconfig.Registry();
189     registry.populateFromCoreRules();
190
191     // Lint all files with each rule config in the registry
192     registry = registry.lintSourceCode(sourceCodes, newConfig, total => {
193         bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total); // Subtract out ticks used at beginning
194     });
195     debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`);
196
197     // Create a list of recommended rules, because we don't want to disable them
198     const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
199
200     // Find and disable rules which had no error-free configuration
201     const failingRegistry = registry.getFailingRulesRegistry();
202
203     Object.keys(failingRegistry.rules).forEach(ruleId => {
204
205         // If the rule is recommended, set it to error, otherwise disable it
206         disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0;
207     });
208
209     // Now that we know which rules to disable, strip out configs with errors
210     registry = registry.stripFailingConfigs();
211
212     /*
213      * If there is only one config that results in no errors for a rule, we should use it.
214      * createConfig will only add rules that have one configuration in the registry.
215      */
216     const singleConfigs = registry.createConfig().rules;
217
218     /*
219      * The "sweet spot" for number of options in a config seems to be two (severity plus one option).
220      * Very often, a third option (usually an object) is available to address
221      * edge cases, exceptions, or unique situations. We will prefer to use a config with
222      * specificity of two.
223      */
224     const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
225
226     // Maybe a specific combination using all three options works
227     const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
228
229     // If all else fails, try to use the default (severity only)
230     const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
231
232     // Combine configs in reverse priority order (later take precedence)
233     newConfig.rules = Object.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs);
234
235     // Make sure progress bar has finished (floating point rounding)
236     bar.update(BAR_TOTAL);
237
238     // Log out some stats to let the user know what happened
239     const finalRuleIds = Object.keys(newConfig.rules);
240     const totalRules = finalRuleIds.length;
241     const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;
242     const resultMessage = [
243         `\nEnabled ${enabledRules} out of ${totalRules}`,
244         `rules based on ${fileQty}`,
245         `file${(fileQty === 1) ? "." : "s."}`
246     ].join(" ");
247
248     log.info(resultMessage);
249
250     ConfigOps.normalizeToStrings(newConfig);
251     return newConfig;
252 }
253
254 /**
255  * process user's answers and create config object
256  * @param {Object} answers answers received from enquirer
257  * @returns {Object} config object
258  */
259 function processAnswers(answers) {
260     let config = {
261         rules: {},
262         env: {},
263         parserOptions: {},
264         extends: []
265     };
266
267     config.parserOptions.ecmaVersion = espree.latestEcmaVersion;
268     config.env.es2021 = true;
269
270     // set the module type
271     if (answers.moduleType === "esm") {
272         config.parserOptions.sourceType = "module";
273     } else if (answers.moduleType === "commonjs") {
274         config.env.commonjs = true;
275     }
276
277     // add in browser and node environments if necessary
278     answers.env.forEach(env => {
279         config.env[env] = true;
280     });
281
282     // add in library information
283     if (answers.framework === "react") {
284         config.parserOptions.ecmaFeatures = {
285             jsx: true
286         };
287         config.plugins = ["react"];
288         config.extends.push("plugin:react/recommended");
289     } else if (answers.framework === "vue") {
290         config.plugins = ["vue"];
291         config.extends.push("plugin:vue/essential");
292     }
293
294     if (answers.typescript) {
295         if (answers.framework === "vue") {
296             config.parserOptions.parser = "@typescript-eslint/parser";
297         } else {
298             config.parser = "@typescript-eslint/parser";
299         }
300
301         if (Array.isArray(config.plugins)) {
302             config.plugins.push("@typescript-eslint");
303         } else {
304             config.plugins = ["@typescript-eslint"];
305         }
306     }
307
308     // setup rules based on problems/style enforcement preferences
309     if (answers.purpose === "problems") {
310         config.extends.unshift("eslint:recommended");
311     } else if (answers.purpose === "style") {
312         if (answers.source === "prompt") {
313             config.extends.unshift("eslint:recommended");
314             config.rules.indent = ["error", answers.indent];
315             config.rules.quotes = ["error", answers.quotes];
316             config.rules["linebreak-style"] = ["error", answers.linebreak];
317             config.rules.semi = ["error", answers.semi ? "always" : "never"];
318         } else if (answers.source === "auto") {
319             config = configureRules(answers, config);
320             config = autoconfig.extendFromRecommended(config);
321         }
322     }
323     if (answers.typescript && config.extends.includes("eslint:recommended")) {
324         config.extends.push("plugin:@typescript-eslint/recommended");
325     }
326
327     // normalize extends
328     if (config.extends.length === 0) {
329         delete config.extends;
330     } else if (config.extends.length === 1) {
331         config.extends = config.extends[0];
332     }
333
334     ConfigOps.normalizeToStrings(config);
335     return config;
336 }
337
338 /**
339  * Get the version of the local ESLint.
340  * @returns {string|null} The version. If the local ESLint was not found, returns null.
341  */
342 function getLocalESLintVersion() {
343     try {
344         const eslintPath = ModuleResolver.resolve("eslint", path.join(process.cwd(), "__placeholder__.js"));
345         const eslint = require(eslintPath);
346
347         return eslint.linter.version || null;
348     } catch {
349         return null;
350     }
351 }
352
353 /**
354  * Get the shareable config name of the chosen style guide.
355  * @param {Object} answers The answers object.
356  * @returns {string} The shareable config name.
357  */
358 function getStyleGuideName(answers) {
359     if (answers.styleguide === "airbnb" && answers.framework !== "react") {
360         return "airbnb-base";
361     }
362     return answers.styleguide;
363 }
364
365 /**
366  * Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
367  * @param {Object} answers The answers object.
368  * @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
369  */
370 function hasESLintVersionConflict(answers) {
371
372     // Get the local ESLint version.
373     const localESLintVersion = getLocalESLintVersion();
374
375     if (!localESLintVersion) {
376         return false;
377     }
378
379     // Get the required range of ESLint version.
380     const configName = getStyleGuideName(answers);
381     const moduleName = `eslint-config-${configName}@latest`;
382     const peerDependencies = getPeerDependencies(moduleName) || {};
383     const requiredESLintVersionRange = peerDependencies.eslint;
384
385     if (!requiredESLintVersionRange) {
386         return false;
387     }
388
389     answers.localESLintVersion = localESLintVersion;
390     answers.requiredESLintVersionRange = requiredESLintVersionRange;
391
392     // Check the version.
393     if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
394         answers.installESLint = false;
395         return false;
396     }
397
398     return true;
399 }
400
401 /**
402  * Install modules.
403  * @param   {string[]} modules Modules to be installed.
404  * @returns {void}
405  */
406 function installModules(modules) {
407     log.info(`Installing ${modules.join(", ")}`);
408     npmUtils.installSyncSaveDev(modules);
409 }
410
411 /* istanbul ignore next: no need to test enquirer */
412 /**
413  * Ask user to install modules.
414  * @param   {string[]} modules Array of modules to be installed.
415  * @param   {boolean} packageJsonExists Indicates if package.json is existed.
416  * @returns {Promise} Answer that indicates if user wants to install.
417  */
418 function askInstallModules(modules, packageJsonExists) {
419
420     // If no modules, do nothing.
421     if (modules.length === 0) {
422         return Promise.resolve();
423     }
424
425     log.info("The config that you've selected requires the following dependencies:\n");
426     log.info(modules.join(" "));
427     return enquirer.prompt([
428         {
429             type: "toggle",
430             name: "executeInstallation",
431             message: "Would you like to install them now with npm?",
432             enabled: "Yes",
433             disabled: "No",
434             initial: 1,
435             skip() {
436                 return !(modules.length && packageJsonExists);
437             },
438             result(input) {
439                 return this.skipped ? null : input;
440             }
441         }
442     ]).then(({ executeInstallation }) => {
443         if (executeInstallation) {
444             installModules(modules);
445         }
446     });
447 }
448
449 /* istanbul ignore next: no need to test enquirer */
450 /**
451  * Ask use a few questions on command prompt
452  * @returns {Promise} The promise with the result of the prompt
453  */
454 function promptUser() {
455
456     return enquirer.prompt([
457         {
458             type: "select",
459             name: "purpose",
460             message: "How would you like to use ESLint?",
461
462             // The returned number matches the name value of nth in the choices array.
463             initial: 1,
464             choices: [
465                 { message: "To check syntax only", name: "syntax" },
466                 { message: "To check syntax and find problems", name: "problems" },
467                 { message: "To check syntax, find problems, and enforce code style", name: "style" }
468             ]
469         },
470         {
471             type: "select",
472             name: "moduleType",
473             message: "What type of modules does your project use?",
474             initial: 0,
475             choices: [
476                 { message: "JavaScript modules (import/export)", name: "esm" },
477                 { message: "CommonJS (require/exports)", name: "commonjs" },
478                 { message: "None of these", name: "none" }
479             ]
480         },
481         {
482             type: "select",
483             name: "framework",
484             message: "Which framework does your project use?",
485             initial: 0,
486             choices: [
487                 { message: "React", name: "react" },
488                 { message: "Vue.js", name: "vue" },
489                 { message: "None of these", name: "none" }
490             ]
491         },
492         {
493             type: "toggle",
494             name: "typescript",
495             message: "Does your project use TypeScript?",
496             enabled: "Yes",
497             disabled: "No",
498             initial: 0
499         },
500         {
501             type: "multiselect",
502             name: "env",
503             message: "Where does your code run?",
504             hint: "(Press <space> to select, <a> to toggle all, <i> to invert selection)",
505             initial: 0,
506             choices: [
507                 { message: "Browser", name: "browser" },
508                 { message: "Node", name: "node" }
509             ]
510         },
511         {
512             type: "select",
513             name: "source",
514             message: "How would you like to define a style for your project?",
515             choices: [
516                 { message: "Use a popular style guide", name: "guide" },
517                 { message: "Answer questions about your style", name: "prompt" },
518                 { message: "Inspect your JavaScript file(s)", name: "auto" }
519             ],
520             skip() {
521                 return this.state.answers.purpose !== "style";
522             },
523             result(input) {
524                 return this.skipped ? null : input;
525             }
526         },
527         {
528             type: "select",
529             name: "styleguide",
530             message: "Which style guide do you want to follow?",
531             choices: [
532                 { message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb" },
533                 { message: "Standard: https://github.com/standard/standard", name: "standard" },
534                 { message: "Google: https://github.com/google/eslint-config-google", name: "google" }
535             ],
536             skip() {
537                 this.state.answers.packageJsonExists = npmUtils.checkPackageJson();
538                 return !(this.state.answers.source === "guide" && this.state.answers.packageJsonExists);
539             },
540             result(input) {
541                 return this.skipped ? null : input;
542             }
543         },
544         {
545             type: "input",
546             name: "patterns",
547             message: "Which file(s), path(s), or glob(s) should be examined?",
548             skip() {
549                 return this.state.answers.source !== "auto";
550             },
551             validate(input) {
552                 if (!this.skipped && input.trim().length === 0 && input.trim() !== ",") {
553                     return "You must tell us what code to examine. Try again.";
554                 }
555                 return true;
556             }
557         },
558         {
559             type: "select",
560             name: "format",
561             message: "What format do you want your config file to be in?",
562             initial: 0,
563             choices: ["JavaScript", "YAML", "JSON"]
564         },
565         {
566             type: "toggle",
567             name: "installESLint",
568             message() {
569                 const { answers } = this.state;
570                 const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
571                     ? "upgrade"
572                     : "downgrade";
573
574                 return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n  Do you want to ${verb}?`;
575             },
576             enabled: "Yes",
577             disabled: "No",
578             initial: 1,
579             skip() {
580                 return !(this.state.answers.source === "guide" && this.state.answers.packageJsonExists && hasESLintVersionConflict(this.state.answers));
581             },
582             result(input) {
583                 return this.skipped ? null : input;
584             }
585         }
586     ]).then(earlyAnswers => {
587
588         // early exit if no style guide is necessary
589         if (earlyAnswers.purpose !== "style") {
590             const config = processAnswers(earlyAnswers);
591             const modules = getModulesList(config);
592
593             return askInstallModules(modules, earlyAnswers.packageJsonExists)
594                 .then(() => writeFile(config, earlyAnswers.format));
595         }
596
597         // early exit if you are using a style guide
598         if (earlyAnswers.source === "guide") {
599             if (!earlyAnswers.packageJsonExists) {
600                 log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
601                 return void 0;
602             }
603             if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
604                 log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
605             }
606             if (earlyAnswers.styleguide === "airbnb" && earlyAnswers.framework !== "react") {
607                 earlyAnswers.styleguide = "airbnb-base";
608             }
609
610             const config = processAnswers(earlyAnswers);
611
612             if (Array.isArray(config.extends)) {
613                 config.extends.push(earlyAnswers.styleguide);
614             } else if (config.extends) {
615                 config.extends = [config.extends, earlyAnswers.styleguide];
616             } else {
617                 config.extends = [earlyAnswers.styleguide];
618             }
619
620             const modules = getModulesList(config);
621
622             return askInstallModules(modules, earlyAnswers.packageJsonExists)
623                 .then(() => writeFile(config, earlyAnswers.format));
624
625         }
626
627         if (earlyAnswers.source === "auto") {
628             const combinedAnswers = Object.assign({}, earlyAnswers);
629             const config = processAnswers(combinedAnswers);
630             const modules = getModulesList(config);
631
632             return askInstallModules(modules).then(() => writeFile(config, earlyAnswers.format));
633         }
634
635         // continue with the style questions otherwise...
636         return enquirer.prompt([
637             {
638                 type: "select",
639                 name: "indent",
640                 message: "What style of indentation do you use?",
641                 initial: 0,
642                 choices: [{ message: "Tabs", name: "tab" }, { message: "Spaces", name: 4 }]
643             },
644             {
645                 type: "select",
646                 name: "quotes",
647                 message: "What quotes do you use for strings?",
648                 initial: 0,
649                 choices: [{ message: "Double", name: "double" }, { message: "Single", name: "single" }]
650             },
651             {
652                 type: "select",
653                 name: "linebreak",
654                 message: "What line endings do you use?",
655                 initial: 0,
656                 choices: [{ message: "Unix", name: "unix" }, { message: "Windows", name: "windows" }]
657             },
658             {
659                 type: "toggle",
660                 name: "semi",
661                 message: "Do you require semicolons?",
662                 enabled: "Yes",
663                 disabled: "No",
664                 initial: 1
665             }
666         ]).then(answers => {
667             const totalAnswers = Object.assign({}, earlyAnswers, answers);
668
669             const config = processAnswers(totalAnswers);
670             const modules = getModulesList(config);
671
672             return askInstallModules(modules).then(() => writeFile(config, earlyAnswers.format));
673         });
674     });
675 }
676
677 //------------------------------------------------------------------------------
678 // Public Interface
679 //------------------------------------------------------------------------------
680
681 const init = {
682     getModulesList,
683     hasESLintVersionConflict,
684     installModules,
685     processAnswers,
686     /* istanbul ignore next */initializeConfig() {
687         return promptUser();
688     }
689 };
690
691 module.exports = init;