Skip to content

Commit

Permalink
feat(javascript): add variable provider and utility functions
Browse files Browse the repository at this point in the history
Added a new variable provider class `JSPsiContextVariableProvider` for JavaScript language. This class provides context variables for JavaScript. Also, added utility functions in `JSPsiUtil` and `JSRelevantUtil` to support the variable provider. Updated the plugin XML to include the new variable provider.
  • Loading branch information
phodal committed Sep 14, 2024
1 parent cce91ed commit 1e5ff4e
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ import com.intellij.lang.javascript.psi.stubs.JSImplicitElement
import com.intellij.lang.javascript.psi.util.JSDestructuringUtil
import com.intellij.lang.javascript.psi.util.JSStubBasedPsiTreeUtil
import com.intellij.lang.javascript.psi.util.JSUtils
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiPolyVariantReference
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.parents
import com.phodal.shirecore.project.isInProject
import java.io.File
import java.nio.file.Path

object JSPsiUtil {
fun resolveReference(node: JSReferenceExpression, scope: PsiElement): PsiElement? {
Expand Down Expand Up @@ -79,7 +84,6 @@ object JSPsiUtil {
}
}


fun isExportedFileFunction(element: PsiElement): Boolean {
when (val parent = element.parent) {
is JSFile, is JSEmbeddedContent -> {
Expand Down Expand Up @@ -178,4 +182,103 @@ object JSPsiUtil {
val attributeList = element.attributeList
return attributeList?.accessType == JSAttributeList.AccessType.PRIVATE
}
}

/**
* In JavaScript/TypeScript a testable element is a function, a class or a variable.
*
* Function:
* ```javascript
* function testableFunction() {}
* export testableFunction
* ```
*
* Class:
* ```javascript
* export class TestableClass {}
* ```
*
* Variable:
* ```javascript
* var functionA = function() {}
* export functionA
* ```
*/
fun getElementToTest(psiElement: PsiElement): PsiElement? {
val jsFunc = PsiTreeUtil.getParentOfType(psiElement, JSFunction::class.java, false)
val jsVarStatement = PsiTreeUtil.getParentOfType(psiElement, JSVarStatement::class.java, false)
val jsClazz = PsiTreeUtil.getParentOfType(psiElement, JSClass::class.java, false)

val elementForTests: PsiElement? = when {
jsFunc != null -> jsFunc
jsVarStatement != null -> jsVarStatement
jsClazz != null -> jsClazz
else -> null
}

if (elementForTests == null) return null

return when {
JSPsiUtil.isExportedClassPublicMethod(elementForTests) -> elementForTests
JSPsiUtil.isExportedFileFunction(elementForTests) -> elementForTests
JSPsiUtil.isExportedClass(elementForTests) -> elementForTests
else -> {
null
}
}
}

fun getTestFilePath(element: PsiElement): Path? {
val testDirectory = suggestTestDirectory(element)
if (testDirectory == null) {
logger<JSPsiUtil>().warn("Failed to find test directory for: $element")
return null
}

val containingFile: PsiFile = runReadAction { element.containingFile } ?: return null
val extension = containingFile.virtualFile?.extension ?: return null
val elementName = JSPsiUtil.elementName(element) ?: return null
val testFile: Path = generateUniqueTestFile(elementName, containingFile, testDirectory, extension).toPath()
return testFile
}

/**
* Todo: since in JavaScript has different test framework, we need to find the test directory by the framework.
*/
private fun suggestTestDirectory(element: PsiElement): PsiDirectory? =
ReadAction.compute<PsiDirectory?, Throwable> {
val project: Project = element.project
val elementDirectory = element.containingFile

val parentDir = elementDirectory?.virtualFile?.parent ?: return@compute null
val psiManager = PsiManager.getInstance(project)

val findDirectory = psiManager.findDirectory(parentDir)
if (findDirectory != null) {
return@compute findDirectory
}

val createChildDirectory = parentDir.createChildDirectory(this, "test")
return@compute psiManager.findDirectory(createChildDirectory)
}

private fun generateUniqueTestFile(
elementName: String?,
containingFile: PsiFile,
testDirectory: PsiDirectory,
extension: String,
): File {
val testPath = testDirectory.virtualFile.path
val prefix = elementName ?: containingFile.name.substringBefore('.', "")
val nameCandidate = "$prefix.test.$extension"
var testFile = File(testPath, nameCandidate)

var i = 1
while (testFile.exists()) {
val nameCandidateWithIndex = "$prefix${i}.test.$extension"
i++
testFile = File(testPath, nameCandidateWithIndex)
}

return testFile
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.phodal.shirelang.javascript.util

import com.intellij.lang.javascript.psi.JSFunction
import com.intellij.lang.javascript.psi.ecma6.TypeScriptInterface
import com.intellij.lang.javascript.psi.ecma6.TypeScriptSingleType
import com.intellij.lang.javascript.psi.ecmal4.JSClass
import com.intellij.lang.javascript.psi.util.JSStubBasedPsiTreeUtil
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.psi.PsiElement
import com.phodal.shirecore.codemodel.model.ClassStructure
import com.phodal.shirelang.javascript.codemodel.JavaScriptClassStructureProvider

object JSRelevantUtil {
fun lookupRelevantClass(element: PsiElement): List<ClassStructure> {
return ReadAction.compute<List<ClassStructure>, Throwable> {
val elements = mutableListOf<ClassStructure>()
when (element) {
is JSClass -> {
element.functions.map {
elements += resolveByFunction(it).values
}
}

is JSFunction -> {
elements += resolveByFunction(element).values
}

else -> {}
}

return@compute elements
}
}

private fun resolveByFunction(jsFunction: JSFunction): Map<String, ClassStructure> {
val result = mutableMapOf<String, ClassStructure>()
jsFunction.parameterList?.parameters?.map {
it.typeElement?.let { typeElement ->
result += resolveByType(typeElement, it.typeElement!!.text)
}
}

result += jsFunction.returnTypeElement?.let {
resolveByType(it, jsFunction.returnType!!.resolvedTypeText)
} ?: emptyMap()

return result
}

private fun resolveByType(
returnType: PsiElement?,
typeName: String,
): MutableMap<String, ClassStructure> {
val result = mutableMapOf<String, ClassStructure>()
when (returnType) {
is TypeScriptSingleType -> {
val resolveReferenceLocally = JSStubBasedPsiTreeUtil.resolveLocally(
typeName,
returnType
)

when (resolveReferenceLocally) {
is TypeScriptInterface -> {
JavaScriptClassStructureProvider().build(resolveReferenceLocally, false)?.let {
result += mapOf(typeName to it)
}
}

else -> {
logger<JSRelevantUtil>().warn("resolveReferenceLocally is not TypeScriptInterface: $resolveReferenceLocally")
}
}
}

else -> {
logger<JSRelevantUtil>().warn("returnType is not TypeScriptSingleType: $returnType")
}
}

return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.phodal.shirelang.javascript.variable

import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.lang.javascript.psi.JSFile
import com.intellij.lang.javascript.psi.JSFunction
import com.intellij.lang.javascript.psi.ecmal4.JSImportStatement
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.oracle.truffle.js.runtime.builtins.JSClass
import com.phodal.shirecore.provider.variable.PsiContextVariableProvider
import com.phodal.shirecore.provider.variable.model.PsiContextVariable
import com.phodal.shirelang.javascript.codemodel.JavaScriptClassStructureProvider
import com.phodal.shirelang.javascript.codemodel.JavaScriptMethodStructureProvider
import com.phodal.shirelang.javascript.util.JSPsiUtil
import com.phodal.shirelang.javascript.util.JSRelevantUtil

class JSPsiContextVariableProvider : PsiContextVariableProvider {
override fun resolve(variable: PsiContextVariable, project: Project, editor: Editor, psiElement: PsiElement?): Any {
if (psiElement?.language !is JavascriptLanguage) return ""
val underTestElement = JSPsiUtil.getElementToTest(psiElement) ?: return ""
val sourceFile = underTestElement.containingFile as? JSFile ?: return ""

return when (variable) {
PsiContextVariable.CURRENT_CLASS_NAME -> {
// when (underTestElement) {
// is JSClass -> underTestElement.getName() ?: ""
// else -> ""
// }
""
}

PsiContextVariable.CURRENT_CLASS_CODE -> {
val underTestObj = JavaScriptClassStructureProvider()
.build(underTestElement, false)?.format()

if (underTestObj == null) {
val funcObj = JavaScriptMethodStructureProvider()
.build(underTestElement, false, false)?.format()

funcObj ?: ""
} else {
underTestObj
}

}

PsiContextVariable.CURRENT_METHOD_NAME -> {
when (underTestElement) {
is JSFunction -> underTestElement.name ?: ""
else -> ""
}
}

PsiContextVariable.CURRENT_METHOD_CODE -> {
when (underTestElement) {
is JSFunction -> underTestElement.text ?: ""
else -> ""
}
}

PsiContextVariable.RELATED_CLASSES -> JSRelevantUtil.lookupRelevantClass(underTestElement)
PsiContextVariable.SIMILAR_TEST_CASE -> ""
PsiContextVariable.IMPORTS -> PsiTreeUtil.findChildrenOfType(sourceFile, JSImportStatement::class.java)
.map { it.text }

PsiContextVariable.IS_NEED_CREATE_FILE -> TODO()
PsiContextVariable.TARGET_TEST_FILE_NAME -> JSPsiUtil.getTestFilePath(psiElement) ?: ""
PsiContextVariable.UNDER_TEST_METHOD_CODE -> TODO()
PsiContextVariable.FRAMEWORK_CONTEXT -> TODO()
PsiContextVariable.CODE_SMELL -> TODO()
PsiContextVariable.METHOD_CALLER -> TODO()
PsiContextVariable.CALLED_METHOD -> TODO()
PsiContextVariable.SIMILAR_CODE -> TODO()
PsiContextVariable.STRUCTURE -> TODO()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,9 @@
<shireAutoTesting language="TypeScript"
implementationClass="com.phodal.shirelang.javascript.codeedit.JSAutoTestingService"/>

<shirePsiVariableProvider
language="TypeScript"
implementationClass="com.phodal.shirelang.javascript.variable.JSPsiContextVariableProvider"/>

</extensions>
</idea-plugin>

0 comments on commit 1e5ff4e

Please sign in to comment.