Skip to content

Query API

David Sisco edited this page Jan 4, 2026 · 4 revisions

Query API

The Query static class provides a fluent CSS-like selector API for finding nodes in a syntax tree.

Basic Usage

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();

Named Queries

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 identifier

Any-Kind Queries

Match 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 node

Block Queries

Match block delimiters:

Query.BraceBlock               // All { } blocks
Query.BracketBlock             // All [ ] blocks
Query.ParenBlock               // All ( ) blocks
Query.AnyBlock                 // Any block type

Keyword Queries

Match 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.

Boundary Assertions

Match document boundaries:

Query.BOF                      // Beginning of file (first token at root)
Query.EOF                      // End of file (last token at root)

Filters

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)

Pseudo-Selectors

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 matches

Composition

Combine 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"))

Negation

Zero-width negative assertion:

// Negative lookahead
Query.Not(Query.Ident("if"))         // Not the identifier "if"
Query.AnyIdent.Not()                 // Fluent syntax

Sequences

Match 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()

Repetition

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)

Lookahead

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 call

Navigation Queries

Queries 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 ancestor

Content Matching

Match content between delimiters:

// Match everything between < and >
Query.Between(Query.Operator("<"), Query.Operator(">"))

Exact Node Reference

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"

Insertion Positions

Queries resolve to insertion points with position modifiers:

// Relative to matched node
Query.Ident("foo").First().Before()    // Insert before
Query.Ident("foo").First().After()     // Insert after

// Inside blocks
Query.BraceBlock.First().InnerStart()  // After opening {
Query.BraceBlock.First().InnerEnd()    // Before closing }

Named Node Queries

For syntax nodes implementing INamedNode:

// Find functions by name
Query.Syntax<FunctionSyntax>().Named("main")

// Use with positions
tree.CreateEditor()
    .Insert(Query.Syntax<FunctionSyntax>().Named("foo").Before(), "// doc\n")
    .Commit();

Block Container Queries

For syntax nodes implementing IBlockContainerNode:

// Insert into named blocks
var funcQuery = Query.Syntax<FunctionSyntax>().Named("main");

tree.CreateEditor()
    .Insert(funcQuery.InnerStart("body"), "\n    console.log('enter');")
    .Insert(funcQuery.InnerEnd("body"), "\n    console.log('exit');")
    .Insert(funcQuery.InnerStart("params"), "int x")
    .Commit();

Quick Reference Table

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 => ...)
.Before() Position before Query.Ident("x").Before()
.After() Position after Query.Ident("x").After()
.InnerStart() Inside block start Query.BraceBlock.InnerStart()
.InnerEnd() Inside block end Query.BraceBlock.InnerEnd()

See Also

Clone this wiki locally