metalint 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +42 -9
  3. package/package.json +51 -35
  4. package/src/bin/argv.js +1 -1
  5. package/src/bin/index.js +10 -105
  6. package/src/core/configuration/flatten.js +12 -12
  7. package/src/core/configuration/normalize.js +2 -3
  8. package/src/core/formatter/checkstyle.js +9 -10
  9. package/src/core/formatter/console.js +31 -34
  10. package/src/core/formatter/csv.js +7 -7
  11. package/src/core/formatter/formatter.js +5 -18
  12. package/src/core/formatter/github.js +6 -6
  13. package/src/core/formatter/json.js +9 -9
  14. package/src/core/formatter/unix.js +6 -6
  15. package/src/core/index.js +128 -118
  16. package/src/core/results.js +86 -0
  17. package/src/core/utils/array.js +1 -1
  18. package/src/core/utils/glob.js +1 -1
  19. package/src/core/wrapper/addons-linter.js +3 -6
  20. package/src/core/wrapper/ajv.js +122 -0
  21. package/src/core/wrapper/csslint.js +1 -6
  22. package/src/core/wrapper/depcheck.js +148 -0
  23. package/src/core/wrapper/doiuse.js +3 -4
  24. package/src/core/wrapper/eslint.js +25 -7
  25. package/src/core/wrapper/htmlhint.js +1 -6
  26. package/src/core/wrapper/htmllint.js +1 -6
  27. package/src/core/wrapper/jshint.js +1 -6
  28. package/src/core/wrapper/npm-package-json-lint.js +1 -4
  29. package/src/core/wrapper/publint.js +136 -0
  30. package/src/core/wrapper/standard.js +7 -1
  31. package/src/core/wrapper/stylelint.js +4 -11
  32. package/src/core/wrapper/svglint.js +92 -0
  33. package/src/core/wrapper/wrapper.js +5 -18
  34. package/src/core/wrapper/yaml-lint.js +1 -1
  35. package/types/configuration/flatten.d.ts +2 -3
  36. package/types/configuration/normalize.d.ts +0 -1
  37. package/types/formatter/checkstyle.d.ts +9 -10
  38. package/types/formatter/console.d.ts +13 -16
  39. package/types/formatter/csv.d.ts +6 -6
  40. package/types/formatter/github.d.ts +7 -7
  41. package/types/formatter/json.d.ts +10 -10
  42. package/types/formatter/unix.d.ts +7 -7
  43. package/types/index.d.ts +20 -14
  44. package/types/results.d.ts +7 -0
  45. package/types/wrapper/ajv.d.ts +36 -0
  46. package/types/wrapper/depcheck.d.ts +36 -0
  47. package/types/wrapper/publint.d.ts +33 -0
  48. package/types/wrapper/stylelint.d.ts +2 -2
  49. package/types/wrapper/svglint.d.ts +36 -0
@@ -12,20 +12,6 @@ import { fileURLToPath } from "node:url";
12
12
  * @typedef {import("../../type/index.js").Notice} Notice
13
13
  */
14
14
 
15
- if (undefined === import.meta.resolve) {
16
- /**
17
- * Résous un chemin relatif à partir du module.
18
- *
19
- * @param {string} specifier Le chemin relatif vers un fichier ou un
20
- * répertoire.
21
- * @returns {string} L'URL absolue vers le fichier ou le répertoire.
22
- * @see https://nodejs.org/api/esm.html#importmetaresolvespecifier-parent
23
- */
24
- import.meta.resolve = (specifier) => {
25
- return new URL(specifier, import.meta.url).href;
26
- };
27
- }
28
-
29
15
  /**
30
16
  * La liste des fichiers JavaScript des formateurs.
31
17
  *
@@ -39,10 +25,11 @@ const scripts = await fs.readdir(fileURLToPath(import.meta.resolve("./")));
39
25
  * @constant {string[]} FORMATTERS
40
26
  */
41
27
  export const FORMATTERS = scripts
