UNPKG

eslint

Version:

An AST-based pattern checker for JavaScript.

996 lines (893 loc) 31.9 kB
/** * @fileoverview Helper functions for ESLint class * @author Nicholas C. Zakas */ "use strict"; //----------------------------------------------------------------------------- // Requirements //----------------------------------------------------------------------------- const path = require("node:path"); const fs = require("node:fs"); const fsp = fs.promises; const isGlob = require("is-glob"); const hash = require("../cli-engine/hash"); const minimatch = require("minimatch"); const globParent = require("glob-parent"); //----------------------------------------------------------------------------- // Fixup references //----------------------------------------------------------------------------- const Minimatch = minimatch.Minimatch; const MINIMATCH_OPTIONS = { dot: true }; //----------------------------------------------------------------------------- // Types //----------------------------------------------------------------------------- /** * @typedef {Object} GlobSearch * @property {Array<string>} patterns The normalized patterns to use for a search. * @property {Array<string>} rawPatterns The patterns as entered by the user * before doing any normalization. */ //----------------------------------------------------------------------------- // Errors //----------------------------------------------------------------------------- /** * The error type when no files match a glob. */ class NoFilesFoundError extends Error { /** * @param {string} pattern The glob pattern which was not found. * @param {boolean} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled. */ constructor(pattern, globEnabled) { super( `No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`, ); this.messageTemplate = "file-not-found"; this.messageData = { pattern, globDisabled: !globEnabled }; } } /** * The error type when a search fails to match multiple patterns. */ class UnmatchedSearchPatternsError extends Error { /** * @param {Object} options The options for the error. * @param {string} options.basePath The directory that was searched. * @param {Array<string>} options.unmatchedPatterns The glob patterns * which were not found. * @param {Array<string>} options.patterns The glob patterns that were * searched. * @param {Array<string>} options.rawPatterns The raw glob patterns that * were searched. */ constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) { super( `No files matching '${rawPatterns}' in '${basePath}' were found.`, ); this.basePath = basePath; this.unmatchedPatterns = unmatchedPatterns; this.patterns = patterns; this.rawPatterns = rawPatterns; } } /** * The error type when there are files matched by a glob, but all of them have been ignored. */ class AllFilesIgnoredError extends Error { /** * @param {string} pattern The glob pattern which was not found. */ constructor(pattern) { super(`All files matched by '${pattern}' are ignored.`); this.messageTemplate = "all-matched-files-ignored"; this.messageData = { pattern }; } } //----------------------------------------------------------------------------- // General Helpers //----------------------------------------------------------------------------- /** * Check if a given value is a non-empty string or not. * @param {any} value The value to check. * @returns {boolean} `true` if `value` is a non-empty string. */ function isNonEmptyString(value) { return typeof value === "string" && value.trim() !== ""; } /** * Check if a given value is an array of non-empty strings or not. * @param {any} value The value to check. * @returns {boolean} `true` if `value` is an array of non-empty strings. */ function isArrayOfNonEmptyString(value) { return ( Array.isArray(value) && value.length && value.every(isNonEmptyString) ); } /** * Check if a given value is an empty array or an array of non-empty strings. * @param {any} value The value to check. * @returns {boolean} `true` if `value` is an empty array or an array of non-empty * strings. */ function isEmptyArrayOrArrayOfNonEmptyString(value) { return Array.isArray(value) && value.every(isNonEmptyString); } //----------------------------------------------------------------------------- // File-related Helpers //----------------------------------------------------------------------------- /** * Normalizes slashes in a file pattern to posix-style. * @param {string} pattern The pattern to replace slashes in. * @returns {string} The pattern with slashes normalized. */ function normalizeToPosix(pattern) { return pattern.replace(/\\/gu, "/"); } /** * Check if a string is a glob pattern or not. * @param {string} pattern A glob pattern. * @returns {boolean} `true` if the string is a glob pattern. */ function isGlobPattern(pattern) { return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern); } /** * Determines if a given glob pattern will return any results. * Used primarily to help with useful error messages. * @param {Object} options The options for the function. * @param {string} options.basePath The directory to search. * @param {string} options.pattern An absolute path glob pattern to match. * @returns {Promise<boolean>} True if there is a glob match, false if not. */ async function globMatch({ basePath, pattern }) { let found = false; const { hfs } = await import("@humanfs/node"); const patternToUse = normalizeToPosix(path.relative(basePath, pattern)); const matcher = new Minimatch(patternToUse, MINIMATCH_OPTIONS); const walkSettings = { directoryFilter(entry) { return !found && matcher.match(entry.path, true); }, entryFilter(entry) { if (found || entry.isDirectory) { return false; } if (matcher.match(entry.path)) { found = true; return true; } return false; }, }; if (await hfs.isDirectory(basePath)) { return hfs .walk(basePath, walkSettings) .next() .then(() => found); } return found; } /** * Searches a directory looking for matching glob patterns. This uses * the config array's logic to determine if a directory or file should * be ignored, so it is consistent with how ignoring works throughout * ESLint. * @param {Object} options The options for this function. * @param {string} options.basePath The directory to search. * @param {Array<string>} options.patterns An array of absolute path glob patterns * to match. * @param {Array<string>} options.rawPatterns An array of glob patterns * as the user inputted them. Used for errors. * @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config array to use for * determining what to ignore. * @param {boolean} options.errorOnUnmatchedPattern Determines if an error * should be thrown when a pattern is unmatched. * @returns {Promise<Array<string>>} An array of matching file paths * or an empty array if there are no matches. * @throws {UnmatchedSearchPatternsError} If there is a pattern that doesn't * match any files. */ async function globSearch({ basePath, patterns, rawPatterns, configLoader, errorOnUnmatchedPattern, }) { if (patterns.length === 0) { return []; } /* * In this section we are converting the patterns into Minimatch * instances for performance reasons. Because we are doing the same * matches repeatedly, it's best to compile those patterns once and * reuse them multiple times. * * To do that, we convert any patterns with an absolute path into a * relative path and normalize it to Posix-style slashes. We also keep * track of the relative patterns to map them back to the original * patterns, which we need in order to throw an error if there are any * unmatched patterns. */ const relativeToPatterns = new Map(); const matchers = patterns.map((pattern, i) => { const patternToUse = normalizeToPosix(path.relative(basePath, pattern)); relativeToPatterns.set(patternToUse, patterns[i]); return new Minimatch(patternToUse, MINIMATCH_OPTIONS); }); /* * We track unmatched patterns because we may want to throw an error when * they occur. To start, this set is initialized with all of the patterns. * Every time a match occurs, the pattern is removed from the set, making * it easy to tell if we have any unmatched patterns left at the end of * search. */ const unmatchedPatterns = new Set([...relativeToPatterns.keys()]); const { hfs } = await import("@humanfs/node"); const walk = hfs.walk(basePath, { async directoryFilter(entry) { if (!matchers.some(matcher => matcher.match(entry.path, true))) { return false; } const absolutePath = path.resolve(basePath, entry.path); const configs = await configLoader.loadConfigArrayForDirectory(absolutePath); return !configs.isDirectoryIgnored(absolutePath); }, async entryFilter(entry) { const absolutePath = path.resolve(basePath, entry.path); // entries may be directories or files so filter out directories if (entry.isDirectory) { return false; } const configs = await configLoader.loadConfigArrayForFile(absolutePath); const config = configs.getConfig(absolutePath); /* * Optimization: We need to track when patterns are left unmatched * and so we use `unmatchedPatterns` to do that. There is a bit of * complexity here because the same file can be matched by more than * one pattern. So, when we start, we actually need to test every * pattern against every file. Once we know there are no remaining * unmatched patterns, then we can switch to just looking for the * first matching pattern for improved speed. */ const matchesPattern = unmatchedPatterns.size > 0 ? matchers.reduce((previousValue, matcher) => { const pathMatches = matcher.match(entry.path); /* * We updated the unmatched patterns set only if the path * matches and the file has a config. If the file has no * config, that means there wasn't a match for the * pattern so it should not be removed. * * Performance note: `getConfig()` aggressively caches * results so there is no performance penalty for calling * it multiple times with the same argument. */ if (pathMatches && config) { unmatchedPatterns.delete(matcher.pattern); } return pathMatches || previousValue; }, false) : matchers.some(matcher => matcher.match(entry.path)); return matchesPattern && config !== void 0; }, }); const filePaths = []; if (await hfs.isDirectory(basePath)) { for await (const entry of walk) { filePaths.push(path.resolve(basePath, entry.path)); } } // now check to see if we have any unmatched patterns if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) { throw new UnmatchedSearchPatternsError({ basePath, unmatchedPatterns: [...unmatchedPatterns].map(pattern => relativeToPatterns.get(pattern), ), patterns, rawPatterns, }); } return filePaths; } /** * Throws an error for unmatched patterns. The error will only contain information about the first one. * Checks to see if there are any ignored results for a given search. * @param {Object} options The options for this function. * @param {string} options.basePath The directory to search. * @param {Array<string>} options.patterns An array of glob patterns * that were used in the original search. * @param {Array<string>} options.rawPatterns An array of glob patterns * as the user inputted them. Used for errors. * @param {Array<string>} options.unmatchedPatterns A non-empty array of absolute path glob patterns * that were unmatched in the original search. * @returns {void} Always throws an error. * @throws {NoFilesFoundError} If the first unmatched pattern * doesn't match any files even when there are no ignores. * @throws {AllFilesIgnoredError} If the first unmatched pattern * matches some files when there are no ignores. */ async function throwErrorForUnmatchedPatterns({ basePath, patterns, rawPatterns, unmatchedPatterns, }) { const pattern = unmatchedPatterns[0]; const rawPattern = rawPatterns[patterns.indexOf(pattern)]; const patternHasMatch = await globMatch({ basePath, pattern, }); if (patternHasMatch) { throw new AllFilesIgnoredError(rawPattern); } // if we get here there are truly no matches throw new NoFilesFoundError(rawPattern, true); } /** * Performs multiple glob searches in parallel. * @param {Object} options The options for this function. * @param {Map<string,GlobSearch>} options.searches * A map of absolute path glob patterns to match. * @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config loader to use for * determining what to ignore. * @param {boolean} options.errorOnUnmatchedPattern Determines if an * unmatched glob pattern should throw an error. * @returns {Promise<Array<string>>} An array of matching file paths * or an empty array if there are no matches. */ async function globMultiSearch({ searches, configLoader, errorOnUnmatchedPattern, }) { /* * For convenience, we normalized the search map into an array of objects. * Next, we filter out all searches that have no patterns. This happens * primarily for the cwd, which is prepopulated in the searches map as an * optimization. However, if it has no patterns, it means all patterns * occur outside of the cwd and we can safely filter out that search. */ const normalizedSearches = [...searches] .map(([basePath, { patterns, rawPatterns }]) => ({ basePath, patterns, rawPatterns, })) .filter(({ patterns }) => patterns.length > 0); const results = await Promise.allSettled( normalizedSearches.map(({ basePath, patterns, rawPatterns }) => globSearch({ basePath, patterns, rawPatterns, configLoader, errorOnUnmatchedPattern, }), ), ); /* * The first loop handles errors from the glob searches. Since we can't * use `await` inside `flatMap`, we process errors separately in this loop. * This results in two iterations over `results`, but since the length is * less than or equal to the number of globs and directories passed on the * command line, the performance impact should be minimal. */ for (let i = 0; i < results.length; i++) { const result = results[i]; const currentSearch = normalizedSearches[i]; if (result.status === "fulfilled") { continue; } // if we make it here then there was an error const error = result.reason; // unexpected errors should be re-thrown if (!error.basePath) { throw error; } if (errorOnUnmatchedPattern) { await throwErrorForUnmatchedPatterns({ ...currentSearch, unmatchedPatterns: error.unmatchedPatterns, }); } } // second loop for `fulfulled` results return results.flatMap(result => result.value); } /** * Finds all files matching the options specified. * @param {Object} args The arguments objects. * @param {Array<string>} args.patterns An array of glob patterns. * @param {boolean} args.globInputPaths true to interpret glob patterns, * false to not interpret glob patterns. * @param {string} args.cwd The current working directory to find from. * @param {ConfigLoader|LegacyConfigLoader} args.configLoader The config loeader for the current run. * @param {boolean} args.errorOnUnmatchedPattern Determines if an unmatched pattern * should throw an error. * @returns {Promise<Array<string>>} The fully resolved file paths. * @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern. * @throws {NoFilesFoundError} If no files matched the given patterns. */ async function findFiles({ patterns, globInputPaths, cwd, configLoader, errorOnUnmatchedPattern, }) { const results = []; const missingPatterns = []; let globbyPatterns = []; let rawPatterns = []; const searches = new Map([ [cwd, { patterns: globbyPatterns, rawPatterns: [] }], ]); /* * This part is a bit involved because we need to account for * the different ways that the patterns can match directories. * For each different way, we need to decide if we should look * for a config file or just use the default config. (Directories * without a config file always use the default config.) * * Here are the cases: * * 1. A directory is passed directly (e.g., "subdir"). In this case, we * can assume that the user intends to lint this directory and we should * not look for a config file in the parent directory, because the only * reason to do that would be to ignore this directory (which we already * know we don't want to do). Instead, we use the default config until we * get to the directory that was passed, at which point we start looking * for config files again. * * 2. A dot (".") or star ("*"). In this case, we want to read * the config file in the current directory because the user is * explicitly asking to lint the current directory. Note that "." * will traverse into subdirectories while "*" will not. * * 3. A directory is passed in the form of "subdir/subsubdir". * In this case, we don't want to look for a config file in the * parent directory ("subdir"). We can skip looking for a config * file until `entry.depth` is greater than 1 because there's no * way that the pattern can match `entry.path` yet. * * 4. A directory glob pattern is passed (e.g., "subd*"). We want * this case to act like case 2 because it's unclear whether or not * any particular directory is meant to be traversed. * * 5. A recursive glob pattern is passed (e.g., "**"). We want this * case to act like case 2. */ // check to see if we have explicit files and directories const filePaths = patterns.map(filePath => path.resolve(cwd, filePath)); const stats = await Promise.all( filePaths.map(filePath => fsp.stat(filePath).catch(() => {})), ); stats.forEach((stat, index) => { const filePath = filePaths[index]; const pattern = normalizeToPosix(patterns[index]); if (stat) { // files are added directly to the list if (stat.isFile()) { results.push(filePath); } // directories need extensions attached if (stat.isDirectory()) { if (!searches.has(filePath)) { searches.set(filePath, { patterns: [], rawPatterns: [] }); } ({ patterns: globbyPatterns, rawPatterns } = searches.get(filePath)); globbyPatterns.push(`${normalizeToPosix(filePath)}/**`); rawPatterns.push(pattern); } return; } // save patterns for later use based on whether globs are enabled if (globInputPaths && isGlobPattern(pattern)) { /* * We are grouping patterns by their glob parent. This is done to * make it easier to determine when a config file should be loaded. */ const basePath = path.resolve(cwd, globParent(pattern)); if (!searches.has(basePath)) { searches.set(basePath, { patterns: [], rawPatterns: [] }); } ({ patterns: globbyPatterns, rawPatterns } = searches.get(basePath)); globbyPatterns.push(filePath); rawPatterns.push(pattern); } else { missingPatterns.push(pattern); } }); // there were patterns that didn't match anything, tell the user if (errorOnUnmatchedPattern && missingPatterns.length) { throw new NoFilesFoundError(missingPatterns[0], globInputPaths); } // now we are safe to do the search const globbyResults = await globMultiSearch({ searches, configLoader, errorOnUnmatchedPattern, }); return [...new Set([...results, ...globbyResults])]; } //----------------------------------------------------------------------------- // Results-related Helpers //----------------------------------------------------------------------------- /** * Checks if the given message is an error message. * @param {LintMessage} message The message to check. * @returns {boolean} Whether or not the message is an error message. * @private */ function isErrorMessage(message) { return message.severity === 2; } /** * Returns result with warning by ignore settings * @param {string} filePath Absolute file path of checked code * @param {string} baseDir Absolute path of base directory * @param {"ignored"|"external"|"unconfigured"} configStatus A status that determines why the file is ignored * @returns {LintResult} Result with single warning * @private */ function createIgnoreResult(filePath, baseDir, configStatus) { let message; switch (configStatus) { case "external": message = "File ignored because outside of base path."; break; case "unconfigured": message = "File ignored because no matching configuration was supplied."; break; default: { const isInNodeModules = baseDir && path .dirname(path.relative(baseDir, filePath)) .split(path.sep) .includes("node_modules"); if (isInNodeModules) { message = 'File ignored by default because it is located under the node_modules directory. Use ignore pattern "!**/node_modules/" to disable file ignore settings or use "--no-warn-ignored" to suppress this warning.'; } else { message = 'File ignored because of a matching ignore pattern. Use "--no-ignore" to disable file ignore settings or use "--no-warn-ignored" to suppress this warning.'; } } break; } return { filePath, messages: [ { ruleId: null, fatal: false, severity: 1, message, nodeType: null, }, ], suppressedMessages: [], errorCount: 0, warningCount: 1, fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, }; } //----------------------------------------------------------------------------- // Options-related Helpers //----------------------------------------------------------------------------- /** * Check if a given value is a valid fix type or not. * @param {any} x The value to check. * @returns {boolean} `true` if `x` is valid fix type. */ function isFixType(x) { return ( x === "directive" || x === "problem" || x === "suggestion" || x === "layout" ); } /** * Check if a given value is an array of fix types or not. * @param {any} x The value to check. * @returns {boolean} `true` if `x` is an array of fix types. */ function isFixTypeArray(x) { return Array.isArray(x) && x.every(isFixType); } /** * The error for invalid options. */ class ESLintInvalidOptionsError extends Error { constructor(messages) { super(`Invalid Options:\n- ${messages.join("\n- ")}`); this.code = "ESLINT_INVALID_OPTIONS"; Error.captureStackTrace(this, ESLintInvalidOptionsError); } } /** * Validates and normalizes options for the wrapped CLIEngine instance. * @param {ESLintOptions} options The options to process. * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors. * @returns {ESLintOptions} The normalized options. */ function processOptions({ allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. baseConfig = null, cache = false, cacheLocation = ".eslintcache", cacheStrategy = "metadata", cwd = process.cwd(), errorOnUnmatchedPattern = true, fix = false, fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property. flags = [], globInputPaths = true, ignore = true, ignorePatterns = null, overrideConfig = null, overrideConfigFile = null, plugins = {}, stats = false, warnIgnored = true, passOnNoPatterns = false, ruleFilter = () => true, ...unknownOptions }) { const errors = []; const unknownOptionKeys = Object.keys(unknownOptions); if (unknownOptionKeys.length >= 1) { errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); if (unknownOptionKeys.includes("cacheFile")) { errors.push( "'cacheFile' has been removed. Please use the 'cacheLocation' option instead.", ); } if (unknownOptionKeys.includes("configFile")) { errors.push( "'configFile' has been removed. Please use the 'overrideConfigFile' option instead.", ); } if (unknownOptionKeys.includes("envs")) { errors.push("'envs' has been removed."); } if (unknownOptionKeys.includes("extensions")) { errors.push("'extensions' has been removed."); } if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) { errors.push("'resolvePluginsRelativeTo' has been removed."); } if (unknownOptionKeys.includes("globals")) { errors.push( "'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.", ); } if (unknownOptionKeys.includes("ignorePath")) { errors.push("'ignorePath' has been removed."); } if (unknownOptionKeys.includes("ignorePattern")) { errors.push( "'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.", ); } if (unknownOptionKeys.includes("parser")) { errors.push( "'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.", ); } if (unknownOptionKeys.includes("parserOptions")) { errors.push( "'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.", ); } if (unknownOptionKeys.includes("rules")) { errors.push( "'rules' has been removed. Please use the 'overrideConfig.rules' option instead.", ); } if (unknownOptionKeys.includes("rulePaths")) { errors.push( "'rulePaths' has been removed. Please define your rules using plugins.", ); } if (unknownOptionKeys.includes("reportUnusedDisableDirectives")) { errors.push( "'reportUnusedDisableDirectives' has been removed. Please use the 'overrideConfig.linterOptions.reportUnusedDisableDirectives' option instead.", ); } } if (typeof allowInlineConfig !== "boolean") { errors.push("'allowInlineConfig' must be a boolean."); } if (typeof baseConfig !== "object") { errors.push("'baseConfig' must be an object or null."); } if (typeof cache !== "boolean") { errors.push("'cache' must be a boolean."); } if (!isNonEmptyString(cacheLocation)) { errors.push("'cacheLocation' must be a non-empty string."); } if (cacheStrategy !== "metadata" && cacheStrategy !== "content") { errors.push('\'cacheStrategy\' must be any of "metadata", "content".'); } if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { errors.push("'cwd' must be an absolute path."); } if (typeof errorOnUnmatchedPattern !== "boolean") { errors.push("'errorOnUnmatchedPattern' must be a boolean."); } if (typeof fix !== "boolean" && typeof fix !== "function") { errors.push("'fix' must be a boolean or a function."); } if (fixTypes !== null && !isFixTypeArray(fixTypes)) { errors.push( '\'fixTypes\' must be an array of any of "directive", "problem", "suggestion", and "layout".', ); } if (!isEmptyArrayOrArrayOfNonEmptyString(flags)) { errors.push("'flags' must be an array of non-empty strings."); } if (typeof globInputPaths !== "boolean") { errors.push("'globInputPaths' must be a boolean."); } if (typeof ignore !== "boolean") { errors.push("'ignore' must be a boolean."); } if ( !isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null ) { errors.push( "'ignorePatterns' must be an array of non-empty strings or null.", ); } if (typeof overrideConfig !== "object") { errors.push("'overrideConfig' must be an object or null."); } if ( !isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true ) { errors.push( "'overrideConfigFile' must be a non-empty string, null, or true.", ); } if (typeof passOnNoPatterns !== "boolean") { errors.push("'passOnNoPatterns' must be a boolean."); } if (typeof plugins !== "object") { errors.push("'plugins' must be an object or null."); } else if (plugins !== null && Object.keys(plugins).includes("")) { errors.push("'plugins' must not include an empty string."); } if (Array.isArray(plugins)) { errors.push( "'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.", ); } if (typeof stats !== "boolean") { errors.push("'stats' must be a boolean."); } if (typeof warnIgnored !== "boolean") { errors.push("'warnIgnored' must be a boolean."); } if (typeof ruleFilter !== "function") { errors.push("'ruleFilter' must be a function."); } if (errors.length > 0) { throw new ESLintInvalidOptionsError(errors); } return { allowInlineConfig, baseConfig, cache, cacheLocation, cacheStrategy, // when overrideConfigFile is true that means don't do config file lookup configFile: overrideConfigFile === true ? false : overrideConfigFile, overrideConfig, cwd: path.normalize(cwd), errorOnUnmatchedPattern, fix, fixTypes, flags: [...flags], globInputPaths, ignore, ignorePatterns, stats, passOnNoPatterns, warnIgnored, ruleFilter, }; } //----------------------------------------------------------------------------- // Cache-related helpers //----------------------------------------------------------------------------- /** * return the cacheFile to be used by eslint, based on whether the provided parameter is * a directory or looks like a directory (ends in `path.sep`), in which case the file * name will be the `cacheFile/.cache_hashOfCWD` * * if cacheFile points to a file or looks like a file then in will just use that file * @param {string} cacheFile The name of file to be used to store the cache * @param {string} cwd Current working directory * @returns {string} the resolved path to the cache file */ function getCacheFile(cacheFile, cwd) { /* * make sure the path separators are normalized for the environment/os * keeping the trailing path separator if present */ const normalizedCacheFile = path.normalize(cacheFile); const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile); const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep; /** * return the name for the cache file in case the provided parameter is a directory * @returns {string} the resolved path to the cacheFile */ function getCacheFileForDirectory() { return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`); } let fileStats; try { fileStats = fs.lstatSync(resolvedCacheFile); } catch { fileStats = null; } /* * in case the file exists we need to verify if the provided path * is a directory or a file. If it is a directory we want to create a file * inside that directory */ if (fileStats) { /* * is a directory or is a file, but the original file the user provided * looks like a directory but `path.resolve` removed the `last path.sep` * so we need to still treat this like a directory */ if (fileStats.isDirectory() || looksLikeADirectory) { return getCacheFileForDirectory(); } // is file so just use that file return resolvedCacheFile; } /* * here we known the file or directory doesn't exist, * so we will try to infer if its a directory if it looks like a directory * for the current operating system. */ // if the last character passed is a path separator we assume is a directory if (looksLikeADirectory) { return getCacheFileForDirectory(); } return resolvedCacheFile; } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- module.exports = { findFiles, isNonEmptyString, isArrayOfNonEmptyString, createIgnoreResult, isErrorMessage, processOptions, getCacheFile, };