1 | ;
|
2 |
|
3 | const fs = require("fs");
|
4 | const 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 | */
|
12 | const 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 | */
|
23 | const 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 | */
|
108 | const 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 |
|
125 | const 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 | */
|
156 | const 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 | */
|
170 | const 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 |
|
182 | module.exports = { test, walk };
|