Add duplicate class name support to the AST deobfuscator

Signed-off-by: Graham <gpe@openrs2.dev>
pull/132/head
Graham 4 years ago
parent f9021f7fe6
commit a87a289c49
  1. 1
      deob-ast/build.gradle.kts
  2. 61
      deob-ast/src/main/java/dev/openrs2/deob/ast/AstDeobfuscator.kt
  3. 13
      deob-ast/src/main/java/dev/openrs2/deob/ast/DeobfuscateAstCommand.kt
  4. 87
      deob-ast/src/main/java/dev/openrs2/deob/ast/Library.kt
  5. 13
      deob-ast/src/main/java/dev/openrs2/deob/ast/LibraryGroup.kt
  6. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/AddSubTransformer.kt
  7. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/BinaryExprOrderTransformer.kt
  8. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/BitMaskTransformer.kt
  9. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/ComplementTransformer.kt
  10. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/EncloseTransformer.kt
  11. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/ForLoopConditionTransformer.kt
  12. 22
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/GlTransformer.kt
  13. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/IdentityTransformer.kt
  14. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/IfElseTransformer.kt
  15. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/IncrementTransformer.kt
  16. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/NegativeLiteralTransformer.kt
  17. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/NewInstanceTransformer.kt
  18. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/TernaryTransformer.kt
  19. 20
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/Transformer.kt
  20. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/UnencloseTransformer.kt
  21. 7
      deob-ast/src/main/java/dev/openrs2/deob/ast/transform/ValueOfTransformer.kt

