Ohm-Management - Projektarbeit B-ME
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

config-file.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /**
  2. * @fileoverview Helper to locate and load configuration files.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const fs = require("fs"),
  10. path = require("path"),
  11. ConfigOps = require("./config-ops"),
  12. validator = require("./config-validator"),
  13. ModuleResolver = require("../util/module-resolver"),
  14. naming = require("../util/naming"),
  15. pathIsInside = require("path-is-inside"),
  16. stripComments = require("strip-json-comments"),
  17. stringify = require("json-stable-stringify-without-jsonify"),
  18. requireUncached = require("require-uncached");
  19. const debug = require("debug")("eslint:config-file");
  20. //------------------------------------------------------------------------------
  21. // Helpers
  22. //------------------------------------------------------------------------------
  23. /**
  24. * Determines sort order for object keys for json-stable-stringify
  25. *
  26. * see: https://github.com/samn/json-stable-stringify#cmp
  27. *
  28. * @param {Object} a The first comparison object ({key: akey, value: avalue})
  29. * @param {Object} b The second comparison object ({key: bkey, value: bvalue})
  30. * @returns {number} 1 or -1, used in stringify cmp method
  31. */
  32. function sortByKey(a, b) {
  33. return a.key > b.key ? 1 : -1;
  34. }
  35. //------------------------------------------------------------------------------
  36. // Private
  37. //------------------------------------------------------------------------------
  38. const CONFIG_FILES = [
  39. ".eslintrc.js",
  40. ".eslintrc.yaml",
  41. ".eslintrc.yml",
  42. ".eslintrc.json",
  43. ".eslintrc",
  44. "package.json"
  45. ];
  46. const resolver = new ModuleResolver();
  47. /**
  48. * Convenience wrapper for synchronously reading file contents.
  49. * @param {string} filePath The filename to read.
  50. * @returns {string} The file contents, with the BOM removed.
  51. * @private
  52. */
  53. function readFile(filePath) {
  54. return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");
  55. }
  56. /**
  57. * Determines if a given string represents a filepath or not using the same
  58. * conventions as require(), meaning that the first character must be nonalphanumeric
  59. * and not the @ sign which is used for scoped packages to be considered a file path.
  60. * @param {string} filePath The string to check.
  61. * @returns {boolean} True if it's a filepath, false if not.
  62. * @private
  63. */
  64. function isFilePath(filePath) {
  65. return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0));
  66. }
  67. /**
  68. * Loads a YAML configuration from a file.
  69. * @param {string} filePath The filename to load.
  70. * @returns {Object} The configuration object from the file.
  71. * @throws {Error} If the file cannot be read.
  72. * @private
  73. */
  74. function loadYAMLConfigFile(filePath) {
  75. debug(`Loading YAML config file: ${filePath}`);
  76. // lazy load YAML to improve performance when not used
  77. const yaml = require("js-yaml");
  78. try {
  79. // empty YAML file can be null, so always use
  80. return yaml.safeLoad(readFile(filePath)) || {};
  81. } catch (e) {
  82. debug(`Error reading YAML file: ${filePath}`);
  83. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  84. throw e;
  85. }
  86. }
  87. /**
  88. * Loads a JSON configuration from a file.
  89. * @param {string} filePath The filename to load.
  90. * @returns {Object} The configuration object from the file.
  91. * @throws {Error} If the file cannot be read.
  92. * @private
  93. */
  94. function loadJSONConfigFile(filePath) {
  95. debug(`Loading JSON config file: ${filePath}`);
  96. try {
  97. return JSON.parse(stripComments(readFile(filePath)));
  98. } catch (e) {
  99. debug(`Error reading JSON file: ${filePath}`);
  100. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  101. e.messageTemplate = "failed-to-read-json";
  102. e.messageData = {
  103. path: filePath,
  104. message: e.message
  105. };
  106. throw e;
  107. }
  108. }
  109. /**
  110. * Loads a legacy (.eslintrc) configuration from a file.
  111. * @param {string} filePath The filename to load.
  112. * @returns {Object} The configuration object from the file.
  113. * @throws {Error} If the file cannot be read.
  114. * @private
  115. */
  116. function loadLegacyConfigFile(filePath) {
  117. debug(`Loading config file: ${filePath}`);
  118. // lazy load YAML to improve performance when not used
  119. const yaml = require("js-yaml");
  120. try {
  121. return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
  122. } catch (e) {
  123. debug(`Error reading YAML file: ${filePath}`);
  124. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  125. throw e;
  126. }
  127. }
  128. /**
  129. * Loads a JavaScript configuration from a file.
  130. * @param {string} filePath The filename to load.
  131. * @returns {Object} The configuration object from the file.
  132. * @throws {Error} If the file cannot be read.
  133. * @private
  134. */
  135. function loadJSConfigFile(filePath) {
  136. debug(`Loading JS config file: ${filePath}`);
  137. try {
  138. return requireUncached(filePath);
  139. } catch (e) {
  140. debug(`Error reading JavaScript file: ${filePath}`);
  141. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  142. throw e;
  143. }
  144. }
  145. /**
  146. * Loads a configuration from a package.json file.
  147. * @param {string} filePath The filename to load.
  148. * @returns {Object} The configuration object from the file.
  149. * @throws {Error} If the file cannot be read.
  150. * @private
  151. */
  152. function loadPackageJSONConfigFile(filePath) {
  153. debug(`Loading package.json config file: ${filePath}`);
  154. try {
  155. return loadJSONConfigFile(filePath).eslintConfig || null;
  156. } catch (e) {
  157. debug(`Error reading package.json file: ${filePath}`);
  158. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  159. throw e;
  160. }
  161. }
  162. /**
  163. * Creates an error to notify about a missing config to extend from.
  164. * @param {string} configName The name of the missing config.
  165. * @returns {Error} The error object to throw
  166. * @private
  167. */
  168. function configMissingError(configName) {
  169. const error = new Error(`Failed to load config "${configName}" to extend from.`);
  170. error.messageTemplate = "extend-config-missing";
  171. error.messageData = {
  172. configName
  173. };
  174. return error;
  175. }
  176. /**
  177. * Loads a configuration file regardless of the source. Inspects the file path
  178. * to determine the correctly way to load the config file.
  179. * @param {Object} file The path to the configuration.
  180. * @returns {Object} The configuration information.
  181. * @private
  182. */
  183. function loadConfigFile(file) {
  184. const filePath = file.filePath;
  185. let config;
  186. switch (path.extname(filePath)) {
  187. case ".js":
  188. config = loadJSConfigFile(filePath);
  189. if (file.configName) {
  190. config = config.configs[file.configName];
  191. if (!config) {
  192. throw configMissingError(file.configFullName);
  193. }
  194. }
  195. break;
  196. case ".json":
  197. if (path.basename(filePath) === "package.json") {
  198. config = loadPackageJSONConfigFile(filePath);
  199. if (config === null) {
  200. return null;
  201. }
  202. } else {
  203. config = loadJSONConfigFile(filePath);
  204. }
  205. break;
  206. case ".yaml":
  207. case ".yml":
  208. config = loadYAMLConfigFile(filePath);
  209. break;
  210. default:
  211. config = loadLegacyConfigFile(filePath);
  212. }
  213. return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);
  214. }
  215. /**
  216. * Writes a configuration file in JSON format.
  217. * @param {Object} config The configuration object to write.
  218. * @param {string} filePath The filename to write to.
  219. * @returns {void}
  220. * @private
  221. */
  222. function writeJSONConfigFile(config, filePath) {
  223. debug(`Writing JSON config file: ${filePath}`);
  224. const content = stringify(config, { cmp: sortByKey, space: 4 });
  225. fs.writeFileSync(filePath, content, "utf8");
  226. }
  227. /**
  228. * Writes a configuration file in YAML format.
  229. * @param {Object} config The configuration object to write.
  230. * @param {string} filePath The filename to write to.
  231. * @returns {void}
  232. * @private
  233. */
  234. function writeYAMLConfigFile(config, filePath) {
  235. debug(`Writing YAML config file: ${filePath}`);
  236. // lazy load YAML to improve performance when not used
  237. const yaml = require("js-yaml");
  238. const content = yaml.safeDump(config, { sortKeys: true });
  239. fs.writeFileSync(filePath, content, "utf8");
  240. }
  241. /**
  242. * Writes a configuration file in JavaScript format.
  243. * @param {Object} config The configuration object to write.
  244. * @param {string} filePath The filename to write to.
  245. * @returns {void}
  246. * @private
  247. */
  248. function writeJSConfigFile(config, filePath) {
  249. debug(`Writing JS config file: ${filePath}`);
  250. const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
  251. fs.writeFileSync(filePath, content, "utf8");
  252. }
  253. /**
  254. * Writes a configuration file.
  255. * @param {Object} config The configuration object to write.
  256. * @param {string} filePath The filename to write to.
  257. * @returns {void}
  258. * @throws {Error} When an unknown file type is specified.
  259. * @private
  260. */
  261. function write(config, filePath) {
  262. switch (path.extname(filePath)) {
  263. case ".js":
  264. writeJSConfigFile(config, filePath);
  265. break;
  266. case ".json":
  267. writeJSONConfigFile(config, filePath);
  268. break;
  269. case ".yaml":
  270. case ".yml":
  271. writeYAMLConfigFile(config, filePath);
  272. break;
  273. default:
  274. throw new Error("Can't write to unknown file type.");
  275. }
  276. }
  277. /**
  278. * Determines the base directory for node packages referenced in a config file.
  279. * This does not include node_modules in the path so it can be used for all
  280. * references relative to a config file.
  281. * @param {string} configFilePath The config file referencing the file.
  282. * @returns {string} The base directory for the file path.
  283. * @private
  284. */
  285. function getBaseDir(configFilePath) {
  286. // calculates the path of the project including ESLint as dependency
  287. const projectPath = path.resolve(__dirname, "../../../");
  288. if (configFilePath && pathIsInside(configFilePath, projectPath)) {
  289. // be careful of https://github.com/substack/node-resolve/issues/78
  290. return path.join(path.resolve(configFilePath));
  291. }
  292. /*
  293. * default to ESLint project path since it's unlikely that plugins will be
  294. * in this directory
  295. */
  296. return path.join(projectPath);
  297. }
  298. /**
  299. * Determines the lookup path, including node_modules, for package
  300. * references relative to a config file.
  301. * @param {string} configFilePath The config file referencing the file.
  302. * @returns {string} The lookup path for the file path.
  303. * @private
  304. */
  305. function getLookupPath(configFilePath) {
  306. const basedir = getBaseDir(configFilePath);
  307. return path.join(basedir, "node_modules");
  308. }
  309. /**
  310. * Resolves a eslint core config path
  311. * @param {string} name The eslint config name.
  312. * @returns {string} The resolved path of the config.
  313. * @private
  314. */
  315. function getEslintCoreConfigPath(name) {
  316. if (name === "eslint:recommended") {
  317. /*
  318. * Add an explicit substitution for eslint:recommended to
  319. * conf/eslint-recommended.js.
  320. */
  321. return path.resolve(__dirname, "../../conf/eslint-recommended.js");
  322. }
  323. if (name === "eslint:all") {
  324. /*
  325. * Add an explicit substitution for eslint:all to conf/eslint-all.js
  326. */
  327. return path.resolve(__dirname, "../../conf/eslint-all.js");
  328. }
  329. throw configMissingError(name);
  330. }
  331. /**
  332. * Applies values from the "extends" field in a configuration file.
  333. * @param {Object} config The configuration information.
  334. * @param {Config} configContext Plugin context for the config instance
  335. * @param {string} filePath The file path from which the configuration information
  336. * was loaded.
  337. * @param {string} [relativeTo] The path to resolve relative to.
  338. * @returns {Object} A new configuration object with all of the "extends" fields
  339. * loaded and merged.
  340. * @private
  341. */
  342. function applyExtends(config, configContext, filePath, relativeTo) {
  343. let configExtends = config.extends;
  344. // normalize into an array for easier handling
  345. if (!Array.isArray(config.extends)) {
  346. configExtends = [config.extends];
  347. }
  348. // Make the last element in an array take the highest precedence
  349. return configExtends.reduceRight((previousValue, parentPath) => {
  350. try {
  351. let extensionPath;
  352. if (parentPath.startsWith("eslint:")) {
  353. extensionPath = getEslintCoreConfigPath(parentPath);
  354. } else if (isFilePath(parentPath)) {
  355. /*
  356. * If the `extends` path is relative, use the directory of the current configuration
  357. * file as the reference point. Otherwise, use as-is.
  358. */
  359. extensionPath = (path.isAbsolute(parentPath)
  360. ? parentPath
  361. : path.join(relativeTo || path.dirname(filePath), parentPath)
  362. );
  363. } else {
  364. extensionPath = parentPath;
  365. }
  366. debug(`Loading ${extensionPath}`);
  367. // eslint-disable-next-line no-use-before-define
  368. return ConfigOps.merge(load(extensionPath, configContext, relativeTo), previousValue);
  369. } catch (e) {
  370. /*
  371. * If the file referenced by `extends` failed to load, add the path
  372. * to the configuration file that referenced it to the error
  373. * message so the user is able to see where it was referenced from,
  374. * then re-throw.
  375. */
  376. e.message += `\nReferenced from: ${filePath}`;
  377. throw e;
  378. }
  379. }, config);
  380. }
  381. /**
  382. * Resolves a configuration file path into the fully-formed path, whether filename
  383. * or package name.
  384. * @param {string} filePath The filepath to resolve.
  385. * @param {string} [relativeTo] The path to resolve relative to.
  386. * @returns {Object} An object containing 3 properties:
  387. * - 'filePath' (required) the resolved path that can be used directly to load the configuration.
  388. * - 'configName' the name of the configuration inside the plugin.
  389. * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
  390. * or the absolute path to a config file. This should uniquely identify a config.
  391. * @private
  392. */
  393. function resolve(filePath, relativeTo) {
  394. if (isFilePath(filePath)) {
  395. const fullPath = path.resolve(relativeTo || "", filePath);
  396. return { filePath: fullPath, configFullName: fullPath };
  397. }
  398. let normalizedPackageName;
  399. if (filePath.startsWith("plugin:")) {
  400. const configFullName = filePath;
  401. const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
  402. const configName = filePath.slice(filePath.lastIndexOf("/") + 1);
  403. normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin");
  404. debug(`Attempting to resolve ${normalizedPackageName}`);
  405. return {
  406. filePath: require.resolve(normalizedPackageName),
  407. configName,
  408. configFullName
  409. };
  410. }
  411. normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");
  412. debug(`Attempting to resolve ${normalizedPackageName}`);
  413. return {
  414. filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)),
  415. configFullName: filePath
  416. };
  417. }
  418. /**
  419. * Loads a configuration file from the given file path.
  420. * @param {Object} resolvedPath The value from calling resolve() on a filename or package name.
  421. * @param {Config} configContext Plugins context
  422. * @returns {Object} The configuration information.
  423. */
  424. function loadFromDisk(resolvedPath, configContext) {
  425. const dirname = path.dirname(resolvedPath.filePath),
  426. lookupPath = getLookupPath(dirname);
  427. let config = loadConfigFile(resolvedPath);
  428. if (config) {
  429. // ensure plugins are properly loaded first
  430. if (config.plugins) {
  431. configContext.plugins.loadAll(config.plugins);
  432. }
  433. // include full path of parser if present
  434. if (config.parser) {
  435. if (isFilePath(config.parser)) {
  436. config.parser = path.resolve(dirname || "", config.parser);
  437. } else {
  438. config.parser = resolver.resolve(config.parser, lookupPath);
  439. }
  440. }
  441. const ruleMap = configContext.linterContext.getRules();
  442. // validate the configuration before continuing
  443. validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);
  444. /*
  445. * If an `extends` property is defined, it represents a configuration file to use as
  446. * a "parent". Load the referenced file and merge the configuration recursively.
  447. */
  448. if (config.extends) {
  449. config = applyExtends(config, configContext, resolvedPath.filePath, dirname);
  450. }
  451. }
  452. return config;
  453. }
  454. /**
  455. * Loads a config object, applying extends if present.
  456. * @param {Object} configObject a config object to load
  457. * @param {Config} configContext Context for the config instance
  458. * @returns {Object} the config object with extends applied if present, or the passed config if not
  459. * @private
  460. */
  461. function loadObject(configObject, configContext) {
  462. return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;
  463. }
  464. /**
  465. * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet
  466. * cached.
  467. * @param {string} filePath the path to the config file
  468. * @param {Config} configContext Context for the config instance
  469. * @param {string} [relativeTo] The path to resolve relative to.
  470. * @returns {Object} the parsed config object (empty object if there was a parse error)
  471. * @private
  472. */
  473. function load(filePath, configContext, relativeTo) {
  474. const resolvedPath = resolve(filePath, relativeTo);
  475. const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);
  476. if (cachedConfig) {
  477. return cachedConfig;
  478. }
  479. const config = loadFromDisk(resolvedPath, configContext);
  480. if (config) {
  481. config.filePath = resolvedPath.filePath;
  482. config.baseDirectory = path.dirname(resolvedPath.filePath);
  483. configContext.configCache.setConfig(resolvedPath.configFullName, config);
  484. }
  485. return config;
  486. }
  487. /**
  488. * Checks whether the given filename points to a file
  489. * @param {string} filename A path to a file
  490. * @returns {boolean} `true` if a file exists at the given location
  491. */
  492. function isExistingFile(filename) {
  493. try {
  494. return fs.statSync(filename).isFile();
  495. } catch (err) {
  496. if (err.code === "ENOENT") {
  497. return false;
  498. }
  499. throw err;
  500. }
  501. }
  502. //------------------------------------------------------------------------------
  503. // Public Interface
  504. //------------------------------------------------------------------------------
  505. module.exports = {
  506. getBaseDir,
  507. getLookupPath,
  508. load,
  509. loadObject,
  510. resolve,
  511. write,
  512. applyExtends,
  513. CONFIG_FILES,
  514. /**
  515. * Retrieves the configuration filename for a given directory. It loops over all
  516. * of the valid configuration filenames in order to find the first one that exists.
  517. * @param {string} directory The directory to check for a config file.
  518. * @returns {?string} The filename of the configuration file for the directory
  519. * or null if there is no configuration file in the directory.
  520. */
  521. getFilenameForDirectory(directory) {
  522. return CONFIG_FILES.map(filename => path.join(directory, filename)).find(isExistingFile) || null;
  523. }
  524. };