Current section
Files
Jump to
Current section
Files
lib/ragex/editor/validators/python.ex
defmodule Ragex.Editor.Validators.Python do
@moduledoc """
Python code validator.
Uses Python's `ast.parse()` via shell command to validate Python syntax.
Requires Python 3 to be installed on the system.
"""
@behaviour Ragex.Editor.Validator
alias Ragex.Editor.Types
@impl true
def validate(content, _opts \\ []) do
# Create temporary file to avoid shell escaping issues
with {:ok, temp_path} <- write_temp_file(content),
result <- run_python_validation(temp_path),
:ok <- File.rm(temp_path) do
result
else
{:error, reason} when is_binary(reason) ->
error = Types.validation_error(reason, severity: :error)
{:error, [error]}
{:error, reason} ->
error = Types.validation_error("Failed to validate: #{inspect(reason)}", severity: :error)
{:error, [error]}
end
end
@impl true
def can_validate?(path) when is_binary(path) do
ext = Path.extname(path)
ext == ".py"
end
def can_validate?(_), do: false
# Private functions
defp write_temp_file(content) do
temp_dir = System.tmp_dir!()
temp_path = Path.join(temp_dir, "ragex_python_validate_#{:rand.uniform(999_999)}.py")
case File.write(temp_path, content) do
:ok -> {:ok, temp_path}
error -> error
end
end
defp run_python_validation(path) do
# Use Python to parse the file and report syntax errors
python_script = """
import ast
import sys
try:
with open('#{escape_path(path)}', 'r') as f:
code = f.read()
ast.parse(code)
print('VALID')
sys.exit(0)
except SyntaxError as e:
print(f'SYNTAX_ERROR:{e.lineno}:{e.offset}:{e.msg}')
sys.exit(1)
except Exception as e:
print(f'ERROR:{str(e)}')
sys.exit(1)
"""
case System.cmd("python3", ["-c", python_script], stderr_to_stdout: true) do
{"VALID\n", 0} ->
{:ok, :valid}
{output, 1} ->
parse_python_error(output)
{output, _exit_code} ->
error = Types.validation_error("Python validation failed: #{output}", severity: :error)
{:error, [error]}
end
rescue
e in ErlangError ->
# Python not found
if e.original == :enoent do
error =
Types.validation_error(
"Python 3 not found. Please install Python 3 to validate Python files.",
severity: :warning
)
{:error, [error]}
else
reraise e, __STACKTRACE__
end
end
defp escape_path(path) do
# Escape single quotes in path for Python string
String.replace(path, "'", "\\'")
end
defp parse_python_error(output) do
case String.split(String.trim(output), ":", parts: 4) do
["SYNTAX_ERROR", line_str, offset_str, message] ->
line = String.to_integer(line_str)
offset = String.to_integer(offset_str)
error =
Types.validation_error("#{message}",
line: line,
column: offset,
severity: :error
)
{:error, [error]}
["ERROR", message] ->
error = Types.validation_error(message, severity: :error)
{:error, [error]}
_ ->
error = Types.validation_error("Unknown error: #{output}", severity: :error)
{:error, [error]}
end
end
end