feat: make jadx-script-kotlin plugin external
This commit is contained in:
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
|
||||
|
||||
implementation("org.openrewrite:plugin:6.19.1")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ plugins {
|
||||
id("org.jetbrains.kotlin.jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("reflect")) // don't work from plugin classloader
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
|
||||
@@ -19,7 +19,6 @@ dependencies {
|
||||
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
|
||||
|
||||
@@ -138,11 +138,16 @@ public final class JadxDecompiler implements Closeable {
|
||||
loadFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload passes and plugins without processing classes and inputs
|
||||
*/
|
||||
public void reloadPasses() {
|
||||
LOG.info("reloading (passes only) ...");
|
||||
customPasses.clear();
|
||||
root.resetPasses();
|
||||
events.reset();
|
||||
unloadPlugins();
|
||||
|
||||
loadPlugins();
|
||||
root.mergePasses(customPasses);
|
||||
root.restartVisitors();
|
||||
|
||||
@@ -148,7 +148,7 @@ public class JadxPluginManager {
|
||||
}
|
||||
context.init();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to init plugin: {}", context.getPluginId(), e);
|
||||
LOG.error("Failed to init plugin: {}", context.getPluginId(), e);
|
||||
}
|
||||
}
|
||||
for (PluginContext context : pluginContexts) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
|
||||
private final JadxPluginsData pluginsData;
|
||||
private final JadxPlugin plugin;
|
||||
private final JadxPluginInfo pluginInfo;
|
||||
private final ClassLoader pluginClassLoader;
|
||||
|
||||
private AppContext appContext;
|
||||
|
||||
@@ -53,16 +54,30 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
|
||||
this.pluginsData = pluginsData;
|
||||
this.plugin = plugin;
|
||||
this.pluginInfo = plugin.getPluginInfo();
|
||||
this.pluginClassLoader = plugin.getClass().getClassLoader();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
plugin.init(this);
|
||||
initialized = true;
|
||||
classLoaderWrap(() -> {
|
||||
plugin.init(this);
|
||||
initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
public void unload() {
|
||||
if (initialized) {
|
||||
plugin.unload();
|
||||
classLoaderWrap(plugin::unload);
|
||||
}
|
||||
}
|
||||
|
||||
public void classLoaderWrap(Runnable task) {
|
||||
Thread thread = Thread.currentThread();
|
||||
ClassLoader prevClassLoader = thread.getContextClassLoader();
|
||||
thread.setContextClassLoader(pluginClassLoader);
|
||||
try {
|
||||
task.run();
|
||||
} finally {
|
||||
thread.setContextClassLoader(prevClassLoader);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,21 +16,12 @@ dependencies {
|
||||
// import mappings
|
||||
implementation(project(":jadx-plugins:jadx-rename-mappings"))
|
||||
|
||||
// jadx-script autocomplete support
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-ide"))
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||
|
||||
// use KtLint for format and check jadx scripts
|
||||
implementation("com.pinterest.ktlint:ktlint-rule-engine:1.8.0")
|
||||
implementation("com.pinterest.ktlint:ktlint-ruleset-standard:1.8.0")
|
||||
|
||||
implementation("org.jcommander:jcommander:2.0")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.21")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
|
||||
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||
implementation("com.fifesoft:rsyntaxtextarea:3.6.1")
|
||||
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||
implementation("org.drjekyll:fontchooser:3.1.0")
|
||||
implementation("hu.kazocsaba:image-viewer:1.2.3")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0") // WebP support for image viewer
|
||||
|
||||
@@ -21,6 +21,9 @@ import jadx.api.JavaPackage;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.impl.InMemoryCodeCache;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.plugins.pass.JadxPassInfo;
|
||||
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo;
|
||||
import jadx.api.plugins.pass.types.JadxPreparePass;
|
||||
import jadx.api.usage.impl.EmptyUsageInfoCache;
|
||||
import jadx.api.usage.impl.InMemoryUsageInfoCache;
|
||||
import jadx.cli.JadxAppCommon;
|
||||
@@ -30,6 +33,7 @@ import jadx.core.dex.nodes.ProcessState;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.plugins.AppContext;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.cache.code.CodeCacheMode;
|
||||
import jadx.gui.cache.code.CodeStringCache;
|
||||
import jadx.gui.cache.code.disk.BufferCodeCache;
|
||||
import jadx.gui.cache.code.disk.DiskCodeCache;
|
||||
@@ -73,10 +77,9 @@ public class JadxWrapper {
|
||||
decompiler = new JadxDecompiler(jadxArgs);
|
||||
guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow);
|
||||
initUsageCache(jadxArgs);
|
||||
registerCodeCache(decompiler);
|
||||
decompiler.setEventsImpl(mainWindow.events());
|
||||
|
||||
decompiler.load();
|
||||
initCodeCache();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Jadx decompiler wrapper init error", e);
|
||||
@@ -114,22 +117,39 @@ public class JadxWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
private void initCodeCache() {
|
||||
switch (getSettings().getCodeCacheMode()) {
|
||||
case MEMORY:
|
||||
getArgs().setCodeCache(new InMemoryCodeCache());
|
||||
break;
|
||||
case DISK_WITH_CACHE:
|
||||
getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache()));
|
||||
break;
|
||||
case DISK:
|
||||
getArgs().setCodeCache(buildBufferedDiskCache());
|
||||
break;
|
||||
/**
|
||||
* Disk cache require loaded classes to operate, but cache should be set before 'after load' event
|
||||
* to allow plugins decompile classes with cache enabled.
|
||||
* To resolve this, register last 'prepare' pass for cache initialization.
|
||||
*/
|
||||
private void registerCodeCache(JadxDecompiler jadxDecompiler) {
|
||||
CodeCacheMode codeCacheMode = getSettings().getCodeCacheMode();
|
||||
if (codeCacheMode == CodeCacheMode.MEMORY) {
|
||||
jadxDecompiler.getArgs().setCodeCache(new InMemoryCodeCache());
|
||||
return;
|
||||
}
|
||||
jadxDecompiler.addCustomPass(new JadxPreparePass() {
|
||||
@Override
|
||||
public JadxPassInfo getInfo() {
|
||||
return new SimpleJadxPassInfo("CacheInit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
switch (getSettings().getCodeCacheMode()) {
|
||||
case DISK_WITH_CACHE:
|
||||
root.getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache(root)));
|
||||
break;
|
||||
case DISK:
|
||||
root.getArgs().setCodeCache(buildBufferedDiskCache(root));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private BufferCodeCache buildBufferedDiskCache() {
|
||||
DiskCodeCache diskCache = new DiskCodeCache(getDecompiler().getRoot(), getProject().getCacheDir());
|
||||
private BufferCodeCache buildBufferedDiskCache(RootNode root) {
|
||||
DiskCodeCache diskCache = new DiskCodeCache(root, getProject().getCacheDir());
|
||||
return new BufferCodeCache(diskCache);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
class LogAppender implements ILogListener {
|
||||
private final LogOptions options;
|
||||
private final RSyntaxTextArea textArea;
|
||||
@@ -39,7 +37,7 @@ class LogAppender implements ILogListener {
|
||||
return true;
|
||||
|
||||
case ALL_SCRIPTS:
|
||||
return logEvent.getLoggerName().startsWith(JADX_SCRIPT_LOG_PREFIX);
|
||||
return logEvent.getLoggerName().startsWith("JadxScript:");
|
||||
|
||||
case CURRENT_SCRIPT:
|
||||
return logEvent.getLoggerName().equals(options.getFilter());
|
||||
|
||||
@@ -6,8 +6,6 @@ import ch.qos.logback.classic.Level;
|
||||
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
public class LogOptions {
|
||||
|
||||
/**
|
||||
@@ -30,7 +28,7 @@ public class LogOptions {
|
||||
}
|
||||
|
||||
public static LogOptions forScript(String scriptName) {
|
||||
String filter = JADX_SCRIPT_LOG_PREFIX + scriptName;
|
||||
String filter = "JadxScript:" + scriptName;
|
||||
return store(new LogOptions(LogMode.CURRENT_SCRIPT, current.getLogLevel(), filter));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import ch.qos.logback.classic.Level;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
@@ -144,7 +143,7 @@ public class LogPanel extends JPanel {
|
||||
TabBlueprint selectedTab = mainWindow.getTabsController().getSelectedTab();
|
||||
if (selectedTab != null) {
|
||||
JNode node = selectedTab.getNode();
|
||||
if (node instanceof JInputScript) {
|
||||
if (node.getClass().getSimpleName().equals("JInputScript")) { // TODO: register custom log filters
|
||||
return node.getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.codearea.CodeArea;
|
||||
import jadx.gui.ui.codearea.JNodePopupBuilder;
|
||||
@@ -23,6 +24,8 @@ public class CommonGuiPluginsContext {
|
||||
|
||||
private final List<CodePopupAction> codePopupActionList = new ArrayList<>();
|
||||
private final List<TreePopupMenuEntry> treePopupMenuEntries = new ArrayList<>();
|
||||
private final List<ITreeInputCategory> treeInputCategories = new ArrayList<>();
|
||||
private final List<ITabStatePersist> tabStatePersistAdapters = new ArrayList<>();
|
||||
|
||||
public CommonGuiPluginsContext(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
@@ -38,9 +41,19 @@ public class CommonGuiPluginsContext {
|
||||
return pluginsMap.get(pluginContext);
|
||||
}
|
||||
|
||||
public @Nullable GuiPluginContext getGuiPluginContextById(String pluginId) {
|
||||
for (GuiPluginContext guiPluginContext : pluginsMap.values()) {
|
||||
if (guiPluginContext.getPluginContext().getPluginId().equals(pluginId)) {
|
||||
return guiPluginContext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
codePopupActionList.clear();
|
||||
treePopupMenuEntries.clear();
|
||||
treeInputCategories.clear();
|
||||
mainWindow.resetPluginsMenu();
|
||||
}
|
||||
|
||||
@@ -56,6 +69,14 @@ public class CommonGuiPluginsContext {
|
||||
return treePopupMenuEntries;
|
||||
}
|
||||
|
||||
public List<ITreeInputCategory> getTreeInputCategories() {
|
||||
return treeInputCategories;
|
||||
}
|
||||
|
||||
public List<ITabStatePersist> getTabStatePersistAdapters() {
|
||||
return tabStatePersistAdapters;
|
||||
}
|
||||
|
||||
public void addMenuAction(String name, Runnable action) {
|
||||
ActionHandler item = new ActionHandler(ev -> {
|
||||
try {
|
||||
|
||||
@@ -26,6 +26,7 @@ import jadx.api.plugins.gui.JadxGuiContext;
|
||||
import jadx.api.plugins.gui.JadxGuiSettings;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
|
||||
@@ -82,6 +83,14 @@ public class GuiPluginContext implements JadxGuiContext {
|
||||
commonContext.getTreePopupMenuEntries().add(new TreePopupMenuEntry(name, addPredicate, action));
|
||||
}
|
||||
|
||||
public void registerTreeInputCategory(ITreeInputCategory inputCategory) {
|
||||
commonContext.getTreeInputCategories().add(inputCategory);
|
||||
}
|
||||
|
||||
public void registerTabStatePersistAdapter(ITabStatePersist tabStatePersist) {
|
||||
commonContext.getTabStatePersistAdapters().add(tabStatePersist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) {
|
||||
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package jadx.gui.plugins.context;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
/**
|
||||
* Custom category for 'Inputs' tree section
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
public interface ITreeInputCategory {
|
||||
|
||||
/**
|
||||
* Check if file should be moved into this category
|
||||
*/
|
||||
boolean filesFilter(Path file);
|
||||
|
||||
/**
|
||||
* Build node for filtered files
|
||||
*/
|
||||
JNode buildInputNode(List<Path> files);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import org.fife.ui.autocomplete.AutoCompletion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.gui.jobs.IBackgroundTask;
|
||||
import jadx.gui.jobs.LoadTask;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.ui.action.JadxAutoCompletion;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
import jadx.gui.utils.shortcut.ShortcutsController;
|
||||
|
||||
public class ScriptCodeArea extends AbstractCodeArea {
|
||||
|
||||
private final JInputScript scriptNode;
|
||||
private final AutoCompletion autoCompletion;
|
||||
private final ShortcutsController shortcutsController;
|
||||
|
||||
public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) {
|
||||
super(contentPanel, node);
|
||||
scriptNode = node;
|
||||
|
||||
setSyntaxEditingStyle(node.getSyntaxName());
|
||||
setCodeFoldingEnabled(true);
|
||||
setCloseCurlyBraces(true);
|
||||
|
||||
shortcutsController = contentPanel.getMainWindow().getShortcutsController();
|
||||
JadxSettings settings = contentPanel.getMainWindow().getSettings();
|
||||
autoCompletion = addAutoComplete(settings);
|
||||
}
|
||||
|
||||
private AutoCompletion addAutoComplete(JadxSettings settings) {
|
||||
ScriptCompleteProvider provider = new ScriptCompleteProvider(this);
|
||||
provider.setAutoActivationRules(false, ".");
|
||||
JadxAutoCompletion ac = new JadxAutoCompletion(provider);
|
||||
ac.setListCellRenderer(new ScriptCompletionRenderer(settings));
|
||||
ac.setAutoActivationEnabled(true);
|
||||
ac.setAutoCompleteSingleChoices(true);
|
||||
ac.install(this);
|
||||
shortcutsController.bindImmediate(ac);
|
||||
return ac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
return node.getCodeInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBackgroundTask getLoadTask() {
|
||||
return new LoadTask<>(
|
||||
() -> node.getCodeInfo().getCodeStr(),
|
||||
code -> {
|
||||
setText(code);
|
||||
setCaretPosition(0);
|
||||
setLoaded();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
setText(node.getCodeInfo().getCodeStr());
|
||||
}
|
||||
|
||||
public void updateCode(String newCode) {
|
||||
int caretPos = getCaretPosition();
|
||||
setText(newCode);
|
||||
setCaretPosition(caretPos);
|
||||
scriptNode.setChanged(true);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
scriptNode.save(getText());
|
||||
scriptNode.setChanged(false);
|
||||
}
|
||||
|
||||
public JInputScript getScriptNode() {
|
||||
return scriptNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
shortcutsController.unbindActionsForComponent(this);
|
||||
autoCompletion.uninstall();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionProviderBase;
|
||||
import org.fife.ui.autocomplete.ParameterizedCompletion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic;
|
||||
import kotlin.script.experimental.api.SourceCode;
|
||||
import kotlin.script.experimental.api.SourceCodeCompletionVariant;
|
||||
|
||||
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.ScriptCompletionResult;
|
||||
import jadx.plugins.script.ide.ScriptServices;
|
||||
|
||||
import static jadx.plugins.script.ide.ScriptServicesKt.AUTO_COMPLETE_INSERT_STR;
|
||||
|
||||
public class ScriptCompleteProvider extends CompletionProviderBase {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScriptCompleteProvider.class);
|
||||
|
||||
private static final Map<String, Icon> ICONS_MAP = buildIconsMap();
|
||||
|
||||
private static Map<String, Icon> buildIconsMap() {
|
||||
Map<String, Icon> map = new HashMap<>();
|
||||
map.put("class", Icons.CLASS);
|
||||
map.put("method", Icons.METHOD);
|
||||
map.put("field", Icons.FIELD);
|
||||
map.put("property", Icons.PROPERTY);
|
||||
map.put("parameter", Icons.PARAMETER);
|
||||
map.put("package", Icons.PACKAGE);
|
||||
return map;
|
||||
}
|
||||
|
||||
private final AbstractCodeArea codeArea;
|
||||
private ScriptServices scriptServices;
|
||||
|
||||
public ScriptCompleteProvider(AbstractCodeArea codeArea) {
|
||||
this.codeArea = codeArea;
|
||||
}
|
||||
|
||||
private List<Completion> getCompletions() {
|
||||
try {
|
||||
String code = codeArea.getText();
|
||||
int caretPos = codeArea.getCaretPosition();
|
||||
// TODO: resolve error after reusing ScriptCompiler
|
||||
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());
|
||||
}
|
||||
return convertCompletions(result.getCompletions(), code, replacePos);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Code completion failed", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Completion> convertCompletions(List<SourceCodeCompletionVariant> completions, String code, int replacePos) {
|
||||
if (completions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
String cmplStr = completions.stream().map(SourceCodeCompletionVariant::toString).collect(Collectors.joining("\n"));
|
||||
LOG.debug("Completions:\n{}", cmplStr);
|
||||
}
|
||||
int count = completions.size();
|
||||
List<Completion> list = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
SourceCodeCompletionVariant c = completions.get(i);
|
||||
if (Objects.equals(c.getIcon(), "keyword")) {
|
||||
// too many, not very useful
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
cmpl.setSummary(c.getDisplayText() + " " + c.getTail());
|
||||
} else {
|
||||
cmpl.setSummary(c.getTail());
|
||||
}
|
||||
cmpl.setToolTip(c.getDisplayText());
|
||||
Icon icon = ICONS_MAP.get(c.getIcon());
|
||||
cmpl.setIcon(icon != null ? icon : Icons.FILE);
|
||||
list.add(cmpl);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private int getReplacePos(int caretPos, ScriptCompletionResult result) throws BadLocationException {
|
||||
int lineRaw = codeArea.getLineOfOffset(caretPos);
|
||||
int lineStart = codeArea.getLineStartOffset(lineRaw);
|
||||
int line = lineRaw + 1;
|
||||
int col = caretPos - lineStart + 1;
|
||||
|
||||
List<ScriptDiagnostic> reports = result.getReports();
|
||||
ScriptDiagnostic cmplReport = ListUtils.filterOnlyOne(reports, r -> {
|
||||
if (r.getSeverity() == ScriptDiagnostic.Severity.ERROR && r.getLocation() != null) {
|
||||
SourceCode.Position start = r.getLocation().getStart();
|
||||
return start.getLine() == line && r.getMessage().endsWith(AUTO_COMPLETE_INSERT_STR);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (cmplReport == null) {
|
||||
LOG.warn("Failed to find completion report in: {}", reports);
|
||||
return caretPos;
|
||||
}
|
||||
reports.remove(cmplReport);
|
||||
int reportCol = Objects.requireNonNull(cmplReport.getLocation()).getStart().getCol();
|
||||
return caretPos - (col - reportCol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlreadyEnteredText(JTextComponent comp) {
|
||||
try {
|
||||
int pos = codeArea.getCaretPosition();
|
||||
return codeArea.getText(0, pos);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to get text before caret", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Completion> getCompletionsAt(JTextComponent comp, Point p) {
|
||||
return getCompletions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Completion> getCompletionsImpl(JTextComponent comp) {
|
||||
return getCompletions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterizedCompletion> getParameterizedCompletions(JTextComponent tc) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionProvider;
|
||||
|
||||
public class ScriptCompletionData implements Completion {
|
||||
|
||||
private final CompletionProvider provider;
|
||||
private final int relevance;
|
||||
|
||||
private String input;
|
||||
private String code;
|
||||
private int replacePos;
|
||||
private Icon icon;
|
||||
private String summary;
|
||||
private String toolTip;
|
||||
|
||||
public ScriptCompletionData(CompletionProvider provider, int relevance) {
|
||||
this.provider = provider;
|
||||
this.relevance = relevance;
|
||||
}
|
||||
|
||||
public void setData(String input, String code, int replacePos) {
|
||||
this.input = input;
|
||||
this.code = code;
|
||||
this.replacePos = replacePos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputText() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionProvider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlreadyEntered(JTextComponent comp) {
|
||||
return provider.getAlreadyEnteredText(comp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRelevance() {
|
||||
return relevance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReplacementText() {
|
||||
return code.substring(0, replacePos) + input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(Icon icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText() {
|
||||
return toolTip;
|
||||
}
|
||||
|
||||
public void setToolTip(String toolTip) {
|
||||
this.toolTip = toolTip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Completion other) {
|
||||
return Integer.compare(relevance, other.getRelevance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import javax.swing.JList;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionCellRenderer;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
|
||||
import static jadx.gui.utils.UiUtils.escapeHtml;
|
||||
import static jadx.gui.utils.UiUtils.fadeHtml;
|
||||
import static jadx.gui.utils.UiUtils.wrapHtml;
|
||||
|
||||
public class ScriptCompletionRenderer extends CompletionCellRenderer {
|
||||
|
||||
public ScriptCompletionRenderer(JadxSettings settings) {
|
||||
setDisplayFont(settings.getCodeFont());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareForOtherCompletion(JList list, Completion c, int index, boolean selected, boolean hasFocus) {
|
||||
ScriptCompletionData cmpl = (ScriptCompletionData) c;
|
||||
setText(wrapHtml(escapeHtml(cmpl.getInputText()) + " "
|
||||
+ fadeHtml(escapeHtml(cmpl.getSummary()))));
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
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 kotlin.script.experimental.api.ScriptDiagnostic;
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic.Severity;
|
||||
|
||||
import jadx.gui.logs.LogOptions;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.settings.LineNumbersMode;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.action.ActionModel;
|
||||
import jadx.gui.ui.action.JadxGuiAction;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
|
||||
import jadx.gui.ui.codearea.SearchBar;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.utils.Icons;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.NodeLabel;
|
||||
import jadx.plugins.script.ide.ScriptAnalyzeResult;
|
||||
import jadx.plugins.script.ide.ScriptServices;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
public class ScriptContentPanel extends AbstractCodeContentPanel {
|
||||
private static final long serialVersionUID = 6575696321112417513L;
|
||||
|
||||
private final ScriptCodeArea scriptArea;
|
||||
private final SearchBar searchBar;
|
||||
private final RTextScrollPane codeScrollPane;
|
||||
private final JPanel actionPanel;
|
||||
private final JLabel resultLabel;
|
||||
private final ScriptErrorService errorService;
|
||||
private final Logger scriptLog;
|
||||
|
||||
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);
|
||||
scriptLog = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.getName());
|
||||
|
||||
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() {
|
||||
JadxGuiAction runAction = new JadxGuiAction(ActionModel.SCRIPT_RUN, this::runScript);
|
||||
JadxGuiAction saveAction = new JadxGuiAction(ActionModel.SCRIPT_SAVE, scriptArea::save);
|
||||
|
||||
runAction.setShortcutComponent(scriptArea);
|
||||
saveAction.setShortcutComponent(scriptArea);
|
||||
|
||||
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(runAction);
|
||||
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(saveAction);
|
||||
|
||||
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());
|
||||
JButton scriptLog = new JButton(NLS.str("script.log"), Icons.FORMAT);
|
||||
scriptLog.addActionListener(ev -> showScriptLog());
|
||||
|
||||
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());
|
||||
panel.add(scriptLog);
|
||||
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().reloadPasses();
|
||||
} catch (Exception e) {
|
||||
scriptLog.error("Passes reload failed", e);
|
||||
}
|
||||
}, taskStatus -> {
|
||||
mainWindow.passesReloaded();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean checkScript() {
|
||||
try {
|
||||
resetResultLabel();
|
||||
String code = scriptArea.getText();
|
||||
String fileName = scriptArea.getNode().getName();
|
||||
|
||||
ScriptServices scriptServices = new ScriptServices();
|
||||
ScriptAnalyzeResult result = scriptServices.analyze(fileName, code);
|
||||
boolean success = result.getSuccess();
|
||||
List<ScriptDiagnostic> issues = result.getIssues();
|
||||
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 if (severity == Severity.WARNING) {
|
||||
scriptLog.warn("Compile issue: {}", issue);
|
||||
}
|
||||
}
|
||||
List<JadxLintError> lintErrs = Collections.emptyList();
|
||||
if (success) {
|
||||
lintErrs = getLintIssues(code);
|
||||
}
|
||||
|
||||
errorService.clearErrors();
|
||||
errorService.addCompilerIssues(issues);
|
||||
errorService.addLintErrors(lintErrs);
|
||||
if (!success) {
|
||||
resultLabel.setText("Compile issues: " + issues.size());
|
||||
showScriptLog();
|
||||
} else if (!lintErrs.isEmpty()) {
|
||||
resultLabel.setText("Lint issues: " + lintErrs.size());
|
||||
} else {
|
||||
resultLabel.setText("OK");
|
||||
}
|
||||
errorService.apply();
|
||||
return success;
|
||||
} catch (Throwable e) {
|
||||
scriptLog.error("Failed to check code", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private List<JadxLintError> getLintIssues(String code) {
|
||||
try {
|
||||
List<JadxLintError> lintErrs = KtLintUtils.INSTANCE.lint(code);
|
||||
for (JadxLintError error : lintErrs) {
|
||||
scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})",
|
||||
error.getDetail(), error.getLine(), error.getCol(), error.getRuleId());
|
||||
}
|
||||
return lintErrs;
|
||||
} catch (Throwable e) { // can throw initialization error
|
||||
scriptLog.warn("KtLint failed", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private void reformatCode() {
|
||||
resetResultLabel();
|
||||
try {
|
||||
String code = scriptArea.getText();
|
||||
String formattedCode = KtLintUtils.INSTANCE.format(code);
|
||||
if (!code.equals(formattedCode)) {
|
||||
scriptArea.updateCode(formattedCode);
|
||||
resultLabel.setText("Code updated");
|
||||
errorService.clearErrors();
|
||||
}
|
||||
} catch (Throwable e) { // can throw initialization error
|
||||
scriptLog.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.getCodeFont());
|
||||
scriptArea.loadSettings();
|
||||
}
|
||||
|
||||
private void showScriptLog() {
|
||||
getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractCodeArea getCodeArea() {
|
||||
return scriptArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getChildrenComponent() {
|
||||
return getCodeArea();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSettings() {
|
||||
applySettings();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
scriptArea.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
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 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();
|
||||
scriptArea.requestFocus();
|
||||
jumpCaretToFirstError();
|
||||
}
|
||||
|
||||
private void jumpCaretToFirstError() {
|
||||
List<ParserNotice> parserNotices = result.getNotices();
|
||||
if (parserNotices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ParserNotice notice = parserNotices.get(0);
|
||||
int offset = notice.getOffset();
|
||||
if (offset == -1) {
|
||||
try {
|
||||
offset = scriptArea.getLineStartOffset(notice.getLine());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to jump to first error", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
scriptArea.scrollToPos(offset);
|
||||
}
|
||||
|
||||
public void addCompilerIssues(List<ScriptDiagnostic> issues) {
|
||||
for (ScriptDiagnostic issue : issues) {
|
||||
if (issue.getSeverity() == ScriptDiagnostic.Severity.DEBUG) {
|
||||
continue;
|
||||
}
|
||||
DefaultParserNotice notice;
|
||||
SourceCode.Location loc = issue.getLocation();
|
||||
if (loc == null) {
|
||||
notice = new DefaultParserNotice(this, issue.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, issue.getMessage(), line, offset - 1, len);
|
||||
notice.setLevel(convertLevel(issue.getSeverity()));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to convert script issue", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
addNotice(notice);
|
||||
}
|
||||
}
|
||||
|
||||
private static ParserNotice.Level convertLevel(ScriptDiagnostic.Severity severity) {
|
||||
switch (severity) {
|
||||
case FATAL:
|
||||
case ERROR:
|
||||
return ParserNotice.Level.ERROR;
|
||||
case WARNING:
|
||||
return ParserNotice.Level.WARNING;
|
||||
case INFO:
|
||||
case DEBUG:
|
||||
return ParserNotice.Level.INFO;
|
||||
}
|
||||
return ParserNotice.Level.ERROR;
|
||||
}
|
||||
|
||||
public void addLintErrors(List<JadxLintError> errors) {
|
||||
for (JadxLintError 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);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public class JadxProject {
|
||||
private static final int SEARCH_HISTORY_LIMIT = 30;
|
||||
|
||||
private final transient MainWindow mainWindow;
|
||||
private final transient TabStateViewAdapter tabStateViewAdapter = new TabStateViewAdapter();
|
||||
|
||||
private transient String name = "New Project";
|
||||
private transient @Nullable Path projectPath;
|
||||
@@ -155,7 +156,7 @@ public class JadxProject {
|
||||
|
||||
public void saveOpenTabs(List<EditorViewState> tabs) {
|
||||
List<TabViewState> tabStateList = tabs.stream()
|
||||
.map(TabStateViewAdapter::build)
|
||||
.map(tabStateViewAdapter::build)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
if (data.setOpenTabs(tabStateList)) {
|
||||
@@ -164,8 +165,9 @@ public class JadxProject {
|
||||
}
|
||||
|
||||
public List<EditorViewState> getOpenTabs(MainWindow mw) {
|
||||
tabStateViewAdapter.setCustomAdapters(mw.getWrapper().getGuiPluginsContext().getTabStatePersistAdapters());
|
||||
return data.getOpenTabs().stream()
|
||||
.map(s -> TabStateViewAdapter.load(mw, s))
|
||||
.map(s -> tabStateViewAdapter.load(mw, s))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package jadx.gui.settings;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.plugins.mappings.JInputMapping;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.settings.data.TabViewState;
|
||||
import jadx.gui.settings.data.ViewPoint;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.treemodel.JSubResource;
|
||||
@@ -20,8 +24,9 @@ import jadx.gui.utils.UiUtils;
|
||||
public class TabStateViewAdapter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TabStateViewAdapter.class);
|
||||
|
||||
@Nullable
|
||||
public static TabViewState build(EditorViewState viewState) {
|
||||
private final Map<String, ITabStatePersist> customAdaptersMap = new HashMap<>();
|
||||
|
||||
public @Nullable TabViewState build(EditorViewState viewState) {
|
||||
TabViewState tvs = new TabViewState();
|
||||
tvs.setSubPath(viewState.getSubPath());
|
||||
if (!saveJNode(tvs, viewState.getNode())) {
|
||||
@@ -40,8 +45,7 @@ public class TabStateViewAdapter {
|
||||
return tvs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EditorViewState load(MainWindow mw, TabViewState tvs) {
|
||||
public @Nullable EditorViewState load(MainWindow mw, TabViewState tvs) {
|
||||
try {
|
||||
JNode node = loadJNode(mw, tvs);
|
||||
if (node == null) {
|
||||
@@ -63,8 +67,15 @@ public class TabStateViewAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCustomAdapters(List<ITabStatePersist> customAdapters) {
|
||||
customAdaptersMap.clear();
|
||||
for (ITabStatePersist customAdapter : customAdapters) {
|
||||
customAdaptersMap.put(customAdapter.getNodeClass().getName(), customAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JNode loadJNode(MainWindow mw, TabViewState tvs) {
|
||||
private JNode loadJNode(MainWindow mw, TabViewState tvs) {
|
||||
switch (tvs.getType()) {
|
||||
case "class":
|
||||
JavaClass javaClass = mw.getWrapper().searchJavaClassByRawName(tvs.getTabPath());
|
||||
@@ -85,18 +96,21 @@ public class TabStateViewAdapter {
|
||||
}
|
||||
return null;
|
||||
|
||||
case "script":
|
||||
return mw.getTreeRoot()
|
||||
.followStaticPath("JInputs", "JInputScripts")
|
||||
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
|
||||
|
||||
case "mapping":
|
||||
return mw.getTreeRoot().followStaticPath("JInputs").searchNode(node -> node instanceof JInputMapping);
|
||||
}
|
||||
ITabStatePersist statePersist = customAdaptersMap.get(tvs.getType());
|
||||
if (statePersist != null) {
|
||||
try {
|
||||
return statePersist.load(tvs.getTabPath());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to restore tab for custom node adapter: {}", tvs.getType(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean saveJNode(TabViewState tvs, JNode node) {
|
||||
private boolean saveJNode(TabViewState tvs, JNode node) {
|
||||
if (node instanceof JClass) {
|
||||
tvs.setType("class");
|
||||
tvs.setTabPath(((JClass) node).getCls().getRawName());
|
||||
@@ -113,15 +127,22 @@ public class TabStateViewAdapter {
|
||||
tvs.setTabPath(node.getName());
|
||||
return true;
|
||||
}
|
||||
if (node instanceof JInputScript) {
|
||||
tvs.setType("script");
|
||||
tvs.setTabPath(node.getName());
|
||||
return true;
|
||||
}
|
||||
if (node instanceof JInputMapping) {
|
||||
tvs.setType("mapping");
|
||||
return true;
|
||||
}
|
||||
|
||||
String typeName = node.getClass().getName();
|
||||
ITabStatePersist statePersist = customAdaptersMap.get(typeName);
|
||||
if (statePersist != null) {
|
||||
try {
|
||||
tvs.setTabPath(statePersist.save(node));
|
||||
tvs.setType(statePersist.getNodeClass().getName());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to save state for custom node: {}", typeName, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package jadx.gui.settings.data;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
/**
|
||||
* Adapter interface to allow save/load state of opened tabs
|
||||
*/
|
||||
public interface ITabStatePersist {
|
||||
|
||||
Class<? extends JNode> getNodeClass();
|
||||
|
||||
String save(JNode node);
|
||||
|
||||
@Nullable
|
||||
JNode load(String stateStr);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JPopupMenu;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.panel.ContentPanel;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.SimpleMenuItem;
|
||||
|
||||
public class JInputScript extends JEditableNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JInputScript.class);
|
||||
|
||||
private static final ImageIcon SCRIPT_ICON = UiUtils.openSvgIcon("nodes/kotlin_script");
|
||||
|
||||
private final Path scriptPath;
|
||||
private final String name;
|
||||
|
||||
public JInputScript(Path scriptPath) {
|
||||
this.scriptPath = scriptPath;
|
||||
this.name = scriptPath.getFileName().toString().replace(".jadx.kts", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
|
||||
return new ScriptContentPanel(tabbedPane, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
try {
|
||||
return new SimpleCodeInfo(FileUtils.readFile(scriptPath));
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String newContent) {
|
||||
try {
|
||||
FileUtils.writeFile(scriptPath, newContent);
|
||||
LOG.debug("Script saved: {}", scriptPath.toAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(scriptPath)));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.rename"), () -> mainWindow.renameInput(scriptPath)));
|
||||
return menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSyntaxName() {
|
||||
return SyntaxConstants.SYNTAX_STYLE_KOTLIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JClass getJParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return SCRIPT_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTooltip() {
|
||||
return scriptPath.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JPopupMenu;
|
||||
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.SimpleMenuItem;
|
||||
|
||||
public class JInputScripts extends JNode {
|
||||
private static final ImageIcon INPUT_SCRIPTS_ICON = UiUtils.openSvgIcon("nodes/scriptsModel");
|
||||
|
||||
public JInputScripts(List<Path> scripts) {
|
||||
for (Path script : scripts) {
|
||||
add(new JInputScript(script));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
|
||||
return menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JClass getJParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return INPUT_SCRIPTS_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return "JInputScripts";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return NLS.str("tree.input_scripts");
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,29 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.plugins.TreeInputsHelper;
|
||||
|
||||
public class JInputs extends JNode {
|
||||
private static final ImageIcon INPUTS_ICON = UiUtils.openSvgIcon("nodes/projectStructure");
|
||||
|
||||
public JInputs(JadxWrapper wrapper) {
|
||||
JadxProject project = wrapper.getProject();
|
||||
public JInputs(MainWindow mainWindow) {
|
||||
JadxProject project = mainWindow.getProject();
|
||||
List<Path> inputs = project.getFilePaths();
|
||||
List<Path> files = FileUtils.expandDirs(inputs);
|
||||
List<Path> scripts = new ArrayList<>();
|
||||
Iterator<Path> it = files.iterator();
|
||||
while (it.hasNext()) {
|
||||
Path file = it.next();
|
||||
if (file.getFileName().toString().endsWith(".jadx.kts")) {
|
||||
scripts.add(file);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
add(new JInputFiles(files));
|
||||
add(new JInputScripts(scripts));
|
||||
TreeInputsHelper inputsHelper = new TreeInputsHelper(mainWindow);
|
||||
inputsHelper.processInputs(files);
|
||||
add(new JInputFiles(inputsHelper.getSimpleFiles()));
|
||||
inputsHelper.getCustomNodes().forEach(this::add);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,6 +19,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.treemodel.JResource.JResType;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
@@ -28,18 +29,20 @@ public class JRoot extends JNode {
|
||||
private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/rootPackageFolder");
|
||||
|
||||
private final transient JadxWrapper wrapper;
|
||||
private final transient MainWindow mainWindow;
|
||||
|
||||
private transient boolean flatPackages = false;
|
||||
|
||||
private final List<JNode> customNodes = new ArrayList<>();
|
||||
private final transient List<JNode> customNodes = new ArrayList<>();
|
||||
|
||||
public JRoot(JadxWrapper wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
public JRoot(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.wrapper = mainWindow.getWrapper();
|
||||
}
|
||||
|
||||
public final void update() {
|
||||
removeAllChildren();
|
||||
add(new JInputs(wrapper));
|
||||
add(new JInputs(mainWindow));
|
||||
add(new JSources(this, wrapper));
|
||||
|
||||
List<ResourceFile> resources = wrapper.getResources();
|
||||
|
||||
@@ -828,7 +828,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
public void initTree() {
|
||||
treeRoot = new JRoot(wrapper);
|
||||
treeRoot = new JRoot(this);
|
||||
treeRoot.setFlatPackages(isFlattenPackage);
|
||||
treeModel.setRoot(treeRoot);
|
||||
addTreeCustomNodes();
|
||||
|
||||
@@ -3,13 +3,13 @@ package jadx.gui.utils;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
public class IconsCache {
|
||||
|
||||
private static final Map<String, FlatSVGIcon> SVG_ICONS = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ImageIcon> SVG_ICONS = new ConcurrentHashMap<>();
|
||||
|
||||
public static FlatSVGIcon getSVGIcon(String name) {
|
||||
public static ImageIcon getSVGIcon(String name) {
|
||||
return SVG_ICONS.computeIfAbsent(name, UiUtils::openSvgIcon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class UiUtils {
|
||||
private UiUtils() {
|
||||
}
|
||||
|
||||
public static FlatSVGIcon openSvgIcon(String name) {
|
||||
public static ImageIcon openSvgIcon(String name) {
|
||||
String iconPath = "icons/" + name + ".svg";
|
||||
FlatSVGIcon icon = new FlatSVGIcon(iconPath);
|
||||
boolean found;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package jadx.gui.utils.plugins;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.plugins.context.ITreeInputCategory;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
|
||||
public class TreeInputsHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TreeInputsHelper.class);
|
||||
|
||||
private final List<CategoryData> categoryData;
|
||||
private List<Path> simpleFiles;
|
||||
|
||||
public TreeInputsHelper(MainWindow mainWindow) {
|
||||
categoryData = mainWindow.getWrapper().getGuiPluginsContext()
|
||||
.getTreeInputCategories()
|
||||
.stream()
|
||||
.map(CategoryData::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void processInputs(List<Path> files) {
|
||||
simpleFiles = new ArrayList<>(files.size());
|
||||
for (Path file : files) {
|
||||
boolean added = false;
|
||||
for (CategoryData data : categoryData) {
|
||||
if (data.filesFilter(file)) {
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
simpleFiles.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<JNode> getCustomNodes() {
|
||||
return categoryData.stream()
|
||||
.filter(CategoryData::notEmpty)
|
||||
.map(CategoryData::buildInputNode)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<Path> getSimpleFiles() {
|
||||
return simpleFiles;
|
||||
}
|
||||
|
||||
private static final class CategoryData {
|
||||
private final ITreeInputCategory provider;
|
||||
private final List<Path> collectedFiles = new ArrayList<>();
|
||||
|
||||
private CategoryData(ITreeInputCategory provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public boolean filesFilter(Path file) {
|
||||
try {
|
||||
if (provider.filesFilter(file)) {
|
||||
collectedFiles.add(file);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to filter input files", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable JNode buildInputNode() {
|
||||
try {
|
||||
return provider.buildInputNode(collectedFiles);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to build custom input node", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean notEmpty() {
|
||||
return !collectedFiles.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -116,12 +117,25 @@ public class TestI18n {
|
||||
fail("I18n file: " + path.getFileName() + " and " + DEFAULT_LANG_FILE + " differ in line " + line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary solution to allow use I18N strings in plugins until proper API implemented
|
||||
*/
|
||||
private static final List<String> EXCLUDED_KEYS = Arrays.asList(
|
||||
// keys from `jadx-script-kotlin`
|
||||
"tree.input_scripts",
|
||||
"popup.new_script",
|
||||
"popup.add_scripts",
|
||||
"script.log",
|
||||
"script.format",
|
||||
"script.check");
|
||||
|
||||
@Test
|
||||
public void keyIsUsed() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (Reader reader = Files.newBufferedReader(i18nPath.resolve(DEFAULT_LANG_FILE))) {
|
||||
properties.load(reader);
|
||||
}
|
||||
EXCLUDED_KEYS.forEach(properties.keySet()::remove);
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (Object key : properties.keySet()) {
|
||||
keys.add("\"" + key + '"');
|
||||
|
||||
@@ -26,6 +26,7 @@ import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.plugins.versions.VerifyRequiredVersion;
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.plugins.tools.data.JadxInstalledPlugins;
|
||||
import jadx.plugins.tools.data.JadxPluginMetadata;
|
||||
@@ -83,12 +84,13 @@ public class JadxPluginsTools {
|
||||
install(pluginMetadata);
|
||||
return pluginMetadata;
|
||||
}
|
||||
rejectedVersions.add(" version " + pluginMetadata.getVersion()
|
||||
+ " not compatible, require: " + pluginMetadata.getRequiredJadxVersion());
|
||||
String pluginVersion = Utils.getOrElse(pluginMetadata.getVersion(), "unknown");
|
||||
rejectedVersions.add(" version '" + pluginVersion + "' not compatible, require: "
|
||||
+ pluginMetadata.getRequiredJadxVersion());
|
||||
}
|
||||
throw new JadxRuntimeException("Can't find compatible version to install"
|
||||
+ ", current jadx version: " + verifyRequiredVersion.getJadxVersion()
|
||||
+ "\nrejected versions:\n"
|
||||
+ "\nrejected plugin versions:\n"
|
||||
+ String.join("\n", rejectedVersions));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ plugins {
|
||||
dependencies {
|
||||
api(project(":jadx-core"))
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10")
|
||||
|
||||
testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output)
|
||||
testImplementation("org.apache.commons:commons-lang3:3.20.0")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
## JADX scripting support
|
||||
|
||||
### Examples
|
||||
|
||||
Check script examples in [`examples/`](https://github.com/skylot/jadx/tree/master/jadx-plugins/jadx-script-kotlin/examples/)(start with [`hello`](https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-script-kotlin/examples/hello.jadx.kts))
|
||||
|
||||
### Script usage
|
||||
|
||||
#### In jadx-cli
|
||||
|
||||
Just add script file as input
|
||||
|
||||
#### In jadx-gui
|
||||
|
||||
1. Add script file to the project (using `Add files` or `New script` by right-click menu on `Inputs/Scripts`)
|
||||
2. Script will appear in `Inputs/Scripts` section
|
||||
3. After script change, you can run it using `Run` button in script editor toolbar or reload whole project (`Reload` button in toolbar or `F5`).
|
||||
Also, you can enable `Live reload` option in `File` menu to reload project automatically on scripts change
|
||||
@@ -0,0 +1,87 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("java-library")
|
||||
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
version = System.getenv("JADX_SCRIPT_KOTLIN_PLUGIN_VERSION") ?: "dev"
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":jadx-core"))
|
||||
compileOnly(project(":jadx-commons:jadx-app-commons"))
|
||||
|
||||
compileOnly(project(":jadx-gui"))
|
||||
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation(kotlin("scripting-jvm"))
|
||||
implementation(kotlin("scripting-jvm-host"))
|
||||
implementation(kotlin("scripting-ide-services"))
|
||||
implementation(kotlin("scripting-compiler-embeddable"))
|
||||
implementation(kotlin("compiler-embeddable"))
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
|
||||
// allow to use maven dependencies in scripts
|
||||
implementation(kotlin("scripting-dependencies"))
|
||||
implementation(kotlin("scripting-dependencies-maven"))
|
||||
|
||||
// autocomplete support in editor
|
||||
compileOnly("com.fifesoft:autocomplete:3.3.2")
|
||||
compileOnly("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||
|
||||
// use KtLint for format and check jadx scripts
|
||||
implementation("com.pinterest.ktlint:ktlint-rule-engine:1.8.0")
|
||||
implementation("com.pinterest.ktlint:ktlint-ruleset-standard:1.8.0")
|
||||
|
||||
compileOnly("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
compileOnly("org.slf4j:slf4j-api:2.0.17")
|
||||
|
||||
// register jadx script for IDE support (don't work now)
|
||||
// kotlinScriptDef(project(":jadx-plugins:jadx-script-kotlin"))
|
||||
|
||||
testImplementation(project(":jadx-core"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||
|
||||
testImplementation("ch.qos.logback:logback-classic:1.5.22")
|
||||
testImplementation("org.assertj:assertj-core:3.27.6")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
register<Zip>("dist") {
|
||||
group = "jadx-plugin"
|
||||
dependsOn(jar)
|
||||
|
||||
from(jar)
|
||||
from(project.configurations.runtimeClasspath)
|
||||
|
||||
archiveBaseName = project.name
|
||||
destinationDirectory = layout.buildDirectory.dir("dist")
|
||||
}
|
||||
|
||||
withType(Test::class) {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package jadx.plugins.script.kotlin
|
||||
|
||||
import jadx.api.plugins.JadxPlugin
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.JadxPluginInfo
|
||||
import jadx.api.plugins.JadxPluginInfoBuilder
|
||||
import jadx.plugins.script.kotlin.gui.JadxScriptInputCategory
|
||||
import jadx.plugins.script.kotlin.gui.JadxScriptOptionsUI
|
||||
import jadx.plugins.script.kotlin.passes.JadxScriptAfterLoadPass
|
||||
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
|
||||
|
||||
class JadxScriptKotlinPlugin : JadxPlugin {
|
||||
companion object {
|
||||
const val PLUGIN_ID = "jadx-script-kotlin"
|
||||
}
|
||||
|
||||
override fun getPluginInfo(): JadxPluginInfo = JadxPluginInfoBuilder.pluginId(PLUGIN_ID)
|
||||
.name("Jadx Script (Kotlin)")
|
||||
.description("Scripting support for jadx using Kotlin script")
|
||||
.homepage("https://github.com/jadx-decompiler/jadx-script-kotlin")
|
||||
.requiredJadxVersion("1.5.4, r2596")
|
||||
.provides("jadx-script") // conflict with bundled plugin from older jadx versions
|
||||
.build()
|
||||
|
||||
override fun init(context: JadxPluginContext) {
|
||||
val scriptOptions = JadxScriptAllOptions()
|
||||
context.registerOptions(scriptOptions)
|
||||
val scripts = ScriptEval().process(context, scriptOptions)
|
||||
if (scripts.isNotEmpty()) {
|
||||
context.addPass(JadxScriptAfterLoadPass(scripts))
|
||||
context.guiContext?.let { guiContext ->
|
||||
JadxScriptOptionsUI.setup(guiContext, scriptOptions)
|
||||
JadxScriptInputCategory.register(context, guiContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-22
@@ -1,9 +1,11 @@
|
||||
package jadx.plugins.script
|
||||
package jadx.plugins.script.kotlin
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.core.utils.files.FileUtils
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.security.MessageDigest
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.script.experimental.api.CompiledScript
|
||||
import kotlin.script.experimental.api.ScriptCompilationConfiguration
|
||||
import kotlin.script.experimental.api.SourceCode
|
||||
@@ -12,6 +14,8 @@ import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
|
||||
import kotlin.script.experimental.jvmhost.loadScriptFromJar
|
||||
import kotlin.script.experimental.jvmhost.saveToJar
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
class ScriptCache {
|
||||
private val enableCache = System.getProperty("JADX_SCRIPT_CACHE_ENABLE", "true").equals("true", ignoreCase = true)
|
||||
|
||||
@@ -19,7 +23,9 @@ class ScriptCache {
|
||||
if (!enableCache) {
|
||||
return CompiledJvmScriptsCache.NoCache
|
||||
}
|
||||
return JadxScriptsCache(getCacheDir(context))
|
||||
val cacheDir = getCacheDir(context)
|
||||
log.debug { "script cache created in : $cacheDir" }
|
||||
return JadxScriptsCache(cacheDir)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,21 +33,22 @@ class ScriptCache {
|
||||
* but remove all previous cache versions for the script with the same path and name.
|
||||
* This should reduce old cache entries count
|
||||
*/
|
||||
class JadxScriptsCache(private val baseCacheDir: File) : CompiledJvmScriptsCache {
|
||||
class JadxScriptsCache(private val baseCacheDir: Path) : CompiledJvmScriptsCache {
|
||||
override fun get(
|
||||
script: SourceCode,
|
||||
scriptCompilationConfiguration: ScriptCompilationConfiguration,
|
||||
): CompiledScript? {
|
||||
val cacheDir = hashDir(baseCacheDir, script)
|
||||
val file = hashFile(cacheDir, script, scriptCompilationConfiguration)
|
||||
if (!file.exists()) {
|
||||
return null
|
||||
}
|
||||
return file.loadScriptFromJar() ?: run {
|
||||
// invalidate cache if the script cannot be loaded
|
||||
FileUtils.deleteDir(cacheDir)
|
||||
null
|
||||
if (file.exists()) {
|
||||
file.toFile().loadScriptFromJar().let {
|
||||
log.debug { "loaded script from cache: $file" }
|
||||
return it
|
||||
}
|
||||
}
|
||||
log.debug { "script not found in cache: $file" }
|
||||
FileUtils.deleteDirIfExists(cacheDir)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun store(
|
||||
@@ -55,34 +62,35 @@ class ScriptCache {
|
||||
val cacheDir = hashDir(baseCacheDir, script)
|
||||
val file = hashFile(cacheDir, script, scriptCompilationConfiguration)
|
||||
|
||||
cacheDir.deleteRecursively()
|
||||
cacheDir.mkdirs()
|
||||
jvmScript.saveToJar(file)
|
||||
FileUtils.deleteDirIfExists(cacheDir)
|
||||
FileUtils.makeDirs(cacheDir)
|
||||
jvmScript.saveToJar(file.toFile())
|
||||
log.debug { "script cached: $file" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCacheDir(context: JadxPluginContext): File {
|
||||
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled").toFile()
|
||||
private fun getCacheDir(context: JadxPluginContext): Path {
|
||||
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled")
|
||||
FileUtils.makeDirs(cacheBaseDir)
|
||||
return cacheBaseDir
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun hashDir(baseCacheDir: File, script: SourceCode): File {
|
||||
private fun hashDir(baseCacheDir: Path, script: SourceCode): Path {
|
||||
if (script.name == null && script.locationId == null) {
|
||||
return File(baseCacheDir, "tmp")
|
||||
return baseCacheDir.resolve("tmp")
|
||||
}
|
||||
val digest = MessageDigest.getInstance("MD5")
|
||||
digest.add(script.name)
|
||||
digest.add(script.locationId)
|
||||
return File(baseCacheDir, digest.digest().toHexString())
|
||||
return baseCacheDir.resolve(digest.digest().toHexString())
|
||||
}
|
||||
|
||||
private fun hashFile(
|
||||
cacheDir: File,
|
||||
cacheDir: Path,
|
||||
script: SourceCode,
|
||||
scriptCompilationConfiguration: ScriptCompilationConfiguration,
|
||||
): File {
|
||||
): Path {
|
||||
val digest = MessageDigest.getInstance("MD5")
|
||||
digest.add(script.text)
|
||||
scriptCompilationConfiguration.notTransientData.entries
|
||||
@@ -91,7 +99,7 @@ class ScriptCache {
|
||||
digest.add(it.key.name)
|
||||
digest.add(it.value.toString())
|
||||
}
|
||||
return File(cacheDir, digest.digest().toHexString() + ".jar")
|
||||
return cacheDir.resolve(digest.digest().toHexString() + ".jar")
|
||||
}
|
||||
|
||||
private fun MessageDigest.add(str: String?) {
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package jadx.plugins.script.kotlin
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptData
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptTemplate
|
||||
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.kotlin.scripting.resolve.skipExtensionsResolutionForImplicitsExceptInnermost
|
||||
import java.io.File
|
||||
import kotlin.script.experimental.api.EvaluationResult
|
||||
import kotlin.script.experimental.api.KotlinType
|
||||
import kotlin.script.experimental.api.ResultValue
|
||||
import kotlin.script.experimental.api.ResultWithDiagnostics
|
||||
import kotlin.script.experimental.api.ScriptAcceptedLocation
|
||||
import kotlin.script.experimental.api.ScriptCollectedData
|
||||
import kotlin.script.experimental.api.ScriptCompilationConfiguration
|
||||
import kotlin.script.experimental.api.ScriptConfigurationRefinementContext
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic.Severity
|
||||
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
|
||||
import kotlin.script.experimental.api.acceptedLocations
|
||||
import kotlin.script.experimental.api.asSuccess
|
||||
import kotlin.script.experimental.api.collectedAnnotations
|
||||
import kotlin.script.experimental.api.compilationConfiguration
|
||||
import kotlin.script.experimental.api.compilerOptions
|
||||
import kotlin.script.experimental.api.constructorArgs
|
||||
import kotlin.script.experimental.api.defaultIdentifier
|
||||
import kotlin.script.experimental.api.defaultImports
|
||||
import kotlin.script.experimental.api.displayName
|
||||
import kotlin.script.experimental.api.fileExtension
|
||||
import kotlin.script.experimental.api.filePathPattern
|
||||
import kotlin.script.experimental.api.hostConfiguration
|
||||
import kotlin.script.experimental.api.ide
|
||||
import kotlin.script.experimental.api.implicitReceivers
|
||||
import kotlin.script.experimental.api.isStandalone
|
||||
import kotlin.script.experimental.api.onSuccess
|
||||
import kotlin.script.experimental.api.refineConfiguration
|
||||
import kotlin.script.experimental.api.with
|
||||
import kotlin.script.experimental.dependencies.CompoundDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.DependsOn
|
||||
import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.Repository
|
||||
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations
|
||||
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
||||
import kotlin.script.experimental.host.getScriptingClass
|
||||
import kotlin.script.experimental.host.toScriptSource
|
||||
import kotlin.script.experimental.host.with
|
||||
import kotlin.script.experimental.jvm.JvmGetScriptingClass
|
||||
import kotlin.script.experimental.jvm.baseClassLoader
|
||||
import kotlin.script.experimental.jvm.compilationCache
|
||||
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
|
||||
import kotlin.script.experimental.jvm.jvm
|
||||
import kotlin.script.experimental.jvm.updateClasspath
|
||||
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
object DefCompileConf : ScriptCompilationConfiguration(ScriptEval.buildDefaultCompileConf())
|
||||
|
||||
class ScriptEval {
|
||||
companion object {
|
||||
fun buildDefaultCompileConf(): ScriptCompilationConfiguration {
|
||||
val scriptEval = ScriptEval()
|
||||
val hostConf = scriptEval.buildHostConf(null)
|
||||
return scriptEval.buildCompileConf(hostConf)
|
||||
}
|
||||
}
|
||||
|
||||
fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
|
||||
val jadx = context.decompiler
|
||||
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
|
||||
if (scripts.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val scriptDataList = mutableListOf<JadxScriptData>()
|
||||
for (scriptFile in scripts) {
|
||||
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
|
||||
scriptDataList.add(scriptData)
|
||||
eval(context, scriptData)
|
||||
}
|
||||
return scriptDataList
|
||||
}
|
||||
|
||||
private fun eval(
|
||||
context: JadxPluginContext,
|
||||
scriptData: JadxScriptData,
|
||||
) {
|
||||
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
|
||||
val hostConf = buildHostConf(context)
|
||||
val compileConf = buildCompileConf(hostConf)
|
||||
val evalConf = buildEvalConf(scriptData, compileConf)
|
||||
val scriptingHost = BasicJvmScriptingHost(hostConf)
|
||||
val execTime = measureTimeMillis {
|
||||
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compileConf, evalConf)
|
||||
processEvalResult(result, scriptData)
|
||||
}
|
||||
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
|
||||
}
|
||||
|
||||
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, scriptData: JadxScriptData) {
|
||||
val log = scriptData.log
|
||||
for (r in res.reports) {
|
||||
val msg = r.render(withSeverity = false)
|
||||
when (r.severity) {
|
||||
Severity.FATAL, Severity.ERROR -> log.error(r.exception) { "Script execution error: $msg" }
|
||||
Severity.WARNING -> log.warn { "Script execution issue: $msg" }
|
||||
Severity.INFO -> log.info { "Script report: $msg" }
|
||||
Severity.DEBUG -> log.debug { "Script debug: $msg" }
|
||||
}
|
||||
}
|
||||
when (res) {
|
||||
is ResultWithDiagnostics.Success -> {
|
||||
when (val retVal = res.value.returnValue) {
|
||||
is ResultValue.Error -> log.error(retVal.error) { "Script execution error:" }
|
||||
is ResultValue.Value -> log.info { "Script execution result: $retVal" }
|
||||
is ResultValue.Unit -> {}
|
||||
ResultValue.NotEvaluated -> {}
|
||||
}
|
||||
}
|
||||
|
||||
is ResultWithDiagnostics.Failure -> {
|
||||
scriptData.error = true
|
||||
log.error { "Script execution failed: ${scriptData.scriptName}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildHostConf(context: JadxPluginContext?) = ScriptingHostConfiguration {
|
||||
jvm {
|
||||
getScriptingClass(JvmGetScriptingClass())
|
||||
baseClassLoader.put(JadxScriptTemplate::class.java.classLoader)
|
||||
context?.let {
|
||||
compilationCache(ScriptCache().build(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCompileConf(scriptingHostConf: ScriptingHostConfiguration) = ScriptCompilationConfiguration {
|
||||
hostConfiguration.put(scriptingHostConf)
|
||||
|
||||
displayName.put("Jadx script")
|
||||
defaultIdentifier.put("JadxScript")
|
||||
|
||||
fileExtension.put("jadx.kts")
|
||||
filePathPattern.put(".*\\.jadx\\.kts")
|
||||
|
||||
val receiversTypes = listOf(KotlinType(JadxScriptTemplate::class))
|
||||
implicitReceivers(receiversTypes)
|
||||
skipExtensionsResolutionForImplicitsExceptInnermost(receiversTypes)
|
||||
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(
|
||||
wholeClasspath = true,
|
||||
)
|
||||
}
|
||||
|
||||
addBaseClass<JadxScriptTemplate>()
|
||||
defaultImports(DependsOn::class, Repository::class)
|
||||
|
||||
refineConfiguration {
|
||||
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
|
||||
}
|
||||
|
||||
ide {
|
||||
acceptedLocations(ScriptAcceptedLocation.Everywhere)
|
||||
}
|
||||
|
||||
isStandalone(true)
|
||||
|
||||
// forcing compiler to not use modules while building script classpath
|
||||
// because shadow jar remove all modules-info.class (https://github.com/GradleUp/shadow/issues/710)
|
||||
compilerOptions.append("-Xjdk-release=1.8")
|
||||
}
|
||||
|
||||
inline fun <reified T> ScriptCompilationConfiguration.Builder.addBaseClass() {
|
||||
val kClass = T::class
|
||||
defaultImports.append(kClass.java.name)
|
||||
hostConfiguration.update {
|
||||
it.with {
|
||||
this[jvm.baseClassLoader] = kClass.java.classLoader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildEvalConf(scriptData: JadxScriptData, compileConf: ScriptCompilationConfiguration): ScriptEvaluationConfiguration {
|
||||
return ScriptEvaluationConfiguration {
|
||||
hostConfiguration.put(compileConf[hostConfiguration]!!)
|
||||
compilationConfiguration.put(compileConf)
|
||||
constructorArgs(JadxScriptTemplate(scriptData))
|
||||
}
|
||||
}
|
||||
|
||||
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
|
||||
|
||||
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
|
||||
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: return context.compilationConfiguration.asSuccess()
|
||||
return runBlocking {
|
||||
resolver.resolveFromScriptSourceAnnotations(annotations)
|
||||
}.onSuccess { files: List<File> ->
|
||||
log.debug { "add script dependency: $files" }
|
||||
context.compilationConfiguration.with {
|
||||
updateClasspath(files)
|
||||
}.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-17
@@ -1,27 +1,27 @@
|
||||
package jadx.plugins.script.ide
|
||||
package jadx.plugins.script.kotlin
|
||||
|
||||
import jadx.plugins.script.ScriptEval
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.services.FirReplHistoryProviderImpl
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.services.firReplHistoryProvider
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.services.isReplSnippetSource
|
||||
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices
|
||||
import kotlin.script.experimental.api.ReplAnalyzerResult
|
||||
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.repl
|
||||
import kotlin.script.experimental.api.valueOrNull
|
||||
import kotlin.script.experimental.host.toScriptSource
|
||||
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
|
||||
import kotlin.script.experimental.host.with
|
||||
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
|
||||
|
||||
data class ScriptCompletionResult(
|
||||
val completions: List<SourceCodeCompletionVariant>,
|
||||
val reports: List<ScriptDiagnostic>,
|
||||
val reports: MutableList<ScriptDiagnostic>,
|
||||
)
|
||||
|
||||
data class ScriptAnalyzeResult(
|
||||
@@ -30,12 +30,28 @@ data class ScriptAnalyzeResult(
|
||||
val renderType: String?,
|
||||
)
|
||||
|
||||
class ScriptServices {
|
||||
private val compileConf = ScriptEval().buildCompileConf()
|
||||
private val replCompiler = KJvmReplCompilerWithIdeServices(
|
||||
compileConf[ScriptCompilationConfiguration.hostConfiguration]
|
||||
?: defaultJvmScriptingHostConfiguration,
|
||||
)
|
||||
class ScriptServices(pluginContext: JadxPluginContext? = null) {
|
||||
companion object {
|
||||
const val AUTO_COMPLETE_INSERT_STR = "ABCDEF" // defined at KJvmReplCompleter.INSERTED_STRING
|
||||
}
|
||||
|
||||
private val compileConf: ScriptCompilationConfiguration
|
||||
private val replCompiler: KJvmReplCompilerWithIdeServices
|
||||
|
||||
init {
|
||||
val scriptEval = ScriptEval()
|
||||
val hostConf = scriptEval.buildHostConf(pluginContext)
|
||||
hostConf.with {
|
||||
repl {
|
||||
firReplHistoryProvider(FirReplHistoryProviderImpl())
|
||||
isReplSnippetSource { sourceFile, _ ->
|
||||
sourceFile?.name?.endsWith(".jadx.kts", ignoreCase = true) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
compileConf = scriptEval.buildCompileConf(hostConf)
|
||||
replCompiler = KJvmReplCompilerWithIdeServices(hostConf)
|
||||
}
|
||||
|
||||
fun complete(scriptName: String, code: String, cursor: Int): ScriptCompletionResult {
|
||||
val snippet = code.toScriptSource(scriptName)
|
||||
@@ -44,15 +60,14 @@ class ScriptServices {
|
||||
}
|
||||
return ScriptCompletionResult(
|
||||
completions = result.valueOrNull()?.toList() ?: emptyList(),
|
||||
reports = result.reports,
|
||||
reports = result.reports.toMutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun analyze(scriptName: String, code: String): ScriptAnalyzeResult {
|
||||
val sourceCode = code.toScriptSource(scriptName)
|
||||
val result = runBlocking {
|
||||
val cursor = SourceCode.Position(0, 0) // not used
|
||||
replCompiler.analyze(sourceCode, cursor, compileConf)
|
||||
replCompiler.analyze(sourceCode, 0.toSourceCodePosition(sourceCode), compileConf)
|
||||
}
|
||||
val analyzerResult = result.valueOrNull()
|
||||
val issues = mutableListOf<ScriptDiagnostic>()
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jadx.api.ICodeInfo
|
||||
import jadx.api.impl.SimpleCodeInfo
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException
|
||||
import jadx.core.utils.files.FileUtils
|
||||
import jadx.gui.treemodel.JClass
|
||||
import jadx.gui.treemodel.JEditableNode
|
||||
import jadx.gui.ui.MainWindow
|
||||
import jadx.gui.ui.panel.ContentPanel
|
||||
import jadx.gui.ui.tab.TabbedPane
|
||||
import jadx.gui.utils.NLS
|
||||
import jadx.gui.utils.UiUtils
|
||||
import jadx.gui.utils.ui.SimpleMenuItem
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
import javax.swing.ImageIcon
|
||||
import javax.swing.JPopupMenu
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
class JInputScript(
|
||||
val pluginContext: JadxPluginContext,
|
||||
private val scriptPath: Path,
|
||||
) : JEditableNode() {
|
||||
companion object {
|
||||
private val SCRIPT_ICON: ImageIcon = UiUtils.openSvgIcon("nodes/kotlin_script")
|
||||
}
|
||||
|
||||
private val name: String = scriptPath.fileName.toString().replace(".jadx.kts", "")
|
||||
|
||||
override fun hasContent(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getContentPanel(tabbedPane: TabbedPane): ContentPanel {
|
||||
return ScriptContentPanel(pluginContext, tabbedPane, this)
|
||||
}
|
||||
|
||||
override fun getCodeInfo(): ICodeInfo {
|
||||
try {
|
||||
return SimpleCodeInfo(FileUtils.readFile(scriptPath))
|
||||
} catch (e: Exception) {
|
||||
throw JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(newContent: String?) {
|
||||
try {
|
||||
FileUtils.writeFile(scriptPath, newContent)
|
||||
log.debug { "Script saved: ${scriptPath.toAbsolutePath()}" }
|
||||
} catch (e: Exception) {
|
||||
throw JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTreePopupMenu(mainWindow: MainWindow): JPopupMenu {
|
||||
val menu = JPopupMenu()
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.add_scripts")) { mainWindow.addFiles() })
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.new_script")) { mainWindow.addNewScript() })
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.remove")) { mainWindow.removeInput(scriptPath) })
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.rename")) { mainWindow.renameInput(scriptPath) })
|
||||
return menu
|
||||
}
|
||||
|
||||
override fun getSyntaxName(): String {
|
||||
return SyntaxConstants.SYNTAX_STYLE_KOTLIN
|
||||
}
|
||||
|
||||
override fun getJParent(): JClass? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return SCRIPT_ICON
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
override fun makeString(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
override fun getTooltip(): String {
|
||||
return scriptPath.normalize().toAbsolutePath().toString()
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.gui.treemodel.JClass
|
||||
import jadx.gui.treemodel.JNode
|
||||
import jadx.gui.ui.MainWindow
|
||||
import jadx.gui.utils.NLS
|
||||
import jadx.gui.utils.UiUtils
|
||||
import jadx.gui.utils.ui.SimpleMenuItem
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
import javax.swing.ImageIcon
|
||||
import javax.swing.JPopupMenu
|
||||
|
||||
class JInputScripts(
|
||||
pluginContext: JadxPluginContext,
|
||||
scripts: List<Path>,
|
||||
) : JNode() {
|
||||
companion object {
|
||||
private val INPUT_SCRIPTS_ICON: ImageIcon = UiUtils.openSvgIcon("nodes/scriptsModel")
|
||||
}
|
||||
|
||||
init {
|
||||
for (script in scripts) {
|
||||
add(JInputScript(pluginContext, script))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTreePopupMenu(mainWindow: MainWindow): JPopupMenu {
|
||||
val menu = JPopupMenu()
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.add_scripts")) { mainWindow.addFiles() })
|
||||
menu.add(SimpleMenuItem(NLS.str("popup.new_script")) { mainWindow.addNewScript() })
|
||||
return menu
|
||||
}
|
||||
|
||||
override fun getJParent(): JClass? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return INPUT_SCRIPTS_ICON
|
||||
}
|
||||
|
||||
override fun getID(): String {
|
||||
return "JInputScripts"
|
||||
}
|
||||
|
||||
override fun makeString(): String {
|
||||
return NLS.str("tree.input_scripts")
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.gui.JadxGuiContext
|
||||
import jadx.gui.plugins.context.GuiPluginContext
|
||||
import jadx.gui.plugins.context.ITreeInputCategory
|
||||
import jadx.gui.settings.data.ITabStatePersist
|
||||
import jadx.gui.treemodel.JNode
|
||||
import java.nio.file.Path
|
||||
|
||||
object JadxScriptInputCategory {
|
||||
fun register(pluginContext: JadxPluginContext, guiContext: JadxGuiContext) {
|
||||
val internalContext = guiContext as GuiPluginContext
|
||||
val inputCategory = InputScriptsBuilder(pluginContext)
|
||||
internalContext.registerTreeInputCategory(inputCategory)
|
||||
internalContext.registerTabStatePersistAdapter(InputScriptTabStatePersist(inputCategory))
|
||||
}
|
||||
}
|
||||
|
||||
class InputScriptsBuilder(private val pluginContext: JadxPluginContext) : ITreeInputCategory {
|
||||
var scriptsRootNode: JInputScripts? = null
|
||||
|
||||
override fun filesFilter(file: Path): Boolean {
|
||||
return file.fileName.toString().endsWith(".jadx.kts", ignoreCase = true)
|
||||
}
|
||||
|
||||
override fun buildInputNode(files: List<Path>): JNode {
|
||||
val scriptsNode = JInputScripts(pluginContext, files)
|
||||
scriptsRootNode = scriptsNode
|
||||
return scriptsNode
|
||||
}
|
||||
}
|
||||
|
||||
class InputScriptTabStatePersist(private val scriptsBuilder: InputScriptsBuilder) : ITabStatePersist {
|
||||
override fun getNodeClass() = JInputScript::class.java
|
||||
|
||||
override fun save(node: JNode): String {
|
||||
return node.name
|
||||
}
|
||||
|
||||
override fun load(nodeName: String): JNode? {
|
||||
return scriptsBuilder.scriptsRootNode?.searchNode { it.name.equals(nodeName) }
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.api.plugins.gui.ISettingsGroup
|
||||
import jadx.api.plugins.gui.JadxGuiContext
|
||||
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
|
||||
import javax.swing.JPanel
|
||||
|
||||
object JadxScriptOptionsUI {
|
||||
fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) {
|
||||
guiContext.settings().setCustomSettingsGroup(ScriptOptionsRootGroup(guiContext, scriptOptions))
|
||||
}
|
||||
}
|
||||
|
||||
private class ScriptOptionsRootGroup(
|
||||
private val guiContext: JadxGuiContext,
|
||||
private val scriptOptions: JadxScriptAllOptions,
|
||||
) : ISettingsGroup {
|
||||
|
||||
override fun getTitle() = "Scripts"
|
||||
|
||||
override fun buildComponent() = JPanel() // empty panel for root node
|
||||
|
||||
override fun getSubGroups(): List<ISettingsGroup> {
|
||||
val settings = guiContext.settings()
|
||||
return scriptOptions.descriptions
|
||||
.groupBy { it.script }
|
||||
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
+17
-15
@@ -1,4 +1,4 @@
|
||||
package jadx.gui.plugins.script
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import com.pinterest.ktlint.rule.engine.api.Code
|
||||
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
|
||||
@@ -9,13 +9,15 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
|
||||
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
|
||||
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
|
||||
import org.ec4j.core.model.PropertyType
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
data class JadxLintError(
|
||||
val line: Int,
|
||||
val col: Int,
|
||||
val ruleId: String,
|
||||
val detail: String,
|
||||
)
|
||||
|
||||
object KtLintUtils {
|
||||
|
||||
val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java)
|
||||
|
||||
private val ktLint by lazy {
|
||||
KtLintRuleEngine(
|
||||
ruleProviders = StandardRuleSetProvider().getRuleProviders(),
|
||||
@@ -36,18 +38,18 @@ object KtLintUtils {
|
||||
}
|
||||
|
||||
fun lint(content: String): List<JadxLintError> {
|
||||
val code = Code.fromSnippet(content, script = true)
|
||||
val errors = mutableListOf<JadxLintError>()
|
||||
val code = Code.fromSnippet(content, script = true)
|
||||
ktLint.lint(code) { lintError ->
|
||||
errors.add(JadxLintError(lintError.line, lintError.col, lintError.ruleId.value, lintError.detail))
|
||||
errors.add(
|
||||
JadxLintError(
|
||||
line = lintError.line,
|
||||
col = lintError.col,
|
||||
ruleId = lintError.ruleId.value,
|
||||
detail = lintError.detail,
|
||||
),
|
||||
)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
||||
data class JadxLintError(
|
||||
val line: Int,
|
||||
val col: Int,
|
||||
val ruleId: String,
|
||||
val detail: String,
|
||||
)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.api.ICodeInfo
|
||||
import jadx.gui.jobs.IBackgroundTask
|
||||
import jadx.gui.jobs.LoadTask
|
||||
import jadx.gui.settings.JadxSettings
|
||||
import jadx.gui.ui.action.JadxAutoCompletion
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea
|
||||
import jadx.gui.ui.panel.ContentPanel
|
||||
import jadx.gui.utils.shortcut.ShortcutsController
|
||||
import org.fife.ui.autocomplete.AutoCompletion
|
||||
|
||||
class ScriptCodeArea(contentPanel: ContentPanel, val scriptNode: JInputScript) :
|
||||
AbstractCodeArea(contentPanel, scriptNode) {
|
||||
private val autoCompletion: AutoCompletion
|
||||
private val shortcutsController: ShortcutsController
|
||||
|
||||
init {
|
||||
setSyntaxEditingStyle(scriptNode.syntaxName)
|
||||
isCodeFoldingEnabled = true
|
||||
closeCurlyBraces = true
|
||||
|
||||
shortcutsController = contentPanel.mainWindow.shortcutsController
|
||||
val settings = contentPanel.mainWindow.settings
|
||||
autoCompletion = addAutoComplete(settings)
|
||||
}
|
||||
|
||||
private fun addAutoComplete(settings: JadxSettings): AutoCompletion {
|
||||
val provider = ScriptCompleteProvider(this, scriptNode.pluginContext)
|
||||
provider.setAutoActivationRules(false, ".")
|
||||
val ac = JadxAutoCompletion(provider)
|
||||
ac.setListCellRenderer(ScriptCompletionRenderer(settings))
|
||||
ac.isAutoActivationEnabled = true
|
||||
ac.autoCompleteSingleChoices = true
|
||||
ac.install(this)
|
||||
shortcutsController.bindImmediate(ac)
|
||||
return ac
|
||||
}
|
||||
|
||||
override fun getCodeInfo(): ICodeInfo {
|
||||
return node.codeInfo
|
||||
}
|
||||
|
||||
override fun getLoadTask(): IBackgroundTask {
|
||||
return LoadTask(
|
||||
{ node.codeInfo.getCodeStr() },
|
||||
{ code ->
|
||||
text = code
|
||||
setCaretPosition(0)
|
||||
setLoaded()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
text = node.codeInfo.getCodeStr()
|
||||
}
|
||||
|
||||
fun updateCode(newCode: String?) {
|
||||
val caretPos = caretPosition
|
||||
text = newCode
|
||||
setCaretPosition(caretPos)
|
||||
scriptNode.isChanged = true
|
||||
}
|
||||
|
||||
fun save() {
|
||||
scriptNode.save(getText())
|
||||
scriptNode.isChanged = false
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
shortcutsController.unbindActionsForComponent(this)
|
||||
autoCompletion.uninstall()
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea
|
||||
import jadx.gui.utils.Icons
|
||||
import jadx.plugins.script.kotlin.ScriptCompletionResult
|
||||
import jadx.plugins.script.kotlin.ScriptServices
|
||||
import jadx.plugins.script.kotlin.ScriptServices.Companion.AUTO_COMPLETE_INSERT_STR
|
||||
import org.fife.ui.autocomplete.Completion
|
||||
import org.fife.ui.autocomplete.CompletionProviderBase
|
||||
import org.fife.ui.autocomplete.ParameterizedCompletion
|
||||
import java.awt.Point
|
||||
import javax.swing.Icon
|
||||
import javax.swing.text.BadLocationException
|
||||
import javax.swing.text.JTextComponent
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic
|
||||
import kotlin.script.experimental.api.SourceCodeCompletionVariant
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
private val ICONS_MAP = mapOf<String, Icon>(
|
||||
"class" to Icons.CLASS,
|
||||
"method" to Icons.METHOD,
|
||||
"field" to Icons.FIELD,
|
||||
"property" to Icons.PROPERTY,
|
||||
"parameter" to Icons.PARAMETER,
|
||||
"package" to Icons.PACKAGE,
|
||||
)
|
||||
|
||||
class ScriptCompleteProvider(
|
||||
private val codeArea: AbstractCodeArea,
|
||||
private val pluginContext: JadxPluginContext,
|
||||
) : CompletionProviderBase() {
|
||||
|
||||
private val completions: List<Completion>
|
||||
get() {
|
||||
try {
|
||||
val code = codeArea.getText()
|
||||
val caretPos = codeArea.caretPosition
|
||||
val scriptServices = ScriptServices(pluginContext)
|
||||
val scriptName = codeArea.getNode().getName()
|
||||
val result = scriptServices.complete(scriptName, code, caretPos)
|
||||
if (result.completions.isEmpty()) {
|
||||
return listOf()
|
||||
}
|
||||
val replacePos = getReplacePos(caretPos, result)
|
||||
if (!result.reports.isEmpty()) {
|
||||
log.debug { "Script completion reports: ${result.reports}" }
|
||||
}
|
||||
log.debug { "Completions:\n${result.completions.joinToString(separator = "\n")}" }
|
||||
return convertCompletions(result.completions, code, replacePos)
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "Code completion failed" }
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertCompletions(
|
||||
completions: List<SourceCodeCompletionVariant>,
|
||||
code: String,
|
||||
replacePos: Int,
|
||||
): List<Completion> {
|
||||
val count = completions.size
|
||||
val list = ArrayList<Completion>(count)
|
||||
for (i in 0..<count) {
|
||||
val c = completions[i]
|
||||
if (c.icon == "keyword") {
|
||||
// too many, not very useful
|
||||
continue
|
||||
}
|
||||
val summary = if (c.icon == "method" && c.text != c.displayText) {
|
||||
// add method args details for methods
|
||||
"${c.displayText} ${c.tail}"
|
||||
} else {
|
||||
c.tail
|
||||
}
|
||||
list += ScriptCompletionData(
|
||||
provider = this,
|
||||
input = c.text,
|
||||
code = code,
|
||||
relevance = count - i,
|
||||
replacePos = replacePos,
|
||||
summary = summary,
|
||||
toolTip = c.displayText,
|
||||
icon = ICONS_MAP[c.icon] ?: Icons.FILE,
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
@Throws(BadLocationException::class)
|
||||
private fun getReplacePos(caretPos: Int, result: ScriptCompletionResult): Int {
|
||||
val lineRaw = codeArea.getLineOfOffset(caretPos)
|
||||
val lineStart = codeArea.getLineStartOffset(lineRaw)
|
||||
val line = lineRaw + 1
|
||||
|
||||
val completeReport = result.reports.find { report ->
|
||||
if (report.severity == ScriptDiagnostic.Severity.ERROR) {
|
||||
report.location?.let { location ->
|
||||
location.start.line == line && report.message.endsWith(AUTO_COMPLETE_INSERT_STR)
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
if (completeReport == null) {
|
||||
log.warn { "Failed to find completion report in: ${result.reports}" }
|
||||
return caretPos
|
||||
}
|
||||
result.reports.remove(completeReport)
|
||||
val col = caretPos - lineStart + 1
|
||||
return caretPos - (col - completeReport.location!!.start.col)
|
||||
}
|
||||
|
||||
override fun getAlreadyEnteredText(comp: JTextComponent?): String? {
|
||||
try {
|
||||
val pos = codeArea.caretPosition
|
||||
return codeArea.getText(0, pos)
|
||||
} catch (e: Exception) {
|
||||
throw JadxRuntimeException("Failed to get text before caret", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCompletionsAt(comp: JTextComponent, p: Point): List<Completion> {
|
||||
return this.completions
|
||||
}
|
||||
|
||||
override fun getCompletionsImpl(comp: JTextComponent): List<Completion> {
|
||||
return this.completions
|
||||
}
|
||||
|
||||
override fun getParameterizedCompletions(tc: JTextComponent): List<ParameterizedCompletion>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import org.fife.ui.autocomplete.Completion
|
||||
import org.fife.ui.autocomplete.CompletionProvider
|
||||
import javax.swing.Icon
|
||||
import javax.swing.text.JTextComponent
|
||||
|
||||
class ScriptCompletionData(
|
||||
private val provider: CompletionProvider,
|
||||
private val relevance: Int,
|
||||
private val input: String,
|
||||
private val code: String,
|
||||
private val replacePos: Int,
|
||||
private val icon: Icon,
|
||||
private val toolTip: String,
|
||||
private val summary: String,
|
||||
) : Completion {
|
||||
|
||||
override fun getInputText(): String {
|
||||
return input
|
||||
}
|
||||
|
||||
override fun getProvider(): CompletionProvider {
|
||||
return provider
|
||||
}
|
||||
|
||||
override fun getAlreadyEntered(comp: JTextComponent?): String? {
|
||||
return provider.getAlreadyEnteredText(comp)
|
||||
}
|
||||
|
||||
override fun getRelevance(): Int {
|
||||
return relevance
|
||||
}
|
||||
|
||||
override fun getReplacementText(): String {
|
||||
return code.substring(0, replacePos) + input
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return icon
|
||||
}
|
||||
|
||||
override fun getSummary(): String {
|
||||
return summary
|
||||
}
|
||||
|
||||
override fun getToolTipText(): String {
|
||||
return toolTip
|
||||
}
|
||||
|
||||
override fun compareTo(other: Completion): Int {
|
||||
return relevance.compareTo(other.relevance)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return input
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.gui.settings.JadxSettings
|
||||
import jadx.gui.utils.UiUtils
|
||||
import org.fife.ui.autocomplete.Completion
|
||||
import org.fife.ui.autocomplete.CompletionCellRenderer
|
||||
import javax.swing.JList
|
||||
|
||||
class ScriptCompletionRenderer(settings: JadxSettings) : CompletionCellRenderer() {
|
||||
init {
|
||||
displayFont = settings.codeFont
|
||||
}
|
||||
|
||||
override fun prepareForOtherCompletion(
|
||||
list: JList<*>?,
|
||||
c: Completion?,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
hasFocus: Boolean,
|
||||
) {
|
||||
val cmpl = c as ScriptCompletionData
|
||||
setText(
|
||||
UiUtils.wrapHtml((UiUtils.escapeHtml(cmpl.inputText) + " " + UiUtils.fadeHtml(UiUtils.escapeHtml(cmpl.summary)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.gui.logs.LogOptions
|
||||
import jadx.gui.settings.LineNumbersMode
|
||||
import jadx.gui.ui.action.ActionModel
|
||||
import jadx.gui.ui.action.JadxGuiAction
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea
|
||||
import jadx.gui.ui.codearea.AbstractCodeContentPanel
|
||||
import jadx.gui.ui.codearea.SearchBar
|
||||
import jadx.gui.ui.tab.TabbedPane
|
||||
import jadx.gui.utils.Icons
|
||||
import jadx.gui.utils.NLS
|
||||
import jadx.gui.utils.UiUtils
|
||||
import jadx.gui.utils.ui.NodeLabel
|
||||
import jadx.plugins.script.kotlin.ScriptServices
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptData.Companion.JADX_SCRIPT_LOG_PREFIX
|
||||
import org.fife.ui.rsyntaxtextarea.ErrorStrip
|
||||
import org.fife.ui.rtextarea.RTextScrollPane
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.KeyEvent
|
||||
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 kotlin.script.experimental.api.ScriptDiagnostic
|
||||
|
||||
class ScriptContentPanel(
|
||||
private val pluginContext: JadxPluginContext,
|
||||
panel: TabbedPane,
|
||||
scriptNode: JInputScript,
|
||||
) : AbstractCodeContentPanel(panel, scriptNode) {
|
||||
private val scriptArea: ScriptCodeArea = ScriptCodeArea(this, scriptNode)
|
||||
private val searchBar: SearchBar
|
||||
private val codeScrollPane: RTextScrollPane
|
||||
private val actionPanel: JPanel
|
||||
private val resultLabel: JLabel = NodeLabel("")
|
||||
private val errorService: ScriptErrorService = ScriptErrorService(scriptArea)
|
||||
private val scriptLog: Logger = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.name)
|
||||
|
||||
init {
|
||||
actionPanel = buildScriptActionsPanel()
|
||||
searchBar = SearchBar(scriptArea)
|
||||
codeScrollPane = RTextScrollPane(scriptArea)
|
||||
|
||||
initUI()
|
||||
applySettings()
|
||||
scriptArea.load()
|
||||
}
|
||||
|
||||
private fun initUI() {
|
||||
val topPanel = JPanel(BorderLayout())
|
||||
topPanel.setBorder(EmptyBorder(5, 5, 5, 5))
|
||||
topPanel.add(actionPanel, BorderLayout.NORTH)
|
||||
topPanel.add(searchBar, BorderLayout.SOUTH)
|
||||
|
||||
val codePanel = JPanel(BorderLayout())
|
||||
codePanel.setBorder(EmptyBorder(0, 0, 0, 0))
|
||||
codePanel.add(codeScrollPane)
|
||||
codePanel.add(ErrorStrip(scriptArea), BorderLayout.LINE_END)
|
||||
|
||||
setLayout(BorderLayout())
|
||||
setBorder(EmptyBorder(0, 0, 0, 0))
|
||||
add(topPanel, BorderLayout.NORTH)
|
||||
add(codeScrollPane, BorderLayout.CENTER)
|
||||
|
||||
val key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton())
|
||||
UiUtils.addKeyBinding(scriptArea, key, "SearchAction") { searchBar.toggle() }
|
||||
}
|
||||
|
||||
private fun buildScriptActionsPanel(): JPanel {
|
||||
val runAction = JadxGuiAction(ActionModel.SCRIPT_RUN, Runnable { this.runScript() })
|
||||
val saveAction = JadxGuiAction(ActionModel.SCRIPT_SAVE, Runnable { scriptArea.save() })
|
||||
|
||||
runAction.shortcutComponent = scriptArea
|
||||
saveAction.shortcutComponent = scriptArea
|
||||
|
||||
tabbedPane.mainWindow.shortcutsController.bindImmediate(runAction)
|
||||
tabbedPane.mainWindow.shortcutsController.bindImmediate(saveAction)
|
||||
|
||||
val save = saveAction.makeButton()
|
||||
scriptArea.scriptNode.addChangeListener { save.setEnabled(it) }
|
||||
|
||||
val check = JButton(NLS.str("script.check"), Icons.CHECK)
|
||||
check.addActionListener { checkScript() }
|
||||
val format = JButton(NLS.str("script.format"), Icons.FORMAT)
|
||||
format.addActionListener { reformatCode() }
|
||||
val scriptLog = JButton(NLS.str("script.log"), Icons.FORMAT)
|
||||
scriptLog.addActionListener { showScriptLog() }
|
||||
|
||||
val panel = JPanel()
|
||||
panel.setLayout(BoxLayout(panel, BoxLayout.LINE_AXIS))
|
||||
panel.setBorder(EmptyBorder(0, 0, 0, 0))
|
||||
panel.add(runAction.makeButton())
|
||||
panel.add(Box.createRigidArea(Dimension(10, 0)))
|
||||
panel.add(save)
|
||||
panel.add(Box.createRigidArea(Dimension(10, 0)))
|
||||
panel.add(check)
|
||||
panel.add(Box.createRigidArea(Dimension(10, 0)))
|
||||
panel.add(format)
|
||||
panel.add(Box.createRigidArea(Dimension(30, 0)))
|
||||
panel.add(resultLabel)
|
||||
panel.add(Box.createHorizontalGlue())
|
||||
panel.add(scriptLog)
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun runScript() {
|
||||
scriptArea.save()
|
||||
if (!checkScript(runScript = true)) {
|
||||
return
|
||||
}
|
||||
resetResultLabel()
|
||||
|
||||
val tabbedPane = getTabbedPane()
|
||||
val mainWindow = tabbedPane.mainWindow
|
||||
mainWindow.backgroundExecutor.execute(NLS.str("script.run"), {
|
||||
try {
|
||||
mainWindow.wrapper.reloadPasses()
|
||||
} catch (e: Exception) {
|
||||
scriptLog.error("Passes reload failed", e)
|
||||
}
|
||||
}, {
|
||||
mainWindow.passesReloaded()
|
||||
})
|
||||
}
|
||||
|
||||
private fun checkScript(runScript: Boolean = false): Boolean {
|
||||
try {
|
||||
resetResultLabel()
|
||||
val code = scriptArea.getText()
|
||||
|
||||
if (code.contains("@file:DependsOn")) {
|
||||
if (!runScript) {
|
||||
resultLabel.setText("Checks disabled for scripts with external dependencies")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val fileName = scriptArea.getNode().getName()
|
||||
val scriptServices = ScriptServices(pluginContext)
|
||||
val result = scriptServices.analyze(fileName, code)
|
||||
var success = result.success
|
||||
val issues: List<ScriptDiagnostic> = result.issues
|
||||
for (issue in issues) {
|
||||
val severity = issue.severity
|
||||
if (severity == ScriptDiagnostic.Severity.ERROR || severity == ScriptDiagnostic.Severity.FATAL) {
|
||||
scriptLog.error(
|
||||
issue.render(
|
||||
withSeverity = false,
|
||||
withLocation = true,
|
||||
withException = true,
|
||||
withStackTrace = true,
|
||||
),
|
||||
)
|
||||
success = false
|
||||
} else if (severity == ScriptDiagnostic.Severity.WARNING) {
|
||||
scriptLog.warn("Compile issue: {}", issue)
|
||||
}
|
||||
}
|
||||
val lintErrs: List<JadxLintError> = when {
|
||||
success -> getLintIssues(code)
|
||||
else -> listOf()
|
||||
}
|
||||
|
||||
errorService.clearErrors()
|
||||
errorService.addCompilerIssues(issues)
|
||||
errorService.addLintErrors(lintErrs)
|
||||
if (!success) {
|
||||
resultLabel.setText("Compile issues: " + issues.size)
|
||||
showScriptLog()
|
||||
} else if (!lintErrs.isEmpty()) {
|
||||
resultLabel.setText("Lint issues: " + lintErrs.size)
|
||||
} else {
|
||||
resultLabel.setText("OK")
|
||||
}
|
||||
errorService.apply()
|
||||
return success
|
||||
} catch (e: Throwable) {
|
||||
scriptLog.error("Failed to check code", e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLintIssues(code: String): List<JadxLintError> {
|
||||
try {
|
||||
val lintErrs = KtLintUtils.lint(code)
|
||||
for (error in lintErrs) {
|
||||
scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})", error.detail, error.line, error.col, error.ruleId)
|
||||
}
|
||||
return lintErrs
|
||||
} catch (e: Throwable) { // can throw initialization error
|
||||
scriptLog.warn("KtLint failed", e)
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reformatCode() {
|
||||
resetResultLabel()
|
||||
try {
|
||||
val code = scriptArea.getText()
|
||||
val formattedCode = KtLintUtils.format(code)
|
||||
if (code != formattedCode) {
|
||||
scriptArea.updateCode(formattedCode)
|
||||
resultLabel.setText("Code updated")
|
||||
errorService.clearErrors()
|
||||
}
|
||||
} catch (e: Throwable) { // can throw initialization error
|
||||
scriptLog.error("Failed to reformat code", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetResultLabel() {
|
||||
resultLabel.setText("")
|
||||
}
|
||||
|
||||
private fun applySettings() {
|
||||
val settings = getSettings()
|
||||
codeScrollPane.setLineNumbersEnabled(settings.lineNumbersMode != LineNumbersMode.DISABLE)
|
||||
codeScrollPane.gutter.setLineNumberFont(settings.codeFont)
|
||||
scriptArea.loadSettings()
|
||||
}
|
||||
|
||||
private fun showScriptLog() {
|
||||
mainWindow.showLogViewer(LogOptions.forScript(getNode().getName()))
|
||||
}
|
||||
|
||||
override fun getCodeArea(): AbstractCodeArea {
|
||||
return scriptArea
|
||||
}
|
||||
|
||||
override fun getChildrenComponent(): Component {
|
||||
return codeArea
|
||||
}
|
||||
|
||||
override fun loadSettings() {
|
||||
applySettings()
|
||||
updateUI()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
scriptArea.dispose()
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package jadx.plugins.script.kotlin.gui
|
||||
|
||||
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 kotlin.script.experimental.api.ScriptDiagnostic
|
||||
|
||||
class ScriptErrorService(private val scriptArea: ScriptCodeArea) : AbstractParser() {
|
||||
private val result: DefaultParseResult = DefaultParseResult(this)
|
||||
|
||||
override fun parse(doc: RSyntaxDocument?, style: String?): ParseResult {
|
||||
return result
|
||||
}
|
||||
|
||||
fun clearErrors() {
|
||||
result.clearNotices()
|
||||
scriptArea.removeParser(this)
|
||||
}
|
||||
|
||||
fun apply() {
|
||||
scriptArea.removeParser(this)
|
||||
scriptArea.addParser(this)
|
||||
scriptArea.addNotify()
|
||||
scriptArea.requestFocus()
|
||||
jumpCaretToFirstError()
|
||||
}
|
||||
|
||||
private fun jumpCaretToFirstError() {
|
||||
val parserNotices = result.notices
|
||||
if (parserNotices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val notice = parserNotices.get(0)
|
||||
var offset = notice.offset
|
||||
if (offset == -1) {
|
||||
try {
|
||||
offset = scriptArea.getLineStartOffset(notice.line)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to jump to first error", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
scriptArea.scrollToPos(offset)
|
||||
}
|
||||
|
||||
fun addCompilerIssues(issues: List<ScriptDiagnostic>) {
|
||||
for (issue in issues) {
|
||||
if (issue.severity == ScriptDiagnostic.Severity.DEBUG) {
|
||||
continue
|
||||
}
|
||||
val notice: DefaultParserNotice?
|
||||
val loc = issue.location
|
||||
if (loc == null) {
|
||||
notice = DefaultParserNotice(this, issue.message, 0)
|
||||
} else {
|
||||
try {
|
||||
val line = loc.start.line
|
||||
val offset = scriptArea.getLineStartOffset(line - 1) + loc.start.col
|
||||
val len = if (loc.end == null) -1 else loc.end!!.col - loc.start.col
|
||||
notice = DefaultParserNotice(this, issue.message, line, offset - 1, len)
|
||||
notice.setLevel(convertLevel(issue.severity))
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to convert script issue", e)
|
||||
continue
|
||||
}
|
||||
}
|
||||
addNotice(notice)
|
||||
}
|
||||
}
|
||||
|
||||
fun addLintErrors(errors: List<JadxLintError>) {
|
||||
for (error in errors) {
|
||||
try {
|
||||
val line = error.line
|
||||
val offset = scriptArea.getLineStartOffset(line - 1) + error.col - 1
|
||||
val word = scriptArea.getWordByPosition(offset)
|
||||
val len = word?.length ?: -1
|
||||
val notice = DefaultParserNotice(this, error.detail, line, offset, len)
|
||||
notice.setLevel(ParserNotice.Level.WARNING)
|
||||
addNotice(notice)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to convert lint error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNotice(notice: DefaultParserNotice) {
|
||||
LOG.debug("Add notice: {}:{}:{} - {}", notice.line, notice.offset, notice.length, notice.message)
|
||||
result.addNotice(notice)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(ScriptErrorService::class.java)
|
||||
|
||||
private fun convertLevel(severity: ScriptDiagnostic.Severity): ParserNotice.Level {
|
||||
return when (severity) {
|
||||
ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> ParserNotice.Level.ERROR
|
||||
ScriptDiagnostic.Severity.WARNING -> ParserNotice.Level.WARNING
|
||||
ScriptDiagnostic.Severity.INFO, ScriptDiagnostic.Severity.DEBUG -> ParserNotice.Level.INFO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package jadx.plugins.script.passes
|
||||
package jadx.plugins.script.kotlin.passes
|
||||
|
||||
import jadx.api.JadxDecompiler
|
||||
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo
|
||||
import jadx.api.plugins.pass.types.JadxAfterLoadPass
|
||||
import jadx.plugins.script.runtime.JadxScriptData
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptData
|
||||
|
||||
class JadxScriptAfterLoadPass(private val scripts: List<JadxScriptData>) : JadxAfterLoadPass {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package jadx.plugins.script.kotlin.runtime
|
||||
|
||||
import jadx.plugins.script.kotlin.DefCompileConf
|
||||
import kotlin.script.experimental.annotations.KotlinScript
|
||||
|
||||
@KotlinScript(
|
||||
displayName = "Jadx Script",
|
||||
fileExtension = "jadx.kts",
|
||||
filePathPattern = ".*\\.jadx\\.kts",
|
||||
compilationConfiguration = DefCompileConf::class,
|
||||
)
|
||||
open class JadxScriptTemplate(
|
||||
scriptData: JadxScriptData,
|
||||
) {
|
||||
val scriptName = scriptData.scriptName
|
||||
val log = scriptData.log
|
||||
|
||||
private val scriptInstance = JadxScriptInstance(scriptData, log)
|
||||
|
||||
fun getJadxInstance() = scriptInstance
|
||||
|
||||
fun println(message: Any?) {
|
||||
log.info { message }
|
||||
}
|
||||
|
||||
fun print(message: Any?) {
|
||||
log.info { message }
|
||||
}
|
||||
}
|
||||
+14
-13
@@ -1,7 +1,7 @@
|
||||
@file:JvmName("ScriptRuntime")
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jadx.plugins.script.runtime
|
||||
package jadx.plugins.script.kotlin.runtime
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@@ -11,29 +11,30 @@ import jadx.api.JavaClass
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.events.IJadxEvents
|
||||
import jadx.api.plugins.pass.JadxPass
|
||||
import jadx.plugins.script.runtime.data.Debug
|
||||
import jadx.plugins.script.runtime.data.Decompile
|
||||
import jadx.plugins.script.runtime.data.Gui
|
||||
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
|
||||
import jadx.plugins.script.runtime.data.JadxScriptOptions
|
||||
import jadx.plugins.script.runtime.data.Rename
|
||||
import jadx.plugins.script.runtime.data.Replace
|
||||
import jadx.plugins.script.runtime.data.Search
|
||||
import jadx.plugins.script.runtime.data.Stages
|
||||
import jadx.plugins.script.kotlin.runtime.data.Debug
|
||||
import jadx.plugins.script.kotlin.runtime.data.Decompile
|
||||
import jadx.plugins.script.kotlin.runtime.data.Gui
|
||||
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
|
||||
import jadx.plugins.script.kotlin.runtime.data.JadxScriptOptions
|
||||
import jadx.plugins.script.kotlin.runtime.data.Rename
|
||||
import jadx.plugins.script.kotlin.runtime.data.Replace
|
||||
import jadx.plugins.script.kotlin.runtime.data.Search
|
||||
import jadx.plugins.script.kotlin.runtime.data.Stages
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.io.File
|
||||
|
||||
const val JADX_SCRIPT_LOG_PREFIX = "JadxScript:"
|
||||
|
||||
class JadxScriptData(
|
||||
val jadxInstance: JadxDecompiler,
|
||||
val pluginContext: JadxPluginContext,
|
||||
val options: JadxScriptAllOptions,
|
||||
val scriptFile: File,
|
||||
) {
|
||||
companion object {
|
||||
const val JADX_SCRIPT_LOG_PREFIX = "JadxScript:"
|
||||
}
|
||||
val scriptName = scriptFile.name.removeSuffix(".jadx.kts")
|
||||
val log = KotlinLogging.logger("$JADX_SCRIPT_LOG_PREFIX$scriptName")
|
||||
val afterLoad: MutableList<() -> Unit> = ArrayList()
|
||||
val afterLoad = mutableListOf<() -> Unit>()
|
||||
var error: Boolean = false
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.core.dex.nodes.MethodNode
|
||||
import jadx.core.dex.visitors.DotGraphVisitor
|
||||
import jadx.core.utils.DebugUtils
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
import java.io.File
|
||||
|
||||
class Debug(private val jadx: JadxScriptInstance) {
|
||||
+8
-4
@@ -1,14 +1,18 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.api.JadxArgs
|
||||
import jadx.api.JavaClass
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class Decompile(private val jadx: JadxScriptInstance) {
|
||||
|
||||
fun all() {
|
||||
jadx.classes.forEach(JavaClass::decompile)
|
||||
fun all(ignoreCache: Boolean = false) {
|
||||
if (ignoreCache) {
|
||||
jadx.classes.forEach(JavaClass::reload)
|
||||
} else {
|
||||
jadx.classes.forEach(JavaClass::decompile)
|
||||
}
|
||||
}
|
||||
|
||||
fun allThreaded(threadsCount: Int = JadxArgs.DEFAULT_THREADS_COUNT) {
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.api.metadata.ICodeNodeRef
|
||||
import jadx.api.plugins.gui.JadxGuiContext
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class Gui(
|
||||
private val jadx: JadxScriptInstance,
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.api.plugins.options.JadxPluginOptions
|
||||
import jadx.api.plugins.options.OptionDescription
|
||||
import jadx.api.plugins.options.OptionFlag
|
||||
import jadx.api.plugins.options.OptionType
|
||||
import jadx.api.plugins.options.impl.JadxOptionDescription
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class JadxScriptAllOptions : JadxPluginOptions {
|
||||
lateinit var values: Map<String, String>
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.core.dex.attributes.AFlag
|
||||
import jadx.core.dex.attributes.IAttributeNode
|
||||
import jadx.core.dex.nodes.IDexNode
|
||||
import jadx.core.dex.nodes.RootNode
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class Rename(private val jadx: JadxScriptInstance) {
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.core.dex.instructions.args.InsnArg
|
||||
import jadx.core.dex.instructions.args.InsnWrapArg
|
||||
import jadx.core.dex.nodes.InsnNode
|
||||
import jadx.core.dex.nodes.MethodNode
|
||||
import jadx.core.utils.InsnRemover
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class Replace(private val jadx: JadxScriptInstance) {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class Search(jadx: JadxScriptInstance) {
|
||||
private val dec = jadx.internalDecompiler
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.core.dex.nodes.BlockNode
|
||||
import jadx.core.dex.nodes.InsnNode
|
||||
import jadx.core.dex.nodes.MethodNode
|
||||
import jadx.core.dex.nodes.RootNode
|
||||
import jadx.core.dex.regions.Region
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
class Stages(private val jadx: JadxScriptInstance) {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package jadx.plugins.script.runtime.data
|
||||
package jadx.plugins.script.kotlin.runtime.data
|
||||
|
||||
import jadx.api.plugins.pass.JadxPass
|
||||
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo
|
||||
@@ -8,7 +8,7 @@ import jadx.api.plugins.pass.types.JadxPreparePass
|
||||
import jadx.core.dex.nodes.ClassNode
|
||||
import jadx.core.dex.nodes.MethodNode
|
||||
import jadx.core.dex.nodes.RootNode
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
|
||||
|
||||
private fun buildScriptName(jadx: JadxScriptInstance, name: String) = "JadxScript$name(${jadx.scriptName})"
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
jadx.plugins.script.kotlin.JadxScriptKotlinPlugin
|
||||
+23
-1
@@ -1,7 +1,10 @@
|
||||
package jadx.plugins.script.ide
|
||||
package jadx.plugins.script
|
||||
|
||||
import jadx.plugins.script.kotlin.ScriptServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic.Severity.ERROR
|
||||
|
||||
class ScriptServicesTest {
|
||||
|
||||
@@ -12,8 +15,10 @@ class ScriptServicesTest {
|
||||
val result = ScriptServices().analyze(name, script)
|
||||
println(result)
|
||||
assertThat(result.success).isTrue()
|
||||
assertThat(result.issues).noneMatch { it.severity == ERROR }
|
||||
}
|
||||
|
||||
@Disabled("External dependencies not resolved")
|
||||
@Test
|
||||
fun testAnalyzeDeps() {
|
||||
val name = "test-deps"
|
||||
@@ -21,6 +26,7 @@ class ScriptServicesTest {
|
||||
val result = ScriptServices().analyze(name, script)
|
||||
println(result)
|
||||
assertThat(result.success).isTrue()
|
||||
assertThat(result.issues).noneMatch { it.severity == ERROR }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -38,6 +44,22 @@ class ScriptServicesTest {
|
||||
.allMatch { c -> c.text == "log" }
|
||||
}
|
||||
|
||||
@Disabled("External dependencies not resolved")
|
||||
@Test
|
||||
fun testCompleteDeps() {
|
||||
val sampleName = "test-deps"
|
||||
val script = getSampleScript(sampleName)
|
||||
val startPos = script.indexOf("StringEscapeUtils.escapeJava")
|
||||
val completePos = startPos + 26 // StringEscapeUtils.escapeJa| <- complete 'escapeJava('
|
||||
val exprEnd = script.indexOf('}', startIndex = completePos)
|
||||
val curScript = script.removeRange(completePos, exprEnd)
|
||||
val result = ScriptServices().complete(sampleName, curScript, completePos)
|
||||
println(result)
|
||||
assertThat(result.completions)
|
||||
.hasSize(1)
|
||||
.allMatch { c -> c.text == "escapeJava(" }
|
||||
}
|
||||
|
||||
private fun getSampleScript(scriptName: String): String {
|
||||
val resFile = javaClass.classLoader.getResource("samples/$scriptName.jadx.kts")
|
||||
return resFile!!.readText()
|
||||
@@ -1,29 +0,0 @@
|
||||
## JADX scripting support
|
||||
|
||||
:exclamation: Work still in progress! Script API is not stable!
|
||||
|
||||
### Examples
|
||||
|
||||
Check script examples in [`examples/scripts/`](https://github.com/skylot/jadx/tree/master/jadx-plugins/jadx-script/examples/scripts)(start with [`hello`](https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-script/examples/scripts/hello.jadx.kts))
|
||||
|
||||
### Script usage
|
||||
|
||||
#### In jadx-cli
|
||||
|
||||
Just add script file as input
|
||||
|
||||
#### In jadx-gui
|
||||
|
||||
1. Add script file to the project (using `Add files` or `New script` by right-click menu on `Inputs/Scripts`)
|
||||
2. Script will appear in `Inputs/Scripts` section
|
||||
3. After script change, you can run it using `Run` button in script editor toolbar or reload whole project (`Reload` button in toolbar or `F5`).
|
||||
Also, you can enable `Live reload` option in `File` menu to reload project automatically on scripts change
|
||||
|
||||
### Script development
|
||||
|
||||
Jadx-gui for now don't support ~~autocompletion~~, ~~errors highlighting~~, code navigation and docs,
|
||||
so the best approach for script editing is to open jadx project in IntelliJ IDEA and write your script in `examples/scripts/` folder.
|
||||
Also, this allows to debug your scripts: for that you need to create run configuration for jadx-cli or jadx-gui
|
||||
add breakpoints and next run it in debug mode (jadx-gui is preferred because of faster script reload).
|
||||
|
||||
Script logs and compilation errors will appear in `Log viewer` (try filter for show only script related logs)
|
||||
@@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
id("jadx-kotlin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
|
||||
|
||||
implementation(kotlin("stdlib-common"))
|
||||
implementation(kotlin("script-runtime"))
|
||||
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
|
||||
// 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")
|
||||
implementation("org.apache.commons:commons-text:1.15.0")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
kotlin.srcDirs(
|
||||
"scripts",
|
||||
"scripts/deobf",
|
||||
"scripts/gui",
|
||||
"context",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
@file:Suppress("MayBeConstant", "unused")
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jadx.plugins.script.runtime.JadxScriptInstance
|
||||
|
||||
/**
|
||||
* Stubs for JadxScriptBaseClass script super class
|
||||
*/
|
||||
|
||||
val log = KotlinLogging.logger("JadxScript")
|
||||
val scriptName = "script"
|
||||
|
||||
fun getJadxInstance(): JadxScriptInstance {
|
||||
throw IllegalStateException("Stub method!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotations for maven imports
|
||||
*/
|
||||
@Target(AnnotationTarget.FILE)
|
||||
@Repeatable
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class DependsOn(vararg val artifactsCoordinates: String, val options: Array<String> = [])
|
||||
|
||||
@Target(AnnotationTarget.FILE)
|
||||
@Repeatable
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class Repository(vararg val repositoriesCoordinates: String, val options: Array<String> = [])
|
||||
@@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
id("jadx-kotlin")
|
||||
id("jadx-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
|
||||
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation(kotlin("scripting-jvm"))
|
||||
implementation(kotlin("scripting-compiler-embeddable"))
|
||||
implementation(kotlin("scripting-ide-services"))
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
id("jadx-kotlin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
|
||||
implementation(project(":jadx-commons:jadx-app-commons"))
|
||||
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation(kotlin("scripting-jvm"))
|
||||
implementation(kotlin("scripting-jvm-host"))
|
||||
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
|
||||
testImplementation(project(":jadx-core"))
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package jadx.plugins.script
|
||||
|
||||
import jadx.api.plugins.gui.ISettingsGroup
|
||||
import jadx.api.plugins.gui.JadxGuiContext
|
||||
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
|
||||
import javax.swing.JPanel
|
||||
|
||||
object JadxScriptOptionsUI {
|
||||
|
||||
fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) {
|
||||
val settings = guiContext.settings()
|
||||
val subGroups = scriptOptions.descriptions
|
||||
.groupBy { it.script }
|
||||
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
|
||||
.toList()
|
||||
settings.setCustomSettingsGroup(EmptyRootGroup("Scripts", subGroups))
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyRootGroup(
|
||||
private val title: String,
|
||||
private val subGroups: List<ISettingsGroup>,
|
||||
) : ISettingsGroup {
|
||||
|
||||
override fun getTitle() = title
|
||||
|
||||
override fun buildComponent() = JPanel()
|
||||
|
||||
override fun getSubGroups() = subGroups
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package jadx.plugins.script
|
||||
|
||||
import jadx.api.plugins.JadxPlugin
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.JadxPluginInfo
|
||||
import jadx.plugins.script.passes.JadxScriptAfterLoadPass
|
||||
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
|
||||
|
||||
class JadxScriptPlugin : JadxPlugin {
|
||||
override fun getPluginInfo() = JadxPluginInfo("jadx-script", "Jadx Script", "Scripting support for jadx")
|
||||
|
||||
override fun init(context: JadxPluginContext) {
|
||||
val scriptOptions = JadxScriptAllOptions()
|
||||
context.registerOptions(scriptOptions)
|
||||
val scripts = ScriptEval().process(context, scriptOptions)
|
||||
if (scripts.isNotEmpty()) {
|
||||
context.addPass(JadxScriptAfterLoadPass(scripts))
|
||||
context.guiContext?.let { JadxScriptOptionsUI.setup(it, scriptOptions) }
|
||||
}
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
package jadx.plugins.script
|
||||
|
||||
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.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.constructorArgs
|
||||
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
||||
import kotlin.script.experimental.host.toScriptSource
|
||||
import kotlin.script.experimental.jvm.compilationCache
|
||||
import kotlin.script.experimental.jvm.jvm
|
||||
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
|
||||
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
|
||||
import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
class ScriptEval {
|
||||
|
||||
fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
|
||||
val jadx = context.decompiler
|
||||
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
|
||||
if (scripts.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val scriptingHost = buildScriptingHost(context)
|
||||
val compileConf = buildCompileConf()
|
||||
val scriptDataList = mutableListOf<JadxScriptData>()
|
||||
for (scriptFile in scripts) {
|
||||
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
|
||||
scriptDataList.add(scriptData)
|
||||
eval(scriptingHost, compileConf, scriptData)
|
||||
}
|
||||
return scriptDataList
|
||||
}
|
||||
|
||||
private fun eval(
|
||||
scriptingHost: BasicJvmScriptingHost,
|
||||
compileConf: ScriptCompilationConfiguration,
|
||||
scriptData: JadxScriptData,
|
||||
) {
|
||||
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
|
||||
val evalConf = buildEvalConf(scriptData)
|
||||
val execTime = measureTimeMillis {
|
||||
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compileConf, evalConf)
|
||||
processEvalResult(result, scriptData)
|
||||
}
|
||||
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
|
||||
}
|
||||
|
||||
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, scriptData: JadxScriptData) {
|
||||
val log = scriptData.log
|
||||
for (r in res.reports) {
|
||||
val msg = r.render(withSeverity = false)
|
||||
when (r.severity) {
|
||||
Severity.FATAL, Severity.ERROR -> log.error(r.exception) { "Script execution error: $msg" }
|
||||
Severity.WARNING -> log.warn { "Script execution issue: $msg" }
|
||||
Severity.INFO -> log.info { "Script report: $msg" }
|
||||
Severity.DEBUG -> {} // ignore, too verbose
|
||||
}
|
||||
}
|
||||
when (res) {
|
||||
is ResultWithDiagnostics.Success -> {
|
||||
when (val retVal = res.value.returnValue) {
|
||||
is ResultValue.Error -> log.error(retVal.error) { "Script execution error:" }
|
||||
is ResultValue.Value -> log.info { "Script execution result: $retVal" }
|
||||
is ResultValue.Unit -> {}
|
||||
ResultValue.NotEvaluated -> {}
|
||||
}
|
||||
}
|
||||
|
||||
is ResultWithDiagnostics.Failure -> {
|
||||
scriptData.error = true
|
||||
log.error { "Script execution failed: ${scriptData.scriptName}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildScriptingHost(context: JadxPluginContext) = BasicJvmScriptingHost(
|
||||
baseHostConfiguration = ScriptingHostConfiguration {
|
||||
jvm {
|
||||
compilationCache(ScriptCache().build(context))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate<JadxScriptTemplate>()
|
||||
|
||||
fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration {
|
||||
val baseEvalConf = createJvmEvaluationConfigurationFromTemplate<JadxScriptTemplate>()
|
||||
return ScriptEvaluationConfiguration(baseEvalConf) {
|
||||
constructorArgs(scriptData)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
jadx.plugins.script.JadxScriptPlugin
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
@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)}" }
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
id("jadx-kotlin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":jadx-core"))
|
||||
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation(kotlin("scripting-jvm"))
|
||||
|
||||
// allow to use maven dependencies in scripts
|
||||
implementation(kotlin("scripting-dependencies"))
|
||||
implementation(kotlin("scripting-dependencies-maven"))
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
|
||||
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-java-convert"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-raung-input"))
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
package jadx.plugins.script.runtime
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.script.experimental.annotations.KotlinScript
|
||||
import kotlin.script.experimental.api.ResultWithDiagnostics
|
||||
import kotlin.script.experimental.api.ScriptAcceptedLocation
|
||||
import kotlin.script.experimental.api.ScriptCollectedData
|
||||
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.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
|
||||
import kotlin.script.experimental.api.isStandalone
|
||||
import kotlin.script.experimental.api.onSuccess
|
||||
import kotlin.script.experimental.api.refineConfiguration
|
||||
import kotlin.script.experimental.api.with
|
||||
import kotlin.script.experimental.dependencies.CompoundDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.DependsOn
|
||||
import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.Repository
|
||||
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
|
||||
import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations
|
||||
import kotlin.script.experimental.jvm.JvmDependency
|
||||
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
|
||||
import kotlin.script.experimental.jvm.jvm
|
||||
|
||||
@KotlinScript(
|
||||
displayName = "Jadx Script",
|
||||
fileExtension = "jadx.kts",
|
||||
compilationConfiguration = JadxScriptConfiguration::class,
|
||||
)
|
||||
abstract class JadxScriptTemplate(
|
||||
scriptData: JadxScriptData,
|
||||
) {
|
||||
val scriptName = scriptData.scriptName
|
||||
val log = scriptData.log
|
||||
|
||||
private val scriptInstance = JadxScriptInstance(scriptData, log)
|
||||
|
||||
fun getJadxInstance() = scriptInstance
|
||||
|
||||
fun println(message: Any?) {
|
||||
log.info { message }
|
||||
}
|
||||
|
||||
fun print(message: Any?) {
|
||||
log.info { message }
|
||||
}
|
||||
}
|
||||
|
||||
object JadxScriptConfiguration : ScriptCompilationConfiguration({
|
||||
defaultImports(DependsOn::class, Repository::class)
|
||||
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(
|
||||
wholeClasspath = true,
|
||||
)
|
||||
}
|
||||
ide {
|
||||
acceptedLocations(ScriptAcceptedLocation.Everywhere)
|
||||
}
|
||||
|
||||
refineConfiguration {
|
||||
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
|
||||
}
|
||||
|
||||
isStandalone(true)
|
||||
|
||||
// forcing compiler to not use modules while building script classpath
|
||||
// because shadow jar remove all modules-info.class (https://github.com/GradleUp/shadow/issues/710)
|
||||
compilerOptions.append("-Xjdk-release=1.8")
|
||||
})
|
||||
|
||||
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
|
||||
|
||||
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user