diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java index df260bd22..b8204b68c 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java @@ -49,7 +49,7 @@ public class ScriptCompleteProvider extends CompletionProviderBase { } private final AbstractCodeArea codeArea; - private ScriptServices scriptComplete; + private ScriptServices scriptServices; public ScriptCompleteProvider(AbstractCodeArea codeArea) { this.codeArea = codeArea; @@ -60,8 +60,9 @@ public class ScriptCompleteProvider extends CompletionProviderBase { String code = codeArea.getText(); int caretPos = codeArea.getCaretPosition(); // TODO: resolve error after reusing ScriptCompiler - scriptComplete = new ScriptServices(codeArea.getNode().getName()); - ScriptCompletionResult result = scriptComplete.complete(code, caretPos); + scriptServices = new ScriptServices(); + String scriptName = codeArea.getNode().getName(); + ScriptCompletionResult result = scriptServices.complete(scriptName, code, caretPos); int replacePos = getReplacePos(caretPos, result); if (!result.getReports().isEmpty()) { LOG.debug("Script completion reports: {}", result.getReports()); diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java index c2ad6be06..722697f62 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -128,6 +128,9 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { private void runScript() { scriptArea.save(); + if (!checkScript()) { + return; + } resetResultLabel(); TabbedPane tabbedPane = getTabbedPane(); @@ -152,17 +155,17 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { String code = scriptArea.getText(); String fileName = scriptArea.getNode().getName(); - ScriptServices scriptServices = new ScriptServices(fileName); - ScriptAnalyzeResult result = scriptServices.analyze(code, scriptArea.getCaretPosition()); + ScriptServices scriptServices = new ScriptServices(); + ScriptAnalyzeResult result = scriptServices.analyze(fileName, code); + boolean success = result.getSuccess(); List issues = result.getIssues(); - boolean success = true; for (ScriptDiagnostic issue : issues) { Severity severity = issue.getSeverity(); if (severity == Severity.ERROR || severity == Severity.FATAL) { scriptLog.error("{}", issue.render(false, true, true, true)); success = false; - } else { - scriptLog.warn("Compiler issue: {}", issue); + } else if (severity == Severity.WARNING) { + scriptLog.warn("Compile issue: {}", issue); } } List lintErrs = Collections.emptyList(); @@ -175,7 +178,7 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { errorService.addLintErrors(lintErrs); errorService.apply(); if (!success) { - resultLabel.setText("Compiler issues: " + issues.size()); + resultLabel.setText("Compile issues: " + issues.size()); showScriptLog(); } else if (!lintErrs.isEmpty()) { resultLabel.setText("Lint issues: " + lintErrs.size()); diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java index 652418a62..1fff06a64 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java @@ -62,6 +62,9 @@ public class ScriptErrorService extends AbstractParser { public void addCompilerIssues(List issues) { for (ScriptDiagnostic issue : issues) { + if (issue.getSeverity() == ScriptDiagnostic.Severity.DEBUG) { + continue; + } DefaultParserNotice notice; SourceCode.Location loc = issue.getLocation(); if (loc == null) { diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt index c9f1e5ef0..648932653 100644 --- a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt @@ -4,15 +4,17 @@ import jadx.plugins.script.ScriptEval import kotlinx.coroutines.runBlocking import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices import kotlin.script.experimental.api.ReplAnalyzerResult -import kotlin.script.experimental.api.ReplCompletionResult -import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptDiagnostic import kotlin.script.experimental.api.SourceCode import kotlin.script.experimental.api.SourceCodeCompletionVariant import kotlin.script.experimental.api.analysisDiagnostics +import kotlin.script.experimental.api.hostConfiguration import kotlin.script.experimental.api.renderedResultType import kotlin.script.experimental.api.valueOrNull import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration +import kotlin.script.experimental.jvm.util.isError import kotlin.script.experimental.jvm.util.toSourceCodePosition const val AUTO_COMPLETE_INSERT_STR = "ABCDEF" // defined at KJvmReplCompleter.INSERTED_STRING @@ -23,42 +25,55 @@ data class ScriptCompletionResult( ) data class ScriptAnalyzeResult( + val success: Boolean, val issues: List, val renderType: String?, - val reports: List, ) -class ScriptServices(private val scriptName: String) { - private val replCompiler = KJvmReplCompilerWithIdeServices() - private val compileConf = ScriptEval().buildCompileConf() +class ScriptServices { + private val compileConf = ScriptEval.compileConf + private val replCompiler = KJvmReplCompilerWithIdeServices( + compileConf[ScriptCompilationConfiguration.hostConfiguration] + ?: defaultJvmScriptingHostConfiguration, + ) - fun complete(code: String, cursor: Int): ScriptCompletionResult { - val result = complete(code.toScriptSource(scriptName), cursor) + fun complete(scriptName: String, code: String, cursor: Int): ScriptCompletionResult { + val snippet = code.toScriptSource(scriptName) + val result = runBlocking { + replCompiler.complete(snippet, cursor.toSourceCodePosition(snippet), compileConf) + } return ScriptCompletionResult( completions = result.valueOrNull()?.toList() ?: emptyList(), reports = result.reports, ) } - fun analyze(code: String, cursor: Int): ScriptAnalyzeResult { - val result = analyze(code.toScriptSource(scriptName), cursor) - val analyzerResult = result.valueOrNull() - return ScriptAnalyzeResult( - issues = analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.toList() ?: emptyList(), - renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType), - reports = result.reports, - ) - } - - private fun complete(code: SourceCode, cursor: Int): ResultWithDiagnostics { - return runBlocking { - replCompiler.complete(code, cursor.toSourceCodePosition(code), compileConf) - } - } - - private fun analyze(code: SourceCode, cursor: Int): ResultWithDiagnostics { - return runBlocking { - replCompiler.analyze(code, cursor.toSourceCodePosition(code), compileConf) + fun analyze(scriptName: String, code: String): ScriptAnalyzeResult { + // TODO: temp solution: analyze do not work with dependencies, use compile instead + val sourceCode = code.toScriptSource(scriptName) + if (code.contains("@file:DependsOn(")) { + val result = runBlocking { + ScriptEval().compile(sourceCode) + } + return ScriptAnalyzeResult( + success = !result.isError(), + issues = result.reports, + renderType = null, + ) + } else { + val result = runBlocking { + val cursor = SourceCode.Position(0, 0) // not used + replCompiler.analyze(sourceCode, cursor, compileConf) + } + val analyzerResult = result.valueOrNull() + val issues = mutableListOf() + analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.let(issues::addAll) + issues.addAll(result.reports) + return ScriptAnalyzeResult( + success = !result.isError(), + issues = issues, + renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType), + ) } } } diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/test/kotlin/ScriptServicesTest.kt b/jadx-plugins/jadx-script/jadx-script-ide/src/test/kotlin/ScriptServicesTest.kt new file mode 100644 index 000000000..f9dc782a4 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/test/kotlin/ScriptServicesTest.kt @@ -0,0 +1,45 @@ +package jadx.plugins.script.ide + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ScriptServicesTest { + + @Test + fun testAnalyzeSimple() { + val name = "simple" + val script = getSampleScript(name) + val result = ScriptServices().analyze(name, script) + println(result) + assertThat(result.success).isTrue() + } + + @Test + fun testAnalyzeDeps() { + val name = "test-deps" + val script = getSampleScript(name) + val result = ScriptServices().analyze(name, script) + println(result) + assertThat(result.success).isTrue() + } + + @Test + fun testComplete() { + val name = "simple" + val script = getSampleScript(name) + val idx = script.indexOf("jadx.log.info") + val completePos = idx + 7 // jadx.lo| <- complete 'log' + val curScript = script.removeRange(completePos, script.indexOf("\n", idx)) + + val result = ScriptServices().complete(name, curScript, completePos) + println(result) + assertThat(result.completions) + .hasSize(1) + .allMatch { c -> c.text == "log" } + } + + private fun getSampleScript(scriptName: String): String { + val resFile = javaClass.classLoader.getResource("samples/$scriptName.jadx.kts") + return resFile!!.readText() + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/simple.jadx.kts b/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/simple.jadx.kts new file mode 100644 index 000000000..5f75ceb36 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/simple.jadx.kts @@ -0,0 +1,5 @@ +val jadx = getJadxInstance() + +jadx.afterLoad { + jadx.log.info { "Hello" } +} diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/test-deps.jadx.kts b/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/test-deps.jadx.kts new file mode 100644 index 000000000..39953db1c --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/test/resources/samples/test-deps.jadx.kts @@ -0,0 +1,11 @@ +@file:DependsOn("org.apache.commons:commons-text:1.10.0") + +import org.apache.commons.text.StringEscapeUtils + +val jadx = getJadxInstance() + +jadx.afterLoad { + jadx.classes.forEach { + jadx.log.info { "Escaped name: ${StringEscapeUtils.escapeJava(it.fullName)}" } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt index 2d3dbf179..59428c694 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt @@ -4,13 +4,13 @@ import jadx.api.plugins.JadxPluginContext import jadx.plugins.script.runtime.JadxScriptData import jadx.plugins.script.runtime.JadxScriptTemplate import jadx.plugins.script.runtime.data.JadxScriptAllOptions +import kotlin.script.experimental.api.CompiledScript import kotlin.script.experimental.api.EvaluationResult import kotlin.script.experimental.api.ResultValue import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptDiagnostic.Severity import kotlin.script.experimental.api.ScriptEvaluationConfiguration -import kotlin.script.experimental.api.compilerOptions +import kotlin.script.experimental.api.SourceCode import kotlin.script.experimental.api.constructorArgs import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost @@ -22,7 +22,19 @@ import kotlin.time.toDuration class ScriptEval { - private val scriptingHost = BasicJvmScriptingHost() + companion object { + val scriptingHost = BasicJvmScriptingHost() + + val compileConf = createJvmCompilationConfigurationFromTemplate() + + private val baseEvalConf = createJvmEvaluationConfigurationFromTemplate() + + private fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration { + return ScriptEvaluationConfiguration(baseEvalConf) { + constructorArgs(scriptData) + } + } + } fun process(init: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List { val jadx = init.decompiler @@ -39,26 +51,18 @@ class ScriptEval { return scriptDataList } - fun buildCompileConf(): ScriptCompilationConfiguration { - return createJvmCompilationConfigurationFromTemplate { - // forcing compiler to not use modules while building script classpath - // because shadow jar remove all modules-info.class (https://github.com/johnrengelman/shadow/issues/710) - compilerOptions("-Xjdk-release=1.8") - } - } - - fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration { - return createJvmEvaluationConfigurationFromTemplate { - constructorArgs(scriptData) - } + suspend fun compile(script: SourceCode): ResultWithDiagnostics { + return scriptingHost.compiler(script, compileConf) } private fun eval(scriptData: JadxScriptData) { scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" } val execTime = measureTimeMillis { - val compilationConf = buildCompileConf() - val evalConf = buildEvalConf(scriptData) - val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compilationConf, evalConf) + val result = scriptingHost.eval( + scriptData.scriptFile.toScriptSource(), + compileConf, + buildEvalConf(scriptData), + ) processEvalResult(result, scriptData) } scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/test/kotlin/JadxScriptPluginTest.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/test/kotlin/JadxScriptPluginTest.kt index a7a40a19d..d0e61462f 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/test/kotlin/JadxScriptPluginTest.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/test/kotlin/JadxScriptPluginTest.kt @@ -17,6 +17,7 @@ class JadxScriptPluginTest { args.inputFiles.run { add(getSampleFile("hello.smali")) add(getSampleFile("test.jadx.kts")) + add(getSampleFile("test-deps.jadx.kts")) } val elapsed = measureTimeMillis { JadxDecompiler(args).use { jadx -> diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/test/resources/samples/test-deps.jadx.kts b/jadx-plugins/jadx-script/jadx-script-plugin/src/test/resources/samples/test-deps.jadx.kts new file mode 100644 index 000000000..39953db1c --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/test/resources/samples/test-deps.jadx.kts @@ -0,0 +1,11 @@ +@file:DependsOn("org.apache.commons:commons-text:1.10.0") + +import org.apache.commons.text.StringEscapeUtils + +val jadx = getJadxInstance() + +jadx.afterLoad { + jadx.classes.forEach { + jadx.log.info { "Escaped name: ${StringEscapeUtils.escapeJava(it.fullName)}" } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt index 2ca8ba297..160054a28 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt @@ -10,6 +10,7 @@ import kotlin.script.experimental.api.ScriptConfigurationRefinementContext import kotlin.script.experimental.api.acceptedLocations import kotlin.script.experimental.api.asSuccess import kotlin.script.experimental.api.collectedAnnotations +import kotlin.script.experimental.api.compilerOptions import kotlin.script.experimental.api.defaultImports import kotlin.script.experimental.api.dependencies import kotlin.script.experimental.api.ide @@ -28,6 +29,7 @@ import kotlin.script.experimental.jvm.dependenciesFromCurrentContext import kotlin.script.experimental.jvm.jvm @KotlinScript( + displayName = "Jadx Script", fileExtension = "jadx.kts", compilationConfiguration = JadxScriptConfiguration::class, ) @@ -66,7 +68,11 @@ object JadxScriptConfiguration : ScriptCompilationConfiguration({ onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations) } - isStandalone(false) + isStandalone(true) + + // forcing compiler to not use modules while building script classpath + // because shadow jar remove all modules-info.class (https://github.com/johnrengelman/shadow/issues/710) + compilerOptions.append("-Xjdk-release=1.8") }) private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) @@ -75,10 +81,11 @@ fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContex val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) ?.takeIf { it.isNotEmpty() } ?: return context.compilationConfiguration.asSuccess() - return runBlocking { resolver.resolveFromScriptSourceAnnotations(annotations) } - .onSuccess { - context.compilationConfiguration.with { - dependencies.append(JvmDependency(it)) - }.asSuccess() - } + return runBlocking { + resolver.resolveFromScriptSourceAnnotations(annotations) + }.onSuccess { + context.compilationConfiguration.with { + dependencies.append(JvmDependency(it)) + }.asSuccess() + } }