Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Implemented project-wide import/exports manager
  • Loading branch information
zishkaz committed Apr 5, 2023
commit 4467a9e0e7364f43cfe8df061d164c364251912c
65 changes: 44 additions & 21 deletions utbot-js/src/main/kotlin/api/JsTestGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import codegen.JsCodeGenerator
import com.google.javascript.rhino.Node
import framework.api.js.JsClassId
import framework.api.js.JsMethodId
import framework.api.js.JsMultipleClassId
import framework.api.js.JsUtFuzzedExecution
import framework.api.js.util.isClass
import framework.api.js.util.isJsArray
Expand Down Expand Up @@ -62,6 +63,7 @@ import utils.constructClass
import utils.data.ResultData
import utils.toJsAny
import java.io.File
import java.nio.file.Paths
import java.util.concurrent.CancellationException
import settings.JsExportsSettings.endComment
import settings.JsExportsSettings.startComment
Expand Down Expand Up @@ -103,19 +105,27 @@ class JsTestGenerator(
*/
fun run(): String {
parsedFile = runParser(fileText)
astScrapper = JsAstScrapper(parsedFile, sourceFilePath)
val packageJson = PackageJsonService(
sourceFilePath,
File(projectPath),
).findClosestConfig()
val moduleType = ModuleType.fromPackageJson(packageJson)
astScrapper = JsAstScrapper(
parsedFile,
sourceFilePath,
Paths.get("$projectPath/$utbotDir"),
moduleType,
settings
)
val context = ServiceContext(
utbotDir = utbotDir,
projectPath = projectPath,
filePathToInference = astScrapper.filesToInfer,
parsedFile = parsedFile,
settings = settings,
importsMap = astScrapper.importsMap
importsMap = astScrapper.importsMap,
packageJson = packageJson
)
context.packageJson = PackageJsonService(
sourceFilePath,
File(projectPath),
).findClosestConfig()
val paramNames = mutableMapOf<ExecutableId, List<String>>()
val testSets = mutableListOf<CgMethodTestSet>()
val classNode =
Expand All @@ -131,28 +141,40 @@ class JsTestGenerator(
methods.forEach { funcNode ->
makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames)
}
val importPrefix = makeImportPrefix()
val moduleType = ModuleType.fromPackageJson(context.packageJson)
val imports = listOf(
val imports = context.necessaryImports.makeImportsForCodegen(moduleType)
val codeGen = JsCodeGenerator(
classUnderTest = classId,
paramNames = paramNames,
imports = imports
)
return codeGen.generateAsStringWithTestReport(testSets).generatedCode
}

private fun Map<String, Node>.makeImportsForCodegen(moduleType: ModuleType): List<JsImport> {
val baseImports = listOf(
JsImport(
"*",
fileUnderTestAliases,
"./$importPrefix/${sourceFilePath.substringAfterLast("/")}",
"assert",
"assert",
moduleType
),
JsImport(
"*",
"assert",
"assert",
fileUnderTestAliases,
"./${makeImportPrefix()}/${sourceFilePath.substringAfterLast("/")}",
moduleType
)
)
val codeGen = JsCodeGenerator(
classUnderTest = classId,
paramNames = paramNames,
imports = imports
)
return codeGen.generateAsStringWithTestReport(testSets).generatedCode
return baseImports + this.map { (key, value) ->
JsImport(
key,
key,
outputFilePath?.let { path ->
PathResolver.getRelativePath(File(path).parent, value.sourceFileName!!)
} ?: "",
moduleType
)
}
}

private fun makeTestsForMethod(
Expand Down Expand Up @@ -393,9 +415,10 @@ class JsTestGenerator(

private fun JsClassId.collectExportsRecursively(): List<String> {
return when {
this.isClass -> listOf(this.name) + (this.constructor?.parameters ?: emptyList())
this.isClass && !astScrapper.importsMap.contains(this.name) ->
listOf(this.name) + (this.constructor?.parameters ?: emptyList())
.flatMap { it.collectExportsRecursively() }

this is JsMultipleClassId -> this.classIds.flatMap { it.collectExportsRecursively() }
this.isJsArray -> (this.elementClassId as? JsClassId)?.collectExportsRecursively() ?: emptyList()
else -> emptyList()
}
Expand Down
4 changes: 2 additions & 2 deletions utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ val JsClassId.isJsBasic: Boolean
get() = this in jsBasic

val JsClassId.isExportable: Boolean
get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure)
get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure || this is JsMultipleClassId)

val JsClassId.isClass: Boolean
get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure)
get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure || this is JsMultipleClassId)

val JsClassId.isUndefined: Boolean
get() = this == jsUndefinedClassId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package framework.codegen.model.constructor.visitor

import framework.api.js.JsClassId
import framework.api.js.util.isExportable
import framework.codegen.JsImport
import framework.codegen.ModuleType
import org.apache.commons.text.StringEscapeUtils
Expand Down Expand Up @@ -50,7 +48,6 @@ import org.utbot.framework.codegen.renderer.CgAbstractRenderer
import org.utbot.framework.codegen.renderer.CgPrinter
import org.utbot.framework.codegen.renderer.CgPrinterImpl
import org.utbot.framework.codegen.renderer.CgRendererContext
import org.utbot.framework.codegen.services.language.isLanguageKeyword
import org.utbot.framework.codegen.tree.VisibilityModifier
import org.utbot.framework.plugin.api.BuiltinMethodId
import org.utbot.framework.plugin.api.ClassId
Expand Down Expand Up @@ -257,19 +254,24 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP
}

