// @ts-check import { readFileSync } from 'node:fs'; import js from '@eslint/js'; import globals from 'globals'; import tseslint from 'typescript-eslint'; /** * Was Git nicht erfasst, prueft auch der Linter nicht. * * Prettier liest die .gitignore von sich aus, ESLint nicht; ohne diese * Uebersetzung stuende jede Zeile ein zweites Mal hier, und die beiden Listen * liefen auseinander. Umgesetzt sind die Formen, die dort stehen: `name/` und * `*.endung` gelten auf jeder Ebene, eine Zeile mit Schraegstrich am Anfang * oder in der Mitte nur ab der Wurzel. Eine Negation (`!`) oder ein Backslash * bricht ab, statt still anders gelesen zu werden als von Git. * * @returns {string[]} */ function ausGitignore() { const muster = []; const text = readFileSync(new URL('.gitignore', import.meta.url), 'utf8'); for (const roh of text.split(/\r?\n/)) { const zeile = roh.trim(); if (zeile === '' || zeile.startsWith('#')) continue; if (zeile.startsWith('!') || zeile.includes('\\')) { throw new Error(`.gitignore: diese Zeile setzt eslint.config.mjs nicht um: ${zeile}`); } const verzeichnis = zeile.endsWith('/'); const kern = verzeichnis ? zeile.slice(0, -1) : zeile; const pfad = kern.includes('/') ? kern.replace(/^\//, '') : `**/${kern}`; muster.push(verzeichnis ? `${pfad}/` : pfad); } return muster; } /** * Regelwerk fuer den LSA-Planer. * * Was der Linter hier NICHT leisten muss: Typfehler finden. Das erledigt * TypeScript selbst, und zwar streng (strict, noUncheckedIndexedAccess, * exactOptionalPropertyTypes). Der Linter ist fuer die Fehlerklasse da, die * der Uebersetzer durchlaesst - allen voran die schwebenden Zusagen: * * ipcMain.handle('kanal', async () => { ... }); // fehlt ein await, * speichern(); // faellt der Fehler ins Nichts * * In einer Electron-Anwendung mit asynchronem IPC ist das der haeufigste Weg, * auf dem eine Ausnahme lautlos verschwindet. Genau dagegen sind * no-floating-promises und no-misused-promises eingeschaltet, und dafuer * braucht der Linter Typinformationen (projectService). * * Formatierung steht hier bewusst nicht drin - dafuer gibt es Prettier. */ export default tseslint.config( { /* * Ein Ausnahmevermerk, den es nicht mehr braucht, ist ein Fehler. * * 28 Zeilen tragen einen `eslint-disable-next-line` samt Grund, statt dass * 36 Warnungen als Rueckstand gefuehrt werden (bis 5.16.0 war es so). Ohne diese * Zeile waere das nur eine leisere Art, denselben Rueckstand zu haben: * Vermerke bleiben stehen, wenn die Stelle umgeschrieben wird oder * TypeScript eines Tages besser verengt, und niemand merkt es. So faellt * jeder ueberfluessig gewordene Vermerk beim naechsten Lauf auf. */ linterOptions: { reportUnusedDisableDirectives: 'error' }, }, { // Erzeugtes, Fremdes und was nicht Teil der Fassung ist, wird nicht // geprueft; ein Regelwerk darueber erzeugte nur Laerm. Dazu alles, was // die .gitignore nennt (siehe ausGitignore). ignores: [ 'build/**', 'coverage/**', 'release/**', 'node_modules/**', 'public/**', // Vom Anwender abgelegte Arbeitsunterlagen (gespeicherte Netzseiten) // bringen fremdes JavaScript mit. Es gehoert nicht zu diesem Programm, // steht in keiner tsconfig, und der typbewusste Linter bricht daran ab. '*_files/**', '*.htm', ...ausGitignore(), ], }, js.configs.recommended, ...tseslint.configs.recommendedTypeChecked, { // Die beiden Projekte ausdruecklich benennen statt sie raten zu lassen. // Mit `projectService: true` fielen electron/main.ts, preload.ts und // protokoll.ts durch das Raster ("was not found by the project service"), // waehrend electron/karte.ts geprueft wurde - eine Zuordnung, die man // beim Lesen des Regelwerks nicht nachvollziehen kann. files: [ 'src/**/*.ts', 'shared/**/*.ts', 'electron/**/*.ts', 'tests/**/*.ts', 'e2e/**/*.ts', 'tools/**/*.ts', ], languageOptions: { parserOptions: { project: ['./tsconfig.json', './tsconfig.electron.json'], tsconfigRootDir: import.meta.dirname, }, }, rules: { // --- Die eigentlichen Waechter ----------------------------------- '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/await-thenable': 'error', // FEHLER, NICHT WARNUNG. // // Bis zur Fassung 5.16.0 standen beide Regeln auf 'warn', weil 36 Faelle im Baum // standen, die Fall fuer Fall durchgesehen und als beabsichtigt // bestaetigt waren. Ein geduldeter Rueckstand hat aber eine Eigenschaft, // die man erst spaet merkt: Der 37. Fall faellt niemandem auf. Er wird // eine Warnung unter sechsunddreissig, das Gate bleibt gruen, und die // Durchsicht muesste jemand von sich aus wiederholen. // // Der Rueckstand steht jetzt an den Stellen selbst. Jede der 28 Zeilen, // die die Regeln beanstandeten, traegt ihren eigenen // `eslint-disable-next-line ... -- `; die ausfuehrliche // Begruendung stand ohnehin schon als Kommentar darueber. Der eine Fall, // der wirklich tot war (migrate.ts - `hasIntergreen` enthielt die // Abfrage schon), ist entfernt, und die vier Faelle der // Vollstaendigkeitspruefung sind ueber die Einstellung darunter // erledigt statt ueber einen Vermerk. // // Wichtiger als die Vermerke ist `reportUnusedDisableDirectives` weiter // oben: Ein Vermerk, den es nicht mehr braucht, ist selbst ein Fehler. // Ohne diese Zeile waeren 28 Ausnahmen nur eine leisere Art, denselben // Rueckstand zu fuehren - sie blieben stehen, wenn TypeScript eines // Tages besser verengt oder die Stelle umgeschrieben wird. // // WARUM DIE PRUEFUNGEN NICHT EINFACH WEGFALLEN: Sie stehen fast alle vor // Fremddaten - IPC-Nutzlast, Projektdatei, Satz aus IndexedDB, Antwort // eines Kartendienstes. Der Typ ist dort die Behauptung des Aufrufers, // keine Messung; die Pruefung ist die Wache dahinter. Sie zu streichen, // weil der Uebersetzer sie fuer ueberfluessig haelt, waere die // gefaehrliche Richtung. Dazu kommen zu grosszuegige Typpraedikate // (`isFiniteNumber(x): x is number` weist Infinity und NaN ab, obwohl // beide Zahlen sind - der Nein-Zweig wird dadurch falsch verengt), // veraenderliche Felder ueber `await` hinweg, Faehigkeitsproben // (`crypto?.randomUUID`, `unref?.()`) und Rueckfaelle in der // Fehlerbehandlung, die eine Fehlermeldung nicht selbst zum Absturz // bringen sollen. // // Die Durchsicht vom 03.09. und 07.09.2026, die zu dieser Einstufung // gefuehrt hat, ist damit nicht mehr eine Liste hier, sondern steht Zeile // fuer Zeile im Quelltext: // // * no-unnecessary-condition: 27 Vermerke, die groesste Haeufung mit // sieben in electron/main.ts, dazu einer im Rauchtest. In tests/ ist // die Regel abgeschaltet (Block weiter unten), in e2e/ nicht. // * no-unnecessary-type-assertion: ein Vermerk (electron/karte.ts). // Gemessen: Ohne die Zusicherung verengt der Uebersetzer auf // Uint8Array, und die Abfrage `wert === undefined` eine Zeile darunter // wird ihrerseits beanstandet - Aufraeumen verschoebe die Meldung, // statt sie abzutragen. '@typescript-eslint/no-unnecessary-condition': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', // Ein default-Zweig macht den Schalter vollstaendig. Das ist hier keine // Nachsicht, sondern die Hausregel: Alle vier Faelle, die die Regel // beanstandete, haben einen begruendeten default - intergreen.ts und // engine.ts geben dort denselben Rueckgabewert wie ein eigener case, // assistent.ts ergaenzt bewusst nur fuer Sonderformen einen Satz. Ohne // die Einstellung muesste jede dieser Stellen einen Ausnahmevermerk // tragen; mit ihr faengt die Regel weiterhin, was sie fangen soll: einen // Schalter OHNE default, dem ein Fall fehlt. '@typescript-eslint/switch-exhaustiveness-check': [ 'error', { considerDefaultExhaustiveForUnions: true }, ], // Nicht benutzte Bezeichner meldet schon tsc (noUnusedLocals). Hier // bleibt die Regel trotzdem an, aber mit der ueblichen Ausnahme fuer // absichtlich ungenutzte Parameter. '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], // --- Bewusst entschaerft ----------------------------------------- // Die Anwendung liest Projektdateien fremder Herkunft ein; an der // Grenze ist `any` unvermeidlich, dahinter wird geprueft (schema.ts). // Diese Regeln schluegen dort an, ohne einen Fehler zu zeigen. '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-return': 'off', }, }, { // Konfigurationsdateien und Hilfsskripte stehen in keiner tsconfig - fuer // sie gibt es keine Typinformationen, also auch keine typbewussten Regeln. // Geprueft werden sie trotzdem, nur eben flacher. files: ['**/*.mjs', '**/*.cjs', '*.config.ts'], extends: [tseslint.configs.disableTypeChecked], languageOptions: { globals: globals.node }, }, { // Hauptprozess: laeuft in Node, kein DOM. files: ['electron/**/*.ts', '**/*.mjs', '**/*.cjs', 'tools/**/*.ts'], languageOptions: { globals: globals.node }, }, { // Renderer: laeuft im Browser. files: ['src/**/*.ts'], languageOptions: { globals: globals.browser }, }, { // Ausgabetests und die Pruefsummen pruefen genau die Zeichen, die die // Regel beanstandet: geschuetzte und schmale Leerzeichen in CSV- und // PDF-Ausgaben. Sie sind dort der Pruefgegenstand, nicht ein Versehen. files: ['tests/export/**/*.ts', 'scripts/pruefsummen.mjs'], rules: { 'no-irregular-whitespace': 'off' }, }, { // Tests duerfen mehr: Vitest-Globale, und die Sorgfalt gegenueber // schwebenden Zusagen ist dort weniger kritisch als im Programm. files: ['tests/**/*.ts'], languageOptions: { globals: { ...globals.node, ...globals.browser } }, rules: { '@typescript-eslint/no-unnecessary-condition': 'off', // Eine Testfunktion ohne await ist gaengige Schreibweise und kein Mangel. '@typescript-eslint/require-await': 'off', }, }, );