Walk and Filter the AST
After parsing, the AST is a tree of Kolasu (Starlasu) nodes. The first thing most tools do is walk that tree: visit every node, filter by type, and read properties or source positions.
This recipe shows the two main approaches bundled with the library: a full depth-first walk, and a targeted search for nodes of a given type.
Add Dependencies
- Kotlin
- Java
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/sas-parser-with-dependencies-1.6.5-all.jar"))
}
</TabItem>
<TabItem value="java" label="Java">
<CodeBlock language="gradle">
{`repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/sas-parser-with-dependencies-${LATEST_VERSION}-all.jar"))
implementation "com.strumenta.kolasu:kolasu-javalib:1.5.96"
}`}
</CodeBlock>
</TabItem>
</Tabs>
## Walk every node
Use `walk()` (Kotlin) or `Traversing.walk` (Java) to visit the entire tree in depth-first order. Each node exposes its runtime class, its `position` in the source, and its parent link.
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
import com.strumenta.kolasu.traversing.walk
import com.strumenta.sas.ast.SourceFile
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.sas.parser.SASLanguage
import java.io.File
private const val INDENT = " "
fun walkAst(sasFile: File, license: File) {
LicenseManager.registerLicense(license)
val sas = SASLanguage()
sas.parseNativeSQL = true
val result = sas.parse(sasFile)
val root: SourceFile = result.root ?: return
root.walk().forEach { node ->
var depth = 0
var parent = node.parent
while (parent != null) {
depth++
parent = parent.parent
}
print(INDENT.repeat(depth))
val simpleName = node.simpleNodeType
print(simpleName)
node.position?.let {
print(node.position)
}
println()
}
}
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.SourceFile;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.sas.parser.SASLanguage;
import com.strumenta.kolasu.model.Node;
import com.strumenta.kolasu.parsing.ParsingResult;
import java.io.File;
public class WalkAst {
private static final String INDENTATION = " ";
public static void walkAst(File sasFile, File license) {
LicenseManager.INSTANCE.registerLicense(license);
SASLanguage sas = new SASLanguage();
sas.setParseNativeSQL(true);
ParsingResult<SourceFile> result = sas.parse(sasFile);
SourceFile root = result.getRoot();
if (root == null) {
return;
}
Traversing.walk(root).forEach(node -> {
for (Node parent = node.getParent(); parent != null; parent = parent.getParent()) {
System.out.print(INDENTATION);
}
String simpleName = node.getSimpleNodeType();
System.out.print(simpleName);
if (node.getPosition() != null) {
System.out.print(node.getPosition());
}
System.out.println();
});
}
}
Filter nodes by type
When you need a simple and controllable way to inspect the AST and work with it, you can use any of the walk methods.
These methods also provides quick ways to filter and pick the type of walk. You can pick the type of walk between depth-first (the default), breadth first or even from leaves to root.
You can filter the type of nodes you work with either using standard Kotlin or Java methods to filter a stream, or using specific overloading of the walk methods that provides a class filter.
For instance, walkDescendants. In this sample code we filter for DataStep — using walkDescendantsBreadthFirst or walkDescendants with that node class.
- Kotlin
- Java
import com.strumenta.kolasu.traversing.walk
import com.strumenta.kolasu.traversing.walkDescendants
import com.strumenta.sas.ast.Identifier
import com.strumenta.sas.ast.SourceFile
import com.strumenta.sas.ast.VariableExpression
import com.strumenta.sas.ast.datastep.DataStep
import com.strumenta.sas.ast.sql.SqlProcedure
fun listTopLevelConstructs(root: SourceFile) {
println("DATA steps:")
root.walkDescendants(DataStep::class).forEach { step ->
val outputs = step.datasets.map { spec ->
val name = spec.name
val textName = when(name) {
is VariableExpression -> name.variable
is Identifier -> name.name
else -> name.toString()
}
if(spec.library != null)
"${spec.library}.${textName}"
else
textName
}
println(" outputs: ${outputs.joinToString()}")
}
println("PROC SQL blocks:")
root.walk().filterIsInstance<SqlProcedure>().forEach { proc ->
println(" ${proc.sqlStatements.size} SQL statement(s)")
}
}
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.Identifier;
import com.strumenta.sas.ast.SourceFile;
import com.strumenta.sas.ast.VariableExpression;
import com.strumenta.sas.ast.datastep.DataStep;
import com.strumenta.sas.ast.sql.SqlProcedure;
import java.util.stream.Collectors;
public class FilterAst {
public static void listTopLevelConstructs(SourceFile root) {
System.out.println("DATA steps:");
Traversing.walkDescendantsBreadthFirst(root, DataStep.class, step -> {
List<String> outputs = step.getDatasets().stream()
.map(spec -> {
String textName = "";
if (spec.getName() instanceof VariableExpression ve)
textName = ve.getVariable();
else if (spec.getName() instanceof Identifier id)
textName = id.getName();
else if (spec.getName() != null)
textName = spec.getName().toString();
return spec.getLibrary() != null
? spec.getLibrary() + "." + textName
: textName;
})
.collect(Collectors.toList());
System.out.println(" outputs: " + String.join(", ", outputs));
});
System.out.println("PROC SQL blocks:");
Traversing.walk(root).filter(SqlProcedure.class::isInstance)
.map(SqlProcedure.class::cast)
.collect(Collectors.toList())
.forEach(proc ->
System.out.println(" " + proc.getSqlStatements().size() + " SQL statement(s)")
);
}
}
Inspect node properties
Kolasu discovers AST properties automatically. In Kotlin you can iterate node.properties directly; in Java use Processing.processProperties. Both list scalar fields and node-reference properties — useful when exploring an unfamiliar node type or building debug output.
- Kotlin
- Java
fun dumpProperties(node: com.strumenta.kolasu.model.Node) {
node.properties.forEach { property ->
// scalar properties (strings, numbers, booleans, …)
if (!property.providesNodes) {
println(" ${property.name} = ${property.value}")
}
// properties whose value is another AST node or a list of nodes
else {
println(" ${property.name} = <Nodes>")
}
}
}
import com.strumenta.kolasu.model.Node;
import com.strumenta.kolasu.model.Processing;
public class DumpProperties {
public static void dumpProperties(Node node) {
Processing.processProperties(node, property -> {
// this lists simple properties
if (!property.getProvideNodes()) {
System.out.println(" " + property.getName() + " = " + property.getValue());
// this lists properties that contain other nodes
} else {
System.out.println(" " + property.getName() + " = <Nodes>");
}
return null;
});
}
}
When to stop walking
A simple Traversing callback is enough for one-off filters and exploration.
When logic spans many node types, accumulates state, or needs a complex control or the order of the visit, consider a typed visitor instead.