diff --git a/analysis/analysers/parsers/UnifiedParser/build.gradle.kts b/analysis/analysers/parsers/UnifiedParser/build.gradle.kts index a2a9c09768..1a3ef9ee3f 100644 --- a/analysis/analysers/parsers/UnifiedParser/build.gradle.kts +++ b/analysis/analysers/parsers/UnifiedParser/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(libs.treesitter.kotlin) implementation(libs.treesitter.java) implementation(libs.treesitter.csharp) + implementation(libs.treesitter.python) testImplementation(libs.jsonassert) } diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt index 19546e258a..c8d3ea01b9 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt @@ -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) } diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt index e2bcc705ea..ddcf5f9be6 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt @@ -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( @@ -34,6 +35,7 @@ abstract class MetricCollector( fun collectMetricsForFile(file: File): MutableNode { val rootNode = getRootNode(file) + rootNodeType = rootNode.type lastCountedCommentLine = -1 lastCountedCodeLine = -1 @@ -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, Unit> { (cursor, metrics) -> @@ -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()) { @@ -113,7 +119,7 @@ abstract class MetricCollector( rlocForNode++ } - if (node.childCount == 0 && endRow > lastCountedCodeLine) { + if (endRow > lastCountedCodeLine && countWholeNodeLength(node)) { lastCountedCodeLine = endRow rlocForNode += endRow - startRow } @@ -132,11 +138,20 @@ abstract class MetricCollector( private fun isNestedTypeAllowed(node: TSNode, nodeType: String, nestedTypes: Set): 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 + } } diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollector.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollector.kt new file mode 100644 index 0000000000..221e565735 --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollector.kt @@ -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) + } +} diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/MetricNodeTypes.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/MetricNodeTypes.kt index 3a7b797a58..75ac4488e2 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/MetricNodeTypes.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/MetricNodeTypes.kt @@ -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 ) diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/PythonNodeTypes.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/PythonNodeTypes.kt new file mode 100644 index 0000000000..5184d3aa94 --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/PythonNodeTypes.kt @@ -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") + ) + ) + ) +} diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt index 7e23635ecc..d1e99325d2 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt @@ -42,7 +42,8 @@ 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 @@ -50,8 +51,8 @@ class UnifiedParserTest { 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)) @@ -135,6 +136,7 @@ class UnifiedParserTest { "bar/hello.kt", "bar/foo.kt", "foo.kt", + "foo.py", "whenCase.kt", "helloWorld.ts" ) @@ -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 diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollectorTest.kt b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollectorTest.kt new file mode 100644 index 0000000000..16990bf41b --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/PythonCollectorTest.kt @@ -0,0 +1,188 @@ +package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors + +import de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes.AvailableMetrics +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.treesitter.TSParser +import org.treesitter.TreeSitterPython +import java.io.File + +class PythonCollectorTest { + private var parser = TSParser() + private val collector = PythonCollector() + + @BeforeEach + fun setUp() { + parser.setLanguage(TreeSitterPython()) + } + + private fun createTestFile(content: String): File { + val tempFile = File.createTempFile("testFile", ".txt") + tempFile.writeText(content) + tempFile.deleteOnExit() + return tempFile + } + + @Test + fun `should count lambda expressions for complexity`() { + // given + val fileContent = """x = lambda a : a * 5""" + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMPLEXITY.metricName]).isEqualTo(1) + } + + @Test + fun `should count for in loop for complexity`() { + // given + val fileContent = """ + for x in y: + result += x + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMPLEXITY.metricName]).isEqualTo(1) + } + + @Test + fun `should count 'and' and 'or' patterns for complexity`() { + // given + val fileContent = """ + if (x == 2 or y != none and y < 5): + print("") + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMPLEXITY.metricName]).isEqualTo(3) + } + + @Test + fun `should count case pattern for complexity`() { + // given + val fileContent = """ + match lang: + case "JavaScript": + print("You can become a web developer.") + case "Python": + print("You can become a Data Scientist") + case _: + print("The language doesn't matter, what matters is solving problems.") + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMPLEXITY.metricName]).isEqualTo(3) + } + + @Test + fun `should count line and block comments for comment_lines`() { + // given + val fileContent = """ + # line comment + ${"\"\"\""} + unassigned string used as block comment + ${"\"\"\""} + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMMENT_LINES.metricName]).isEqualTo(4) + } + + @Test + fun `should count assigned multiline strings for rloc`() { + // given + val fileContent = """ + x = ${"\"\"\""} + normal assigned string + ${"\"\"\""} + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.REAL_LINES_OF_CODE.metricName]).isEqualTo(3) + } + + @Test + fun `should not count unassigned multiline strings for rloc`() { + // given + val fileContent = """ + ${"\"\"\""} + unassigned string, used as block comment + ${"\"\"\""} + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.COMPLEXITY.metricName]).isEqualTo(0) + } + + @Test + fun `should not include comments or empty lines for rloc`() { + // given + val fileContent = """ + ${"\"\"\""} + unassigned string, used as block comment + ${"\"\"\""} + + if (x == 2 or y != none and y < 5): + # prints x + + print(x) # inline comment + """.trimIndent() + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.REAL_LINES_OF_CODE.metricName]).isEqualTo(2) + } + + @Test + fun `should count empty lines and comments for loc`() { + // given + val fileContent = """ + ${"\"\"\""} + unassigned string, used as block comment + ${"\"\"\""} + + if (x == 2 or y != none and y < 5): + # prints x + + print(x) # inline comment + """.trimIndent() + "\n" // this newline simulates end of file + val input = createTestFile(fileContent) + + // when + val result = collector.collectMetricsForFile(input) + + // then + Assertions.assertThat(result.attributes[AvailableMetrics.LINES_OF_CODE.metricName]).isEqualTo(8) + } +} diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json index e0837414a2..4e1e664636 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json @@ -1,5 +1,5 @@ { - "checksum": "ff9abfcd28324a978ef8b8f7da6a338a", + "checksum": "8116f44f6e2be3a83f516a33d690214a", "data": { "projectName": "", "nodes": [ @@ -73,6 +73,18 @@ "link": "", "children": [] }, + { + "name": "foo.py", + "type": "File", + "attributes": { + "complexity": 2, + "comment_lines": 4, + "loc": 14, + "rloc": 9 + }, + "link": "", + "children": [] + }, { "name": "helloWorld.ts", "type": "File", diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/cSharpSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/cSharpSample.cc.json rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/cSharpSample.cs b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cs similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/cSharpSample.cs rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cs diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/javaSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/javaSample.cc.json rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/javaSample.java b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.java similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/javaSample.java rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.java diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/javascriptSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json similarity index 96% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/javascriptSample.cc.json rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json index 2a25d4957e..3f1e965810 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/javascriptSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "d65e9fd093e285977c0fd3b66de6ff4d", + "checksum": "36429191730fdb0005bd49b160803d46", "data": { "projectName": "", "nodes": [ @@ -16,7 +16,7 @@ "complexity": 33, "comment_lines": 57, "loc": 191, - "rloc": 110 + "rloc": 109 }, "link": "", "children": [] diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/javascriptSample.js b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.js similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/javascriptSample.js rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.js diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinSample.cc.json rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinSample.kt b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.kt similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinSample.kt rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.kt diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json new file mode 100644 index 0000000000..5a3e9e89c7 --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json @@ -0,0 +1,66 @@ +{ + "checksum": "595ba57961df876c22c06eb0b42f864f", + "data": { + "projectName": "", + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "link": "", + "children": [ + { + "name": "pythonSample.py", + "type": "File", + "attributes": { + "complexity": 12, + "comment_lines": 61, + "loc": 110, + "rloc": 38 + }, + "link": "", + "children": [] + } + ] + } + ], + "apiVersion": "1.3", + "edges": [], + "attributeTypes": {}, + "attributeDescriptors": { + "complexity": { + "title": "Complexity", + "description": "Complexity of the file based on the number of paths through the code (McCabe Complexity)", + "hintLowValue": "", + "hintHighValue": "", + "link": "https://en.wikipedia.org/wiki/Cyclomatic_complexity", + "direction": -1 + }, + "comment_lines": { + "title": "Comment lines", + "description": "Number of lines containing either a comment or commented-out code", + "hintLowValue": "", + "hintHighValue": "", + "link": "https://codecharta.com/docs/parser/unified", + "direction": -1 + }, + "loc": { + "title": "Lines of Code", + "description": "Lines of code including empty lines and comments", + "hintLowValue": "", + "hintHighValue": "", + "link": "https://codecharta.com/docs/parser/unified", + "direction": -1 + }, + "rloc": { + "title": "Real Lines of Code", + "description": "Number of lines that contain at least one character which is neither a whitespace nor a tabulation nor part of a comment", + "hintLowValue": "", + "hintHighValue": "", + "link": "https://codecharta.com/docs/parser/unified", + "direction": -1 + } + }, + "blacklist": [] + } +} diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.py b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.py new file mode 100644 index 0000000000..7242cb5b6c --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.py @@ -0,0 +1,110 @@ +""" +User service module for managing user operations +""" +from src.models.entities.user import User +from src.utils.helpers import hash_password + + +class UserService: + """Service for user-related operations""" + + def __init__(self): + """Initialize user service with empty user store""" + self.users = {} # username -> User + + def register_user(self, username, email, password, first_name=None, last_name=None): + """ + Register a new user + + Args: + username (str): Username for the new user + email (str): Email address + password (str): User password + first_name (str, optional): User's first name + last_name (str, optional): User's last name + + Returns: + User: The created user object + + Raises: + ValueError: If username already exists + """ + if username in self.users or username == "admin": + raise ValueError(f"Username '{username}' already exists") + + user = User( + username, + email, + password, + first_name, + last_name + ) + self.users[username] = user + return user + + def authenticate(self, username, password): + """ + Authenticate a user with username and password + + Args: + username (str): Username to authenticate + password (str): Password to verify + + Returns: + User: Authenticated user if successful, None otherwise + """ + if username not in self.users: + return None + + user = self.users[username] + if user.password_hash == hash_password(password): + user.record_login() + return user + + return None + + def get_user(self, username): + """ + Get user by username + + Args: + username (str): Username to look up + + Returns: + User: User object if found, None otherwise + """ + return self.users.get(username) + + def update_user_password(self, username, old_password, new_password): + """ + Update user password + + Args: + username (str): Username of account to update + old_password (str): Current password for verification + new_password (str): New password to set + + Returns: + bool: True if update successful, False otherwise + """ + user = self.authenticate(username, old_password) + if not user: + return False + + return user.update_password(new_password) + + def deactivate_user(self, username): + """ + Deactivate a user account + + Args: + username (str): Username to deactivate + + Returns: + bool: True if deactivated, False if user not found + """ + user = self.get_user(username) + if user: + user.deactivate() + return True + return False diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/typescriptSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json similarity index 96% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/typescriptSample.cc.json rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json index 37c6b1cb6d..7cb9fb73a3 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/typescriptSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "cba50e4765cefed97feacc767e7f1d6b", + "checksum": "f4a7ca2b3cfe15e5bd0e6bcc5fed4752", "data": { "projectName": "", "nodes": [ @@ -16,7 +16,7 @@ "complexity": 32, "comment_lines": 61, "loc": 200, - "rloc": 115 + "rloc": 114 }, "link": "", "children": [] diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/typescriptSample.ts b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.ts similarity index 100% rename from analysis/analysers/parsers/UnifiedParser/src/test/resources/typescriptSample.ts rename to analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.ts diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json index 268c0394dc..e51471909e 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json @@ -1,5 +1,5 @@ { - "checksum": "85ec89e6fa8d6d561d51df81ae987e30", + "checksum": "57c8e122387ce42a8ab80a3caed435aa", "data": { "projectName": "", "nodes": [ @@ -71,6 +71,18 @@ "link": "", "children": [] }, + { + "name": "foo.py", + "type": "File", + "attributes": { + "complexity": 2, + "comment_lines": 4, + "loc": 14, + "rloc": 9 + }, + "link": "", + "children": [] + }, { "name": "helloWorld.ts", "type": "File", diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json index cef849503e..92634ee37a 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json @@ -1,5 +1,5 @@ { - "checksum": "48c91eb854bb71c773cc9a48d9e918ff", + "checksum": "49e750bf6aaabb3c9b1dbbc5f236c082", "data": { "projectName": "", "nodes": [ @@ -53,6 +53,18 @@ "link": "", "children": [] }, + { + "name": "foo.py", + "type": "File", + "attributes": { + "complexity": 2, + "comment_lines": 4, + "loc": 14, + "rloc": 9 + }, + "link": "", + "children": [] + }, { "name": "helloWorld.ts", "type": "File", diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleproject/empty.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleproject/empty.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/analysis/gradle/libs.versions.toml b/analysis/gradle/libs.versions.toml index 23c1520db2..90648d9f89 100644 --- a/analysis/gradle/libs.versions.toml +++ b/analysis/gradle/libs.versions.toml @@ -33,6 +33,7 @@ tree-sitter-javascript = "0.23.1" tree-sitter-kotlin = "0.3.8.1" tree-sitter-java = "0.23.4" tree-sitter-csharp = "0.23.1" +tree-sitter-python = "0.23.4" [libraries] kotlin-logging-jvm = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "kotlin-logging" } @@ -77,6 +78,7 @@ treesitter-kotlin = { group = "io.github.bonede", name = "tree-sitter-kotlin", v treesitter-java = { group = "io.github.bonede", name = "tree-sitter-java", version.ref = "tree-sitter-java"} treesitter-csharp = { group = "io.github.bonede", name = "tree-sitter-c-sharp", version.ref = "tree-sitter-csharp"} +treesitter-python = { group = "io.github.bonede", name = "tree-sitter-python", version.ref = "tree-sitter-python"} [plugins] sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } diff --git a/gh-pages/_docs/01-overview/01-introduction.md b/gh-pages/_docs/01-overview/01-introduction.md index 5d209652fa..12a1a50a13 100644 --- a/gh-pages/_docs/01-overview/01-introduction.md +++ b/gh-pages/_docs/01-overview/01-introduction.md @@ -14,7 +14,7 @@ CodeCharta is a **communication tool** that helps you understand and manage comp It turns metrics and your code base into a **city-like map** and makes your code **tangible**! A map where you can move around and find hotspots where your team has always had problems. So you can finally tackle the problems that have always been on your mind. But never talked about. -# Why did we develope CodeCharta? +# Why did we develop CodeCharta? CodeCharta was mainly developed by MaibornWolff to help us with our [Software Health Checks](https://www.maibornwolff.de/en/service/software-health-check/). We needed something to help us find potential issues to discuss with customers or to show the management where their teams are struggling.