• 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 `IgnorePattern` class.
15 *
16 * `IgnorePattern` class has the set of glob patterns and the base path.
17 *
18 * It provides two static methods.
19 *
20 * - `IgnorePattern.createDefaultIgnore(cwd)`
21 *      Create the default predicate function.
22 * - `IgnorePattern.createIgnore(ignorePatterns)`
23 *      Create the predicate function from multiple `IgnorePattern` objects.
24 *
25 * It provides two properties and a method.
26 *
27 * - `patterns`
28 *      The glob patterns that ignore to lint.
29 * - `basePath`
30 *      The base path of the glob patterns. If absolute paths existed in the
31 *      glob patterns, those are handled as relative paths to the base path.
32 * - `getPatternsRelativeTo(basePath)`
33 *      Get `patterns` as modified for a given base path. It modifies the
34 *      absolute paths in the patterns as prepending the difference of two base
35 *      paths.
36 *
37 * `ConfigArrayFactory` creates `IgnorePattern` objects when it processes
38 * `ignorePatterns` properties.
39 *
40 * @author Toru Nagashima <https://github.com/mysticatea>
41 */
42"use strict";
43
44//------------------------------------------------------------------------------
45// Requirements
46//------------------------------------------------------------------------------
47
48const assert = require("assert");
49const path = require("path");
50const ignore = require("ignore");
51const debug = require("debug")("eslint:ignore-pattern");
52
53/** @typedef {ReturnType<import("ignore").default>} Ignore */
54
55//------------------------------------------------------------------------------
56// Helpers
57//------------------------------------------------------------------------------
58
59/**
60 * Get the path to the common ancestor directory of given paths.
61 * @param {string[]} sourcePaths The paths to calculate the common ancestor.
62 * @returns {string} The path to the common ancestor directory.
63 */
64function getCommonAncestorPath(sourcePaths) {
65    let result = sourcePaths[0];
66
67    for (let i = 1; i < sourcePaths.length; ++i) {
68        const a = result;
69        const b = sourcePaths[i];
70
71        // Set the shorter one (it's the common ancestor if one includes the other).
72        result = a.length < b.length ? a : b;
73
74        // Set the common ancestor.
75        for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
76            if (a[j] !== b[j]) {
77                result = a.slice(0, lastSepPos);
78                break;
79            }
80            if (a[j] === path.sep) {
81                lastSepPos = j;
82            }
83        }
84    }
85
86    let resolvedResult = result || path.sep;
87
88    // if Windows common ancestor is root of drive must have trailing slash to be absolute.
89    if (resolvedResult && resolvedResult.endsWith(":") && process.platform === "win32") {
90        resolvedResult += path.sep;
91    }
92    return resolvedResult;
93}
94
95/**
96 * Make relative path.
97 * @param {string} from The source path to get relative path.
98 * @param {string} to The destination path to get relative path.
99 * @returns {string} The relative path.
100 */
101function relative(from, to) {
102    const relPath = path.relative(from, to);
103
104    if (path.sep === "/") {
105        return relPath;
106    }
107    return relPath.split(path.sep).join("/");
108}
109
110/**
111 * Get the trailing slash if existed.
112 * @param {string} filePath The path to check.
113 * @returns {string} The trailing slash if existed.
114 */
115function dirSuffix(filePath) {
116    const isDir = (
117        filePath.endsWith(path.sep) ||
118        (process.platform === "win32" && filePath.endsWith("/"))
119    );
120
121    return isDir ? "/" : "";
122}
123
124const DefaultPatterns = Object.freeze(["/**/node_modules/*"]);
125const DotPatterns = Object.freeze([".*", "!.eslintrc.*", "!../"]);
126
127//------------------------------------------------------------------------------
128// Public
129//------------------------------------------------------------------------------
130
131class IgnorePattern {
132
133    /**
134     * The default patterns.
135     * @type {string[]}
136     */
137    static get DefaultPatterns() {
138        return DefaultPatterns;
139    }
140
141    /**
142     * Create the default predicate function.
143     * @param {string} cwd The current working directory.
144     * @returns {((filePath:string, dot:boolean) => boolean) & {basePath:string; patterns:string[]}}
145     * The preficate function.
146     * The first argument is an absolute path that is checked.
147     * The second argument is the flag to not ignore dotfiles.
148     * If the predicate function returned `true`, it means the path should be ignored.
149     */
150    static createDefaultIgnore(cwd) {
151        return this.createIgnore([new IgnorePattern(DefaultPatterns, cwd)]);
152    }
153
154    /**
155     * Create the predicate function from multiple `IgnorePattern` objects.
156     * @param {IgnorePattern[]} ignorePatterns The list of ignore patterns.
157     * @returns {((filePath:string, dot?:boolean) => boolean) & {basePath:string; patterns:string[]}}
158     * The preficate function.
159     * The first argument is an absolute path that is checked.
160     * The second argument is the flag to not ignore dotfiles.
161     * If the predicate function returned `true`, it means the path should be ignored.
162     */
163    static createIgnore(ignorePatterns) {
164        debug("Create with: %o", ignorePatterns);
165
166        const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath));
167        const patterns = [].concat(
168            ...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath))
169        );
170        const ig = ignore().add([...DotPatterns, ...patterns]);
171        const dotIg = ignore().add(patterns);
172
173        debug("  processed: %o", { basePath, patterns });
174
175        return Object.assign(
176            (filePath, dot = false) => {
177                assert(path.isAbsolute(filePath), "'filePath' should be an absolute path.");
178                const relPathRaw = relative(basePath, filePath);
179                const relPath = relPathRaw && (relPathRaw + dirSuffix(filePath));
180                const adoptedIg = dot ? dotIg : ig;
181                const result = relPath !== "" && adoptedIg.ignores(relPath);
182
183                debug("Check", { filePath, dot, relativePath: relPath, result });
184                return result;
185            },
186            { basePath, patterns }
187        );
188    }
189
190    /**
191     * Initialize a new `IgnorePattern` instance.
192     * @param {string[]} patterns The glob patterns that ignore to lint.
193     * @param {string} basePath The base path of `patterns`.
194     */
195    constructor(patterns, basePath) {
196        assert(path.isAbsolute(basePath), "'basePath' should be an absolute path.");
197
198        /**
199         * The glob patterns that ignore to lint.
200         * @type {string[]}
201         */
202        this.patterns = patterns;
203
204        /**
205         * The base path of `patterns`.
206         * @type {string}
207         */
208        this.basePath = basePath;
209
210        /**
211         * If `true` then patterns which don't start with `/` will match the paths to the outside of `basePath`. Defaults to `false`.
212         *
213         * It's set `true` for `.eslintignore`, `package.json`, and `--ignore-path` for backward compatibility.
214         * It's `false` as-is for `ignorePatterns` property in config files.
215         * @type {boolean}
216         */
217        this.loose = false;
218    }
219
220    /**
221     * Get `patterns` as modified for a given base path. It modifies the
222     * absolute paths in the patterns as prepending the difference of two base
223     * paths.
224     * @param {string} newBasePath The base path.
225     * @returns {string[]} Modifired patterns.
226     */
227    getPatternsRelativeTo(newBasePath) {
228        assert(path.isAbsolute(newBasePath), "'newBasePath' should be an absolute path.");
229        const { basePath, loose, patterns } = this;
230
231        if (newBasePath === basePath) {
232            return patterns;
233        }
234        const prefix = `/${relative(newBasePath, basePath)}`;
235
236        return patterns.map(pattern => {
237            const negative = pattern.startsWith("!");
238            const head = negative ? "!" : "";
239            const body = negative ? pattern.slice(1) : pattern;
240
241            if (body.startsWith("/") || body.startsWith("../")) {
242                return `${head}${prefix}${body}`;
243            }
244            return loose ? pattern : `${head}${prefix}/**/${body}`;
245        });
246    }
247}
248
249module.exports = { IgnorePattern };
250