@ -11,6 +11,7 @@ application {
dependencies {
api("com.github.ajalt:clikt:${Versions.clikt}")
implementation(project(":deob-util"))
implementation(project(":util"))
implementation("com.github.javaparser:javaparser-symbol-solver-core:${Versions.javaParser}")
implementation("com.google.guava:guava:${Versions.guava}")

@ -1,18 +1,8 @@
package dev.openrs2.deob.ast
import com.github.javaparser.ParserConfiguration
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.printer.PrettyPrinter
import com.github.javaparser.printer.PrettyPrinterConfiguration
import com.github.javaparser.symbolsolver.JavaSymbolSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver
import com.github.javaparser.utils.SourceRoot
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.deob.ast.transform.Transformer
import java.nio.file.Path
import java.util.function.Function
import dev.openrs2.deob.util.Module
import javax.inject.Inject
import javax.inject.Singleton
@ -20,59 +10,18 @@ import javax.inject.Singleton
class AstDeobfuscator @Inject constructor(
private val transformers: Set<@JvmSuppressWildcards Transformer>
) {
fun run(modules: List<Path>) {
val solver = CombinedTypeSolver(ClassLoaderTypeSolver(ClassLoader.getPlatformClassLoader()))
for (module in modules) {
solver.add(JavaParserTypeSolver(module))
}
val config = ParserConfiguration()
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11)
.setSymbolResolver(JavaSymbolSolver(solver))
val printerConfig = PrettyPrinterConfiguration()
.setIndentType(PrettyPrinterConfiguration.IndentType.TABS_WITH_SPACE_ALIGN)
.setIndentSize(1)
.setIndentCaseInSwitch(false)
.setOrderImports(true)
val printer = PrettyPrinter(printerConfig)
val roots = modules.map { SourceRoot(it, config) }
val units = mutableMapOf<String, CompilationUnit>()
for (root in roots) {
logger.info { "Parsing root ${root.root}" }
val results = root.tryToParseParallelized()
for (result in results) {
require(result.isSuccessful) { result }
}
for (unit in root.compilationUnits) {
val name = unit.primaryType.get().fullyQualifiedName.get()
units[name] = unit
}
}
fun run(modules: Set<Module>) {
val group = LibraryGroup(modules.map(Library.Companion::parse))
for (transformer in transformers) {
logger.info { "Running transformer ${transformer.javaClass.simpleName}" }
transformer.transform(units)
transformer.transform(group)
}
for (root in roots) {
logger.info { "Saving root ${root.root}" }
root.printer = Function<CompilationUnit, String>(printer::print).andThen(::stripNewlineAfterPcAnnotation)
root.saveAll()
}
group.forEach(Library::save)
}
private companion object {
private val logger = InlineLogger()
private val PC_ANNOTATION_REGEX = Regex("@Pc\\(([0-9]+)\\)\\s+")
private fun stripNewlineAfterPcAnnotation(s: String): String {
return s.replace(PC_ANNOTATION_REGEX, "@Pc($1) ")
}
}
}

@ -2,7 +2,7 @@ package dev.openrs2.deob.ast
import com.github.ajalt.clikt.core.CliktCommand
import com.google.inject.Guice
import java.nio.file.Paths
import dev.openrs2.deob.util.Module
fun main(args: Array<String>) = DeobfuscateAstCommand().main(args)
@ -10,15 +10,6 @@ class DeobfuscateAstCommand : CliktCommand(name = "deob-ast") {
override fun run() {
val injector = Guice.createInjector(AstDeobfuscatorModule)
val deobfuscator = injector.getInstance(AstDeobfuscator::class.java)
deobfuscator.run(
listOf(
Paths.get("nonfree/client/src/main/java"),
Paths.get("nonfree/gl/src/main/java"),
Paths.get("nonfree/loader/src/main/java"),
Paths.get("nonfree/signlink/src/main/java"),
Paths.get("nonfree/unpack/src/main/java"),
Paths.get("nonfree/unpackclass/src/main/java")
)
)
deobfuscator.run(Module.all)
}
}

@ -0,0 +1,87 @@
package dev.openrs2.deob.ast
import com.github.javaparser.ParserConfiguration
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.printer.PrettyPrinter
import com.github.javaparser.printer.PrettyPrinterConfiguration
import com.github.javaparser.symbolsolver.JavaSymbolSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver
import com.github.javaparser.utils.SourceRoot
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.deob.util.Module
import java.util.function.Function
class Library(
val name: String,
private val root: SourceRoot
) : Iterable<CompilationUnit> {
private val units = mutableMapOf<String, CompilationUnit>()
init {
for (unit in root.compilationUnits) {
val name = unit.primaryType.get().fullyQualifiedName.get()
units[name] = unit
}
}
operator fun get(name: String): CompilationUnit? {
return units[name]
}
override fun iterator(): Iterator<CompilationUnit> {
return units.values.iterator()
}
fun save() {
logger.info { "Saving root ${root.root}" }
root.saveAll()
}
companion object {
private val logger = InlineLogger()
private val PC_ANNOTATION_REGEX = Regex("@Pc\\(([0-9]+)\\)\\s+")
private val printer = Function<CompilationUnit, String>(
PrettyPrinter(
PrettyPrinterConfiguration()
.setIndentType(PrettyPrinterConfiguration.IndentType.TABS_WITH_SPACE_ALIGN)
.setIndentSize(1)
.setIndentCaseInSwitch(false)
.setOrderImports(true)
)::print
).andThen(::stripNewlineAfterPcAnnotation)
fun parse(module: Module): Library {
logger.info { "Parsing root ${module.sources}" }
val solver = CombinedTypeSolver(
ClassLoaderTypeSolver(ClassLoader.getPlatformClassLoader()),
JavaParserTypeSolver(module.sources)
)
for (dependency in module.transitiveDependencies) {
solver.add(JavaParserTypeSolver(dependency.sources))
}
val config = ParserConfiguration()
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11)
.setSymbolResolver(JavaSymbolSolver(solver))
val root = SourceRoot(module.sources, config)
root.printer = printer
val results = root.tryToParseParallelized()
for (result in results) {
require(result.isSuccessful) { result }
}
return Library(module.name, root)
}
private fun stripNewlineAfterPcAnnotation(s: String): String {
return s.replace(PC_ANNOTATION_REGEX, "@Pc($1) ")
}
}
}

@ -0,0 +1,13 @@
package dev.openrs2.deob.ast
class LibraryGroup(libraries: Iterable<Library>) : Iterable<Library> {
private val libraries = libraries.associateBy { it.name }
operator fun get(name: String): Library? {
return libraries[name]
}
override fun iterator(): Iterator<Library> {
return libraries.values.iterator()
}
}

@ -6,6 +6,8 @@ import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.IntegerLiteralExpr
import com.github.javaparser.ast.expr.LongLiteralExpr
import com.github.javaparser.ast.expr.UnaryExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.hasSideEffects
import dev.openrs2.deob.ast.util.isString
import dev.openrs2.deob.ast.util.negate
@ -14,10 +16,7 @@ import javax.inject.Singleton
@Singleton
class AddSubTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: BinaryExpr ->
val op = expr.operator
val left = expr.left

@ -2,16 +2,15 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.BinaryExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.isString
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class BinaryExprOrderTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: BinaryExpr ->
val op = expr.operator.flip() ?: return@walk

@ -3,6 +3,8 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.expr.IntegerLiteralExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.checkedAsInt
import dev.openrs2.deob.ast.util.checkedAsLong
import dev.openrs2.deob.ast.util.isIntegerOrLongLiteral
@ -12,10 +14,7 @@ import javax.inject.Singleton
@Singleton
class BitMaskTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: BinaryExpr ->
val shiftOp = expr.operator
val left = expr.left

@ -5,6 +5,8 @@ import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.IntegerLiteralExpr
import com.github.javaparser.ast.expr.UnaryExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.checkedAsInt
import dev.openrs2.deob.ast.util.checkedAsLong
import dev.openrs2.deob.ast.util.isIntegerOrLongLiteral
@ -14,10 +16,7 @@ import javax.inject.Singleton
@Singleton
class ComplementTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: BinaryExpr ->
val op = complement(expr.operator) ?: return@walk

@ -4,6 +4,8 @@ import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.expr.EnclosedExpr
import com.github.javaparser.ast.expr.Expression
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@ -73,10 +75,7 @@ class EncloseTransformer : Transformer() {
}
}
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: Expression ->
when {
expr.isArrayAccessExpr -> {

@ -3,16 +3,15 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.stmt.ForStmt
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.hasSideEffects
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class ForLoopConditionTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { stmt: ForStmt ->
stmt.compare.ifPresent { compare ->
if (!compare.isBinaryExpr) {

@ -19,6 +19,8 @@ import com.github.javaparser.ast.type.PrimitiveType
import com.github.javaparser.resolution.types.ResolvedPrimitiveType
import com.github.javaparser.resolution.types.ResolvedType
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.gl.GlCommand
import dev.openrs2.deob.ast.gl.GlEnum
import dev.openrs2.deob.ast.gl.GlParameter
@ -31,16 +33,15 @@ import javax.inject.Singleton
@Singleton
class GlTransformer @Inject constructor(private val registry: GlRegistry) : Transformer() {
private val enums = mutableSetOf<GlEnum>()
private var glUnit: CompilationUnit? = null
override fun preTransform(units: Map<String, CompilationUnit>) {
override fun preTransform(group: LibraryGroup) {
enums.clear()
glUnit = group["gl"]?.get(GL_CLASS)
}
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
if (!units.containsKey(GL_CLASS)) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
if (glUnit == null) {
return
}
@ -175,9 +176,12 @@ class GlTransformer @Inject constructor(private val registry: GlRegistry) : Tran
}
}
override fun postTransform(units: Map<String, CompilationUnit>) {
val glUnit = units[GL_CLASS] ?: return
val glInterface = glUnit.primaryType.get()
override fun postTransform(group: LibraryGroup) {
if (glUnit == null) {
return
}
val glInterface = glUnit!!.primaryType.get()
// add missing fields
val fields = enums.filter { glInterface.getFieldByName(it.name).isEmpty }

@ -4,15 +4,14 @@ import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.UnaryExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class IdentityTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: BinaryExpr ->
@Suppress("NON_EXHAUSTIVE_WHEN")
when (expr.operator) {

@ -4,6 +4,8 @@ import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.stmt.BlockStmt
import com.github.javaparser.ast.stmt.IfStmt
import com.github.javaparser.ast.stmt.Statement
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.countNots
import dev.openrs2.deob.ast.util.not
import dev.openrs2.deob.ast.util.walk
@ -11,10 +13,7 @@ import javax.inject.Singleton
@Singleton
class IfElseTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { stmt: IfStmt ->
stmt.elseStmt.ifPresent { elseStmt: Statement ->
val condition = stmt.condition

@ -4,15 +4,14 @@ import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.UnaryExpr
import com.github.javaparser.ast.stmt.ExpressionStmt
import com.github.javaparser.ast.stmt.ForStmt
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class IncrementTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { stmt: ExpressionStmt ->
if (!stmt.expression.isUnaryExpr) {
return@walk

@ -2,6 +2,8 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.UnaryExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.isIntegerOrLongLiteral
import dev.openrs2.deob.ast.util.negate
import dev.openrs2.deob.ast.util.walk
@ -9,10 +11,7 @@ import javax.inject.Singleton
@Singleton
class NegativeLiteralTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: UnaryExpr ->
val operand = expr.expression
if (!operand.isIntegerOrLongLiteral()) {

@ -2,16 +2,15 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.MethodCallExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.isClass
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class NewInstanceTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: MethodCallExpr ->
if (expr.nameAsString != "newInstance") {
return@walk

@ -2,6 +2,8 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.ConditionalExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.countNots
import dev.openrs2.deob.ast.util.not
import dev.openrs2.deob.ast.util.walk
@ -9,10 +11,7 @@ import javax.inject.Singleton
@Singleton
class TernaryTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: ConditionalExpr ->
val condition = expr.condition
val notCondition = condition.not()

@ -1,27 +1,31 @@
package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
abstract class Transformer {
fun transform(units: Map<String, CompilationUnit>) {
preTransform(units)
fun transform(group: LibraryGroup) {
preTransform(group)
for (unit in units.values) {
transformUnit(units, unit)
for (library in group) {
for (unit in library) {
transformUnit(group, library, unit)
}
}
postTransform(units)
postTransform(group)
}
protected open fun preTransform(units: Map<String, CompilationUnit>) {
protected open fun preTransform(group: LibraryGroup) {
// empty
}
protected open fun transformUnit(units: Map<String, CompilationUnit>, unit: CompilationUnit) {
protected open fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
// empty
}
protected open fun postTransform(units: Map<String, CompilationUnit>) {
protected open fun postTransform(group: LibraryGroup) {
// empty
}
}

@ -2,15 +2,14 @@ package dev.openrs2.deob.ast.transform
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.EnclosedExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class UnencloseTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: EnclosedExpr ->
expr.replace(expr.inner.clone())
}

@ -4,15 +4,14 @@ import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.MethodCallExpr
import com.github.javaparser.ast.expr.ObjectCreationExpr
import com.github.javaparser.ast.expr.TypeExpr
import dev.openrs2.deob.ast.Library
import dev.openrs2.deob.ast.LibraryGroup
import dev.openrs2.deob.ast.util.walk
import javax.inject.Singleton
@Singleton
class ValueOfTransformer : Transformer() {
override fun transformUnit(
units: Map<String, CompilationUnit>,
unit: CompilationUnit
) {
override fun transformUnit(group: LibraryGroup, library: Library, unit: CompilationUnit) {
unit.walk { expr: ObjectCreationExpr ->
if (expr.type.isBoxedType) {
expr.replace(MethodCallExpr(TypeExpr(expr.type), "valueOf", expr.arguments))

Loading…
Cancel
Save