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.
- package/LICENSE +1 -1
- package/README.md +42 -9
- package/package.json +51 -35
- package/src/bin/argv.js +1 -1
- package/src/bin/index.js +10 -105
- package/src/core/configuration/flatten.js +12 -12
- package/src/core/configuration/normalize.js +2 -3
- package/src/core/formatter/checkstyle.js +9 -10
- package/src/core/formatter/console.js +31 -34
- package/src/core/formatter/csv.js +7 -7
- package/src/core/formatter/formatter.js +5 -18
- package/src/core/formatter/github.js +6 -6
- package/src/core/formatter/json.js +9 -9
- package/src/core/formatter/unix.js +6 -6
- package/src/core/index.js +128 -118
- package/src/core/results.js +86 -0
- package/src/core/utils/array.js +1 -1
- package/src/core/utils/glob.js +1 -1
- package/src/core/wrapper/addons-linter.js +3 -6
- package/src/core/wrapper/ajv.js +122 -0
- package/src/core/wrapper/csslint.js +1 -6
- package/src/core/wrapper/depcheck.js +148 -0
- package/src/core/wrapper/doiuse.js +3 -4
- package/src/core/wrapper/eslint.js +25 -7
- package/src/core/wrapper/htmlhint.js +1 -6
- package/src/core/wrapper/htmllint.js +1 -6
- package/src/core/wrapper/jshint.js +1 -6
- package/src/core/wrapper/npm-package-json-lint.js +1 -4
- package/src/core/wrapper/publint.js +136 -0
- package/src/core/wrapper/standard.js +7 -1
- package/src/core/wrapper/stylelint.js +4 -11
- package/src/core/wrapper/svglint.js +92 -0
- package/src/core/wrapper/wrapper.js +5 -18
- package/src/core/wrapper/yaml-lint.js +1 -1
- package/types/configuration/flatten.d.ts +2 -3
- package/types/configuration/normalize.d.ts +0 -1
- package/types/formatter/checkstyle.d.ts +9 -10
- package/types/formatter/console.d.ts +13 -16
- package/types/formatter/csv.d.ts +6 -6
- package/types/formatter/github.d.ts +7 -7
- package/types/formatter/json.d.ts +10 -10
- package/types/formatter/unix.d.ts +7 -7
- package/types/index.d.ts +20 -14
- package/types/results.d.ts +7 -0
- package/types/wrapper/ajv.d.ts +36 -0
- package/types/wrapper/depcheck.d.ts +36 -0
- package/types/wrapper/publint.d.ts +33 -0
- package/types/wrapper/stylelint.d.ts +2 -2
- 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
|
-
//
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
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 {
|
|
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 {
|
|
26
|
+
* @type {Writable}
|
|
27
27
|
*/
|
|
28
28
|
#writer;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Crée un formateur.
|
|
32
32
|
*
|
|
33
|
-
* @param {Level}
|
|
34
|
-
*
|
|
35
|
-
* @param {Object}
|
|
36
|
-
* @param {
|
|
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 {
|
|
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
|
|
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 {
|
|
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}
|
|
50
|
-
*
|
|
51
|
-
* @param {Object}
|
|
52
|
-
* @param {
|
|
53
|
-
* @param {number}
|
|
54
|
-
*
|
|
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 {
|
|
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 {
|
|
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}
|
|
39
|
-
*
|
|
40
|
-
* @param {Object}
|
|
41
|
-
* @param {
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/utils/array.js
CHANGED
|
@@ -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'
|
|
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];
|
package/src/core/utils/glob.js
CHANGED
|
@@ -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
|
|
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: "
|
|
51
|
-
|
|
52
|
-
metadata: false,
|
|
50
|
+
logLevel: "silent",
|
|
51
|
+
// Ne rien afficher dans la console.
|
|
53
52
|
output: "none",
|
|
54
|
-
|
|
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
|
+
}
|