Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions analysis/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ group = "de.maibornwolff"

repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}

dependencies {
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -27,6 +28,7 @@ class LanguageAnalyzerFactory {
SupportedLanguage.CPP -> CppAnalyzer(fileInfo)
SupportedLanguage.KOTLIN -> KotlinAnalyzer(fileInfo)
SupportedLanguage.VUE -> VueAnalyzer(fileInfo)
SupportedLanguage.ABL -> AblAnalyzer(fileInfo)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TSNode>
): 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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<TSNode> {
return node
.getNamedChildren()
.filter { it.type == "class_definition" }
}
}
Original file line number Diff line number Diff line change
@@ -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<Dependency> {
val dependencies = mutableListOf<Dependency>()

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))
}
}
Original file line number Diff line number Diff line change
@@ -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<Type> {
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()
}
}
Original file line number Diff line number Diff line change
@@ -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<Dependency> {
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))
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Dependency> {
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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class DependencyResolverService {
SupportedLanguage.CPP -> CppStandardLibrary()
SupportedLanguage.KOTLIN -> KotlinStandardLibrary()
SupportedLanguage.VUE -> EmptyStandardLibrary()
SupportedLanguage.ABL -> EmptyStandardLibrary()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedLanguage>) =
Expand Down
Loading
Loading