Skip to content

feat: Swift Language Support + bug fix + missing frontend build docs #53

@ayodm

Description

@ayodm

Summary

Three items bundled in this issue for discussion — happy to split into separate PRs if preferred.

1. Swift Language Support (feat)

I added Swift parsing support to Axon. Tested on a ~350-file Swift iOS project — successfully indexes 2988 symbols and 8351 relationships.

Constructs extracted:

  • Symbols: functions, methods (including init), classes, structs, enums, protocols (→ interface), typealiases (→ type_alias), extensions (walked for methods, no separate symbol emitted)
  • Imports: import Foundation, import UIKit, etc.
  • Calls: direct calls (MyClass(name:)) and method calls (someObj.method())
  • Type references: parameter types and return types from function signatures
  • Heritage: class/struct inheritance and protocol conformance

Files changed:

  • pyproject.toml — added tree-sitter-swift>=0.0.1 dependency
  • src/axon/config/languages.py — added .swift"swift" extension mapping
  • src/axon/core/ingestion/parser_phase.py — registered SwiftParser in _PARSER_FACTORIES
  • src/axon/core/parsers/swift.pynew file, full parser implementation (~400 lines)

tree-sitter-swift AST notes (useful for reviewers):

  • class_declaration is reused for class, struct, enum, and extension — distinguished by the first keyword child
  • protocol_declaration is separate from class_declaration
  • Function names are in simple_identifier children (not a name field)
  • Method calls use navigation_expressionnavigation_suffix (not member_expression)
  • protocol_function_declaration is distinct from function_declaration

2. Bug fix: max_workers crash on empty repos (fix)

process_imports() in src/axon/core/ingestion/imports.py crashes with ValueError: max_workers must be greater than 0 when parse_data is empty (e.g., a repo with no supported files).

# Before (crashes when len(parse_data) == 0)
workers = min(os.cpu_count() or 4, 8, len(parse_data))

# After
workers = max(1, min(os.cpu_count() or 4, 8, len(parse_data)))

3. Frontend build needed for axon ui (docs)

After a fresh pip install -e ., axon ui serves {"detail":"Not Found"} because the frontend hasn't been built. The dist/ directory is listed in pyproject.toml artifacts but isn't included in the repo or built during install.

Fix is to run:

cd src/axon/web/frontend && npm install && npm run build

This could be documented in the README or automated in the build process.


Code Changes

Diff of existing files
diff --git a/pyproject.toml b/pyproject.toml
index 3b0bfaf..613dbf0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,6 +27,7 @@ dependencies = [
     "tree-sitter-python>=0.23.0",
     "tree-sitter-javascript>=0.23.0",
     "tree-sitter-typescript>=0.23.0",
+    "tree-sitter-swift>=0.0.1",
     "kuzu>=0.11.0",
     "igraph>=1.0.0",
     "leidenalg>=0.11.0",
diff --git a/src/axon/config/languages.py b/src/axon/config/languages.py
index bd34817..d4716e5 100644
--- a/src/axon/config/languages.py
+++ b/src/axon/config/languages.py
@@ -12,6 +12,7 @@ SUPPORTED_EXTENSIONS: dict[str, str] = {
     ".jsx": "javascript",
     ".mjs": "javascript",
     ".cjs": "javascript",
+    ".swift": "swift",
 }
diff --git a/src/axon/core/ingestion/imports.py b/src/axon/core/ingestion/imports.py
index 841d88e..dfe8e91 100644
--- a/src/axon/core/ingestion/imports.py
+++ b/src/axon/core/ingestion/imports.py
@@ -154,7 +154,7 @@ def process_imports(
     source_roots = _detect_source_roots(file_index)
 
     if parallel:
-        workers = min(os.cpu_count() or 4, 8, len(parse_data))
+        workers = max(1, min(os.cpu_count() or 4, 8, len(parse_data)))
         with ThreadPoolExecutor(max_workers=workers) as pool:
diff --git a/src/axon/core/ingestion/parser_phase.py b/src/axon/core/ingestion/parser_phase.py
index 93840e9..39558f4 100644
--- a/src/axon/core/ingestion/parser_phase.py
+++ b/src/axon/core/ingestion/parser_phase.py
@@ -26,6 +26,7 @@ from axon.core.graph.model import (
 from axon.core.parsers.base import LanguageParser, ParseResult
 from axon.core.parsers.python_lang import PythonParser
+from axon.core.parsers.swift import SwiftParser
 from axon.core.parsers.typescript import TypeScriptParser
 
@@ -45,6 +46,7 @@ _PARSER_FACTORIES: dict[str, Callable[[], LanguageParser]] = {
     "javascript": lambda: TypeScriptParser(dialect="javascript"),
+    "swift": SwiftParser,
 }
New file: src/axon/core/parsers/swift.py (~400 lines)
"""Swift language parser using tree-sitter.

Extracts functions, classes, structs, enums, protocols, methods, imports,
calls, type annotations, and inheritance relationships from Swift source code.
"""

from __future__ import annotations

import tree_sitter_swift as tsswift
from tree_sitter import Language, Node, Parser

from axon.core.parsers.base import (
    CallInfo,
    ImportInfo,
    LanguageParser,
    ParseResult,
    SymbolInfo,
    TypeRef,
)

SWIFT_LANGUAGE = Language(tsswift.language())

_BUILTIN_TYPES: frozenset[str] = frozenset(
    {
        "Int", "Int8", "Int16", "Int32", "Int64",
        "UInt", "UInt8", "UInt16", "UInt32", "UInt64",
        "Float", "Double", "Bool", "String", "Character",
        "Void", "Any", "AnyObject", "Never",
        "Optional", "Array", "Dictionary", "Set",
    }
)


class SwiftParser(LanguageParser):
    """Parses Swift source code using tree-sitter."""

    def __init__(self) -> None:
        self._parser = Parser(SWIFT_LANGUAGE)

    def parse(self, content: str, file_path: str) -> ParseResult:
        tree = self._parser.parse(content.encode("utf-8"))
        result = ParseResult()
        self._walk(tree.root_node, content, result, class_name="")
        return result

    def _walk(self, node, content, result, class_name):
        for child in node.children:
            ntype = child.type
            if ntype == "function_declaration":
                self._extract_function(child, content, result, class_name)
            elif ntype == "class_declaration":
                self._extract_class_like(child, content, result)
            elif ntype == "protocol_declaration":
                self._extract_protocol(child, content, result)
            elif ntype == "typealias_declaration":
                self._extract_typealias(child, content, result)
            elif ntype == "import_declaration":
                self._extract_import(child, result)
            elif ntype == "call_expression":
                self._extract_call(child, result)
            elif ntype == "init_declaration":
                self._extract_init(child, content, result, class_name)
            elif ntype == "protocol_function_declaration":
                self._extract_protocol_function(child, content, result, class_name)
            else:
                self._walk(child, content, result, class_name)

    # ... (full implementation in the linked file above)

Full file is ~400 lines following the same _extract_* pattern as the Python and TypeScript parsers. Handles Swift-specific AST quirks like class_declaration reuse and navigation_expression for method calls.


Happy to split these into separate issues/PRs, add tests in tests/core/test_parser_swift.py, or adjust scope based on your feedback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions