Skip to content

Commit

Permalink
feat(javascript): add ReactUtil and ReactUtilTest #81
Browse files Browse the repository at this point in the history
This commit adds the `ReactUtil` object, which contains functions for extracting React components from JavaScript files. It also includes the corresponding unit test `ReactUtilTest` to ensure the functionality of the `ReactUtil` methods.
  • Loading branch information
phodal committed Jan 25, 2024
1 parent 146ea56 commit d1323f9
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 72 deletions.
@@ -1,23 +1,19 @@
package cc.unitmesh.ide.javascript.flow

import cc.unitmesh.ide.javascript.util.JSPsiUtil
import cc.unitmesh.ide.javascript.util.ReactUtil
import com.intellij.lang.ecmascript6.JSXHarmonyFileType
import com.intellij.lang.javascript.JavaScriptFileType
import com.intellij.lang.javascript.TypeScriptJSXFileType
import com.intellij.lang.javascript.dialects.TypeScriptJSXLanguageDialect
import com.intellij.lang.javascript.psi.JSFile
import com.intellij.lang.javascript.psi.ecma6.TypeScriptClass
import com.intellij.lang.javascript.psi.ecma6.TypeScriptFunction
import com.intellij.lang.javascript.psi.ecma6.TypeScriptFunctionExpression
import com.intellij.lang.javascript.psi.ecma6.TypeScriptVariable
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import com.intellij.psi.util.PsiTreeUtil
// keep this import
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

enum class RouterFile(val filename: String) {
Expand Down Expand Up @@ -85,7 +81,7 @@ class ReactAutoPage(
override fun getPages(): List<DsComponent> = pages.mapNotNull {
when (it.language) {
is TypeScriptJSXLanguageDialect -> {
Companion.tsxComponentToComponent(it)
ReactUtil.tsxComponentToComponent(it)
}

else -> null
Expand Down Expand Up @@ -124,45 +120,5 @@ class ReactAutoPage(
override fun clarify(): String {
TODO("Not yet implemented")
}
}

companion object {
fun tsxComponentToComponent(jsFile: JSFile): List<DsComponent> {
val exportElements = JSPsiUtil.getExportElements(jsFile)
return exportElements.map { psiElement ->
val name = psiElement.name ?: return@map null
val path = jsFile.virtualFile.canonicalPath ?: return@map null
// is React Functional Component
return@map when (psiElement) {
is TypeScriptFunction -> {
DsComponent(name = name, path)
}

is TypeScriptClass -> {
DsComponent(name = name, path)
}

is TypeScriptVariable -> {
val funcExpr = PsiTreeUtil
.findChildrenOfType(psiElement, TypeScriptFunctionExpression::class.java)
.firstOrNull() ?: return@map null

val map = funcExpr.parameterList?.parameters?.mapNotNull { parameter ->
if (parameter.typeElement != null) {
parameter.typeElement
} else {
null
}
} ?: emptyList()

DsComponent(name = name, path)
}

else -> {
println("unknown type: ${psiElement::class.java}")
null
}
}
}.filterNotNull()
}
}
}
Expand Up @@ -57,30 +57,6 @@ object JSPsiUtil {
}
}

fun getExportElements(file: JSFile): List<PsiNameIdentifierOwner> {
val exportDeclarations =
PsiTreeUtil.getChildrenOfTypeAsList(file, ES6ExportDeclaration::class.java)

val map = exportDeclarations.map { exportDeclaration ->
exportDeclaration.exportSpecifiers
.asSequence()
.mapNotNull {
it.alias?.findAliasedElement()
}
.filterIsInstance<PsiNameIdentifierOwner>()
.toList()
}.flatten()

val defaultAssignments = PsiTreeUtil.getChildrenOfTypeAsList(file, ES6ExportDefaultAssignment::class.java)
val defaultAssignment = defaultAssignments.mapNotNull {
val jsReferenceExpression = it.expression as? JSReferenceExpression ?: return@mapNotNull null
val resolveReference = JSResolveResult.resolveReference(jsReferenceExpression)
resolveReference.firstOrNull() as? PsiNameIdentifierOwner
}

return map + defaultAssignment
}

private fun skipDeclaration(element: PsiElement): Boolean {
return when (element) {
is JSParameter, is TypeScriptGenericOrMappedTypeParameter -> true
Expand Down
@@ -0,0 +1,94 @@
package cc.unitmesh.ide.javascript.util

import cc.unitmesh.ide.javascript.flow.DsComponent
import com.intellij.lang.ecmascript6.psi.ES6ExportDeclaration
import com.intellij.lang.ecmascript6.psi.ES6ExportDefaultAssignment
import com.intellij.lang.javascript.psi.JSFile
import com.intellij.lang.javascript.psi.JSFunctionExpression
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.lang.javascript.psi.JSVariable
import com.intellij.lang.javascript.psi.ecma6.TypeScriptClass
import com.intellij.lang.javascript.psi.ecma6.TypeScriptFunction
import com.intellij.lang.javascript.psi.ecma6.TypeScriptFunctionExpression
import com.intellij.lang.javascript.psi.ecma6.TypeScriptVariable
import com.intellij.lang.javascript.psi.resolve.JSResolveResult
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.util.PsiTreeUtil

object ReactUtil {
private fun getExportElements(file: JSFile): List<PsiNameIdentifierOwner> {
val exportDeclarations =
PsiTreeUtil.getChildrenOfTypeAsList(file, ES6ExportDeclaration::class.java)

val map = exportDeclarations.map { exportDeclaration ->
exportDeclaration.exportSpecifiers
.asSequence()
.mapNotNull {
it.alias?.findAliasedElement()
}
.filterIsInstance<PsiNameIdentifierOwner>()
.toList()
}.flatten()

val defaultAssignments = PsiTreeUtil.getChildrenOfTypeAsList(file, ES6ExportDefaultAssignment::class.java)
val defaultAssignment = defaultAssignments.mapNotNull {
val jsReferenceExpression = it.expression as? JSReferenceExpression ?: return@mapNotNull null
val resolveReference = JSResolveResult.resolveReference(jsReferenceExpression)
resolveReference.firstOrNull() as? PsiNameIdentifierOwner
}

return map + defaultAssignment
}

fun tsxComponentToComponent(jsFile: JSFile): List<DsComponent> {
val exportElements = getExportElements(jsFile)
return exportElements.map { psiElement ->
val name = psiElement.name ?: return@map null
val path = jsFile.virtualFile.canonicalPath ?: return@map null
return@map when (psiElement) {
is TypeScriptFunction -> {
DsComponent(name = name, path)
}

is TypeScriptClass -> {
DsComponent(name = name, path)
}

is TypeScriptVariable -> {
val funcExpr = PsiTreeUtil.findChildrenOfType(psiElement, TypeScriptFunctionExpression::class.java)
.firstOrNull() ?: return@map null

val map = funcExpr.parameterList?.parameters?.mapNotNull { parameter ->
if (parameter.typeElement != null) {
parameter.typeElement
} else {
null
}
} ?: emptyList()

DsComponent(name = name, path)
}

is JSVariable -> {
val funcExpr = PsiTreeUtil.findChildrenOfType(psiElement, JSFunctionExpression::class.java)
.firstOrNull() ?: return@map null

val map = funcExpr.parameterList?.parameters?.mapNotNull { parameter ->
if (parameter.typeElement != null) {
parameter.typeElement
} else {
null
}
} ?: emptyList()

DsComponent(name = name, path)
}

else -> {
println("unknown type: ${psiElement::class.java}")
null
}
}
}.filterNotNull()
}
}
@@ -0,0 +1,28 @@
package cc.unitmesh.ide.javascript.util;

import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.lang.javascript.dialects.TypeScriptJSXLanguageDialect
import com.intellij.lang.javascript.dialects.TypeScriptLanguageDialect
import com.intellij.lang.javascript.psi.JSFile
import com.intellij.lang.javascript.psi.ecmal4.JSClass
import com.intellij.psi.PsiFileFactory
import com.intellij.testFramework.LightPlatformTestCase

class ReactUtilTest : LightPlatformTestCase() {
fun testShouldHandleExportReactComponent() {
val code = """
import type { AppProps } from 'next/app';
const MyApp = ({ Component, pageProps }: AppProps) => (
<Component {...pageProps} />
);
export default MyApp;
""".trimIndent()

val file = PsiFileFactory.getInstance(project).createFileFromText(JavascriptLanguage.INSTANCE, code)
val result = ReactUtil.tsxComponentToComponent(file as JSFile)

assertEquals(1, result.size)
}
}

0 comments on commit d1323f9

Please sign in to comment.