From 42ab2e9dcb52d96cbfa13f6ceabc8a75fb0a33da Mon Sep 17 00:00:00 2001 From: Joe Lencioni Date: Mon, 8 Jun 2020 12:49:20 -0500 Subject: [PATCH] Add code comments to globsToMatcher The logic here might be a little confusing, so I am adding some comments that I hope will help make it easier for future explorers to understand. While I was doing this, I noticed a small way to simplify this function even more. --- packages/jest-util/src/globsToMatcher.ts | 58 +++++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/jest-util/src/globsToMatcher.ts b/packages/jest-util/src/globsToMatcher.ts index 52f72c11b395..3c05fdafc781 100644 --- a/packages/jest-util/src/globsToMatcher.ts +++ b/packages/jest-util/src/globsToMatcher.ts @@ -9,7 +9,7 @@ import micromatch = require('micromatch'); import type {Config} from '@jest/types'; import replacePathSepForGlob from './replacePathSepForGlob'; -const globsMatchers = new Map< +const globsToMatchersMap = new Map< string, { isMatch: (str: string) => boolean; @@ -17,29 +17,54 @@ const globsMatchers = new Map< } >(); +const micromatchOptions = {dot: true}; + +/** + * Converts a list of globs into a function that matches a path against the + * globs. + * + * Every time micromatch is called, it will parse the glob strings and turn + * them into regexp instances. Instead of calling micromatch repeatedly with + * the same globs, we can use this function which will build the micromatch + * matchers ahead of time and then have an optimized path for determining + * whether an individual path matches. + * + * This function is intended to match the behavior of `micromatch()`. + * + * @example + * const isMatch = globsToMatcher(['*.js', '!*.test.js']); + * isMatch('pizza.js'); // true + * isMatch('pizza.test.js'); // false + */ export default function globsToMatcher( globs: Array, ): (path: Config.Path) => boolean { if (globs.length === 0) { + // Since there were no globs given, we can simply have a fast path here and + // return with a very simple function. return (_: Config.Path): boolean => false; } const matchers = globs.map(glob => { - if (!globsMatchers.has(glob)) { - const state = micromatch.scan(glob, {dot: true}); + if (!globsToMatchersMap.has(glob)) { + // Matchers that are negated have different behavior than matchers that + // are not negated, so we need to store this information ahead of time. + const {negated} = micromatch.scan(glob, micromatchOptions); + const matcher = { - isMatch: micromatch.matcher(glob, {dot: true}), - negated: state.negated, + isMatch: micromatch.matcher(glob, micromatchOptions), + negated, }; - globsMatchers.set(glob, matcher); + + globsToMatchersMap.set(glob, matcher); } - return globsMatchers.get(glob)!; + + return globsToMatchersMap.get(glob)!; }); return (path: Config.Path): boolean => { const replacedPath = replacePathSepForGlob(path); - let kept = false; - let omitted = false; + let kept = undefined; let negatives = 0; for (let i = 0; i < matchers.length; i++) { @@ -50,14 +75,23 @@ export default function globsToMatcher( const matched = isMatch(replacedPath); if (!matched && negated) { + // The path was not matched, and the matcher is a negated matcher, so we + // want to omit the path. This means that the negative matcher is + // filtering the path out. kept = false; - omitted = true; } else if (matched && !negated) { + // The path was matched, and the matcher is not a negated matcher, so we + // want to keep the path. kept = true; - omitted = false; } } - return negatives === matchers.length ? !omitted : kept && !omitted; + // If all of the globs were negative globs, then we want to include the path + // as long as it was not explicitly not kept. Otherwise only include + // the path if it was kept. This allows sets of globs that are all negated + // to allow some paths to be matched, while sets of globs that are mixed + // negated and non-negated to cause the negated matchers to only omit paths + // and not keep them. + return negatives === matchers.length ? kept !== false : !!kept; }; }