Cross-File Dependencies
Single-file analysis is rarely enough. Production SAS lives in libraries of programs linked by %include and shared %macro definitions. By parsing a directory and correlating include paths with macro catalogs, you can build a best-effort dependency sketch for migration planning and impact analysis.
When to use this pattern
This approach does not replace a full symbol resolver. It works well when:
- Includes resolve to literal or predictable paths within a known corpus.
- Macro names are defined in a stable set of library files.
- You need a graph for human review, not guaranteed call-site resolution.
For precise call graphs, combine parsing with project-specific naming conventions and optional runtime logging.
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"
}
Parse a directory
Walk all .sas files, parse each one, and index macro definitions by name.
- 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.kolasu.commercial.LicenseManager
import com.strumenta.sas.parser.SASLanguage
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
data class ParsedFile(val path: Path, val root: SourceFile)
fun parseDirectory(dir: File, license: File): List<ParsedFile> {
LicenseManager.registerLicense(license)
val sas = SASLanguage()
sas.parseNativeSQL = true
return Files.walk(dir.toPath())
.filter { it.toString().endsWith(".sas", ignoreCase = true) }
.map { path ->
val root = sas.parse(path.toFile()).root
?: error("No AST for $path")
ParsedFile(path, root)
}
.toList()
}
fun indexMacros(files: List<ParsedFile>): Map<String, Path> {
val index = mutableMapOf<String, Path>()
files.forEach { pf ->
pf.root.walkDescendants(MacroDefinition::class).forEach { macro ->
index.putIfAbsent(macro.name.lowercase(), pf.path)
}
}
return index
}
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.SourceFile;
import com.strumenta.sas.ast.macro.MacroDefinition;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.sas.parser.SASLanguage;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class CrossFileDeps {
public static class ParsedFile {
public final Path path;
public final SourceFile root;
public ParsedFile(Path path, SourceFile root) {
this.path = path;
this.root = root;
}
}
public static List<ParsedFile> parseDirectory(File dir, File license) throws Exception {
LicenseManager.INSTANCE.registerLicense(license);
SASLanguage sas = new SASLanguage();
sas.setParseNativeSQL(true);
List<ParsedFile> result = new ArrayList<>();
try (Stream<Path> paths = Files.walk(dir.toPath())) {
paths.filter(p -> p.toString().toLowerCase().endsWith(".sas"))
.forEach(path -> {
SourceFile root = sas.parse(path.toFile()).getRoot();
if (root == null) {
throw new IllegalStateException("No AST for " + path);
}
result.add(new ParsedFile(path, root));
});
}
return result;
}
public static Map<String, Path> indexMacros(List<ParsedFile> files) {
Map<String, Path> index = new HashMap<>();
for (ParsedFile pf : files) {
Traversing.walk(pf.root).filter(MacroDefinition.class::isInstance)
.map(MacroDefinition.class::cast)
.forEach(macro -> index.putIfAbsent(macro.getName().toLowerCase(), pf.path));
}
return index;
}
}
Link includes to files
Extract %include references from each file and resolve them against files present in the same directory tree. This example handles literal paths only — macro-variable includes such as "&lib./setup.sas" are reported as text but not resolved.
- Kotlin
- Java
import com.strumenta.kolasu.traversing.walkDescendants
import com.strumenta.sas.ast.macro.IncludeMacroStatement
import java.nio.file.Path
fun findIncludes(pf: ParsedFile): List<String> {
val includes = mutableListOf<String>()
pf.root.walkDescendants(IncludeMacroStatement::class).forEach { inc ->
if(inc.resource is SASStringConstant)
includes.add((inc.resource as SASStringConstant).text)
else
includes.add(inc.resource.toString())
}
return includes
}
fun resolveInclude(includeText: String, baseDir: Path, knownFiles: Set<Path>): Path? {
val normalized = includeText.trim().removeSurrounding("\"", "\"")
val candidate = baseDir.resolve(normalized)
return knownFiles.firstOrNull { it.endsWith(normalized) || it == candidate }
}
fun buildIncludeGraph(files: List<ParsedFile>): Map<Path, List<Path>> {
val paths = files.map { it.path }.toSet()
val graph = mutableMapOf<Path, MutableList<Path>>()
files.forEach { pf ->
val edges = mutableListOf<Path>()
findIncludes(pf).forEach { inc ->
resolveInclude(inc, pf.path.parent, paths)?.let { edges.add(it) }
}
graph[pf.path] = edges
}
return graph
}
fun main() {
val dir = File("examples/SAS")
val files = parseDirectory(dir, File("licenses/strumenta.SAS.license"))
val macros = indexMacros(files)
val graph = buildIncludeGraph(files)
graph.forEach { (from, targets) ->
println("$from includes:")
targets.forEach { println(" -> $it") }
}
println("Macro index: ${macros.size} definition(s)")
}
import com.strumenta.kolasu.javalib.Traversing;
import com.strumenta.sas.ast.SASStringConstant;
import com.strumenta.sas.ast.macro.IncludeMacroStatement;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class IncludeGraph {
public static List<String> findIncludes(CrossFileDeps.ParsedFile pf) {
List<String> includes = new ArrayList<>();
Traversing.walkDescendantsBreadthFirst(pf.root, IncludeMacroStatement.class, inc -> {
if (inc.getResource() instanceof SASStringConstant ssc) {
includes.add(ssc.getText());
} else {
includes.add(String.valueOf(inc.getResource()));
}
});
return includes;
}
public static Path resolveInclude(String includeText, Path baseDir, Set<Path> knownFiles) {
String normalized = includeText.trim().replaceAll("^\"|\"$", "");
Path candidate = baseDir.resolve(normalized);
for (Path p : knownFiles) {
if (p.endsWith(normalized) || p.equals(candidate)) {
return p;
}
}
return null;
}
public static Map<Path, List<Path>> buildIncludeGraph(List<CrossFileDeps.ParsedFile> files) {
Set<Path> paths = new HashSet<>();
files.forEach(f -> paths.add(f.path));
Map<Path, List<Path>> graph = new HashMap<>();
for (CrossFileDeps.ParsedFile pf : files) {
List<Path> edges = new ArrayList<>();
for (String inc : findIncludes(pf)) {
Path target = resolveInclude(inc, pf.path.getParent(), paths);
if (target != null) {
edges.add(target);
}
}
graph.put(pf.path, edges);
}
return graph;
}
}
This will help you get the basic idea. You could extend this sketch by matching macro call sites to the macro index, flagging unresolved includes, and exporting the graph to GraphML or Neo4j for visualization.