c3b-lint 0.0.17 → 0.0.20

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/README.md CHANGED
@@ -104,6 +104,14 @@ Findings print in sections, in the order they run — `--- c3b-lint ---` (checks
104
104
  `--- stylelint ---` — each grouped under the file's absolute path the way ESLint prints, with a `✖ N problems` total.
105
105
  A section with no findings is not printed. The timings follow, then the error count.
106
106
 
107
+ With `C3B_LINT_REPORT=<path>` in the environment, every finding of the run is also written to that file as JSON,
108
+ passed or failed: `{ version, errors, warnings, findings }`, each finding
109
+ `{ check, file, line, column, severity, rule, message, fixable }` — `check` is `structure`, `html`, `tokens`,
110
+ `editorconfig`, `lengths`, `eslint` or `stylelint`, `file` is relative to the project root, `rule` and `fixable`
111
+ come from ESLint and Stylelint (null and false for c3b-lint's own checks). It's for a program reading the run —
112
+ colab-desktop's Output summary. An environment variable rather than a flag: a caller that sets it for every project
113
+ can't know which c3b-lint each one pins, and an older version ignores it where it would refuse a flag.
114
+
107
115
  ## Not covered
108
116
 
109
117
  Rules that need judgement stay with review: whether two selectors are structurally disjoint, flex vs grid, whether
package/c3b-lint.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, globSync, readFileSync, writeFileSync } from 'fs';
3
- import { dirname, join, resolve, sep } from 'path';
3
+ import { dirname, join, relative, resolve, sep } from 'path';
4
4
  import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { ESLint } from 'eslint';
6
6
  import stylelint from 'stylelint';
@@ -83,6 +83,12 @@ for (const arg of args) {
83
83
 
84
84
  const fix = args.includes('--fix');
85
85
  const init = args.includes('--init');
86
+ // Where to also write every finding as JSON, for a program reading the run (colab-desktop's Output
87
+ // summary). An environment variable rather than a flag: a caller that sets it for every project can't
88
+ // know which c3b-lint each one pins, and an older version ignores it where it would refuse a flag.
89
+ const REPORT_FILE = process.env.C3B_LINT_REPORT;
90
+ // ESLint's and Stylelint's findings, in the report's shape, as they come in
91
+ const toolFindings = [];
86
92
 
87
93
  const toPosix = (p) => p.split(sep).join('/');
88
94
 
@@ -155,6 +161,16 @@ async function runEslint() {
155
161
  if (fix) {
156
162
  await ESLint.outputFixes(results);
157
163
  }
164
+ results.forEach((result) => result.messages.forEach((message) => toolFindings.push({
165
+ check: 'eslint',
166
+ file: toPosix(relative(process.cwd(), result.filePath)),
167
+ line: message.line ?? null,
168
+ column: message.column ?? null,
169
+ severity: message.severity === 2 ? 'error' : 'warning',
170
+ rule: message.ruleId ?? null,
171
+ message: message.message,
172
+ fixable: Boolean(message.fix)
173
+ })));
158
174
  const formatter = await eslint.loadFormatter('stylish');
159
175
  printSection('eslint', await formatter.format(results));
160
176
 
@@ -176,10 +192,35 @@ async function runStylelint() {
176
192
  formatter: 'string'
177
193
  });
178
194
  printSection('stylelint', result.report ?? result.output);
195
+ result.results.forEach((file) => file.warnings.forEach((warning) => toolFindings.push({
196
+ check: 'stylelint',
197
+ file: toPosix(relative(process.cwd(), file.source)),
198
+ line: warning.line ?? null,
199
+ column: warning.column ?? null,
200
+ severity: warning.severity === 'error' ? 'error' : 'warning',
201
+ rule: warning.rule ?? null,
202
+ // Stylelint ends each message with its rule in brackets; the report has the rule apart
203
+ message: warning.text.replace(/\s*\([^()]+\)$/, ''),
204
+ fixable: false
205
+ })));
179
206
 
180
207
  return result.results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0);
181
208
  }
182
209
 
210
+ // --- the JSON report --------------------------------------------------------
211
+
212
+ // Every finding of the run, whether it failed or not: { version, errors, warnings, findings }, the
213
+ // files relative to the project root. Written whole, once the checks are done.
214
+ function writeReport(path, findings) {
215
+ const count = (severity) => findings.filter((finding) => finding.severity === severity).length;
216
+ const body = { version, errors: count('error'), warnings: count('warning'), findings };
217
+ try {
218
+ writeFileSync(resolve(path), `${ JSON.stringify(body, null, 2) }\n`);
219
+ } catch (error) {
220
+ fail(`could not write the report to ${ path }: ${ error.message }`);
221
+ }
222
+ }
223
+
183
224
  // --- run ------------------------------------------------------------------
184
225
 
185
226
  // The checks run in a fixed order: c3b-lint's own, then ESLint, then Stylelint. Each tool's findings
@@ -213,11 +254,11 @@ async function main() {
213
254
  };
214
255
 
215
256
  const report = createReport();
