glob-utils.js 10 KB

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