check-new-errors.js 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. #!/usr/bin/env node
  2. // Fails when a branch adds lint or typecheck errors compared with its base.
  3. // `yarn lint` and `yarn typecheck` aren't clean on master yet, so CI can't ask
  4. // for zero errors; it asks for no new ones (see AGENTS.md "Before you push").
  5. //
  6. // node scripts/check-new-errors.js \
  7. // --lint <head-eslint.json> <base-eslint.json> \
  8. // --typecheck <head-typecheck.txt> <base-typecheck.txt>
  9. //
  10. // ESLint input is `eslint -f json` output; typecheck input is the text printed by
  11. // scripts/typecheck.js. Errors are compared per file + rule (lint) or file + TS code
  12. // (typecheck), ignoring line numbers, so unrelated edits that shift lines don't count.
  13. // Warnings are ignored.
  14. const fs = require('node:fs');
  15. function args() {
  16. const out = {};
  17. const argv = process.argv.slice(2);
  18. for (let i = 0; i < argv.length; i++) {
  19. if (argv[i] === '--lint' || argv[i] === '--typecheck') {
  20. out[argv[i].slice(2)] = [argv[i + 1], argv[i + 2]];
  21. i += 2;
  22. }
  23. }
  24. return out;
  25. }
  26. /** Path relative to the NEO root, whatever checkout the report came from. */
  27. function rel(file) {
  28. const p = file.replaceAll('\\', '/');
  29. const m = p.match(/(?:^|\/)((?:apps|packages|scripts|crypto-shim)\/.*)$/);
  30. return m ? m[1] : p;
  31. }
  32. function lintCounts(file) {
  33. const counts = new Map();
  34. const where = new Map();
  35. for (const f of JSON.parse(fs.readFileSync(file, 'utf8'))) {
  36. for (const m of f.messages) {
  37. if (m.severity !== 2) continue;
  38. const key = `${rel(f.filePath)}\t${m.ruleId || 'parse-error'}`;
  39. counts.set(key, (counts.get(key) || 0) + 1);
  40. if (!where.has(key)) where.set(key, { line: m.line, message: m.message.split('\n')[0] });
  41. }
  42. }
  43. return { counts, where };
  44. }
  45. // tsc prints `file(12,5): error TS2532: ...` without a TTY and `file:12:5 - error TS2532: ...` with one.
  46. const TSC_LINE = /^(.+?)(?:\((\d+),\d+\)|:(\d+):\d+)(?::| -) error (TS\d+): (.*)$/;
  47. function typecheckCounts(file) {
  48. const counts = new Map();
  49. const where = new Map();
  50. const text = fs.readFileSync(file, 'utf8').replace(/\x1b\[[0-9;]*m/g, '');
  51. for (const raw of text.split(/\r?\n/)) {
  52. const m = raw.trim().match(TSC_LINE);
  53. if (!m) continue;
  54. const key = `${rel(m[1])}\t${m[4]}`;
  55. counts.set(key, (counts.get(key) || 0) + 1);
  56. if (!where.has(key)) where.set(key, { line: Number(m[2] || m[3]), message: m[5] });
  57. }
  58. return { counts, where };
  59. }
  60. function compare(label, head, base) {
  61. const added = [];
  62. for (const [key, n] of head.counts) {
  63. const before = base.counts.get(key) || 0;
  64. if (n > before) added.push({ key, n, before, ...head.where.get(key) });
  65. }
  66. const total = (c) => [...c.counts.values()].reduce((s, n) => s + n, 0);
  67. console.log(`${label}: ${total(base)} errors on base, ${total(head)} on this branch`);
  68. for (const a of added) {
  69. const [file, rule] = a.key.split('\t');
  70. const msg = `${rule}: ${a.n - a.before} new (${a.before} → ${a.n}). First: ${a.message}`;
  71. // GitHub annotation; paths are relative to the repo root, where NEO lives in neo/.
  72. console.log(`::error file=neo/${file},line=${a.line || 1},title=${label}::${msg}`);
  73. console.log(` ${file} ${msg}`);
  74. }
  75. return added.length;
  76. }
  77. const opts = args();
  78. let failures = 0;
  79. if (opts.lint) failures += compare('lint', lintCounts(opts.lint[0]), lintCounts(opts.lint[1]));
  80. if (opts.typecheck) failures += compare('typecheck', typecheckCounts(opts.typecheck[0]), typecheckCounts(opts.typecheck[1]));
  81. if (!opts.lint && !opts.typecheck) {
  82. console.error('usage: check-new-errors.js --lint <head.json> <base.json> --typecheck <head.txt> <base.txt>');
  83. process.exit(2);
  84. }
  85. if (failures) {
  86. console.log(`\n${failures} file/rule pair(s) gained errors. Fix them, or compare with master locally: yarn lint, yarn typecheck.`);
  87. process.exit(1);
  88. }
  89. console.log('\nno new errors');