Skip to main content
Version: Next

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

repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}

dependencies {
implementation(files("deps/sas-parser-with-dependencies-1.6.5-all.jar"))
}

Catalog macro definitions

MacroDefinition nodes appear at the top level or inside other constructs. Collect name, parameter list, and body size.

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

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.

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)
}

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.