Proxy: local abaplint messages on a bare 'save failed' (EPOD syntax check cannot see the rejected source)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-05 12:03:54 +02:00
parent 229f862600
commit a4eb567e4c
2 changed files with 105 additions and 1 deletions

86
harness/localcheck.py Normal file
View File

@@ -0,0 +1,86 @@
"""Local syntax check with abaplint (parser errors) for a source that SAP could not save.
SAP answers a failed write with "An error occured during the save operation" and nothing else, and the
EPOD syntax check reads the stored version (the old one or none), so it cannot check the rejected source.
The proxy runs this check on the rejected source and adds the messages (line + text) to the tool result.
"""
import json
import os
import re
import shutil
import subprocess
import tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ABAPLINT = os.path.join(ROOT, "node_modules", ".bin", "abaplint")
VERSIONS = {"v702", "v740sp05", "v750", "v758"}
BARE_SAVE_FAILURE = re.compile(r"during the save operation", re.I)
def is_bare_save_failure(text):
"""True when the write result is the detail-free 'save operation' error."""
try:
obj = json.loads(text)
except (TypeError, ValueError):
return bool(BARE_SAVE_FAILURE.search(str(text)))
if not isinstance(obj, dict) or obj.get("success") is not False:
return False
return bool(BARE_SAVE_FAILURE.search(str(obj.get("error") or obj.get("message") or ""))) \
and not (obj.get("activation") or {}).get("messages")
def filename(object_type, name, include_type=None, function_group=None):
n = re.sub(r"\W", "_", str(name).lower())
t = str(object_type).upper()
if t == "CLAS":
return f"{n}.clas.{'testclasses.' if str(include_type).lower() == 'testclasses' else ''}abap"
if t == "INTF":
return f"{n}.intf.abap"
if t == "PROG":
return f"{n}.prog.abap"
if t == "FUNC" and function_group:
fg = re.sub(r"\W", "_", str(function_group).lower())
return f"{fg}.fugr.{n}.abap"
if t == "DDLS":
return f"{n}.ddls.asddls"
return None # TABL, FUGR, ...: no local parser check
def parser_messages(object_type, name, source, include_type=None, function_group=None, release=None):
"""[{"line", "severity", "message"}] from abaplint parser_error; [] when nothing is found or no check."""
fn = filename(object_type, name, include_type, function_group)
if not fn or not source or not os.path.exists(ABAPLINT):
return []
version = release if release in VERSIONS else "v758"
d = tempfile.mkdtemp(prefix="proxylint_")
try:
os.makedirs(os.path.join(d, "src"))
open(os.path.join(d, "src", fn), "w").write(source)
cfg = {"global": {"files": "/src/**/*.*"}, "dependencies": [],
"syntax": {"version": version, "errorNamespace": "^(Z|Y)"}, "rules": {"parser_error": True}}
json.dump(cfg, open(os.path.join(d, "abaplint.json"), "w"))
p = subprocess.run([ABAPLINT, "abaplint.json", "-f", "json"], cwd=d, capture_output=True, text=True,
timeout=60)
issues = json.loads(p.stdout or "[]")
except Exception: # noqa: BLE001 the hint is optional; the original SAP result stays
return []
finally:
shutil.rmtree(d, ignore_errors=True)
return [{"line": i.get("start", {}).get("row"), "severity": "E", "message": i.get("description", "")}
for i in issues if i.get("key") == "parser_error"][:10]
def augment(text, messages):
"""Add the messages to a failed-write result (JSON object keeps valid JSON)."""
if not messages:
return text
try:
obj = json.loads(text)
if isinstance(obj, dict):
obj["syntaxCheck"] = {"source": "local abaplint (the rejected source, not stored in SAP)",
"messages": messages}
return json.dumps(obj)
except (TypeError, ValueError):
pass
return text + "\nSyntax check (local abaplint, rejected source): " + "; ".join(
f"line {m['line']}: {m['message']}" for m in messages)