Files
AH-Oil-Trader/tools/check_nfalle.py
T
Axel HocksandClaude Opus 5 e7112e1ab6 JS-Syntaxpruefung: node --check als Stufe B in check_nfalle
CLAUDE.md behauptete "kein node im Env" - das stimmte nicht mehr, Node
v24.18.0 ist installiert. Die Zeile hat die Frage jahrelang falsch
beantwortet.

Gemessen an 5 realistischen Fehlern findet die alte Klammern-Balance 1,
node --check findet 5 (86 ms):
  Klammer zu viel        Balance ok   node ok
  const x = ;            Balance blind
  let doppelt            Balance blind
  const doppelt          Balance blind
  return ausserhalb Fkt. Balance blind

Ein Syntaxfehler in app.js bricht den GESAMTEN Render - das Dashboard
friert auf Altwerten ein. Bei 95 direkt dereferenzierten $("id")-Zugriffen
(0 davon mit ?.) ist das kein theoretisches Risiko.

Geprueft wird als MODUL (.mjs), nicht als CommonJS: als CJS haelt Node ein
top-level return fuer legal, der Browser laedt app.js aber als klassisches
Script, wo es ein Syntaxfehler ist. Verifiziert, dass das echte app.js den
strengeren Modus ohne Anpassung besteht.

Kein npm, keine package.json, kein node_modules - node --check ist die
Laufzeit selbst. Damit entsteht NICHT die Divergenz "Pruefumgebung ist nicht
die Laufumgebung" (der .ps1-BOM-Fehler, das pre-commit-Framework): Node und
Chrome benutzen denselben Parser (V8).

Drei Richtungen verifiziert:
  sauber                -> gruen
  mit eingebautem Fehler-> Exit 1, Commit gestoppt
  ohne node im PATH     -> Rueckfall auf alte Pruefung, Exit 0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:27:37 +02:00

342 lines
15 KiB
Python

