diff --git a/build.gradle b/build.gradle index 0a107fd8b..2b360f5bb 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'com.github.ben-manes.versions' version '0.45.0' id 'com.diffplug.spotless' version '6.13.0' - id 'org.jetbrains.kotlin.jvm' version '1.7.20' // needed for spotless + id 'org.jetbrains.kotlin.jvm' version '1.7.20' } ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev" @@ -23,6 +23,12 @@ allprojects { compileJava { options.encoding = "UTF-8" } + compileKotlin { + kotlinOptions { + incremental = false // cause unexpected issues sometime + // useK2 = true + } + } jar { manifest { @@ -78,6 +84,7 @@ spotless { kotlin { target fileTree(rootDir).matching { include 'jadx-plugins/jadx-script/**/*.kt' + include 'jadx-gui/src/main/**/*.kt' // include 'jadx-plugins/jadx-script/examples/scripts/**/*.jadx.kts' } ktlint() diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index c98773e69..370368200 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -128,6 +128,17 @@ public final class JadxDecompiler implements Closeable { loadFinished(); } + public void reloadPasses() { + LOG.info("reloading (passes only) ..."); + customPasses.clear(); + root.resetPasses(); + loadPlugins(); + root.mergePasses(customPasses); + root.restartVisitors(); + root.initPasses(); + loadFinished(); + } + private void loadInputFiles() { loadedInputs.clear(); List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index a908f6ac9..c818f9533 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -350,7 +350,7 @@ public class ClassNode extends NotificationAttrNode innerClasses.forEach(ClassNode::deepUnload); } - private void unloadFromCache() { + public void unloadFromCache() { if (isInner()) { return; } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 893fb7521..f7b5b4cac 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -343,6 +343,24 @@ public class RootNode { } } + // TODO: make better API for reload passes lists + public void resetPasses() { + preDecompilePasses.clear(); + preDecompilePasses.addAll(Jadx.getPreDecompilePassesList()); + + processClasses.getPasses().clear(); + processClasses.getPasses().addAll(Jadx.getPassesList(args)); + } + + public void restartVisitors() { + for (ClassNode cls : classes) { + cls.unload(); + cls.clearAttributes(); + cls.unloadFromCache(); + } + runPreDecompileStage(); + } + public List getClasses() { return classes; } diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 06794038c..3149e9f51 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -14,9 +14,13 @@ dependencies { // jadx-script autocomplete support implementation(project(":jadx-plugins::jadx-script:jadx-script-ide")) - implementation("org.jetbrains.kotlin:kotlin-scripting-common:1.7.20") + implementation 'org.jetbrains.kotlin:kotlin-scripting-common:1.7.20' implementation 'com.fifesoft:autocomplete:3.3.0' + // use ktlint for lint and format jadx scripts + implementation 'com.pinterest.ktlint:ktlint-core:0.47.1' + implementation 'com.pinterest.ktlint:ktlint-ruleset-standard:0.47.1' + implementation 'com.beust:jcommander:1.82' implementation 'ch.qos.logback:logback-classic:1.3.5' @@ -44,10 +48,13 @@ dependencies { application { applicationName = 'jadx-gui' mainClass.set('jadx.gui.JadxGUI') - // The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC - applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC', - '-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', - '-Djava.util.Arrays.useLegacyMergeSort=true'] + applicationDefaultJvmArgs = [ + '-Xms128M', '-XX:MaxRAMPercentage=70.0', + '-XX:+UseG1GC', // only relevant for Java 8, starting with Java 9 G1GC is already the default GC + '-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', + '-Djava.util.Arrays.useLegacyMergeSort=true', + '-XX:+IgnoreUnrecognizedVMOptions', '--add-opens=java.base/java.lang=ALL-UNNAMED', // for ktlint formatter + ] } applicationDistribution.with { @@ -94,7 +101,7 @@ launch4j { windowTitle = 'jadx' companyName = 'jadx' jreMinVersion = '11' - jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC', '-Djava.util.Arrays.useLegacyMergeSort=true'] + jvmOptions = application.getApplicationDefaultJvmArgs() jreRuntimeBits = "64" bundledJre64Bit = true initialHeapPercent = 5 diff --git a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java index 2d034f819..9b6a1c1d7 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java @@ -182,9 +182,9 @@ public class DiskCodeCache implements ICodeCache { @Override public void remove(String clsFullName) { try { - LOG.debug("Removing class info from disk: {}", clsFullName); Integer clsId = namesMap.remove(clsFullName); if (clsId != null) { + LOG.debug("Removing class info from disk: {}", clsFullName); Files.deleteIfExists(getJavaFile(clsId)); Files.deleteIfExists(getMetadataFile(clsId)); } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt b/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt new file mode 100644 index 000000000..4a91edce2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt @@ -0,0 +1,46 @@ +package jadx.gui.plugins.script + +import com.pinterest.ktlint.core.KtLint +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +object KtLintUtils { + + val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java) + + val rules = lazy { StandardRuleSetProvider().getRuleProviders() } + + fun format(code: String, fileName: String): String { + val params = KtLint.ExperimentalParams( + text = code, + fileName = fileName, + ruleProviders = rules.value, + script = true, + cb = { e: LintError, corrected -> + if (!corrected) { + LOG.warn("Lint error: {}", e) + } + } + ) + return KtLint.format(params) + } + + fun lint(code: String, fileName: String): List { + val errors = mutableListOf() + val params = KtLint.ExperimentalParams( + text = code, + fileName = fileName, + ruleProviders = rules.value, + script = true, + cb = { e: LintError, corrected -> + if (!corrected) { + errors.add(e) + } + } + ) + KtLint.lint(params) + return errors + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java new file mode 100644 index 000000000..1c229a613 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java @@ -0,0 +1,84 @@ +package jadx.gui.plugins.script; + +import java.awt.event.KeyEvent; + +import javax.swing.KeyStroke; + +import org.fife.ui.autocomplete.AutoCompletion; +import org.jetbrains.annotations.NotNull; + +import jadx.api.ICodeInfo; +import jadx.gui.settings.JadxSettings; +import jadx.gui.treemodel.JInputScript; +import jadx.gui.ui.codearea.AbstractCodeArea; +import jadx.gui.ui.panel.ContentPanel; +import jadx.gui.utils.UiUtils; + +public class ScriptCodeArea extends AbstractCodeArea { + + private final JInputScript scriptNode; + private final AutoCompletion autoCompletion; + + public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) { + super(contentPanel, node); + scriptNode = node; + + setSyntaxEditingStyle(node.getSyntaxName()); + setCodeFoldingEnabled(true); + setCloseCurlyBraces(true); + + JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings(); + autoCompletion = addAutoComplete(settings); + } + + private AutoCompletion addAutoComplete(JadxSettings settings) { + ScriptCompleteProvider provider = new ScriptCompleteProvider(this); + provider.setAutoActivationRules(false, "."); + AutoCompletion ac = new AutoCompletion(provider); + ac.setListCellRenderer(new ScriptCompletionRenderer(settings)); + ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, UiUtils.ctrlButton())); + ac.setAutoActivationEnabled(true); + ac.setAutoCompleteSingleChoices(true); + ac.install(this); + return ac; + } + + @Override + public @NotNull ICodeInfo getCodeInfo() { + return node.getCodeInfo(); + } + + @Override + public void load() { + if (getText().isEmpty()) { + setText(getCodeInfo().getCodeStr()); + setCaretPosition(0); + setLoaded(); + } + } + + @Override + public void refresh() { + setText(node.getCodeInfo().getCodeStr()); + } + + public void updateCode(String newCode) { + setText(newCode); + scriptNode.setChanged(true); + } + + public void save() { + scriptNode.save(getText()); + scriptNode.setChanged(false); + } + + public JInputScript getScriptNode() { + return scriptNode; + } + + @Override + public void dispose() { + autoCompletion.uninstall(); + super.dispose(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompleteProvider.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java similarity index 87% rename from jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompleteProvider.java rename to jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java index f616d1655..34c898ee3 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompleteProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompleteProvider.java @@ -27,13 +27,13 @@ import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.utils.Icons; -import jadx.plugins.script.ide.JadxScriptAutoComplete; +import jadx.plugins.script.ide.ScriptCompiler; import jadx.plugins.script.ide.ScriptCompletionResult; -import static jadx.plugins.script.ide.CompleteKt.AUTO_COMPLETE_INSERT_STR; +import static jadx.plugins.script.ide.ScriptCompilerKt.AUTO_COMPLETE_INSERT_STR; -public class JadxScriptCompleteProvider extends CompletionProviderBase { - private static final Logger LOG = LoggerFactory.getLogger(JadxScriptCompleteProvider.class); +public class ScriptCompleteProvider extends CompletionProviderBase { + private static final Logger LOG = LoggerFactory.getLogger(ScriptCompleteProvider.class); private static final Map ICONS_MAP = buildIconsMap(); @@ -49,16 +49,19 @@ public class JadxScriptCompleteProvider extends CompletionProviderBase { } private final AbstractCodeArea codeArea; + private ScriptCompiler scriptComplete; - public JadxScriptCompleteProvider(AbstractCodeArea codeArea) { + public ScriptCompleteProvider(AbstractCodeArea codeArea) { this.codeArea = codeArea; + // this.scriptComplete = new ScriptCompiler(codeArea.getNode().getName()); } private List getCompletions() { try { String code = codeArea.getText(); int caretPos = codeArea.getCaretPosition(); - JadxScriptAutoComplete scriptComplete = new JadxScriptAutoComplete(codeArea.getNode().getName()); + // TODO: resolve error after reusing ScriptCompiler + scriptComplete = new ScriptCompiler(codeArea.getNode().getName()); ScriptCompletionResult result = scriptComplete.complete(code, caretPos); int replacePos = getReplacePos(caretPos, result); if (!result.getReports().isEmpty()) { @@ -88,7 +91,7 @@ public class JadxScriptCompleteProvider extends CompletionProviderBase { continue; } - JadxScriptCompletion cmpl = new JadxScriptCompletion(this, count - i); + ScriptCompletionData cmpl = new ScriptCompletionData(this, count - i); cmpl.setData(c.getText(), code, replacePos); if (Objects.equals(c.getIcon(), "method") && !Objects.equals(c.getText(), c.getDisplayText())) { // add method args details for methods diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompletion.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionData.java similarity index 92% rename from jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompletion.java rename to jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionData.java index 536e397a2..08bbbca9f 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/JadxScriptCompletion.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionData.java @@ -6,7 +6,7 @@ import javax.swing.text.JTextComponent; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; -public class JadxScriptCompletion implements Completion { +public class ScriptCompletionData implements Completion { private final CompletionProvider provider; private final int relevance; @@ -18,7 +18,7 @@ public class JadxScriptCompletion implements Completion { private String summary; private String toolTip; - public JadxScriptCompletion(CompletionProvider provider, int relevance) { + public ScriptCompletionData(CompletionProvider provider, int relevance) { this.provider = provider; this.relevance = relevance; } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/CompletionRenderer.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionRenderer.java similarity index 77% rename from jadx-gui/src/main/java/jadx/gui/plugins/script/CompletionRenderer.java rename to jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionRenderer.java index 2928f5f1d..4af3f927b 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/CompletionRenderer.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCompletionRenderer.java @@ -11,15 +11,15 @@ import static jadx.gui.utils.UiUtils.escapeHtml; import static jadx.gui.utils.UiUtils.fadeHtml; import static jadx.gui.utils.UiUtils.wrapHtml; -public class CompletionRenderer extends CompletionCellRenderer { +public class ScriptCompletionRenderer extends CompletionCellRenderer { - public CompletionRenderer(JadxSettings settings) { + public ScriptCompletionRenderer(JadxSettings settings) { setDisplayFont(settings.getFont()); } @Override protected void prepareForOtherCompletion(JList list, Completion c, int index, boolean selected, boolean hasFocus) { - JadxScriptCompletion cmpl = (JadxScriptCompletion) c; + ScriptCompletionData cmpl = (ScriptCompletionData) c; setText(wrapHtml(escapeHtml(cmpl.getInputText()) + " " + fadeHtml(escapeHtml(cmpl.getSummary())))); } 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 new file mode 100644 index 000000000..f85984fd9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -0,0 +1,233 @@ +package jadx.gui.plugins.script; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.KeyEvent; +import java.util.Collections; +import java.util.List; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; + +import org.fife.ui.rsyntaxtextarea.ErrorStrip; +import org.fife.ui.rtextarea.RTextScrollPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pinterest.ktlint.core.LintError; + +import kotlin.script.experimental.api.ScriptDiagnostic; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.settings.LineNumbersMode; +import jadx.gui.treemodel.JInputScript; +import jadx.gui.ui.MainWindow; +import jadx.gui.ui.TabbedPane; +import jadx.gui.ui.codearea.AbstractCodeArea; +import jadx.gui.ui.codearea.AbstractCodeContentPanel; +import jadx.gui.ui.codearea.SearchBar; +import jadx.gui.utils.Icons; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.ui.ActionHandler; +import jadx.gui.utils.ui.NodeLabel; +import jadx.plugins.script.ide.ScriptAnalyzeResult; +import jadx.plugins.script.ide.ScriptCompiler; + +public class ScriptContentPanel extends AbstractCodeContentPanel { + private static final long serialVersionUID = 6575696321112417513L; + + private static final Logger LOG = LoggerFactory.getLogger(ScriptContentPanel.class); + + private final ScriptCodeArea scriptArea; + private final SearchBar searchBar; + private final RTextScrollPane codeScrollPane; + private final JPanel actionPanel; + private final JLabel resultLabel; + private final ScriptErrorService errorService; + + public ScriptContentPanel(TabbedPane panel, JInputScript scriptNode) { + super(panel, scriptNode); + scriptArea = new ScriptCodeArea(this, scriptNode); + resultLabel = new NodeLabel(""); + errorService = new ScriptErrorService(scriptArea); + actionPanel = buildScriptActionsPanel(); + searchBar = new SearchBar(scriptArea); + codeScrollPane = new RTextScrollPane(scriptArea); + + initUI(); + applySettings(); + scriptArea.load(); + } + + private void initUI() { + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + topPanel.add(actionPanel, BorderLayout.NORTH); + topPanel.add(searchBar, BorderLayout.SOUTH); + + JPanel codePanel = new JPanel(new BorderLayout()); + codePanel.setBorder(new EmptyBorder(0, 0, 0, 0)); + codePanel.add(codeScrollPane); + codePanel.add(new ErrorStrip(scriptArea), BorderLayout.LINE_END); + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(0, 0, 0, 0)); + add(topPanel, BorderLayout.NORTH); + add(codeScrollPane, BorderLayout.CENTER); + + KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton()); + UiUtils.addKeyBinding(scriptArea, key, "SearchAction", searchBar::toggle); + } + + private JPanel buildScriptActionsPanel() { + ActionHandler runAction = new ActionHandler(this::runScript); + runAction.setNameAndDesc(NLS.str("script.run")); + runAction.setIcon(Icons.RUN); + runAction.attachKeyBindingFor(scriptArea, KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0)); + + ActionHandler saveAction = new ActionHandler(scriptArea::save); + saveAction.setNameAndDesc(NLS.str("script.save")); + saveAction.setIcon(Icons.SAVE_ALL); + saveAction.setKeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_S, UiUtils.ctrlButton())); + + JButton save = saveAction.makeButton(); + scriptArea.getScriptNode().addChangeListener(save::setEnabled); + + JButton check = new JButton(NLS.str("script.check"), Icons.CHECK); + check.addActionListener(ev -> checkScript()); + JButton format = new JButton(NLS.str("script.format"), Icons.FORMAT); + format.addActionListener(ev -> reformatCode()); + + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); + panel.setBorder(new EmptyBorder(0, 0, 0, 0)); + panel.add(runAction.makeButton()); + panel.add(Box.createRigidArea(new Dimension(10, 0))); + panel.add(save); + panel.add(Box.createRigidArea(new Dimension(10, 0))); + panel.add(check); + panel.add(Box.createRigidArea(new Dimension(10, 0))); + panel.add(format); + panel.add(Box.createRigidArea(new Dimension(30, 0))); + panel.add(resultLabel); + panel.add(Box.createHorizontalGlue()); + return panel; + } + + private void runScript() { + scriptArea.save(); + if (!checkScript()) { + return; + } + resetResultLabel(); + + TabbedPane tabbedPane = getTabbedPane(); + MainWindow mainWindow = tabbedPane.getMainWindow(); + mainWindow.getBackgroundExecutor().execute(NLS.str("script.run"), () -> { + try { + mainWindow.getWrapper().getDecompiler().reloadPasses(); + } catch (Exception e) { + LOG.error("Passes reload failed", e); + } + }, taskStatus -> { + tabbedPane.reloadInactiveTabs(); + mainWindow.reloadTree(); + }); + } + + private boolean checkScript() { + try { + resetResultLabel(); + String code = scriptArea.getText(); + String fileName = scriptArea.getNode().getName(); + + ScriptCompiler scriptCompiler = new ScriptCompiler(fileName); + ScriptAnalyzeResult result = scriptCompiler.analyze(code, scriptArea.getCaretPosition()); + List errors = result.getErrors(); + for (ScriptDiagnostic error : errors) { + LOG.warn("Parse error: {}", error); + } + + List lintErrs = Collections.emptyList(); + if (errors.isEmpty()) { + lintErrs = getLintIssues(code, fileName); + } + + errorService.clearErrors(); + errorService.addErrors(errors); + errorService.addLintErrors(lintErrs); + errorService.apply(); + boolean success = errors.isEmpty(); + if (!success) { + resultLabel.setText("Parsing errors: " + errors.size()); + } else if (!lintErrs.isEmpty()) { + resultLabel.setText("Lint issues: " + lintErrs.size()); + } + return success; + } catch (Throwable e) { + LOG.error("Failed to check code", e); + return true; + } + } + + private List getLintIssues(String code, String fileName) { + try { + List lintErrs = KtLintUtils.INSTANCE.lint(code, fileName); + for (LintError error : lintErrs) { + LOG.warn("Lint issue: {}", error); + } + return lintErrs; + } catch (Throwable e) { // can throw initialization error + LOG.warn("KtLint failed", e); + return Collections.emptyList(); + } + } + + private void reformatCode() { + resetResultLabel(); + try { + String code = scriptArea.getText(); + String fileName = scriptArea.getNode().getName(); + String formattedCode = KtLintUtils.INSTANCE.format(code, fileName); + if (!code.equals(formattedCode)) { + scriptArea.updateCode(formattedCode); + resultLabel.setText("Code updated"); + errorService.clearErrors(); + } + } catch (Throwable e) { // can throw initialization error + LOG.error("Failed to reformat code", e); + } + } + + private void resetResultLabel() { + resultLabel.setText(""); + } + + private void applySettings() { + JadxSettings settings = getSettings(); + codeScrollPane.setLineNumbersEnabled(settings.getLineNumbersMode() != LineNumbersMode.DISABLE); + codeScrollPane.getGutter().setLineNumberFont(settings.getFont()); + scriptArea.loadSettings(); + } + + @Override + public AbstractCodeArea getCodeArea() { + return scriptArea; + } + + @Override + public void loadSettings() { + applySettings(); + updateUI(); + } + + public void dispose() { + scriptArea.dispose(); + } +} 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 new file mode 100644 index 000000000..511b0fffd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java @@ -0,0 +1,107 @@ +package jadx.gui.plugins.script; + +import java.util.List; + +import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; +import org.fife.ui.rsyntaxtextarea.parser.AbstractParser; +import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult; +import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice; +import org.fife.ui.rsyntaxtextarea.parser.ParseResult; +import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pinterest.ktlint.core.LintError; + +import kotlin.script.experimental.api.ScriptDiagnostic; +import kotlin.script.experimental.api.SourceCode; + +public class ScriptErrorService extends AbstractParser { + + private static final Logger LOG = LoggerFactory.getLogger(ScriptErrorService.class); + private final DefaultParseResult result; + private final ScriptCodeArea scriptArea; + + public ScriptErrorService(ScriptCodeArea scriptArea) { + this.scriptArea = scriptArea; + this.result = new DefaultParseResult(this); + } + + @Override + public ParseResult parse(RSyntaxDocument doc, String style) { + return result; + } + + public void clearErrors() { + result.clearNotices(); + scriptArea.removeParser(this); + } + + public void apply() { + scriptArea.removeParser(this); + scriptArea.addParser(this); + scriptArea.addNotify(); + jumpCaretToFirstError(); + } + + private void jumpCaretToFirstError() { + List parserNotices = result.getNotices(); + if (parserNotices.isEmpty()) { + return; + } + ParserNotice notice = parserNotices.get(0); + int offset = notice.getOffset(); + if (offset != -1) { + scriptArea.setCaretPosition(offset); + } else { + try { + scriptArea.setCaretPosition(scriptArea.getLineStartOffset(notice.getLine())); + } catch (Exception e) { + LOG.error("Failed to jump to first error", e); + } + } + } + + public void addErrors(List errors) { + for (ScriptDiagnostic error : errors) { + DefaultParserNotice notice; + SourceCode.Location loc = error.getLocation(); + if (loc == null) { + notice = new DefaultParserNotice(this, error.getMessage(), 0); + } else { + try { + int line = loc.getStart().getLine(); + int offset = scriptArea.getLineStartOffset(line - 1) + loc.getStart().getCol(); + int len = loc.getEnd() == null ? -1 : loc.getEnd().getCol() - loc.getStart().getCol(); + notice = new DefaultParserNotice(this, error.getMessage(), line, offset - 1, len); + } catch (Exception e) { + LOG.error("Failed to convert script error", e); + continue; + } + } + addNotice(notice); + } + } + + public void addLintErrors(List errors) { + for (LintError error : errors) { + try { + int line = error.getLine(); + int offset = scriptArea.getLineStartOffset(line - 1) + error.getCol() - 1; + String word = scriptArea.getWordByPosition(offset); + int len = word != null ? word.length() : -1; + DefaultParserNotice notice = new DefaultParserNotice(this, error.getDetail(), line, offset, len); + notice.setLevel(ParserNotice.Level.WARNING); + addNotice(notice); + } catch (Exception e) { + LOG.error("Failed to convert lint error", e); + } + } + } + + private void addNotice(DefaultParserNotice notice) { + LOG.debug("Add notice: {}:{}:{} - {}", + notice.getLine(), notice.getOffset(), notice.getLength(), notice.getMessage()); + result.addNotice(notice); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JEditableNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JEditableNode.java index 8f3164602..e84c0c59a 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JEditableNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JEditableNode.java @@ -31,5 +31,6 @@ public abstract class JEditableNode extends JNode { public void addChangeListener(Consumer listener) { changeListeners.add(listener); + listener.accept(changed); } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java index b1a8d19b7..37861e825 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java @@ -15,9 +15,9 @@ import jadx.api.ICodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; +import jadx.gui.plugins.script.ScriptContentPanel; import jadx.gui.ui.MainWindow; import jadx.gui.ui.TabbedPane; -import jadx.gui.ui.codearea.CodeContentPanel; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; @@ -38,7 +38,7 @@ public class JInputScript extends JEditableNode { @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { - return new CodeContentPanel(tabbedPane, this); + return new ScriptContentPanel(tabbedPane, this); } @Override @@ -69,11 +69,6 @@ public class JInputScript extends JEditableNode { return menu; } - @Override - public boolean isEditable() { - return true; - } - @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_KOTLIN; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 95f009a48..6cc501019 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -163,7 +163,6 @@ public class MainWindow extends JFrame { public static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15; private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile"); - private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall"); private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh"); private static final ImageIcon ICON_EXPORT = UiUtils.openSvgIcon("ui/export"); private static final ImageIcon ICON_EXIT = UiUtils.openSvgIcon("ui/exit"); @@ -1083,7 +1082,7 @@ public class MainWindow extends JFrame { }; closeMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.close_mappings")); - Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) { + Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), Icons.SAVE_ALL) { @Override public void actionPerformed(ActionEvent e) { saveAll(false); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 2593b2148..8fa428cae 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -33,6 +33,7 @@ import jadx.gui.ui.panel.ImagePanel; import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; public class TabbedPane extends JTabbedPane { private static final long serialVersionUID = -8833600618794570904L; @@ -332,7 +333,9 @@ public class TabbedPane extends JTabbedPane { private void addContentPanel(ContentPanel contentPanel) { openTabs.put(contentPanel.getNode(), contentPanel); - add(contentPanel); + int tabCount = getTabCount(); + add(contentPanel, tabCount); + setTabComponentAt(tabCount, makeTabComponent(contentPanel)); } public void closeCodePanel(ContentPanel contentPanel) { @@ -351,7 +354,6 @@ public class TabbedPane extends JTabbedPane { } FocusManager.listen(panel); addContentPanel(panel); - setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel)); } return panel; } @@ -364,6 +366,27 @@ public class TabbedPane extends JTabbedPane { } } + public void reloadInactiveTabs() { + UiUtils.uiThreadGuard(); + int tabCount = getTabCount(); + if (tabCount == 1) { + return; + } + int current = getSelectedIndex(); + for (int i = 0; i < tabCount; i++) { + if (i == current) { + continue; + } + JNode node = ((ContentPanel) getComponentAt(i)).getNode(); + ContentPanel panel = node.getContentPanel(this); + FocusManager.listen(panel); + openTabs.put(node, panel); + setComponentAt(i, panel); + setTabComponentAt(i, makeTabComponent(panel)); + } + fireStateChanged(); + } + @Nullable public ContentPanel getSelectedCodePanel() { return (ContentPanel) getSelectedComponent(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index b662a078f..ba093c81e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -20,7 +20,6 @@ import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JViewport; -import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; @@ -30,7 +29,6 @@ import javax.swing.text.BadLocationException; import javax.swing.text.Caret; import javax.swing.text.DefaultCaret; -import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Token; @@ -46,12 +44,9 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; -import jadx.gui.plugins.script.CompletionRenderer; -import jadx.gui.plugins.script.JadxScriptCompleteProvider; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; -import jadx.gui.treemodel.JInputScript; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; @@ -106,9 +101,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { JEditableNode editableNode = (JEditableNode) node; addSaveActions(editableNode); addChangeUpdates(editableNode); - if (node instanceof JInputScript) { - addAutoComplete(settings); - } } else { addCaretActions(); addFastCopyAction(); @@ -224,17 +216,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { })); } - private void addAutoComplete(JadxSettings settings) { - JadxScriptCompleteProvider provider = new JadxScriptCompleteProvider(this); - provider.setAutoActivationRules(false, "."); - AutoCompletion ac = new AutoCompletion(provider); - ac.setListCellRenderer(new CompletionRenderer(settings)); - ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, UiUtils.ctrlButton())); - ac.setAutoActivationEnabled(true); - ac.setAutoCompleteSingleChoices(true); - ac.install(this); - } - private String highlightCaretWord(String lastText, int pos) { String text = getWordByPosition(pos); if (StringUtils.isEmpty(text)) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SearchBar.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SearchBar.java index fb847285c..3efe94e87 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SearchBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SearchBar.java @@ -24,7 +24,7 @@ import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; -class SearchBar extends JToolBar { +public class SearchBar extends JToolBar { private static final long serialVersionUID = 1836871286618633003L; private static final Logger LOG = LoggerFactory.getLogger(SearchBar.class); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java index a10aae210..13a95b8fa 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java @@ -4,6 +4,7 @@ import javax.swing.JPanel; import org.jetbrains.annotations.Nullable; +import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.TabbedPane; @@ -45,6 +46,10 @@ public abstract class ContentPanel extends JPanel { return node.getName(); } + public JadxSettings getSettings() { + return tabbedPane.getMainWindow().getSettings(); + } + public void dispose() { tabbedPane = null; node = null; diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Icons.java b/jadx-gui/src/main/java/jadx/gui/utils/Icons.java index 72704cae1..7e20ac47a 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Icons.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Icons.java @@ -13,6 +13,8 @@ public class Icons { public static final ImageIcon CLOSE = openSvgIcon("ui/closeHovered"); public static final ImageIcon CLOSE_INACTIVE = openSvgIcon("ui/close"); + public static final ImageIcon SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall"); + public static final ImageIcon STATIC = openSvgIcon("nodes/staticMark"); public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark"); @@ -27,4 +29,8 @@ public class Icons { public static final ImageIcon FIELD = UiUtils.openSvgIcon("nodes/field"); public static final ImageIcon PROPERTY = UiUtils.openSvgIcon("nodes/property"); public static final ImageIcon PARAMETER = UiUtils.openSvgIcon("nodes/parameter"); + + public static final ImageIcon RUN = UiUtils.openSvgIcon("ui/run"); + public static final ImageIcon CHECK = UiUtils.openSvgIcon("ui/checkConstraint"); + public static final ImageIcon FORMAT = UiUtils.openSvgIcon("ui/toolWindowMessages"); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java index 324a611bd..9faed3a0f 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java @@ -1,12 +1,17 @@ package jadx.gui.utils.ui; import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.util.function.Consumer; import javax.swing.AbstractAction; import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.KeyStroke; +import jadx.gui.utils.UiUtils; + public class ActionHandler extends AbstractAction { private final Consumer consumer; @@ -40,8 +45,27 @@ public class ActionHandler extends AbstractAction { putValue(ACCELERATOR_KEY, keyStroke); } + public void attachKeyBindingFor(JComponent component, KeyStroke keyStroke) { + UiUtils.addKeyBinding(component, keyStroke, "run", this); + setKeyBinding(keyStroke); + } + + public void addKeyBindToDescription() { + KeyStroke keyStroke = (KeyStroke) getValue(ACCELERATOR_KEY); + if (keyStroke != null) { + String keyText = KeyEvent.getKeyText(keyStroke.getKeyCode()); + String desc = (String) getValue(SHORT_DESCRIPTION); + setShortDescription(desc + " (" + keyText + ")"); + } + } + @Override public void actionPerformed(ActionEvent e) { consumer.accept(e); } + + public JButton makeButton() { + addKeyBindToDescription(); + return new JButton(this); + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 7dd143b0e..6173de5fe 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -248,6 +248,11 @@ popup.search_global=Globale Suche "%s" #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + exclude_dialog.title=Paketauswahl exclude_dialog.ok=OK exclude_dialog.select_all=Alles auswählen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 6c85368ad..1f889cada 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -248,6 +248,11 @@ popup.add_files=Add files popup.add_scripts=Add scripts popup.new_script=New script +script.run=Run +script.save=Save +script.check=Check +script.format=Reformat + exclude_dialog.title=Package Selector exclude_dialog.ok=OK exclude_dialog.select_all=Select all diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 7b60464b8..6eb9ed0e6 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -248,6 +248,11 @@ popup.rename=Nimeta ümber #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + #exclude_dialog.title=Package Selector #exclude_dialog.ok=OK #exclude_dialog.select_all=Select all diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 8b6490b5b..8f2f7e13f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -248,6 +248,11 @@ popup.search_global="%s" 전역 검색 #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + exclude_dialog.title=패키지 선택기 exclude_dialog.ok=확인 exclude_dialog.select_all=모두 선택 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index e00e56834..7c5e9a29f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -248,6 +248,11 @@ popup.search_global=Busca global "%s" #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + exclude_dialog.title=Selecionar pacote exclude_dialog.ok=OK exclude_dialog.select_all=Selecionar tudo diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 5038107ca..aa3d6f6ec 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -248,6 +248,11 @@ popup.search_global=全局搜索 “%s” #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + exclude_dialog.title=选择要排除的包 exclude_dialog.ok=确定 exclude_dialog.select_all=全选 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index c00b2ceae..7e17835b5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -248,6 +248,11 @@ popup.search_global=全域搜尋 "%s" #popup.add_scripts=Add scripts #popup.new_script=New script +#script.run=Run +#script.save=Save +#script.check=Check +#script.format=Reformat + exclude_dialog.title=套件選擇 exclude_dialog.ok=OK exclude_dialog.select_all=全選 diff --git a/jadx-gui/src/main/resources/icons/ui/checkConstraint.svg b/jadx-gui/src/main/resources/icons/ui/checkConstraint.svg new file mode 100644 index 000000000..26e49cc32 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/ui/checkConstraint.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/jadx-gui/src/main/resources/icons/ui/run.svg b/jadx-gui/src/main/resources/icons/ui/run.svg new file mode 100644 index 000000000..413d80fe9 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/ui/run.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jadx-gui/src/main/resources/icons/ui/toolWindowMessages.svg b/jadx-gui/src/main/resources/icons/ui/toolWindowMessages.svg new file mode 100644 index 000000000..e259416ca --- /dev/null +++ b/jadx-gui/src/main/resources/icons/ui/toolWindowMessages.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/jadx-plugins/jadx-script/examples/build.gradle.kts b/jadx-plugins/jadx-script/examples/build.gradle.kts index 0fc0779f5..1df9cf83c 100644 --- a/jadx-plugins/jadx-script/examples/build.gradle.kts +++ b/jadx-plugins/jadx-script/examples/build.gradle.kts @@ -6,12 +6,18 @@ dependencies { implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") + // script context support in IDE is poor, use stubs and manual imports for now + // kotlinScriptDef(project(":jadx-plugins:jadx-script:jadx-script-runtime")) + // manual imports (IDE can't import dependencies by scripts annotations) implementation("com.github.javafaker:javafaker:1.0.2") } sourceSets { main { - java.srcDirs("scripts", "context") + kotlin.srcDirs( + "scripts", + "context" + ) } } diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/Complete.kt b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt similarity index 57% rename from jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/Complete.kt rename to jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt index 23163492c..1936a7acf 100644 --- a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/Complete.kt +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt @@ -3,11 +3,14 @@ package jadx.plugins.script.ide import jadx.plugins.script.runner.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.ScriptDiagnostic import kotlin.script.experimental.api.SourceCode import kotlin.script.experimental.api.SourceCodeCompletionVariant +import kotlin.script.experimental.api.analysisDiagnostics +import kotlin.script.experimental.api.renderedResultType import kotlin.script.experimental.api.valueOrNull import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.jvm.util.toSourceCodePosition @@ -19,14 +22,30 @@ data class ScriptCompletionResult( val reports: List ) -class JadxScriptAutoComplete(private val scriptName: String) { +data class ScriptAnalyzeResult( + val errors: List, + val renderType: String?, + val reports: List +) + +class ScriptCompiler(private val scriptName: String) { private val replCompiler = KJvmReplCompilerWithIdeServices() private val compileConf = ScriptEval().buildCompileConf() fun complete(code: String, cursor: Int): ScriptCompletionResult { val result = complete(code.toScriptSource(scriptName), cursor) return ScriptCompletionResult( - completions = result.valueOrNull()?.toList() ?: listOf(), + 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( + errors = analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.toList() ?: emptyList(), + renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType), reports = result.reports ) } @@ -36,4 +55,10 @@ class JadxScriptAutoComplete(private val scriptName: String) { replCompiler.complete(code, cursor.toSourceCodePosition(code), compileConf) } } + + private fun analyze(code: SourceCode, cursor: Int): ResultWithDiagnostics { + return runBlocking { + replCompiler.analyze(code, cursor.toSourceCodePosition(code), compileConf) + } + } } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt index 8bb727e30..4f134eac4 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt @@ -2,8 +2,8 @@ package jadx.plugins.script.runner import jadx.api.JadxDecompiler import jadx.api.plugins.JadxPluginContext -import jadx.plugins.script.runtime.JadxScript import jadx.plugins.script.runtime.JadxScriptData +import jadx.plugins.script.runtime.JadxScriptTemplate import jadx.plugins.script.runtime.data.JadxScriptAllOptions import mu.KotlinLogging import java.io.File @@ -43,10 +43,10 @@ class ScriptEval { processEvalResult(result, scriptFile) } - fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate() + fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate() fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration { - return createJvmEvaluationConfigurationFromTemplate { + return createJvmEvaluationConfigurationFromTemplate { constructorArgs(scriptData) } } diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts b/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts index bd50cbb09..970611365 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts +++ b/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts @@ -5,12 +5,13 @@ plugins { dependencies { api(project(":jadx-core")) - implementation("org.jetbrains.kotlin:kotlin-scripting-common") - implementation("org.jetbrains.kotlin:kotlin-scripting-jvm") + implementation(kotlin("stdlib")) + implementation(kotlin("scripting-common")) + implementation(kotlin("scripting-jvm")) // allow to use maven dependencies in scripts - implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies") - implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven") + implementation(kotlin("scripting-dependencies")) + implementation(kotlin("scripting-dependencies-maven")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Script.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt similarity index 84% rename from jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Script.kt rename to jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt index 69f1319db..7c8927abe 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Script.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt @@ -1,6 +1,7 @@ package jadx.plugins.script.runtime import kotlinx.coroutines.runBlocking +import mu.KotlinLogging import kotlin.script.experimental.annotations.KotlinScript import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ScriptAcceptedLocation @@ -9,11 +10,11 @@ import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptConfigurationRefinementContext import kotlin.script.experimental.api.acceptedLocations import kotlin.script.experimental.api.asSuccess -import kotlin.script.experimental.api.baseClass import kotlin.script.experimental.api.collectedAnnotations import kotlin.script.experimental.api.defaultImports import kotlin.script.experimental.api.dependencies import kotlin.script.experimental.api.ide +import kotlin.script.experimental.api.isStandalone import kotlin.script.experimental.api.onSuccess import kotlin.script.experimental.api.refineConfiguration import kotlin.script.experimental.api.with @@ -31,7 +32,22 @@ import kotlin.script.experimental.jvm.jvm fileExtension = "jadx.kts", compilationConfiguration = JadxScriptConfiguration::class ) -abstract class JadxScript +abstract class JadxScriptTemplate( + private val scriptData: JadxScriptData +) { + val scriptName = scriptData.scriptName + val log = KotlinLogging.logger("JadxScript:$scriptName") + + fun getJadxInstance() = JadxScriptInstance(scriptData, log) + + fun println(message: Any?) { + log.info(message?.toString()) + } + + fun print(message: Any?) { + log.info(message?.toString()) + } +} object JadxScriptConfiguration : ScriptCompilationConfiguration({ defaultImports(DependsOn::class, Repository::class) @@ -45,11 +61,11 @@ object JadxScriptConfiguration : ScriptCompilationConfiguration({ acceptedLocations(ScriptAcceptedLocation.Everywhere) } - baseClass(JadxScriptBaseClass::class) - refineConfiguration { onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations) } + + isStandalone(false) }) private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt index ffd1fbba0..98aa8481a 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt @@ -17,24 +17,8 @@ import jadx.plugins.script.runtime.data.Replace import jadx.plugins.script.runtime.data.Search import jadx.plugins.script.runtime.data.Stages import mu.KLogger -import mu.KotlinLogging import java.io.File -open class JadxScriptBaseClass(private val scriptData: JadxScriptData) { - val scriptName = scriptData.scriptName - val log = KotlinLogging.logger("JadxScript:$scriptName") - - fun getJadxInstance() = JadxScriptInstance(scriptData, log) - - fun println(message: Any?) { - log.info(message?.toString()) - } - - fun print(message: Any?) { - log.info(message?.toString()) - } -} - class JadxScriptData( val jadxInstance: JadxDecompiler, val pluginContext: JadxPluginContext, diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt index 64c0b4973..54950e666 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt @@ -45,7 +45,6 @@ class JadxScriptOptions( private val jadx: JadxScriptInstance, private val options: JadxScriptAllOptions ) { - fun register( name: String, desc: String, diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/resources/META-INF/kotlin/script/templates/jadx.plugins.script.runtime.JadxScriptTemplate b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/resources/META-INF/kotlin/script/templates/jadx.plugins.script.runtime.JadxScriptTemplate new file mode 100644 index 000000000..e69de29bb