diff --git a/build.gradle.kts b/build.gradle.kts index d0259d5..ec360c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,11 +34,13 @@ dependencies { testImplementation("junit:junit:4.13.2") intellijPlatform { - intellijIdeaCommunity("2025.1.4.1") + intellijIdeaCommunity("2025.2") bundledPlugin("com.intellij.java") bundledPlugin("org.jetbrains.kotlin") plugin("PythonCore", "252.23892.409") + plugin("org.jetbrains.plugins.go", "252.23892.360") + testPlugin("org.jetbrains.plugins.go", "252.23892.360") instrumentationTools() pluginVerifier() diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt index 1a73212..7d4c887 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt @@ -11,7 +11,7 @@ import com.intellij.psi.PsiElement val PLUGIN_EP_NAME: ExtensionPointName = ExtensionPointName("com.github.nikolaikopernik.codecomplexity.languageInfoProvider") val PLUGIN_HINT_KEY = SettingsKey("code.complexity.hint") -val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python") +val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python", "go") /** * Main interface to calculate complexity for different languages. diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoComplexityInfoProvider.kt new file mode 100644 index 0000000..b1ea9a1 --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoComplexityInfoProvider.kt @@ -0,0 +1,95 @@ +package com.github.nikolaikopernik.codecomplexity.go + +import com.github.nikolaikopernik.codecomplexity.core.ComplexityInfoProvider +import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink +import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor +import com.github.nikolaikopernik.codecomplexity.go.GoLanguageVisitor.Companion.isMemberFunction +import com.github.nikolaikopernik.codecomplexity.go.GoLanguageVisitor.Companion.isToplevelVarDeclFunction +import com.goide.GoLanguage +import com.goide.psi.GoElement +import com.goide.psi.GoFieldName +import com.goide.psi.GoFunctionLit +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.psi.GoKey +import com.goide.psi.GoValue +import com.goide.psi.GoVarDefinition +import com.goide.psi.GoVarSpec +import com.intellij.lang.Language +import com.intellij.psi.PsiElement + +class GoComplexityInfoProvider(override val language: Language = GoLanguage.INSTANCE) : ComplexityInfoProvider { + + override fun getVisitor(sink: ComplexitySink): ElementVisitor = GoLanguageVisitor(sink) + + override fun isComplexitySuitableMember(element: PsiElement): Boolean { + return element is GoFunctionOrMethodDeclaration || element is GoFunctionLit + } + + override fun isClassWithBody(element: PsiElement): Boolean { + return false + } + + override fun getNameElementFor(element: PsiElement): PsiElement { + return when (element) { + is GoFunctionOrMethodDeclaration -> element.nameIdentifier ?: element + is GoFunctionLit -> when { + element.isMemberFunction() -> element.memberFieldName ?: element + element.isToplevelVarDeclFunction() -> element.varDefinition ?: element + else -> element + } + else -> element + } + } +} + +/** + * Retrieves the field name of a member function's associated field. + * + * This property is used to extract a field name corresponding to a member function within Go code. + * It navigates the PSI (Program Structure Interface) tree, analyzing the parent hierarchy of the + * `GoFunctionLit` instance and extracts the field name from the related `GoElement`. + * + * [GoFunctionLit] --> [GoValue] --> [GoElement] --> [GoKey] --> [GoFieldName] + * + * If the field name cannot be resolved (e.g., the structure doesn't match the expected hierarchy), + * the property returns `null`. + * + * @return The trimmed field name of the associated field, or `null` if unavailable. + */ +val GoFunctionLit.memberFieldName: GoFieldName? + get() { + val goElement = this.parent?.parent as? GoElement ?: return null + return goElement.key?.fieldName + } + +val GoFunctionLit.memberFunctionFieldName: String? + get() { + val fieldName = this.memberFieldName ?: return null + return fieldName.text + } + + +/** + * Retrieves the variable name associated with a Go function literal + * if it is used in the context of a variable declaration. + * + * This property queries the parent element of the function literal to determine if it is + * part of a `GoVarSpec`. If the parent is a variable specification, it extracts the first + * variable name from the list of variable definitions. + * + * [GoFunctionLit] --> [GoVarSpec] + * + * @return The name of the variable associated with the function literal, or `null` if + * no variable declaration is found or the name cannot be determined. + */ +val GoFunctionLit.varDefinition: GoVarDefinition? + get() { + val goVarSpec = this.parent as? GoVarSpec ?: return null + return goVarSpec.varDefinitionList.firstOrNull() + } + +val GoFunctionLit.varFunctionVariableName: String? + get() { + val first = this.varDefinition ?: return null + return first.name + } diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoLanguageVisitor.kt new file mode 100644 index 0000000..f8a3338 --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/go/GoLanguageVisitor.kt @@ -0,0 +1,485 @@ +package com.github.nikolaikopernik.codecomplexity.go + +import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink +import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor +import com.github.nikolaikopernik.codecomplexity.core.PointType +import com.goide.inspections.GoInspectionUtil +import com.goide.psi.GoAndExpr +import com.goide.psi.GoAssignmentStatement +import com.goide.psi.GoBinaryExpr +import com.goide.psi.GoBlock +import com.goide.psi.GoBreakStatement +import com.goide.psi.GoCallExpr +import com.goide.psi.GoCallLikeExpr +import com.goide.psi.GoConditionalExpr +import com.goide.psi.GoContinueStatement +import com.goide.psi.GoDeferStatement +import com.goide.psi.GoElement +import com.goide.psi.GoElseStatement +import com.goide.psi.GoFile +import com.goide.psi.GoForStatement +import com.goide.psi.GoFunctionDeclaration +import com.goide.psi.GoFunctionLit +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.psi.GoGoStatement +import com.goide.psi.GoIfStatement +import com.goide.psi.GoLabeledStatement +import com.goide.psi.GoLazyBlock +import com.goide.psi.GoLeftHandExprList +import com.goide.psi.GoMethodDeclaration +import com.goide.psi.GoOrExpr +import com.goide.psi.GoParenthesesExpr +import com.goide.psi.GoReferenceExpression +import com.goide.psi.GoReturnStatement +import com.goide.psi.GoSelectStatement +import com.goide.psi.GoSimpleStatement +import com.goide.psi.GoStatement +import com.goide.psi.GoSwitchStart +import com.goide.psi.GoSwitchStatement +import com.goide.psi.GoUnaryExpr +import com.goide.psi.GoValue +import com.goide.psi.GoVarDeclaration +import com.goide.psi.GoVarSpec +import com.intellij.openapi.diagnostic.Logger +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile + +/** + * Represents a type alias used to pair a [GoStatement] and a [PsiElement]. + * This combination is utilized in the context of analyzing Go language code + * within a PSI (Program Structure Interface) tree to evaluate complexity or + * navigate code structure. + */ +private typealias PsiUnit = Pair + +/** + * A type alias representing a pair of a `GoStatement` and an `ArrayDeque` of `PsiUnit`. + * + * The first component, `GoStatement`, typically serves as the root statement or context + * for a given operation or computation. The second component, an `ArrayDeque` of `PsiUnit`, + * stores a sequence of elements (e.g., PSI nodes such as statements or expressions) that + * can be processed in a stack-like manner. + * + * This type alias is primarily used to simplify and streamline operations involving + * traversals or manipulations of PSI tree structures in the context of Go code complexity + * analysis. It supports various utility functions that enable operations like adding, + * removing, and processing elements within the deque while keeping track of associated + * context or root statements. + */ +private typealias PsiUnitDeque = Pair> + +/** + * Adds the given element to the end of the deque. + * + * @param element The PsiElement to be added to the end of the deque. + */ +private fun PsiUnitDeque.addLast(element: PsiElement) { + val root = this.first + this.second.addLast(root to element) +} + +/** + * Adds the child elements of the given PsiElement to the deque in reverse order. + * + * @param element the PsiElement whose child elements should be added to the deque + */ +fun PsiUnitDeque.addChildren(element: PsiElement) { + val children = element.children + children.reversed().forEach { addLast(it) } +} + +private fun PsiUnitDeque.isEmpty(): Boolean = this.second.isEmpty() + +private fun PsiUnitDeque.removeLast(): PsiUnit = this.second.removeLast() + +/** + * A visitor class for analyzing and processing elements of Go programming language syntax trees. + * This class traverses the syntax tree of Go code, identifies specific constructs, + * and evaluates their impact on code complexity and nesting levels. + * + * @constructor + * @param sink an instance of [ComplexitySink] used to track and accumulate complexity and nesting levels. + */ +class GoLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() { + + override fun processElement(element: PsiElement) { + when (element) { + is GoForStatement -> sink.increaseComplexityAndNesting(PointType.LOOP_FOR) + is GoIfStatement -> { + if (element.parent !is GoElseStatement) { + sink.increaseComplexityAndNesting(PointType.IF) + } + } + + is GoElseStatement -> { + sink.decreaseNesting() + sink.increaseComplexity(1, PointType.ELSE) + sink.increaseNesting() + } + + is GoSelectStatement, is GoSwitchStatement -> sink.increaseComplexityAndNesting(element.pointType()) + + is GoCallExpr -> { + // ignore built-in function call + if (element.isBuiltInFunction()) return + if (element.isRecursionCall()) sink.increaseComplexity(PointType.RECURSION) + } + + is GoFunctionLit -> when { + // Ignore function literals in struct member or in top level var declaration function. + element.isMemberFunction() || element.isToplevelVarDeclFunction() -> return + else -> sink.increaseNesting() + } + + is GoBreakStatement -> { + element.labelRef ?: return + sink.increaseComplexity(PointType.BREAK) + } + + is GoContinueStatement -> { + val labelRef = element.labelRef + if (labelRef != null) { + sink.increaseComplexity(PointType.CONTINUE) + } + } + + is GoStatement -> { + val parent = element.parent + when { + // calculate a combination of logical operators in if statements + element is GoSimpleStatement && parent is GoIfStatement -> calculateIfStatementOperators(parent, element) + // calculate a combination of logical operators + element is GoSimpleStatement && element.hasChild() -> calculateIfStatementOperators(element, element) + // calculate a combination of logical operators in assignment statements + element is GoAssignmentStatement && (element.hasChild() || element.hasChild()) -> calculateIfStatementOperators(element, element) + // calculate a combination of logical operators in return statements + element is GoReturnStatement && (element.hasChild() || element.hasChild()) -> calculateIfStatementOperators(element, element) + } + } + } + } + + /** + * Recursively calculates complexity points for "if" statements in Go source code + * by traversing the abstract syntax tree (AST) using a deque. The method examines + * boolean logical operations like `&&` and `||`, and other related conditions + * to increase complexity points appropriately. + * + * @param root the starting `GoStatement` element representing the root of the `if` statement or + * logical expression from which the calculation begins. + * @param statement the specific `GoStatement` element that will be processed to determine + * its role in contributing to overall complexity. + */ + fun calculateIfStatementOperators(root: GoStatement, statement: GoStatement) { + val deque = root to ArrayDeque() + deque.addChildren(statement) + deque.calculateIfStatementOperators() + } + + /** + * Recursively calculates complexity points for "if" statements in Go source code by traversing + * a deque of PSI elements. The method processes boolean logical operations like `&&` and `||`, + * parentheses, and conditions to appropriately increase complexity points. + * + * @param foundRoot a flag indicating whether the root condition of an `if` statement or a + * related logical expression (such as `&&` or `||`) has been successfully located and processed. + * Default is `false`. + */ + tailrec fun PsiUnitDeque.calculateIfStatementOperators(foundRoot: Boolean = false) { + if (isEmpty()) { + return + } + val (root, element) = removeLast() + val parent = element.parent + var newFoundRoot = foundRoot + if (element is GoAndExpr || element is GoOrExpr) { + val parentCondition = parent?.findParentConditionExpr(element) + if (!foundRoot && (parentCondition == root || parentCondition is GoAndExpr || parentCondition is GoOrExpr)) { + sink.increaseComplexity(element.pointType()) + newFoundRoot = true + } else if (parentCondition != null) { + sink.increaseComplexity(element.pointType()) + } + } else if (element is GoParenthesesExpr) { + val next = element.getNextExpr() + if (next != null && next.parent != null) { + val parentCondition = next.parent.findParentConditionExpr(next) + val pointType = parentCondition?.pointType() ?: element.pointType() + sink.increaseComplexity(pointType) + } + } + addChildren(element) + calculateIfStatementOperators(newFoundRoot) + } + + override fun postProcess(element: PsiElement) { + when (element) { + is GoForStatement, + is GoIfStatement, + is GoSwitchStatement, + is GoSelectStatement, + is GoFunctionLit -> sink.decreaseNesting() + } + } + + override fun shouldVisitElement(element: PsiElement): Boolean { + return element is GoFunctionDeclaration || + element is GoMethodDeclaration || + element is GoBlock || + element is GoIfStatement || + element is GoElseStatement || + element is GoForStatement || + element is GoLabeledStatement || + element is GoReturnStatement || + element is GoGoStatement || + element is GoDeferStatement || + (element is GoCallLikeExpr && element.parent is GoGoStatement) || + element is GoSelectStatement || + element is GoSwitchStatement || + element is GoCallExpr || + element is GoFunctionLit || + element is GoBinaryExpr || + (element is GoSimpleStatement && element.parent is GoIfStatement) || + (element is GoLeftHandExprList && element.parent is GoSimpleStatement) || + element is GoLazyBlock + } + + companion object { + + /** + * Determines if the current GoStatement instance contains a child element of the specified type. + * + * @return true if the GoStatement has a child element of type E; false otherwise. + */ + inline fun GoStatement.hasChild(): Boolean { + return this.children.any { element -> element is E } + } + + /** + * A list of predefined names representing the Go language's built-in functions. + * + * These function names can be used to identify whether certain function calls + * in Go code represent standard library operations provided by the language. + * + * The list includes commonly used functions, such as... + * + * - "print", "println": For printing output to the console. + * - "len", "cap": For measuring length and capacity of collections. + * - "make", "new": For creating slices, maps, and channels. + * - Mathematical utilities like "min", "max", and "complex". + * - Error handling and recovery utilities like "panic" and "recover". + */ + val builtinFunctionNames: List = listOf( + "print", "println", + "make", "new", + "len", "cap", + "delete", "copy", + "complex", "imag", "real", + "panic", "recover", + "min", "max", + ) + + /** + * Determines if the provided Go call expression corresponds to a built-in function. + * + * The function checks if the first child of type [GoReferenceExpression] in the + * call expression matches any known built-in function names. + * + * @return true if the call expression represents a built-in function, false otherwise. + */ + fun GoCallExpr.isBuiltInFunction(): Boolean { + val text = this.children.firstOrNull { it is GoReferenceExpression }?.text + return text in builtinFunctionNames + } + + /** + * Determines whether a given Go call expression is a recursive call. + * A call is considered recursive if the function or method being called + * is the same as the one that contains the call. + * + * @return true if the call is recursive, false otherwise + */ + fun GoCallExpr.isRecursionCall(): Boolean { + try { + val resolvedSignatureOwner = GoInspectionUtil.resolveCall(this) + val rootFunctionOrMethodDeclaration = this.findFunctionOrMethodDeclaration() + return resolvedSignatureOwner == rootFunctionOrMethodDeclaration + } catch (e: Throwable) { + logger.warn("error while processing GoCallExpr: ${this.javaClass.simpleName}(${this.text})", e) + return false + } + } + + /** + * Recursively searches for the enclosing Go function or method declaration in the PSI (Program Structure Interface) tree. + * The search continues upward through the tree until a `GoFunctionOrMethodDeclaration` is found or the root element is reached. + * + * @return the enclosing `GoFunctionOrMethodDeclaration` if found, or `null` if no such declaration exists. + */ + tailrec fun PsiElement.findFunctionOrMethodDeclaration(): GoFunctionOrMethodDeclaration? { + return when (this) { + is GoFunctionOrMethodDeclaration -> this + is GoFile, is PsiFile -> null + else -> when (this.parent) { + null -> null + else -> this.parent.findFunctionOrMethodDeclaration() + } + } + } + + /** + * Determines the type of PSI element and maps it to the corresponding `PointType` enumeration value. + * + * This method checks the type of the current PSI element and assigns a `PointType` based on its + * characteristics or role in a Go language program. If no specific type can be determined, the method + * defaults to returning `PointType.UNKNOWN`. + * + * @return the `PointType` corresponding to the type of the current PSI element. Possible return + * values include `PointType.LOGICAL_AND`, `PointType.LOGICAL_OR`, `PointType.IF`, `PointType.LOOP_FOR`, + * `PointType.SWITCH`, `PointType.ELSE`, `PointType.BREAK`, `PointType.CONTINUE`, `PointType.METHOD`, and + * `PointType.UNKNOWN`. + */ + fun PsiElement.pointType(): PointType = when (this) { + is GoAndExpr -> PointType.LOGICAL_AND + is GoOrExpr -> PointType.LOGICAL_OR + is GoIfStatement -> PointType.IF + is GoForStatement -> PointType.LOOP_FOR + is GoSwitchStatement -> PointType.SWITCH + is GoSwitchStart -> PointType.SWITCH + is GoSelectStatement -> PointType.SWITCH + is GoElseStatement -> PointType.ELSE + is GoBreakStatement -> PointType.BREAK + is GoContinueStatement -> PointType.CONTINUE + is GoCallExpr -> PointType.METHOD + is GoCallLikeExpr -> PointType.METHOD + is GoGoStatement -> PointType.METHOD + is GoDeferStatement -> PointType.METHOD + is GoParenthesesExpr -> { + val candidate = children.filter { c -> + c is GoAndExpr || + c is GoOrExpr || + c is GoCallExpr || + c is GoCallLikeExpr + }.map { c -> c.pointType() } + .firstOrNull { c -> c != PointType.UNKNOWN } + candidate ?: PointType.UNKNOWN + } + + else -> PointType.UNKNOWN + } + + + /** + * Recursively finds a parent condition or expression in the PSI tree that matches specific types. + * + * @param start the initial element used to avoid redundant processing of the same expression type. + * @return the parent PsiElement if it matches the expected condition or expression types; otherwise, null. + */ + tailrec fun PsiElement.findParentConditionExpr(start: PsiElement): PsiElement? { + return when (this) { + is GoIfStatement, is GoForStatement -> this // If or For + is GoReturnStatement, is GoAssignmentStatement, is GoVarSpec -> this + is GoAndExpr, is GoOrExpr, is GoConditionalExpr -> { + if (this.javaClass == start.javaClass) { + null + } else { + this + } + } + + is GoParenthesesExpr -> when (start) { + is GoAndExpr, is GoOrExpr, is GoConditionalExpr -> this + else -> null + } + + else -> when (val parent = this.parent) { + null -> null + else -> parent.findParentConditionExpr(start) + } + } + } + + /** + * Recursively retrieves the next relevant expression in the PSI tree based on + * specific patterns of Go expression elements ([GoAndExpr], [GoOrExpr], [GoUnaryExpr]). + * This method traverses through the parent-child relationships in the PSI tree + * to locate the proper next expression. + * + * @return the next expression as a PsiElement if it exists; otherwise, null if no next + * expression can be determined or the traversal ends. + */ + // GoAndExpr(=&&)[GoExpr(left=THIS),GoExpr(right)] -> GoExpr(right) + // GoOrExpr(=||)[GoAndExpr[GoExpr(1-1),GoExpr(1-2=THIS)],GoExpr(2)] -> GoExpr(2) + // GoAndExpr[GoUnaryExpr(=!)[GoExpr(THIS)],GoExpr(right)] -> GoExpr(right) + tailrec fun PsiElement.getNextExpr(): PsiElement? { + return when (val parent = this.parent) { + is GoAndExpr, is GoOrExpr -> when (val next = this.next()) { + null -> parent.next() + else -> next + } + + is GoUnaryExpr -> parent.getNextExpr() + else -> null + } + } + + /** + * Retrieves the next sibling element in the PSI tree that follows this element + * within the same parent. The elements are evaluated in the order of their + * appearance as children of the parent. + * + * @return the next sibling element if it exists; otherwise, null if this is + * the last child or if the parent is null. + */ + fun PsiElement.next(): PsiElement? { + val parent = this.parent ?: return null + val children = parent.children + for ((index, element) in children.withIndex()) { + if (element == this && index < children.lastIndex) { + return children[index + 1] + } + } + return null + } + + /** + * Executes a specified operation on the companion object and returns the result. + * + * @param operation A lambda function that defines the operation to be executed on the companion object. + * @return The result of the operation performed on the companion object. + */ + fun util(operation: Companion.() -> T): T = operation() + + /** + * Determines whether the current function literal is a member function. + * + * A function literal is considered a member function if its direct parent + * is a `GoValue` and the parent's parent is a `GoElement`. + * + * [GoFunctionLit] --> [GoValue] --> [GoElement] + * + * @return `true` if the function literal is a member function, otherwise `false` + */ + fun GoFunctionLit.isMemberFunction(): Boolean { + val value = this.parent as? GoValue ?: return false + return value.parent is GoElement + } + + /** + * Determines if the function literal is declared as part of a top-level variable declaration. + * A function literal is considered to be part of a top-level variable declaration if its + * ancestry chain includes the following elements in order: + * + * [GoFunctionLit] --> [GoVarSpec] --> [GoVarDeclaration] --> [GoFile] + * + * @return true if the function literal is part of a top-level variable declaration, false otherwise + */ + fun GoFunctionLit.isToplevelVarDeclFunction(): Boolean { + val varSpec = this.parent as? GoVarSpec ?: return false + val declaration = varSpec.parent as? GoVarDeclaration ?: return false + return declaration.parent is GoFile + } + + val logger = Logger.getInstance(GoLanguageVisitor::class.java.simpleName) + } +} diff --git a/src/main/resources/META-INF/codecomplexity-go.xml b/src/main/resources/META-INF/codecomplexity-go.xml new file mode 100644 index 0000000..620ea50 --- /dev/null +++ b/src/main/resources/META-INF/codecomplexity-go.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a7e71a4..e38d9bb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -8,6 +8,7 @@ com.intellij.java org.jetbrains.kotlin com.intellij.modules.python + org.jetbrains.plugins.go ()?.associate(GoFileType.INSTANCE, ExtensionFileNameMatcher("go")) + } + // Add extensions/stub indices for the Go language feature to work + @Suppress("DEPRECATION") + val extensionsRoot = Extensions.getRootArea() + extensionsRoot + .registerExtensionPoint("com.goide.packageFactory", MockPackageFactory::class.java.canonicalName, ExtensionPoint.Kind.INTERFACE, true) + extensionsRoot + .registerExtensionPoint("com.goide.sdk.sdkVetoer", MockSdkVetoer::class.java.canonicalName, ExtensionPoint.Kind.INTERFACE, true) + extensionsRoot.registerStubIndex(GoMethodIndex()) + extensionsRoot.registerStubIndex(GoPackagesIndex()) + extensionsRoot.registerStubIndex(GoAllPrivateNamesIndex()) + extensionsRoot.registerStubIndex(GoFunctionIndex()) + extensionsRoot.registerStubIndex(GoAllPublicNamesIndex()) + extensionsRoot.registerStubIndex(GoPackageLevelPublicElementsIndex()) + extensionsRoot.registerStubIndex(GoTypesIndex()) + extensionsRoot.registerStubIndex(GoTypeAliasIndex()) + extensionsRoot.registerStubIndex(GoNonPackageLevelNamesIndex()) + extensionsRoot.registerStubIndex(GoMethodFingerprintIndex()) + extensionsRoot.registerStubIndex(GoMethodSpecFingerprintIndex()) + extensionsRoot.registerStubIndex(GoMethodSpecInheritanceIndex()) + extensionsRoot.registerStubIndex(GoTypeSpecInheritanceIndex()) + extensionsRoot.getExtensionPoint("com.intellij.stubElementTypeHolder") + .registerExtension(StubElementTypeHolderEP().apply { + holderClass = $$"com.goide.stubs.GoBackendElementTypeFactory$StubTypes" + externalIdPrefix = "go." + }, disposableRule.disposable) + + ElementManipulators.INSTANCE.addExplicitExtension(GoStringLiteralImpl::class.java, GoStringManipulator(), disposableRule.disposable) + application.register(MockStubIndex()) + + project.extensionArea.getExtensionPoint("com.intellij.psi.treeChangePreprocessor") + .registerExtension(GoPsiTreeChangeProcessor(), disposableRule.disposable) + } + + override fun tearDown() { + LanguageParserDefinitions.INSTANCE.removeExplicitExtension(GoLanguage.INSTANCE, goParserDefinition) + val application = ApplicationManager.getApplication() + application.runWriteAction { + application.unregisterService(GoElementTypeFactorySupplier::class.java) + application.service() + ?.removeAssociation(GoFileType.INSTANCE, ExtensionFileNameMatcher("go")) + module.unregisterService(GoModuleSettings::class.java) + } + super.tearDown() + } + + @Suppress("JUnitMixedFramework") + @Test + fun testGoComplexity() { + try { + checkAllFilesInFolder(GO_TEST_FILES_PATH, ".go") + } catch (e: NoClassDefFoundError) { + if (e.message != "com/intellij/ultimate/UltimateVerifier") { + throw e + } + } + } + + override fun getTestDataPath() = GO_TEST_FILES_PATH + + override fun createLanguageElementVisitor(sink: ComplexitySink): ElementVisitor = + GoComplexityInfoProvider().getVisitor(sink) + + override fun parseTestFile(file: PsiFile): List> { + val functionOrMethods = file + .childrenOfType() + .asSequence() + .mapNotNull { functionOrMethod -> functionOrMethod.convertToTest() } + val memberFunctions = file + .traverseVariablesOrMembersForTests("members.go") + .filterIsInstance() + .mapNotNull { literal -> + when { + GoLanguageVisitor.util { literal.isMemberFunction() } -> literal.convertToMemberFunctionTest() + GoLanguageVisitor.util { literal.isToplevelVarDeclFunction() } -> literal.convertToVarFunctionTest() + else -> null + } + } + return (memberFunctions + functionOrMethods).toList() + } + + /** + * Registers a PSI-based stub index to the given [ExtensionsArea]. This allows for the + * integration of custom PSI element indexing through the specified [StringStubIndexExtension]. + * + * @param psi The instance of [StringStubIndexExtension] representing the stub index extension + * for the desired type of PSI element. + */ + fun

ExtensionsArea.registerStubIndex(psi: StringStubIndexExtension

) { + val exp = getExtensionPoint>("com.intellij.stubIndex") + exp.registerExtension(psi, disposableRule.disposable) + } + + private companion object { + + /** + * Converts a [GoFunctionOrMethodDeclaration] into a testable representation if possible. + * + * This method extracts relevant information from a Go function or method declaration, + * specifically the complexity and the function name, and formats them into a `Triple`. + * The `Triple` contains the original PsiElement, the formatted function name with complexity, + * and the complexity value. + * + * @return A `Triple` containing the PsiElement, formatted function name, + * and complexity value, or `null` if the complexity or function + * name is not available. + */ + fun GoFunctionOrMethodDeclaration.convertToTest(): Triple? { + val complexity = this.complexity ?: return null + val functionName = this.name ?: return null + return Triple(this, functionName.replace("()", "($complexity)"), complexity) + } + + val GoFunctionOrMethodDeclaration.complexity: Int? get() = complexityForFunctionOrMethodDecl(this.prevSibling) + + /** + * Retrieves the complexity for a given [PsiElement]. + * + * This function examines the provided PSI (Program Structure Interface) element + * recursively, checking for specific comments that define complexity. It navigates + * through previous sibling elements if necessary to locate the relevant definition. + * + * @param element The PSI element to analyze. It may represent different components + * of a code structure, such as comments or function declarations. + * If null, the function will*/ + tailrec fun complexityForFunctionOrMethodDecl(element: PsiElement?): Int? = when (element) { + null -> null + is PsiComment -> { + if (element.text == null) null + else when (val ret = element.text.complexity()) { + null -> complexityForFunctionOrMethodDecl(element.prevSibling) + else -> ret + } + } + + is GoFunctionDeclaration, + is GoImportList, + is GoMethodDeclaration, + is GoPackageClause, + is GoTypeDeclaration, + is GoVarDeclaration -> null + + else -> complexityForFunctionOrMethodDecl(element.prevSibling) + } + + fun String.complexity(): Int? { + val words = this.replaceFirst("//", "").trim().split(Regex("\\s+")) + if (words.size != 3) return null + if (words[0] != "go:generate") { + return null + } + if (words[1] != "complexity") { + return null + } + if (Regex("^[1-9][0-9]*$").matches(words[2])) return words[2].toInt() + return null + } + + fun PsiFile.traverseVariablesOrMembersForTests(vararg fileNames: String): Sequence = + if (this.name in fileNames) this.descendants { !(it is GoFunctionLit || it is GoLiteral || it is GoStringLiteral || it is GoFunctionOrMethodDeclaration) } + else emptySequence() + + /** + * Converts a [GoFunctionLit] instance into a member function test representation. + * + * The conversion is based on the member function's complexity and field name. If either the complexity + * or the field name is not available, the function will return null. + * + * @return a [Triple] containing the [PsiElement] representing the function, the function name as a [String], + * and its complexity as an [Int], or null if the function cannot be converted. + */ + fun GoFunctionLit.convertToMemberFunctionTest(): Triple? { + val complexity = this.memberFunctionComplexity ?: return null + val functionName = this.memberFunctionFieldName ?: return null + return Triple(this, functionName, complexity) + } + + /** + * Finds a comment in the given PSI element or its ancestors. + * + * This function recursively traverses backward through the siblings of the element + * until it encounters a [PsiComment] or reaches the root of the tree. + * + * @param elem the starting PSI element from which to search for a comment. + * Can be null, in which case the function immediately returns null. + * @return the first `PsiComment` found while traversing the PSI tree, + * or null if no comment is found. + */ + tailrec fun findComment(elem: PsiElement?): PsiComment? = + when (elem) { + null -> null + is PsiComment -> elem + else -> findComment(elem.prevSibling) + } + + /** + * Retrieves the complexity value from an associated comment. + * + * This property evaluates the complexity of the member function defined within a + * `GoFunctionLit` instance. The complexity is retrieved from a special comment + * associated with the function's parent element. + * + * [GoFunctionLit] --> [GoValue] --> [GoElement] --> [PsiComment] -> complexity + * + * The expected format of the comment is: + * ```text + * // go:generate complexity + * ``` + * where `` is a positive integer representing the complexity value. + * + * @return The parsed complexity value as an `Int`, or `null` if the comment is + * missing, improperly formatted, or contains an invalid complexity value. + */ + val GoFunctionLit.memberFunctionComplexity: Int? + get() { + val goElement = this.parent?.parent as? GoElement ?: return null + val comment = findComment(goElement) ?: return null + return comment.text?.complexity() + } + + /** + * Converts a `GoFunctionLit` instance into a testable representation if it satisfies certain conditions. + * + * The conditions for conversion are: + * - The variable function complexity is not null. + * - The variable function has a defined variable name. + * + * @return A Triple containing the `GoFunctionLit` element, the variable function's name, and its complexity, + * or null if the required conditions are not satisfied. + */ + fun GoFunctionLit.convertToVarFunctionTest(): Triple? { + val complexity = this.varFunctionComplexity ?: return null + val functionName = this.varFunctionVariableName ?: return null + return Triple(this, functionName, complexity) + } + + /** + * Retrieves the complexity value from a comment + * associated with the variable declaration containing the function literal + * (if such a comment exists and is properly formatted). + * + * Returns: + * - The integer value of the complexity if a valid comment with a specified complexity + * is found for the variable declaration. + * - `null` if no valid complexity comment is found, or the associated variable + * declaration does not exist. + * + * [GoFunctionLit] --> [GoVarSpec] --> [PsiComment] -> complexity + * + * The expected format of the comment is: + * ```text + * // go:generate complexity + * ``` + * where `` is a positive integer representing the complexity value. + */ + val GoFunctionLit.varFunctionComplexity: Int? + get() { + val varSpec = this.parent as? GoVarSpec ?: return null + val comment = findComment(varSpec) ?: return null + return comment.text?.complexity() + } + + /** + * Represents an element container for constructing and managing XML-like registry + * configurations, allowing for content to be dynamically augmented. + * + * @property element The underlying XML element that serves as the base container for registry entries. + */ + class RegistryElement(private val element: Element) { + operator fun String.invoke(value: String) { + val entry = Element("entry") + entry.setAttribute("key", this) + entry.setAttribute("value", value) + element.addContent(entry) + } + } + + /** + * Configures and constructs a registry element using the provided configuration lambda. + * + * @param configure A lambda with receiver of type `RegistryElement` that defines the configuration + * of the registry element. + * @return An `Element` instance representing the configured registry. + */ + fun registry(configure: RegistryElement.() -> Unit): Element { + return Element("registry").apply { + RegistryElement(this).configure() + } + } + + inline fun Application.service(): T? { + return getService(T::class.java) + } + + inline fun Application.register(instance: T) { + registerServiceInstance(T::class.java, instance) + } + } +} + +private class MockPackageFactory : GoPackageFactory { + override fun createPackage(goFile: GoFile): GoPackage? = + when (val parent = goFile.parent) { + null -> GoPackage.`in`(PsiDirectoryFactory.getInstance(goFile.project) + .createDirectory(goFile.virtualFile.parent), "mock") + + else -> GoPackage.`in`(parent, "mock") + } + + override fun createPackage(name: String, + vararg directories: PsiDirectory?): GoPackage = + directories.firstNotNullOf { it }.let { GoPackage.`in`(it, name) } +} + +@Suppress("UnstableApiUsage") +private class MockStubIndex : StubIndex() { + + override fun processElements( + indexKey: StubIndexKey, + key: Key & Any, + project: Project, + scope: GlobalSearchScope?, + idFilter: IdFilter?, + requiredClass: Class, + processor: Processor): Boolean = true + + override fun getAllKeys(p0: StubIndexKey, p1: Project): Collection = + TODO("Not yet implemented") + + override fun getContainingFilesIterator(p0: StubIndexKey, + p1: Key & Any, + p2: Project, + p3: GlobalSearchScope): Iterator = + TODO("Not yet implemented") + + override fun getMaxContainingFileCount( + p0: StubIndexKey, + p1: Key & Any, + p2: Project, + p3: GlobalSearchScope): Int = TODO("Not yet implemented") + + override fun forceRebuild(p0: Throwable) = TODO("Not yet implemented") + + override fun getPerFileElementTypeModificationTracker(p0: IFileElementType): ModificationTracker = + TODO("Not yet implemented") + + override fun getStubIndexModificationTracker(p0: Project): ModificationTracker = TODO("Not yet implemented") +} + +private class MockSdkVetoer : GoBasedSdkVetoer { + override fun isSdkVetoed(p0: GoBasedSdk, p1: Module): Boolean = false +} diff --git a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/PythonComplexityCalculationTest.kt b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/PythonComplexityCalculationTest.kt index 4b571c6..2968576 100644 --- a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/PythonComplexityCalculationTest.kt +++ b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/PythonComplexityCalculationTest.kt @@ -11,7 +11,9 @@ import org.junit.Test private const val PYTHON_TEST_FILES_PATH = "src/test/testData/python" +@Suppress("UnstableApiUsage") class PythonComplexityCalculationTest : BaseComplexityTest() { + @Suppress("JUnitMixedFramework") @Test fun testPythonFiles() { checkAllFilesInFolder(PYTHON_TEST_FILES_PATH, ".py") @@ -22,6 +24,7 @@ class PythonComplexityCalculationTest : BaseComplexityTest() { override fun createLanguageElementVisitor(sink: ComplexitySink): ElementVisitor = PythonComplexityInfoProvider().getVisitor(sink) override fun parseTestFile(file: PsiFile): List> { + //FIXME this code seems not working: returns NO function. val methods: List = requireNotNull(file.getChildrenOfType()).toList() return methods.map { method -> diff --git a/src/test/kotlin/com/intellij/ultimate/UltimateVerifier.kt b/src/test/kotlin/com/intellij/ultimate/UltimateVerifier.kt new file mode 100644 index 0000000..4c9e4a3 --- /dev/null +++ b/src/test/kotlin/com/intellij/ultimate/UltimateVerifier.kt @@ -0,0 +1,14 @@ +package com.intellij.ultimate + +/** + * A mock implementation of the `UltimateVerifier` class from `com.intellij.ultimate.UltimateVerifier`. + * Used to enable index cache operations to execute in test mode. + */ +@Suppress("unused") +class UltimateVerifier { + + companion object { + @JvmStatic + fun getInstance(): UltimateVerifier = UltimateVerifier() + } +} diff --git a/src/test/testData/go/go_defer_function_literals.go b/src/test/testData/go/go_defer_function_literals.go new file mode 100644 index 0000000..72af394 --- /dev/null +++ b/src/test/testData/go/go_defer_function_literals.go @@ -0,0 +1,28 @@ +package main + +//go:generate complexity 3 +func goGoFunctionAddsOnlyNesting(i int) <-chan int { + c := make(chan int) + go func() { + if i%2 == 0 { //+1 (nested +1) + c <- 1 + } else { //+1 + c <- 2 + } + close(c) + }() + return c +} + +//go:generate complexity 3 +func deferFunctionAddsOnlyNesting(i int, c chan int) { + defer func() { + if i%2 == 0 { //+1 (nested +1) + c <- 1 + } else { //+1 + c <- 2 + } + close(c) + }() + c <- i +} diff --git a/src/test/testData/go/logical_operations.go b/src/test/testData/go/logical_operations.go new file mode 100644 index 0000000..3671b77 --- /dev/null +++ b/src/test/testData/go/logical_operations.go @@ -0,0 +1,105 @@ +package main + +//go:generate complexity 7 +func complex( + a, b, c, d, e, f, g, h, i, j bool, +) bool { + if // +1 + a || b && // +1 + c || d && // +1 + e || f && // +1 + g || h && // +1 + i || j { // +1 + return true + } else { // +1 + return false + } +} + +//go:generate complexity 4 +func simpleStatements(b bool, v int) int { + if b { // +1 + return 1 + } + if v < 0 { // +1 + return 2 + } + if v%3 == 0 && // +1 + v%5 == 0 { // +1 + return 15 + } + return v +} + +//go:generate complexity 3 +func twoGroups(a, b, c, d bool) bool { + if // +1 + a || b || // +1 OR + !(c || d) { // +1 OR separate + return true + } + return false +} + +type Aa struct { + a, b, c, d bool +} + +//go:generate complexity 3 +func (a Aa) parenthesisCreateNewGroupAnyway() bool { + if //+1 if + a.a || a.b || //+1 OR + (a.c || a.d) { // +1 OR group + return true + } + return false +} + +//go:generate complexity 4 +func (a Aa) parenthesisInCenterSplitTheGroup(e, f bool) bool { + if //+1 if + a.a || a.b || //+1 OR + !(a.c || // +1 OR group + a.d) || e || f { // +1 new OR + return true + } + return false +} + +func (a Aa) call() bool { + return true +} + +//go:generate complexity 1 +func (a Aa) doesSupportOperation() bool { + return a.a && a.call() +} + +//go:generate complexity 2 +func (a Aa) doesSupportOperationInStatement() bool { + r := a.a && a.call() //+1 + r = r && a.call() //+1 + return r +} + +type MayErr interface { + runAction() error + getMap() map[string]string + consumeSomething(v string) +} + +//go:generate complexity 1 +func goErrorIfStatement(m MayErr) error { + if err := m.runAction(); err != nil { // +1 + return err + } + return nil +} + +//go:generate complexity 1 +func goSpecificIfStatement(k string, m MayErr) { + f := m.getMap() + if v, ok := f[k]; ok { // +1 + m.consumeSomething(v) + } +} diff --git a/src/test/testData/go/loop_statements.go b/src/test/testData/go/loop_statements.go new file mode 100644 index 0000000..1d509bc --- /dev/null +++ b/src/test/testData/go/loop_statements.go @@ -0,0 +1,54 @@ +package main + +//go:generate complexity 3 +func allPossibleLoops() { + a := 0 + for a < 4 { // +1 + a++ + } + for i := 0; i < 3; i++ { // +1 + a += i + } + s := []int{0, 2} + for i := range s { // +1 + v := s[i] + a += v + } +} + +type MyLoop struct { + items []int +} + +//go:generate complexity 3 +func (l *MyLoop) methodAllPossibleLoops() int { + s := len(l.items) + t := 0 + for true { // +1 + t++ + break + } + for i := 0; i < s; i++ { // +1 + t++ + } + for _, i := range l.items { // +1 + t += i + } + return t +} + +//go:generate complexity 5 +func LoopsCreateNesting(items []interface{}) { + for i := 0; i < 10; i++ { // +1 + if i%2 == 0 { // +1 nesting +1 + i++ + break + } else { // +1 + i++ + } + } + a := 0 + for range items { // +1 + a++ + } +} diff --git a/src/test/testData/go/members.go b/src/test/testData/go/members.go new file mode 100644 index 0000000..4d54dc6 --- /dev/null +++ b/src/test/testData/go/members.go @@ -0,0 +1,46 @@ +package main + +var proc = struct { + Call func(variables []int) int +}{ + //go:generate complexity 4 + Call: func(variables []int) int { + m := 0 + for i, v := range variables { //+1 + if i == 0 { //+1 (nested +1) + m = v + } else if v < m { //+1 + m = v + } + } + return m + }, +} + +//go:generate complexity 1 +var myFunc = func(variables []int) int { + m := 0 + for i, v := range variables { //+1 + if i == 0 { //+1 (nested +1) + m = v + } else if v < m { //+1 + m = v + } + } + return m +} + +var ( + //go:generate complexity 4 + myFuncInVars = func(variables []int) int { + m := 0 + for i, v := range variables { //+1 + if i == 0 { //+1 (nested +1) + m = v + } else if v < m { //+1 + m = v + } + } + return m + } +) diff --git a/src/test/testData/go/nested_if.go b/src/test/testData/go/nested_if.go new file mode 100644 index 0000000..6bcfc69 --- /dev/null +++ b/src/test/testData/go/nested_if.go @@ -0,0 +1,43 @@ +package main + +//go:generate complexity 5 +func NestedIf(vs []int) int { + if 0 < len(vs) { // +1 + v := vs[0] + if 2 < v { // +1(nested +1) + return v + } else { // +1 + return len(vs) + } + } else { // +1 + return 0 + } +} + +//go:generate complexity 8 +func labelBreak(m, n int, s ...int) { + for i := 0; i < m; i++ { //+1 + sub: + for j := i + 1; j < n; j++ { //+1 (nested +1) + if s[i] == s[j] { //+1 (nested +2) + break sub //+1 + } else if s[j] == 0 { //+1 + break + } + } + } +} + +//go:generate complexity 8 +func labelContinue(m, n int, s ...int) { +root: + for i := 0; i < m; i++ { //+1 + for j := i + 1; j < n; j++ { //+1 (nested +1) + if s[i] == s[j] { //+1 (nested +2) + continue root //+1 + } else if s[j] == 0 { //+1 + continue + } + } + } +} diff --git a/src/test/testData/go/recursion.go b/src/test/testData/go/recursion.go new file mode 100644 index 0000000..04529a2 --- /dev/null +++ b/src/test/testData/go/recursion.go @@ -0,0 +1,27 @@ +package main + +//go:generate complexity 5 +func fibonacci(n int) int { + if n == 0 { // +1 <1> + return 1 + } else if n == 1 { // +1 <2> + return 1 + } else { // +1 <3> + return fibonacci(n-1) + // +1 + fibonacci(n-2) // +1 + } +} + +type Recursion struct { +} + +//go:generate complexity 5 +func (r Recursion) fib(n int) int { + if n == 0 { + return 1 + } else if n == 1 { + return 1 + } else { + return r.fib(n-1) + r.fib(n-2) + } +} diff --git a/src/test/testData/go/select_and_switch_statements.go b/src/test/testData/go/select_and_switch_statements.go new file mode 100644 index 0000000..c311d83 --- /dev/null +++ b/src/test/testData/go/select_and_switch_statements.go @@ -0,0 +1,38 @@ +package main + +//go:generate complexity 3 +func selectCaseStatements(recv, fin <-chan int) int { + s := 0 + for { //+1 + select { //+1 (nested +1) + case v := <-recv: + s += v + case <-fin: + return s + } + } +} + +type MyTypes int + +const ( + MyFirstCase MyTypes = iota + MySecondCase + MyThirdCase + MyFourthCase +) + +//go:generate complexity 1 +func switchCaseStatements(e MyTypes) int { + switch e { + case MyFirstCase: + return int(e) + case MySecondCase: + return int(e) + case MyThirdCase: + return int(e) + case MyFourthCase: + return int(e) + } + panic("unreachable") +}