diff --git a/analysis/build.gradle.kts b/analysis/build.gradle.kts index b022e5f5e..1ad2e3b10 100644 --- a/analysis/build.gradle.kts +++ b/analysis/build.gradle.kts @@ -16,6 +16,7 @@ group = "de.maibornwolff" repositories { mavenCentral() + maven { url = uri("https://jitpack.io") } } dependencies { @@ -30,6 +31,7 @@ dependencies { implementation("io.github.bonede:tree-sitter-kotlin:0.3.8.1") implementation("io.github.bonede:tree-sitter-javascript:0.23.1") implementation("io.github.bonede:tree-sitter-vue:0.2.1a") + implementation("com.github.ChristianHuehn.tree-sitter-ng:tree-sitter-abl:de274a6") implementation("com.github.ajalt.clikt:clikt:5.0.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("me.tongfei:progressbar:0.10.2") diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/LanguageAnalyzerFactory.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/LanguageAnalyzerFactory.kt index f635c97ff..4ce73a4b3 100644 --- a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/LanguageAnalyzerFactory.kt +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/LanguageAnalyzerFactory.kt @@ -1,5 +1,6 @@ package de.maibornwolff.dependacharta.pipeline.analysis.analyzers +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.AblAnalyzer import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.cpp.CppAnalyzer import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.csharp.CSharpAnalyzer import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.golang.GoAnalyzer @@ -27,6 +28,7 @@ class LanguageAnalyzerFactory { SupportedLanguage.CPP -> CppAnalyzer(fileInfo) SupportedLanguage.KOTLIN -> KotlinAnalyzer(fileInfo) SupportedLanguage.VUE -> VueAnalyzer(fileInfo) + SupportedLanguage.ABL -> AblAnalyzer(fileInfo) } } } diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/AblAnalyzer.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/AblAnalyzer.kt new file mode 100644 index 000000000..3118baf60 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/AblAnalyzer.kt @@ -0,0 +1,121 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.LanguageAnalyzer +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblClassNameQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblDeclarationsQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblIncludeQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblInheritanceQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblRunQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries.AblUsingQuery +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.withoutFileSuffix +import de.maibornwolff.dependacharta.pipeline.analysis.model.FileInfo +import de.maibornwolff.dependacharta.pipeline.analysis.model.FileReport +import de.maibornwolff.dependacharta.pipeline.analysis.model.Node +import de.maibornwolff.dependacharta.pipeline.analysis.model.NodeType +import de.maibornwolff.dependacharta.pipeline.analysis.model.Type +import de.maibornwolff.dependacharta.pipeline.shared.SupportedLanguage +import org.treesitter.TSNode +import org.treesitter.TSParser +import org.treesitter.TreeSitterAbl + +class AblAnalyzer( + private val fileInfo: FileInfo +) : LanguageAnalyzer { + private val abl = TreeSitterAbl() + private val declarationsQuery = AblDeclarationsQuery(abl) + private val classNameQuery = AblClassNameQuery(abl) + private val usingQuery = AblUsingQuery(abl) + private val runQuery = AblRunQuery(abl) + private val includeQuery = AblIncludeQuery(abl) + private val inheritanceQuery = AblInheritanceQuery(abl) + + override fun analyze(): FileReport { + val rootNode = parseCode(fileInfo.content) + + val classDefinitions = declarationsQuery.execute(rootNode) + + if (classDefinitions.isNotEmpty()) { + return analyzeClassFile(rootNode, classDefinitions) + } + + return analyzeProceduralFile(rootNode) + } + + private fun analyzeClassFile( + rootNode: TSNode, + classDefinitions: List + ): FileReport { + val usingDependencies = usingQuery.execute(rootNode, fileInfo.content) + val includeDependencies = includeQuery.execute(rootNode, fileInfo.content) + val allDependencies = (usingDependencies + includeDependencies).toSet() + + // Convert USING dependencies to usedTypes for resolution (skip wildcards) + val usingUsedTypes = usingDependencies + .filter { !it.isWildcard } + .map { dep -> Type.simple(dep.path.parts.last()) } + .toSet() + + // Convert include dependencies to usedTypes for resolution + val includeUsedTypes = includeDependencies + .map { dep -> Type.simple(dep.path.withDots()) } + .toSet() + + val nodes = classDefinitions.map { classNode -> + val classPath = classNameQuery.execute(classNode, fileInfo.content) + ?: fileInfo.physicalPathAsPath() + val inheritedTypes = inheritanceQuery.execute(classNode, fileInfo.content) + + Node( + pathWithName = classPath, + physicalPath = fileInfo.physicalPath, + language = SupportedLanguage.ABL, + nodeType = NodeType.CLASS, + dependencies = allDependencies, + usedTypes = inheritedTypes + usingUsedTypes + includeUsedTypes + ) + } + + return FileReport(nodes) + } + + private fun analyzeProceduralFile(rootNode: TSNode): FileReport { + val filePath = fileInfo.physicalPathAsPath() + val extension = fileInfo.physicalPath.substringAfterLast(".") + val pathWithoutExtension = filePath.withoutFileSuffix(extension) + + val usingDependencies = usingQuery.execute(rootNode, fileInfo.content) + val runDependencies = runQuery.execute(rootNode, fileInfo.content) + val includeDependencies = includeQuery.execute(rootNode, fileInfo.content) + + val allDependencies = (usingDependencies + runDependencies + includeDependencies).toSet() + + // Convert USING dependencies to usedTypes for resolution (skip wildcards) + val usingUsedTypes = usingDependencies + .filter { !it.isWildcard } + .map { dep -> Type.simple(dep.path.parts.last()) } + .toSet() + + // Convert direct dependencies (RUN and include) to usedTypes for resolution + val directUsedTypes = (runDependencies + includeDependencies) + .map { dep -> Type.simple(dep.path.withDots()) } + .toSet() + + val node = Node( + pathWithName = pathWithoutExtension, + physicalPath = fileInfo.physicalPath, + language = SupportedLanguage.ABL, + nodeType = NodeType.CLASS, + dependencies = allDependencies, + usedTypes = usingUsedTypes + directUsedTypes + ) + + return FileReport(listOf(node)) + } + + private fun parseCode(ablCode: String): TSNode { + val parser = TSParser() + parser.language = abl + val tree = parser.parseString(null, ablCode) + return tree.rootNode + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblClassNameQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblClassNameQuery.kt new file mode 100644 index 000000000..3ad7f6e77 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblClassNameQuery.kt @@ -0,0 +1,26 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.nodeAsString +import de.maibornwolff.dependacharta.pipeline.analysis.model.Path +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblClassNameQuery( + val abl: TreeSitterAbl +) { + fun execute( + classNode: TSNode, + source: String + ): Path? { + // The first qualified_name child of class_definition is the class name + val nameNode = classNode + .getNamedChildren() + .firstOrNull { it.type == "qualified_name" } + ?: return null + + val qualifiedName = nodeAsString(nameNode, source) + val parts = qualifiedName.split(".") + return Path(parts) + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblDeclarationsQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblDeclarationsQuery.kt new file mode 100644 index 000000000..a9492a370 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblDeclarationsQuery.kt @@ -0,0 +1,15 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblDeclarationsQuery( + val abl: TreeSitterAbl +) { + fun execute(node: TSNode): List { + return node + .getNamedChildren() + .filter { it.type == "class_definition" } + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblIncludeQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblIncludeQuery.kt new file mode 100644 index 000000000..0891e791c --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblIncludeQuery.kt @@ -0,0 +1,68 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.nodeAsString +import de.maibornwolff.dependacharta.pipeline.analysis.model.Dependency +import de.maibornwolff.dependacharta.pipeline.analysis.model.Path +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblIncludeQuery( + val abl: TreeSitterAbl +) { + fun execute( + rootNode: TSNode, + source: String + ): List { + val dependencies = mutableListOf() + + rootNode.getNamedChildren().forEach { child -> + when (child.type) { + "include_extra" -> { + // Simple include: {include/path.i} + val includeText = nodeAsString(child, source) + extractIncludePath(includeText)?.let { dependencies.add(it) } + } + "include" -> { + // Include with arguments: {path.i &arg=value} + val fileRef = child + .getNamedChildren() + .firstOrNull { it.type == "include_file_reference" } + val fileName = fileRef + ?.getNamedChildren() + ?.firstOrNull { it.type == "file_name" } + + if (fileName != null) { + val path = nodeAsString(fileName, source) + convertPathToDependency(path)?.let { dependencies.add(it) } + } + } + } + } + + return dependencies + } + + private fun extractIncludePath(includeText: String): Dependency? { + // Remove braces and trailing newlines: {include/path.i}\n -> include/path.i + val path = includeText + .trim() + .removePrefix("{") + .removeSuffix("}") + .trim() + + return convertPathToDependency(path) + } + + private fun convertPathToDependency(path: String): Dependency? { + if (path.isBlank()) return null + + // Convert path: include/error-handling.i -> include.error-handling + val normalizedPath = path + .removeSuffix(".i") + .replace("/", ".") + + val parts = normalizedPath.split(".") + return Dependency(path = Path(parts)) + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblInheritanceQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblInheritanceQuery.kt new file mode 100644 index 000000000..4a5b27810 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblInheritanceQuery.kt @@ -0,0 +1,33 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.nodeAsString +import de.maibornwolff.dependacharta.pipeline.analysis.model.Type +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblInheritanceQuery( + val abl: TreeSitterAbl +) { + fun execute( + classNode: TSNode, + source: String + ): Set { + val children = classNode.getNamedChildren() + + // Find the index of the first qualified_name (the class name) + val classNameIndex = children.indexOfFirst { it.type == "qualified_name" } + if (classNameIndex == -1) return emptySet() + + // Collect all qualified_name and identifier nodes AFTER the class name + // These represent INHERITS and IMPLEMENTS types + return children + .drop(classNameIndex + 1) + .filter { it.type == "qualified_name" || it.type == "identifier" } + .map { node -> + val typeName = nodeAsString(node, source) + val simpleName = typeName.substringAfterLast(".") + Type.simple(simpleName) + }.toSet() + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblRunQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblRunQuery.kt new file mode 100644 index 000000000..fd00602a9 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblRunQuery.kt @@ -0,0 +1,36 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.nodeAsString +import de.maibornwolff.dependacharta.pipeline.analysis.model.Dependency +import de.maibornwolff.dependacharta.pipeline.analysis.model.Path +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblRunQuery( + val abl: TreeSitterAbl +) { + fun execute( + rootNode: TSNode, + source: String + ): List { + return rootNode + .getNamedChildren() + .filter { it.type == "run_statement" } + .mapNotNull { runNode -> + // Find the procedure_name child + val procedureNode = runNode + .getNamedChildren() + .firstOrNull { it.type == "procedure_name" } + ?: return@mapNotNull null + + val procedurePath = nodeAsString(procedureNode, source) + .removeSuffix(".p") + .removeSuffix(".w") + .replace("/", ".") + + val parts = procedurePath.split(".") + Dependency(path = Path(parts)) + } + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblUsingQuery.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblUsingQuery.kt new file mode 100644 index 000000000..c2dde6071 --- /dev/null +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/abl/queries/AblUsingQuery.kt @@ -0,0 +1,36 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.queries + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.getNamedChildren +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.common.utils.nodeAsString +import de.maibornwolff.dependacharta.pipeline.analysis.model.Dependency +import de.maibornwolff.dependacharta.pipeline.analysis.model.Path +import org.treesitter.TSNode +import org.treesitter.TreeSitterAbl + +class AblUsingQuery( + val abl: TreeSitterAbl +) { + fun execute( + rootNode: TSNode, + source: String + ): List { + return rootNode + .getNamedChildren() + .filter { it.type == "using_statement" } + .mapNotNull { usingNode -> + val usingText = nodeAsString(usingNode, source) + .removePrefix("USING") + .removeSuffix(".") + .trim() + + if (usingText.endsWith("*")) { + val pathWithoutWildcard = usingText.dropLast(2).trim() + val parts = pathWithoutWildcard.split(".") + Dependency(path = Path(parts), isWildcard = true) + } else { + val parts = usingText.split(".") + Dependency(path = Path(parts)) + } + } + } +} diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/processing/dependencies/DependencyResolverService.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/processing/dependencies/DependencyResolverService.kt index 347c1f76e..7086d3a96 100644 --- a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/processing/dependencies/DependencyResolverService.kt +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/processing/dependencies/DependencyResolverService.kt @@ -39,6 +39,7 @@ class DependencyResolverService { SupportedLanguage.CPP -> CppStandardLibrary() SupportedLanguage.KOTLIN -> KotlinStandardLibrary() SupportedLanguage.VUE -> EmptyStandardLibrary() + SupportedLanguage.ABL -> EmptyStandardLibrary() } } } diff --git a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/shared/SupportedLanguage.kt b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/shared/SupportedLanguage.kt index 5d421f81d..88b310306 100644 --- a/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/shared/SupportedLanguage.kt +++ b/analysis/src/main/kotlin/de/maibornwolff/dependacharta/pipeline/shared/SupportedLanguage.kt @@ -14,6 +14,7 @@ enum class SupportedLanguage( CPP("C++", listOf("cpp", "c", "cc", "cxx", "h", "hpp", "hxx", "hh")), KOTLIN("Kotlin", listOf("kt", "kts")), VUE("Vue", listOf("vue")), + ABL("ABL", listOf("p", "cls", "i", "w")), } fun languagesByExtension(languages: List) = diff --git a/analysis/src/test/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/AblAnalyzerTest.kt b/analysis/src/test/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/AblAnalyzerTest.kt new file mode 100644 index 000000000..4841b4f99 --- /dev/null +++ b/analysis/src/test/kotlin/de/maibornwolff/dependacharta/pipeline/analysis/analyzers/AblAnalyzerTest.kt @@ -0,0 +1,293 @@ +package de.maibornwolff.dependacharta.pipeline.analysis.analyzers + +import de.maibornwolff.dependacharta.pipeline.analysis.analyzers.abl.AblAnalyzer +import de.maibornwolff.dependacharta.pipeline.analysis.model.FileInfo +import de.maibornwolff.dependacharta.pipeline.analysis.model.NodeType +import de.maibornwolff.dependacharta.pipeline.shared.SupportedLanguage +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class AblAnalyzerTest { + @Test + fun `analyzes ABL class file with class name`() { + // Given + val ablCode = """ + CLASS mypackage.MyClass: + DEFINE VARIABLE myVar AS CHARACTER NO-UNDO. + + METHOD PUBLIC VOID doSomething(): + END METHOD. + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/MyClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + assertThat(result.nodes).hasSize(1) + val node = result.nodes.first() + assertThat(node.pathWithName.toString()).isEqualTo("mypackage.MyClass") + assertThat(node.nodeType).isEqualTo(NodeType.CLASS) + assertThat(node.language).isEqualTo(SupportedLanguage.ABL) + } + + @Test + fun `analyzes ABL procedure file`() { + // Given + val ablCode = """ + DEFINE VARIABLE cName AS CHARACTER NO-UNDO. + + cName = "Hello". + MESSAGE cName VIEW-AS ALERT-BOX. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/procedures/myproc.p", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + assertThat(result.nodes).hasSize(1) + val node = result.nodes.first() + assertThat(node.pathWithName.toString()).isEqualTo("src.procedures.myproc") + assertThat(node.nodeType).isEqualTo(NodeType.CLASS) + assertThat(node.language).isEqualTo(SupportedLanguage.ABL) + } + + @Test + fun `analyzes ABL window file`() { + // Given + val ablCode = """ + DEFINE BUTTON btnOK AUTO-GO LABEL "OK". + + ENABLE btnOK. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/windows/mainwindow.w", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + assertThat(result.nodes).hasSize(1) + val node = result.nodes.first() + assertThat(node.pathWithName.toString()).isEqualTo("src.windows.mainwindow") + assertThat(node.nodeType).isEqualTo(NodeType.CLASS) + } + + @Test + fun `analyzes ABL include file`() { + // Given + val ablCode = """ + DEFINE VARIABLE cSharedVar AS CHARACTER NO-UNDO. + + PROCEDURE SharedProcedure: + MESSAGE "Shared". + END PROCEDURE. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/includes/shared.i", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + assertThat(result.nodes).hasSize(1) + val node = result.nodes.first() + assertThat(node.pathWithName.toString()).isEqualTo("src.includes.shared") + assertThat(node.nodeType).isEqualTo(NodeType.CLASS) + } + + @Test + fun `extracts USING statements as dependencies`() { + // Given + val ablCode = """ + USING mypackage.OtherClass. + USING Progress.Lang.*. + + CLASS mypackage.MyClass: + DEFINE VARIABLE obj AS OtherClass NO-UNDO. + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/MyClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val dependencyPaths = node.dependencies.map { it.path.toString() } + assertThat(dependencyPaths).contains("mypackage.OtherClass") + } + + @Test + fun `adds USING types to usedTypes for resolution`() { + // Given + val ablCode = """ + USING mypackage.OtherClass. + USING core.Customer. + + CLASS mypackage.MyClass: + DEFINE VARIABLE obj AS OtherClass NO-UNDO. + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/MyClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val usedTypeNames = node.usedTypes.map { it.name } + assertThat(usedTypeNames).contains("OtherClass") + assertThat(usedTypeNames).contains("Customer") + } + + @Test + fun `extracts RUN statements as dependencies`() { + // Given + val ablCode = """ + DEFINE VARIABLE cResult AS CHARACTER NO-UNDO. + + RUN mypackage/helper.p. + RUN other/util.p (INPUT "test", OUTPUT cResult). + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/procedures/main.p", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val dependencyPaths = node.dependencies.map { it.path.toString() } + assertThat(dependencyPaths).contains("mypackage.helper") + assertThat(dependencyPaths).contains("other.util") + } + + @Test + fun `extracts inheritance from INHERITS clause`() { + // Given + val ablCode = """ + CLASS mypackage.ChildClass INHERITS mypackage.BaseClass: + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/ChildClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val usedTypeNames = node.usedTypes.map { it.name } + assertThat(usedTypeNames).contains("BaseClass") + } + + @Test + fun `extracts interface implementation from IMPLEMENTS clause`() { + // Given + val ablCode = """ + CLASS mypackage.MyClass IMPLEMENTS mypackage.IMyInterface: + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/MyClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val usedTypeNames = node.usedTypes.map { it.name } + assertThat(usedTypeNames).contains("IMyInterface") + } + + @Test + fun `extracts inheritance from INHERITS clause with simple class name`() { + // Given + val ablCode = """ + CLASS mypackage.ChildClass INHERITS BaseClass: + END CLASS. + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/mypackage/ChildClass.cls", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val usedTypeNames = node.usedTypes.map { it.name } + assertThat(usedTypeNames).contains("BaseClass") + } + + @Test + fun `extracts include directives as dependencies`() { + // Given + val ablCode = """ + DEFINE VARIABLE cName AS CHARACTER NO-UNDO. + {include/error-handling.i} + {shared/constants.i ¶m=value} + """.trimIndent() + + val fileInfo = FileInfo( + language = SupportedLanguage.ABL, + physicalPath = "src/procedures/main.p", + content = ablCode + ) + + // When + val result = AblAnalyzer(fileInfo).analyze() + + // Then + val node = result.nodes.first() + val dependencyPaths = node.dependencies.map { it.path.toString() } + assertThat(dependencyPaths).contains("include.error-handling") + assertThat(dependencyPaths).contains("shared.constants") + } +}