1 | /**
|
2 | * @module normalize
|
3 | */
|
4 |
|
5 | ;
|
6 |
|
7 | const fs = require("fs");
|
8 | const path = require("path");
|
9 | const jsonlint = require("jsonlint");
|
10 | const SEVERITY = require("./severity");
|
11 |
|
12 | /**
|
13 | * La liste des formaters.
|
14 | *
|
15 | * @constant {Array.<string>} FORMATTERS
|
16 | */
|
17 | const FORMATTERS = fs.readdirSync(path.join(__dirname, "formatter"));
|
18 |
|
19 | /**
|
20 | * La liste des enrobages.
|
21 | *
|
22 | * @constant {Array.<string>} WRAPPERS
|
23 | */
|
24 | const WRAPPERS = fs.readdirSync(path.join(__dirname, "wrapper"));
|
25 |
|
26 | /**
|
27 | * Fusionne deux objets.
|
28 | *
|
29 | * @param {*} first Le premier objet.
|
30 | * @param {*} second Le second objet.
|
31 | * @returns {*} La fusion des deux objets.
|
32 | */
|
33 | const merge = function (first, second) {
|
34 | let third;
|
35 |
|
36 | if (Array.isArray(first) && Array.isArray(second)) {
|
37 | third = first.concat(second);
|
38 | } else if ("object" === typeof first && !Array.isArray(first) &&
|
39 | "object" === typeof second && !Array.isArray(second)) {
|
40 | third = {};
|
41 | for (const key of new Set([...Object.keys(first),
|
42 | ...Object.keys(second)])) {
|
43 | // Si la propriété est dans les deux objets.
|
44 | if (key in first && key in second) {
|
45 | third[key] = merge(first[key], second[key]);
|
46 | // Si la propriété est seulement dans le premier objet.
|
47 | } else if (key in first) {
|
48 | third[key] = first[key];
|
49 | // Si la propriété est seulement dans le second objet.
|
50 | } else {
|
51 | third[key] = second[key];
|
52 | }
|
53 | }
|
54 | } else {
|
55 | third = second;
|
56 | }
|
57 |
|
58 | return third;
|
59 | };
|
60 |
|
61 | /**
|
62 | * Lit un fichier contenant un objet JSON.
|
63 | *
|
64 | * @param {string} file L’adresse du fichier qui sera lu.
|
65 | * @returns {object} L’objet JSON récupéré.
|
66 | */
|
67 | const read = function (file) {
|
68 | const json = fs.readFileSync(file, "utf-8");
|
69 | try {
|
70 | return jsonlint.parse(json);
|
71 | } catch (err) {
|
72 | throw new Error(file + ": " + err.message);
|
73 | }
|
74 | };
|
75 |
|
76 | /**
|
77 | * Normalise la propriété <code>"patterns"</code>.
|
78 | *
|
79 | * @param {*} rotten La valeur de la proptiété
|
80 | * <code>"patterns"</code>.
|
81 | * @param {Array.<string>} auto La valeur par défaut.
|
82 | * @param {object} [overwriting={}] Les valeurs passées dans la ligne de
|
83 | * commande pour surcharger la
|
84 | * configuration.
|
85 | * @returns {Array.<string>} La valeur normalisée.
|
86 | */
|
87 | const patterns = function (rotten, auto, overwriting = {}) {
|
88 | const interim = "patterns" in overwriting ? overwriting.patterns
|
89 | : rotten;
|
90 |
|
91 | let standard;
|
92 | if (undefined === interim) {
|
93 | standard = auto;
|
94 | } else if ("string" === typeof interim) {
|
95 | standard = [interim];
|
96 | } else if (Array.isArray(interim)) {
|
97 | standard = interim;
|
98 | } else {
|
99 | throw new Error("property 'patterns' is incorrect type (string and" +
|
100 | " array are accepted).");
|
101 | }
|
102 |
|
103 | return standard;
|
104 | };
|
105 |
|
106 | /**
|
107 | * Normalise la propriété <code>"level"</code>.
|
108 | *
|
109 | * @param {*} rotten La valeur de la proptiété
|
110 | * <code>"level"</code>.
|
111 | * @param {number} auto La valeur par défaut.
|
112 | * @param {object} [overwriting={}] Les valeurs passées dans la ligne de
|
113 | * commande pour surcharger la configuration.
|
114 | * @returns {number} La valeur normalisée.
|
115 | */
|
116 | const level = function (rotten, auto, overwriting = {}) {
|
117 | const interim = "level" in overwriting ? overwriting.level
|
118 | : rotten;
|
119 |
|
120 | let standard;
|
121 | if (undefined === interim) {
|
122 | standard = auto;
|
123 | } else if ("string" === typeof interim) {
|
124 | if (interim.toUpperCase() in SEVERITY) {
|
125 | standard = SEVERITY[interim.toUpperCase()];
|
126 | if (standard > auto) {
|
127 | standard = auto;
|
128 | }
|
129 | } else {
|
130 | throw new Error("value of property 'level' is unknown (possibles" +
|
131 | " values : 'off', 'fatal', 'error', 'warn' and" +
|
132 | " 'info').");
|
133 | }
|
134 | } else {
|
135 | throw new Error("property 'level' is incorrect type (only string is" +
|
136 | " accepted).");
|
137 | }
|
138 |
|
139 | return standard;
|
140 | };
|
141 |
|
142 | /**
|
143 | * Normalise la propriété <code>"formatter"</code>.
|
144 | *
|
145 | * @param {*} rotten La valeur de la proptiété
|
146 | * <code>"formatter"</code>.
|
147 | * @param {string} auto La valeur par défaut.
|
148 | * @param {string} root L’adresse du répertoire où se trouve le
|
149 | * dossier <code>.metalint/</code>.
|
150 | * @param {object} [overwriting={}] Les valeurs passées dans la ligne de
|
151 | * commande pour surcharger la configuration.
|
152 | * @returns {object} La valeur normalisée.
|
153 | */
|
154 | const formatter = function (rotten, auto, root, overwriting = {}) {
|
155 | const interim = "formatter" in overwriting ? overwriting.formatter
|
156 | : rotten;
|
157 |
|
158 | let standard;
|
159 | if (undefined === interim) {
|
160 | standard = require("./formatter/" + auto);
|
161 | } else if ("string" === typeof interim) {
|
162 | if (FORMATTERS.includes(interim.toLowerCase() + ".js")) {
|
163 | standard = require("./formatter/" + interim.toLowerCase() + ".js");
|
164 | } else if (interim.startsWith(".")) {
|
165 | standard = require(path.join(root, interim));
|
166 | } else {
|
167 | standard = require(interim);
|
168 | }
|
169 | } else {
|
170 | throw new Error("property 'formatter' is incorrect type (only string" +
|
171 | " is accepted).");
|
172 | }
|
173 | return standard;
|
174 | };
|
175 |
|
176 | /**
|
177 | * Normalise la propriété <code>"output"</code>.
|
178 | *
|
179 | * @param {*} rotten La valeur de la proptiété
|
180 | * <code>"output"</code>.
|
181 | * @param {object} auto La valeur par défaut.
|
182 | * @param {string} root L’adresse du répertoire où se trouve le
|
183 | * dossier <code>.metalint/</code>.
|
184 | * @param {object} [overwriting={}] Les valeurs passées dans la ligne de
|
185 | * commande pour surcharger la configuration.
|
186 | * @returns {object} La valeur normalisée.
|
187 | */
|
188 | const output = function (rotten, auto, root, overwriting = {}) {
|
189 | const interim = "output" in overwriting ? overwriting.output
|
190 | : rotten;
|
191 |
|
192 | let standard;
|
193 | if (undefined === interim || null === interim) {
|
194 | standard = auto;
|
195 | } else if ("string" === typeof interim) {
|
196 | let fd;
|
197 | try {
|
198 | if (interim.startsWith(".")) {
|
199 | fd = fs.openSync(path.join(root, interim), "w");
|
200 | } else {
|
201 | fd = fs.openSync(interim, "w");
|
202 | }
|
203 | } catch (_) {
|
204 | throw new Error("permission denied to open output file '" +
|
205 | interim + "'.");
|
206 | }
|
207 | standard = fs.createWriteStream(null, { fd });
|
208 | } else {
|
209 | throw new Error("property 'output' is incorrect type (only string is" +
|
210 | " accepted).");
|
211 | }
|
212 | return standard;
|
213 | };
|
214 |
|
215 | /**
|
216 | * Normalise la propriété <code>"options"</code>.
|
217 | *
|
218 | * @param {*} rotten La valeur de la proptiété
|
219 | * <code>"options"</code>.
|
220 | * @param {object} auto La valeur par défaut.
|
221 | * @param {object} [overwriting={}] Les valeurs passées dans la ligne de
|
222 | * commande pour surcharger la configuration.
|
223 | * @returns {object} La valeur normalisée.
|
224 | */
|
225 | const options = function (rotten, auto, overwriting = {}) {
|
226 | let standard;
|
227 | // Si les options ne sont pas spécifiées ou si le formateur est surchargé :
|
228 | // utiliser les options par défaut.
|
229 | if (undefined === rotten || "formatter" in overwriting) {
|
230 | standard = auto;
|
231 | } else if ("object" === typeof rotten) {
|
232 | standard = rotten;
|
233 | } else {
|
234 | throw new Error("property 'options' is incorrect type (only object is" +
|
235 | " accepted).");
|
236 | }
|
237 | return standard;
|
238 | };
|
239 |
|
240 | /**
|
241 | * Normalise la propriété <code>"reporters"</code>.
|
242 | *
|
243 | * @param {*} rottens La valeur de la proptiété
|
244 | * <code>"reporters"</code>.
|
245 | * @param {object} auto Les valeurs par défaut.
|
246 | * @param {string} root L’adresse du répertoire où se trouve le dossier
|
247 | * <code>.metalint/</code>.
|
248 | * @param {object} overwriting Les valeurs passées dans la ligne de commande
|
249 | * pour surcharger la configuration.
|
250 | * @returns {object} La valeur normalisée.
|
251 | */
|
252 | const reporters = function (rottens, auto, root, overwriting) {
|
253 | let standards;
|
254 | if (undefined === rottens) {
|
255 | const Formatter = formatter(undefined, "console", root, overwriting);
|
256 | standards = [
|
257 | new Formatter(level(undefined, auto.level),
|
258 | output(undefined, process.stdout, root, overwriting),
|
259 | options(undefined, {}, overwriting))
|
260 | ];
|
261 | } else if (Array.isArray(rottens)) {
|
262 | // Si le formateur ou le fichier de sortie sont surchargés : garder
|
263 | // seulement le premier rapporteur.
|
264 | if ("formatter" in overwriting || "output" in overwriting) {
|
265 | if (0 === rottens.length) {
|
266 | const Formatter = formatter(undefined, "console", root,
|
267 | overwriting);
|
268 | standards = [
|
269 | new Formatter(level(undefined, auto.level),
|
270 | output(undefined, process.stdout, root,
|
271 | overwriting),
|
272 | options(undefined, {}, overwriting))
|
273 | ];
|
274 | } else {
|
275 | const Formatter = formatter(rottens[0].formatter, "console",
|
276 | root, overwriting);
|
277 | standards = [
|
278 | new Formatter(level(rottens[0].level, auto.level),
|
279 | output(rottens[0].output, process.stdout,
|
280 | root, overwriting),
|
281 | options(rottens[0].options, {}, overwriting))
|
282 | ];
|
283 | }
|
284 | } else {
|
285 | standards = rottens.map(function (rotten) {
|
286 | const Formatter = formatter(rotten.formatter, "console", root);
|
287 | return new Formatter(level(rotten.level, auto.level),
|
288 | output(rotten.output, process.stdout,
|
289 | root),
|
290 | options(rotten.options, {}));
|
291 | });
|
292 | }
|
293 | } else if ("object" === typeof rottens) {
|
294 | const Formatter = formatter(rottens.formatter, "console", root,
|
295 | overwriting);
|
296 | standards = [
|
297 | new Formatter(level(rottens.level, auto.level),
|
298 | output(rottens.output, process.stdout, root,
|
299 | overwriting),
|
300 | options(rottens.options, {}, overwriting))
|
301 | ];
|
302 | } else {
|
303 | throw new Error("'reporters' incorrect type.");
|
304 | }
|
305 | return standards;
|
306 | };
|
307 |
|
308 | /**
|
309 | * Normalise le nom d'un enrobage (<em>wrapper</em>).
|
310 | *
|
311 | * @param {string} rotten Le nom d'un enrobage.
|
312 | * @param {string} root L’adresse du répertoire où se trouve le dossier
|
313 | * <code>.metalint/</code>.
|
314 | * @returns {string} Le nom normalisé.
|
315 | */
|
316 | const wrapper = function (rotten, root) {
|
317 | let standard;
|
318 | if (WRAPPERS.includes(rotten + ".js")) {
|
319 | standard = "./wrapper/" + rotten + ".js";
|
320 | } else if (rotten.startsWith(".")) {
|
321 | standard = path.join(root, rotten);
|
322 | } else {
|
323 | standard = rotten;
|
324 | }
|
325 | return standard;
|
326 | };
|
327 |
|
328 | /**
|
329 | * Normalise la propriété <code>"linters"</code>.
|
330 | *
|
331 | * @param {*} rottens Les valeurs de la proptiété <code>"linters"</code>.
|
332 | * @param {string} root L’adresse du répertoire où se trouve le dossier
|
333 | * <code>.metalint/</code>.
|
334 | * @param {string} dir Le répertoire où se trouve le fichier de
|
335 | * configuration <code>metalint.json</code>.
|
336 | * @returns {Array.<object>} La valeur normalisée.
|
337 | */
|
338 | const linters = function (rottens, root, dir) {
|
339 | const standards = {};
|
340 | if (undefined === rottens) {
|
341 | throw new Error("'checkers[].linters' is undefined.");
|
342 | } else if (null === rottens) {
|
343 | throw new Error("'checkers[].linters' is null.");
|
344 | // "linters": "foolint"
|
345 | } else if ("string" === typeof rottens) {
|
346 | standards[wrapper(rottens, root)] =
|
347 | read(path.join(dir, rottens + ".json"));
|
348 | // "linters": ["foolint", "barlint"]
|
349 | } else if (Array.isArray(rottens)) {
|
350 | for (const linter of rottens) {
|
351 | standards[wrapper(linter, root)] =
|
352 | read(path.join(dir, linter + ".json"));
|
353 | }
|
354 | // "linters": { "foolint": ..., "barlint": ... }
|
355 | } else if ("object" === typeof rottens) {
|
356 | for (const linter in rottens) {
|
357 | // "linters": { "foolint": "qux.json" }
|
358 | if ("string" === typeof rottens[linter]) {
|
359 | standards[wrapper(linter, root)] =
|
360 | read(path.join(dir, rottens[linter]));
|
361 | // "linters": { "foolint": [..., ...] }
|
362 | } else if (Array.isArray(rottens[linter])) {
|
363 | standards[wrapper(linter, root)] = {};
|
364 | for (const option of rottens[linter]) {
|
365 | if (null === option) {
|
366 | throw new Error("linter option is null.");
|
367 | // "linters": { "foolint": ["qux.json", ...] }
|
368 | } else if ("string" === typeof option) {
|
369 | standards[wrapper(linter, root)] =
|
370 | merge(standards[wrapper(linter, root)],
|
371 | read(path.join(dir, option)));
|
372 | // "linters": { "foolint": [{ "qux": ..., ... }, ...]
|
373 | } else if ("object" === typeof option) {
|
374 | standards[wrapper(linter, root)] =
|
375 | merge(standards[wrapper(linter, root)], option);
|
376 | } else {
|
377 | throw new Error("linter option incorrect type.");
|
378 | }
|
379 | }
|
380 | // "linters": { "foolint": { "qux": ..., "corge": ... } }
|
381 | // "linters": { "foolint": null }
|
382 | } else if ("object" === typeof rottens[linter]) {
|
383 | standards[wrapper(linter, root)] = rottens[linter];
|
384 | } else {
|
385 | throw new Error("linter incorrect type.");
|
386 | }
|
387 | }
|
388 | } else {
|
389 | throw new Error("'checkers[].linters' incorrect type.");
|
390 | }
|
391 | return standards;
|
392 | };
|
393 |
|
394 | /**
|
395 | * Normalise la propriété <code>"checkers"</code>.
|
396 | *
|
397 | * @param {*} rottens La valeur de la proptiété
|
398 | * <code>"checkers"</code>.
|
399 | * @param {object.<string, *>} auto Les valeurs par défaut.
|
400 | * @param {string} root L’adresse du répertoire où se trouve le
|
401 | * dossier <code>.metalint/</code>.
|
402 | * @param {string} dir Le répertoire où se trouve le fichier de
|
403 | * configuration <code>metalint.json</code>.
|
404 | * @returns {Array.<object>} La valeur normalisée.
|
405 | */
|
406 | const checkers = function (rottens, auto, root, dir) {
|
407 | let standards;
|
408 | if (Array.isArray(rottens)) {
|
409 | if (0 === rottens.length) {
|
410 | throw new Error("'checkers' is empty.");
|
411 | } else {
|
412 | standards = rottens.map(function (rotten) {
|
413 | return {
|
414 | "patterns": patterns(rotten.patterns, ["**"]),
|
415 | "level": level(rotten.level, auto.level),
|
416 | "linters": linters(rotten.linters, root, dir)
|
417 | };
|
418 | });
|
419 | }
|
420 | } else {
|
421 | throw new Error("'checkers' is not an array.");
|
422 | }
|
423 | return standards;
|
424 | };
|
425 |
|
426 | /**
|
427 | * Normalise la configuration. La structure de l’objet JSON contenant la
|
428 | * configuration est souple pour rendre le fichier moins verbeuse. Cette
|
429 | * fonction renseigne les valeurs par défaut pour les propriétes non-présentes.
|
430 | *
|
431 | * @param {object} rotten L’objet JSON contenant la configuration.
|
432 | * @param {string} root L’adresse du répertoire où se trouve le dossier
|
433 | * <code>.metalint/</code>.
|
434 | * @param {string} dir Le répertoire où se trouve le fichier de
|
435 | * configuration.
|
436 | * @param {object} overwriting Les valeurs passées dans la ligne de commande
|
437 | * pour surcharger la configuration.
|
438 | * @returns {object} L’objet JSON normalisé.
|
439 | */
|
440 | const normalize = function (rotten, root, dir, overwriting) {
|
441 | const standard = {
|
442 | "patterns": patterns(rotten.patterns, ["**"], overwriting),
|
443 | "level": level(rotten.level, SEVERITY.INFO, overwriting)
|
444 | };
|
445 | standard.reporters = reporters(rotten.reporters, standard, root,
|
446 | overwriting);
|
447 | standard.checkers = checkers(rotten.checkers, standard, root, dir);
|
448 | return standard;
|
449 | };
|
450 |
|
451 | module.exports = normalize;
|