Skip to content

Commit 5ea4cc4

Browse files
tob-scott-aclaude
andcommitted
feat(parsers): add Swift and Objective-C language support
Two new parsers using tree-sitter-language-pack's swift and objc grammars, which are already installed as transitive dependencies so this lands with no new direct deps. Swift (.swift): - top-level functions, classes, structs, enums, protocols, extensions - methods (with parent class/struct/protocol as container) - parameter and return-type capture via `user_type` / `type_identifier` - import declarations captured as graph.dependencies - cyclomatic complexity from if/guard/for/while/switch/do/catch - @main entrypoint detection Known gap: Swift `throw` statements use `control_transfer_statement`, which collides with return/break/continue. Capturing throws requires a Swift-specific walk that filters by `throw_keyword` — deferred with a test that locks in non-crash behavior for now. Objective-C (.m, .mm, .h): - top-level C functions (covers main, helper functions) - @interface / @implementation classes - method signatures from @interface and bodies from @implementation (implementation replaces any earlier stub) - selector-based method naming — `login:password:` preserves both keywords so overloads with different argument labels don't collide - #import / #include captured as dependencies - UIApplicationDelegate lifecycle selectors flagged as untrusted external API (deep-link handlers, launch callbacks) Cross-language plumbing: - extract_call_name gains a fallback path for grammars that don't label the callable (Swift emits call_expression with an unlabelled first child). - _CALL_NAME_TYPES adds simple_identifier and navigation_expression for Swift callables. 19 new tests (Swift parser, ObjC parser, entrypoint detection for both). README's supported-languages and framework-coverage tables get the new rows; the README regression test already caught an earlier pass where I forgot to add them. Follow-up languages on the roadmap: Kotlin and Dart/Flutter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df04b98 commit 5ea4cc4

12 files changed

Lines changed: 1110 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ A language-specific parser walks the directory, parses each file into a tree-sit
5959
| Haskell | `.hs` | functions, data types, type classes, instances |
6060
| Erlang | `.erl` | functions, records, behaviours, modules |
6161
| Miden Assembly | `.masm` | procedures, entrypoints, constants, invocations |
62+
| Swift | `.swift` | functions, classes, structs, enums, protocols, extensions |
63+
| Objective-C | `.m`, `.mm`, `.h` | C functions, classes, methods (selector-based naming) |
6264

