Skip to content

Commit 58ddeac

Browse files
committed
unified: Hook up swift-syntax AST
At this point it's only a proof-of-concept translation -- it translates `sourceFile` nodes, but everything else gets mapped to 'unsupported_node`.
1 parent ddab6cc commit 58ddeac

13 files changed

Lines changed: 404 additions & 80 deletions

File tree

Cargo.lock

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

shared/yeast-schema/src/schema.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,28 @@ impl Schema {
167167
id
168168
}
169169

170+
/// Register every kind (named and unnamed) and field *name* from `other`
171+
/// into this schema (idempotent). Ids are assigned in this schema's own id
172+
/// space; existing ids are unchanged.
173+
///
174+
/// This is used when running desugaring rules over an AST that was built
175+
/// against a different schema (e.g. from an external parser): the rules
176+
/// build output nodes whose kind/field names come from `other`, and those
177+
/// names must resolve in the AST's own schema. Only names are needed — the
178+
/// rule engine resolves kinds/fields by name and does not consult
179+
/// `other`'s field-type or supertype information.
180+
pub fn register_names_from(&mut self, other: &Schema) {
181+
for name in other.kind_ids.keys() {
182+
self.register_kind(name);
183+
}
184+
for name in other.unnamed_kind_ids.keys() {
185+
self.register_unnamed_kind(name);
186+
}
187+
for name in other.field_ids.keys() {
188+
self.register_field(name);
189+
}
190+
}
191+
170192
/// Track a name for a kind ID without registering it as named or
171193
/// unnamed. Useful when importing tree-sitter ID tables that may
172194
/// contain duplicate IDs across the named/unnamed split.

shared/yeast/src/lib.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,15 @@ impl Ast {
524524
self.schema.register_field(name)
525525
}
526526

527-
fn union_source_range_of_children(
528-
&self,
529-
fields: &BTreeMap<FieldId, Vec<Id>>,
530-
) -> Option<Range> {
527+
/// Register every kind and field name from `schema` into this AST's schema
528+
/// (idempotent). Used before desugaring an externally-built AST so that
529+
/// rules can build output nodes whose kind/field names come from the
530+
/// desugarer's output schema.
531+
pub fn register_names_from_schema(&mut self, schema: &schema::Schema) {
532+
self.schema.register_names_from(schema);
533+
}
534+
535+
fn union_source_range_of_children(&self, fields: &BTreeMap<FieldId, Vec<Id>>) -> Option<Range> {
531536
let mut start_byte: Option<usize> = None;
532537
let mut end_byte: Option<usize> = None;
533538
let mut start_point = Point { row: 0, column: 0 };
@@ -1448,6 +1453,18 @@ impl<'a, C: Clone + Default> Runner<'a, C> {
14481453
let mut user_ctx = C::default();
14491454
self.run_with_ctx(input, &mut user_ctx)
14501455
}
1456+
1457+
/// Run all phases over an already-built `ast`, using the default context
1458+
/// (`C::default()`). Unlike [`run_from_tree`](Self::run_from_tree), the AST
1459+
/// is supplied by the caller (e.g. built from an external parser's output)
1460+
/// rather than constructed from a tree-sitter tree. The caller is
1461+
/// responsible for ensuring the AST's schema knows any output kind/field
1462+
/// names the rules will build (see [`Ast::register_names_from_schema`]).
1463+
pub fn run_from_ast(&self, mut ast: Ast) -> Result<Ast, String> {
1464+
let mut user_ctx = C::default();
1465+
self.run_phases(&mut ast, &mut user_ctx)?;
1466+
Ok(ast)
1467+
}
14511468
}
14521469

14531470
// ---------------------------------------------------------------------------
@@ -1470,6 +1487,12 @@ pub trait Desugarer: Send + Sync {
14701487
/// Parse `tree` against `source` and run the desugaring pipeline.
14711488
/// Each call constructs a fresh default user context internally.
14721489
fn run_from_tree(&self, tree: &tree_sitter::Tree, source: &[u8]) -> Result<Ast, String>;
1490+
1491+
/// Run the desugaring pipeline over an already-built `ast` (e.g. produced
1492+
/// by an external parser rather than tree-sitter). The desugarer ensures
1493+
/// the AST's schema knows its output kind/field names before running the
1494+
/// rules. Each call constructs a fresh default user context internally.
1495+
fn run_from_ast(&self, ast: Ast) -> Result<Ast, String>;
14731496
}
14741497

14751498
/// A concrete [`Desugarer`] backed by a [`DesugaringConfig<C>`] for a
@@ -1507,4 +1530,12 @@ impl<C: Default + Clone + Send + Sync + 'static> Desugarer for ConcreteDesugarer
15071530
let runner = Runner::with_schema(self.language.clone(), &self.schema, &self.config.phases);
15081531
runner.run_from_tree(tree, source)
15091532
}
1533+
1534+
fn run_from_ast(&self, mut ast: Ast) -> Result<Ast, String> {
1535+
// The AST was built against its own (external) schema; make sure the
1536+
// output kind/field names the rules build are resolvable in it.
1537+
ast.register_names_from_schema(&self.schema);
1538+
let runner = Runner::with_schema(self.language.clone(), &self.schema, &self.config.phases);
1539+
runner.run_from_ast(ast)
1540+
}
15101541
}

shared/yeast/tests/test.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,59 @@ fn test_query_match() {
266266
assert!(captures.get_var("right").is_ok());
267267
}
268268

269+
#[test]
270+
fn test_run_from_ast_desugars_hand_built_tree() {
271+
use std::collections::BTreeMap;
272+
273+
// Output schema for the desugared tree. Its kind/field names must become
274+
// resolvable in the hand-built AST's schema for the rule to build them.
275+
let schema_yaml = r#"
276+
named:
277+
assignment:
278+
left: leaf
279+
leaf:
280+
"#;
281+
282+
// A rule over an *input* kind (`wrapper`) that is not in the output schema,
283+
// rewriting to an output `assignment` node.
284+
let rules: Vec<Rule> = vec![yeast::rule!(
285+
(wrapper)
286+
=>
287+
(assignment left: (leaf "lit"))
288+
)];
289+
290+
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
291+
let config = DesugaringConfig::<()>::new()
292+
.add_phase("test", PhaseKind::OneShot, rules)
293+
.with_output_node_types_yaml(schema_yaml);
294+
let desugarer = ConcreteDesugarer::new(lang, config).unwrap();
295+
296+
// Build the input AST by hand, as an external parser adapter would. The
297+
// schema starts empty and gains the `wrapper` input kind on the fly.
298+
let mut ast = Ast::with_schema(yeast::schema::Schema::new());
299+
let wrapper_kind = ast.register_kind("wrapper");
300+
let root = ast.create_node_with_range(
301+
wrapper_kind,
302+
NodeContent::DynamicString(String::new()),
303+
BTreeMap::new(),
304+
true,
305+
None,
306+
);
307+
ast.set_root(root);
308+
309+
// Desugaring the hand-built AST applies the rule, producing `assignment`
310+
// even though the AST was built against a schema with no output kinds.
311+
let out = desugarer
312+
.run_from_ast(ast)
313+
.expect("run_from_ast should succeed");
314+
let out_root = out.get_node(out.get_root()).expect("root exists");
315+
assert_eq!(out_root.kind_name(), "assignment");
316+
let dump = dump_ast(&out, out.get_root(), "");
317+
assert!(dump.contains("assignment"), "unexpected dump: {dump}");
318+
assert!(dump.contains("left"), "unexpected dump: {dump}");
319+
assert!(dump.contains("leaf"), "unexpected dump: {dump}");
320+
}
321+
269322
#[test]
270323
fn test_query_no_match() {
271324
let runner: Runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), &[]);

unified/extractor/src/languages/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ use codeql_extractor::extractor::simple;
33
#[path = "swift/swift.rs"]
44
mod swift;
55

6+
/// swift-syntax JSON -> `yeast::Ast` adapter for the Swift front-end.
7+
///
8+
/// Currently exercised by tests and the forthcoming runtime extraction path;
9+
/// `allow(dead_code)` because this is a binary crate, so its public API isn't
10+
/// counted as used until the binary itself calls it.
11+
#[path = "swift/adapter.rs"]
12+
#[allow(dead_code)]
13+
pub mod swift_adapter;
14+
615
/// Shared YEAST output AST schema for all languages.
716
pub(crate) const OUTPUT_AST_SCHEMA: &str = include_str!("../../ast_types.yml");
817

unified/swift-syntax-rs/src/yeast_adapter.rs renamed to unified/extractor/src/languages/swift/adapter.rs

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
//! Adapter that converts the swift-syntax JSON tree (see [`crate::parse_to_json`])
2-
//! into a [`yeast::Ast`], the in-memory format the CodeQL desugaring rules
3-
//! operate on.
1+
//! Converts the swift-syntax JSON syntax tree into a [`yeast::Ast`], the
2+
//! in-memory format the CodeQL desugaring rules operate on.
3+
//!
4+
//! The JSON tree is produced by the `swift-syntax-rs` crate's Swift FFI shim
5+
//! (`parse_to_json`). This module is pure Rust (only `yeast` + `serde_json`),
6+
//! so the extractor consumes swift-syntax output without pulling in the Swift
7+
//! toolchain (the JSON is produced out-of-process).
48
//!
59
//! The mapping mirrors tree-sitter's node model, which is what yeast (and the
610
//! extractor's rewrite rules) expect:
@@ -15,9 +19,8 @@
1519
//! list-valued field maps directly to that field holding several children.
1620
//!
1721
//! Note: this preserves swift-syntax's own kind/field names. Aligning those
18-
//! names with the tree-sitter-swift schema (so the existing rewrite rules fire)
19-
//! is a separate, later step; this module is only concerned with getting the
20-
//! tree into yeast's format.
22+
//! names with the tree-sitter-swift schema (so the rewrite rules in
23+
//! [`super::swift`] fire) is done incrementally in the rules.
2124
2225
use std::collections::BTreeMap;
2326

@@ -409,40 +412,4 @@ mod tests {
409412
"comment should not appear as an AST node"
410413
);
411414
}
412-
413-
/// End-to-end: real Swift source parsed by the shim, then adapted into a
414-
/// `yeast::Ast`. Requires the Swift toolchain (like the crate's FFI tests).
415-
#[test]
416-
fn end_to_end_from_swift_source() {
417-
let json = crate::parse_to_json("func f(n: Int) -> Int { return n } // trailing")
418-
.expect("parsing should succeed");
419-
let adapted = json_to_ast(&json).expect("adapter should succeed");
420-
let ast = &adapted.ast;
421-
422-
let root = ast.get_node(ast.get_root()).expect("root exists");
423-
assert_eq!(root.kind_name(), "sourceFile");
424-
425-
// The tree contains a `functionDecl` layout node and an anonymous
426-
// `func` keyword token keyed by its text.
427-
let mut kinds: Vec<&str> = ast.nodes().iter().map(|n| n.kind_name()).collect();
428-
kinds.sort_unstable();
429-
assert!(
430-
kinds.contains(&"functionDecl"),
431-
"expected a functionDecl node, got kinds: {kinds:?}"
432-
);
433-
assert!(
434-
kinds.contains(&"func"),
435-
"expected an anonymous `func` token, got kinds: {kinds:?}"
436-
);
437-
438-
// The trailing comment is recovered into the side channel.
439-
assert!(
440-
adapted
441-
.trivia
442-
.iter()
443-
.any(|t| t.kind == "lineComment" && t.text == "// trailing"),
444-
"expected the trailing comment in the trivia side channel, got: {:?}",
445-
adapted.trivia
446-
);
447-
}
448415
}

unified/extractor/src/languages/swift/swift.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,25 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
135135
// Declarations may be wrapped in local/global wrapper nodes.
136136
rule!((global_declaration _ @inner) => stmt { inner }),
137137
rule!((local_declaration _ @inner) => stmt { inner }),
138+
// ---- swift-syntax front-end (minimal hook-up) ----
139+
// These rules target the swift-syntax AST (camelCase kind names),
140+
// produced by the sibling `adapter` module. They coexist with the
141+
// tree-sitter rules (snake_case names): rules are dispatched by exact
142+
// kind name, and the two name spaces never collide, so these are inert
143+
// on the tree-sitter path. Only the minimal top-level mapping lives here
144+
// to demonstrate the pipeline end-to-end; the full translation is added
145+
// separately. Unmatched swift-syntax nodes fall through to the
146+
// `unsupported_node` fallback at the end.
147+
//
148+
// `sourceFile` holds its top-level statements in an (elided)
149+
// `statements` collection; each element is a `codeBlockItem` wrapping
150+
// the real node.
151+
rule!(
152+
(sourceFile statements: _* @items)
153+
=>
154+
(top_level body: (block stmt: {items}))
155+
),
156+
rule!((codeBlockItem item: @item) => stmt { item }),
138157
// ---- Literals ----
139158
rule!((integer_literal) => (int_literal)),
140159
rule!((hex_literal) => (int_literal)),

0 commit comments

Comments
 (0)