UNPKG

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