UNPKG

17.7 kBJavaScriptView Raw
1/**
2 * @module normalize
3 */
4
5"use strict";
6
7const fs = require("fs");
8const path = require("path");
9const jsonlint = require("jsonlint");
10const SEVERITY = require("./severity");
11
12/**
13 * La liste des formaters.
14 *
15 * @constant {Array.<string>} FORMATTERS
16 */
17const FORMATTERS = fs.readdirSync(path.join(__dirname, "formatter"));
18
19/**
20 * La liste des enrobages.
21 *
22 * @constant {Array.<string>} WRAPPERS
23 */
24const 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 */
33const 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 */
67const 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 */
87const 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 */
116const 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 */
154const 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 */
188const 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 */
225const 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 */
252const 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 */
316const 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 */
338const 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 */
406const 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 */
440const 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
451module.exports = normalize;