tools/check_nfalle.py: Scan gegen die \n-Falle — Projekt ist sauber

Auftrag "ueberpruefe alle dateien auf \n-Falle". Die Falle: ueber ein
Shell-Heredoc geschriebener Code macht aus einem gemeinten \n einen ECHTEN
Zeilenumbruch mitten im String-Literal. Heute zweimal passiert (Telegram-Text
in engine.py, Reminder-Eintrag in measurement_reminder.py).

Der Scan prueft in beide Richtungen:
  A) kompiliert alle .py (161 Dateien) — sauber
  B) JS: unabgeschlossene String-Literale — app.js sauber
  C) UMGEKEHRT: literales \n als sichtbarer Text in HTML-title= und Markdown
     — alle sauber
  D) Signatur der beiden heutigen Faelle (Zeile endet offen, naechste besteht
     nur aus einem Quote) — keine

⚠ Fehlalarm im Scanner selbst gefunden und behoben: Quotes zu ZAEHLEN meldet
'"' als offenes Literal (gueltiges JS mit einem Anfuehrungszeichen darin, real
in app.js). Der Scan verfolgt jetzt zeichenweise den AKTIVEN Quote-Typ.

Die betroffene app.js-Stelle trotzdem aufgeraeumt: schliessendes
Anfuehrungszeichen jetzt als U+201C statt ueber eine Konkatenation mit '"'.

