From e83177d24861630f050680ef15e726a6e9813797 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 30 Dec 2025 23:20:36 -0400 Subject: [PATCH] feat: Enhanced PlantUML validation with dual-method checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now validates using two methods for better error detection: 1. plantuml -syntax: Fast check with line numbers 2. plantuml -tsvg: Render check that catches errors via stderr and embedded error indicators in SVG output Also extracts error line numbers from rendered SVG when available. Note: PlantUML only catches syntax errors, not semantic issues (e.g., nesting rectangles in components produces valid syntax but incorrect rendering). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/artifact_editor/gui.py | 65 ++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/artifact_editor/gui.py b/src/artifact_editor/gui.py index ce6ef3d..f80660f 100644 --- a/src/artifact_editor/gui.py +++ b/src/artifact_editor/gui.py @@ -263,15 +263,24 @@ class AIThread(QThread): return True, None # No validation available def _validate_plantuml(self, code: str) -> tuple: - """Validate PlantUML code using plantuml -syntax command.""" + """Validate PlantUML code using plantuml -syntax and render check. + + Uses two validation methods: + 1. plantuml -syntax: Fast syntax check with line numbers + 2. plantuml -tsvg: Render check that catches additional errors via stderr + + Note: PlantUML only catches syntax errors, not semantic issues + (e.g., nesting elements in unsupported ways). + """ import shutil + import re plantuml_cmd = shutil.which('plantuml') if not plantuml_cmd: - # PlantUML not installed, skip validation - return True, None + return True, None # PlantUML not installed, skip validation try: + # Method 1: Syntax check (fast, gives line numbers) result = subprocess.run( [plantuml_cmd, '-syntax', '-pipe'], input=code, @@ -280,28 +289,50 @@ class AIThread(QThread): timeout=30 ) - # Exit code 0 = valid, anything else = error - if result.returncode == 0: - return True, None + # Parse error from syntax check + if result.returncode != 0 or 'ERROR' in result.stdout: + return self._parse_plantuml_error(result.stdout) - # Parse error message from stdout - # Format: "ERROR\n\nSyntax Error?\n" - error_lines = result.stdout.strip().split('\n') - error_msg = "PlantUML syntax error" + # Method 2: Render check (catches some errors -syntax misses) + render_result = subprocess.run( + [plantuml_cmd, '-tsvg', '-pipe'], + input=code, + capture_output=True, + text=True, + timeout=30 + ) - if len(error_lines) >= 2 and error_lines[0] == 'ERROR': - line_num = error_lines[1] - error_msg = f"PlantUML syntax error on line {line_num}" - if len(error_lines) >= 4: - error_msg += f": {error_lines[3]}" + # Check stderr for error indicators + if 'ERROR' in render_result.stderr or 'Syntax Error' in render_result.stderr: + return self._parse_plantuml_error(render_result.stderr) - return False, error_msg + # Check SVG output for embedded error (red error text) + if 'fill="#FF0000"' in render_result.stdout and 'Syntax Error' in render_result.stdout: + line_match = re.search(r'\[From string \(line (\d+)\)', render_result.stdout) + if line_match: + return False, f"PlantUML error on line {line_match.group(1)}" + return False, "PlantUML syntax error (see rendered output)" + + return True, None except subprocess.TimeoutExpired: return True, None # Skip validation on timeout - except Exception as e: + except Exception: return True, None # Skip validation on error + def _parse_plantuml_error(self, output: str) -> tuple: + """Parse PlantUML error output and return (False, error_message).""" + error_lines = output.strip().split('\n') + error_msg = "PlantUML syntax error" + + if len(error_lines) >= 2 and error_lines[0] == 'ERROR': + line_num = error_lines[1] + error_msg = f"PlantUML syntax error on line {line_num}" + if len(error_lines) >= 4: + error_msg += f": {error_lines[3]}" + + return False, error_msg + def _call_ai(self, instruction: str, current_code: str) -> tuple: """Call artifact-ai SmartTool and return (success, response_or_error).