Skip to content

Commit 446ff7c

Browse files
committed
unified: Add mapping from swift-syntax JSON to yeast AST
Adds a preliminary mapping that decodes the JSON into the yeast AST. The JSON AST itself is still very verbose, but it would be useful to see if it actually contains all of the things we want it to contain.
1 parent 87c8173 commit 446ff7c

7 files changed

Lines changed: 369 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shared/yeast/src/lib.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,28 @@ impl Ast {
333333
ast
334334
}
335335

336+
/// Construct an empty AST backed by `schema`, for building a tree
337+
/// programmatically from a source other than a tree-sitter parse (e.g. an
338+
/// external parser's output). Populate it with [`Ast::create_node`] /
339+
/// [`Ast::create_node_with_range`] and then designate the root with
340+
/// [`Ast::set_root`]. The `schema` must already have every node kind and
341+
/// field name registered (see [`schema::Schema`]).
342+
pub fn with_schema(schema: schema::Schema) -> Self {
343+
Self {
344+
root: Id(0),
345+
nodes: Vec::new(),
346+
schema,
347+
source: Vec::new(),
348+
}
349+
}
350+
351+
/// Set the original source bytes, used to resolve `NodeContent::Range`
352+
/// nodes to text. Nodes built with inline `NodeContent::DynamicString`
353+
/// content do not require this.
354+
pub fn set_source(&mut self, source: Vec<u8>) {
355+
self.source = source;
356+
}
357+
336358
/// Returns the source text for `id`, resolving `NodeContent::Range`
337359
/// against the stored source bytes when available.
338360
pub fn source_text(&self, id: Id) -> String {
@@ -463,6 +485,24 @@ impl Ast {
463485
Id(id)
464486
}
465487

488+
/// Register a named node kind, returning its id (idempotent). Lets callers
489+
/// build an AST in a single pass, registering kinds as nodes are created
490+
/// rather than pre-populating the schema.
491+
pub fn register_kind(&mut self, name: &str) -> KindId {
492+
self.schema.register_kind(name)
493+
}
494+
495+
/// Register an anonymous (unnamed) token kind, returning its id
496+
/// (idempotent). Anonymous tokens are keyed by their text (e.g. `"func"`).
497+
pub fn register_unnamed_kind(&mut self, name: &str) -> KindId {
498+
self.schema.register_unnamed_kind(name)
499+
}
500+
501+
/// Register a field name, returning its id (idempotent).
502+
pub fn register_field(&mut self, name: &str) -> FieldId {
503+
self.schema.register_field(name)
504+
}
505+
466506
fn union_source_range_of_children(
467507
&self,
468508
fields: &BTreeMap<FieldId, Vec<Id>>,
@@ -604,7 +644,7 @@ impl Ast {
604644
}
605645
}
606646

607-
fn id_for_unnamed_node_kind(&self, kind: &str) -> Option<KindId> {
647+
pub fn id_for_unnamed_node_kind(&self, kind: &str) -> Option<KindId> {
608648
let id = self.schema.id_for_unnamed_node_kind(kind).unwrap_or(0);
609649
if id == 0 {
610650
None

unified/swift-syntax-rs/BUILD.bazel

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,16 @@ swift_library(
2828
# Swift shim in the local `cargo` workflow.
2929
rust_library(
3030
name = "swift_syntax_rs",
31-
srcs = ["src/lib.rs"],
31+
srcs = glob(
32+
["src/**/*.rs"],
33+
exclude = ["src/main.rs"],
34+
),
3235
edition = "2024",
33-
deps = [":swift_syntax_ffi"],
36+
deps = [
37+
":swift_syntax_ffi",
38+
"//shared/yeast",
39+
"@vendor_ts__serde_json-1.0.145//:serde_json",
40+
],
3441
)
3542

3643
rust_binary(

unified/swift-syntax-rs/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ path = "src/lib.rs"
1212
[[bin]]
1313
name = "swift-syntax-parse"
1414
path = "src/main.rs"
15+
16+
[dependencies]
17+
serde_json = "1.0"
18+
yeast = { path = "../../shared/yeast" }

unified/swift-syntax-rs/README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ The emitted JSON tree preserves the AST's named structure. Every node has a
1313
`kind` and a `range` with `start`/`end` positions (UTF-8 `offset` plus 1-based
1414
`line`/`column`). Beyond that:
1515

16-
- **Tokens** carry `text`, `tokenKind`, and `leadingTrivia`/`trailingTrivia`
17-
arrays of `{ kind, text }` pieces.
16+
- **Tokens** carry `text`, `tokenKind`, and — only when non-empty —
17+
`leadingTrivia`/`trailingTrivia` arrays of `{ kind, text }` pieces.
1818
- **Layout nodes** (e.g. `functionDecl`) embed their children directly as
1919
members keyed by the child's name in the parent (`name`, `signature`,
2020
`body`, …), alongside `kind`/`range`. Absent optional children are omitted.
@@ -49,9 +49,7 @@ abbreviated here as `…`):
4949
"kind": "token",
5050
"text": "let",
5151
"tokenKind": "keyword(SwiftSyntax.Keyword.let)",
52-
"range": …,
53-
"leadingTrivia": [],
54-
"trailingTrivia": []
52+
"range":
5553
},
5654
"bindings": [
5755
{
@@ -60,12 +58,12 @@ abbreviated here as `…`):
6058
"pattern": {
6159
"kind": "identifierPattern",
6260
"range": …,
63-
"identifier": { "kind": "token", "text": "x", "tokenKind": "identifier(\"x\")", "range":, "leadingTrivia": [], "trailingTrivia": [] }
61+
"identifier": { "kind": "token", "text": "x", "tokenKind": "identifier(\"x\")", "range": … }
6462
},
6563
"initializer": {
6664
"kind": "initializerClause",
6765
"range": …,
68-
"equal": { "kind": "token", "text": "=", "tokenKind": "equal", "range":, "leadingTrivia": [], "trailingTrivia": [] },
66+
"equal": { "kind": "token", "text": "=", "tokenKind": "equal", "range": … },
6967
"value": {
7068
"kind": "integerLiteralExpr",
7169
"range": …,
@@ -74,7 +72,6 @@ abbreviated here as `…`):
7472
"text": "1",
7573
"tokenKind": "integerLiteral(\"1\")",
7674
"range": …,
77-
"leadingTrivia": [],
7875
"trailingTrivia": [ { "kind": "lineComment", "text": "// c" } ]
7976
}
8077
}
@@ -84,14 +81,15 @@ abbreviated here as `…`):
8481
}
8582
}
8683
],
87-
"endOfFileToken": { "kind": "token", "text": "", "tokenKind": "endOfFile", "range":, "leadingTrivia": [], "trailingTrivia": [] }
84+
"endOfFileToken": { "kind": "token", "text": "", "tokenKind": "endOfFile", "range": … }
8885
}
8986
```
9087

9188
Note how `statements`, `bindings`, `attributes`, and `modifiers` are plain
9289
arrays (their collection nodes are elided), layout children such as
9390
`bindingSpecifier` and `initializer` are embedded by name, and the `// c`
94-
comment rides along as `trailingTrivia` on the token it follows.
91+
comment rides along as `trailingTrivia` on the token it follows. Tokens without
92+
trivia (most of them) simply omit the `leadingTrivia`/`trailingTrivia` keys.
9593