override fun visit(element: CgConstructorCall) {
val importPrefix = "$fileUnderTestAliases.".takeIf {
(element.executableId.classId as JsClassId).isExportable
} ?: ""
print("new $importPrefix${element.executableId.classId.name}")
print("new ${element.executableId.classId.name}")
print("(")
element.arguments.renderSeparated()
print(")")
}

private fun renderImport(import: JsImport) = with(import) {
when (type) {
ModuleType.COMMONJS -> println("const $aliases = require(\"$path\")")
ModuleType.MODULE -> println("import $name as $aliases from \"$path\"")
ModuleType.COMMONJS -> {
if (name == "*") {
println("const $aliases = require (\"$path\")")
} else println("const {$aliases} = require(\"$path\")")
}
ModuleType.MODULE -> {
if (name == "*") {
println("import $name as $aliases from \"$path\"")
} else println("import {$name as $aliases} from \"$path\"")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ object MultipleValueProvider : ValueProvider<JsClassId, UtModel, JsMethodDescrip

override fun generate(description: JsMethodDescription, type: JsClassId): Sequence<Seed<JsClassId, UtModel>> =
sequence {
(type as JsMultipleClassId).classIds.zip(defaultValueProviders()).forEach { (classId, provider) ->
if (provider.accept(classId)) {
provider.generate(description, classId)
for (classId in (type as JsMultipleClassId).classIds) {
for (provider in defaultValueProviders()) {
if (provider.accept(classId)) {
yieldAll(provider.generate(description, classId))
}
}
}
}
Expand Down
106 changes: 73 additions & 33 deletions utbot-js/src/main/kotlin/parser/JsAstScrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import com.google.javascript.jscomp.Compiler
import com.google.javascript.jscomp.NodeUtil
import com.google.javascript.jscomp.SourceFile
import com.google.javascript.rhino.Node
import framework.codegen.ModuleType
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import mu.KotlinLogging
import org.json.JSONObject
import parser.JsParserUtils.getAbstractFunctionName
import parser.JsParserUtils.getClassMethods
import parser.JsParserUtils.getImportSpecAliases
Expand All @@ -21,13 +23,19 @@ import parser.visitors.IAstVisitor
import parser.visitors.JsClassAstVisitor
import parser.visitors.JsFunctionAstVisitor
import parser.visitors.JsVariableAstVisitor
import settings.JsDynamicSettings
import utils.JsCmdExec
import utils.PathResolver
import kotlin.io.path.pathString

private val logger = KotlinLogging.logger {}

class JsAstScrapper(
private val parsedFile: Node,
private val basePath: String,
private val tempUtbotPath: Path,
private val moduleType: ModuleType,
private val settings: JsDynamicSettings
) {

// Used not to parse the same file multiple times.
Expand All @@ -46,7 +54,7 @@ class JsAstScrapper(
processedFiles.add(Paths.get(this.sourceFileName!!))
val vis = Visitor()
vis.accept(this)
val kek = vis.importNodes.flatMap { node ->
return vis.importNodes.flatMap { node ->
val temp = node.importedNodes()
temp.toList() + temp.flatMap { entry ->
val path = Paths.get(entry.value.sourceFileName!!)
Expand All @@ -55,32 +63,9 @@ class JsAstScrapper(
} else emptyList()
}
}
return kek
}
this.putAll(parsedFile.collectImportsRec().toMap())
this.toMap()
// val res = visitor.importNodes.fold(emptyMap<String, Node>()) { acc, node ->
// val currAcc = acc.toList().toTypedArray()
//
// return kek
//
//
// val temp = this.importedNodes()
// return temp.toList() + temp.flatMap { entry ->
// val path = entry.value.sourceFileName!!
// val pFile = File(path).parseIfNecessary()
// // Not to search for imports in already analyzed files
// _parsedFilesCache[Paths.get(path)]?.let {
// emptyList()
// } ?: File(path).parseIfNecessary().collectImportsRec()
// }
// }
//
// val more = node.collectImportsRec().toTypedArray()
// mapOf(*currAcc, *more)
// }
// this.putAll(res)
// this.toMap()
}
}

Expand Down Expand Up @@ -130,18 +115,69 @@ class JsAstScrapper(

private fun Node.importedNodes(): Map<String, Node> {
return when {
this.isRequireImport() -> mapOf(
this.parent!!.string to (makePathFromImport(this.getRequireImportText())?.let {
File(it).parseIfNecessary().findEntityInFile(null)
// Workaround for std imports.
} ?: this.firstChild!!.next!!)
)

this.isRequireImport() -> this.processRequireImport()
this.isImport -> this.processModuleImport()
else -> emptyMap()
}
}

@Suppress("unchecked_cast")
private fun getExportedKeys(importPath: String): List<String> {
// val importPath = PathResolver.getRelativePath(pathToTempFile.pathString, basePath)
val pathToTempFile = Paths.get(tempUtbotPath.pathString + "/exp_temp.js")
val text = buildString {
when (moduleType) {
ModuleType.COMMONJS -> {
appendLine("const temp = require(\"$importPath\")")
appendLine("const fs = require(\"fs\")")
}

else -> {
appendLine("import * as temp from \"$importPath\"")
appendLine("import * as fs from \"fs\"")
}
}
appendLine("fs.writeFileSync(\"exp_temp.json\", JSON.stringify({files: Object.keys(temp)}))")
}
pathToTempFile.toFile().writeText(text)
JsCmdExec.runCommand(
cmd = arrayOf("\"${settings.pathToNode}\"", pathToTempFile.pathString),
dir = pathToTempFile.parent.pathString,
shouldWait = true,
timeout = settings.timeout,
)
val pathToJson = pathToTempFile.pathString.replace(".js", ".json")
return JSONObject(File(pathToJson).readText()).getJSONArray("files").toList() as List<String>
}

private fun Node.processRequireImport(): Map<String, Node> {
try {
val pathToFile = makePathFromImport(this.getRequireImportText()) ?: return emptyMap()
val pFile = File(pathToFile).parseIfNecessary()
val objPattern = NodeUtil.findPreorder(this.parent, { it.isObjectPattern }, { true })
return when {
objPattern != null -> {
buildList {
var currNode: Node? = objPattern.firstChild!!
while (currNode != null) {
add(currNode.string)
currNode = currNode.next
}
}.associateWith { key -> pFile.findEntityInFile(key) }
}
else -> {
val importPath = PathResolver.getRelativePath(tempUtbotPath.pathString, pathToFile)
getExportedKeys(importPath).associateWith { key ->
pFile.findEntityInFile(key)
}
}
}
} catch (e: ClassNotFoundException) {
logger.error { e.toString() }
return emptyMap()
}
}

private fun Node.processModuleImport(): Map<String, Node> {
try {
val pathToFile = makePathFromImport(this.getModuleImportText()) ?: return emptyMap()
Expand All @@ -156,16 +192,20 @@ class JsAstScrapper(
}

NodeUtil.findPreorder(this, { it.isImportStar }, { true }) != null -> {
// Do we need to know alias for "*" import?
val aliases = this.getImportSpecAliases()
mapOf(aliases to pFile)
val importPath = PathResolver.getRelativePath(tempUtbotPath.pathString, pathToFile)
getExportedKeys(importPath).associateWith { key ->
pFile.findEntityInFile(key)
}
}
// For example: import foo from "bar"
else -> {
val realName = this.getImportSpecName()
mapOf(realName to pFile.findEntityInFile(realName))
}
}
} catch (e: Exception) {
} catch (e: ClassNotFoundException) {
logger.error { e.toString() }
return emptyMap()
}
Expand Down
6 changes: 5 additions & 1 deletion utbot-js/src/main/kotlin/parser/JsParserUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ object JsParserUtils {
*
* Returns variable name as [String].
*/
fun Node.getVariableName(): String = this.firstChild!!.string
fun Node.getVariableName(): String? = try {
this.firstChild!!.string
} catch (_: Exception) {
null
}

/**
* Called upon any variable declaration node.
Expand Down
Loading