Regel in CLAUDE.md ergaenzt: mehrzeilige Strings mit \n nicht per Heredoc
schreiben, sondern ueber den Edit-Weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-07 00:10:16 +02:00
co-authored by Claude Opus 5
parent b858ae9aa2
commit 5a89544a27
2 changed files with 158 additions and 1 deletions
+153
View File
@@ -0,0 +1,153 @@
"""Sucht die \\n-Falle in allen Projektdateien — in BEIDE Richtungen.
Die Falle: beim Schreiben ueber ein Shell-Heredoc wird aus einem gemeinten `\\n`
ein ECHTER Zeilenumbruch mitten im String-Literal. In Python bricht das den
Compile (zweimal heute passiert), in JS ebenso — aber nicht ueberall faellt es
sofort auf.
Geprueft wird:
A) Python: kompiliert jede Datei. Ein zerrissenes Literal faellt hier auf.
B) JS: Zeilen mit ungerader Zahl UNMASKIERTER Anfuehrungszeichen ausserhalb
von Kommentaren = angefangenes, nicht geschlossenes String-Literal.
C) Umgekehrter Fall: literales `\\n` als TEXT dort, wo es niemand lesen will
(HTML-Attribute, Markdown-Fliesstext) — das entsteht, wenn ein `\\\\n`
zu viel escaped wurde.
D) Verdaechtige Zeilenpaare: eine Zeile endet mit oeffnendem Quote, die
naechste besteht nur aus einem Quote — die Signatur der beiden heutigen
Faelle.
"""
import pathlib
import py_compile
import re
import sys
import tempfile
WURZEL = pathlib.Path(r"c:\Users\ah\Downloads\tradingrobot")
SKIP = {".git", "__pycache__", ".reminders", "node_modules", ".removed_backup"}
befunde = []
def dateien(*endungen):
for p in WURZEL.rglob("*"):
if any(s in p.parts for s in SKIP):
continue
if p.is_file() and p.suffix in endungen:
yield p
# ── A) Python ────────────────────────────────────────────────────────────
print("=" * 88)
print(" A) PYTHON — kompiliert jede Datei (ein zerrissenes Literal faellt hier auf)")
print("=" * 88)
npy = 0
tmp = tempfile.mkdtemp()
for p in dateien(".py"):
npy += 1
try:
py_compile.compile(str(p), cfile=f"{tmp}/x.pyc", doraise=True)
except py_compile.PyCompileError as e:
msg = str(e).strip().splitlines()[-1][:110]
befunde.append(f"PY {p.relative_to(WURZEL)}: {msg}")
print(f" FEHLER {p.relative_to(WURZEL)}")
print(f" {msg}")
print(f" {npy} Dateien geprueft.")
# ── B) JavaScript ────────────────────────────────────────────────────────
def js_offene_strings(text: str):
"""Zeilen mit angefangenem, nicht geschlossenem ' oder " (Template-Literale
duerfen mehrzeilig sein und werden uebersprungen)."""
treffer = []
in_tmpl = False
for nr, z in enumerate(text.splitlines(), 1):
# Backticks zaehlen -> Template-Literal-Zustand fortschreiben
bt = len(re.findall(r"(?<!\\)`", z))
if in_tmpl:
in_tmpl ^= bool(bt % 2)
continue
if bt % 2:
in_tmpl = True
continue
s = re.sub(r"//.*$", "", z) # Zeilenkommentar weg
# ⚠ Quotes NICHT einfach zaehlen — das erzeugt Fehlalarme: '"' ist ein
# gueltiges Literal, das EIN doppeltes Anfuehrungszeichen enthaelt (real
# in app.js aufgetreten). Stattdessen zeichenweise laufen und den jeweils
# AKTIVEN Quote-Typ verfolgen; Quotes der anderen Art zaehlen dann nicht.
aktiv, esc = None, False
for ch in s:
if esc:
esc = False
continue
if ch == "\\":
esc = True
continue
if aktiv:
if ch == aktiv:
aktiv = None
elif ch in ('"', "'"):
aktiv = ch
if aktiv:
treffer.append((nr, z.strip()[:100]))
return treffer
print()
print("=" * 88)
print(" B) JAVASCRIPT — unabgeschlossene String-Literale")
print("=" * 88)
for p in dateien(".js"):
if "lightweight-charts" in p.name:
continue
tr = js_offene_strings(p.read_text(encoding="utf-8", errors="replace"))
if tr:
for nr, z in tr:
befunde.append(f"JS {p.relative_to(WURZEL)}:{nr}")
print(f" FEHLER {p.relative_to(WURZEL)}:{nr} {z}")
else:
print(f" OK {p.relative_to(WURZEL)}")
# ── C) literales \n als TEXT ─────────────────────────────────────────────
print()
print("=" * 88)
print(" C) UMGEKEHRT — literales \\n im sichtbaren Text (HTML-Attribute, Markdown)")
print("=" * 88)
for p in list(dateien(".html")) + list(dateien(".md")):
t = p.read_text(encoding="utf-8", errors="replace")
roh = []
for m in re.finditer(r'title="([^"]*)"', t):
if "\\n" in m.group(1):
roh.append(f"title=...{m.group(1)[:60]}")
if p.suffix == ".md":
for nr, z in enumerate(t.splitlines(), 1):
if "\\n" in z and "`" not in z and "\\\\n" not in z:
roh.append(f"Zeile {nr}: {z.strip()[:80]}")
if roh:
for r in roh[:5]:
befunde.append(f"TXT {p.relative_to(WURZEL)}: {r}")
print(f" FEHLER {p.relative_to(WURZEL)} {r}")
else:
print(f" OK {p.relative_to(WURZEL)}")
# ── D) Signatur der heutigen Faelle ──────────────────────────────────────
print()
print("=" * 88)
print(' D) SIGNATUR — Zeile endet offen, naechste besteht nur aus einem Quote')
print("=" * 88)
sig = 0
for p in list(dateien(".py")) + list(dateien(".js")):
zeilen = p.read_text(encoding="utf-8", errors="replace").splitlines()
for i in range(len(zeilen) - 1):
if zeilen[i + 1].strip() in ('"', "'", '",', "',"):
sig += 1
befunde.append(f"SIG {p.relative_to(WURZEL)}:{i+2}")
print(f" VERDAECHTIG {p.relative_to(WURZEL)}:{i+2}")
print(" keine gefunden." if not sig else f" {sig} Stellen.")
print()
print("=" * 88)
print(" ERGEBNIS: " + ("keine \\n-Falle gefunden" if not befunde
else f"{len(befunde)} Befund(e)"))
print("=" * 88)
sys.exit(1 if befunde else 0)
+5 -1
View File
@@ -275,9 +275,13 @@ function renderVerdict(v, snap) {
const top = (bm.top || []).map(x => `${x.grund} ${pc(x.n)} %`).join(" · "); const top = (bm.top || []).map(x => `${x.grund} ${pc(x.n)} %`).join(" · ");
blkEl.textContent = `⛔ heute ${pc(bm.frei)} % ohne Block` blkEl.textContent = `⛔ heute ${pc(bm.frei)} % ohne Block`
+ (top ? ` · ${top}` : ""); + (top ? ` · ${top}` : "");
// ⚠ Schliessendes Anführungszeichen als „ “ (U+201C), NICHT über eine
// Konkatenation mit '"' — die war valide, aber schwer lesbar und lässt
// jeden Quote-Zähler auflaufen (der \n-Scan hat sie als offenes Literal
// gemeldet).
blkEl.title = "Welches Gate hält die Empfehlung gerade zurück — über den " blkEl.title = "Welches Gate hält die Empfehlung gerade zurück — über den "
+ "heutigen Tag gezählt. Ohne diese Zeile war „warum gab es so wenige " + "heutigen Tag gezählt. Ohne diese Zeile war „warum gab es so wenige "
+ "Empfehlungen?" + '"' + " nur per Datenbank-Abfrage zu beantworten."; + "Empfehlungen? nur per Datenbank-Abfrage zu beantworten.";
} else { } else {
blkEl.textContent = ""; blkEl.textContent = "";
} }