9694
## Prerequisites
9795

@@ -171,10 +169,29 @@ CLI (reads a file argument or stdin, prints the syntax tree as JSON):
171169
echo 'let x = 1' | cargo run --bin swift-syntax-parse
172170
```
173171

172+
## Converting to a yeast AST
173+
174+
For use in the CodeQL extractor, the JSON tree can be converted into a
175+
[`yeast::Ast`](../../shared/yeast) — the in-memory format the extractor's
176+
rewrite rules operate on — via [`yeast_adapter::json_to_ast`](src/yeast_adapter.rs):
177+
178+
```rust
179+
let json = swift_syntax_rs::parse_to_json("let x = 1")?;
180+
let ast = swift_syntax_rs::yeast_adapter::json_to_ast(&json)?;
181+
```
182+
183+
The adapter mirrors tree-sitter's node model, which is what yeast expects:
184+
layout nodes and varying tokens (identifiers, literals, operators) become
185+
**named** nodes; fixed tokens (keywords, punctuation) become **anonymous**
186+
nodes keyed by their text. It preserves swift-syntax's own kind/field names —
187+
aligning them with the tree-sitter-swift schema so the existing rewrite rules
188+
fire is a separate, later step.
189+
174190
## Layout
175191

176192
- `swift/` — Swift package exposing the `ssr_parse_json` / `ssr_string_free` C ABI.
177193
- `build.rs` — builds the Swift package and emits link/rpath flags (local `cargo` only).
178194
- `BUILD.bazel` — Bazel targets for the hermetic CI build (swift_library + rust targets).
179195
- `src/lib.rs` — safe Rust bindings (`parse_to_json`).
196+
- `src/yeast_adapter.rs` — converts the JSON tree into a `yeast::Ast`.
180197
- `src/main.rs` — demo CLI.

unified/swift-syntax-rs/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
//! against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. This module
66
//! provides safe Rust bindings on top of that ABI.
77
8+
pub mod yeast_adapter;
9+
810
use std::ffi::{CStr, CString};
911
use std::os::raw::c_char;
1012

0 commit comments

Comments
 (0)