Transform the AST
Parsers produce ASTs so you can analyze and rewrite code. Starlasu (Kolasu) supports transforming trees: visit each node, replace it or transform it, and create a new root.
Use transforms for refactoring helpers, normalization passes, and the first stage of a transpiler. Transformers are the foundational feature we at Strumenta use to build our transpilers.
When to use transforms
| Tool | Typical use |
|---|---|
| Read-only walk / visitor | Inventory, lineage, metrics |
| Transform | Rename identifiers, strip constructs, transpile to target language |
Add Dependencies
- Kotlin
- Java
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/sas-parser-with-dependencies-1.6.5-all.jar"))
}
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/sas-parser-with-dependencies-1.6.5-all.jar"))
implementation "com.strumenta.kolasu:kolasu-javalib:1.5.96"
}
Collect DatasetSpec
This example collect every DatasetSpec name. The ASTTransformer.transform give you complete control on every transformation.
The consequence of this approach is that you need to define a transformation not just for every node you need to transform, but for every node that could contain that node. Otherwise the transformer will apply the default transformation you selected on the creation of the ASTTransformer.
- Kotlin
- Java
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.kolasu.transformation.ASTTransformer
import com.strumenta.kolasu.validation.Issue
import com.strumenta.sas.ast.SourceFile
import com.strumenta.sas.parser.SASLanguage
import java.io.File
import com.strumenta.kolasu.transformation.IDENTTITY_TRANSFORMATION
import com.strumenta.sas.ast.Identifier
import com.strumenta.sas.ast.VariableExpression
import com.strumenta.sas.ast.datastep.DataStep
import com.strumenta.sas.ast.other.DatasetSpec
import com.strumenta.kolasu.model.Node
fun formatDatasetSpec(spec: DatasetSpec): String {
val name = spec.name
val textName = when (name) {
is VariableExpression -> name.variable
is Identifier -> name.name
else -> name?.toString() ?: "?"
}
return if (spec.library != null) "${spec.library}.$textName" else textName
}
data class ActualName(val name: String) : Node()
data class ActualNames(val names: List<ActualName>) : Node()
data class DataSteps(val names: List<String>) : Node()
fun collectDatasetSpecNames(sasFile: File, license: File) {
LicenseManager.registerLicense(license)
val sas = SASLanguage()
val result = sas.parse(sasFile)
val root = result.root
val issues: MutableList<Issue> = result.issues.toMutableList()
val documenter: ASTTransformer = ASTTransformer(
issues = issues,
allowGenericNode = true,
defaultTransformation = IDENTTITY_TRANSFORMATION
)
documenter.registerNodeFactory(SourceFile::class)
{ sf : SourceFile, transformer ->
val dataSteps = sf.statementsAndDeclarations.filterIsInstance<DataStep>()
DataSteps(
dataSteps.flatMap { (transformer.transform(it) as ActualNames).names.map { it.name } }
)
}
documenter.registerNodeFactory(DataStep::class)
{ dt : DataStep, transformer ->
ActualNames(
dt.datasets.map { transformer.transform(it) as ActualName}
)
}
documenter.registerNodeFactory(DatasetSpec::class)
{ dt : DatasetSpec, transformer ->
ActualName(formatDatasetSpec(dt))
}
val newAST = documenter.transform(root)
println(newAST)
}
package com.strumenta.example;
import com.strumenta.kolasu.javalib.ASTTransformer;
import com.strumenta.kolasu.model.Node;
import com.strumenta.kolasu.transformation.IdentityTransformationKt;
import com.strumenta.kolasu.validation.Issue;
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.other.DatasetSpec;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.sas.parser.SASLanguage;
import kotlin.jvm.JvmClassMappingKt;
public class Transform {
public static String formatDatasetSpec(DatasetSpec spec) {
Object name = spec.getName();
String textName;
if (name instanceof VariableExpression varExpr) {
textName = varExpr.getVariable();
} else if (name instanceof Identifier identifier) {
textName = identifier.getName();
} else {
textName = name != null ? name.toString() : "?";
}
return spec.getLibrary() != null ? spec.getLibrary() + "." + textName : textName;
}
// --- Node Classes ---
public static class ActualName extends Node {
private final String name;
public ActualName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static class ActualNames extends Node {
private final List<ActualName> names;
public ActualNames(List<ActualName> names) {
this.names = names;
}
public List<ActualName> getNames() {
return names;
}
}
public static class DataSteps extends Node {
private final List<String> names;
public DataSteps(List<String> names) {
this.names = names;
}
public List<String> getNames() {
return names;
}
}
// --- Main Logic ---
public static void collectDatasetSpecNames(File sasFile, File license) {
LicenseManager.INSTANCE.registerLicense(license);
SASLanguage sas = new SASLanguage();
var result = sas.parse(sasFile);
var root = result.getRoot();
// Convert issues to a mutable list
List<Issue> issues = new ArrayList<>(result.getIssues());
ASTTransformer documenter = new ASTTransformer(issues,
true,
false,
true,
IdentityTransformationKt.getIDENTTITY_TRANSFORMATION()
);
documenter.registerNodeFactory(JvmClassMappingKt.getKotlinClass(SourceFile.class), (sf, transformer) -> {
// Filter instances of DataStep
List<DataStep> dataSteps = sf.getStatementsAndDeclarations().stream()
.filter(DataStep.class::isInstance)
.map(DataStep.class::cast)
.toList();
// Transform, flatMap the names, and collect into a list of Strings
List<String> stringNames = dataSteps.stream()
.flatMap(dt -> ((ActualNames) transformer.transform(dt)).getNames().stream())
.map(ActualName::getName)
.toList();
return new DataSteps(stringNames);
});
documenter.registerNodeFactory(JvmClassMappingKt.getKotlinClass(DataStep.class), (dt, transformer) -> {
List<ActualName> mappedNames = dt.getDatasets().stream()
.map(ds -> (ActualName) transformer.transform(ds))
.toList();
return new ActualNames(mappedNames);
});
documenter.registerNodeFactory(JvmClassMappingKt.getKotlinClass(DatasetSpec.class), (dt, transformer) -> {
return new ActualName(formatDatasetSpec(dt));
});
DataSteps newAST = (DataSteps) documenter.transform(root);
System.out.println(newAST);
}
}
Typically you define a transformation for every node, unless you have a well-defined and specific need, such as transforming all Data Step in SQL statements.