UNPKG

13.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Responsible for loading ignore config files and managing ignore patterns
3 * @author Jonathan Rajavuori
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const fs = require("fs"),
13 path = require("path"),
14 ignore = require("ignore"),
15 pathUtils = require("./path-utils");
16
17const debug = require("debug")("eslint:ignored-paths");
18
19//------------------------------------------------------------------------------
20// Constants
21//------------------------------------------------------------------------------
22
23const ESLINT_IGNORE_FILENAME = ".eslintignore";
24
25/**
26 * Adds `"*"` at the end of `"node_modules/"`,
27 * so that subtle directories could be re-included by .gitignore patterns
28 * such as `"!node_modules/should_not_ignored"`
29 */
30const DEFAULT_IGNORE_DIRS = [
31 "/node_modules/*",
32 "/bower_components/*"
33];
34const DEFAULT_OPTIONS = {
35 dotfiles: false,
36 cwd: process.cwd()
37};
38
39//------------------------------------------------------------------------------
40// Helpers
41//------------------------------------------------------------------------------
42
43/**
44 * Find a file in the current directory.
45 * @param {string} cwd Current working directory
46 * @param {string} name File name
47 * @returns {string} Path of ignore file or an empty string.
48 */
49function findFile(cwd, name) {
50 const ignoreFilePath = path.resolve(cwd, name);
51
52 return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
53}
54
55/**
56 * Find an ignore file in the current directory.
57 * @param {string} cwd Current working directory
58 * @returns {string} Path of ignore file or an empty string.
59 */
60function findIgnoreFile(cwd) {
61 return findFile(cwd, ESLINT_IGNORE_FILENAME);
62}
63
64/**
65 * Find an package.json file in the current directory.
66 * @param {string} cwd Current working directory
67 * @returns {string} Path of package.json file or an empty string.
68 */
69function findPackageJSONFile(cwd) {
70 return findFile(cwd, "package.json");
71}
72
73/**
74 * Merge options with defaults
75 * @param {Object} options Options to merge with DEFAULT_OPTIONS constant
76 * @returns {Object} Merged options
77 */
78function mergeDefaultOptions(options) {
79 return Object.assign({}, DEFAULT_OPTIONS, options);
80}
81
82/* eslint-disable valid-jsdoc */
83/**
84 * Normalize the path separators in a given string.
85 * On Windows environment, this replaces `\` by `/`.
86 * Otherwrise, this does nothing.
87 * @param {string} str The path string to normalize.
88 * @returns {string} The normalized path.
89 */
90const normalizePathSeps = path.sep === "/"
91 ? (str => str)
92 : ((seps, str) => str.replace(seps, "/")).bind(null, new RegExp(`\\${path.sep}`, "gu"));
93/* eslint-enable valid-jsdoc */
94
95/**
96 * Converts a glob pattern to a new glob pattern relative to a different directory
97 * @param {string} globPattern The glob pattern, relative the the old base directory
98 * @param {string} relativePathToOldBaseDir A relative path from the new base directory to the old one
99 * @returns {string} A glob pattern relative to the new base directory
100 */
101function relativize(globPattern, relativePathToOldBaseDir) {
102 if (relativePathToOldBaseDir === "") {
103 return globPattern;
104 }
105
106 const prefix = globPattern.startsWith("!") ? "!" : "";
107 const globWithoutPrefix = globPattern.replace(/^!/u, "");
108
109 if (globWithoutPrefix.startsWith("/")) {
110 return `${prefix}/${normalizePathSeps(relativePathToOldBaseDir)}${globWithoutPrefix}`;
111 }
112
113 return globPattern;
114}
115
116//------------------------------------------------------------------------------
117// Public Interface
118//------------------------------------------------------------------------------
119
120/**
121 * IgnoredPaths class
122 */
123class IgnoredPaths {
124
125 /**
126 * @param {Object} providedOptions object containing 'ignore', 'ignorePath' and 'patterns' properties
127 */
128 constructor(providedOptions) {
129 const options = mergeDefaultOptions(providedOptions);
130
131 this.cache = {};
132
133 this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []);
134
135 this.ignoreFileDir = options.ignore !== false && options.ignorePath
136 ? path.dirname(path.resolve(options.cwd, options.ignorePath))
137 : options.cwd;
138 this.options = options;
139 this._baseDir = null;
140
141 this.ig = {
142 custom: ignore(),
143 default: ignore()
144 };
145
146 this.defaultPatterns.forEach(pattern => this.addPatternRelativeToCwd(this.ig.default, pattern));
147 if (options.dotfiles !== true) {
148
149 /*
150 * ignore files beginning with a dot, but not files in a parent or
151 * ancestor directory (which in relative format will begin with `../`).
152 */
153 this.addPatternRelativeToCwd(this.ig.default, ".*");
154 this.addPatternRelativeToCwd(this.ig.default, "!../");
155 }
156
157 /*
158 * Add a way to keep track of ignored files. This was present in node-ignore
159 * 2.x, but dropped for now as of 3.0.10.
160 */
161 this.ig.custom.ignoreFiles = [];
162 this.ig.default.ignoreFiles = [];
163
164 if (options.ignore !== false) {
165 let ignorePath;
166
167 if (options.ignorePath) {
168 debug("Using specific ignore file");
169
170 try {
171 fs.statSync(options.ignorePath);
172 ignorePath = options.ignorePath;
173 } catch (e) {
174 e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`;
175 throw e;
176 }
177 } else {
178 debug(`Looking for ignore file in ${options.cwd}`);
179 ignorePath = findIgnoreFile(options.cwd);
180
181 try {
182 fs.statSync(ignorePath);
183 debug(`Loaded ignore file ${ignorePath}`);
184 } catch (e) {
185 debug("Could not find ignore file in cwd");
186 }
187 }
188
189 if (ignorePath) {
190 debug(`Adding ${ignorePath}`);
191 this.addIgnoreFile(this.ig.custom, ignorePath);
192 this.addIgnoreFile(this.ig.default, ignorePath);
193 } else {
194 try {
195
196 // if the ignoreFile does not exist, check package.json for eslintIgnore
197 const packageJSONPath = findPackageJSONFile(options.cwd);
198
199 if (packageJSONPath) {
200 let packageJSONOptions;
201
202 try {
203 packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"));
204 } catch (e) {
205 debug("Could not read package.json file to check eslintIgnore property");
206 e.messageTemplate = "failed-to-read-json";
207 e.messageData = {
208 path: packageJSONPath,
209 message: e.message
210 };
211 throw e;
212 }
213
214 if (packageJSONOptions.eslintIgnore) {
215 if (Array.isArray(packageJSONOptions.eslintIgnore)) {
216 packageJSONOptions.eslintIgnore.forEach(pattern => {
217 this.addPatternRelativeToIgnoreFile(this.ig.custom, pattern);
218 this.addPatternRelativeToIgnoreFile(this.ig.default, pattern);
219 });
220 } else {
221 throw new TypeError("Package.json eslintIgnore property requires an array of paths");
222 }
223 }
224 }
225 } catch (e) {
226 debug("Could not find package.json to check eslintIgnore property");
227 throw e;
228 }
229 }
230
231 if (options.ignorePattern) {
232 this.addPatternRelativeToCwd(this.ig.custom, options.ignorePattern);
233 this.addPatternRelativeToCwd(this.ig.default, options.ignorePattern);
234 }
235 }
236 }
237
238 /*
239 * If `ignoreFileDir` is a subdirectory of `cwd`, all paths will be normalized to be relative to `cwd`.
240 * Otherwise, all paths will be normalized to be relative to `ignoreFileDir`.
241 * This ensures that the final normalized ignore rule will not contain `..`, which is forbidden in
242 * ignore rules.
243 */
244
245 addPatternRelativeToCwd(ig, pattern) {
246 const baseDir = this.getBaseDir();
247 const cookedPattern = baseDir === this.options.cwd
248 ? pattern
249 : relativize(pattern, path.relative(baseDir, this.options.cwd));
250
251 ig.addPattern(cookedPattern);
252 debug("addPatternRelativeToCwd:\n original = %j\n cooked = %j", pattern, cookedPattern);
253 }
254
255 addPatternRelativeToIgnoreFile(ig, pattern) {
256 const baseDir = this.getBaseDir();
257 const cookedPattern = baseDir === this.ignoreFileDir
258 ? pattern
259 : relativize(pattern, path.relative(baseDir, this.ignoreFileDir));
260
261 ig.addPattern(cookedPattern);
262 debug("addPatternRelativeToIgnoreFile:\n original = %j\n cooked = %j", pattern, cookedPattern);
263 }
264
265 // Detect the common ancestor
266 getBaseDir() {
267 if (!this._baseDir) {
268 const a = path.resolve(this.options.cwd);
269 const b = path.resolve(this.ignoreFileDir);
270 let lastSepPos = 0;
271
272 // Set the shorter one (it's the common ancestor if one includes the other).
273 this._baseDir = a.length < b.length ? a : b;
274
275 // Set the common ancestor.
276 for (let i = 0; i < a.length && i < b.length; ++i) {
277 if (a[i] !== b[i]) {
278 this._baseDir = a.slice(0, lastSepPos);
279 break;
280 }
281 if (a[i] === path.sep) {
282 lastSepPos = i;
283 }
284 }
285
286 // If it's only Windows drive letter, it needs \
287 if (/^[A-Z]:$/u.test(this._baseDir)) {
288 this._baseDir += "\\";
289 }
290
291 debug("baseDir = %j", this._baseDir);
292 }
293 return this._baseDir;
294 }
295
296 /**
297 * read ignore filepath
298 * @param {string} filePath, file to add to ig
299 * @returns {Array} raw ignore rules
300 */
301 readIgnoreFile(filePath) {
302 if (typeof this.cache[filePath] === "undefined") {
303 this.cache[filePath] = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu).filter(Boolean);
304 }
305 return this.cache[filePath];
306 }
307
308 /**
309 * add ignore file to node-ignore instance
310 * @param {Object} ig instance of node-ignore
311 * @param {string} filePath file to add to ig
312 * @returns {void}
313 */
314 addIgnoreFile(ig, filePath) {
315 ig.ignoreFiles.push(filePath);
316 this
317 .readIgnoreFile(filePath)
318 .forEach(ignoreRule => this.addPatternRelativeToIgnoreFile(ig, ignoreRule));
319 }
320
321 /**
322 * Determine whether a file path is included in the default or custom ignore patterns
323 * @param {string} filepath Path to check
324 * @param {string} [category=undefined] check 'default', 'custom' or both (undefined)
325 * @returns {boolean} true if the file path matches one or more patterns, false otherwise
326 */
327 contains(filepath, category) {
328
329 let result = false;
330 const absolutePath = path.resolve(this.options.cwd, filepath);
331 const relativePath = pathUtils.getRelativePath(absolutePath, this.getBaseDir());
332
333 if (typeof category === "undefined") {
334 result = (this.ig.default.filter([relativePath]).length === 0) ||
335 (this.ig.custom.filter([relativePath]).length === 0);
336 } else {
337 result = (this.ig[category].filter([relativePath]).length === 0);
338 }
339 debug("contains:");
340 debug(" target = %j", filepath);
341 debug(" result = %j", result);
342
343 return result;
344
345 }
346
347 /**
348 * Returns a list of dir patterns for glob to ignore
349 * @returns {function()} method to check whether a folder should be ignored by glob.
350 */
351 getIgnoredFoldersGlobChecker() {
352 const baseDir = this.getBaseDir();
353 const ig = ignore();
354
355 DEFAULT_IGNORE_DIRS.forEach(ignoreDir => this.addPatternRelativeToCwd(ig, ignoreDir));
356
357 if (this.options.dotfiles !== true) {
358
359 // Ignore hidden folders. (This cannot be ".*", or else it's not possible to unignore hidden files)
360 ig.add([".*/*", "!../*"]);
361 }
362
363 if (this.options.ignore) {
364 ig.add(this.ig.custom);
365 }
366
367 const filter = ig.createFilter();
368
369 return function(absolutePath) {
370 const relative = pathUtils.getRelativePath(absolutePath, baseDir);
371
372 if (!relative) {
373 return false;
374 }
375
376 return !filter(relative);
377 };
378 }
379}
380
381module.exports = IgnoredPaths;