Skip to content
Merged
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
1 change: 1 addition & 0 deletions analysis/analysers/parsers/UnifiedParser/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
implementation(libs.treesitter.kotlin)
implementation(libs.treesitter.java)
implementation(libs.treesitter.csharp)
implementation(libs.treesitter.python)

testImplementation(libs.jsonassert)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ enum class AvailableCollectors(
JAVASCRIPT(FileExtension.JAVASCRIPT, ::JavascriptCollector),
KOTLIN(FileExtension.KOTLIN, ::KotlinCollector),
JAVA(FileExtension.JAVA, ::JavaCollector),
CSHARP(FileExtension.CSHARP, ::CSharpCollector)
CSHARP(FileExtension.CSHARP, ::CSharpCollector),
PYTHON(FileExtension.PYTHON, ::PythonCollector)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ abstract class MetricCollector(
) {
private var lastCountedCommentLine = -1
private var lastCountedCodeLine = -1
private var rootNodeType: String = ""

// maps a metric to its index for the more performant IntArray and the function how to calculate the metric
private val metricInfo = mapOf(
Expand All @@ -34,6 +35,7 @@ abstract class MetricCollector(

fun collectMetricsForFile(file: File): MutableNode {
val rootNode = getRootNode(file)
rootNodeType = rootNode.type

lastCountedCommentLine = -1
lastCountedCodeLine = -1
Expand All @@ -57,7 +59,8 @@ abstract class MetricCollector(
private fun getRootNode(file: File): TSNode {
val parser = TSParser()
parser.setLanguage(treeSitterLanguage)
return parser.parseString(null, file.readText()).rootNode
val rootNode = parser.parseString(null, file.readText()).rootNode
return rootNode
}

private val walkTree = DeepRecursiveFunction<Pair<TSTreeCursor, IntArray>, Unit> { (cursor, metrics) ->
Expand All @@ -67,9 +70,12 @@ abstract class MetricCollector(
val startRow = currentNode.startPoint.row
val endRow = currentNode.endPoint.row

for ((_, indexAndCalculateMetricForNodeFn) in metricInfo) {
val (index, calculateMetricForNodeFn) = indexAndCalculateMetricForNodeFn
metrics[index] += calculateMetricForNodeFn(currentNode, nodeType, startRow, endRow)
val skipRootToAvoidDoubleCountingLines = nodeType != rootNodeType
if (skipRootToAvoidDoubleCountingLines) {
for ((_, indexAndCalculateMetricForNodeFn) in metricInfo) {
val (index, calculateMetricForNodeFn) = indexAndCalculateMetricForNodeFn
metrics[index] += calculateMetricForNodeFn(currentNode, nodeType, startRow, endRow)
}
}

if (cursor.gotoFirstChild()) {
Expand Down Expand Up @@ -113,7 +119,7 @@ abstract class MetricCollector(
rlocForNode++
}

if (node.childCount == 0 && endRow > lastCountedCodeLine) {
if (endRow > lastCountedCodeLine && countWholeNodeLength(node)) {
lastCountedCodeLine = endRow
rlocForNode += endRow - startRow
}
Expand All @@ -132,11 +138,20 @@ abstract class MetricCollector(

private fun isNestedTypeAllowed(node: TSNode, nodeType: String, nestedTypes: Set<NestedNodeType>): Boolean {
for (nestedType in nestedTypes) {
if (nestedType.baseNodeType == nodeType) {
if (nestedType.baseNodeType != nodeType) continue

if (nestedType.childNodePosition != null && nestedType.childNodeCount == node.childCount) {
val childNode = node.getChild(nestedType.childNodePosition)
if (nestedType.childNodeTypes.contains(childNode.type)) return true
} else if (nestedType.childNodeFieldName != null) {
val childNode = node.getChildByFieldName(nestedType.childNodeFieldName)
if (nestedType.childNodeTypes.contains(childNode.type)) return true
}
}
return false
}

protected open fun countWholeNodeLength(node: TSNode): Boolean {
return node.childCount == 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors

import de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes.PythonNodeTypes
import org.treesitter.TSNode
import org.treesitter.TreeSitterPython

class PythonCollector : MetricCollector(
treeSitterLanguage = TreeSitterPython(),
queryProvider = PythonNodeTypes()
) {
override fun calculateRealLinesOfCodeForNode(node: TSNode, nodeType: String, startRow: Int, endRow: Int): Int {
if (shouldIgnoreNodeType(node, nodeType) || doesNodeStartWithComment(node)) return 0
return super.calculateRealLinesOfCodeForNode(node, nodeType, startRow, endRow)
}

private fun shouldIgnoreNodeType(node: TSNode, nodeType: String): Boolean {
return nodeType == "string_start" ||
nodeType == "string_content" ||
nodeType == "string_end" ||
(nodeType == "string" && node.parent.childCount == 1)
}

private fun doesNodeStartWithComment(node: TSNode): Boolean {
val childNode = node.getChild(0)
if (childNode.isNull) return false
return childNode.type == "expression_statement" && childNode.childCount == 1
}

override fun countWholeNodeLength(node: TSNode): Boolean {
return node.childCount == 0 || (node.type == "string" && node.parent.childCount != 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class TreeNodeTypes(

class NestedNodeType(
val baseNodeType: String,
val childNodeFieldName: String?,
val childNodeFieldName: String? = null,
val childNodeCount: Int? = null,
val childNodePosition: Int? = null,
val childNodeTypes: Set<String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes

class PythonNodeTypes : MetricNodeTypes {
override val complexityNodeTypes = TreeNodeTypes(
simpleNodeTypes = setOf(
// if
"if_statement",
"elif_clause",
"if_clause",
// loop
"for_statement",
"while_statement",
"for_in_clause",
// conditional
"conditional_expression",
"list",
// logical binary
"boolean_operator",
// case label
"case_pattern",
// catch block
"except_clause",
// function
"function_definition"
),
nestedNodeTypes = setOf(
// lambda needs to be complex to not be counted double as type of first child is also lambda
NestedNodeType(
baseNodeType = "lambda",
childNodeCount = 4,
childNodePosition = 0,
childNodeTypes = setOf("lambda")
)
)
)

override val commentLineNodeTypes = TreeNodeTypes(
simpleNodeTypes = setOf(
"comment"
),
nestedNodeTypes = setOf(
// in python unassigned strings are used as block comments, meaning an expression that only has string as a child
NestedNodeType(
baseNodeType = "expression_statement",
childNodeCount = 1,
childNodePosition = 0,
childNodeTypes = setOf("string")
)
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ class UnifiedParserTest {
Arguments.of("javascript", ".js"),
Arguments.of("java", ".java"),
Arguments.of("kotlin", ".kt"),
Arguments.of("cSharp", ".cs")
Arguments.of("cSharp", ".cs"),
Arguments.of("python", ".py")
)

@ParameterizedTest
@MethodSource("provideSupportedLanguages")
fun `Should produce correct output for a single source file of each supported language`(language: String, fileExtension: String) {
// given
val pipedProject = ""
val inputFilePath = "${testResourceBaseFolder}${language}Sample$fileExtension"
val expectedResultFile = File("${testResourceBaseFolder}${language}Sample.cc.json")
val inputFilePath = "${testResourceBaseFolder}languageSamples/${language}Sample$fileExtension"
val expectedResultFile = File("${testResourceBaseFolder}languageSamples/${language}Sample.cc.json")

// when
val result = executeForOutput(pipedProject, arrayOf(inputFilePath))
Expand Down Expand Up @@ -135,6 +136,7 @@ class UnifiedParserTest {
"bar/hello.kt",
"bar/foo.kt",
"foo.kt",
"foo.py",
"whenCase.kt",
"helloWorld.ts"
)
Expand Down Expand Up @@ -166,7 +168,9 @@ class UnifiedParserTest {

// then
Assertions.assertThat(errContent.toString()).contains(
"2 Files with the following extensions were ignored as they are currently not supported:\n[.strange, .py]"
"2 Files with the following extensions were ignored as they are currently not supported:",
".json",
".strange"
)

// clean up
Expand Down
Loading