lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ eslint.config.mjs
| 1 | // @ts-check |
| 2 | import { readFileSync } from 'node:fs'; |
| 3 | import js from '@eslint/js'; |
| 4 | import globals from 'globals'; |
| 5 | import tseslint from 'typescript-eslint'; |
| 6 | |
| 7 | /** |
| 8 | * Was Git nicht erfasst, prueft auch der Linter nicht. |
| 9 | * |
| 10 | * Prettier liest die .gitignore von sich aus, ESLint nicht; ohne diese |
| 11 | * Uebersetzung stuende jede Zeile ein zweites Mal hier, und die beiden Listen |
| 12 | * liefen auseinander. Umgesetzt sind die Formen, die dort stehen: `name/` und |
| 13 | * `*.endung` gelten auf jeder Ebene, eine Zeile mit Schraegstrich am Anfang |
| 14 | * oder in der Mitte nur ab der Wurzel. Eine Negation (`!`) oder ein Backslash |
| 15 | * bricht ab, statt still anders gelesen zu werden als von Git. |
| 16 | * |
| 17 | * @returns {string[]} |
| 18 | */ |
| 19 | function ausGitignore() { |
| 20 | const muster = []; |
| 21 | const text = readFileSync(new URL('.gitignore', import.meta.url), 'utf8'); |
| 22 | for (const roh of text.split(/\r?\n/)) { |
| 23 | const zeile = roh.trim(); |
| 24 | if (zeile === '' || zeile.startsWith('#')) continue; |
| 25 | if (zeile.startsWith('!') || zeile.includes('\\')) { |
| 26 | throw new Error(`.gitignore: diese Zeile setzt eslint.config.mjs nicht um: ${zeile}`); |
| 27 | } |
| 28 | const verzeichnis = zeile.endsWith('/'); |
| 29 | const kern = verzeichnis ? zeile.slice(0, -1) : zeile; |
| 30 | const pfad = kern.includes('/') ? kern.replace(/^\//, '') : `**/${kern}`; |
| 31 | muster.push(verzeichnis ? `${pfad}/` : pfad); |
| 32 | } |
| 33 | return muster; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Regelwerk fuer den LSA-Planer. |
| 38 | * |
| 39 | * Was der Linter hier NICHT leisten muss: Typfehler finden. Das erledigt |
| 40 | * TypeScript selbst, und zwar streng (strict, noUncheckedIndexedAccess, |
| 41 | * exactOptionalPropertyTypes). Der Linter ist fuer die Fehlerklasse da, die |
| 42 | * der Uebersetzer durchlaesst - allen voran die schwebenden Zusagen: |
| 43 | * |
| 44 | * ipcMain.handle('kanal', async () => { ... }); // fehlt ein await, |
| 45 | * speichern(); // faellt der Fehler ins Nichts |
| 46 | * |
| 47 | * In einer Electron-Anwendung mit asynchronem IPC ist das der haeufigste Weg, |
| 48 | * auf dem eine Ausnahme lautlos verschwindet. Genau dagegen sind |
| 49 | * no-floating-promises und no-misused-promises eingeschaltet, und dafuer |
| 50 | * braucht der Linter Typinformationen (projectService). |
| 51 | * |
| 52 | * Formatierung steht hier bewusst nicht drin - dafuer gibt es Prettier. |
| 53 | */ |
| 54 | export default tseslint.config( |
| 55 | { |
| 56 | /* |
| 57 | * Ein Ausnahmevermerk, den es nicht mehr braucht, ist ein Fehler. |
| 58 | * |
| 59 | * 28 Zeilen tragen einen `eslint-disable-next-line` samt Grund, statt dass |
| 60 | * 36 Warnungen als Rueckstand gefuehrt werden (bis 5.16.0 war es so). Ohne diese |
| 61 | * Zeile waere das nur eine leisere Art, denselben Rueckstand zu haben: |
| 62 | * Vermerke bleiben stehen, wenn die Stelle umgeschrieben wird oder |
| 63 | * TypeScript eines Tages besser verengt, und niemand merkt es. So faellt |
| 64 | * jeder ueberfluessig gewordene Vermerk beim naechsten Lauf auf. |
| 65 | */ |
| 66 | linterOptions: { reportUnusedDisableDirectives: 'error' }, |
| 67 | }, |
| 68 | |
| 69 | { |
| 70 | // Erzeugtes, Fremdes und was nicht Teil der Fassung ist, wird nicht |
| 71 | // geprueft; ein Regelwerk darueber erzeugte nur Laerm. Dazu alles, was |
| 72 | // die .gitignore nennt (siehe ausGitignore). |
| 73 | ignores: [ |
| 74 | 'build/**', |
| 75 | 'coverage/**', |
| 76 | 'release/**', |
| 77 | 'node_modules/**', |
| 78 | 'public/**', |
| 79 | // Vom Anwender abgelegte Arbeitsunterlagen (gespeicherte Netzseiten) |
| 80 | // bringen fremdes JavaScript mit. Es gehoert nicht zu diesem Programm, |
| 81 | // steht in keiner tsconfig, und der typbewusste Linter bricht daran ab. |
| 82 | '*_files/**', |
| 83 | '*.htm', |
| 84 | ...ausGitignore(), |
| 85 | ], |
| 86 | }, |
| 87 | |
| 88 | js.configs.recommended, |
| 89 | ...tseslint.configs.recommendedTypeChecked, |
| 90 | |
| 91 | { |
| 92 | // Die beiden Projekte ausdruecklich benennen statt sie raten zu lassen. |
| 93 | // Mit `projectService: true` fielen electron/main.ts, preload.ts und |
| 94 | // protokoll.ts durch das Raster ("was not found by the project service"), |
| 95 | // waehrend electron/karte.ts geprueft wurde - eine Zuordnung, die man |
| 96 | // beim Lesen des Regelwerks nicht nachvollziehen kann. |
| 97 | files: [ |
| 98 | 'src/**/*.ts', |
| 99 | 'shared/**/*.ts', |
| 100 | 'electron/**/*.ts', |
| 101 | 'tests/**/*.ts', |
| 102 | 'e2e/**/*.ts', |
| 103 | 'tools/**/*.ts', |
| 104 | ], |
| 105 | languageOptions: { |
| 106 | parserOptions: { |
| 107 | project: ['./tsconfig.json', './tsconfig.electron.json'], |
| 108 | tsconfigRootDir: import.meta.dirname, |
| 109 | }, |
| 110 | }, |
| 111 | rules: { |
| 112 | // --- Die eigentlichen Waechter ----------------------------------- |
| 113 | '@typescript-eslint/no-floating-promises': 'error', |
| 114 | '@typescript-eslint/no-misused-promises': 'error', |
| 115 | '@typescript-eslint/await-thenable': 'error', |
| 116 | // FEHLER, NICHT WARNUNG. |
| 117 | // |
| 118 | // Bis zur Fassung 5.16.0 standen beide Regeln auf 'warn', weil 36 Faelle im Baum |
| 119 | // standen, die Fall fuer Fall durchgesehen und als beabsichtigt |
| 120 | // bestaetigt waren. Ein geduldeter Rueckstand hat aber eine Eigenschaft, |
| 121 | // die man erst spaet merkt: Der 37. Fall faellt niemandem auf. Er wird |
| 122 | // eine Warnung unter sechsunddreissig, das Gate bleibt gruen, und die |
| 123 | // Durchsicht muesste jemand von sich aus wiederholen. |
| 124 | // |
| 125 | // Der Rueckstand steht jetzt an den Stellen selbst. Jede der 28 Zeilen, |
| 126 | // die die Regeln beanstandeten, traegt ihren eigenen |
| 127 | // `eslint-disable-next-line ... -- <Grund>`; die ausfuehrliche |
| 128 | // Begruendung stand ohnehin schon als Kommentar darueber. Der eine Fall, |
| 129 | // der wirklich tot war (migrate.ts - `hasIntergreen` enthielt die |
| 130 | // Abfrage schon), ist entfernt, und die vier Faelle der |
| 131 | // Vollstaendigkeitspruefung sind ueber die Einstellung darunter |
| 132 | // erledigt statt ueber einen Vermerk. |
| 133 | // |
| 134 | // Wichtiger als die Vermerke ist `reportUnusedDisableDirectives` weiter |
| 135 | // oben: Ein Vermerk, den es nicht mehr braucht, ist selbst ein Fehler. |
| 136 | // Ohne diese Zeile waeren 28 Ausnahmen nur eine leisere Art, denselben |
| 137 | // Rueckstand zu fuehren - sie blieben stehen, wenn TypeScript eines |
| 138 | // Tages besser verengt oder die Stelle umgeschrieben wird. |
| 139 | // |
| 140 | // WARUM DIE PRUEFUNGEN NICHT EINFACH WEGFALLEN: Sie stehen fast alle vor |
| 141 | // Fremddaten - IPC-Nutzlast, Projektdatei, Satz aus IndexedDB, Antwort |
| 142 | // eines Kartendienstes. Der Typ ist dort die Behauptung des Aufrufers, |
| 143 | // keine Messung; die Pruefung ist die Wache dahinter. Sie zu streichen, |
| 144 | // weil der Uebersetzer sie fuer ueberfluessig haelt, waere die |
| 145 | // gefaehrliche Richtung. Dazu kommen zu grosszuegige Typpraedikate |
| 146 | // (`isFiniteNumber(x): x is number` weist Infinity und NaN ab, obwohl |
| 147 | // beide Zahlen sind - der Nein-Zweig wird dadurch falsch verengt), |
| 148 | // veraenderliche Felder ueber `await` hinweg, Faehigkeitsproben |
| 149 | // (`crypto?.randomUUID`, `unref?.()`) und Rueckfaelle in der |
| 150 | // Fehlerbehandlung, die eine Fehlermeldung nicht selbst zum Absturz |
| 151 | // bringen sollen. |
| 152 | // |
| 153 | // Die Durchsicht vom 03.09. und 07.09.2026, die zu dieser Einstufung |
| 154 | // gefuehrt hat, ist damit nicht mehr eine Liste hier, sondern steht Zeile |
| 155 | // fuer Zeile im Quelltext: |
| 156 | // |
| 157 | // * no-unnecessary-condition: 27 Vermerke, die groesste Haeufung mit |
| 158 | // sieben in electron/main.ts, dazu einer im Rauchtest. In tests/ ist |
| 159 | // die Regel abgeschaltet (Block weiter unten), in e2e/ nicht. |
| 160 | // * no-unnecessary-type-assertion: ein Vermerk (electron/karte.ts). |
| 161 | // Gemessen: Ohne die Zusicherung verengt der Uebersetzer auf |
| 162 | // Uint8Array, und die Abfrage `wert === undefined` eine Zeile darunter |
| 163 | // wird ihrerseits beanstandet - Aufraeumen verschoebe die Meldung, |
| 164 | // statt sie abzutragen. |
| 165 | '@typescript-eslint/no-unnecessary-condition': 'error', |
| 166 | '@typescript-eslint/no-unnecessary-type-assertion': 'error', |
| 167 | // Ein default-Zweig macht den Schalter vollstaendig. Das ist hier keine |
| 168 | // Nachsicht, sondern die Hausregel: Alle vier Faelle, die die Regel |
| 169 | // beanstandete, haben einen begruendeten default - intergreen.ts und |
| 170 | // engine.ts geben dort denselben Rueckgabewert wie ein eigener case, |
| 171 | // assistent.ts ergaenzt bewusst nur fuer Sonderformen einen Satz. Ohne |
| 172 | // die Einstellung muesste jede dieser Stellen einen Ausnahmevermerk |
| 173 | // tragen; mit ihr faengt die Regel weiterhin, was sie fangen soll: einen |
| 174 | // Schalter OHNE default, dem ein Fall fehlt. |
| 175 | '@typescript-eslint/switch-exhaustiveness-check': [ |
| 176 | 'error', |
| 177 | { considerDefaultExhaustiveForUnions: true }, |
| 178 | ], |
| 179 | |
| 180 | // Nicht benutzte Bezeichner meldet schon tsc (noUnusedLocals). Hier |
| 181 | // bleibt die Regel trotzdem an, aber mit der ueblichen Ausnahme fuer |
| 182 | // absichtlich ungenutzte Parameter. |
| 183 | '@typescript-eslint/no-unused-vars': [ |
| 184 | 'error', |
| 185 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, |
| 186 | ], |
| 187 | |
| 188 | // --- Bewusst entschaerft ----------------------------------------- |
| 189 | // Die Anwendung liest Projektdateien fremder Herkunft ein; an der |
| 190 | // Grenze ist `any` unvermeidlich, dahinter wird geprueft (schema.ts). |
| 191 | // Diese Regeln schluegen dort an, ohne einen Fehler zu zeigen. |
| 192 | '@typescript-eslint/no-unsafe-assignment': 'off', |
| 193 | '@typescript-eslint/no-unsafe-member-access': 'off', |
| 194 | '@typescript-eslint/no-unsafe-argument': 'off', |
| 195 | '@typescript-eslint/no-unsafe-call': 'off', |
| 196 | '@typescript-eslint/no-unsafe-return': 'off', |
| 197 | }, |
| 198 | }, |
| 199 | |
| 200 | { |
| 201 | // Konfigurationsdateien und Hilfsskripte stehen in keiner tsconfig - fuer |
| 202 | // sie gibt es keine Typinformationen, also auch keine typbewussten Regeln. |
| 203 | // Geprueft werden sie trotzdem, nur eben flacher. |
| 204 | files: ['**/*.mjs', '**/*.cjs', '*.config.ts'], |
| 205 | extends: [tseslint.configs.disableTypeChecked], |
| 206 | languageOptions: { globals: globals.node }, |
| 207 | }, |
| 208 | |
| 209 | { |
| 210 | // Hauptprozess: laeuft in Node, kein DOM. |
| 211 | files: ['electron/**/*.ts', '**/*.mjs', '**/*.cjs', 'tools/**/*.ts'], |
| 212 | languageOptions: { globals: globals.node }, |
| 213 | }, |
| 214 | |
| 215 | { |
| 216 | // Renderer: laeuft im Browser. |
| 217 | files: ['src/**/*.ts'], |
| 218 | languageOptions: { globals: globals.browser }, |
| 219 | }, |
| 220 | |
| 221 | { |
| 222 | // Ausgabetests und die Pruefsummen pruefen genau die Zeichen, die die |
| 223 | // Regel beanstandet: geschuetzte und schmale Leerzeichen in CSV- und |
| 224 | // PDF-Ausgaben. Sie sind dort der Pruefgegenstand, nicht ein Versehen. |
| 225 | files: ['tests/export/**/*.ts', 'scripts/pruefsummen.mjs'], |
| 226 | rules: { 'no-irregular-whitespace': 'off' }, |
| 227 | }, |
| 228 | |
| 229 | { |
| 230 | // Tests duerfen mehr: Vitest-Globale, und die Sorgfalt gegenueber |
| 231 | // schwebenden Zusagen ist dort weniger kritisch als im Programm. |
| 232 | files: ['tests/**/*.ts'], |
| 233 | languageOptions: { globals: { ...globals.node, ...globals.browser } }, |
| 234 | rules: { |
| 235 | '@typescript-eslint/no-unnecessary-condition': 'off', |
| 236 | // Eine Testfunktion ohne await ist gaengige Schreibweise und kein Mangel. |
| 237 | '@typescript-eslint/require-await': 'off', |
| 238 | }, |
| 239 | }, |
| 240 | ); |