42
- // Enlever l'extension du fichier.
43
- .map((f) => f.slice(0, -3))
44
- // Enlever ce fichier qui n'est pas un vrai formateur.
45
- .filter((f) => "formatter" !== f);
28
+ // Garder seulement les fichiers JavaScript et enlever le fichier
29
+ // "formatter.js" qui n'est pas un vrai formateur.
30
+ .filter((f) => f.endsWith(".js") && "formatter.js" !== f)
31
+ // Enlever l'extension des fichiers.
32
+ .map((f) => f.slice(0, -3));
46
33
 
47
34
  /**
48
35
  * Le formateur parent.
@@ -9,7 +9,7 @@ import Severities from "../severities.js";
9
9
  import Formatter from "./formatter.js";
10
10
 
11
11
  /**
12
- * @typedef {NodeJS.WritableStream} WritableStream
12
+ * @typedef {import("node:stream").Writable} Writable
13
13
  * @typedef {import("../../type/index.js").Level} Level
14
14
  * @typedef {import("../../type/index.js").Notice} Notice
15
15
  */
@@ -23,17 +23,17 @@ export default class GitHubFormatter extends Formatter {
23
23
  /**
24
24
  * Le flux où écrire les résultats.
25
25
  *
26
- * @type {WritableStream}
26
+ * @type {Writable}
27
27
  */
28
28
  #writer;
29
29
 
30
30
  /**
31
31
  * Crée un formateur.
32
32
  *
33
- * @param {Level} level Le niveau de sévérité minimum
34
- * des notifications affichées.
35
- * @param {Object} options Les options du formateur.
36
- * @param {WritableStream} [options.writer] Le flux où écrire les résultats.
33
+ * @param {Level} level Le niveau de sévérité minimum des
34
+ * notifications affichées.
35
+ * @param {Object} options Les options du formateur.
36
+ * @param {Writable} [options.writer] Le flux où écrire les résultats.
37
37
  */
38
38
  constructor(level, options) {
39
39
  super(level);
@@ -8,13 +8,13 @@ import process from "node:process";
8
8
  import Formatter from "./formatter.js";
9
9
 
10
10
  /**
11
- * @typedef {NodeJS.WritableStream} WritableStream
11
+ * @typedef {import("node:stream").Writable} Writable
12
12
  * @typedef {import("../../type/index.js").Level} Level
13
13
  * @typedef {import("../../type/index.js").Notice} Notice
14
14
  */
15
15
 
16
16
  /**
17
- * Le formateur qui écrit les résultats brut (au format JSON). La seul
17
+ * Le formateur qui écrit les résultats bruts (au format JSON). La seule
18
18
  * altération des résultats est le remplacement des <code>undefined</code> par
19
19
  * des <code>null</code>.
20
20
  *
@@ -24,7 +24,7 @@ export default class JSONFormatter extends Formatter {
24
24
  /**
25
25
  * Le flux où écrire les résultats.
26
26
  *
27
- * @type {WritableStream}
27
+ * @type {Writable}
28
28
  */
29
29
  #writer;
30
30
 
@@ -46,12 +46,12 @@ export default class JSONFormatter extends Formatter {
46
46
  /**
47
47
  * Crée un formateur.
48
48
  *
49
- * @param {Level} level Le niveau de sévérité minimum
50
- * des notifications affichées.
51
- * @param {Object} options Les options du formateur.
52
- * @param {WritableStream} [options.writer] Le flux où écrire les résultats.
53
- * @param {number} [options.indent] La taille des indentations (en
54
- * espace).
49
+ * @param {Level} level Le niveau de sévérité minimum des
50
+ * notifications affichées.
51
+ * @param {Object} options Les options du formateur.
52
+ * @param {Writable} [options.writer] Le flux où écrire les résultats.
53
+ * @param {number} [options.indent] La taille des indentations (en
54
+ * espace).
55
55
  */
56
56
  constructor(level, options) {
57
57
  super(level);
@@ -8,7 +8,7 @@ import process from "node:process";
8
8
  import Formatter from "./formatter.js";
9
9
 
10
10
  /**
11
- * @typedef {NodeJS.WritableStream} WritableStream
11
+ * @typedef {import("node:stream").Writable} Writable
12
12
  * @typedef {import("../../type/index.js").Level} Level
13
13
  * @typedef {import("../../type/index.js").Notice} Notice
14
14
  */
@@ -21,7 +21,7 @@ export default class UnixFormatter extends Formatter {
21
21
  /**
22
22
  * Le flux où écrire les résultats.
23
23
  *
24
- * @type {WritableStream}
24
+ * @type {Writable}
25
25
  */
26
26
  #writer;
27
27
 
@@ -35,10 +35,10 @@ export default class UnixFormatter extends Formatter {
35
35
  /**
36
36
  * Crée un formateur.
37
37
  *
38
- * @param {Level} level Le niveau de sévérité minimum
39
- * des notifications affichées.
40
- * @param {Object} options Les options du formateur.
41
- * @param {WritableStream} [options.writer] Le flux où écrire les résultats.
38
+ * @param {Level} level Le niveau de sévérité minimum des
39
+ * notifications affichées.
40
+ * @param {Object} options Les options du formateur.
41
+ * @param {Writable} [options.writer] Le flux où écrire les résultats.
42
42
  */
43
43
  constructor(level, options) {
44
44
  super(level);
package/src/core/index.js CHANGED
@@ -4,8 +4,14 @@
4
4
  * @author Sébastien Règne
5
5
  */
6
6
 
7
+ import path from "node:path";
8
+ import process from "node:process";
9
+ import { pathToFileURL } from "node:url";
10
+ import flatten from "./configuration/flatten.js";
11
+ import normalize from "./configuration/normalize.js";
7
12
  import { mergeLinters } from "./configuration/override.js";
8
- import Severities from "./severities.js";
13
+ import Results from "./results.js";
14
+ import { exists } from "./utils/file.js";
9
15
  import Glob from "./utils/glob.js";
10
16
 
11
17
  /**
@@ -13,140 +19,144 @@ import Glob from "./utils/glob.js";
13
19
  * @typedef {import("../type/index.d.ts").Notice} Notice
14
20
  */
15
21
 
16
- /**
17
- * Compare deux notifications. En commençant par le numéro de la ligne, puis
18
- * celui de la colonne.
19
- *
20
- * @param {Notice} notice1 La première notification.
21
- * @param {Notice} notice2 La seconde notification.
22
- * @returns {number} Un nombre négatif si la 1<sup>re</sup> notification est
23
- * inférieure à la 2<sup>de</sup> ; <code>0</code> si elles
24
- * sont égales ; sinon un nombre positif.
25
- */
26
- const compare = function (notice1, notice2) {
27
- for (
28
- let i = 0;
29
- i < notice1.locations.length && i < notice2.locations.length;
30
- ++i
31
- ) {
32
- const locations1 = notice1.locations[i];
33
- const locations2 = notice2.locations[i];
34
-
35
- let diff = locations1.line - locations2.line;
36
- if (0 !== diff) {
37
- return diff;
38
- }
22
+ export default class Metalint {
23
+ #root;
24
+ #glob;
25
+ #checkers;
26
+ #formatters;
27
+ #cache = new Map();
28
+
29
+ static async fromFileSystem(options = {}) {
30
+ const config = options.config ?? ".metalint/metalint.config.js";
39
31
 
40
- diff = (locations1.column ?? -1) - (locations2.column ?? -1);
41
- if (0 !== diff) {
42
- return diff;
32
+ // Rechercher le fichier de configuration dans le répertoire courant,
33
+ // puis dans les parents, grands-parents...
34
+ let root = process.cwd();
35
+ while (!(await exists(path.join(root, config)))) {
36
+ // Si on est remonté à la racine.
37
+ if (path.join(root, "..") === root) {
38
+ throw new Error("No such config file.");
39
+ }
40
+ root = path.join(root, "..");
43
41
  }
42
+
43
+ // eslint-disable-next-line no-unsanitized/method
44
+ const { default: configuration } = await import(
45
+ pathToFileURL(path.join(root, config))
46
+ );
47
+ const { patterns, reporters, checkers } = flatten(
48
+ await normalize(configuration, {
49
+ dir: path.dirname(path.join(root, config)),
50
+ }),
51
+ {
52
+ fix: options.fix,
53
+ formatter: options.formatter,
54
+ level: options.level,
55
+ },
56
+ );
57
+ return new Metalint({ root, patterns, reporters, checkers });
44
58
  }
45
- return notice1.locations.length - notice2.locations.length;
46
- };
47
59
 
48
- const Results = class {
49
- /**
50
- * Les données des résultats.
51
- *
52
- * @type {Record<string, Notice[]|undefined>}
53
- */
54
- #data;
60
+ constructor(config) {
61
+ this.#root = config.root;
62
+ this.#glob = new Glob(config.patterns, { root: config.root });
55
63
 
56
- constructor(files) {
57
- this.#data = Object.fromEntries(files.map((f) => [f, undefined]));
64
+ this.#formatters = config.reporters.map((reporter) => {
65
+ // eslint-disable-next-line new-cap
66
+ return new reporter.formatter(reporter.level, reporter.options);
67
+ });
68
+
69
+ this.#checkers = config.checkers.map((checker) => ({
70
+ ...checker,
71
+ glob: new Glob(checker.patterns, { root: this.#root }),
72
+ overrides: checker.overrides.map((override) => ({
73
+ ...override,
74
+ glob: new Glob(override.patterns, { root: this.#root }),
75
+ })),
76
+ }));
58
77
  }
59
78
 
60
- add(file, notices) {
61
- // Ajouter un tableau vide dans les données pour indiquer que le fichier
62
- // a été analysé par au moins un linter.
63
- if (undefined === this.#data[file]) {
64
- this.#data[file] = [];
79
+ /**
80
+ * Vérifie (en appelant des linters) des répertoires et des fichiers.
81
+ *
82
+ * @param {string[]} bases Les répertoires et les fichiers.
83
+ * @returns {Promise<Record<string, Notice[]|undefined>>} Une promesse
84
+ * retournant la
85
+ * liste des
86
+ * notifications
87
+ * regroupées par
88
+ * fichier.
89
+ */
90
+ async lintFiles(bases) {
91
+ const files = [];
92
+ for (const base of bases) {
93
+ files.push(...(await this.#glob.walk(base)));
65
94
  }
66
- for (const notice of notices) {
67
- // Vérifier aussi le fichier de la notification car il peut être
68
- // différent du fichier d'origine (qui est peut être un répertoire
69
- // ou une archive).
70
- if (undefined === this.#data[notice.file]) {
71
- this.#data[notice.file] = [];
95
+ const results = new Results(files);
96
+ for (const file of files) {
97
+ for (const checker of this.#checkers) {
98
+ if (checker.glob.test(file)) {
99
+ let key = checker.patterns.join();
100
+ const values = [checker.linters];
101
+ for (const override of checker.overrides) {
102
+ if (override.glob.test(file)) {
103
+ key += "|" + override.patterns.join();
104
+ values.push(override.linters);
105
+ }
106
+ }
107
+
108
+ let wrappers = [];
109
+ if (this.#cache.has(key)) {
110
+ wrappers = this.#cache.get(key);
111
+ } else {
112
+ const linters = values.reduce(mergeLinters);
113
+ for (const linter of linters) {
114
+ wrappers.push(
115
+ // eslint-disable-next-line new-cap
116
+ new linter.wrapper(
117
+ {
118
+ fix: linter.fix,
119
+ level: linter.level,
120
+ root: this.#root,
121
+ files,
122
+ },
123
+ linter.options,
124
+ ),
125
+ );
126
+ }
127
+ this.#cache.set(key, wrappers);
128
+ }
129
+ for (const wrapper of wrappers) {
130
+ results.add(file, await wrapper.lint(file));
131
+ }
132
+ }
72
133
  }
73
- this.#data[notice.file].push({
74
- rule: undefined,
75
- severity: Severities.ERROR,
76
- locations: [],
77
- ...notice,
78
- });
79
134
  }
80
- }
81
135
 
82
- toObject() {
83
- // Trier les notifications.
84
- Object.values(this.#data)
85
- .filter((r) => undefined !== r)
86
- .forEach((r) => r.sort(compare));
87
- return this.#data;
136
+ return results.toObject();
88
137
  }
89
- };
90
138
 
91
- /**
92
- * Vérifie (en appelant des linters) une liste de fichiers.
93
- *
94
- * @param {string[]} files La liste des fichiers.
95
- * @param {FlattenedConfigChecker[]} checkers La liste des vérifications faites
96
- * sur les fichiers.
97
- * @param {string} root L'adresse du répertoire où se
98
- * trouve le répertoire
99
- * <code>.metalint/</code>.
100
- * @returns {Promise<Record<string, Notice[]|undefined>>} Une promesse
101
- * retournant la liste
102
- * des notifications
103
- * regroupées par
104
- * fichier.
105
- */
106
- export default async function metalint(files, checkers, root) {
107
- const cache = new Map();
108
- const results = new Results(files);
109
- for (const file of files) {
110
- for (const checker of checkers) {
111
- const glob = new Glob(checker.patterns, { root });
112
- if (glob.test(file)) {
113
- const key = [glob];
114
- const values = [checker.linters];
115
- for (const override of checker.overrides) {
116
- const subglob = new Glob(override.patterns, { root });
117
- if (subglob.test(file)) {
118
- key.push(subglob);
119
- values.push(override.linters);
120
- }
121
- }
139
+ async report(results) {
140
+ let severity;
122
141
 
123
- let wrappers = [];
124
- if (cache.has(key)) {
125
- wrappers = cache.get(key);
126
- } else {
127
- const linters = values.reduce(mergeLinters);
128
- for (const linter of linters) {
129
- wrappers.push(
130
- // eslint-disable-next-line new-cap
131
- new linter.wrapper(
132
- {
133
- fix: linter.fix,
134
- level: linter.level,
135
- root,
136
- files,
137
- },
138
- linter.options,
139
- ),
140
- );
142
+ for (const [file, notices] of Object.entries(results)) {
143
+ // Déterminer la sévérité la plus élevée des résultats.
144
+ if (undefined !== notices) {
145
+ for (const notice of notices) {
146
+ if (undefined === severity || severity > notice.severity) {
147
+ severity = notice.severity;
141
148
  }
142
- cache.set(key, wrappers);
143
- }
144
- for (const wrapper of wrappers) {
145
- results.add(file, await wrapper.lint(file));
146
149
  }
147
150
  }
151
+
152
+ // Afficher les notifications avec chaque rapporteur.
153
+ for (const formatter of this.#formatters) {
154
+ await formatter.notify(file, notices);
155
+ }
148
156
  }
149
- }
150
157
 
151
- return results.toObject();
158
+ // Attendre tous les rapporteurs.
159
+ await Promise.all(this.#formatters.map((f) => f.finalize()));
160
+ return severity;
161
+ }
152
162
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module
3
+ * @license MIT
4
+ * @author Sébastien Règne
5
+ */
6
+
7
+ import Severities from "./severities.js";
8
+
9
+ /**
10
+ * @typedef {import("../type/index.d.ts").Notice} Notice
11
+ */
12
+
13
+ /**
14
+ * Compare deux notifications. En commençant par le numéro de la ligne, puis
15
+ * celui de la colonne.
16
+ *
17
+ * @param {Notice} notice1 La première notification.
18
+ * @param {Notice} notice2 La seconde notification.
19
+ * @returns {number} Un nombre négatif si la 1<sup>re</sup> notification est
20
+ * inférieure à la 2<sup>de</sup> ; <code>0</code> si elles
21
+ * sont égales ; sinon un nombre positif.
22
+ */
23
+ const compare = function (notice1, notice2) {
24
+ for (
25
+ let i = 0;
26
+ i < notice1.locations.length && i < notice2.locations.length;
27
+ ++i
28
+ ) {
29
+ const locations1 = notice1.locations[i];
30
+ const locations2 = notice2.locations[i];
31
+
32
+ let diff = locations1.line - locations2.line;
33
+ if (0 !== diff) {
34
+ return diff;
35
+ }
36
+
37
+ diff = (locations1.column ?? -1) - (locations2.column ?? -1);
38
+ if (0 !== diff) {
39
+ return diff;
40
+ }
41
+ }
42
+ return notice1.locations.length - notice2.locations.length;
43
+ };
44
+
45
+ export default class Results {
46
+ /**
47
+ * Les données des résultats.
48
+ *
49
+ * @type {Record<string, Notice[]|undefined>}
50
+ */
51
+ #data;
52
+
53
+ constructor(files) {
54
+ this.#data = Object.fromEntries(files.map((f) => [f, undefined]));
55
+ }
56
+
57
+ add(file, notices) {
58
+ // Ajouter un tableau vide dans les données pour indiquer que le fichier
59
+ // a été analysé par au moins un linter.
60
+ if (undefined === this.#data[file]) {
61
+ this.#data[file] = [];
62
+ }
63
+ for (const notice of notices) {
64
+ // Vérifier aussi le fichier de la notification, car il peut être
65
+ // différent du fichier d'origine (qui est peut-être un répertoire
66
+ // ou une archive).
67
+ if (undefined === this.#data[notice.file]) {
68
+ this.#data[notice.file] = [];
69
+ }
70
+ this.#data[notice.file].push({
71
+ rule: undefined,
72
+ severity: Severities.ERROR,
73
+ locations: [],
74
+ ...notice,
75
+ });
76
+ }
77
+ }
78
+
79
+ toObject() {
80
+ // Trier les notifications.
81
+ Object.values(this.#data)
82
+ .filter((r) => undefined !== r)
83
+ .forEach((r) => r.sort(compare));
84
+ return this.#data;
85
+ }
86
+ }
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @template T
11
11
  * @param {T|T[]} value L'élément.
12
- * @returns {T[]} Un avec seulement l'élément d'entré ou l'élément.
12
+ * @returns {T[]} Un tableau avec seulement l'élément d'entrée ou l'élément.
13
13
  */
14
14
  export const wrap = function (value) {
15
15
  return Array.isArray(value) ? value : [value];
@@ -180,7 +180,7 @@ export default class Glob {
180
180
  /**
181
181
  * Teste si un fichier / répertoire respecte les patrons.
182
182
  *
183
- * @param {string} file Le chemin du fichier / répertoire qui sera vérifié.
183
+ * @param {string} file Le chemin du fichier / répertoire qui sera vérifié.
184
184
  * @returns {string} <code>"DEEP_NEGATIVE"</code> pour un répertoire ne
185
185
  * respectant pas un patron qui exclue aussi ses
186
186
  * fichiers ; <code>"NEGATIVE"</code> si le fichier /
@@ -47,13 +47,10 @@ export default class AddonsLinterWrapper extends Wrapper {
47
47
  constructor(context, options) {
48
48
  super(context);
49
49
  this.#options = {
50
- logLevel: "error",
51
- stack: false,
52
- metadata: false,
50
+ logLevel: "silent",
51
+ // Ne rien afficher dans la console.
53
52
  output: "none",
54
- boring: false,
55
- selfHosted: false,
56
- shouldScanFile: () => true,
53
+ shouldScanFile: (_fileOrDirName, _isDir) => true,
57
54
  ...options,
58
55
  };
59
56
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module
3
+ * @license MIT
4
+ * @author Sébastien Règne
5
+ */
6
+
7
+ import fs from "node:fs/promises";
8
+ import Ajv from "ajv";
9
+ import Levels from "../levels.js";
10
+ import Severities from "../severities.js";
11
+ import Wrapper from "./wrapper.js";
12
+
13
+ /**
14
+ * @typedef {import("../../type/index.d.ts").Level} Level
15
+ * @typedef {import("../../type/index.d.ts").PartialNotice} PartialNotice
16
+ */
17
+
18
+ /**
19
+ * L'enrobage du linter <strong>Ajv</strong>.
20
+ *
21
+ * @see https://www.npmjs.com/package/ajv
22
+ */
23
+ export default class AjvWrapper extends Wrapper {
24
+ /**
25
+ * Les options du linter.
26
+ *
27
+ * @type {Record<string, any>}
28
+ * @see https://ajv.js.org/options.html#ajv-options
29
+ */
30
+ #options;
31
+
32
+ /**
33
+ * L'éventuelle fonction pour ajouter des formats.
34
+ *
35
+ * @type {Function|undefined}
36
+ * @see https://ajv.js.org/guide/formats.html
37
+ */
38
+ #addFormats;
39
+
40
+ /**
41
+ * Le schéma du fichier JSON.
42
+ *
43
+ * @type {Record<string, any>}
44
+ */
45
+ #schema;
46
+
47
+ /**
48
+ * Crée un enrobage pour le linter <strong>Ajv</strong>.
49
+ *
50
+ * @param {Object} context Le contexte de l'enrobage.
51
+ * @param {Level} context.level Le niveau de sévérité minimum
52
+ * des notifications retournées.
53
+ * @param {boolean} context.fix La marque indiquant s'il faut
54
+ * corriger le fichier.
55
+ * @param {string} context.root L'adresse du répertoire où se
56
+ * trouve le répertoire
57
+ * <code>.metalint/</code>.
58
+ * @param {string[]} context.files La liste de tous les fichiers
59
+ * analysés.
60
+ * @param {Record<string, any>} options Les options du linter.
61
+ */
62
+ constructor(context, options) {
63
+ super(context);
64
+ const { addFormats, schema, ...others } = options;
65
+ this.#options = {
66
+ loadSchema: (uri) =>
67
+ Promise.reject(
68
+ new Error(
69
+ `loadSchema() must be implemented to load ${uri}`,
70
+ ),
71
+ ),
72
+ ...others,
73
+ };
74
+ this.#addFormats = addFormats;
75
+ this.#schema = schema;
76
+ }
77
+
78
+ /**
79
+ * Vérifie un fichier.
80
+ *
81
+ * @param {string} file Le fichier qui sera vérifié.
82
+ * @returns {Promise<PartialNotice[]>} Une promesse retournant la liste des
83
+ * notifications.
84
+ */
85
+ async lint(file) {
86
+ if (Levels.FATAL > this.level) {
87
+ return [];
88
+ }
89
+
90
+ try {
91
+ const source = await fs.readFile(file, "utf8");
92
+ const ajv = new Ajv(this.#options);
93
+ if (undefined !== this.#addFormats) {
94
+ this.#addFormats(ajv);
95
+ }
96
+ const validate = await ajv.compileAsync(this.#schema);
97
+ validate(JSON.parse(source));
98
+ if (null === validate.errors) {
99
+ return [];
100
+ }
101
+
102
+ return validate.errors
103
+ .map((result) => ({
104
+ file,
105
+ linter: "ajv",
106
+ rule: result.keyword,
107
+ severity: Severities.ERROR,
108
+ message: ajv.errorsText([result], { dataVar: "" }),
109
+ }))
110
+ .filter((n) => this.level >= n.severity);
111
+ } catch (err) {
112
+ return [
113
+ {
114
+ file,
115
+ linter: "ajv",
116
+ severity: Severities.FATAL,
117
+ message: err.message,
118
+ },
119
+ ];
120
+ }
121
+ }
122
+ }