Program Inventory
Before refactoring or migrating SAS, teams need a high-level map of each program: how many DATA steps and procedures it contains, where they appear in the source, and what kinds of constructs dominate. The parser turns source files into a structured inventory you can print, export to CSV, or feed into a dashboard.
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 top-level steps
SourceFile exposes statementsAndDeclarations — the top-level elements of the program. Classify each node with simpleNodeType (or getSimpleNodeType() in Java) and record its source position.
- Kotlin
- Java
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.macro.MacroDefinition
import com.strumenta.sas.ast.other.DatasetSpec
import com.strumenta.sas.ast.sql.SqlProcedure
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.sas.parser.SASLanguage
import java.io.File
data class StepEntry(val kind: String, val detail: String, val line: Int)
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
}
fun inventory(root: SourceFile): List<StepEntry> {
return root.statementsAndDeclarations.map { node ->
val line = node.position?.start?.line ?: 0
when (node) {
is DataStep -> {
val outs = node.datasets.joinToString(", ") { formatDatasetSpec(it) }
StepEntry("DATA", outs.ifEmpty { "?" }, line)
}
is SqlProcedure -> StepEntry("PROC SQL", "${node.sqlStatements.size} statement(s)", line)
is MacroDefinition -> StepEntry("MACRO", node.name, line)
else -> StepEntry(node.simpleNodeType, "", line)
}
}
}
fun printMarkdownInventory(file: File, entries: List<StepEntry>) {
println("# Inventory: ${file.name}")
println()
println("| Kind | Detail | Line |")
println("|------|--------|------|")
entries.forEach { e ->
println("| ${e.kind} | ${e.detail} | ${e.line} |")
}
}
fun main() {
val sasFile = File("examples/SAS/all-the-code.sas")
LicenseManager.registerLicense(File("licenses/strumenta.SAS.license"))
val sas = SASLanguage()
sas.parseNativeSQL = true
val root = sas.parse(sasFile).root ?: return
printMarkdownInventory(sasFile, inventory(root))
}
import com.strumenta.kolasu.model.Node;
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.macro.MacroDefinition;
import com.strumenta.sas.ast.other.DatasetSpec;
import com.strumenta.sas.ast.sql.SqlProcedure;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ProgramInventory {
public static class StepEntry {
public final String kind;
public final String detail;
public final int line;
public StepEntry(String kind, String detail, int line) {
this.kind = kind;
this.detail = detail;
this.line = line;
}
}
public static String formatDatasetSpec(DatasetSpec 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;
}
public static List<StepEntry> inventory(SourceFile root) {
List<StepEntry> entries = new ArrayList<>();
for (Node node : root.getStatementsAndDeclarations()) {
int line = node.getPosition() != null
? node.getPosition().getStart().getLine()
: 0;
if (node instanceof DataStep dataStep) {
String outs = dataStep.getDatasets().stream()
.map(ProgramInventory::formatDatasetSpec)
.collect(Collectors.joining(", "));
entries.add(new StepEntry("DATA", outs.isEmpty() ? "?" : outs, line));
} else if (node instanceof SqlProcedure sql) {
entries.add(new StepEntry("PROC SQL",
sql.getSqlStatements().size() + " statement(s)", line));
} else if (node instanceof MacroDefinition macro) {
entries.add(new StepEntry("MACRO", macro.getName(), line));
} else {
entries.add(new StepEntry(node.getSimpleNodeType(), "", line));
}
}
return entries;
}
public static void printMarkdownInventory(File file, List<StepEntry> entries) {
System.out.println("# Inventory: " + file.getName());
System.out.println();
System.out.println("| Kind | Detail | Line |");
System.out.println("|------|--------|------|");
for (StepEntry e : entries) {
System.out.printf("| %s | %s | %d |%n", e.kind, e.detail, e.line);
}
}
}
Batch a directory
Wrap the same logic in a directory walk (mirroring the jsonast CLI layout) to produce one inventory per .sas file or a consolidated CSV for the whole codebase.
Combine with deeper analysis
An inventory answers what is in a file. Pair it with SQL lineage and DATA step I/O to answer how data flows through those steps.