Packages

Elixir wrapper for Swift AST parsing using swift-ast-explorer

Current section

Files

Jump to
ex_swift_parser lib ex_swift_parser.ex
Raw

lib/ex_swift_parser.ex

defmodule ExSwiftParser do
@moduledoc """
Elixir wrapper for Swift AST parsing using swift-ast-explorer.
This module provides functionality to parse Swift source code and extract
its Abstract Syntax Tree (AST) in JSON format. It supports multiple Swift
versions and can be used both as a library and as a command-line tool.
## Examples
# Parse simple Swift code
{:ok, result} = ExSwiftParser.parse("let x = 5", "5.10")
# Access the AST
ast = result["ast"]
version = result["version"]
## Supported Swift Versions
- `"5.8"` - Swift 5.8 parser
- `"5.9"` - Swift 5.9 parser
- `"5.10"` - Swift 5.10 parser
"""
@doc """
Parses the Swift `source` code using the specified `version`.
Returns `{:ok, result}` on success where result contains the Swift version
and parsed AST, or `{:error, reason}` on failure.
## Parameters
- `source` - Swift source code as a string
- `version` - Swift version to use for parsing ("5.8", "5.9", or "5.10")
## Examples
iex> ExSwiftParser.parse("let x = 5", "5.10")
{:ok, %{"version" => "5.10.0", "ast" => %{"kind" => "SourceFile", ...}}}
iex> ExSwiftParser.parse("invalid swift", "5.10")
{:error, :parse_error}
iex> ExSwiftParser.parse("let x = 5", "invalid")
{:error, :invalid_version}
"""
def parse(source, version) do
with {:ok, exec} <- exec_for_version(version),
{:ok, cmd} <- validate_exec(exec) do
port = Port.open({:spawn_executable, cmd}, [:binary, :eof])
Port.command(port, source)
result =
port
|> receive_output("")
|> Jason.decode!()
{:ok, %{"version" => result["swiftVersion"], "ast" => Jason.decode!(result["syntaxJSON"])}}
end
end
defp exec_for_version(version) do
case version do
"5.8" -> {:ok, "parser_50800"}
"5.9" -> {:ok, "parser_509000"}
"5.10" -> {:ok, "parser_51000"}
_ -> {:error, :invalid_version}
end
end
defp validate_exec(exec) do
path = Path.join([priv_dir(), "parsers", exec])
# Check if the file exists first, then try to read the link
cond do
File.exists?(path) ->
case File.read_link(path) do
{:ok, _} -> {:ok, path}
{:error, :einval} ->
# Not a symlink, but file exists - use it directly
{:ok, path}
err -> err
end
true ->
{:error, :enoent}
end
end
defp priv_dir do
case :code.priv_dir(:ex_swift_parser) do
{:error, :bad_name} ->
# We're likely in an escript environment
escript_priv_dir()
priv_dir when is_list(priv_dir) ->
List.to_string(priv_dir)
end
end
defp escript_priv_dir do
# In escript, we need to find the priv directory relative to the escript executable
# Try multiple possible locations
possible_paths = [
# Next to the escript executable
case :escript.script_name() do
:undefined ->
case System.argv() |> List.first() do
nil -> Path.join([File.cwd!(), "priv"])
path -> Path.join([Path.dirname(path), "priv"])
end
script_name ->
script_dir = script_name |> List.to_string() |> Path.dirname()
Path.join([script_dir, "priv"])
end,
# Current working directory
Path.join([File.cwd!(), "priv"]),
# Relative to the current file (development scenario)
Path.join([__DIR__, "..", "..", "priv"])
]
# Return the first path that exists
Enum.find(possible_paths, &File.exists?/1) || List.first(possible_paths)
end
defp receive_output(port, acc) do
receive do
{^port, {:data, data}} ->
receive_output(port, acc <> data)
{^port, :eof} ->
Port.close(port)
acc
end
end
end
defmodule ExSwiftParser.CLI do
@moduledoc """
Command line interface for ExSwiftParser.
This module is used when running as an escript.
"""
def main(args) do
case args do
[version, source] ->
case ExSwiftParser.parse(source, version) do
{:ok, result} ->
IO.puts(Jason.encode!(result))
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
System.halt(1)
end
_ ->
IO.puts("Usage: ex_swift_parser <version> <source>")
IO.puts("Example: ex_swift_parser \"5.10\" \"let x = 5\"")
System.halt(1)
end
end
end