lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests tools linterRueckstand.test.ts
| 1 | import { readFileSync, readdirSync } from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import { describe, expect, it } from 'vitest'; |
| 4 | |
| 5 | /* |
| 6 | * Der Linter duldet keinen Rueckstand mehr. |
| 7 | * |
| 8 | * WAS VORHER WAR: 36 Warnungen standen im Baum, Fall fuer Fall durchgesehen und |
| 9 | * als beabsichtigt bestaetigt, und die Zahl stand als offener Punkt vermerkt. |
| 10 | * Das las sich wie Ordnung, hatte aber eine Luecke, die man erst spaet bemerkt: |
| 11 | * Der siebenunddreissigste Fall faellt niemandem auf. Er wird eine Warnung |
| 12 | * unter sechsunddreissig, das Gate bleibt gruen, und die Durchsicht muesste |
| 13 | * jemand von sich aus wiederholen. |
| 14 | * |
| 15 | * WAS JETZT GILT: Die beiden Regeln melden FEHLER. Die 28 Zeilen, die sie |
| 16 | * beanstandeten, tragen ihren eigenen Ausnahmevermerk samt Grund - der |
| 17 | * ausfuehrliche Kommentar stand ohnehin schon darueber. Der Rueckstand ist |
| 18 | * damit nicht verschwunden, sondern an die Stellen gewandert, an denen er |
| 19 | * hingehoert. |
| 20 | * |
| 21 | * WOGEGEN DIESER FALL GESCHRIEBEN IST: Diese Umstellung laesst sich in einer |
| 22 | * Minute rueckgaengig machen, ohne dass ein Test rot wird - `'error'` zu |
| 23 | * `'warn'`, und der Rueckstand ist zurueck, nur diesmal ohne dass jemand |
| 24 | * mitzaehlt. Drei Zeilen tragen die Umstellung, und dieser Fall haelt alle |
| 25 | * drei fest: |
| 26 | * |
| 27 | * 1. Das Skript `lint` laeuft mit `--max-warnings 0`. Ohne das waere eine |
| 28 | * Warnung wieder folgenlos, egal was in der Konfiguration steht. |
| 29 | * 2. Die beiden Regeln stehen auf `'error'`. |
| 30 | * 3. `reportUnusedDisableDirectives` steht auf `'error'`. Das ist die |
| 31 | * wichtigste der drei: Ohne sie waeren 28 Ausnahmevermerke nur eine |
| 32 | * leisere Art, denselben Rueckstand zu fuehren - sie blieben stehen, wenn |
| 33 | * die Stelle umgeschrieben wird oder TypeScript eines Tages besser |
| 34 | * verengt. |
| 35 | * |
| 36 | * Was dieser Fall NICHT tut: den Linter fahren. Das tut `gate:schnell` bei |
| 37 | * jedem Commit, und zwar mit den Einstellungen, die hier geprueft werden. Eine |
| 38 | * zweite Ausfuehrung hier waere dieselbe Messung ein zweites Mal - und |
| 39 | * langsamer. |
| 40 | */ |
| 41 | |
| 42 | const REGELWERK = readFileSync('eslint.config.mjs', 'utf8'); |
| 43 | const PAKET = JSON.parse(readFileSync('package.json', 'utf8')) as { |
| 44 | scripts: Record<string, string>; |
| 45 | }; |
| 46 | |
| 47 | /** Die Regeln, die den Rueckstand ausmachten. */ |
| 48 | const RUECKSTANDSREGELN = [ |
| 49 | '@typescript-eslint/no-unnecessary-condition', |
| 50 | '@typescript-eslint/no-unnecessary-type-assertion', |
| 51 | ] as const; |
| 52 | |
| 53 | describe('Der Linter duldet keinen Rueckstand', () => { |
| 54 | it('laesst das Skript "lint" keine Warnung durchgehen', () => { |
| 55 | const skript = PAKET.scripts['lint'] ?? ''; |
| 56 | expect(skript, 'kein Skript "lint" in package.json').not.toBe(''); |
| 57 | expect(skript, 'ohne --max-warnings 0 ist eine Warnung wieder folgenlos').toContain( |
| 58 | '--max-warnings 0', |
| 59 | ); |
| 60 | }); |
| 61 | |
| 62 | it('faehrt auch das Gate ueber dieses Skript', () => { |
| 63 | // Sonst liefe der Commit-Haken an der Verschaerfung vorbei. |
| 64 | const schnell = PAKET.scripts['gate:schnell'] ?? ''; |
| 65 | expect(schnell, 'kein Skript "gate:schnell"').not.toBe(''); |
| 66 | expect(schnell, 'die Schnellstufe faehrt den Linter nicht').toContain('lint'); |
| 67 | }); |
| 68 | |
| 69 | it.each(RUECKSTANDSREGELN)('meldet %s einen Fehler und keine Warnung', (regel) => { |
| 70 | const stelle = REGELWERK.indexOf(`'${regel}'`); |
| 71 | expect(stelle, `die Regel ${regel} steht nicht im Regelwerk`).toBeGreaterThanOrEqual(0); |
| 72 | // Die Einstufung steht hinter dem Namen, bis zum Zeilenende. |
| 73 | const bisEnde = REGELWERK.slice(stelle, REGELWERK.indexOf('\n', stelle)); |
| 74 | expect(bisEnde, `${regel} steht wieder auf 'warn'`).not.toContain("'warn'"); |
| 75 | expect(bisEnde, `${regel} steht nicht auf 'error'`).toContain("'error'"); |
| 76 | }); |
| 77 | |
| 78 | it('ist ein ueberfluessig gewordener Ausnahmevermerk selbst ein Fehler', () => { |
| 79 | /* |
| 80 | * Die Zeile, an der alles haengt. 28 Vermerke stehen im Baum; ohne diese |
| 81 | * Einstellung ueberleben sie jede Umschreibung der Stelle, und der |
| 82 | * Rueckstand waere nur unsichtbar geworden statt abgetragen. |
| 83 | */ |
| 84 | expect( |
| 85 | REGELWERK.replace(/\s+/g, ' '), |
| 86 | 'reportUnusedDisableDirectives steht nicht auf error', |
| 87 | ).toContain("linterOptions: { reportUnusedDisableDirectives: 'error' }"); |
| 88 | }); |
| 89 | |
| 90 | it('nennt jeder Ausnahmevermerk im Baum einen Grund', () => { |
| 91 | /* |
| 92 | * `eslint-disable-next-line <regel>` ohne `-- <Grund>` waere ein |
| 93 | * stillschweigendes Ausschalten. Geprueft wird an den Dateien selbst, nicht |
| 94 | * an einer Liste: Eine Liste muesste gepflegt werden und ginge irgendwann |
| 95 | * an der Wirklichkeit vorbei. |
| 96 | */ |
| 97 | const ohneGrund: string[] = []; |
| 98 | let gefunden = 0; |
| 99 | for (const datei of quelldateien('src', 'electron', 'shared', 'tools', 'e2e')) { |
| 100 | const zeilen = readFileSync(datei, 'utf8').split('\n'); |
| 101 | for (const [nr, zeile] of zeilen.entries()) { |
| 102 | if (!zeile.includes('eslint-disable')) continue; |
| 103 | gefunden += 1; |
| 104 | if (!zeile.includes(' -- ')) ohneGrund.push(`${datei}:${nr + 1}`); |
| 105 | } |
| 106 | } |
| 107 | expect( |
| 108 | gefunden, |
| 109 | 'kein einziger Ausnahmevermerk gefunden - sucht der Fall am rechten Ort?', |
| 110 | ).toBeGreaterThan(0); |
| 111 | expect(ohneGrund, 'Ausnahmevermerke ohne Grund').toEqual([]); |
| 112 | }); |
| 113 | }); |
| 114 | |
| 115 | /** Alle `.ts` unter den genannten Verzeichnissen. */ |
| 116 | function quelldateien(...wurzeln: readonly string[]): readonly string[] { |
| 117 | const gefunden: string[] = []; |
| 118 | const suche = (ordner: string): void => { |
| 119 | for (const eintrag of readdirSync(ordner, { withFileTypes: true })) { |
| 120 | const pfad = path.join(ordner, eintrag.name); |
| 121 | if (eintrag.isDirectory()) suche(pfad); |
| 122 | else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad); |
| 123 | } |
| 124 | }; |
| 125 | for (const wurzel of wurzeln) suche(wurzel); |
| 126 | return gefunden; |
| 127 | } |