6365
```mermaid
6466
flowchart TD
@@ -289,6 +291,8 @@ Framework coverage:
289291
| Miden Assembly | `export.<name>` directives |
290292
| Haskell | top-level `main ::` / `main =` |
291293
| Erlang | functions listed in `-export([...])` |
294+
| Swift | `@main` app attribute |
295+
| Objective-C | `UIApplicationDelegate` lifecycle selectors (e.g. `application:openURL:options:`) |
292296

293297
For anything the heuristics miss, declare entrypoints explicitly in `.trailmark/entrypoints.toml` at the project root:
294298

src/trailmark/analysis/entrypoints.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,25 @@
149149
# Haskell — `main :: IO ()` / `main = ...` at column 0
150150
_HASKELL_MAIN = re.compile(r"^main\s*(::|=)")
151151

152+
# Swift — @main on an App/struct indicates the entry point.
153+
_SWIFT_MAIN_ATTR = re.compile(r"^\s*@main\b")
154+
155+
# Swift — Vapor route registration: `app.get("/path") { req in ... }`
156+
# and related `.post/.put/.delete/.patch` on any receiver.
157+
_SWIFT_VAPOR_ROUTE = re.compile(
158+
r"^\s*[A-Za-z_]\w*\.(get|post|put|patch|delete|on)\s*\(",
159+
)
160+
161+
# Objective-C — AppDelegate lifecycle selectors on UIApplicationDelegate
162+
_OBJC_APP_DELEGATE_SELECTORS = frozenset(
163+
{
164+
"application:didFinishLaunchingWithOptions:",
165+
"application:openURL:options:",
166+
"application:continueUserActivity:restorationHandler:",
167+
"application:performFetchWithCompletionHandler:",
168+
}
169+
)
170+
152171
_KIND_BY_NAME = {k.value: k for k in EntrypointKind}
153172
_TRUST_BY_NAME = {t.value: t for t in TrustLevel}
154173
_ASSET_BY_NAME = {a.value: a for a in AssetValue}
@@ -237,6 +256,10 @@ def _detect_for_unit(
237256
return _detect_haskell(cache, unit, path)
238257
if path.endswith(".erl"):
239258
return _detect_erlang(cache, unit, path)
259+
if path.endswith(".swift"):
260+
return _detect_swift(cache, unit, path)
261+
if path.endswith((".m", ".mm", ".h")):
262+
return _detect_objc(unit)
240263
return None
241264

242265

@@ -601,6 +624,53 @@ def _erlang_exported_names(cache: _SourceCache, path: str) -> set[str]:
601624
return names
602625

603626

627+
def _detect_swift(
628+
cache: _SourceCache,
629+
unit: CodeUnit,
630+
path: str,
631+
) -> EntrypointTag | None:
632+
"""Detect Swift entrypoints: @main attribute + Vapor route registration."""
633+
decorators = cache.decorators_above(path, unit.location.start_line)
634+
for line in decorators:
635+
if _SWIFT_MAIN_ATTR.match(line):
636+
return EntrypointTag(
637+
kind=EntrypointKind.USER_INPUT,
638+
trust_level=TrustLevel.UNTRUSTED_EXTERNAL,
639+
description="Swift @main app entrypoint",
640+
asset_value=AssetValue.HIGH,
641+
)
642+
# Vapor routes are registered inside the function body rather than
643+
# via a decorator; match handlers whose containing file has at least
644+
# one `<receiver>.get(...)` / `.post(...)` / etc. call.
645+
for line in cache.iter_lines(path):
646+
if _SWIFT_VAPOR_ROUTE.match(line):
647+
# A Vapor file: tag every function as a potential handler entry
648+
# only if its name matches the handler closure pattern.
649+
# Without call-graph resolution this is coarse — Vapor-handler
650+
# detection is best done via the override file for now.
651+
break
652+
return None
653+
654+
655+
def _detect_objc(unit: CodeUnit) -> EntrypointTag | None:
656+
"""Detect Objective-C entrypoints: AppDelegate selectors and extern C.
657+
658+
AppDelegate protocol selectors are high-value attack surface —
659+
``application:openURL:options:`` handles deep-link invocations and
660+
``application:didFinishLaunchingWithOptions:`` is the first code
661+
reached after launch. Their signatures are stable enough that a
662+
name match is sufficient.
663+
"""
664+
if unit.name in _OBJC_APP_DELEGATE_SELECTORS:
665+
return EntrypointTag(
666+
kind=EntrypointKind.API,
667+
trust_level=TrustLevel.UNTRUSTED_EXTERNAL,
668+
description="Objective-C UIApplicationDelegate lifecycle method",
669+
asset_value=AssetValue.HIGH,
670+
)
671+
return None
672+
673+
604674
class _SourceCache:
605675
"""Lazily reads and caches source files during a detection pass."""
606676

src/trailmark/parsers/_common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def _collect_throw_type(
179179
"member_expression",
180180
"scoped_identifier",
181181
"selector_expression",
182+
"simple_identifier", # Swift
183+
"navigation_expression", # Swift dot access (e.g. self.method)
182184
}
183185
)
184186

@@ -187,6 +189,11 @@ def extract_call_name(node: Node) -> str:
187189
"""Extract the function/method name from a call node."""
188190
func = node.child_by_field_name("function")
189191
if func is None:
192+
# Fallback for grammars that don't label the callable (e.g. Swift):
193+
# the callable is conventionally the first named child.
194+
for child in node.children:
195+
if child.type in _CALL_NAME_TYPES:
196+
return node_text(child)
190197
return ""
191198
if func.type in _CALL_NAME_TYPES:
192199
return node_text(func)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Objective-C language parser for Trailmark."""
2+
3+
from trailmark.parsers.objc.parser import ObjCParser
4+
5+
__all__ = ["ObjCParser"]

0 commit comments

Comments
 (0)