/** * @fileoverview Responsible for loading ignore config files and managing ignore patterns * @author Jonathan Rajavuori */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const fs = require("fs"), path = require("path"), ignore = require("ignore"), pathUtil = require("./util/path-util"); const debug = require("debug")("eslint:ignored-paths"); //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ const ESLINT_IGNORE_FILENAME = ".eslintignore"; /** * Adds `"*"` at the end of `"node_modules/"`, * so that subtle directories could be re-included by .gitignore patterns * such as `"!node_modules/should_not_ignored"` */ const DEFAULT_IGNORE_DIRS = [ "/node_modules/*", "/bower_components/*" ]; const DEFAULT_OPTIONS = { dotfiles: false, cwd: process.cwd() }; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Find a file in the current directory. * @param {string} cwd Current working directory * @param {string} name File name * @returns {string} Path of ignore file or an empty string. */ function findFile(cwd, name) { const ignoreFilePath = path.resolve(cwd, name); return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : ""; } /** * Find an ignore file in the current directory. * @param {string} cwd Current working directory * @returns {string} Path of ignore file or an empty string. */ function findIgnoreFile(cwd) { return findFile(cwd, ESLINT_IGNORE_FILENAME); } /** * Find an package.json file in the current directory. * @param {string} cwd Current working directory * @returns {string} Path of package.json file or an empty string. */ function findPackageJSONFile(cwd) { return findFile(cwd, "package.json"); } /** * Merge options with defaults * @param {Object} options Options to merge with DEFAULT_OPTIONS constant * @returns {Object} Merged options */ function mergeDefaultOptions(options) { return Object.assign({}, DEFAULT_OPTIONS, options); } //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * IgnoredPaths class */ class IgnoredPaths { /** * @param {Object} providedOptions object containing 'ignore', 'ignorePath' and 'patterns' properties */ constructor(providedOptions) { const options = mergeDefaultOptions(providedOptions); this.cache = {}; /** * add pattern to node-ignore instance * @param {Object} ig, instance of node-ignore * @param {string} pattern, pattern do add to ig * @returns {array} raw ignore rules */ function addPattern(ig, pattern) { return ig.addPattern(pattern); } this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []); this.baseDir = options.cwd; this.ig = { custom: ignore(), default: ignore() }; /* * Add a way to keep track of ignored files. This was present in node-ignore * 2.x, but dropped for now as of 3.0.10. */ this.ig.custom.ignoreFiles = []; this.ig.default.ignoreFiles = []; if (options.dotfiles !== true) { /* * ignore files beginning with a dot, but not files in a parent or * ancestor directory (which in relative format will begin with `../`). */ addPattern(this.ig.default, [".*", "!../"]); } addPattern(this.ig.default, this.defaultPatterns); if (options.ignore !== false) { let ignorePath; if (options.ignorePath) { debug("Using specific ignore file"); try { fs.statSync(options.ignorePath); ignorePath = options.ignorePath; } catch (e) { e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`; throw e; } } else { debug(`Looking for ignore file in ${options.cwd}`); ignorePath = findIgnoreFile(options.cwd); try { fs.statSync(ignorePath); debug(`Loaded ignore file ${ignorePath}`); } catch (e) { debug("Could not find ignore file in cwd"); this.options = options; } } if (ignorePath) { debug(`Adding ${ignorePath}`); this.baseDir = path.dirname(path.resolve(options.cwd, ignorePath)); this.addIgnoreFile(this.ig.custom, ignorePath); this.addIgnoreFile(this.ig.default, ignorePath); } else { try { // if the ignoreFile does not exist, check package.json for eslintIgnore const packageJSONPath = findPackageJSONFile(options.cwd); if (packageJSONPath) { let packageJSONOptions; try { packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8")); } catch (e) { debug("Could not read package.json file to check eslintIgnore property"); throw e; } if (packageJSONOptions.eslintIgnore) { if (Array.isArray(packageJSONOptions.eslintIgnore)) { packageJSONOptions.eslintIgnore.forEach(pattern => { addPattern(this.ig.custom, pattern); addPattern(this.ig.default, pattern); }); } else { throw new TypeError("Package.json eslintIgnore property requires an array of paths"); } } } } catch (e) { debug("Could not find package.json to check eslintIgnore property"); throw e; } } if (options.ignorePattern) { addPattern(this.ig.custom, options.ignorePattern); addPattern(this.ig.default, options.ignorePattern); } } this.options = options; } /** * read ignore filepath * @param {string} filePath, file to add to ig * @returns {array} raw ignore rules */ readIgnoreFile(filePath) { if (typeof this.cache[filePath] === "undefined") { this.cache[filePath] = fs.readFileSync(filePath, "utf8"); } return this.cache[filePath]; } /** * add ignore file to node-ignore instance * @param {Object} ig, instance of node-ignore * @param {string} filePath, file to add to ig * @returns {array} raw ignore rules */ addIgnoreFile(ig, filePath) { ig.ignoreFiles.push(filePath); return ig.add(this.readIgnoreFile(filePath)); } /** * Determine whether a file path is included in the default or custom ignore patterns * @param {string} filepath Path to check * @param {string} [category=null] check 'default', 'custom' or both (null) * @returns {boolean} true if the file path matches one or more patterns, false otherwise */ contains(filepath, category) { let result = false; const absolutePath = path.resolve(this.options.cwd, filepath); const relativePath = pathUtil.getRelativePath(absolutePath, this.baseDir); if ((typeof category === "undefined") || (category === "default")) { result = result || (this.ig.default.filter([relativePath]).length === 0); } if ((typeof category === "undefined") || (category === "custom")) { result = result || (this.ig.custom.filter([relativePath]).length === 0); } return result; } /** * Returns a list of dir patterns for glob to ignore * @returns {function()} method to check whether a folder should be ignored by glob. */ getIgnoredFoldersGlobChecker() { const ig = ignore().add(DEFAULT_IGNORE_DIRS); if (this.options.dotfiles !== true) { // Ignore hidden folders. (This cannot be ".*", or else it's not possible to unignore hidden files) ig.add([".*/*", "!../"]); } if (this.options.ignore) { ig.add(this.ig.custom); } const filter = ig.createFilter(); const base = this.baseDir; return function(absolutePath) { const relative = pathUtil.getRelativePath(absolutePath, base); if (!relative) { return false; } return !filter(relative); }; } } module.exports = IgnoredPaths;