configuration.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. "use strict";
  2. /**
  3. * @license
  4. * Copyright 2013 Palantir Technologies, Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. var fs = require("fs");
  20. var yaml = require("js-yaml");
  21. var os = require("os");
  22. var path = require("path");
  23. var resolve = require("resolve");
  24. var error_1 = require("./error");
  25. var utils_1 = require("./utils");
  26. // Note: eslint prefers yaml over json, while tslint prefers json over yaml
  27. // for backward-compatibility.
  28. exports.JSON_CONFIG_FILENAME = "tslint.json";
  29. /** @deprecated use `JSON_CONFIG_FILENAME` or `CONFIG_FILENAMES` instead. */
  30. exports.CONFIG_FILENAME = exports.JSON_CONFIG_FILENAME;
  31. exports.CONFIG_FILENAMES = [exports.JSON_CONFIG_FILENAME, "tslint.yaml", "tslint.yml"];
  32. exports.DEFAULT_CONFIG = {
  33. defaultSeverity: "error",
  34. extends: ["tslint:recommended"],
  35. jsRules: new Map(),
  36. rules: new Map(),
  37. rulesDirectory: [],
  38. };
  39. exports.EMPTY_CONFIG = {
  40. defaultSeverity: "error",
  41. extends: [],
  42. jsRules: new Map(),
  43. rules: new Map(),
  44. rulesDirectory: [],
  45. };
  46. var BUILT_IN_CONFIG = /^tslint:(.*)$/;
  47. function findConfiguration(configFile, inputFilePath) {
  48. var configPath = findConfigurationPath(configFile, inputFilePath);
  49. var loadResult = { path: configPath };
  50. try {
  51. loadResult.results = loadConfigurationFromPath(configPath);
  52. return loadResult;
  53. }
  54. catch (error) {
  55. throw new error_1.FatalError("Failed to load " + configPath + ": " + error.message, error);
  56. }
  57. }
  58. exports.findConfiguration = findConfiguration;
  59. function findConfigurationPath(suppliedConfigFilePath, inputFilePath) {
  60. if (suppliedConfigFilePath != undefined) {
  61. if (!fs.existsSync(suppliedConfigFilePath)) {
  62. throw new error_1.FatalError("Could not find config file at: " + path.resolve(suppliedConfigFilePath));
  63. }
  64. else {
  65. return path.resolve(suppliedConfigFilePath);
  66. }
  67. }
  68. else {
  69. // convert to dir if it's a file or doesn't exist
  70. var useDirName = false;
  71. try {
  72. var stats = fs.statSync(inputFilePath);
  73. if (stats.isFile()) {
  74. useDirName = true;
  75. }
  76. }
  77. catch (e) {
  78. // throws if file doesn't exist
  79. useDirName = true;
  80. }
  81. if (useDirName) {
  82. inputFilePath = path.dirname(inputFilePath);
  83. }
  84. // search for tslint.json from input file location
  85. var configFilePath = findup(exports.CONFIG_FILENAMES, path.resolve(inputFilePath));
  86. if (configFilePath !== undefined) {
  87. return configFilePath;
  88. }
  89. // search for tslint.json in home directory
  90. var homeDir = os.homedir();
  91. for (var _i = 0, CONFIG_FILENAMES_1 = exports.CONFIG_FILENAMES; _i < CONFIG_FILENAMES_1.length; _i++) {
  92. var configFilename = CONFIG_FILENAMES_1[_i];
  93. configFilePath = path.join(homeDir, configFilename);
  94. if (fs.existsSync(configFilePath)) {
  95. return path.resolve(configFilePath);
  96. }
  97. }
  98. // no path could be found
  99. return undefined;
  100. }
  101. }
  102. exports.findConfigurationPath = findConfigurationPath;
  103. /**
  104. * Find a file by names in a directory or any ancestor directory.
  105. * Will try each filename in filenames before recursing to a parent directory.
  106. * This is case-insensitive, so it can find 'TsLiNt.JsOn' when searching for 'tslint.json'.
  107. */
  108. function findup(filenames, directory) {
  109. while (true) {
  110. var res = findFile(directory);
  111. if (res !== undefined) {
  112. return path.join(directory, res);
  113. }
  114. var parent = path.dirname(directory);
  115. if (parent === directory) {
  116. return undefined;
  117. }
  118. directory = parent;
  119. }
  120. function findFile(cwd) {
  121. var dirFiles = fs.readdirSync(cwd);
  122. var _loop_1 = function (filename) {
  123. var index = dirFiles.indexOf(filename);
  124. if (index > -1) {
  125. return { value: filename };
  126. }
  127. // TODO: remove in v6.0.0
  128. // Try reading in the entire directory and looking for a file with different casing.
  129. var result = dirFiles.find(function (entry) { return entry.toLowerCase() === filename; });
  130. if (result !== undefined) {
  131. error_1.showWarningOnce("Using mixed case " + filename + " is deprecated. Found: " + path.join(cwd, result));
  132. return { value: result };
  133. }
  134. };
  135. for (var _i = 0, filenames_1 = filenames; _i < filenames_1.length; _i++) {
  136. var filename = filenames_1[_i];
  137. var state_1 = _loop_1(filename);
  138. if (typeof state_1 === "object")
  139. return state_1.value;
  140. }
  141. return undefined;
  142. }
  143. }
  144. /**
  145. * Used Node semantics to load a configuration file given configFilePath.
  146. * For example:
  147. * '/path/to/config' will be treated as an absolute path
  148. * './path/to/config' will be treated as a relative path
  149. * 'path/to/config' will attempt to load a to/config file inside a node module named path
  150. * @param configFilePath The configuration to load
  151. * @param originalFilePath (deprecated) The entry point configuration file
  152. * @returns a configuration object for TSLint loaded from the file at configFilePath
  153. */
  154. function loadConfigurationFromPath(configFilePath, _originalFilePath) {
  155. if (configFilePath == undefined) {
  156. return exports.DEFAULT_CONFIG;
  157. }
  158. else {
  159. var resolvedConfigFilePath = resolveConfigurationPath(configFilePath);
  160. var rawConfigFile = readConfigurationFile(resolvedConfigFilePath);
  161. return parseConfigFile(rawConfigFile, path.dirname(resolvedConfigFilePath), readConfigurationFile);
  162. }
  163. }
  164. exports.loadConfigurationFromPath = loadConfigurationFromPath;
  165. /** Reads the configuration file from disk and parses it as raw JSON, YAML or JS depending on the extension. */
  166. function readConfigurationFile(filepath) {
  167. var resolvedConfigFileExt = path.extname(filepath);
  168. if (/\.(json|ya?ml)/.test(resolvedConfigFileExt)) {
  169. var fileContent = fs.readFileSync(filepath, "utf8").replace(/^\uFEFF/, "");
  170. try {
  171. if (resolvedConfigFileExt === ".json") {
  172. return JSON.parse(utils_1.stripComments(fileContent));
  173. }
  174. else {
  175. return yaml.safeLoad(fileContent, {
  176. // Note: yaml.LoadOptions expects a schema value of type "any",
  177. // but this trips up the no-unsafe-any rule.
  178. // tslint:disable-next-line:no-unsafe-any
  179. schema: yaml.JSON_SCHEMA,
  180. strict: true,
  181. });
  182. }
  183. }
  184. catch (e) {
  185. var error = e;
  186. // include the configuration file being parsed in the error since it may differ from the directly referenced config
  187. throw new Error(error.message + " in " + filepath);
  188. }
  189. }
  190. else {
  191. var rawConfigFile = require(filepath);
  192. // tslint:disable-next-line no-dynamic-delete
  193. delete require.cache[filepath];
  194. return rawConfigFile;
  195. }
  196. }
  197. exports.readConfigurationFile = readConfigurationFile;
  198. /**
  199. * Resolve configuration file path or node_module reference
  200. * @param filePath Relative ("./path"), absolute ("/path"), node module ("path"), or built-in ("tslint:path")
  201. */
  202. function resolveConfigurationPath(filePath, relativeTo) {
  203. var matches = filePath.match(BUILT_IN_CONFIG);
  204. var isBuiltInConfig = matches !== null && matches.length > 0;
  205. if (isBuiltInConfig) {
  206. var configName = matches[1];
  207. try {
  208. return require.resolve("./configs/" + configName);
  209. }
  210. catch (err) {
  211. throw new Error(filePath + " is not a built-in config, try \"tslint:recommended\" instead.");
  212. }
  213. }
  214. var basedir = relativeTo !== undefined ? relativeTo : process.cwd();
  215. try {
  216. return resolve.sync(filePath, { basedir: basedir });
  217. }
  218. catch (err) {
  219. try {
  220. return require.resolve(filePath);
  221. }
  222. catch (err) {
  223. throw new Error("Invalid \"extends\" configuration value - could not require \"" + filePath + "\". " +
  224. "Review the Node lookup algorithm (https://nodejs.org/api/modules.html#modules_all_together) " +
  225. "for the approximate method TSLint uses to find the referenced configuration file.");
  226. }
  227. }
  228. }
  229. function extendConfigurationFile(targetConfig, nextConfigSource) {
  230. function combineProperties(targetProperty, nextProperty) {
  231. var combinedProperty = {};
  232. add(targetProperty);
  233. // next config source overwrites the target config object
  234. add(nextProperty);
  235. return combinedProperty;
  236. function add(property) {
  237. if (property !== undefined) {
  238. for (var name in property) {
  239. if (utils_1.hasOwnProperty(property, name)) {
  240. combinedProperty[name] = property[name];
  241. }
  242. }
  243. }
  244. }
  245. }
  246. function combineMaps(target, next) {
  247. var combined = new Map();
  248. target.forEach(function (options, ruleName) {
  249. combined.set(ruleName, options);
  250. });
  251. next.forEach(function (options, ruleName) {
  252. var combinedRule = combined.get(ruleName);
  253. if (combinedRule !== undefined) {
  254. combined.set(ruleName, combineProperties(combinedRule, options));
  255. }
  256. else {
  257. combined.set(ruleName, options);
  258. }
  259. });
  260. return combined;
  261. }
  262. var combinedRulesDirs = targetConfig.rulesDirectory.concat(nextConfigSource.rulesDirectory);
  263. var dedupedRulesDirs = Array.from(new Set(combinedRulesDirs));
  264. return {
  265. extends: [],
  266. jsRules: combineMaps(targetConfig.jsRules, nextConfigSource.jsRules),
  267. linterOptions: combineProperties(targetConfig.linterOptions, nextConfigSource.linterOptions),
  268. rules: combineMaps(targetConfig.rules, nextConfigSource.rules),
  269. rulesDirectory: dedupedRulesDirs,
  270. };
  271. }
  272. exports.extendConfigurationFile = extendConfigurationFile;
  273. /**
  274. * returns the absolute path (contrary to what the name implies)
  275. *
  276. * @deprecated use `path.resolve` instead
  277. */
  278. function getRelativePath(directory, relativeTo) {
  279. if (directory != undefined) {
  280. var basePath = relativeTo !== undefined ? relativeTo : process.cwd();
  281. return path.resolve(basePath, directory);
  282. }
  283. return undefined;
  284. }
  285. exports.getRelativePath = getRelativePath;
  286. // check if directory should be used as path or if it should be resolved like a module
  287. // matches if directory starts with '/', './', '../', 'node_modules/' or equals '.' or '..'
  288. function useAsPath(directory) {
  289. return /^(?:\.?\.?(?:\/|$)|node_modules\/)/.test(directory);
  290. }
  291. exports.useAsPath = useAsPath;
  292. /**
  293. * @param directories A path(s) to a directory of custom rules
  294. * @param relativeTo A path that directories provided are relative to.
  295. * For example, if the directories come from a tslint.json file, this path
  296. * should be the path to the tslint.json file.
  297. * @return An array of absolute paths to directories potentially containing rules
  298. */
  299. function getRulesDirectories(directories, relativeTo) {
  300. return utils_1.arrayify(directories)
  301. .map(function (dir) {
  302. if (!useAsPath(dir)) {
  303. try {
  304. return path.dirname(resolve.sync(dir, { basedir: relativeTo }));
  305. }
  306. catch (err) {
  307. // swallow error and fallback to using directory as path
  308. }
  309. }
  310. var absolutePath = relativeTo === undefined ? path.resolve(dir) : path.resolve(relativeTo, dir);
  311. if (absolutePath !== undefined) {
  312. if (!fs.existsSync(absolutePath)) {
  313. throw new error_1.FatalError("Could not find custom rule directory: " + dir);
  314. }
  315. }
  316. return absolutePath;
  317. });
  318. }
  319. exports.getRulesDirectories = getRulesDirectories;
  320. /**
  321. * Parses the options of a single rule and upgrades legacy settings such as `true`, `[true, "option"]`
  322. *
  323. * @param ruleConfigValue The raw option setting of a rule
  324. */
  325. function parseRuleOptions(ruleConfigValue, rawDefaultRuleSeverity) {
  326. var ruleArguments;
  327. var defaultRuleSeverity = "error";
  328. if (rawDefaultRuleSeverity !== undefined) {
  329. switch (rawDefaultRuleSeverity.toLowerCase()) {
  330. case "warn":
  331. case "warning":
  332. defaultRuleSeverity = "warning";
  333. break;
  334. case "off":
  335. case "none":
  336. defaultRuleSeverity = "off";
  337. break;
  338. default:
  339. defaultRuleSeverity = "error";
  340. }
  341. }
  342. var ruleSeverity = defaultRuleSeverity;
  343. if (ruleConfigValue == undefined) {
  344. ruleArguments = [];
  345. ruleSeverity = "off";
  346. }
  347. else if (Array.isArray(ruleConfigValue)) {
  348. if (ruleConfigValue.length > 0) {
  349. // old style: array
  350. ruleArguments = ruleConfigValue.slice(1);
  351. ruleSeverity = ruleConfigValue[0] === true ? defaultRuleSeverity : "off";
  352. }
  353. }
  354. else if (typeof ruleConfigValue === "boolean") {
  355. // old style: boolean
  356. ruleArguments = [];
  357. ruleSeverity = ruleConfigValue ? defaultRuleSeverity : "off";
  358. }
  359. else if (typeof ruleConfigValue === "object") {
  360. if (ruleConfigValue.severity !== undefined) {
  361. switch (ruleConfigValue.severity.toLowerCase()) {
  362. case "default":
  363. ruleSeverity = defaultRuleSeverity;
  364. break;
  365. case "error":
  366. ruleSeverity = "error";
  367. break;
  368. case "warn":
  369. case "warning":
  370. ruleSeverity = "warning";
  371. break;
  372. case "off":
  373. case "none":
  374. ruleSeverity = "off";
  375. break;
  376. default:
  377. console.warn("Invalid severity level: " + ruleConfigValue.severity);
  378. ruleSeverity = defaultRuleSeverity;
  379. }
  380. }
  381. if (ruleConfigValue.options != undefined) {
  382. ruleArguments = utils_1.arrayify(ruleConfigValue.options);
  383. }
  384. }
  385. return {
  386. ruleArguments: ruleArguments,
  387. ruleSeverity: ruleSeverity,
  388. };
  389. }
  390. /**
  391. * Parses a config file and normalizes legacy config settings.
  392. * If `configFileDir` and `readConfig` are provided, this function will load all base configs and reduce them to the final configuration.
  393. *
  394. * @param configFile The raw object read from the JSON of a config file
  395. * @param configFileDir The directory of the config file
  396. * @param readConfig Will be used to load all base configurations while parsing. The function is called with the resolved path.
  397. */
  398. function parseConfigFile(configFile, configFileDir, readConfig) {
  399. var defaultSeverity = configFile.defaultSeverity;
  400. if (readConfig === undefined || configFileDir === undefined) {
  401. return parse(configFile, configFileDir);
  402. }
  403. return loadExtendsRecursive(configFile, configFileDir)
  404. .map(function (_a) {
  405. var dir = _a.dir, config = _a.config;
  406. return parse(config, dir);
  407. })
  408. .reduce(extendConfigurationFile, exports.EMPTY_CONFIG);
  409. /** Read files in order, depth first, and assign `defaultSeverity` (last config in extends wins). */
  410. function loadExtendsRecursive(raw, dir) {
  411. var configs = [];
  412. for (var _i = 0, _a = utils_1.arrayify(raw.extends); _i < _a.length; _i++) {
  413. var relativePath = _a[_i];
  414. var resolvedPath = resolveConfigurationPath(relativePath, dir);
  415. var extendedRaw = readConfig(resolvedPath);
  416. configs.push.apply(configs, loadExtendsRecursive(extendedRaw, path.dirname(resolvedPath)));
  417. }
  418. if (raw.defaultSeverity !== undefined) {
  419. defaultSeverity = raw.defaultSeverity;
  420. }
  421. configs.push({ dir: dir, config: raw });
  422. return configs;
  423. }
  424. function parse(config, dir) {
  425. return {
  426. extends: utils_1.arrayify(config.extends),
  427. jsRules: parseRules(config.jsRules),
  428. linterOptions: parseLinterOptions(config.linterOptions, dir),
  429. rules: parseRules(config.rules),
  430. rulesDirectory: getRulesDirectories(config.rulesDirectory, dir),
  431. };
  432. }
  433. function parseRules(config) {
  434. var map = new Map();
  435. if (config !== undefined) {
  436. for (var ruleName in config) {
  437. if (utils_1.hasOwnProperty(config, ruleName)) {
  438. map.set(ruleName, parseRuleOptions(config[ruleName], defaultSeverity));
  439. }
  440. }
  441. }
  442. return map;
  443. }
  444. function parseLinterOptions(raw, dir) {
  445. if (raw === undefined || raw.exclude === undefined) {
  446. return {};
  447. }
  448. return {
  449. exclude: utils_1.arrayify(raw.exclude).map(function (pattern) { return dir === undefined ? path.resolve(pattern) : path.resolve(dir, pattern); }),
  450. };
  451. }
  452. }
  453. exports.parseConfigFile = parseConfigFile;
  454. /**
  455. * Fills in default values for `IOption` properties and outputs an array of `IOption`
  456. */
  457. function convertRuleOptions(ruleConfiguration) {
  458. var output = [];
  459. ruleConfiguration.forEach(function (_a, ruleName) {
  460. var ruleArguments = _a.ruleArguments, ruleSeverity = _a.ruleSeverity;
  461. var options = {
  462. disabledIntervals: [],
  463. ruleArguments: ruleArguments != undefined ? ruleArguments : [],
  464. ruleName: ruleName,
  465. ruleSeverity: ruleSeverity != undefined ? ruleSeverity : "error",
  466. };
  467. output.push(options);
  468. });
  469. return output;
  470. }
  471. exports.convertRuleOptions = convertRuleOptions;