• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * STOP!!! DO NOT MODIFY.
3 *
4 * This file is part of the ongoing work to move the eslintrc-style config
5 * system into the @eslint/eslintrc package. This file needs to remain
6 * unchanged in order for this work to proceed.
7 *
8 * If you think you need to change this file, please contact @nzakas first.
9 *
10 * Thanks in advance for your cooperation.
11 */
12
13/**
14 * @fileoverview `OverrideTester` class.
15 *
16 * `OverrideTester` class handles `files` property and `excludedFiles` property
17 * of `overrides` config.
18 *
19 * It provides one method.
20 *
21 * - `test(filePath)`
22 *      Test if a file path matches the pair of `files` property and
23 *      `excludedFiles` property. The `filePath` argument must be an absolute
24 *      path.
25 *
26 * `ConfigArrayFactory` creates `OverrideTester` objects when it processes
27 * `overrides` properties.
28 *
29 * @author Toru Nagashima <https://github.com/mysticatea>
30 */
31"use strict";
32
33const assert = require("assert");
34const path = require("path");
35const util = require("util");
36const { Minimatch } = require("minimatch");
37const minimatchOpts = { dot: true, matchBase: true };
38
39/**
40 * @typedef {Object} Pattern
41 * @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
42 * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
43 */
44
45/**
46 * Normalize a given pattern to an array.
47 * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
48 * @returns {string[]|null} Normalized patterns.
49 * @private
50 */
51function normalizePatterns(patterns) {
52    if (Array.isArray(patterns)) {
53        return patterns.filter(Boolean);
54    }
55    if (typeof patterns === "string" && patterns) {
56        return [patterns];
57    }
58    return [];
59}
60
61/**
62 * Create the matchers of given patterns.
63 * @param {string[]} patterns The patterns.
64 * @returns {InstanceType<Minimatch>[] | null} The matchers.
65 */
66function toMatcher(patterns) {
67    if (patterns.length === 0) {
68        return null;
69    }
70    return patterns.map(pattern => {
71        if (/^\.[/\\]/u.test(pattern)) {
72            return new Minimatch(
73                pattern.slice(2),
74
75                // `./*.js` should not match with `subdir/foo.js`
76                { ...minimatchOpts, matchBase: false }
77            );
78        }
79        return new Minimatch(pattern, minimatchOpts);
80    });
81}
82
83/**
84 * Convert a given matcher to string.
85 * @param {Pattern} matchers The matchers.
86 * @returns {string} The string expression of the matcher.
87 */
88function patternToJson({ includes, excludes }) {
89    return {
90        includes: includes && includes.map(m => m.pattern),
91        excludes: excludes && excludes.map(m => m.pattern)
92    };
93}
94
95/**
96 * The class to test given paths are matched by the patterns.
97 */
98class OverrideTester {
99
100    /**
101     * Create a tester with given criteria.
102     * If there are no criteria, returns `null`.
103     * @param {string|string[]} files The glob patterns for included files.
104     * @param {string|string[]} excludedFiles The glob patterns for excluded files.
105     * @param {string} basePath The path to the base directory to test paths.
106     * @returns {OverrideTester|null} The created instance or `null`.
107     */
108    static create(files, excludedFiles, basePath) {
109        const includePatterns = normalizePatterns(files);
110        const excludePatterns = normalizePatterns(excludedFiles);
111        let endsWithWildcard = false;
112
113        if (includePatterns.length === 0) {
114            return null;
115        }
116
117        // Rejects absolute paths or relative paths to parents.
118        for (const pattern of includePatterns) {
119            if (path.isAbsolute(pattern) || pattern.includes("..")) {
120                throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
121            }
122            if (pattern.endsWith("*")) {
123                endsWithWildcard = true;
124            }
125        }
126        for (const pattern of excludePatterns) {
127            if (path.isAbsolute(pattern) || pattern.includes("..")) {
128                throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
129            }
130        }
131
132        const includes = toMatcher(includePatterns);
133        const excludes = toMatcher(excludePatterns);
134
135        return new OverrideTester(
136            [{ includes, excludes }],
137            basePath,
138            endsWithWildcard
139        );
140    }
141
142    /**
143     * Combine two testers by logical and.
144     * If either of the testers was `null`, returns the other tester.
145     * The `basePath` property of the two must be the same value.
146     * @param {OverrideTester|null} a A tester.
147     * @param {OverrideTester|null} b Another tester.
148     * @returns {OverrideTester|null} Combined tester.
149     */
150    static and(a, b) {
151        if (!b) {
152            return a && new OverrideTester(
153                a.patterns,
154                a.basePath,
155                a.endsWithWildcard
156            );
157        }
158        if (!a) {
159            return new OverrideTester(
160                b.patterns,
161                b.basePath,
162                b.endsWithWildcard
163            );
164        }
165
166        assert.strictEqual(a.basePath, b.basePath);
167        return new OverrideTester(
168            a.patterns.concat(b.patterns),
169            a.basePath,
170            a.endsWithWildcard || b.endsWithWildcard
171        );
172    }
173
174    /**
175     * Initialize this instance.
176     * @param {Pattern[]} patterns The matchers.
177     * @param {string} basePath The base path.
178     * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
179     */
180    constructor(patterns, basePath, endsWithWildcard = false) {
181
182        /** @type {Pattern[]} */
183        this.patterns = patterns;
184
185        /** @type {string} */
186        this.basePath = basePath;
187
188        /** @type {boolean} */
189        this.endsWithWildcard = endsWithWildcard;
190    }
191
192    /**
193     * Test if a given path is matched or not.
194     * @param {string} filePath The absolute path to the target file.
195     * @returns {boolean} `true` if the path was matched.
196     */
197    test(filePath) {
198        if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
199            throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
200        }
201        const relativePath = path.relative(this.basePath, filePath);
202
203        return this.patterns.every(({ includes, excludes }) => (
204            (!includes || includes.some(m => m.match(relativePath))) &&
205            (!excludes || !excludes.some(m => m.match(relativePath)))
206        ));
207    }
208
209    // eslint-disable-next-line jsdoc/require-description
210    /**
211     * @returns {Object} a JSON compatible object.
212     */
213    toJSON() {
214        if (this.patterns.length === 1) {
215            return {
216                ...patternToJson(this.patterns[0]),
217                basePath: this.basePath
218            };
219        }
220        return {
221            AND: this.patterns.map(patternToJson),
222            basePath: this.basePath
223        };
224    }
225
226    // eslint-disable-next-line jsdoc/require-description
227    /**
228     * @returns {Object} an object to display by `console.log()`.
229     */
230    [util.inspect.custom]() {
231        return this.toJSON();
232    }
233}
234
235module.exports = { OverrideTester };
236