Macro Analysis
SAS macros make static analysis harder: they can generate code at read time. The parser recognizes macro syntax and builds AST nodes for definitions and statements without fully expanding macros. That is enough for many tasks — cataloging %macro blocks, listing %let assignments, and finding %include dependencies.
Macro bodies may be parsed lazily when you access them. See the challenges of parsing SAS macros for background on what the parser can and cannot guarantee.
Sample SAS
%let lib=work;
%macro summarize(table);
proc means data=&table;
var amount;
run;
%mend;
%include "&lib./setup.sas";
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"
}
Catalog macro definitions
MacroDefinition nodes appear at the top level or inside other constructs. Collect name, parameter list, and body size.
- Kotlin
- Java
import com.strumenta.kolasu.traversing.walkDescendants
import com.strumenta.sas.ast.SourceFile
import com.strumenta.sas.ast.macro.IncludeMacroStatement
import com.strumenta.sas.ast.macro.MacroDefinition
import com.strumenta.sas.ast.macro.VariableDeclaration
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.sas.parser.SASLanguage
import java.io.File
data class MacroSummary(
val name: String,
val parameters: List<String>,
val bodyElements: Int,
val line: Int,
)
fun summarizeMacros(root: SourceFile): List<MacroSummary> =
root.walkDescendants(MacroDefinition::class).map { macro ->
MacroSummary(
name = macro.name,
parameters = macro.arguments.mapNotNull { it.name },
bodyElements = macro.body.size,
line = macro.position?.start?.line ?: 0,
)
}.toList()
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.SourceFile;
import com.strumenta.sas.ast.macro.MacroDefinition;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class MacroAnalysis {
public static class MacroSummary {
public final String name;
public final List<String> parameters;
public final int bodyElements;
public final int line;
public MacroSummary(String name, List<String> parameters, int bodyElements, int line) {
this.name = name;
this.parameters = parameters;
this.bodyElements = bodyElements;
this.line = line;
}
}
public static List<MacroSummary> summarizeMacros(SourceFile root) {
List<MacroSummary> summaries = new ArrayList<>();
Traversing.walkDescendantsBreadthFirst(root, MacroDefinition.class, macro -> {
List<String> params = macro.getArguments().stream()
.map(a -> a.getName())
.filter(java.util.Objects::nonNull)
.collect(Collectors.toList());
summaries.add(new MacroSummary(
macro.getName(),
params,
macro.getBody().size(),
macro.getPosition() != null ? macro.getPosition().getStart().getLine() : 0
));
});
return summaries;
}
}
List macro variables and includes
VariableDeclaration represents %let. IncludeMacroStatement captures %include targets — often a string literal, sometimes a macro variable reference that the parser represents as an expression node.
- Kotlin
- Java
import com.strumenta.kolasu.traversing.walkDescendants
import com.strumenta.sas.ast.SourceFile
import com.strumenta.sas.ast.macro.IncludeMacroStatement
import com.strumenta.sas.ast.macro.VariableDeclaration
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.sas.parser.SASLanguage
import java.io.File
fun listMacroVariables(root: SourceFile) {
root.walkDescendants(VariableDeclaration::class).forEach { decl ->
val rhs = if(decl.expression is ConstantTextExpression)
(decl.expression as ConstantTextExpression).text
else
decl.expression?.simpleNodeType ?: "?"
println("%let ${decl.name} = $rhs")
}
}
fun getTextForInclude(expression: Expression) : String {
return if(expression is SASStringConstant)
expression.text
else if(expression is SASStringComposed)
expression.stringContents.map { getTextForInclude(it) }.joinToString("")
else if(expression is SASStringMacroVariable)
expression.macro.variable
else
expression.toString()
}
fun listIncludes(root: SourceFile) {
root.walkDescendants(IncludeMacroStatement::class).forEach { inc ->
val text = getTextForInclude(expression = inc.resource!!)
println("%include ${text}")
}
}
fun main() {
LicenseManager.registerLicense(File("licenses/strumenta.SAS.license"))
val root = SASLanguage().parse(File("examples/SAS/all-the-code.sas")).root ?: return
summarizeMacros(root).forEach { m ->
println("Macro ${m.name}(${m.parameters.joinToString()}) — ${m.bodyElements} body element(s) @ line ${m.line}")
}
listMacroVariables(root)
listIncludes(root)
}
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.ConstantTextExpression;
import com.strumenta.sas.ast.Expression;
import com.strumenta.sas.ast.SASStringComposed;
import com.strumenta.sas.ast.SASStringConstant;
import com.strumenta.sas.ast.SASStringMacroVariable;
import com.strumenta.sas.ast.SourceFile;
import com.strumenta.sas.ast.macro.IncludeMacroStatement;
import com.strumenta.sas.ast.macro.VariableDeclaration;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.sas.parser.SASLanguage;
import java.io.File;
import java.util.stream.Collectors;
public class MacroVariables {
public static String getTextForInclude(Expression expression) {
if (expression instanceof SASStringConstant ssc) {
return ssc.getText();
}
if (expression instanceof SASStringComposed composed) {
return composed.getStringContents().stream()
.map(MacroVariables::getTextForInclude)
.collect(Collectors.joining());
}
if (expression instanceof SASStringMacroVariable smv) {
return smv.getMacro().getVariable();
}
return expression.toString();
}
public static void listMacroVariables(SourceFile root) {
Traversing.walkDescendantsBreadthFirst(root, VariableDeclaration.class, decl -> {
String rhs;
if (decl.getExpression() instanceof ConstantTextExpression cte) {
rhs = cte.getText();
} else {
rhs = decl.getExpression() != null
? decl.getExpression().getSimpleNodeType()
: "?";
}
System.out.println("%let " + decl.getName() + " = " + rhs);
});
}
public static void listIncludes(SourceFile root) {
Traversing.walkDescendantsBreadthFirst(root, IncludeMacroStatement.class, inc -> {
String text = getTextForInclude(inc.getResource());
System.out.println("%include " + text);
});
}
}
Set expectations
Macro analysis tells you what the source declares, not necessarily what runs after expansion. Use it for scoping, documentation, and include graphs — then validate critical paths on representative runtime inputs.
Next: Cross-file dependencies connects %include paths and macro names across a directory.