Skip to main content
Version: Next

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

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

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

Parse a directory

Walk all .sas files, parse each one, and index macro definitions by name.

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
}

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.

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

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.