"use strict"; /** * @license * Copyright 2013 Palantir Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var fs = require("fs"); var yaml = require("js-yaml"); var minimatch_1 = require("minimatch"); var os = require("os"); var path = require("path"); var error_1 = require("./error"); var ruleLoader_1 = require("./ruleLoader"); var utils_1 = require("./utils"); // Note: eslint prefers yaml over json, while tslint prefers json over yaml // for backward-compatibility. exports.JSON_CONFIG_FILENAME = "tslint.json"; /** @deprecated use `JSON_CONFIG_FILENAME` or `CONFIG_FILENAMES` instead. */ exports.CONFIG_FILENAME = exports.JSON_CONFIG_FILENAME; exports.CONFIG_FILENAMES = [exports.JSON_CONFIG_FILENAME, "tslint.yaml", "tslint.yml"]; exports.DEFAULT_CONFIG = { defaultSeverity: "error", extends: ["tslint:recommended"], jsRules: new Map(), rules: new Map(), rulesDirectory: [], }; exports.EMPTY_CONFIG = { defaultSeverity: "error", extends: [], jsRules: new Map(), rules: new Map(), rulesDirectory: [], }; var BUILT_IN_CONFIG = /^tslint:(.*)$/; function findConfiguration(configFile, inputFilePath) { var configPath = findConfigurationPath(configFile, inputFilePath); var loadResult = { path: configPath }; try { loadResult.results = loadConfigurationFromPath(configPath); return loadResult; } catch (error) { throw new error_1.FatalError("Failed to load " + configPath + ": " + error.message, error); } } exports.findConfiguration = findConfiguration; function findConfigurationPath(suppliedConfigFilePath, inputFilePath) { if (suppliedConfigFilePath != undefined) { if (!fs.existsSync(suppliedConfigFilePath)) { throw new error_1.FatalError("Could not find config file at: " + path.resolve(suppliedConfigFilePath)); } else { return path.resolve(suppliedConfigFilePath); } } else { // convert to dir if it's a file or doesn't exist var useDirName = false; try { var stats = fs.statSync(inputFilePath); if (stats.isFile()) { useDirName = true; } } catch (e) { // throws if file doesn't exist useDirName = true; } if (useDirName) { inputFilePath = path.dirname(inputFilePath); } // search for tslint.json from input file location var configFilePath = findup(exports.CONFIG_FILENAMES, path.resolve(inputFilePath)); if (configFilePath !== undefined) { return configFilePath; } // search for tslint.json in home directory var homeDir = os.homedir(); for (var _i = 0, CONFIG_FILENAMES_1 = exports.CONFIG_FILENAMES; _i < CONFIG_FILENAMES_1.length; _i++) { var configFilename = CONFIG_FILENAMES_1[_i]; configFilePath = path.join(homeDir, configFilename); if (fs.existsSync(configFilePath)) { return path.resolve(configFilePath); } } // no path could be found return undefined; } } exports.findConfigurationPath = findConfigurationPath; /** * Find a file by names in a directory or any ancestor directory. * Will try each filename in filenames before recursing to a parent directory. * This is case-insensitive, so it can find 'TsLiNt.JsOn' when searching for 'tslint.json'. */ function findup(filenames, directory) { while (true) { var res = findFile(directory); if (res !== undefined) { return path.join(directory, res); } var parent = path.dirname(directory); if (parent === directory) { return undefined; } directory = parent; } function findFile(cwd) { var dirFiles = fs.readdirSync(cwd); var _loop_1 = function (filename) { var index = dirFiles.indexOf(filename); if (index > -1) { return { value: filename }; } // TODO: remove in v6.0.0 // Try reading in the entire directory and looking for a file with different casing. var result = dirFiles.find(function (entry) { return entry.toLowerCase() === filename; }); if (result !== undefined) { error_1.showWarningOnce("Using mixed case " + filename + " is deprecated. Found: " + path.join(cwd, result)); return { value: result }; } }; for (var _i = 0, filenames_1 = filenames; _i < filenames_1.length; _i++) { var filename = filenames_1[_i]; var state_1 = _loop_1(filename); if (typeof state_1 === "object") return state_1.value; } return undefined; } } /** * Used Node semantics to load a configuration file given configFilePath. * For example: * '/path/to/config' will be treated as an absolute path * './path/to/config' will be treated as a relative path * 'path/to/config' will attempt to load a to/config file inside a node module named path * @param configFilePath The configuration to load * @param originalFilePath (deprecated) The entry point configuration file * @returns a configuration object for TSLint loaded from the file at configFilePath */ function loadConfigurationFromPath(configFilePath, _originalFilePath) { if (configFilePath == undefined) { return exports.DEFAULT_CONFIG; } else { var resolvedConfigFilePath = resolveConfigurationPath(configFilePath); var rawConfigFile = readConfigurationFile(resolvedConfigFilePath); return parseConfigFile(rawConfigFile, path.dirname(resolvedConfigFilePath), readConfigurationFile); } } exports.loadConfigurationFromPath = loadConfigurationFromPath; /** Reads the configuration file from disk and parses it as raw JSON, YAML or JS depending on the extension. */ function readConfigurationFile(filepath) { var resolvedConfigFileExt = path.extname(filepath); if (/\.(json|ya?ml)/.test(resolvedConfigFileExt)) { var fileContent = fs.readFileSync(filepath, "utf8").replace(/^\uFEFF/, ""); try { if (resolvedConfigFileExt === ".json") { return JSON.parse(utils_1.stripComments(fileContent)); } else { return yaml.safeLoad(fileContent); } } catch (e) { var error = e; // include the configuration file being parsed in the error since it may differ from the directly referenced config throw new Error(error.message + " in " + filepath); } } else { var rawConfigFile = require(filepath); // tslint:disable-next-line no-dynamic-delete delete require.cache[filepath]; return rawConfigFile; } } exports.readConfigurationFile = readConfigurationFile; /** * Resolve configuration file path or node_module reference * @param filePath Relative ("./path"), absolute ("/path"), node module ("path"), or built-in ("tslint:path") */ function resolveConfigurationPath(filePath, relativeTo) { var matches = filePath.match(BUILT_IN_CONFIG); var isBuiltInConfig = matches !== null && matches.length > 0; if (isBuiltInConfig) { var configName = matches[1]; try { return require.resolve("./configs/" + configName); } catch (err) { throw new Error(filePath + " is not a built-in config, try \"tslint:recommended\" instead."); } } var basedir = relativeTo !== undefined ? relativeTo : process.cwd(); try { var resolvedPackagePath = utils_1.tryResolvePackage(filePath, basedir); if (resolvedPackagePath === undefined) { resolvedPackagePath = require.resolve(filePath); } return resolvedPackagePath; } catch (err) { throw new Error("Invalid \"extends\" configuration value - could not require \"" + filePath + "\". " + "Review the Node lookup algorithm (https://nodejs.org/api/modules.html#modules_all_together) " + "for the approximate method TSLint uses to find the referenced configuration file."); } } function extendConfigurationFile(targetConfig, nextConfigSource) { function combineProperties(targetProperty, nextProperty) { var combinedProperty = {}; add(targetProperty); // next config source overwrites the target config object add(nextProperty); return combinedProperty; function add(property) { if (property !== undefined) { for (var _i = 0, _a = Object.keys(property); _i < _a.length; _i++) { var name = _a[_i]; combinedProperty[name] = property[name]; } } } } function combineMaps(target, next) { var combined = new Map(); target.forEach(function (options, ruleName) { combined.set(ruleName, options); }); next.forEach(function (options, ruleName) { var combinedRule = combined.get(ruleName); if (combinedRule !== undefined) { combined.set(ruleName, combineProperties(combinedRule, options)); } else { combined.set(ruleName, options); } }); return combined; } var combinedRulesDirs = targetConfig.rulesDirectory.concat(nextConfigSource.rulesDirectory); var dedupedRulesDirs = Array.from(new Set(combinedRulesDirs)); return { extends: [], jsRules: combineMaps(targetConfig.jsRules, nextConfigSource.jsRules), linterOptions: combineProperties(targetConfig.linterOptions, nextConfigSource.linterOptions), rules: combineMaps(targetConfig.rules, nextConfigSource.rules), rulesDirectory: dedupedRulesDirs, }; } exports.extendConfigurationFile = extendConfigurationFile; /** * returns the absolute path (contrary to what the name implies) * * @deprecated use `path.resolve` instead */ // tslint:disable-next-line no-null-undefined-union function getRelativePath(directory, relativeTo) { if (directory != undefined) { var basePath = relativeTo !== undefined ? relativeTo : process.cwd(); return path.resolve(basePath, directory); } return undefined; } exports.getRelativePath = getRelativePath; // check if directory should be used as path or if it should be resolved like a module // matches if directory starts with '/', './', '../', 'node_modules/' or equals '.' or '..' function useAsPath(directory) { return /^(?:\.?\.?(?:\/|$)|node_modules\/)/.test(directory); } exports.useAsPath = useAsPath; /** * @param directories A path(s) to a directory of custom rules * @param relativeTo A path that directories provided are relative to. * For example, if the directories come from a tslint.json file, this path * should be the path to the tslint.json file. * @return An array of absolute paths to directories potentially containing rules */ function getRulesDirectories(directories, relativeTo) { return utils_1.arrayify(directories).map(function (dir) { if (!useAsPath(dir)) { var resolvedPackagePath = utils_1.tryResolvePackage(dir, relativeTo); if (resolvedPackagePath !== undefined) { return path.dirname(resolvedPackagePath); } } var absolutePath = relativeTo === undefined ? path.resolve(dir) : path.resolve(relativeTo, dir); if (absolutePath !== undefined) { if (!fs.existsSync(absolutePath)) { throw new error_1.FatalError("Could not find custom rule directory: " + dir); } } return absolutePath; }); } exports.getRulesDirectories = getRulesDirectories; /** * Parses the options of a single rule and upgrades legacy settings such as `true`, `[true, "option"]` * * @param ruleConfigValue The raw option setting of a rule */ function parseRuleOptions(ruleConfigValue, rawDefaultRuleSeverity) { var ruleArguments; var defaultRuleSeverity = "error"; if (rawDefaultRuleSeverity !== undefined) { switch (rawDefaultRuleSeverity.toLowerCase()) { case "warn": case "warning": defaultRuleSeverity = "warning"; break; case "off": case "none": defaultRuleSeverity = "off"; break; default: defaultRuleSeverity = "error"; } } var ruleSeverity = defaultRuleSeverity; if (ruleConfigValue == undefined) { ruleArguments = []; ruleSeverity = "off"; } else if (Array.isArray(ruleConfigValue)) { if (ruleConfigValue.length > 0) { // old style: array ruleArguments = ruleConfigValue.slice(1); ruleSeverity = ruleConfigValue[0] === true ? defaultRuleSeverity : "off"; } } else if (typeof ruleConfigValue === "boolean") { // old style: boolean ruleArguments = []; ruleSeverity = ruleConfigValue ? defaultRuleSeverity : "off"; } else if (typeof ruleConfigValue === "object") { if (ruleConfigValue.severity !== undefined) { switch (ruleConfigValue.severity.toLowerCase()) { case "default": ruleSeverity = defaultRuleSeverity; break; case "error": ruleSeverity = "error"; break; case "warn": case "warning": ruleSeverity = "warning"; break; case "off": case "none": ruleSeverity = "off"; break; default: console.warn("Invalid severity level: " + ruleConfigValue.severity); ruleSeverity = defaultRuleSeverity; } } if (ruleConfigValue.options != undefined) { ruleArguments = utils_1.arrayify(ruleConfigValue.options); } } return { ruleArguments: ruleArguments, ruleSeverity: ruleSeverity, }; } /** * Parses a config file and normalizes legacy config settings. * If `configFileDir` and `readConfig` are provided, this function will load all base configs and reduce them to the final configuration. * * @param configFile The raw object read from the JSON of a config file * @param configFileDir The directory of the config file * @param readConfig Will be used to load all base configurations while parsing. The function is called with the resolved path. */ function parseConfigFile(configFile, configFileDir, readConfig) { var defaultSeverity = configFile.defaultSeverity; if (readConfig === undefined || configFileDir === undefined) { return parse(configFile, configFileDir); } return loadExtendsRecursive(configFile, configFileDir) .map(function (_a) { var dir = _a.dir, config = _a.config; return parse(config, dir); }) .reduce(extendConfigurationFile, exports.EMPTY_CONFIG); /** Read files in order, depth first, and assign `defaultSeverity` (last config in extends wins). */ function loadExtendsRecursive(raw, dir) { var configs = []; for (var _i = 0, _a = utils_1.arrayify(raw.extends); _i < _a.length; _i++) { var relativePath = _a[_i]; var resolvedPath = resolveConfigurationPath(relativePath, dir); var extendedRaw = readConfig(resolvedPath); configs.push.apply(configs, loadExtendsRecursive(extendedRaw, path.dirname(resolvedPath))); } if (raw.defaultSeverity !== undefined) { defaultSeverity = raw.defaultSeverity; } configs.push({ dir: dir, config: raw }); return configs; } function parse(config, dir) { var rulesDirectory = getRulesDirectories(config.rulesDirectory, dir); var rules = parseRules(config.rules); var jsRules = typeof config.jsRules === "boolean" ? filterValidJsRules(rules, config.jsRules, rulesDirectory) : parseRules(config.jsRules); return { extends: utils_1.arrayify(config.extends), jsRules: jsRules, linterOptions: parseLinterOptions(config.linterOptions, dir), rules: rules, rulesDirectory: rulesDirectory, }; } function parseRules(config) { var map = new Map(); if (config !== undefined) { for (var _i = 0, _a = Object.keys(config); _i < _a.length; _i++) { var ruleName = _a[_i]; map.set(ruleName, parseRuleOptions(config[ruleName], defaultSeverity)); } } return map; } function filterValidJsRules(rules, copyRulestoJsRules, rulesDirectory) { if (copyRulestoJsRules === void 0) { copyRulestoJsRules = false; } var validJsRules = new Map(); if (copyRulestoJsRules) { rules.forEach(function (ruleOptions, ruleName) { var Rule = ruleLoader_1.findRule(ruleName, rulesDirectory); if (Rule !== undefined && (Rule.metadata === undefined || !Rule.metadata.typescriptOnly)) { validJsRules.set(ruleName, ruleOptions); } }); } return validJsRules; } function parseLinterOptions(raw, dir) { if (raw === undefined) { return {}; } return tslib_1.__assign({}, (raw.exclude !== undefined ? { exclude: utils_1.arrayify(raw.exclude).map(function (pattern) { return dir === undefined ? path.resolve(pattern) : path.resolve(dir, pattern); }), } : {}), (raw.format !== undefined ? { format: raw.format, } : {})); } } exports.parseConfigFile = parseConfigFile; /** * Fills in default values for `IOption` properties and outputs an array of `IOption` */ function convertRuleOptions(ruleConfiguration) { var output = []; ruleConfiguration.forEach(function (_a, ruleName) { var ruleArguments = _a.ruleArguments, ruleSeverity = _a.ruleSeverity; var options = { disabledIntervals: [], ruleArguments: ruleArguments != undefined ? ruleArguments : [], ruleName: ruleName, ruleSeverity: ruleSeverity != undefined ? ruleSeverity : "error", }; output.push(options); }); return output; } exports.convertRuleOptions = convertRuleOptions; function isFileExcluded(filepath, configFile) { if (configFile === undefined || configFile.linterOptions == undefined || configFile.linterOptions.exclude == undefined) { return false; } var fullPath = path.resolve(filepath); return configFile.linterOptions.exclude.some(function (pattern) { return new minimatch_1.Minimatch(pattern).match(fullPath); }); } exports.isFileExcluded = isFileExcluded; function stringifyConfiguration(configFile) { return JSON.stringify({ extends: configFile.extends, jsRules: convertRulesMapToObject(configFile.jsRules), linterOptions: configFile.linterOptions, rules: convertRulesMapToObject(configFile.rules), rulesDirectory: configFile.rulesDirectory, }, undefined, 2); } exports.stringifyConfiguration = stringifyConfiguration; function convertRulesMapToObject(rules) { return Array.from(rules).reduce(function (result, _a) { var _b; var key = _a[0], value = _a[1]; return (tslib_1.__assign({}, result, (_b = {}, _b[key] = value, _b))); }, {}); }