UNPKG

10.5 kBJavaScriptView Raw
1/**
2 * @fileoverview Utilities for working with globs and the filesystem.
3 * @author Ian VanSchooten
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const lodash = require("lodash"),
12 fs = require("fs"),
13 path = require("path"),
14 GlobSync = require("./glob"),
15
16 pathUtils = require("./path-utils"),
17 IgnoredPaths = require("./ignored-paths");
18
19const debug = require("debug")("eslint:glob-utils");
20
21//------------------------------------------------------------------------------
22// Helpers
23//------------------------------------------------------------------------------
24
25/**
26 * Checks whether a directory exists at the given location
27 * @param {string} resolvedPath A path from the CWD
28 * @returns {boolean} `true` if a directory exists
29 */
30function directoryExists(resolvedPath) {
31 return fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory();
32}
33
34/**
35 * Checks if a provided path is a directory and returns a glob string matching
36 * all files under that directory if so, the path itself otherwise.
37 *
38 * Reason for this is that `glob` needs `/**` to collect all the files under a
39 * directory where as our previous implementation without `glob` simply walked
40 * a directory that is passed. So this is to maintain backwards compatibility.
41 *
42 * Also makes sure all path separators are POSIX style for `glob` compatibility.
43 *
44 * @param {Object} [options] An options object
45 * @param {string[]} [options.extensions=[".js"]] An array of accepted extensions
46 * @param {string} [options.cwd=process.cwd()] The cwd to use to resolve relative pathnames
47 * @returns {Function} A function that takes a pathname and returns a glob that
48 * matches all files with the provided extensions if
49 * pathname is a directory.
50 */
51function processPath(options) {
52 const cwd = (options && options.cwd) || process.cwd();
53 let extensions = (options && options.extensions) || [".js"];
54
55 extensions = extensions.map(ext => ext.replace(/^\./u, ""));
56
57 let suffix = "/**";
58
59 if (extensions.length === 1) {
60 suffix += `/*.${extensions[0]}`;
61 } else {
62 suffix += `/*.{${extensions.join(",")}}`;
63 }
64
65 /**
66 * A function that converts a directory name to a glob pattern
67 *
68 * @param {string} pathname The directory path to be modified
69 * @returns {string} The glob path or the file path itself
70 * @private
71 */
72 return function(pathname) {
73 if (pathname === "") {
74 return "";
75 }
76
77 let newPath = pathname;
78 const resolvedPath = path.resolve(cwd, pathname);
79
80 if (directoryExists(resolvedPath)) {
81 newPath = pathname.replace(/[/\\]$/u, "") + suffix;
82 }
83
84 return pathUtils.convertPathToPosix(newPath);
85 };
86}
87
88/**
89 * The error type when no files match a glob.
90 */
91class NoFilesFoundError extends Error {
92
93 /**
94 * @param {string} pattern - The glob pattern which was not found.
95 */
96 constructor(pattern) {
97 super(`No files matching '${pattern}' were found.`);
98
99 this.messageTemplate = "file-not-found";
100 this.messageData = { pattern };
101 }
102
103}
104
105/**
106 * The error type when there are files matched by a glob, but all of them have been ignored.
107 */
108class AllFilesIgnoredError extends Error {
109
110 /**
111 * @param {string} pattern - The glob pattern which was not found.
112 */
113 constructor(pattern) {
114 super(`All files matched by '${pattern}' are ignored.`);
115 this.messageTemplate = "all-files-ignored";
116 this.messageData = { pattern };
117 }
118}
119
120const NORMAL_LINT = {};
121const SILENTLY_IGNORE = {};
122const IGNORE_AND_WARN = {};
123
124/**
125 * Tests whether a file should be linted or ignored
126 * @param {string} filename The file to be processed
127 * @param {{ignore: (boolean|null)}} options If `ignore` is false, updates the behavior to
128 * not process custom ignore paths, and lint files specified by direct path even if they
129 * match the default ignore path
130 * @param {boolean} isDirectPath True if the file was provided as a direct path
131 * (as opposed to being resolved from a glob)
132 * @param {IgnoredPaths} ignoredPaths An instance of IgnoredPaths to check whether a given
133 * file is ignored.
134 * @returns {(NORMAL_LINT|SILENTLY_IGNORE|IGNORE_AND_WARN)} A directive for how the
135 * file should be processed (either linted normally, or silently ignored, or ignored
136 * with a warning that it is being ignored)
137 */
138function testFileAgainstIgnorePatterns(filename, options, isDirectPath, ignoredPaths) {
139 const shouldProcessCustomIgnores = options.ignore !== false;
140 const shouldLintIgnoredDirectPaths = options.ignore === false;
141 const fileMatchesIgnorePatterns = ignoredPaths.contains(filename, "default") ||
142 (shouldProcessCustomIgnores && ignoredPaths.contains(filename, "custom"));
143
144 if (fileMatchesIgnorePatterns && isDirectPath && !shouldLintIgnoredDirectPaths) {
145 return IGNORE_AND_WARN;
146 }
147
148 if (!fileMatchesIgnorePatterns || (isDirectPath && shouldLintIgnoredDirectPaths)) {
149 return NORMAL_LINT;
150 }
151
152 return SILENTLY_IGNORE;
153}
154
155//------------------------------------------------------------------------------
156// Public Interface
157//------------------------------------------------------------------------------
158
159/**
160 * Resolves any directory patterns into glob-based patterns for easier handling.
161 * @param {string[]} patterns File patterns (such as passed on the command line).
162 * @param {Object} options An options object.
163 * @param {string} [options.globInputPaths] False disables glob resolution.
164 * @returns {string[]} The equivalent glob patterns and filepath strings.
165 */
166function resolveFileGlobPatterns(patterns, options) {
167 if (options.globInputPaths === false) {
168 return patterns;
169 }
170
171 const processPathExtensions = processPath(options);
172
173 return patterns.map(processPathExtensions);
174}
175
176const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
177
178/**
179 * Build a list of absolute filesnames on which ESLint will act.
180 * Ignored files are excluded from the results, as are duplicates.
181 *
182 * @param {string[]} globPatterns Glob patterns.
183 * @param {Object} [providedOptions] An options object.
184 * @param {string} [providedOptions.cwd] CWD (considered for relative filenames)
185 * @param {boolean} [providedOptions.ignore] False disables use of .eslintignore.
186 * @param {string} [providedOptions.ignorePath] The ignore file to use instead of .eslintignore.
187 * @param {string} [providedOptions.ignorePattern] A pattern of files to ignore.
188 * @param {string} [providedOptions.globInputPaths] False disables glob resolution.
189 * @returns {string[]} Resolved absolute filenames.
190 */
191function listFilesToProcess(globPatterns, providedOptions) {
192 const options = providedOptions || { ignore: true };
193 const cwd = options.cwd || process.cwd();
194
195 const getIgnorePaths = lodash.memoize(
196 optionsObj =>
197 new IgnoredPaths(optionsObj)
198 );
199
200 /*
201 * The test "should use default options if none are provided" (source-code-utils.js) checks that 'module.exports.resolveFileGlobPatterns' was called.
202 * So it cannot use the local function "resolveFileGlobPatterns".
203 */
204 const resolvedGlobPatterns = module.exports.resolveFileGlobPatterns(globPatterns, options);
205
206 debug("Creating list of files to process.");
207 const resolvedPathsByGlobPattern = resolvedGlobPatterns.map(pattern => {
208 if (pattern === "") {
209 return [{
210 filename: "",
211 behavior: SILENTLY_IGNORE
212 }];
213 }
214
215 const file = path.resolve(cwd, pattern);
216
217 if (options.globInputPaths === false || (fs.existsSync(file) && fs.statSync(file).isFile())) {
218 const ignoredPaths = getIgnorePaths(options);
219 const fullPath = options.globInputPaths === false ? file : fs.realpathSync(file);
220
221 return [{
222 filename: fullPath,
223 behavior: testFileAgainstIgnorePatterns(fullPath, options, true, ignoredPaths)
224 }];
225 }
226
227 // regex to find .hidden or /.hidden patterns, but not ./relative or ../relative
228 const globIncludesDotfiles = dotfilesPattern.test(pattern);
229 let newOptions = options;
230
231 if (!options.dotfiles) {
232 newOptions = Object.assign({}, options, { dotfiles: globIncludesDotfiles });
233 }
234
235 const ignoredPaths = getIgnorePaths(newOptions);
236 const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker();
237 const globOptions = {
238 nodir: true,
239 dot: true,
240 cwd
241 };
242
243 return new GlobSync(pattern, globOptions, shouldIgnore).found.map(globMatch => {
244 const relativePath = path.resolve(cwd, globMatch);
245
246 return {
247 filename: relativePath,
248 behavior: testFileAgainstIgnorePatterns(relativePath, options, false, ignoredPaths)
249 };
250 });
251 });
252
253 const allPathDescriptors = resolvedPathsByGlobPattern.reduce((pathsForAllGlobs, pathsForCurrentGlob, index) => {
254 if (pathsForCurrentGlob.every(pathDescriptor => pathDescriptor.behavior === SILENTLY_IGNORE && pathDescriptor.filename !== "")) {
255 throw new (pathsForCurrentGlob.length ? AllFilesIgnoredError : NoFilesFoundError)(globPatterns[index]);
256 }
257
258 pathsForCurrentGlob.forEach(pathDescriptor => {
259 switch (pathDescriptor.behavior) {
260 case NORMAL_LINT:
261 pathsForAllGlobs.push({ filename: pathDescriptor.filename, ignored: false });
262 break;
263 case IGNORE_AND_WARN:
264 pathsForAllGlobs.push({ filename: pathDescriptor.filename, ignored: true });
265 break;
266 case SILENTLY_IGNORE:
267
268 // do nothing
269 break;
270
271 default:
272 throw new Error(`Unexpected file behavior for ${pathDescriptor.filename}`);
273 }
274 });
275
276 return pathsForAllGlobs;
277 }, []);
278
279 return lodash.uniqBy(allPathDescriptors, pathDescriptor => pathDescriptor.filename);
280}
281
282module.exports = {
283 resolveFileGlobPatterns,
284 listFilesToProcess
285};