File size: 6,877 Bytes
bc20498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
 * @fileoverview `OverrideTester` class.
 *
 * `OverrideTester` class handles `files` property and `excludedFiles` property
 * of `overrides` config.
 *
 * It provides one method.
 *
 * - `test(filePath)`
 *      Test if a file path matches the pair of `files` property and
 *      `excludedFiles` property. The `filePath` argument must be an absolute
 *      path.
 *
 * `ConfigArrayFactory` creates `OverrideTester` objects when it processes
 * `overrides` properties.
 *
 * @author Toru Nagashima <https://github.com/mysticatea>
 */

import assert from "assert";
import path from "path";
import util from "util";
import minimatch from "minimatch";

const { Minimatch } = minimatch;

const minimatchOpts = { dot: true, matchBase: true };

/**
 * @typedef {Object} Pattern
 * @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
 * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
 */

/**
 * Normalize a given pattern to an array.
 * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
 * @returns {string[]|null} Normalized patterns.
 * @private
 */
function normalizePatterns(patterns) {
    if (Array.isArray(patterns)) {
        return patterns.filter(Boolean);
    }
    if (typeof patterns === "string" && patterns) {
        return [patterns];
    }
    return [];
}

/**
 * Create the matchers of given patterns.
 * @param {string[]} patterns The patterns.
 * @returns {InstanceType<Minimatch>[] | null} The matchers.
 */
function toMatcher(patterns) {
    if (patterns.length === 0) {
        return null;
    }
    return patterns.map(pattern => {
        if (/^\.[/\\]/u.test(pattern)) {
            return new Minimatch(
                pattern.slice(2),

                // `./*.js` should not match with `subdir/foo.js`
                { ...minimatchOpts, matchBase: false }
            );
        }
        return new Minimatch(pattern, minimatchOpts);
    });
}

/**
 * Convert a given matcher to string.
 * @param {Pattern} matchers The matchers.
 * @returns {string} The string expression of the matcher.
 */
function patternToJson({ includes, excludes }) {
    return {
        includes: includes && includes.map(m => m.pattern),
        excludes: excludes && excludes.map(m => m.pattern)
    };
}

/**
 * The class to test given paths are matched by the patterns.
 */
class OverrideTester {

    /**
     * Create a tester with given criteria.
     * If there are no criteria, returns `null`.
     * @param {string|string[]} files The glob patterns for included files.
     * @param {string|string[]} excludedFiles The glob patterns for excluded files.
     * @param {string} basePath The path to the base directory to test paths.
     * @returns {OverrideTester|null} The created instance or `null`.
     */
    static create(files, excludedFiles, basePath) {
        const includePatterns = normalizePatterns(files);
        const excludePatterns = normalizePatterns(excludedFiles);
        let endsWithWildcard = false;

        if (includePatterns.length === 0) {
            return null;
        }

        // Rejects absolute paths or relative paths to parents.
        for (const pattern of includePatterns) {
            if (path.isAbsolute(pattern) || pattern.includes("..")) {
                throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
            }
            if (pattern.endsWith("*")) {
                endsWithWildcard = true;
            }
        }
        for (const pattern of excludePatterns) {
            if (path.isAbsolute(pattern) || pattern.includes("..")) {
                throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
            }
        }

        const includes = toMatcher(includePatterns);
        const excludes = toMatcher(excludePatterns);

        return new OverrideTester(
            [{ includes, excludes }],
            basePath,
            endsWithWildcard
        );
    }

    /**
     * Combine two testers by logical and.
     * If either of the testers was `null`, returns the other tester.
     * The `basePath` property of the two must be the same value.
     * @param {OverrideTester|null} a A tester.
     * @param {OverrideTester|null} b Another tester.
     * @returns {OverrideTester|null} Combined tester.
     */
    static and(a, b) {
        if (!b) {
            return a && new OverrideTester(
                a.patterns,
                a.basePath,
                a.endsWithWildcard
            );
        }
        if (!a) {
            return new OverrideTester(
                b.patterns,
                b.basePath,
                b.endsWithWildcard
            );
        }

        assert.strictEqual(a.basePath, b.basePath);
        return new OverrideTester(
            a.patterns.concat(b.patterns),
            a.basePath,
            a.endsWithWildcard || b.endsWithWildcard
        );
    }

    /**
     * Initialize this instance.
     * @param {Pattern[]} patterns The matchers.
     * @param {string} basePath The base path.
     * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
     */
    constructor(patterns, basePath, endsWithWildcard = false) {

        /** @type {Pattern[]} */
        this.patterns = patterns;

        /** @type {string} */
        this.basePath = basePath;

        /** @type {boolean} */
        this.endsWithWildcard = endsWithWildcard;
    }

    /**
     * Test if a given path is matched or not.
     * @param {string} filePath The absolute path to the target file.
     * @returns {boolean} `true` if the path was matched.
     */
    test(filePath) {
        if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
            throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
        }
        const relativePath = path.relative(this.basePath, filePath);

        return this.patterns.every(({ includes, excludes }) => (
            (!includes || includes.some(m => m.match(relativePath))) &&
            (!excludes || !excludes.some(m => m.match(relativePath)))
        ));
    }

    // eslint-disable-next-line jsdoc/require-description
    /**
     * @returns {Object} a JSON compatible object.
     */
    toJSON() {
        if (this.patterns.length === 1) {
            return {
                ...patternToJson(this.patterns[0]),
                basePath: this.basePath
            };
        }
        return {
            AND: this.patterns.map(patternToJson),
            basePath: this.basePath
        };
    }

    // eslint-disable-next-line jsdoc/require-description
    /**
     * @returns {Object} an object to display by `console.log()`.
     */
    [util.inspect.custom]() {
        return this.toJSON();
    }
}

export { OverrideTester };