440 lines
17 KiB
JavaScript
440 lines
17 KiB
JavaScript
"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 fs = require("fs");
|
|
var path = require("path");
|
|
var resolve = require("resolve");
|
|
var error_1 = require("./error");
|
|
var utils_1 = require("./utils");
|
|
exports.CONFIG_FILENAME = "tslint.json";
|
|
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:(.*)$/;
|
|
/**
|
|
* Searches for a TSLint configuration and returns the data from the config.
|
|
* @param configFile A path to a config file, this can be null if the location of a config is not known
|
|
* @param inputFilePath A path containing the current file being linted. This is the starting location
|
|
* of the search for a configuration.
|
|
* @returns Load status for a TSLint configuration object
|
|
*/
|
|
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;
|
|
/**
|
|
* Searches for a TSLint configuration and returns the path to it.
|
|
* Could return undefined if not configuration is found.
|
|
* @param suppliedConfigFilePath A path to an known config file supplied by a user. Pass null here if
|
|
* the location of the config file is not known and you want to search for one.
|
|
* @param inputFilePath A path to the current file being linted. This is the starting location
|
|
* of the search for a configuration.
|
|
* @returns An absolute path to a tslint.json file
|
|
* or undefined if neither can be found.
|
|
*/
|
|
function findConfigurationPath(suppliedConfigFilePath, inputFilePath) {
|
|
if (suppliedConfigFilePath != null) {
|
|
if (!fs.existsSync(suppliedConfigFilePath)) {
|
|
throw new Error("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_FILENAME, inputFilePath);
|
|
if (configFilePath !== undefined) {
|
|
return path.resolve(configFilePath);
|
|
}
|
|
// search for tslint.json in home directory
|
|
var homeDir = getHomeDir();
|
|
if (homeDir != null) {
|
|
configFilePath = path.join(homeDir, exports.CONFIG_FILENAME);
|
|
if (fs.existsSync(configFilePath)) {
|
|
return path.resolve(configFilePath);
|
|
}
|
|
}
|
|
// no path could be found
|
|
return undefined;
|
|
}
|
|
}
|
|
exports.findConfigurationPath = findConfigurationPath;
|
|
/**
|
|
* Find a file by name in a directory or any ancestory directory.
|
|
* This is case-insensitive, so it can find 'TsLiNt.JsOn' when searching for 'tslint.json'.
|
|
*/
|
|
function findup(filename, 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) {
|
|
if (fs.existsSync(path.join(cwd, filename))) {
|
|
return filename;
|
|
}
|
|
// TODO: remove in v6.0.0
|
|
// Try reading in the entire directory and looking for a file with different casing.
|
|
var filenameLower = filename.toLowerCase();
|
|
var result = fs.readdirSync(cwd).find(function (entry) { return entry.toLowerCase() === filenameLower; });
|
|
if (result !== undefined) {
|
|
error_1.showWarningOnce("Using mixed case tslint.json is deprecated. Found: " + path.join(cwd, result));
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
/**
|
|
* 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 The entry point configuration file
|
|
* @returns a configuration object for TSLint loaded from the file at configFilePath
|
|
*/
|
|
function loadConfigurationFromPath(configFilePath, originalFilePath) {
|
|
if (originalFilePath === void 0) { originalFilePath = configFilePath; }
|
|
if (configFilePath == null) {
|
|
return exports.DEFAULT_CONFIG;
|
|
}
|
|
else {
|
|
var resolvedConfigFilePath = resolveConfigurationPath(configFilePath);
|
|
var rawConfigFile = void 0;
|
|
if (path.extname(resolvedConfigFilePath) === ".json") {
|
|
var fileContent = utils_1.stripComments(fs.readFileSync(resolvedConfigFilePath)
|
|
.toString()
|
|
.replace(/^\uFEFF/, ""));
|
|
try {
|
|
rawConfigFile = JSON.parse(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 configFilePath === originalFilePath ? error : new Error(error.message + " in " + configFilePath);
|
|
}
|
|
}
|
|
else {
|
|
rawConfigFile = require(resolvedConfigFilePath);
|
|
delete require.cache[resolvedConfigFilePath];
|
|
}
|
|
var configFileDir_1 = path.dirname(resolvedConfigFilePath);
|
|
var configFile = parseConfigFile(rawConfigFile, configFileDir_1);
|
|
// load configurations, in order, using their identifiers or relative paths
|
|
// apply the current configuration last by placing it last in this array
|
|
var configs = configFile.extends.map(function (name) {
|
|
var nextConfigFilePath = resolveConfigurationPath(name, configFileDir_1);
|
|
return loadConfigurationFromPath(nextConfigFilePath, originalFilePath);
|
|
}).concat([configFile]);
|
|
return configs.reduce(extendConfigurationFile, exports.EMPTY_CONFIG);
|
|
}
|
|
}
|
|
exports.loadConfigurationFromPath = loadConfigurationFromPath;
|
|
/**
|
|
* 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 {
|
|
return resolve.sync(filePath, { basedir: basedir });
|
|
}
|
|
catch (err) {
|
|
try {
|
|
return require.resolve(filePath);
|
|
}
|
|
catch (err) {
|
|
// tslint:disable-next-line prefer-template (fixed in 5.3)
|
|
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 name in property) {
|
|
if (utils_1.hasOwnProperty(property, name)) {
|
|
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 != null) {
|
|
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;
|
|
function getHomeDir() {
|
|
var environment = global.process.env;
|
|
var paths = [
|
|
environment.USERPROFILE,
|
|
environment.HOME,
|
|
environment.HOMEPATH,
|
|
environment.HOMEDRIVE + environment.HOMEPATH,
|
|
];
|
|
for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) {
|
|
var homePath = paths_1[_i];
|
|
if (homePath != null && fs.existsSync(homePath)) {
|
|
return homePath;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
// returns the absolute path (contrary to what the name implies)
|
|
function getRelativePath(directory, relativeTo) {
|
|
if (directory != null) {
|
|
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)) {
|
|
try {
|
|
return path.dirname(resolve.sync(dir, { basedir: relativeTo }));
|
|
}
|
|
catch (err) {
|
|
// swallow error and fallback to using directory as path
|
|
}
|
|
}
|
|
var absolutePath = getRelativePath(dir, relativeTo);
|
|
if (absolutePath != null) {
|
|
if (!fs.existsSync(absolutePath)) {
|
|
throw new Error("Could not find custom rule directory: " + dir);
|
|
}
|
|
}
|
|
return absolutePath;
|
|
})
|
|
.filter(function (dir) { return dir !== undefined; });
|
|
}
|
|
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 == null) {
|
|
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 != null) {
|
|
ruleArguments = utils_1.arrayify(ruleConfigValue.options);
|
|
}
|
|
}
|
|
return {
|
|
ruleArguments: ruleArguments,
|
|
ruleSeverity: ruleSeverity,
|
|
};
|
|
}
|
|
/**
|
|
* Parses a config file and normalizes legacy config settings
|
|
*
|
|
* @param configFile The raw object read from the JSON of a config file
|
|
* @param configFileDir The directory of the config file
|
|
*/
|
|
function parseConfigFile(configFile, configFileDir) {
|
|
return {
|
|
extends: utils_1.arrayify(configFile.extends),
|
|
jsRules: parseRules(configFile.jsRules),
|
|
linterOptions: configFile.linterOptions !== undefined ? configFile.linterOptions : {},
|
|
rules: parseRules(configFile.rules),
|
|
rulesDirectory: getRulesDirectories(configFile.rulesDirectory, configFileDir),
|
|
};
|
|
function parseRules(config) {
|
|
var map = new Map();
|
|
if (config !== undefined) {
|
|
for (var ruleName in config) {
|
|
if (utils_1.hasOwnProperty(config, ruleName)) {
|
|
map.set(ruleName, parseRuleOptions(config[ruleName], configFile.defaultSeverity));
|
|
}
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
}
|
|
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 != null ? ruleArguments : [],
|
|
ruleName: ruleName,
|
|
ruleSeverity: ruleSeverity != null ? ruleSeverity : "error",
|
|
};
|
|
output.push(options);
|
|
});
|
|
return output;
|
|
}
|
|
exports.convertRuleOptions = convertRuleOptions;
|