"""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 subprocess
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.")
# ── A2) pyflakes: was py_compile NICHT sieht ─────────────────────────────
# ⚠⚠ WARUM DIESE STUFE NOETIG IST. `py_compile` prueft nur die SYNTAX. Ein
# Tippfehler in einem Namen ist syntaktisch einwandfrei und fliegt erst zur
# Laufzeit — und dieses Projekt hat sehr viele breite `except Exception`-Bloecke
# (bewusst, als Fail-safe im Trading-Betrieb). Ein `NameError` darin ist damit
# UNSICHTBAR: keine Meldung, kein Logeintrag, nur ein Zweig, der nichts tut.
# Real gefunden beim ERSTEN Lauf ueberhaupt (2026-08-07):
# · `core/trader.py` nutzte zweimal `log.` — den Namen gibt es dort nicht
# (nur `log_trade`/`log_hist`). Einer davon seit dem Initial-Commit.
# · Folge 1 (`_broker_offset_s`): der NameError lief in ein `except: pass`
# und nahm die Zuweisung `self._boff = int(off)` mit → ein echter
# Broker-Zeitzonen-Wechsel waere nie uebernommen worden.
# · Folge 2 (`modify_sltp`): kein try drumherum → der Fehler lief bis in
# `/api/sltp`; der Broker hatte SL/TP schon geaendert, der User bekam
# trotzdem einen Fehler, und `trail.deactivate()` wurde uebersprungen.
# GETRENNT NACH SCHWEREGRAD: eine Pruefung, die dauerhaft 100 harmlose Zeilen
# ausgibt, wird ignoriert — und dann auch die eine wichtige.
# ⚠ `redefinition of unused` steht BEWUSST NICHT hier drin. pyflakes meldet damit
# auch das voellig legitime Muster `hook = None` gefolgt von einem bedingten
# `def hook(...)` — real in `backtest_flipclose2.py` und
# `backtest_adverse15_squeeze.py`, beide geprueft und korrekt. Zwei unvermeidbare
# Dauer-Treffer sind genau der Weg, auf dem eine Pruefung ignoriert wird, und mit
# ihr dann auch die eine echte Meldung. Wer die Klasse doch sehen will, ruft
# `python -m pyflakes <datei>` direkt auf.
SCHWER = ("undefined name", "undefined local", "syntax error")
print()
print("=" * 88)
print(" A2) LINTER — undefinierte Namen (was py_compile nicht sieht)")
print("=" * 88)
# ⚠ RUFF statt pyflakes (2026-08-07, Web-Recherche zum 2026er-Standard): gleiche
# Befunde, deutlich schneller, ein Werkzeug statt vieler. Bewusst NUR
# `ruff check` mit ENGER Regelauswahl — **kein `ruff format`**: das wuerde 161
# Dateien umformatieren, darunter alle Backtests, und damit die Migrations-Regel
# verletzen (deren Zahlen sind dokumentiert).
# F821 = undefined name ← die Klasse, die hier real weh tut
# E999 = syntax error
# F811 (redefinition) bleibt DRAUSSEN, s. Begruendung bei SCHWER oben.
# pyflakes bleibt als Rueckfall, damit die Pruefung auch ohne ruff laeuft.
# ⚠⚠ NUR F821 — `E999` gibt es in ruff 0.16 NICHT MEHR. Wird es trotzdem
# selektiert, bricht ruff KOMPLETT ab („Rule E999 was removed") mit Exit 2.
# Genau das ist beim ersten Bau passiert, und schlimmer: der Rueckgabecode
# wurde nicht geprueft, die Fehlermeldung ging nach stderr — die Stufe meldete
# also „keine undefinierten Namen", waehrend sie GAR NICHTS geprueft hat. Ein
# absichtlich eingebauter `F821`-Fehler kam glatt durch den Hook.
# Syntaxfehler deckt ohnehin Stufe A (`py_compile`) ab.
# ⚠ RUFF-EXIT-CODES: 0 = sauber · 1 = Befunde · 2 = Werkzeugfehler.
# 2 darf NIEMALS als „sauber" durchgehen.
_lint_ok = False
try:
r = subprocess.run([sys.executable, "-m", "ruff", "check",
"--select", "F821", "--no-cache",
"--output-format", "concise", str(WURZEL)],
capture_output=True, text=True, timeout=180)
if r.returncode not in (0, 1):
print(f" ⚠ ruff meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — "
f"Rueckfall auf pyflakes:")
for z in (r.stderr or r.stdout).strip().splitlines()[:3]:
print(f" {z}")
raise RuntimeError("ruff nicht nutzbar")
_lint_ok = True
treffer = [z for z in r.stdout.splitlines()
if z.strip() and not z.startswith(("Found", "All checks", "warning:"))]
for z in treffer:
kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
befunde.append(f"RUFF {kurz}")
print(f" FEHLER {kurz}")
if not treffer:
print(" keine undefinierten Namen (ruff, F821/E999).")
except Exception:
pass
if not _lint_ok:
try:
import io
from pyflakes.api import checkPath
from pyflakes.reporter import Reporter
aus, err = io.StringIO(), io.StringIO()
rep = Reporter(aus, err)
for p in dateien(".py"):
checkPath(str(p), rep)
zeilen = [z for z in (aus.getvalue() + err.getvalue()).splitlines()
if z.strip()]
schwer = [z for z in zeilen if any(s in z for s in SCHWER)]
for z in schwer:
kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
befunde.append(f"PF {kurz}")
print(f" FEHLER {kurz}")
if not schwer:
print(" keine undefinierten Namen (pyflakes-Rueckfall).")
except ImportError:
print(" ⚠ weder ruff noch pyflakes installiert — Stufe uebersprungen.")
print(" Installation: python -m pip install -r requirements-dev.txt")
# ── 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 — Syntax (node) + unabgeschlossene String-Literale")
print("=" * 88)
def node_pruefen(p: pathlib.Path):
"""Echte Syntaxpruefung mit `node --check`. Gibt die Fehlerzeile zurueck oder
None; `False` heisst „Node nicht verfuegbar".
⚠ WARUM DAS DAZUKAM (2026-08-07). Die Klammern-Balance findet gemessen nur
1 von 5 realistischen Fehlern — `const x = ;`, ein doppeltes `let`/`const`
und ein `return` ausserhalb einer Funktion laufen glatt durch. Ein
Syntaxfehler in `app.js` bricht aber den GESAMTEN Render: das Dashboard
friert auf Altwerten ein. Bei 95 direkten Element-Zugriffen ist das kein
theoretisches Risiko.
⚠ GEPRUEFT WIRD ALS MODUL (.mjs), NICHT ALS CommonJS. Als CJS haelt Node ein
top-level `return` fuer legal — der Browser laedt `app.js` aber als
klassisches Script, wo es ein Syntaxfehler ist. Der Modul-Modus liegt naeher
am Browser; verifiziert, dass das echte `app.js` ihn ohne Anpassung besteht.
⚠ Kein npm, keine package.json, kein node_modules — `node --check` ist die
Laufzeit selbst. Damit entsteht NICHT die Divergenz „Pruefumgebung ist nicht
die Laufumgebung", an der dieses Projekt schon zweimal aufgelaufen ist:
Node und Chrome benutzen denselben Parser (V8).
⚠ Fehlt Node, faellt die Stufe still auf die alte Prueflogik zurueck — eine
frische Maschine soll den Hook nicht blockieren.
"""
try:
with tempfile.NamedTemporaryFile("w", suffix=".mjs", delete=False,
encoding="utf-8") as f:
f.write(p.read_text(encoding="utf-8", errors="replace"))
tmp = f.name
try:
r = subprocess.run(["node", "--check", tmp],
capture_output=True, text=True, timeout=60)
finally:
try:
pathlib.Path(tmp).unlink()
except OSError:
pass
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
return False
if r.returncode == 0:
return None
for z in (r.stderr or "").splitlines():
if "Error" in z:
return z.strip()
return "node --check meldet einen Fehler"
for p in dateien(".js"):
if "lightweight-charts" in p.name:
continue
rel = p.relative_to(WURZEL)
nd = node_pruefen(p)
if nd is False:
print(f" ⚠ node fehlt — nur Ersatzpruefung fuer {rel}")
elif nd:
befunde.append(f"JS {rel} (node)")
print(f" FEHLER {rel} {nd}")
continue # bei kaputter Syntax ist die Zeilenheuristik sinnlos
tr = js_offene_strings(p.read_text(encoding="utf-8", errors="replace"))
if tr:
for nr, z in tr:
befunde.append(f"JS {rel}:{nr}")
print(f" FEHLER {rel}:{nr} {z}")
else:
print(f" OK {rel}" + (" (node)" if nd is None else ""))
# ── 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 ──────────────────────────────────────
# ── C2) .ps1 ohne BOM mit Nicht-ASCII = Zeitbombe ────────────────────────
# ⚠⚠ REAL PASSIERT (2026-08-07): `tools/autologon_einrichten.ps1` liess sich nicht
# ausfuehren — „Schliessende ) fehlt", „Zeichenfolge hat kein Abschlusszeichen".
# Ursache war KEIN Syntaxfehler: **Windows PowerShell 5.1 (`powershell.exe`) liest
# .ps1-Dateien als CP1252, wenn kein BOM da ist.** Aus dem Gedankenstrich „—"
# werden dann drei Zeichen (â€"), und das enthaltene " schliesst den String
# vorzeitig — Folgefehler bis zum Dateiende.
# ⚠ Warum es beim Schreiben nicht auffiel: `pwsh` 7 (mein Werkzeug) liest UTF-8
# OHNE BOM klaglos. Die Datei war also fuer mich fehlerfrei und fuer den Nutzer
# kaputt. Genau die Sorte Divergenz, die dieses Projekt als Deployment-Drift
# kennt — hier auf der Werkzeug-Ebene.
# Fix: UTF-8 MIT BOM schreiben. Gefunden wurde dabei ein zweiter Fall
# (`scripts/send_daily_report.ps1`, mit Umlauten).
print()
print("=" * 88)
print(" C2) POWERSHELL — .ps1 mit Nicht-ASCII braucht ein BOM (sonst CP1252)")
print("=" * 88)
for p in dateien(".ps1"):
roh = p.read_bytes()
bom = roh[:3] == b"\xef\xbb\xbf"
try:
txt = roh.decode("utf-8-sig")
except UnicodeDecodeError:
txt = roh.decode("cp1252", errors="replace")
na = sorted({c for c in txt if ord(c) > 127})
if na and not bom:
befunde.append(f"PS1 {p.relative_to(WURZEL)}: Nicht-ASCII ohne BOM")
print(f" FEHLER {p.relative_to(WURZEL)} "
f"{len(na)} Nicht-ASCII-Zeichen, KEIN BOM → bricht unter "
f"powershell.exe 5.1")
else:
print(f" OK {p.relative_to(WURZEL)}"
+ (" (BOM)" if bom else " (rein ASCII)"))
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)