-
Notifications
You must be signed in to change notification settings - Fork 0
Query API
The Query static class provides a fluent CSS-like selector API for finding nodes in a syntax tree.
using TinyTokenizer.Ast;
var tree = SyntaxTree.Parse("foo(x) + bar(y)");
// Select nodes matching a query
var idents = tree.Select(Query.AnyIdent).ToList(); // [Ident("foo"), Ident("x"), Ident("bar"), Ident("y")]
// Use in editor
tree.CreateEditor()
.Replace(Query.Ident("foo"), "baz") // Matches: Ident("foo")
.Commit();Match specific token content:
Query.Ident("main") // Identifier with text "main"
Query.Symbol(".") // Dot symbol
Query.Operator("=>") // Arrow operator
Query.Numeric("42") // Number literal "42"
Query.String("\"hello\"") // String literal
Query.TaggedIdent("#define") // Tagged identifierMatch any token of a kind:
Query.AnyIdent // All identifiers
Query.AnyNumeric // All numbers
Query.AnyString // All strings
Query.AnyOperator // All operators
Query.AnySymbol // All symbols
Query.AnyTaggedIdent // All tagged identifiers
Query.AnyComment // All comments
Query.Any // Any single nodeMatch block delimiters:
Query.BraceBlock // All { } blocks
Query.BracketBlock // All [ ] blocks
Query.ParenBlock // All ( ) blocks
Query.AnyBlock // Any block typeMatch language keywords defined in the schema:
// All keywords (requires schema with keyword definitions)
Query.AnyKeyword // All keyword tokens
// Specific keyword text
Query.Keyword("if") // Matches "if" keyword
Query.Keyword("return") // Matches "return" keyword
// Keywords in a category
Query.KeywordCategory("Types") // int, float, double, void...
Query.KeywordCategory("ControlFlow") // if, else, while, for, return...
Query.KeywordCategory("Modifiers") // public, private, static, const...Keywords are defined via Schema.Create().DefineKeywords() — see Schema#Keyword Definitions.
Match document boundaries:
Query.BOF // Beginning of file (first token at root)
Query.EOF // End of file (last token at root)Line-based matching is done via trivia, not via dedicated newline tokens.
Query.Newline // Node occurs after a newline
Query.NotNewline // Exact negation of Query.NewlineSemantics:
- A node matches
Query.Newlinewhen either:- The node owns leading newline trivia, OR
- The previous sibling owns trailing newline trivia.
This is useful with repetition:
// Consume tokens on the current line (terminator is not consumed)
Query.Any.Until(Query.Newline)Refine queries with predicates:
// Text-based filters
Query.AnyIdent.WithText("foo") // Exact match
Query.AnyIdent.WithTextContaining("test") // Contains substring
Query.AnyIdent.WithTextStartingWith("_") // Starts with prefix
Query.AnyIdent.WithTextEndingWith("Async") // Ends with suffix
// Custom predicate
Query.AnyIdent.Where(n => n.Width > 5) // Width > 5
Query.AnyNumeric.Where(n => int.Parse(n.Text) > 100)Limit matches:
Query.AnyIdent.First() // First match only
Query.AnyIdent.Last() // Last match only
Query.AnyIdent.Nth(2) // Third match (0-indexed)
Query.AnyIdent.Skip(1) // Skip first match
Query.AnyIdent.Take(3) // Take first 3 matchesCombine queries:
// Union (OR) — matches if either matches
Query.AnyIdent | Query.AnyNumeric
// Intersection (AND) — matches if both match
Query.AnyIdent & Query.Leaf
// Variadic OR
Query.AnyOf(Query.AnyIdent, Query.AnyNumeric, Query.AnyString)
// Match when none match
Query.NoneOf(Query.Ident("if"), Query.Ident("else"), Query.Ident("while"))Zero-width negative assertion:
// Negative lookahead
Query.Not(Query.Ident("if")) // Not the identifier "if"
Query.AnyIdent.Not() // Fluent syntaxMatch consecutive nodes:
// Static method
Query.Sequence(Query.AnyIdent, Query.ParenBlock) // ident then parens
// Fluent chaining
Query.AnyIdent.Then(Query.ParenBlock)
// Multiple elements
Query.Sequence(
Query.AnyIdent,
Query.Symbol("."),
Query.AnyIdent,
Query.ParenBlock
) // Matches: obj.method()Match repeated patterns:
Query.AnyIdent.Optional() // Match 0 or 1
Query.AnyIdent.ZeroOrMore() // Match 0 or more
Query.AnyIdent.OneOrMore() // Match 1 or more
Query.AnyIdent.Exactly(3) // Match exactly 3
Query.AnyIdent.Repeat(2, 5) // Match 2 to 5
// Repeat until terminator (terminator not consumed)
Query.Any.Until(Query.Newline)Zero-width assertions about following nodes:
// Positive lookahead — must be followed by
Query.AnyIdent.FollowedBy(Query.ParenBlock) // func(...)
// Negative lookahead — must not be followed by
Query.AnyIdent.NotFollowedBy(Query.ParenBlock) // Not a function callQueries based on tree structure:
// Sibling checks
Query.Sibling(1) // Next sibling exists
Query.Sibling(-1) // Previous sibling exists
Query.Sibling(1, Query.AnyIdent) // Next sibling is identifier
// Fluent sibling checks
Query.AnyIdent.NextSibling() // Has next sibling
Query.AnyIdent.PreviousSibling() // Has previous sibling
Query.AnyIdent.NextSibling(Query.ParenBlock) // Next sibling is paren block
// Parent/ancestor checks
Query.Parent(Query.BraceBlock) // Parent is a brace block
Query.Ancestor(Query.BraceBlock) // Any ancestor is a brace block
// Match by parent/ancestor
Query.BraceBlock.AsParent() // Match nodes whose parent is brace block
Query.BraceBlock.AsAncestor() // Match nodes with brace block ancestorMatch content between delimiters:
// Match everything between < and >
Query.Between(Query.Operator("<"), Query.Operator(">"))Match a specific node instance:
var tree = SyntaxTree.Parse("foo bar baz");
var node = tree.Select(Query.Ident("foo")).First(); // Ident("foo")
// Query that only matches this exact node
Query.Exact(node)
// Useful in editor when you have a node reference
tree.CreateEditor()
.Replace(Query.Exact(node), "qux") // Replaces only the matched "foo"
.Commit();
// Result: "qux bar baz"Block queries can select their boundaries for insertion:
// Get block boundary positions
Query.BraceBlock.First().Start() // Opening delimiter {
Query.BraceBlock.First().End() // Closing delimiter }
// Use with InsertBefore/InsertAfter
tree.CreateEditor()
.InsertAfter(Query.BraceBlock.First().Start(), "// first line") // After {
.InsertBefore(Query.BraceBlock.First().End(), "// last line") // Before }
.Commit();| Position | Code | Result |
|---|---|---|
| Inside start | InsertAfter(block.Start(), x) |
{ x ...} |
| Inside end | InsertBefore(block.End(), x) |
{... x } |
| Before block | InsertBefore(block, x) |
x { } |
| After block | InsertAfter(block, x) |
{ } x |
For syntax nodes implementing INamedNode:
// Find functions by name
Query.Syntax<FunctionSyntax>().Named("main")
// Use with InsertBefore/InsertAfter
tree.CreateEditor()
.InsertBefore(Query.Syntax<FunctionSyntax>().Named("foo"), "// doc\n")
.Commit();For syntax nodes implementing IBlockContainerNode:
// Insert into named blocks using convenience methods
var funcQuery = Query.Syntax<FunctionSyntax>().Named("main");
tree.CreateEditor()
.InsertAfter(funcQuery.InnerStart("body"), "\n console.log('enter');")
.InsertBefore(funcQuery.InnerEnd("body"), "\n console.log('exit');")
.Commit();
// Or use the explicit Block().Start()/End() form
tree.CreateEditor()
.InsertAfter(funcQuery.Block("body").Start(), "\n console.log('enter');")
.InsertBefore(funcQuery.Block("body").End(), "\n console.log('exit');")
.Commit();| Combinator | Description | Example |
|---|---|---|
Query.Ident("x") |
Specific identifier | Query.Ident("main") |
Query.Symbol(".") |
Specific symbol | Query.Symbol(".") |
Query.Operator("=>") |
Specific operator | Query.Operator("=>") |
Query.Numeric("42") |
Specific number | Query.Numeric("3.14") |
Query.Keyword("if") |
Specific keyword | Query.Keyword("return") |
Query.AnyIdent |
Any identifier | Query.AnyIdent |
Query.AnySymbol |
Any symbol | Query.AnySymbol |
Query.AnyOperator |
Any operator | Query.AnyOperator |
Query.AnyNumeric |
Any number | Query.AnyNumeric |
Query.AnyString |
Any string | Query.AnyString |
Query.AnyKeyword |
Any keyword | Query.AnyKeyword |
Query.KeywordCategory("X") |
Keywords in category | Query.KeywordCategory("Types") |
Query.ParenBlock |
( ) block |
Query.ParenBlock |
Query.BraceBlock |
{ } block |
Query.BraceBlock |
Query.BracketBlock |
[ ] block |
Query.BracketBlock |
Query.Any |
Any single node | Query.Any |
Query.Sequence(...) |
Match A then B | Query.Sequence(a, b) |
a | b |
Match A or B | Query.AnyIdent | Query.AnyNumeric |
.Optional() |
Match 0 or 1 | Query.AnyOperator.Optional() |
.ZeroOrMore() |
Match 0+ | Query.AnyIdent.ZeroOrMore() |
.OneOrMore() |
Match 1+ | Query.AnyIdent.OneOrMore() |
.Exactly(n) |
Match exactly n | Query.AnyIdent.Exactly(3) |
.Repeat(min, max) |
Match range | Query.AnyIdent.Repeat(2, 5) |
.Until(q) |
Repeat until | Query.Any.Until(Query.Newline) |
.FollowedBy(q) |
Positive lookahead | Query.AnyIdent.FollowedBy(Query.ParenBlock) |
.NotFollowedBy(q) |
Negative lookahead | Query.AnyIdent.NotFollowedBy(Query.ParenBlock) |
.Then(q) |
Fluent sequence | Query.AnyIdent.Then(Query.ParenBlock) |
Query.Exact(node) |
Exact node | Query.Exact(myNode) |
.First() |
First match | Query.AnyIdent.First() |
.Last() |
Last match | Query.AnyIdent.Last() |
.Nth(n) |
Nth match | Query.AnyIdent.Nth(2) |
.Where(pred) |
Filter | Query.AnyIdent.Where(n => ...) |
.Start() |
Block opener boundary | Query.BraceBlock.Start() |
.End() |
Block closer boundary | Query.BraceBlock.End() |
.InnerStart(name) |
Named block start | funcQuery.InnerStart("body") |
.InnerEnd(name) |
Named block end | funcQuery.InnerEnd("body") |
- SyntaxEditor — Using queries for editing
- Syntax Nodes — Named and block container nodes
- TinyAst Guide — Syntax tree overview