216
- await timed('structure', () => checkStructure(report));
217
- await timed('html', () => checkHtml(report));
218
- await timed('tokens', () => checkCssTokens(report));
219
- await timed('editorconfig', () => checkEditorconfig(report));
220
- await timed('lengths', () => checkFileLengths(report));
257
+ await timed('structure', () => checkStructure(report.as('structure')));
258
+ await timed('html', () => checkHtml(report.as('html')));
259
+ await timed('tokens', () => checkCssTokens(report.as('tokens')));
260
+ await timed('editorconfig', () => checkEditorconfig(report.as('editorconfig')));
261
+ await timed('lengths', () => checkFileLengths(report.as('lengths')));
221
262
  printSection(name, report.format());
222
263
  let errors = report.errors;
223
264
  errors += await timed('eslint', () => runEslint());
@@ -225,6 +266,9 @@ async function main() {
225
266
 
226
267
  console.log(`\n${ timings.join('\n') }`);
227
268
  console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
269
+ if (REPORT_FILE) {
270
+ writeReport(REPORT_FILE, [...report.findings(), ...toolFindings]);
271
+ }
228
272
  if (errors > 0) {
229
273
  fail(`${ errors } error(s)`);
230
274
  }
package/check/report.js CHANGED
@@ -2,31 +2,56 @@ import { resolve } from 'path';
2
2
 
3
3
  // Findings are collected, then printed grouped under each file's absolute path the way ESLint's
4
4
  // stylish formatter prints, so the c3b-lint and ESLint sections of a run read the same. Returns a
5
- // counter so main() knows how many errors were found.
5
+ // counter so main() knows how many errors were found. Each finding also keeps the check it came
6
+ // from (as(check) hands a check its own reporter), for the JSON report (C3B_LINT_REPORT).
6
7
  export function createReport() {
7
8
  const byFile = new Map();
8
9
  let errors = 0;
9
10
  let warnings = 0;
10
11
 
11
12
  // Checks pass "file" or "file:line"
12
- function add(where, level, message) {
13
+ function add(where, level, message, check) {
13
14
  const match = where.match(/^(.*):(\d+)$/);
14
15
  const file = match ? match[1] : where;
15
16
  const line = match ? match[2] : '';
16
17
  if (!byFile.has(file)) {
17
18
  byFile.set(file, []);
18
19
  }
19
- byFile.get(file).push({ line, level, message });
20
+ byFile.get(file).push({ line, level, message, check });
21
+ }
22
+
23
+ function error(where, message, check = null) {
24
+ add(where, 'error', message, check);
25
+ errors++;
26
+ }
27
+
28
+ function warn(where, message, check = null) {
29
+ add(where, 'warning', message, check);
30
+ warnings++;
20
31
  }
21
32
 
22
33
  return {
23
- error(where, message) {
24
- add(where, 'error', message);
25
- errors++;
34
+ error,
35
+ warn,
36
+ // the reporter one check is handed: the same report, its findings marked as that check's
37
+ as(check) {
38
+ return {
39
+ error: (where, message) => error(where, message, check),
40
+ warn: (where, message) => warn(where, message, check)
41
+ };
26
42
  },
27
- warn(where, message) {
28
- add(where, 'warning', message);
29
- warnings++;
43
+ // every finding, for the JSON report: { check, file, line, column, severity, rule, message, fixable }
44
+ findings() {
45
+ return [...byFile.entries()].flatMap(([file, found]) => found.map((finding) => ({
46
+ check: finding.check,
47
+ file,
48
+ line: finding.line ? Number(finding.line) : null,
49
+ column: null,
50
+ severity: finding.level,
51
+ rule: null,
52
+ message: finding.message,
53
+ fixable: false
54
+ })));
30
55
  },
31
56
  // The whole report as one string, empty when nothing was found
32
57
  format() {
@@ -130,8 +130,13 @@ export const librarySrc = {
130
130
  }
131
131
  };
132
132
 
133
- // the "ignoreLength" files: everything but max-lines still applies (ESLint refuses an empty files list)
134
- const lengthExempt = settings.ignoreLength.length ? [{ files: settings.ignoreLength, rules: { 'max-lines': 'off' } }] : [];
133
+ // the "ignoreLength" files: everything but max-lines still applies (ESLint refuses an empty files list).
134
+ // Each glob is ANDed with the JS extensions (a nested array): a `files` pattern also adds what it matches
135
+ // to what ESLint lints, so a bare "src/index.html" here had ESLint parse the page as JavaScript.
136
+ const JS_FILES = '**/*.{js,mjs,cjs}';
137
+ const lengthExempt = settings.ignoreLength.length
138
+ ? [{ files: settings.ignoreLength.map((glob) => [glob, JS_FILES]), rules: { 'max-lines': 'off' } }]
139
+ : [];
135
140
 
136
141
  export default [
137
142
  { ignores: ['node_modules/**', 'dist/**', 'c10g/**', 'src/**/vendor/**', '**/*.min.js', ...settings.ignore] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c3b-lint",
3
- "version": "0.0.17",
3
+ "version": "0.0.20",
4
4
  "description": "Lints a co:lab project against the rules a tool can check: ESLint, Stylelint, the 500-line limit and the shared .editorconfig; --init writes the config files.",
5
5
  "license": "GPL-3.0",
6
6
  "type": "module",