"""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)