feat: Enhanced PlantUML validation with dual-method checking
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 <noreply@anthropic.com>
This commit is contained in:
parent
1923053f6a
commit
e83177d248
|
|
@ -263,15 +263,24 @@ class AIThread(QThread):
|
||||||
return True, None # No validation available
|
return True, None # No validation available
|
||||||
|
|
||||||
def _validate_plantuml(self, code: str) -> tuple:
|
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 shutil
|
||||||
|
import re
|
||||||
|
|
||||||
plantuml_cmd = shutil.which('plantuml')
|
plantuml_cmd = shutil.which('plantuml')
|
||||||
if not plantuml_cmd:
|
if not plantuml_cmd:
|
||||||
# PlantUML not installed, skip validation
|
return True, None # PlantUML not installed, skip validation
|
||||||
return True, None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Method 1: Syntax check (fast, gives line numbers)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[plantuml_cmd, '-syntax', '-pipe'],
|
[plantuml_cmd, '-syntax', '-pipe'],
|
||||||
input=code,
|
input=code,
|
||||||
|
|
@ -280,13 +289,40 @@ class AIThread(QThread):
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exit code 0 = valid, anything else = error
|
# Parse error from syntax check
|
||||||
if result.returncode == 0:
|
if result.returncode != 0 or 'ERROR' in result.stdout:
|
||||||
|
return self._parse_plantuml_error(result.stdout)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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
|
return True, None
|
||||||
|
|
||||||
# Parse error message from stdout
|
except subprocess.TimeoutExpired:
|
||||||
# Format: "ERROR\n<line_number>\nSyntax Error?\n<description>"
|
return True, None # Skip validation on timeout
|
||||||
error_lines = result.stdout.strip().split('\n')
|
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"
|
error_msg = "PlantUML syntax error"
|
||||||
|
|
||||||
if len(error_lines) >= 2 and error_lines[0] == 'ERROR':
|
if len(error_lines) >= 2 and error_lines[0] == 'ERROR':
|
||||||
|
|
@ -297,11 +333,6 @@ class AIThread(QThread):
|
||||||
|
|
||||||
return False, error_msg
|
return False, error_msg
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return True, None # Skip validation on timeout
|
|
||||||
except Exception as e:
|
|
||||||
return True, None # Skip validation on error
|
|
||||||
|
|
||||||
def _call_ai(self, instruction: str, current_code: str) -> tuple:
|
def _call_ai(self, instruction: str, current_code: str) -> tuple:
|
||||||
"""Call artifact-ai SmartTool and return (success, response_or_error).
|
"""Call artifact-ai SmartTool and return (success, response_or_error).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue