UNPKG

6.1 kBJavaScriptView Raw
1"use strict";
2
3const fs = require("fs");
4const path = require("path");
5
6/**
7 * Protège les caractères spéciaux des expressions rationnelles.
8 *
9 * @param {string} pattern Les caractères.
10 * @return {string} Les caractères protégés.
11 */
12const sanitize = function (pattern) {
13 return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14};
15
16/**
17 * Transforme un patron en expression rationnelle.
18 *
19 * @param {string} pattern Le patron.
20 * @return {Object.<string, Object>} La marque pour la négation et l'expression
21 * rationnelle.
22 */
23const compile = function (pattern) {
24 const negate = pattern.startsWith("!");
25 const glob = pattern.replace(/^!/, "");
26 if ("**" === glob) {
27 return { negate, "regexp": /.*/ };
28 }
29
30 let regexp = glob.startsWith("/") ? "^"
31 : "^(.*/)?";
32
33 for (let i = 0; i < glob.length; ++i) {
34 if ("/" === glob[i]) {
35 if ("/**/" === glob.substr(i, 4)) {
36 regexp += "/.*/";
37 i += 3;
38 } else if ("/**" === glob.substr(i, 3)) {
39 if (glob.length === i + 3) {
40 regexp += "/.*";
41 i += 2;
42 } else {
43 throw new Error(pattern + ": ** non suivi d'un slash");
44 }
45 } else {
46 regexp += "/";
47 }
48 } else if ("*" === glob[i]) {
49 // Si c'est le dernier caractère ou qu'il n'est pas suivi par
50 // étoile.
51 if (glob.length === i + 1 || "*" !== glob[i + 1]) {
52 regexp += "[^/]*";
53 // Sinon : ce n'est pas le dernier caractère et le prochain est
54 // une étoile.
55 } else if (0 === i) {
56 regexp += ".*";
57 ++i;
58 } else {
59 throw new Error(pattern + ": ** non précédé d'un slash");
60 }
61 } else if ("?" === glob[i]) {
62 regexp += "[^/]";
63 } else if ("[" === glob[i]) {
64 regexp += "[";
65 for (++i; i < glob.length; ++i) {
66 if ("]" === glob[i]) {
67 regexp += "]";
68 break;
69 } else {
70 regexp += sanitize(glob[i]);
71 }
72 }
73 if (i === glob.length) {
74 throw new Error(pattern + ": ] manquant");
75 }
76 } else {
77 regexp += sanitize(glob[i]);
78 }
79 }
80
81 if (negate) {
82 if (regexp.endsWith("/")) {
83 regexp += ".*";
84 } else {
85 regexp += "(/.*)?";
86 }
87 } else if (!regexp.endsWith("/")) {
88 regexp += "/?";
89 }
90 regexp += "$";
91
92 return { negate, "regexp": new RegExp(regexp) };
93};
94
95/**
96 * Teste si un fichier respecte un des patrons.
97 *
98 * @param {string} file L’adresse du fichier qui sera vérifié.
99 * @param {Array.<string>} patterns La liste des patrons.
100 * @param {string} root L’adresse du répertoire où se trouve le
101 * dossier <code>.metalint/</code>.
102 * @param {boolean} directory La marque indiquant si le fichier est un
103 * répertoire.
104 * @return {string} <code>"MATCHED"</code> si le fichier respecte au moins un
105 * patron ; <code>"NEGATE"</code> si le fichier ne respecte
106 * pas un patron négatif ; sinon <code>"NONE"</code>.
107 */
108const exec = function (file, patterns, root, directory) {
109 const relative = "/" + path.relative(root, path.join(process.cwd(), file)) +
110 (directory ? "/" : "");
111
112 let result = "NONE";
113 for (const pattern of patterns) {
114 if (pattern.negate) {
115 if (pattern.regexp.test(relative)) {
116 return "NEGATE";
117 }
118 } else if ("NONE" === result && pattern.regexp.test(relative)) {
119 result = "MATCHED";
120 }
121 }
122 return result;
123};
124
125const deep = function (base, patterns, root) {
126 const directory = fs.lstatSync(base).isDirectory();
127 const result = exec(base, patterns, root, directory);
128 if ("NEGATE" === result) {
129 return [];
130 }
131
132 const files = [];
133 if ("MATCHED" === result) {
134 files.push(base);
135 }
136 if (directory) {
137 for (const file of fs.readdirSync(base)) {
138 files.push(...deep(path.join(base, file), patterns, root));
139 }
140 }
141 return files;
142};
143
144/**
145 * Vérifie si un fichier respecte un des patrons.
146 *
147 * @param {string} file L’adresse du fichier qui sera vérifié.
148 * @param {Array.<string>} patterns La liste des patrons.
149 * @param {string} root L’adresse du répertoire où se trouve le
150 * dossier <code>.metalint/</code>.
151 * @param {boolean} directory La marque indiquant si le fichier est un
152 * répertoire.
153 * @return {boolean} <code>true</code> si le fichier respectent au moins un
154 * patron ; sinon <code>false</code>.
155 */
156const test = function (file, patterns, root, directory) {
157 return "MATCHED" === exec(file, patterns.map(compile), root, directory);
158};
159
160/**
161 * Récupère toute l’arborescence des fichiers respectant un des patrons.
162 *
163 * @param {string} bases La liste des fichiers / répertoires servant
164 * de racine pour l’arborescence.
165 * @param {Array.<string>} patterns La liste des patrons.
166 * @param {string} root L’adresse du répertoire où se trouve le
167 * dossier <code>.metalint/</code>.
168 * @return {Array.<string>} La liste des fichiers respectant un des patrons.
169 */
170const walk = function (bases, patterns, root) {
171 if (0 === bases.length) {
172 return deep(".", patterns.map(compile), root);
173 }
174
175 const files = [];
176 for (const base of bases) {
177 files.push(...deep(base, patterns.map(compile), root));
178 }
179 return files;
180};
181
182module.exports = { test, walk };