Skip to content
Draft
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 build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
bundledPlugin("com.intellij.java")
bundledPlugin("org.jetbrains.kotlin")
plugin("PythonCore", "243.18137.10")
plugin("JavaScript", "243.18137.10")

instrumentationTools()
pluginVerifier()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.intellij.psi.PsiElement
val PLUGIN_EP_NAME: ExtensionPointName<ComplexityInfoProvider> = ExtensionPointName("com.github.nikolaikopernik.codecomplexity.languageInfoProvider")
val PLUGIN_HINT_KEY = SettingsKey<NoSettings>("code.complexity.hint")

val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python")
// TODO: How to handle language with dialects?
val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python", "javascript", "typescript", "typescript jsx", "ecmascript 6", "ecma script level 4", "flow js")

/**
* Main interface to calculate complexity for different languages.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.github.nikolaikopernik.codecomplexity.javascript

import com.github.nikolaikopernik.codecomplexity.core.ComplexityInfoProvider
import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink
import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor
import com.intellij.lang.Language
import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.lang.javascript.dialects.ECMA6LanguageDialect
import com.intellij.lang.javascript.dialects.ECMAL4LanguageDialect
import com.intellij.lang.javascript.dialects.FlowJSLanguageDialect
import com.intellij.lang.javascript.dialects.TypeScriptJSXLanguageDialect
import com.intellij.lang.javascript.dialects.TypeScriptLanguageDialect
import com.intellij.lang.javascript.psi.JSFunction
import com.intellij.lang.javascript.psi.ecma6.TypeScriptTypeAlias
import com.intellij.lang.javascript.psi.ecma6.TypeScriptTypeMember
import com.intellij.lang.javascript.psi.ecmal4.JSClass
import com.intellij.psi.PsiElement

class JSComplexityInfoProvider : ComplexityInfoProvider {
// FIXME: language with dialects
override val language: Language = listOf(
TypeScriptLanguageDialect.getInstance(),
TypeScriptJSXLanguageDialect.getInstance(),
JavascriptLanguage.INSTANCE,
ECMA6LanguageDialect.getInstance(),
ECMAL4LanguageDialect.getInstance(),
FlowJSLanguageDialect.getInstance(),
).first()

override fun getVisitor(sink: ComplexitySink): ElementVisitor = JSLanguageVisitor(sink)

override fun isComplexitySuitableMember(element: PsiElement): Boolean = when {
element !is JSFunction -> false
element is TypeScriptTypeMember -> false
element.isShorthandArrowFunction -> false
element.block == null -> false
else -> true
}

override fun isClassWithBody(element: PsiElement): Boolean = when {
element !is JSClass -> false
element.isInterface -> false
element is TypeScriptTypeAlias -> false
element.members.iterator().hasNext().not() -> false
else -> true
}

override fun getNameElementFor(element: PsiElement): PsiElement = when (element) {
is JSFunction -> element.nameIdentifier ?: element
is JSClass -> element.members.firstOrNull() ?: element
else -> element
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.github.nikolaikopernik.codecomplexity.javascript

import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink
import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor
import com.github.nikolaikopernik.codecomplexity.core.PointType
import com.github.nikolaikopernik.codecomplexity.core.PointType.BREAK
import com.github.nikolaikopernik.codecomplexity.core.PointType.CATCH
import com.github.nikolaikopernik.codecomplexity.core.PointType.CONTINUE
import com.github.nikolaikopernik.codecomplexity.core.PointType.IF
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOGICAL_AND
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOGICAL_OR
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_FOR
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_WHILE
import com.github.nikolaikopernik.codecomplexity.core.PointType.RECURSION
import com.github.nikolaikopernik.codecomplexity.core.PointType.SWITCH
import com.github.nikolaikopernik.codecomplexity.core.PointType.UNKNOWN
import com.intellij.lang.javascript.JSElementType
import com.intellij.lang.javascript.JSTokenTypes
import com.intellij.lang.javascript.psi.JSBinaryExpression
import com.intellij.lang.javascript.psi.JSBreakStatement
import com.intellij.lang.javascript.psi.JSCallExpression
import com.intellij.lang.javascript.psi.JSCatchBlock
import com.intellij.lang.javascript.psi.JSConditionalExpression
import com.intellij.lang.javascript.psi.JSContinueStatement
import com.intellij.lang.javascript.psi.JSDoWhileStatement
import com.intellij.lang.javascript.psi.JSExpression
import com.intellij.lang.javascript.psi.JSForInStatement
import com.intellij.lang.javascript.psi.JSForStatement
import com.intellij.lang.javascript.psi.JSFunction
import com.intellij.lang.javascript.psi.JSFunctionExpression
import com.intellij.lang.javascript.psi.JSIfStatement
import com.intellij.lang.javascript.psi.JSParenthesizedExpression
import com.intellij.lang.javascript.psi.JSPrefixExpression
import com.intellij.lang.javascript.psi.JSSwitchStatement
import com.intellij.lang.javascript.psi.JSWhileStatement
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiKeyword
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.tree.IElementType

class JSLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() {
override fun processElement(element: PsiElement) {
when (element) {
is JSWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE)
is JSDoWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE)
is JSForStatement -> sink.increaseComplexityAndNesting(LOOP_FOR)
is JSForInStatement -> sink.increaseComplexityAndNesting(LOOP_FOR)
is JSIfStatement -> element.processIfExpression()
is JSSwitchStatement -> sink.increaseComplexityAndNesting(SWITCH)
is JSConditionalExpression -> {
sink.increaseComplexityAndNesting(IF)
element.calculateBinaryComplexity()
}

is JSCatchBlock -> sink.increaseComplexityAndNesting(CATCH)
is JSBreakStatement -> if (element.labelIdentifier != null) sink.increaseComplexity(BREAK)
is JSContinueStatement -> if (element.labelIdentifier != null) sink.increaseComplexity(CONTINUE)
is JSFunctionExpression -> sink.increaseNesting()
is JSCallExpression -> if (element.isRecursion()) sink.increaseComplexity(RECURSION)
}
}

override fun postProcess(element: PsiElement) {
if (element is JSWhileStatement ||
element is JSDoWhileStatement ||
element is JSForStatement ||
element is JSForInStatement ||
element is JSCatchBlock ||
element is JSFunctionExpression ||
element is JSIfStatement && element.elseBranch !is JSIfStatement
) {
sink.decreaseNesting()
}
}

override fun shouldVisitElement(element: PsiElement) = true

private fun JSIfStatement.processIfExpression() {
if (this.isElseIf()) {
return
}
sink.increaseComplexityAndNesting(IF)
}

private fun JSExpression.calculateBinaryComplexity(operands: MutableList<JSElementType> = mutableListOf()) {
val elements = when (this) {
is JSBinaryExpression -> listOf(this.lOperand, this.operationSign, this.rOperand)
is JSParenthesizedExpression -> listOf(this.innerExpression)
is JSPrefixExpression -> listOf(this.operationSign)
else -> emptyList()
}

elements.forEach { element ->
when (element) {
is JSElementType -> if (element in listOf(JSTokenTypes.AND, JSTokenTypes.OR)) {
if (operands.lastOrNull() == null || element != operands.lastOrNull()) {
sink.increaseComplexity(element.toPointType())
}
operands.add(element)
}

is JSParenthesizedExpression -> {
element.calculateBinaryComplexity()
operands.clear()
}

is JSPrefixExpression -> {
element.calculateBinaryComplexity()
operands.clear()
}

is JSBinaryExpression -> element.calculateBinaryComplexity(operands)
}
}
}
}

private fun JSCallExpression.isRecursion(): Boolean {
val parentMethod: JSFunction = this.findCurrentJSFunction() ?: return false
if (this.methodExpression?.text != parentMethod.name) return false
if (this.arguments.size != parentMethod.parameterList?.parameters?.size) return false
return true
}

private fun PsiElement.findCurrentJSFunction(): JSFunction? {
var element: PsiElement? = this
while (element != null && element !is JSFunction) element = element.parent
return element?.let { it as JSFunction }
}

private fun JSIfStatement.isElseIf(): Boolean = this.prevNotWhitespace().isElse()

private fun PsiElement?.isElse(): Boolean = this?.let {
it is PsiKeyword && it.text == PsiKeyword.ELSE
} ?: false

private fun JSIfStatement.prevNotWhitespace(): PsiElement? {
var prev: PsiElement = this
while (prev.prevSibling != null) {
prev = prev.prevSibling
if (prev !is PsiWhiteSpace) {
return prev
}
}
return null
}

private fun IElementType.toPointType(): PointType = when (this) {
JSTokenTypes.OROR -> LOGICAL_OR
JSTokenTypes.ANDAND -> LOGICAL_AND
else -> UNKNOWN
}
6 changes: 6 additions & 0 deletions src/main/resources/META-INF/codecomplexity-javascript.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<idea-plugin>
<extensions defaultExtensionNs="com.github.nikolaikopernik.codecomplexity">
<languageInfoProvider id="JavaScriptComplexityInfoProvider"
implementation="com.github.nikolaikopernik.codecomplexity.javascript.JSComplexityInfoProvider"/>
</extensions>
</idea-plugin>
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<depends optional="true" config-file="codecomplexity-java.xml">com.intellij.java</depends>
<depends optional="true" config-file="codecomplexity-kotlin.xml">org.jetbrains.kotlin</depends>
<depends optional="true" config-file="codecomplexity-python.xml">com.intellij.modules.python</depends>
<depends optional="true" config-file="codecomplexity-javascript.xml">com.intellij.modules.javascript</depends>

<extensionPoints>
<extensionPoint name="languageInfoProvider"
Expand Down