├── src ├── test │ ├── resources │ │ ├── partial_invalid_inputs │ │ │ ├── invalid_firsthalfofclass.jimple │ │ │ ├── invalid_secondhalfofclass.jimple │ │ │ ├── invalid_juststmt.jimple │ │ │ └── invalid_juststmts.jimple │ │ ├── FieldSignatureOccurences.jimple │ │ └── signatureOccurences.jimple │ └── java │ │ └── com │ │ └── github │ │ └── swissiety │ │ └── jimplelsp │ │ ├── UtilTest.java │ │ ├── a2048Test.java │ │ ├── tetusTest.java │ │ ├── pomodoroTest.java │ │ ├── sms_googleTest.java │ │ ├── resolver │ │ └── SignaturePositionResolverTest.java │ │ └── SemanticTokenTest.java └── main │ └── java │ └── com │ └── github │ └── swissiety │ └── jimplelsp │ ├── workingtree │ ├── VersionedFile.java │ ├── InMemoryVersionedFileImpl.java │ ├── ShadowedWorkingTree.java │ └── WorkingTree.java │ ├── resolver │ ├── PositionComparator.java │ ├── SignatureRangeContainer.java │ ├── SignaturePositionResolver.java │ └── LocalPositionResolver.java │ ├── provider │ ├── JimpleSymbolProvider.java │ ├── JimpleLabelProvider.java │ ├── SemanticTokenManager.java │ └── SyntaxHighlightingProvider.java │ ├── JimpleLsp.java │ ├── Util.java │ ├── Main.java │ ├── JimpleWorkspaceService.java │ ├── JimpleLspServer.java │ └── JimpleTextDocumentService.java ├── vscode ├── .vscodeignore ├── tsconfig.json ├── .vscode │ └── launch.json ├── README.md ├── package.json └── src │ └── extension.ts ├── .github └── workflows │ ├── distribute-release.yml │ └── maven.yml ├── README.md └── pom.xml /src/test/resources/partial_invalid_inputs/invalid_firsthalfofclass.jimple: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/partial_invalid_inputs/invalid_secondhalfofclass.jimple: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | **/*.ts 3 | **/*.map 4 | node_modules/** 5 | tsconfig.json 6 | .gitignore 7 | -------------------------------------------------------------------------------- /src/test/resources/partial_invalid_inputs/invalid_juststmt.jimple: -------------------------------------------------------------------------------- 1 | r0. = "something "; -------------------------------------------------------------------------------- /src/test/resources/partial_invalid_inputs/invalid_juststmts.jimple: -------------------------------------------------------------------------------- 1 | r0. = 1208; 2 | r0. = "something amazing"; -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/workingtree/VersionedFile.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.workingtree; 2 | 3 | public interface VersionedFile { 4 | 5 | String getUriStr(); 6 | 7 | int getVersion(); 8 | 9 | String getContent(); 10 | } 11 | -------------------------------------------------------------------------------- /vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "rootDir": "src", 7 | "lib": [ "es6" ], 8 | "sourceMap": true 9 | }, 10 | "include": [ 11 | "src" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test" 16 | ] 17 | } -------------------------------------------------------------------------------- /vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | 6 | { 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "name": "Launch & Install LSP Client", 10 | "runtimeExecutable": "${execPath}", 11 | "args": [ 12 | "--extensionDevelopmentPath=${workspaceRoot}" 13 | ], 14 | "outFiles": [ 15 | "${workspaceRoot}/client/out/**/*.js" 16 | ], 17 | "preLaunchTask": { 18 | "type": "npm", 19 | "script": "compile" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/FieldSignatureOccurences.jimple: -------------------------------------------------------------------------------- 1 | // this Jimple file is not valid in respect to other classes i.e. dont parse a sootclass from it! 2 | // DO NOT MODIFY POSITIONS! 3 | class de.upb.soot.concrete.fieldReference.A extends java.lang.Object 4 | { 5 | int integervariable; 6 | public java.lang.String str; 7 | 8 | void () 9 | { 10 | de.upb.soot.concrete.fieldReference.A r0; 11 | 12 | r0 := @this: de.upb.soot.concrete.fieldReference.A; 13 | 14 | specialinvoke r0.()>(); 15 | 16 | r0. = 15; 17 | 18 | r0. = "greater"; 19 | 20 | return; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/signatureOccurences.jimple: -------------------------------------------------------------------------------- 1 | // this Jimple file is not valid in respect to other classes i.e. dont parse a sootclass from it! 2 | // DO NOT MODIFY POSITIONS! 3 | public class de.upb.Car extends de.upb.Vehicle implements de.upb.SelfDriving 4 | { 5 | 6 | public void () 7 | { 8 | de.upb.Car r0; 9 | 10 | r0 := @this: de.upb.Car; 11 | 12 | specialinvoke r0.()>(); 13 | 14 | return; 15 | } 16 | 17 | public void driving() throws java.lang.Exception 18 | { 19 | java.lang.Exception $r0; 20 | de.upb.Car r1; 21 | 22 | r1 := @this: de.upb.Car; 23 | 24 | $r0 = new java.lang.Exception; 25 | 26 | specialinvoke $r0.(java.lang.String)>("Banana"); 27 | 28 | throw $r0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/workingtree/InMemoryVersionedFileImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.workingtree; 2 | 3 | 4 | import javax.annotation.Nonnull; 5 | 6 | public class InMemoryVersionedFileImpl implements VersionedFile { 7 | 8 | private final String uri; 9 | private final String data; 10 | private final int version; 11 | 12 | public InMemoryVersionedFileImpl(@Nonnull String uri, @Nonnull String data, int version) { 13 | this.version = version; 14 | this.uri = uri; 15 | this.data = data; 16 | } 17 | 18 | @Override 19 | public String getUriStr() { 20 | return uri; 21 | } 22 | 23 | @Override 24 | public int getVersion() { 25 | return version; 26 | } 27 | 28 | @Override 29 | public String getContent() { 30 | return data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vscode/README.md: -------------------------------------------------------------------------------- 1 | # JimpleLSP 2 | 3 | This is a Plugin to use [JimpleLSP](https://github.com/swissiety/JimpleLSP) - a Language Server Protocol implementation 4 | for Jimple - with Visual Studio Code. 5 | 6 | ### Usage 7 | Open existing **.jimple** files in your workspace and get spoiled by syntax highlighting as well as support for code exploring. 8 | 9 | Or you can extract Jimple from **.apk**s or **.jar**s in the workspace. 10 | To enable the extraction of Jimple adapt the configuration accordingly so JimpleLSP can find [Soot](). 11 | For more information on this please visit [JimpleLSP](https://github.com/swissiety/JimpleLSP). 12 | 13 | **Hint:** The extraction is only triggered if no jimple files are in the workspace on language server startup! 14 | 15 | 16 | ### Production mode 17 | 18 | Set Lsp Transport to stdio (default). 19 | 20 | ### Development mode 21 | 22 | 1. Set Lsp Transport to socket. 23 | 2. Execute the JimpleLSP Jar with argument -s to communicate via socket 2403. -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/resolver/PositionComparator.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.resolver; 2 | 3 | import java.util.Comparator; 4 | import org.eclipse.lsp4j.Position; 5 | 6 | /** 7 | * The PositionComparator helps to compare Position e.g. for creating an ordered Collection 8 | * 9 | * @author Markus Schmidt 10 | */ 11 | class PositionComparator implements Comparator { 12 | private static final PositionComparator INSTANCE = new PositionComparator(); 13 | 14 | public static PositionComparator getInstance() { 15 | return INSTANCE; 16 | } 17 | 18 | @Override 19 | public int compare(Position o1, Position o2) { 20 | if (o1.getLine() < o2.getLine()) { 21 | return -1; 22 | } else if (o1.getLine() == o2.getLine()) { 23 | if (o1.getCharacter() < o2.getCharacter()) { 24 | return -1; 25 | } else if (o1.getCharacter() == o2.getCharacter()) { 26 | return 0; 27 | } 28 | return 1; 29 | } 30 | return 1; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/distribute-release.yml: -------------------------------------------------------------------------------- 1 | name: distribute Artifact to marketplace 2 | 3 | on: 4 | release: 5 | type: released 6 | 7 | jobs: 8 | distribute-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - id: download-release-asset 12 | name: Download release asset 13 | uses: dsaltares/fetch-gh-release-asset@master 14 | with: 15 | version: github.ref #defaults to latest - but this is not necessarily the released draft 16 | regex: true 17 | file: "**.vsix" 18 | target: './' 19 | 20 | - name: "Check .vsix file existence" 21 | uses: andstor/file-existence-action@v1 22 | with: 23 | files: "./**.vsix" 24 | 25 | # - name: Publish to Open VSX Registry 26 | # uses: HaaLeo/publish-vscode-extension@v1 27 | # id: publishToOpenVSX 28 | # with: 29 | # pat: ${{ secrets.OPEN_VSX_TOKEN }} 30 | - name: Publish to Visual Studio Marketplace 31 | uses: HaaLeo/publish-vscode-extension@v1 32 | with: 33 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 34 | registryUrl: https://marketplace.visualstudio.com 35 | extensionFile: "./**.vsix" -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | 5 | name: Java CI with Maven 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | jobs: 14 | # Code coverage (i.e. jacoco) needs the same classes for its test otherwise its classids can possibly not match 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up JDK 1.8 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: 8 25 | cache: maven 26 | distribution: temurin 27 | 28 | - name: Check Format 29 | run: mvn com.coveo:fmt-maven-plugin:check -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 30 | 31 | - name: Build 32 | run: mvn compile -B -Dfmt.skip -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 33 | 34 | - name: Test 35 | run: mvn -am verify -B -Dfmt.skip -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 36 | 37 | draft-release: 38 | needs: test 39 | if: startsWith(github.ref, 'refs/tags/v') 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: checkout 43 | uses: actions/checkout@v3 44 | 45 | - name: Set up JDK 1.8 46 | uses: actions/setup-java@v3 47 | with: 48 | java-version: 8 49 | cache: maven 50 | distribution: temurin 51 | 52 | - name: build vscode extension 53 | run: cd vscode && npm install && npm install -g vsce && vsce package 54 | 55 | - name: create Release info 56 | uses: softprops/action-gh-release@v1 57 | if: startsWith(github.ref, 'refs/tags/') 58 | with: 59 | draft: true 60 | body: | 61 | // TODO: 62 | **Bugfixes** 63 | - [ ] something 64 | **Features** 65 | - [ ] sth else 66 | 67 | files: | 68 | **/jimplelsp-*.jar 69 | **/jimplelsp-*.vsix 70 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/UtilTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import static junit.framework.TestCase.assertEquals; 4 | import static org.junit.Assert.fail; 5 | 6 | import java.nio.file.Paths; 7 | import org.junit.Test; 8 | 9 | public class UtilTest { 10 | 11 | // TODO: test utf8 en/decoding in uri 12 | 13 | @Test 14 | public void testPathToUri() { 15 | 16 | assertEquals( 17 | "file:/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 18 | Paths.get("file:///home/smarkus/IdeaProjects/JimpleLspExampleProject/module1").toString()); 19 | assertEquals( 20 | "file:/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 21 | Paths.get("file://home/smarkus/IdeaProjects/JimpleLspExampleProject/module1").toString()); 22 | assertEquals( 23 | "/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 24 | Paths.get("/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1").toString()); 25 | 26 | assertEquals( 27 | "file:///home/smarkus/IdeaProjects/JimpleLspExampleProject/module1/", 28 | Util.pathToUri(Paths.get("/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1"))); 29 | } 30 | 31 | @Test 32 | public void testUriToPath() { 33 | assertEquals( 34 | "/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 35 | Util.uriToPath("file:///home/smarkus/IdeaProjects/JimpleLspExampleProject/module1") 36 | .toString()); 37 | try { 38 | assertEquals( 39 | "/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 40 | Util.uriToPath("file://home/smarkus/IdeaProjects/JimpleLspExampleProject/module1") 41 | .toString()); 42 | fail(); 43 | } catch (IllegalArgumentException ignore) { 44 | } 45 | ; 46 | 47 | assertEquals( 48 | "/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 49 | Util.uriToPath("file:/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1") 50 | .toString()); 51 | 52 | try { 53 | assertEquals( 54 | "/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1", 55 | Util.uriToPath("/home/smarkus/IdeaProjects/JimpleLspExampleProject/module1").toString()); 56 | // fail(); 57 | } catch (IllegalArgumentException ignore) { 58 | } 59 | ; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/provider/JimpleSymbolProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.provider; 2 | 3 | import com.github.swissiety.jimplelsp.resolver.SignaturePositionResolver; 4 | import org.eclipse.lsp4j.Location; 5 | import org.eclipse.lsp4j.SymbolInformation; 6 | import org.eclipse.lsp4j.SymbolKind; 7 | import org.eclipse.lsp4j.SymbolKindCapabilities; 8 | import sootup.core.model.SootClass; 9 | import sootup.core.model.SootField; 10 | import sootup.core.model.SootMethod; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.util.List; 14 | 15 | /** 16 | * The JimpleSymbolProvider retrieves symbols for WorkspaceSymbolRequest and DocumentSymbolRequest 17 | * 18 | * @author Markus Schmidt 19 | */ 20 | public class JimpleSymbolProvider { 21 | 22 | public static void retrieveAndFilterSymbolsFromClass( 23 | @Nonnull List resultList, 24 | String query, 25 | @Nonnull SootClass clazz, 26 | SignaturePositionResolver resolver, 27 | @Nonnull SymbolKindCapabilities symbolKind, 28 | int limit) { 29 | final List clientSupportedSymbolKinds = symbolKind.getValueSet(); 30 | 31 | // TODO: implement limit 32 | 33 | if (clientSupportedSymbolKinds.contains(SymbolKind.Class)) { 34 | // retrieve classes 35 | if (query == null || clazz.getName().toLowerCase().contains(query)) { 36 | Location location = 37 | resolver.findFirstMatchingSignature(clazz.getType(), clazz.getPosition()); 38 | resultList.add(new SymbolInformation(clazz.getName(), SymbolKind.Class, location)); 39 | } 40 | } 41 | 42 | if (clientSupportedSymbolKinds.contains(SymbolKind.Method)) { 43 | // retrieve methods 44 | for (SootMethod method : clazz.getMethods()) { 45 | if (query == null || method.getName().toLowerCase().contains(query)) { 46 | // find first signature matching 47 | Location location = 48 | resolver.findFirstMatchingSignature(method.getSignature(), method.getPosition()); 49 | resultList.add(new SymbolInformation(method.getName(), SymbolKind.Method, location)); 50 | } 51 | } 52 | } 53 | 54 | if (clientSupportedSymbolKinds.contains(SymbolKind.Field)) { 55 | // retrieve fields 56 | for (SootField field : clazz.getFields()) { 57 | if (query == null || field.getName().toLowerCase().contains(query)) { 58 | Location location = 59 | resolver.findFirstMatchingSignature(field.getSignature(), field.getPosition()); 60 | resultList.add(new SymbolInformation(field.getName(), SymbolKind.Field, location)); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/JimpleLsp.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import java.io.IOException; 4 | import java.text.MessageFormat; 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.Future; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | 12 | import org.apache.commons.cli.*; 13 | import org.eclipse.lsp4j.jsonrpc.Launcher; 14 | import org.eclipse.lsp4j.jsonrpc.MessageConsumer; 15 | import org.eclipse.lsp4j.jsonrpc.validation.ReflectiveMessageValidator; 16 | import org.eclipse.lsp4j.launch.LSPLauncher; 17 | import org.eclipse.lsp4j.services.LanguageClient; 18 | import org.eclipse.lsp4j.services.LanguageServer; 19 | import org.graalvm.polyglot.Language; 20 | 21 | /** 22 | * @author Markus Schmidt 23 | */ 24 | public class JimpleLsp { 25 | 26 | private static final String DEFAULT_PORT = "2403"; 27 | private static CommandLine cmd = null; 28 | 29 | public static void main(String... args) throws IOException, InterruptedException, ExecutionException { 30 | Options cliOptions = 31 | new Options() 32 | .addOption( 33 | "s", 34 | "socket", 35 | false, 36 | MessageFormat.format("run in socket mode, standard port is {0}", DEFAULT_PORT)) 37 | .addOption( 38 | "p", 39 | "port", 40 | true, 41 | MessageFormat.format( 42 | "sets the port for socket mode, standard port is {0}", DEFAULT_PORT)); 43 | 44 | CommandLineParser parser = new DefaultParser(); 45 | 46 | try { 47 | cmd = parser.parse(cliOptions, args); 48 | } catch (ParseException e) { 49 | e.printStackTrace(); 50 | System.exit(1); 51 | } 52 | 53 | JimpleLspServer server = new JimpleLspServer(); 54 | 55 | /* if (cmd.hasOption("socket")) { 56 | int port = Integer.parseInt(cmd.getOptionValue("port", DEFAULT_PORT)); 57 | System.out.println("Socket:" + port); 58 | // FIXME LanguageServer.launchOnSocketPort(port, createServer); 59 | } else */ { 60 | 61 | Launcher l = LSPLauncher.createServerLauncher(server, System.in, System.out); 62 | Future startListening = l.startListening(); 63 | server.connectClient(l.getRemoteProxy()); 64 | startListening.get(); 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/provider/JimpleLabelProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.provider; 2 | 3 | import com.github.swissiety.jimplelsp.Util; 4 | import org.apache.commons.lang3.tuple.Pair; 5 | import org.eclipse.lsp4j.Range; 6 | import org.eclipse.lsp4j.TextDocumentPositionParams; 7 | import org.jheaps.annotations.Beta; 8 | import sootup.core.model.SootClass; 9 | import sootup.jimple.JimpleBaseListener; 10 | import sootup.jimple.JimpleParser; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * This listener implementation lists all label usage positions when its passed into the Jimple 20 | * Parser 21 | * 22 | * @author Markus Schmidt 23 | */ 24 | @Beta 25 | public class JimpleLabelProvider extends JimpleBaseListener { 26 | @Nonnull private final Map labelTargets = new HashMap<>(); 27 | @Nonnull private final List> labelUsage = new ArrayList<>(); 28 | 29 | public void resolve(@Nonnull SootClass sc, @Nonnull TextDocumentPositionParams pos) { 30 | // TODO: implement label indexing 31 | } 32 | 33 | @Override 34 | public void enterStatement(JimpleParser.StatementContext ctx) { 35 | // add label preceeding a stmt as target into the map 36 | if (ctx.label_name != null) { 37 | String text = ctx.label_name.getText(); 38 | if (text.length() > 0) { 39 | labelTargets.put(text, Util.ctxToRange(ctx)); 40 | } 41 | } 42 | super.enterStatement(ctx); 43 | } 44 | 45 | @Override 46 | public void enterGoto_stmt(JimpleParser.Goto_stmtContext ctx) { 47 | // adds the labels of all branching stmts (goto,if,switch) -> JimpleParser.Goto_stmtContext is 48 | // part of all of these stmts! 49 | final JimpleParser.IdentifierContext labelCtx = ctx.label_name; 50 | if (labelCtx != null) { 51 | labelUsage.add(Pair.of(labelCtx.getText(), Util.ctxToRange(labelCtx))); 52 | } 53 | super.enterGoto_stmt(ctx); 54 | } 55 | 56 | @Override 57 | public void enterTrap_clause(JimpleParser.Trap_clauseContext ctx) { 58 | // add labes usage from catch clause 59 | final JimpleParser.IdentifierContext from = ctx.from; 60 | if (from != null) { 61 | labelUsage.add(Pair.of(from.getText(), Util.ctxToRange(from))); 62 | } 63 | final JimpleParser.IdentifierContext to = ctx.to; 64 | if (to != null) { 65 | labelUsage.add(Pair.of(to.getText(), Util.ctxToRange(to))); 66 | } 67 | final JimpleParser.IdentifierContext with = ctx.with; 68 | if (with != null) { 69 | labelUsage.add(Pair.of(with.getText(), Util.ctxToRange(with))); 70 | } 71 | super.enterTrap_clause(ctx); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/workingtree/ShadowedWorkingTree.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.workingtree; 2 | 3 | import com.ibm.wala.classLoader.SourceFileModule; 4 | 5 | import java.io.IOException; 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | 14 | /** 15 | * 16 | * Manages a copy of the actual workingDirectory to allow e.g. a longer running analysis to do its magic while not blocking the language client 17 | * 18 | * */ 19 | public class ShadowedWorkingTree extends WorkingTree{ 20 | 21 | private Map sourceFileModules; 22 | /** Server-side URI string mapped to client-side URI string. */ 23 | private Map serverClientUri; 24 | 25 | /** 26 | * Instantiates a new source file manager. 27 | * 28 | * @param language the language 29 | * @param serverClientUri the server client uri 30 | */ 31 | public ShadowedWorkingTree(String language, Map serverClientUri) { 32 | super(language); 33 | this.sourceFileModules = new HashMap<>(); 34 | this.serverClientUri = serverClientUri; 35 | } 36 | 37 | 38 | /* 39 | public void generateSourceFileModule(URI clientUri, VersionedFile versionedFile) { 40 | SourceFileModule sourceFile = null; 41 | try { 42 | File file = File.createTempFile("temp", getFileSuffix()); 43 | file.deleteOnExit(); 44 | String text = versionedFile.getContent(); 45 | TemporaryFile.stringToFile(file, text); 46 | String[] strs = clientUri.toString().split("/"); 47 | String className = strs[strs.length - 1]; 48 | sourceFile = new SourceFileModule(file, className, null); 49 | this.sourceFileModules.put(clientUri, sourceFile); 50 | URI serverUri = Paths.get(file.toURI()).toUri(); 51 | // store the mapping from server-side URI to client-side URI. 52 | this.serverClientUri.put(serverUri.toString(), clientUri.toString()); 53 | if (serverUri.toString().startsWith("file:///")) { 54 | this.serverClientUri.put( 55 | "file:/" + serverUri.toString().substring(8), clientUri.toString()); 56 | } 57 | } catch (IOException e) { 58 | // FIXME 59 | e.printStackTrace(); 60 | } 61 | } 62 | */ 63 | 64 | 65 | /** Delete all server-side source files sent by the client. */ 66 | public void cleanUp() { 67 | for (String file : this.serverClientUri.keySet()) { 68 | try { 69 | Files.deleteIfExists(Paths.get(new URI(file))); 70 | } catch (IOException | URISyntaxException e) { 71 | // FIXME! 72 | e.printStackTrace(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import org.antlr.v4.runtime.ParserRuleContext; 4 | import org.eclipse.lsp4j.Location; 5 | import org.eclipse.lsp4j.LocationLink; 6 | import org.eclipse.lsp4j.Range; 7 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 8 | import sootup.core.model.Position; 9 | import sootup.core.model.SootClass; 10 | import sootup.jimple.parser.JimpleConverterUtil; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.net.URI; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.Collections; 17 | import java.util.List; 18 | 19 | /** @author Markus Schmidt */ 20 | public class Util { 21 | 22 | @Nonnull 23 | public static String classToUri(@Nonnull SootClass clazz) { 24 | return pathToUri(clazz.getClassSource().getSourcePath()); 25 | } 26 | 27 | @Nonnull 28 | public static String pathToUri(@Nonnull Path sourcePath) { 29 | return sourcePath.toUri().toString(); 30 | } 31 | 32 | @Nonnull 33 | public static Path uriToPath(@Nonnull String uri) { 34 | return Paths.get(URI.create(uri)); 35 | } 36 | 37 | @Nonnull 38 | public static Range ctxToRange(@Nonnull ParserRuleContext ctx) { 39 | // line numbers starting zero-based in LSP vs one-based in antlr 40 | final Position sootPosition = JimpleConverterUtil.buildPositionFromCtx(ctx); 41 | return new Range( 42 | new org.eclipse.lsp4j.Position(sootPosition.getFirstLine(), sootPosition.getFirstCol()), 43 | new org.eclipse.lsp4j.Position(sootPosition.getLastLine(), sootPosition.getLastCol())); 44 | } 45 | 46 | @Nonnull 47 | public static Range positionToDefRange(@Nonnull Position position) { 48 | // line numbers starting zero-based in LSP vs one-based in antlr 49 | // extract interesting part /beginning which usually is the signature of the current Range 50 | return new Range( 51 | new org.eclipse.lsp4j.Position(position.getFirstLine(), position.getFirstCol()), 52 | // to next line 53 | new org.eclipse.lsp4j.Position(position.getFirstLine() + 1, 0)); 54 | } 55 | 56 | @Nonnull 57 | public static Range positionToRange(@Nonnull Position position) { 58 | // line numbers starting zero-based in LSP vs one-based in antlr 59 | return new Range( 60 | new org.eclipse.lsp4j.Position(position.getFirstLine(), position.getFirstCol()), 61 | new org.eclipse.lsp4j.Position(position.getLastLine(), position.getLastCol())); 62 | } 63 | 64 | public static Either, List> 65 | positionToLocationList(@Nonnull String uri, @Nonnull Position position) { 66 | return Either.forLeft( 67 | Collections.singletonList(new Location(uri, positionToDefRange(position)))); 68 | } 69 | 70 | public static Location positionToDefLocation(@Nonnull String uri, @Nonnull Position position) { 71 | return new Location(uri, positionToDefRange(position)); 72 | } 73 | 74 | public static Location positionToLocation(@Nonnull String uri, @Nonnull Position position) { 75 | return new Location(uri, positionToRange(position)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/Main.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import java.io.*; 4 | import java.net.InetSocketAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | import java.util.concurrent.Executors; 8 | import javax.annotation.Nonnull; 9 | import org.apache.commons.io.input.TeeInputStream; 10 | import org.apache.commons.io.output.TeeOutputStream; 11 | import org.eclipse.lsp4j.jsonrpc.Launcher; 12 | import org.eclipse.lsp4j.launch.LSPLauncher; 13 | import org.eclipse.lsp4j.services.LanguageClient; 14 | 15 | /** 16 | * Entrypoint for JimpleLSP for Usecases where the MagpieBridge is not necessary. 17 | * 18 | * @author Markus Schmidt 19 | */ 20 | @Deprecated 21 | public class Main { 22 | 23 | public static void main(@Nonnull String[] args) { 24 | 25 | if (args.length > 0) { 26 | int port = Integer.parseInt(args[0]); 27 | System.out.println(">Socket:" + port); 28 | ServerSocket clientSocket; 29 | InputStream in; 30 | OutputStream out; 31 | 32 | // start the socket server 33 | try (ServerSocket serverSocket = new ServerSocket()) { 34 | serverSocket.bind(new InetSocketAddress(port)); 35 | 36 | while (true) { 37 | Socket socket = serverSocket.accept(); 38 | System.out.println(">new Connection from " + socket.getInetAddress()); 39 | 40 | final JimpleLspServer server = new JimpleLspServer(); 41 | Launcher launcher = 42 | new Launcher.Builder() 43 | .setLocalService(server) 44 | .setRemoteInterface(LanguageClient.class) 45 | .setInput(logStream(socket.getInputStream(), "serverOut")) 46 | .setOutput(logStream(socket.getOutputStream(), "serverIn")) 47 | .setExecutorService(Executors.newCachedThreadPool()) 48 | .traceMessages(new PrintWriter(System.out)) 49 | .create(); 50 | launcher.startListening(); 51 | server.connectClient(launcher.getRemoteProxy()); 52 | } 53 | 54 | } catch (IOException exception) { 55 | exception.printStackTrace(); 56 | } 57 | 58 | } else { 59 | JimpleLspServer server = new JimpleLspServer(); 60 | Launcher l = 61 | LSPLauncher.createServerLauncher( 62 | server, logStream(System.in, "serverOut"), logStream(System.out, "serverIn")); 63 | l.startListening(); 64 | server.connectClient(l.getRemoteProxy()); 65 | } 66 | } 67 | 68 | static InputStream logStream(InputStream is, String logFileName) { 69 | File log; 70 | try { 71 | log = File.createTempFile(logFileName, ".txt"); 72 | return new TeeInputStream(is, new FileOutputStream(log)); 73 | } catch (IOException e) { 74 | return is; 75 | } 76 | } 77 | 78 | static OutputStream logStream(OutputStream os, String logFileName) { 79 | File log; 80 | try { 81 | log = File.createTempFile(logFileName, ".txt"); 82 | return new TeeOutputStream(os, new FileOutputStream(log)); 83 | } catch (IOException e) { 84 | return os; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/a2048Test.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import org.eclipse.lsp4j.*; 4 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 5 | import org.junit.Before; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import sootup.core.model.SootClass; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @Ignore 21 | public class a2048Test { 22 | 23 | final JimpleLspServer jimpleLspServer = new JimpleLspServer(); 24 | 25 | @Before 26 | public void setup() { 27 | 28 | final InitializeParams params = new InitializeParams(); 29 | Path root = Paths.get("src/test/resources/a2048/"); 30 | params.setRootUri(Util.pathToUri(root)); 31 | ClientCapabilities clientCaps = new ClientCapabilities(); 32 | TextDocumentClientCapabilities docCaps = new TextDocumentClientCapabilities(); 33 | docCaps.setDeclaration(new DeclarationCapabilities(true)); 34 | docCaps.setDefinition(new DefinitionCapabilities(true)); 35 | docCaps.setReferences(new ReferencesCapabilities(true)); 36 | docCaps.setImplementation(new ImplementationCapabilities(true)); 37 | docCaps.setDocumentSymbol(new DocumentSymbolCapabilities(true)); 38 | docCaps.setHover(new HoverCapabilities(true)); 39 | 40 | clientCaps.setTextDocument(docCaps); 41 | 42 | WorkspaceClientCapabilities wCaps = new WorkspaceClientCapabilities(); 43 | wCaps.setSymbol( 44 | new SymbolCapabilities( 45 | new SymbolKindCapabilities( 46 | Arrays.asList(SymbolKind.Class, SymbolKind.Method, SymbolKind.Field)))); 47 | clientCaps.setWorkspace(wCaps); 48 | 49 | params.setCapabilities(clientCaps); 50 | 51 | jimpleLspServer.initialize(params); 52 | jimpleLspServer.initialized(new InitializedParams()); 53 | final Collection> classes = jimpleLspServer.getView().getClasses(); 54 | assertEquals("Not all Classes are loaded/parsed", 87, classes.size()); 55 | } 56 | 57 | @Test 58 | public void testReferences() throws ExecutionException, InterruptedException { 59 | 60 | final ReferenceParams params = new ReferenceParams(new ReferenceContext(false)); 61 | params.setTextDocument(new TextDocumentIdentifier("uri")); 62 | params.setPosition(new Position(0, 0)); 63 | final CompletableFuture> references = 64 | jimpleLspServer.getTextDocumentService().references(params); 65 | final List locations = references.get(); 66 | } 67 | 68 | @Test 69 | public void testDefinition() throws ExecutionException, InterruptedException { 70 | final DefinitionParams position = 71 | new DefinitionParams(new TextDocumentIdentifier("uri"), new Position(0, 0)); 72 | final CompletableFuture, List>> 73 | definition = jimpleLspServer.getTextDocumentService().definition(position); 74 | final Either, List> listListEither = 75 | definition.get(); 76 | 77 | final Range range = new Range(new Position(0, 0), new Position(0, 0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/tetusTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import org.eclipse.lsp4j.*; 4 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 5 | import org.junit.Before; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import sootup.core.model.SootClass; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @Ignore 21 | public class tetusTest { 22 | 23 | final JimpleLspServer jimpleLspServer = new JimpleLspServer(); 24 | 25 | @Before 26 | public void setup() { 27 | 28 | final InitializeParams params = new InitializeParams(); 29 | Path root = Paths.get("src/test/resources/tetus/"); 30 | params.setRootUri(Util.pathToUri(root)); 31 | ClientCapabilities clientCaps = new ClientCapabilities(); 32 | TextDocumentClientCapabilities docCaps = new TextDocumentClientCapabilities(); 33 | docCaps.setDeclaration(new DeclarationCapabilities(true)); 34 | docCaps.setDefinition(new DefinitionCapabilities(true)); 35 | docCaps.setReferences(new ReferencesCapabilities(true)); 36 | docCaps.setImplementation(new ImplementationCapabilities(true)); 37 | docCaps.setDocumentSymbol(new DocumentSymbolCapabilities(true)); 38 | docCaps.setHover(new HoverCapabilities(true)); 39 | 40 | clientCaps.setTextDocument(docCaps); 41 | 42 | WorkspaceClientCapabilities wCaps = new WorkspaceClientCapabilities(); 43 | wCaps.setSymbol( 44 | new SymbolCapabilities( 45 | new SymbolKindCapabilities( 46 | Arrays.asList(SymbolKind.Class, SymbolKind.Method, SymbolKind.Field)))); 47 | clientCaps.setWorkspace(wCaps); 48 | 49 | params.setCapabilities(clientCaps); 50 | 51 | jimpleLspServer.initialize(params); 52 | jimpleLspServer.initialized(new InitializedParams()); 53 | final Collection> classes = jimpleLspServer.getView().getClasses(); 54 | assertEquals("Not all Classes are loaded/parsed", 59, classes.size()); 55 | } 56 | 57 | @Test 58 | public void testReferences() throws ExecutionException, InterruptedException { 59 | 60 | final ReferenceParams params = new ReferenceParams(new ReferenceContext(false)); 61 | params.setTextDocument(new TextDocumentIdentifier("uri")); 62 | params.setPosition(new Position(0, 0)); 63 | final CompletableFuture> references = 64 | jimpleLspServer.getTextDocumentService().references(params); 65 | final List locations = references.get(); 66 | } 67 | 68 | @Test 69 | public void testDefinition() throws ExecutionException, InterruptedException { 70 | final DefinitionParams position = 71 | new DefinitionParams(new TextDocumentIdentifier("uri"), new Position(0, 0)); 72 | final CompletableFuture, List>> 73 | definition = jimpleLspServer.getTextDocumentService().definition(position); 74 | final Either, List> listListEither = 75 | definition.get(); 76 | 77 | final Range range = new Range(new Position(0, 0), new Position(0, 0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/pomodoroTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import org.eclipse.lsp4j.*; 4 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 5 | import org.junit.Before; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import sootup.core.model.SootClass; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @Ignore 21 | public class pomodoroTest { 22 | 23 | final JimpleLspServer jimpleLspServer = new JimpleLspServer(); 24 | 25 | @Before 26 | public void setup() { 27 | 28 | final InitializeParams params = new InitializeParams(); 29 | Path root = Paths.get("src/test/resources/pomodoro/"); 30 | params.setRootUri(Util.pathToUri(root)); 31 | ClientCapabilities clientCaps = new ClientCapabilities(); 32 | TextDocumentClientCapabilities docCaps = new TextDocumentClientCapabilities(); 33 | docCaps.setDeclaration(new DeclarationCapabilities(true)); 34 | docCaps.setDefinition(new DefinitionCapabilities(true)); 35 | docCaps.setReferences(new ReferencesCapabilities(true)); 36 | docCaps.setImplementation(new ImplementationCapabilities(true)); 37 | docCaps.setDocumentSymbol(new DocumentSymbolCapabilities(true)); 38 | docCaps.setHover(new HoverCapabilities(true)); 39 | 40 | clientCaps.setTextDocument(docCaps); 41 | 42 | WorkspaceClientCapabilities wCaps = new WorkspaceClientCapabilities(); 43 | wCaps.setSymbol( 44 | new SymbolCapabilities( 45 | new SymbolKindCapabilities( 46 | Arrays.asList(SymbolKind.Class, SymbolKind.Method, SymbolKind.Field)))); 47 | clientCaps.setWorkspace(wCaps); 48 | 49 | params.setCapabilities(clientCaps); 50 | 51 | jimpleLspServer.initialize(params); 52 | jimpleLspServer.initialized(new InitializedParams()); 53 | final Collection> classes = jimpleLspServer.getView().getClasses(); 54 | assertEquals("Not all Classes are loaded/parsed", 65, classes.size()); 55 | } 56 | 57 | @Test 58 | public void testReferences() throws ExecutionException, InterruptedException { 59 | 60 | final ReferenceParams params = new ReferenceParams(new ReferenceContext(false)); 61 | params.setTextDocument(new TextDocumentIdentifier("uri")); 62 | params.setPosition(new Position(0, 0)); 63 | final CompletableFuture> references = 64 | jimpleLspServer.getTextDocumentService().references(params); 65 | final List locations = references.get(); 66 | } 67 | 68 | @Test 69 | public void testDefinition() throws ExecutionException, InterruptedException { 70 | final DefinitionParams position = 71 | new DefinitionParams(new TextDocumentIdentifier("uri"), new Position(0, 0)); 72 | final CompletableFuture, List>> 73 | definition = jimpleLspServer.getTextDocumentService().definition(position); 74 | final Either, List> listListEither = 75 | definition.get(); 76 | 77 | final Range range = new Range(new Position(0, 0), new Position(0, 0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/sms_googleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import org.eclipse.lsp4j.*; 4 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 5 | import org.junit.Before; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import sootup.core.model.SootClass; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @Ignore 21 | public class sms_googleTest { 22 | 23 | final JimpleLspServer jimpleLspServer = new JimpleLspServer(); 24 | 25 | @Before 26 | public void setup() { 27 | 28 | final InitializeParams params = new InitializeParams(); 29 | Path root = Paths.get("src/test/resources/sms_google/"); 30 | params.setRootUri(Util.pathToUri(root)); 31 | ClientCapabilities clientCaps = new ClientCapabilities(); 32 | TextDocumentClientCapabilities docCaps = new TextDocumentClientCapabilities(); 33 | docCaps.setDeclaration(new DeclarationCapabilities(true)); 34 | docCaps.setDefinition(new DefinitionCapabilities(true)); 35 | docCaps.setReferences(new ReferencesCapabilities(true)); 36 | docCaps.setImplementation(new ImplementationCapabilities(true)); 37 | docCaps.setDocumentSymbol(new DocumentSymbolCapabilities(true)); 38 | docCaps.setHover(new HoverCapabilities(true)); 39 | 40 | clientCaps.setTextDocument(docCaps); 41 | 42 | WorkspaceClientCapabilities wCaps = new WorkspaceClientCapabilities(); 43 | wCaps.setSymbol( 44 | new SymbolCapabilities( 45 | new SymbolKindCapabilities( 46 | Arrays.asList(SymbolKind.Class, SymbolKind.Method, SymbolKind.Field)))); 47 | clientCaps.setWorkspace(wCaps); 48 | 49 | params.setCapabilities(clientCaps); 50 | 51 | jimpleLspServer.initialize(params); 52 | jimpleLspServer.initialized(new InitializedParams()); 53 | final Collection> classes = jimpleLspServer.getView().getClasses(); 54 | assertEquals("Not all Classes are loaded/parsed", 39, classes.size()); 55 | } 56 | 57 | @Test 58 | public void testReferences() throws ExecutionException, InterruptedException { 59 | 60 | final ReferenceParams params = new ReferenceParams(new ReferenceContext(false)); 61 | params.setTextDocument(new TextDocumentIdentifier("uri")); 62 | params.setPosition(new Position(0, 0)); 63 | final CompletableFuture> references = 64 | jimpleLspServer.getTextDocumentService().references(params); 65 | final List locations = references.get(); 66 | } 67 | 68 | @Test 69 | public void testDefinition() throws ExecutionException, InterruptedException { 70 | final DefinitionParams position = 71 | new DefinitionParams(new TextDocumentIdentifier("uri"), new Position(0, 0)); 72 | final CompletableFuture, List>> 73 | definition = jimpleLspServer.getTextDocumentService().definition(position); 74 | final Either, List> listListEither = 75 | definition.get(); 76 | 77 | final Range range = new Range(new Position(0, 0), new Position(0, 0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/JimpleWorkspaceService.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import com.github.swissiety.jimplelsp.provider.JimpleSymbolProvider; 4 | import com.github.swissiety.jimplelsp.resolver.SignaturePositionResolver; 5 | import org.eclipse.lsp4j.*; 6 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 7 | import org.eclipse.lsp4j.services.WorkspaceService; 8 | import sootup.core.model.SootClass; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.concurrent.CompletableFuture; 13 | 14 | /** @author Markus Schmidt */ 15 | public class JimpleWorkspaceService implements WorkspaceService { 16 | private final JimpleLspServer server; 17 | 18 | public JimpleWorkspaceService(JimpleLspServer server) { 19 | this.server = server; 20 | } 21 | 22 | JimpleLspServer getServer() { 23 | return (JimpleLspServer) server; 24 | } 25 | 26 | @Override 27 | public CompletableFuture, List>> symbol(WorkspaceSymbolParams params) { 28 | return getServer() 29 | .pool( 30 | () -> { 31 | int limit = 32; 32 | List list = new ArrayList<>(limit); 33 | 34 | final String query = params.getQuery().trim().toLowerCase(); 35 | 36 | // start searching if the query has sth relevant/"enough" input for searching 37 | if (query.length() >= 2) { 38 | getServer() 39 | .getView() 40 | .getClasses() 41 | .forEach( 42 | clazz -> { 43 | if (list.size() >= limit) { 44 | return; 45 | } 46 | 47 | final SymbolCapabilities workspaceSymbol = 48 | getServer().getClientCapabilities().getWorkspace().getSymbol(); 49 | if (workspaceSymbol == null) { 50 | return; 51 | } 52 | final SymbolKindCapabilities symbolKind = workspaceSymbol.getSymbolKind(); 53 | if (symbolKind == null) { 54 | return; 55 | } 56 | 57 | final SignaturePositionResolver signaturePositionResolver = 58 | ((JimpleTextDocumentService) getServer().getTextDocumentService()) 59 | .getSignaturePositionResolver(Util.classToUri(clazz)); 60 | /* FIXME; JimpleSymbolProvider.retrieveAndFilterSymbolsFromClass( 61 | list, 62 | query, 63 | (SootClass) clazz, 64 | signaturePositionResolver, 65 | symbolKind, 66 | limit); */ 67 | }); 68 | } 69 | return Either.forLeft(list); 70 | }); 71 | } 72 | 73 | @Override 74 | public void didChangeConfiguration(DidChangeConfigurationParams didChangeConfigurationParams) { 75 | 76 | } 77 | 78 | @Override 79 | public void didChangeWatchedFiles(DidChangeWatchedFilesParams didChangeWatchedFilesParams) { 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/provider/SemanticTokenManager.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.provider; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.ListIterator; 6 | import javax.annotation.Nonnull; 7 | import org.eclipse.lsp4j.SemanticTokensLegend; 8 | 9 | /** @author Markus Schmidt */ 10 | public class SemanticTokenManager { 11 | @Nonnull private final SemanticTokensLegend legend; 12 | 13 | @Nonnull final List encodedSemanticTokens = new ArrayList<>(); 14 | int lastTokenLine, lastTokenColumn = 0; 15 | 16 | public SemanticTokenManager(@Nonnull SemanticTokensLegend legend) { 17 | this.legend = legend; 18 | } 19 | 20 | public void paintText(@Nonnull String type, @Nonnull String mod, int line, int col, int length) { 21 | // at index 5*i - deltaLine: token line number, relative to the previous token 22 | encodedSemanticTokens.add(line - lastTokenLine); 23 | 24 | // at index 5*i+1 - deltaStart: token start character, relative to the previous token 25 | // (relative to 0 or the previous token’s start if they are on the same line) 26 | if (line == lastTokenLine) { 27 | encodedSemanticTokens.add(col - lastTokenColumn); 28 | } else { 29 | encodedSemanticTokens.add(col); 30 | } 31 | 32 | // at index 5*i+2 - length: the length of the token. 33 | encodedSemanticTokens.add(length); 34 | 35 | // at index 5*i+3 - tokenType: will be looked up in SemanticTokensLegend.tokenTypes. We 36 | // currently ask that tokenType < 65536. 37 | final int typeIdx = legend.getTokenTypes().indexOf(type); // TODO [ms] performance 38 | if (typeIdx < 0) { 39 | throw new RuntimeException(type + " is not supported in semantic token legend!"); 40 | } 41 | encodedSemanticTokens.add(typeIdx); 42 | 43 | // at index 5*i+4 - tokenModifiers: each set bit will be looked up in 44 | // SemanticTokensLegend.tokenModifiers 45 | encodedSemanticTokens.add( 46 | Math.max(legend.getTokenModifiers().indexOf(mod), 0)); // TODO performance 47 | 48 | lastTokenLine = line; 49 | lastTokenColumn = col; 50 | } 51 | 52 | public List getCanvas() { 53 | return encodedSemanticTokens; 54 | } 55 | 56 | public SemanticTokensLegend getLegend() { 57 | return legend; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "SemanticTokenManager{" 63 | + "legend=" 64 | + legend 65 | + ", lastTokenLine=" 66 | + lastTokenLine 67 | + ", lastTokenColumn=" 68 | + lastTokenColumn 69 | + ", encodedSemanticTokens=\n" 70 | + humanReadableTokenList() 71 | + "\n}"; 72 | } 73 | 74 | public String humanReadableTokenList() { 75 | StringBuilder sb = new StringBuilder(); 76 | ListIterator it = encodedSemanticTokens.listIterator(); 77 | while (it.hasNext()) { 78 | int deltaLine = it.next(); 79 | int deltaCol = it.next(); 80 | int length = it.next(); 81 | int tokenTypeIdx = it.next(); 82 | int tokenModIdx = it.next(); 83 | 84 | sb.append("token: ").append(legend.getTokenTypes().get(tokenTypeIdx)); 85 | sb.append("\tmodifier: ").append(tokenModIdx); 86 | sb.append("\tdeltaLine: ").append(deltaLine); 87 | sb.append("\tdeltaCol: ").append(deltaCol); 88 | sb.append("\tlength: ").append(length); 89 | sb.append('\n'); 90 | } 91 | return sb.toString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "JimpleLSP", 3 | "name": "jimplelsp", 4 | "description": "A Language Server implementation for Soots Jimple powered by FutureSoot & the MagpieBridge framework.", 5 | "author": "Markus Schmidt", 6 | "license": "MIT", 7 | "version": "0.1.0", 8 | "homepage": "https://github.com/swissiety/JimpleLSP/", 9 | "bugs": { 10 | "url": "https://github.com/swissiety/JimpleLSP/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/swissiety/JimpleLSP" 15 | }, 16 | "publisher": "swissiety", 17 | "categories": [ 18 | "Programming Languages" 19 | ], 20 | "keywords": [ 21 | "Soot, FutureSoot, Jimple, Language Server, LSP, .jimple" 22 | ], 23 | "engines": { 24 | "vscode": "^1.56.0" 25 | }, 26 | "activationEvents": [ 27 | "onLanguage:jimple", 28 | "workspaceContains:**/*.apk", 29 | "workspaceContains:**/*.jar" 30 | ], 31 | "main": "./out/extension", 32 | "contributes": { 33 | "languages": [ 34 | { 35 | "id": "jimple", 36 | "aliases": [ 37 | "Jimple", 38 | "jimple" 39 | ], 40 | "extensions": [ 41 | ".Jimple", 42 | ".jimple" 43 | ] 44 | } 45 | ], 46 | "configuration": { 47 | "title": "JimpleLSP", 48 | "properties": { 49 | "JimpleLSP.trace.server": { 50 | "scope": "window", 51 | "type": "string", 52 | "enum": [ 53 | "off", 54 | "messages", 55 | "verbose" 56 | ], 57 | "default": "off", 58 | "description": "Traces the communication between VS Code and the language server." 59 | }, 60 | "JimpleLSP.lspTransport": { 61 | "scope": "window", 62 | "type": "string", 63 | "enum": [ 64 | "stdio", 65 | "socket" 66 | ], 67 | "default": "stdio", 68 | "description": "Specifies the mode of transport used to communicate with the Jimple language server." 69 | }, 70 | "JimpleLSP.jimpleextraction.sootpath": { 71 | "scope": "window", 72 | "type": "string", 73 | "default": "", 74 | "description": "Tells JimpleLSP where it can find the Soot executable to extract Jimple on demand." 75 | }, 76 | "JimpleLSP.jimpleextraction.androidplatforms": { 77 | "scope": "window", 78 | "type": "string", 79 | "default": "", 80 | "description": "Tells Soot where it can find the necessary android.jar to convert an .apk to jimple. Usually found in ANDROID_HOME/platforms" 81 | } 82 | } 83 | }, 84 | "configurationDefaults": { 85 | "[jimple]": { 86 | "editor.semanticHighlighting.enabled": true 87 | } 88 | } 89 | }, 90 | "scripts": { 91 | "vscode:prepublish": "cd ../ && mvn package && cd vscode && rm jimplelsp-*.jar && cp `ls -v ../target/jimplelsp-* | tail -n 1` ./ && npm run -S esbuild-base -- --minify", 92 | "compile": "tsc -b", 93 | "watch": "tsc -b -w", 94 | "postinstall": "node ./node_modules/vscode/bin/install", 95 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", 96 | "esbuild": "npm run -S esbuild-base -- --sourcemap", 97 | "esbuild-watch": "npm run -S esbuild-base -- --sourcemap --watch", 98 | "test-compile": "tsc -p ./" 99 | }, 100 | "dependencies": { 101 | "vscode-languageclient": "^7.0.0", 102 | "xmlhttprequest-ts": "^1.0.1" 103 | }, 104 | "devDependencies": { 105 | "@angular/common": "^5.0.0", 106 | "@angular/core": "^5.0.0", 107 | "@types/mocha": "^5.2.6", 108 | "@types/node": "^11.15.53", 109 | "esbuild": "^0.11.23", 110 | "rxjs": "^5.5.12", 111 | "tslint": "^5.15.0", 112 | "typescript": "^3.4.2", 113 | "vsce": "^1.88.0", 114 | "vscode": "^1.1.37", 115 | "zone.js": "^0.8.29" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/resolver/SignatureRangeContainer.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.resolver; 2 | 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import org.eclipse.lsp4j.Position; 5 | import org.eclipse.lsp4j.Range; 6 | import sootup.core.signatures.Signature; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | 15 | /** 16 | * The Datastructure holds information about Signatures of Soot and their Range positions in a File. 17 | * 18 | * @author Markus Schmidt 19 | */ 20 | class SignatureRangeContainer { 21 | 22 | @Nonnull List startPositions = new ArrayList<>(); 23 | @Nonnull List endPositions = new ArrayList<>(); 24 | @Nonnull List signatures = new ArrayList<>(); 25 | 26 | Comparator comparator = new PositionComparator(); 27 | 28 | void add( sootup.core.model.Position position, Signature sig) { 29 | // insert sorted to be accessed via binary search 30 | final Position startPos = new Position(position.getFirstLine(), position.getFirstCol()); 31 | int idx = Collections.binarySearch(startPositions, startPos, new PositionComparator()); 32 | if (idx < 0) { 33 | // calculate insertion index 34 | idx = -idx - 1; 35 | 36 | } else { 37 | throw new IllegalStateException( 38 | "position " + startPos + " is already taken by " + signatures.get(idx)); 39 | } 40 | 41 | startPositions.add(idx, startPos); 42 | endPositions.add(idx, new Position(position.getLastLine(), position.getLastCol())); 43 | 44 | signatures.add(idx, sig); 45 | } 46 | 47 | @Nullable 48 | Pair resolve(@Nonnull Position position) { 49 | if (startPositions.isEmpty()) { 50 | return null; 51 | } 52 | int index = getStartingIndex(position); 53 | 54 | if (index < 0) { 55 | return null; 56 | } 57 | 58 | final Position startPos = startPositions.get(index); 59 | final Position endPos = endPositions.get(index); 60 | if (comparator.compare(startPos, position) <= 0 && comparator.compare(position, endPos) <= 0) { 61 | return Pair.of(signatures.get(index), new Range(startPos, endPos)); 62 | } 63 | return null; 64 | } 65 | 66 | // binary search for position or return next smaller occurence (i.e. start position of a Range) 67 | private int getStartingIndex(@Nonnull Position position) { 68 | int index = 69 | Collections.binarySearch(startPositions, position, PositionComparator.getInstance()); 70 | if (index < 0) { 71 | // not exactly found: check if next smaller neighbour is surrounding it 72 | index = (-index) - 1 - 1; 73 | } else if (index >= startPositions.size()) { 74 | // not exactly found: (greater than last element) check if next smaller neighbour is 75 | // surrounding it 76 | index = index - 1; 77 | } 78 | return Math.max(0, index); 79 | } 80 | 81 | public List resolve(@Nonnull Signature signature) { 82 | final List ranges = new ArrayList<>(); 83 | for (int i = 0, signaturesSize = signatures.size(); i < signaturesSize; i++) { 84 | Signature sig = signatures.get(i); 85 | if (sig.equals(signature)) { 86 | ranges.add(new Range(startPositions.get(i), endPositions.get(i))); 87 | } 88 | } 89 | return ranges; 90 | } 91 | 92 | @Nullable 93 | public Range findFirstMatchingSignature( 94 | Signature signature, sootup.core.model.Position position) { 95 | 96 | int idx = getStartingIndex(new Position(position.getFirstLine(), position.getFirstCol())); 97 | 98 | // loop is expected to do max. 2 iterations 99 | for (int i = idx, signaturesSize = signatures.size(); i < signaturesSize; i++) { 100 | Signature sig = signatures.get(i); 101 | if (sig.equals(signature)) { 102 | return new Range(startPositions.get(i), endPositions.get(i)); 103 | } 104 | } 105 | return null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JimpleLSP 2 | This is an implementation of the Language Server Protocol for Jimple. 3 | A LSP Server implementation allows an IDE, which supports the LSP, to provide well known features of programming languages for a specific Language. 4 | 5 | # Installation 6 | ## Get the server Jar. 7 | **Either:** download the JimpleLSP Server Release and extract it to any Location. 8 | 9 | **Or:** Compile from source. 10 | 1. run `git clone https://github.com/swissiety/JimpleLSP` to clone this Repository. 11 | 2. run `mvn package` to build a Jar. The generated Jar can be found in target/jimplelsp-0.0.1-SNAPSHOT-jar-with-dependencies.jar. 12 | 13 | 14 | ## IntelliJ Idea 15 | 1. Install or use an LSP Plugin in your IDE to enable Language Server Protocol Support. 16 | You can use [IntelliJLSP](https://github.com/MagpieBridge/IntelliJLSP/tree/intellijclientlib) 17 | 2. Configure the LSP Plugin in your IDE to use the JimpleLSP Server Jar. 18 | 19 | ## VSCode 20 | Install the [JimpleLSP Extension](https://marketplace.visualstudio.com/items?itemName=swissiety.jimplelsp) from the Marketplace. 21 | 22 | Or if you want to compile it yourself: 23 | 1. `cd vscode/ && npm install` to install the dependencies. 24 | 2. `npm install -g vsce` to install the Visual Studio Code Extension commandline tool. 25 | 3. `vsce package` to package the plugin. 26 | 4. `code --install-extension JimpleLSP-0.0.5.vsix` to install the packaged plugin. 27 | 5. restart VSCode 28 | 29 | # Usage 30 | 31 | You can import already extracted Jimple files into your workspace. 32 | 33 | JimpleLSP can extract Jimple from a .jar or an .apk file do simplify your workflow. To enable JimpleLSP to extract 34 | Jimple your .apk or .jar file needs to be in your workspace on startup of your IDE, but the workspace must not contain a 35 | .jimple file. Additionally you need to configure the following config key in your IDE: ** 36 | JimpleLSP.jimpleextraction.androidplatforms** to tell Soot where it can find the Android runtimes usually located in 37 | ANDROID_HOME/platforms. 38 | 39 | A Collection of android.jars is available in the [android-platforms](https://github.com/Sable/android-platforms/) Repo. 40 | 41 | # Development 42 | 43 | The Language Server starts by default in STDIO mode. Use `-s` to start the Language Server executable in socket mode on 44 | Port 2403, to change the port use `-p `. 45 | 46 | ## Server Capabilities 47 | 48 | This Server implementation was initially oriented towards LSP 3.15. The currently implemented features are focussed on improving code exploring in 49 | Jimple. For checked capabilities there are previews of the functionality in the linked Issue. 50 | 51 | ### Text Document Capabilities 52 | 53 | - ✅ didOpen 54 | - ✅ didChange 55 | - ✅ Full text sync 56 | - ❌ Incremental text sync 57 | - ✅ didSave 58 | - ✅ Include text 59 | - ❌ completion 60 | - ✅ [hover](/../../issues/15) 61 | - ❌ signatureHelp 62 | - ✅ [declaration](/../../issues/4) 63 | - ❌ [planned #18] link support 64 | - ✅ [definition](/../../issues/6) 65 | - ✅ link support 66 | - ✅ [typeDefinition](/../../issues/5) 67 | - ❌ [planned #18] link support 68 | - ✅ [implementation](/../../issues/2) 69 | - ❌ [planned #18] link support 70 | - ✅ [references](/../../issues/3) 71 | - ✅ [documentHighlight](/../../issues/11) 72 | - ✅ for Locals 73 | - ❌ types 74 | - ✅ [documentSymbol](/../../issues/12) 75 | - ❌ codeAction 76 | - ❌ resolve 77 | - ❌ codeLens 78 | - ❌ documentLink 79 | - ❌ formatting 80 | - ❌ rangeFormatting 81 | - ❌ onTypeFormatting 82 | - ❌ rename 83 | - ✅ publishDiagnostics 84 | - ❌ [WIP #16] foldingRange 85 | - ❌ selectionRange 86 | - ✅ [semanticTokens](/../../issues/1) 87 | - ❌ callHierarchy 88 | 89 | ### Workspace Capabilities 90 | - ❌ [planned #9] applyEdit 91 | - ❌ didChangeConfiguration 92 | - ❌ [planned #17] didChangeWatchedFiles 93 | - ✅ [symbol](/../../issues/13) 94 | - ❌ executeCommand 95 | 96 | ### Window Capabilities 97 | 98 | - ❌ workDoneProgress 99 | - ❌ create 100 | - ❌ cancel 101 | - ✅ logMessage 102 | - ✅ showMessage 103 | - ✅ showMessage request 104 | 105 | 106 | ## About 107 | This piece of software was created as part of a bachelor thesis at University of Paderborn (UPB), Germany. -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/resolver/SignaturePositionResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.resolver; 2 | 3 | import junit.framework.TestCase; 4 | import org.antlr.v4.runtime.CharStreams; 5 | import org.apache.commons.lang3.tuple.Pair; 6 | import org.eclipse.lsp4j.Position; 7 | import org.eclipse.lsp4j.Range; 8 | import sootup.core.signatures.Signature; 9 | import sootup.jimple.parser.JimpleConverterUtil; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | 15 | public class SignaturePositionResolverTest extends TestCase { 16 | 17 | private SignaturePositionResolver resolver; 18 | private SignaturePositionResolver fieldResolver; 19 | 20 | @Override 21 | protected void setUp() { 22 | try { 23 | Path path = Paths.get("src/test/resources/signatureOccurences.jimple").toAbsolutePath(); 24 | resolver = 25 | new SignaturePositionResolver( 26 | path, 27 | JimpleConverterUtil.createJimpleParser(CharStreams.fromPath(path), path).file()); 28 | 29 | Path path2 = Paths.get("src/test/resources/FieldSignatureOccurences.jimple"); 30 | fieldResolver = 31 | new SignaturePositionResolver( 32 | path2, 33 | JimpleConverterUtil.createJimpleParser(CharStreams.fromPath(path2), path2).file()); 34 | 35 | } catch (IOException exception) { 36 | exception.printStackTrace(); 37 | fail("filenotfound"); 38 | } 39 | } 40 | 41 | public void testResolveClassSigFromClassDefinition() { 42 | final Pair sig = resolver.resolve(new Position(2, 20)); 43 | assertNotNull(sig); 44 | assertEquals("de.upb.Car", sig.getLeft().toString()); 45 | } 46 | 47 | public void testResolveMethodSigFromMethodDefinitionExcatStart() { 48 | // beginning position 49 | final Pair sig = resolver.resolve(new Position(16, 17)); 50 | assertNotNull(sig); 51 | assertEquals("", sig.getLeft().toString()); 52 | } 53 | 54 | public void testResolveMethodSigFromMethodDefinition() { 55 | // middle position 56 | final Pair sig = resolver.resolve(new Position(16, 18)); 57 | assertNotNull(sig); 58 | assertEquals("", sig.getLeft().toString()); 59 | } 60 | 61 | public void testResolveClassSigUsageFromMethodSigException() { 62 | final Pair sig = resolver.resolve(new Position(16, 46)); 63 | assertNotNull(sig); 64 | assertEquals("java.lang.Exception", sig.getLeft().toString()); 65 | } 66 | 67 | public void testResolveClassSigUsageFromExtends() { 68 | final Pair sig = resolver.resolve(new Position(2, 42)); 69 | assertNotNull(sig); 70 | assertEquals("de.upb.Vehicle", sig.getLeft().toString()); 71 | } 72 | 73 | public void testResolveClassSigUsageFromImplements() { 74 | final Pair sig = resolver.resolve(new Position(2, 69)); 75 | assertNotNull(sig); 76 | assertEquals("de.upb.SelfDriving", sig.getLeft().toString()); 77 | } 78 | 79 | public void testResolveClassSigUsageFromBody() { 80 | { 81 | final Pair sig = resolver.resolve(new Position(25, 28)); 82 | assertNotNull(sig); 83 | assertEquals("java.lang.Exception", sig.getLeft().toString()); 84 | } 85 | 86 | { 87 | final Pair sig = resolver.resolve(new Position(23, 20)); 88 | assertNotNull(sig); 89 | assertEquals("java.lang.Exception", sig.getLeft().toString()); 90 | } 91 | } 92 | 93 | // methodsig iside body 94 | public void testResolveMethodSigUsageFromInsideBody() { 95 | final Pair sig = resolver.resolve(new Position(25, 57)); 96 | assertNotNull(sig); 97 | assertEquals("(java.lang.String)>", sig.getLeft().toString()); 98 | } 99 | 100 | public void testResolveClassSigUsageFromInsideBodyMethodParameterType() { 101 | final Pair sig = resolver.resolve(new Position(25, 62)); 102 | assertNotNull(sig); 103 | assertEquals("java.lang.String", sig.getLeft().toString()); 104 | } 105 | 106 | public void testResolveFieldSigUsageFromInsideBody() { 107 | final Pair sig = fieldResolver.resolve(new Position(17, 71)); 108 | assertNotNull(sig); 109 | assertEquals( 110 | "", sig.getLeft().toString()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/workingtree/WorkingTree.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.workingtree; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.StringReader; 6 | import java.io.StringWriter; 7 | import java.net.URI; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import org.eclipse.lsp4j.DidChangeTextDocumentParams; 12 | import org.eclipse.lsp4j.DidOpenTextDocumentParams; 13 | import org.eclipse.lsp4j.DidSaveTextDocumentParams; 14 | import org.eclipse.lsp4j.Range; 15 | import org.eclipse.lsp4j.TextDocumentContentChangeEvent; 16 | import org.eclipse.lsp4j.TextDocumentIdentifier; 17 | import org.eclipse.lsp4j.TextDocumentItem; 18 | import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; 19 | 20 | import javax.annotation.Nonnull; 21 | 22 | /** 23 | * This class manages all files of a given language sent from the language client to server. 24 | * 25 | * @author Linghui Luo, Markus Schmidt 26 | */ 27 | public class WorkingTree { 28 | 29 | /** The language. */ 30 | private final String language; 31 | /** Client-side URI mapped to versioned source file. */ 32 | private final Map versionedFiles; 33 | 34 | /** 35 | * Instantiates a new source file manager. 36 | * 37 | * @param language the language 38 | */ 39 | public WorkingTree(@Nonnull String language) { 40 | this.language = language; 41 | this.versionedFiles = new HashMap<>(); 42 | } 43 | 44 | /** 45 | * Add the opened file to versionedFiles and generate source file module for it. 46 | * 47 | * @param params the params 48 | */ 49 | public void didOpen(DidOpenTextDocumentParams params) { 50 | TextDocumentItem doc = params.getTextDocument(); 51 | if (!doc.getLanguageId().equals(language)) { 52 | return; 53 | } 54 | 55 | String uri = doc.getUri(); 56 | VersionedFile sourceFile = new InMemoryVersionedFileImpl(uri, doc.getText(), doc.getVersion()); 57 | //this.fileStates.put(clientUri, FileState.OPENED); 58 | this.versionedFiles.put(uri, sourceFile); 59 | } 60 | 61 | /** 62 | * Update the changed file and generate source file module for updated file. 63 | * 64 | * @param params the params 65 | */ 66 | public void didChange(DidChangeTextDocumentParams params) { 67 | VersionedTextDocumentIdentifier doc = params.getTextDocument(); 68 | String uri = doc.getUri(); 69 | URI clientUri = URI.create(uri); 70 | // this.fileStates.put(clientUri, FileState.CHANGED); 71 | VersionedFile existFile = versionedFiles.get(URI.create(uri)); 72 | int newVersion = doc.getVersion(); 73 | if (newVersion > existFile.getVersion()) { 74 | String existText = existFile.getContent(); 75 | String newText = existText; 76 | for (TextDocumentContentChangeEvent change : params.getContentChanges()) { 77 | if (change.getRange() == null) { 78 | // the nextText should be the full context of the file. 79 | newText = change.getText(); 80 | } else { 81 | newText = replaceText(newText, change); 82 | } 83 | } 84 | if (newText != null) { 85 | VersionedFile newFile = new InMemoryVersionedFileImpl(uri, newText, newVersion); 86 | this.versionedFiles.put(uri, newFile); 87 | } 88 | } 89 | } 90 | 91 | public void didSave(DidSaveTextDocumentParams params) { 92 | TextDocumentIdentifier doc = params.getTextDocument(); 93 | String uri = doc.getUri(); 94 | // this.fileStates.put(clientUri, FileState.SAVED); 95 | } 96 | 97 | /** 98 | * Replace the old text according to the change. 99 | * 100 | * @param text the text 101 | * @param change the change 102 | * @return the string 103 | */ 104 | private String replaceText(String text, TextDocumentContentChangeEvent change) { 105 | try { 106 | Range range = change.getRange(); 107 | BufferedReader reader = new BufferedReader(new StringReader(text)); 108 | StringWriter writer = new StringWriter(); 109 | int line = 0; 110 | while (line < range.getStart().getLine()) { 111 | writer.write(reader.readLine() + '\n'); 112 | line++; 113 | } 114 | for (int character = 0; character < range.getStart().getCharacter(); character++) { 115 | writer.write(reader.read()); 116 | } 117 | // write the changed text 118 | writer.write(change.getText()); 119 | // skip the old text 120 | reader.skip(change.getRangeLength()); 121 | int next = reader.read(); 122 | while (next != -1) { 123 | writer.write(next); 124 | next = reader.read(); 125 | } 126 | return writer.toString(); 127 | } catch (IOException e) { 128 | // FIXME log(e); 129 | } 130 | return null; 131 | } 132 | 133 | 134 | /** 135 | * Gets the versioned files. 136 | * 137 | * @return the versioned files 138 | */ 139 | public Map getVersionedFiles() { 140 | return versionedFiles; 141 | } 142 | 143 | public VersionedFile get(String uri) { 144 | return versionedFiles.get(uri); 145 | } 146 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | JimpleLSP 6 | com.github.swissiety 7 | jimplelsp 8 | 0.1.0 9 | jar 10 | 11 | 12 | 13 | Markus Schmidt 14 | markus262@web.de 15 | 16 | 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 27 | com.github.soot-oss.SootUp 28 | develop-SNAPSHOT 29 | 30 | 31 | 32 | 33 | 34 | org.eclipse.lsp4j 35 | org.eclipse.lsp4j 36 | 0.21.1 37 | 38 | 39 | 40 | org.eclipse.lsp4j 41 | org.eclipse.lsp4j.jsonrpc 42 | 0.21.1 43 | 44 | 45 | 46 | ${sootup.groupid} 47 | sootup.core 48 | ${sootup.version} 49 | 50 | 51 | ${sootup.groupid} 52 | sootup.java.core 53 | ${sootup.version} 54 | 55 | 56 | ${sootup.groupid} 57 | sootup.jimple.parser 58 | ${sootup.version} 59 | 60 | 61 | junit 62 | junit 63 | 4.13.2 64 | test 65 | 66 | 67 | 68 | commons-cli 69 | commons-cli 70 | 1.4 71 | 72 | 73 | junit 74 | junit 75 | 4.13.1 76 | test 77 | 78 | 79 | org.mockito 80 | mockito-core 81 | 3.6.28 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 89 | com.coveo 90 | fmt-maven-plugin 91 | 2.8 92 | 93 | 94 | package 95 | 96 | format 97 | 98 | 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-shade-plugin 104 | 3.2.2 105 | 106 | 107 | 108 | package 109 | 110 | shade 111 | 112 | 113 | true 114 | 115 | 116 | 118 | com.github.swissiety.jimplelsp.JimpleLsp 119 | 120 | 121 | 122 | 123 | *:* 124 | 125 | META-INF/*.SF 126 | META-INF/*.DSA 127 | META-INF/*.RSA 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | maven-compiler-plugin 137 | 3.7.0 138 | 139 | 1.8 140 | 1.8 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | jitpack.io 150 | https://jitpack.io 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as net from 'net'; 3 | import { XMLHttpRequest } from 'xmlhttprequest-ts'; 4 | import { commands, workspace, ExtensionContext, window, ViewColumn, env, Uri} from 'vscode'; 5 | import { LanguageClient, LanguageClientOptions, ServerOptions, StreamInfo, DynamicFeature, ClientCapabilities, DocumentSelector, InitializeParams, RegistrationData, ServerCapabilities, VersionedTextDocumentIdentifier, RegistrationType } from 'vscode-languageclient/node'; 6 | 7 | var client: LanguageClient = null; 8 | 9 | async function configureAndStartClient(context: ExtensionContext) { 10 | 11 | // Startup options for the language server 12 | const settings = workspace.getConfiguration("JimpleLSP"); 13 | const lspTransport: string = settings.get("lspTransport"); 14 | let executable = 'java'; 15 | let relativePath = "jimplelsp-"+ context.extension.packageJSON.version +".jar"; 16 | let args = ['-jar', context.asAbsolutePath(relativePath)]; 17 | 18 | const serverOptionsStdio = { 19 | run: { command: executable, args: args }, 20 | debug: { command: executable, args: args } 21 | } 22 | 23 | const serverOptionsSocket = () => { 24 | const socket = net.connect({ port: 2403 }) 25 | const result: StreamInfo = { 26 | writer: socket, 27 | reader: socket 28 | } 29 | 30 | return new Promise((resolve) => { 31 | socket.on("connect", () => resolve(result)) 32 | socket.on("error", _ => { 33 | 34 | window.showErrorMessage( 35 | "Failed to connect to the Jimple language server. Make sure that the language server is running " + 36 | "-or- configure the extension to connect via standard IO.", "Open settings", "Reconnect") 37 | .then( function( str ){ 38 | if( str.startsWith("Open") ){ 39 | commands.executeCommand('workbench.action.openSettings', '@ext:' + context.extension.id); 40 | }else if(str.startsWith("Reconnect")){ 41 | configureAndStartClient(context); 42 | } 43 | }); 44 | client = null; 45 | }); 46 | }) 47 | } 48 | 49 | const serverOptions: ServerOptions = 50 | (lspTransport === "stdio") ? serverOptionsStdio : (lspTransport === "socket") ? serverOptionsSocket : null 51 | 52 | let clientOptions: LanguageClientOptions = { 53 | 54 | documentSelector: [{ scheme: 'file', language: 'jimple' }], 55 | synchronize: { 56 | configurationSection: 'JimpleLSP', 57 | fileEvents: [ 58 | workspace.createFileSystemWatcher('**/*.jimple'), 59 | workspace.createFileSystemWatcher('**/*.apk'), 60 | workspace.createFileSystemWatcher('**/*.jar') 61 | ], 62 | } 63 | }; 64 | 65 | // Create the language client and start the client. 66 | client = new LanguageClient('JimpleLSP', 'JimpleLSP', serverOptions, clientOptions); 67 | client.registerFeature(new SupportsShowHTML(client)); 68 | let disposable = client.start(); 69 | context.subscriptions.push(disposable); 70 | await client.onReady(); 71 | } 72 | 73 | export class SupportsShowHTML implements DynamicFeature { 74 | 75 | registrationType: RegistrationType; 76 | 77 | constructor(private _client: LanguageClient) { 78 | 79 | } 80 | 81 | fillInitializeParams?: (params: InitializeParams) => void; 82 | fillClientCapabilities(capabilities: ClientCapabilities): void { 83 | capabilities.experimental = { 84 | supportsShowHTML: true, 85 | } 86 | } 87 | 88 | initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { 89 | let client = this._client; 90 | client.onNotification("magpiebridge/showHTML",(content: string)=>{ 91 | const panel = window.createWebviewPanel("Configuration", "MagpieBridge Control Panel",ViewColumn.One,{ 92 | enableScripts: true 93 | }); 94 | panel.webview.html = content; 95 | panel.webview.onDidReceiveMessage( 96 | message => { 97 | switch(message.command){ 98 | case 'action': 99 | var httpRequest = new XMLHttpRequest(); 100 | var url = message.text; 101 | httpRequest.open('GET',url); 102 | httpRequest.send(); 103 | return ; 104 | case 'configuration': 105 | var httpRequest = new XMLHttpRequest(); 106 | var splits = message.text.split("?"); 107 | var url = splits[0]; 108 | var formData = splits[1]; 109 | httpRequest.open('POST',url); 110 | httpRequest.send(formData); 111 | return ; 112 | 113 | } 114 | } 115 | ); 116 | }) 117 | } 118 | 119 | register( data: RegistrationData): void { 120 | 121 | } 122 | unregister(id: string): void { 123 | 124 | } 125 | dispose(): void { 126 | 127 | } 128 | 129 | } 130 | 131 | 132 | function showWelcomeMessage(context: ExtensionContext) { 133 | let previousVersion = context.globalState.get('jimplelsp-version'); 134 | let currentVersion = context.extension.packageJSON.version; 135 | let message : string | null = null; 136 | let previousVersionArray = previousVersion ? previousVersion.split('.').map((s: string) => Number(s)) : [0, 0, 0]; 137 | let currentVersionArray = currentVersion.split('.').map((s: string) => Number(s)); 138 | if (previousVersion === undefined || previousVersion.length === 0) { 139 | message = "Thanks for using JimpleLSP!\n"; 140 | } else if (currentVersion !== previousVersion && ( 141 | (previousVersionArray[0] === currentVersionArray[0] && previousVersionArray[1] === currentVersionArray[1] && previousVersionArray[2] < currentVersionArray[2]) || 142 | (previousVersionArray[0] === currentVersionArray[0] && previousVersionArray[1] < currentVersionArray[1]) || 143 | (previousVersionArray[0] < currentVersionArray[0]) 144 | ) 145 | ) { 146 | message = "JimpleLSP Plugin updated to " + currentVersion + ".\n"; 147 | } 148 | if (message) { 149 | window.showInformationMessage(message, 'Settings', '⭐️ Star on Github', '🐞 Report Bug') 150 | .then(function (val: string | undefined) { 151 | /*if (val === '⭐️ Rate') { 152 | env.openExternal(vscode.Uri.parse('https://marketplace.visualstudio.com/items?itemName=swissiety.jimplelsp&ssr=false#review-details')); 153 | } else */ 154 | if( val === 'Settings'){ 155 | commands.executeCommand('workbench.action.openSettings', '@ext:' + context.extension.id); 156 | }else if (val === '🐞 Report Bug') { 157 | env.openExternal(Uri.parse('https://github.com/swissiety/JimpleLSP/issues')); 158 | } else if (val === '⭐️ Star on Github') { 159 | env.openExternal(Uri.parse('https://github.com/swissiety/JimpleLSP')); 160 | } 161 | }); 162 | context.globalState.update('jimplelsp-version', currentVersion); 163 | } 164 | } 165 | 166 | export async function activate(context: ExtensionContext) { 167 | showWelcomeMessage(context); 168 | configureAndStartClient(context); 169 | workspace.onDidChangeConfiguration(e => { 170 | if (client) 171 | client.stop().then(() => configureAndStartClient(context)); 172 | else 173 | configureAndStartClient(context) 174 | }) 175 | } 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /src/test/java/com/github/swissiety/jimplelsp/SemanticTokenTest.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.ExecutionException; 15 | import java.util.stream.Collectors; 16 | import org.eclipse.lsp4j.*; 17 | import org.eclipse.lsp4j.services.LanguageClient; 18 | import org.junit.Test; 19 | 20 | public class SemanticTokenTest { 21 | 22 | class LspClient implements LanguageClient { 23 | private final List worskspaceFolders; 24 | 25 | public LspClient(List worskspaceFolders) { 26 | this.worskspaceFolders = 27 | worskspaceFolders.stream() 28 | .map(f -> new WorkspaceFolder(Util.pathToUri(f))) 29 | .collect(Collectors.toList()); 30 | } 31 | 32 | public void connectTo(JimpleLspServer server) { 33 | server.connectClient(this); 34 | server.initialize(getInitializeParams()); 35 | server.initialized(new InitializedParams()); 36 | } 37 | 38 | private InitializeParams getInitializeParams() { 39 | final InitializeParams params = new InitializeParams(); 40 | // params.setRootUri(Util.pathToUri(root)); 41 | ClientCapabilities clientCaps = new ClientCapabilities(); 42 | TextDocumentClientCapabilities docCaps = new TextDocumentClientCapabilities(); 43 | docCaps.setDeclaration(new DeclarationCapabilities(true)); 44 | docCaps.setDefinition(new DefinitionCapabilities(true)); 45 | docCaps.setReferences(new ReferencesCapabilities(true)); 46 | docCaps.setImplementation(new ImplementationCapabilities(true)); 47 | docCaps.setDocumentSymbol(new DocumentSymbolCapabilities(true)); 48 | docCaps.setHover(new HoverCapabilities(true)); 49 | 50 | clientCaps.setTextDocument(docCaps); 51 | 52 | WorkspaceClientCapabilities wCaps = new WorkspaceClientCapabilities(); 53 | wCaps.setSymbol( 54 | new SymbolCapabilities( 55 | new SymbolKindCapabilities( 56 | Arrays.asList(SymbolKind.Class, SymbolKind.Method, SymbolKind.Field)))); 57 | clientCaps.setWorkspace(wCaps); 58 | 59 | params.setCapabilities(clientCaps); 60 | return params; 61 | } 62 | 63 | @Override 64 | public CompletableFuture applyEdit( 65 | ApplyWorkspaceEditParams params) { 66 | return CompletableFuture.completedFuture(null); 67 | } 68 | 69 | @Override 70 | public CompletableFuture registerCapability(RegistrationParams params) { 71 | return CompletableFuture.completedFuture(null); 72 | } 73 | 74 | @Override 75 | public CompletableFuture unregisterCapability(UnregistrationParams params) { 76 | return CompletableFuture.completedFuture(null); 77 | } 78 | 79 | @Override 80 | public void telemetryEvent(Object o) { 81 | } 82 | 83 | @Override 84 | public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) { 85 | } 86 | 87 | @Override 88 | public void showMessage(MessageParams messageParams) { 89 | } 90 | 91 | @Override 92 | public CompletableFuture showMessageRequest( 93 | ShowMessageRequestParams showMessageRequestParams) { 94 | return CompletableFuture.completedFuture(null); 95 | } 96 | 97 | @Override 98 | public CompletableFuture showDocument(ShowDocumentParams params) { 99 | return CompletableFuture.completedFuture(null); 100 | } 101 | 102 | @Override 103 | public void logMessage(MessageParams messageParams) { 104 | final String str = messageParams.getType() + ": " + messageParams.getMessage(); 105 | System.out.println(str); 106 | } 107 | 108 | @Override 109 | public CompletableFuture> workspaceFolders() { 110 | return CompletableFuture.completedFuture(worskspaceFolders); 111 | } 112 | 113 | @Override 114 | public CompletableFuture> configuration(ConfigurationParams configurationParams) { 115 | return CompletableFuture.completedFuture(null); 116 | } 117 | 118 | @Override 119 | public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { 120 | return CompletableFuture.completedFuture(null); 121 | } 122 | 123 | @Override 124 | public void notifyProgress(ProgressParams params) { 125 | } 126 | 127 | @Override 128 | public void logTrace(LogTraceParams params) { 129 | } 130 | 131 | @Override 132 | public CompletableFuture refreshSemanticTokens() { 133 | return CompletableFuture.completedFuture(null); 134 | } 135 | 136 | @Override 137 | public CompletableFuture refreshCodeLenses() { 138 | return CompletableFuture.completedFuture(null); 139 | } 140 | 141 | @Test 142 | public void partialValidInputTest_justStmt() { 143 | JimpleLspServer server = new JimpleLspServer(); 144 | final Path workspaceFolder = Paths.get("src/test/resources/partial_invalid_inputs/"); 145 | assertTrue(Files.exists(workspaceFolder)); 146 | 147 | LspClient client = new LspClient(Collections.singletonList(workspaceFolder)); 148 | client.connectTo(server); 149 | 150 | Path path = Paths.get("src/test/resources/partial_invalid_inputs/invalid_juststmt.jimple"); 151 | CompletableFuture semanticTokensCompletableFuture = 152 | server 153 | .getTextDocumentService() 154 | .semanticTokensFull( 155 | new SemanticTokensParams(new TextDocumentIdentifier(Util.pathToUri(path)))); 156 | try { 157 | final SemanticTokens semanticTokens = semanticTokensCompletableFuture.get(); 158 | assertNotNull(semanticTokens); 159 | System.out.println(semanticTokens.getData()); 160 | 161 | } catch (InterruptedException | ExecutionException e) { 162 | e.printStackTrace(); 163 | } 164 | } 165 | 166 | @Test 167 | public void partialValidInputTest_justStmts() { 168 | JimpleLspServer server = new JimpleLspServer(); 169 | final Path workspaceFolder = Paths.get("src/test/resources/partial_invalid_inputs/"); 170 | assertTrue(Files.exists(workspaceFolder)); 171 | 172 | LspClient client = new LspClient(Collections.singletonList(workspaceFolder)); 173 | client.connectTo(server); 174 | 175 | Path path = Paths.get("src/test/resources/partial_invalid_inputs/invalid_juststmts.jimple"); 176 | CompletableFuture semanticTokensCompletableFuture = 177 | server 178 | .getTextDocumentService() 179 | .semanticTokensFull( 180 | new SemanticTokensParams(new TextDocumentIdentifier(Util.pathToUri(path)))); 181 | try { 182 | final SemanticTokens semanticTokens = semanticTokensCompletableFuture.get(); 183 | assertNotNull(semanticTokens); 184 | System.out.println(semanticTokens.getData()); 185 | 186 | } catch (InterruptedException | ExecutionException e) { 187 | e.printStackTrace(); 188 | } 189 | } 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/resolver/SignaturePositionResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.resolver; 2 | 3 | import com.github.swissiety.jimplelsp.Util; 4 | import org.antlr.v4.runtime.ParserRuleContext; 5 | import org.antlr.v4.runtime.Token; 6 | import org.antlr.v4.runtime.tree.ParseTree; 7 | import org.antlr.v4.runtime.tree.ParseTreeWalker; 8 | import org.apache.commons.lang3.tuple.Pair; 9 | import org.eclipse.lsp4j.Location; 10 | import org.eclipse.lsp4j.Range; 11 | import sootup.core.frontend.ResolveException; 12 | import sootup.core.jimple.Jimple; 13 | import sootup.core.model.FullPosition; 14 | import sootup.core.model.Position; 15 | import sootup.core.signatures.FieldSignature; 16 | import sootup.core.signatures.MethodSignature; 17 | import sootup.core.signatures.Signature; 18 | import sootup.core.types.ClassType; 19 | import sootup.core.types.Type; 20 | import sootup.jimple.JimpleBaseListener; 21 | import sootup.jimple.JimpleParser; 22 | import sootup.jimple.parser.JimpleConverterUtil; 23 | 24 | import javax.annotation.Nonnull; 25 | import javax.annotation.Nullable; 26 | import java.nio.file.Path; 27 | import java.util.List; 28 | import java.util.stream.Collectors; 29 | 30 | /** 31 | * This Class organizes information about (open) jimple files. Especially range information about 32 | * occurences of Signature's in the given file. 33 | * 34 | * @author Markus Schmidt 35 | */ 36 | public class SignaturePositionResolver { 37 | @Nonnull 38 | private final SignatureOccurenceAggregator occurences = new SignatureOccurenceAggregator(); 39 | 40 | @Nonnull private final Path path; 41 | @Nonnull private final JimpleConverterUtil util; 42 | 43 | public SignaturePositionResolver(@Nonnull Path path, @Nonnull ParseTree parseTree) { 44 | this.path = path; 45 | util = new JimpleConverterUtil(path); 46 | 47 | ParseTreeWalker walker = new ParseTreeWalker(); 48 | walker.walk(occurences, parseTree); 49 | } 50 | 51 | @Nullable 52 | public Pair resolve(@Nonnull org.eclipse.lsp4j.Position position) { 53 | return occurences.resolve(position); 54 | } 55 | 56 | @Nullable 57 | public List resolve(@Nonnull Signature signature) { 58 | return occurences.resolve(signature).stream() 59 | .map(range -> new Location(Util.pathToUri(path), range)) 60 | .collect(Collectors.toList()); 61 | } 62 | 63 | /** skips e.g. the methods returntype to get the identifier (or class type) */ 64 | @Nullable 65 | public Location findFirstMatchingSignature(Signature signature, Position position) { 66 | final Range firstMatchingSignature = occurences.findFirstMatchingSignature(signature, position); 67 | if (firstMatchingSignature == null) { 68 | return null; 69 | } 70 | 71 | return new Location(Util.pathToUri(path), firstMatchingSignature); 72 | } 73 | 74 | private final class SignatureOccurenceAggregator extends JimpleBaseListener { 75 | 76 | SignatureRangeContainer positionContainer = new SignatureRangeContainer(); 77 | ClassType clazz; 78 | 79 | @Nullable 80 | public Pair resolve(org.eclipse.lsp4j.Position position) { 81 | return positionContainer.resolve(position); 82 | } 83 | 84 | public List resolve(Signature signature) { 85 | return positionContainer.resolve(signature); 86 | } 87 | 88 | public Range findFirstMatchingSignature(Signature signature, Position position) { 89 | return positionContainer.findFirstMatchingSignature(signature, position); 90 | } 91 | 92 | @Nonnull 93 | public Position buildPositionFromToken(@Nonnull Token token) { 94 | // TODO: refactor to SootUp 95 | String tokenstr = token.getText(); 96 | int lineCount = -1; 97 | int fromIdx = 0; 98 | 99 | int lastLineBreakIdx; 100 | for(lastLineBreakIdx = 0; (fromIdx = tokenstr.indexOf("\n", fromIdx)) != -1; ++fromIdx) { 101 | lastLineBreakIdx = fromIdx; 102 | ++lineCount; 103 | } 104 | 105 | int endCharLength = tokenstr.length() - lastLineBreakIdx; 106 | return new FullPosition(token.getLine() - 1, token.getCharPositionInLine(), token.getLine() + lineCount, token.getCharPositionInLine() + endCharLength); 107 | } 108 | 109 | @Override 110 | public void enterFile(JimpleParser.FileContext ctx) { 111 | if (ctx.classname == null) { 112 | throw new ResolveException( 113 | "Identifier for this unit is not found.", 114 | path, 115 | JimpleConverterUtil.buildPositionFromCtx(ctx)); 116 | } 117 | String classname = Jimple.unescape(ctx.classname.getText()); 118 | clazz = util.getClassType(classname); 119 | 120 | positionContainer.add(buildPositionFromToken(ctx.classname), clazz); 121 | 122 | if (ctx.extends_clause() != null) { 123 | ClassType superclass = util.getClassType(ctx.extends_clause().classname.getText()); 124 | positionContainer.add( 125 | JimpleConverterUtil.buildPositionFromCtx(ctx.extends_clause().classname), superclass); 126 | } 127 | 128 | super.enterFile(ctx); 129 | } 130 | 131 | @Override 132 | public void enterMethod(JimpleParser.MethodContext ctx) { 133 | // parsing the declaration 134 | Type type = util.getType(ctx.method_subsignature().type().getText()); 135 | if (type == null) { 136 | throw new ResolveException( 137 | "Returntype not found.", path, JimpleConverterUtil.buildPositionFromCtx(ctx)); 138 | } 139 | String methodname = ctx.method_subsignature().method_name().getText(); 140 | if (methodname == null) { 141 | throw new ResolveException( 142 | "Methodname not found.", path, JimpleConverterUtil.buildPositionFromCtx(ctx)); 143 | } 144 | 145 | List params = util.getTypeList(ctx.method_subsignature().type_list()); 146 | MethodSignature methodSignature = 147 | util.getIdentifierFactory() 148 | .getMethodSignature(clazz, Jimple.unescape(methodname), type, params); 149 | 150 | positionContainer.add( 151 | JimpleConverterUtil.buildPositionFromCtx(ctx.method_subsignature().method_name()), 152 | methodSignature); 153 | 154 | super.enterMethod(ctx); 155 | } 156 | 157 | @Override 158 | public void enterField(JimpleParser.FieldContext ctx) { 159 | String fieldname = ctx.identifier().getText(); 160 | FieldSignature fieldSignature = 161 | util.getIdentifierFactory() 162 | .getFieldSignature( 163 | Jimple.unescape(fieldname), clazz, util.getType(ctx.type().getText())); 164 | positionContainer.add( 165 | JimpleConverterUtil.buildPositionFromCtx(ctx.identifier()), fieldSignature); 166 | super.enterField(ctx); 167 | } 168 | 169 | @Override 170 | public void enterMethod_signature(JimpleParser.Method_signatureContext ctx) { 171 | positionContainer.add( 172 | JimpleConverterUtil.buildPositionFromCtx(ctx.class_name), 173 | util.getClassType(ctx.class_name.getText())); 174 | final JimpleParser.Method_nameContext method_nameCtx = 175 | ctx.method_subsignature().method_name(); 176 | positionContainer.add( 177 | JimpleConverterUtil.buildPositionFromCtx(method_nameCtx), 178 | util.getMethodSignature(ctx, null)); 179 | 180 | super.enterMethod_signature(ctx); 181 | } 182 | 183 | @Override 184 | public void enterField_signature(JimpleParser.Field_signatureContext ctx) { 185 | positionContainer.add( 186 | JimpleConverterUtil.buildPositionFromCtx(ctx.classname), 187 | util.getClassType(ctx.classname.getText())); 188 | positionContainer.add( 189 | JimpleConverterUtil.buildPositionFromCtx(ctx.fieldname), util.getFieldSignature(ctx)); 190 | super.enterField_signature(ctx); 191 | } 192 | 193 | @Override 194 | public void enterConstant(JimpleParser.ConstantContext ctx) { 195 | if (ctx.CLASS() != null) { 196 | // FIXME what is this now: positionContainer.add(JimpleConverterUtil.buildPositionFromCtx(ctx.identifier()),util.getClassType(ctx.identifier().getText())); 197 | } 198 | super.enterConstant(ctx); 199 | } 200 | 201 | @Override 202 | public void enterValue(JimpleParser.ValueContext ctx) { 203 | if (ctx.NEW() != null) { 204 | positionContainer.add( 205 | JimpleConverterUtil.buildPositionFromCtx(ctx.base_type), 206 | util.getClassType(ctx.base_type.getText())); 207 | } 208 | super.enterValue(ctx); 209 | } 210 | 211 | @Override 212 | public void enterImportItem(JimpleParser.ImportItemContext ctx) { 213 | // add information for resolving classes correctly 214 | util.addImport(ctx); 215 | 216 | positionContainer.add( 217 | JimpleConverterUtil.buildPositionFromCtx(ctx.location), 218 | util.getClassType(ctx.location.getText())); 219 | super.enterImportItem(ctx); 220 | } 221 | 222 | @Override 223 | public void enterType(JimpleParser.TypeContext ctx) { 224 | final Type type = util.getType(ctx.getText()); 225 | if (type instanceof ClassType) { 226 | positionContainer.add( 227 | JimpleConverterUtil.buildPositionFromCtx(ctx.identifier()), (Signature) type); 228 | } 229 | super.enterType(ctx); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/resolver/LocalPositionResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.resolver; 2 | 3 | import com.github.swissiety.jimplelsp.Util; 4 | import org.antlr.v4.runtime.tree.ParseTree; 5 | import org.antlr.v4.runtime.tree.ParseTreeWalker; 6 | import org.apache.commons.lang3.tuple.Pair; 7 | import org.eclipse.lsp4j.Location; 8 | import org.eclipse.lsp4j.LocationLink; 9 | import org.eclipse.lsp4j.TextDocumentPositionParams; 10 | import sootup.core.frontend.ResolveException; 11 | import sootup.core.jimple.Jimple; 12 | import sootup.core.model.Position; 13 | import sootup.core.model.SootClass; 14 | import sootup.core.model.SootMethod; 15 | import sootup.core.signatures.MethodSubSignature; 16 | import sootup.core.types.Type; 17 | import sootup.jimple.JimpleBaseListener; 18 | import sootup.jimple.JimpleParser; 19 | import sootup.jimple.parser.JimpleConverterUtil; 20 | 21 | import javax.annotation.Nonnull; 22 | import javax.annotation.Nullable; 23 | import java.nio.file.Path; 24 | import java.util.*; 25 | 26 | /** 27 | * The LocalResolver handles gathering and queriing for Local Positions in a given File. 28 | * 29 | * @author Markus Schmidt 30 | */ 31 | public class LocalPositionResolver { 32 | @Nonnull final Path path; 33 | 34 | @Nonnull 35 | private final Map>> localsOfMethod = 36 | new HashMap<>(); 37 | 38 | @Nonnull private final Map> localToType = new HashMap<>(); 39 | 40 | public LocalPositionResolver(Path path, ParseTree parseTree) { 41 | this.path = path; 42 | 43 | ParseTreeWalker walker = new ParseTreeWalker(); 44 | walker.walk(new LocalDeclarationFinder(path), parseTree); 45 | } 46 | 47 | @Nullable 48 | private List> getLocals(SootClass sc, org.eclipse.lsp4j.Position pos) { 49 | final Optional surroundingMethod = getSootMethodFromPosition(sc, pos); 50 | return surroundingMethod 51 | .map(sootMethod -> localsOfMethod.get(sootMethod.getSignature().getSubSignature())) 52 | .orElse(null); 53 | } 54 | 55 | @Nonnull 56 | private Optional getSootMethodFromPosition( 57 | @Nonnull SootClass sc, @Nonnull org.eclipse.lsp4j.Position pos) { 58 | return sc.getMethods().stream().filter(m -> isInRangeOf(pos, m.getPosition())).findAny(); 59 | } 60 | 61 | private Optional> getSelectedLocalInfo( 62 | @Nonnull List> locals, @Nonnull org.eclipse.lsp4j.Position pos) { 63 | // determine name/range of selected local 64 | return locals.stream().filter(p -> isInRangeOf(pos, p.getLeft())).findAny(); 65 | } 66 | 67 | @Nonnull 68 | public List resolveReferences( 69 | SootClass sc, TextDocumentPositionParams pos) { 70 | List> locals = getLocals(sc, pos.getPosition()); 71 | if (locals == null) { 72 | return Collections.emptyList(); 73 | } 74 | final Optional> localOpt = 75 | getSelectedLocalInfo(locals, pos.getPosition()); 76 | if (!localOpt.isPresent()) { 77 | return Collections.emptyList(); 78 | } 79 | final String localname = localOpt.get().getRight(); 80 | List list = new ArrayList<>(); 81 | for (Pair l : locals) { 82 | if (l.getRight().equals(localname)) { 83 | Location location = Util.positionToLocation(pos.getTextDocument().getUri(), l.getLeft()); 84 | list.add(location); 85 | } 86 | } 87 | return list; 88 | } 89 | 90 | @Nullable 91 | public Type resolveTypeDefinition( 92 | @Nonnull SootClass sc, @Nonnull org.eclipse.lsp4j.Position pos) { 93 | final Optional surroundingMethod = getSootMethodFromPosition(sc, pos); 94 | if (!surroundingMethod.isPresent()) { 95 | return null; 96 | } 97 | final SootMethod sm = surroundingMethod.get(); 98 | List> locals = localsOfMethod.get(sm.getSignature().getSubSignature()); 99 | if (locals == null) { 100 | return null; 101 | } 102 | 103 | final Optional> localOpt = getSelectedLocalInfo(locals, pos); 104 | if (!localOpt.isPresent()) { 105 | return null; 106 | } 107 | final String localname = localOpt.get().getRight(); 108 | 109 | final Map stringTypeMap = localToType.get(sm.getSignature().getSubSignature()); 110 | if (stringTypeMap != null) { 111 | return stringTypeMap.get(localname); 112 | } 113 | return null; 114 | } 115 | 116 | @Nullable 117 | public LocationLink resolveDefinition( 118 | @Nonnull SootClass sc, @Nonnull TextDocumentPositionParams pos) { 119 | List> locals = getLocals(sc, pos.getPosition()); 120 | if (locals == null) { 121 | return null; 122 | } 123 | final Optional> localOpt = 124 | getSelectedLocalInfo(locals, pos.getPosition()); 125 | if (!localOpt.isPresent()) { 126 | return null; 127 | } 128 | Pair positionStringPair = localOpt.get(); 129 | final String localname = positionStringPair.getRight(); 130 | // first occurence of that local (in the current method) is the definition (or declaration if 131 | // existing). 132 | final Optional> deflocalOpt = 133 | locals.stream().filter(p -> p.getRight().equals(localname)).findFirst(); 134 | if (deflocalOpt.isPresent()) { 135 | Location location = 136 | Util.positionToDefLocation(pos.getTextDocument().getUri(), deflocalOpt.get().getLeft()); 137 | location 138 | .getRange() 139 | .setEnd( 140 | new org.eclipse.lsp4j.Position( 141 | deflocalOpt.get().getLeft().getLastLine(), 142 | deflocalOpt.get().getLeft().getFirstCol() 143 | + deflocalOpt.get().getRight().length())); 144 | return new LocationLink( 145 | location.getUri(), 146 | location.getRange(), 147 | location.getRange(), 148 | Util.positionToRange(positionStringPair.getLeft())); 149 | } 150 | return null; 151 | } 152 | 153 | private boolean isInRangeOf(@Nonnull org.eclipse.lsp4j.Position p1, @Nonnull Position p2) { 154 | if (p1.getLine() < p2.getFirstLine()) { 155 | return false; 156 | } else if (p1.getLine() == p2.getFirstLine() && p1.getCharacter() < p2.getFirstCol()) { 157 | return false; 158 | } 159 | 160 | if (p1.getLine() > p2.getLastLine()) { 161 | return false; 162 | } else if (p1.getLine() == p2.getLastLine() && p1.getCharacter() > p2.getLastCol()) { 163 | return false; 164 | } 165 | 166 | return true; 167 | } 168 | 169 | private final class LocalDeclarationFinder extends JimpleBaseListener { 170 | private final Path path; 171 | private final JimpleConverterUtil util; 172 | 173 | private MethodSubSignature currentMethodSig = null; 174 | private List> currentLocalPositionList = null; 175 | private Map currentLocalToType = null; 176 | 177 | private LocalDeclarationFinder(@Nonnull Path path) { 178 | this.path = path; 179 | util = new JimpleConverterUtil(path); 180 | } 181 | 182 | @Override 183 | public void enterMethod(JimpleParser.MethodContext ctx) { 184 | currentMethodSig = util.getMethodSubSignature(ctx.method_subsignature(), ctx); 185 | currentLocalPositionList = new ArrayList<>(); 186 | currentLocalToType = new HashMap<>(); 187 | super.enterMethod(ctx); 188 | } 189 | 190 | @Override 191 | public void exitMethod(JimpleParser.MethodContext ctx) { 192 | localsOfMethod.put(currentMethodSig, currentLocalPositionList); 193 | localToType.put(currentMethodSig, currentLocalToType); 194 | } 195 | 196 | @Override 197 | public void enterDeclaration(JimpleParser.DeclarationContext ctx) { 198 | final JimpleParser.Arg_listContext arg_listCtx = ctx.arg_list(); 199 | if (arg_listCtx == null) { 200 | throw new ResolveException( 201 | "Jimple Syntaxerror: Locals are missing.", 202 | path, 203 | JimpleConverterUtil.buildPositionFromCtx(ctx)); 204 | } 205 | if (ctx.type() == null) { 206 | throw new ResolveException( 207 | "Jimple Syntaxerror: Type missing.", 208 | path, 209 | JimpleConverterUtil.buildPositionFromCtx(ctx)); 210 | } 211 | 212 | final Type type = util.getType(ctx.type().getText()); 213 | for (JimpleParser.ImmediateContext immediateCtx : arg_listCtx.immediate()) { 214 | // remember type 215 | currentLocalToType.put(Jimple.unescape(immediateCtx.local.getText()), type); 216 | } 217 | 218 | super.enterDeclaration(ctx); 219 | } 220 | 221 | @Override 222 | public void enterAssignments(JimpleParser.AssignmentsContext ctx) { 223 | if (ctx.local != null) { 224 | currentLocalPositionList.add( 225 | Pair.of( 226 | JimpleConverterUtil.buildPositionFromCtx(ctx.local), 227 | Jimple.unescape(ctx.local.getText()))); 228 | } 229 | super.enterAssignments(ctx); 230 | } 231 | 232 | @Override 233 | public void enterReference(JimpleParser.ReferenceContext ctx) { 234 | if (ctx.identifier() != null) { 235 | currentLocalPositionList.add( 236 | Pair.of( 237 | JimpleConverterUtil.buildPositionFromCtx(ctx.identifier()), 238 | Jimple.unescape(ctx.identifier().getText()))); 239 | } 240 | super.enterReference(ctx); 241 | } 242 | 243 | @Override 244 | public void enterInvoke_expr(JimpleParser.Invoke_exprContext ctx) { 245 | if (ctx.local_name != null) { 246 | currentLocalPositionList.add( 247 | Pair.of( 248 | JimpleConverterUtil.buildPositionFromCtx(ctx.local_name), 249 | Jimple.unescape(ctx.local_name.getText()))); 250 | } 251 | super.enterInvoke_expr(ctx); 252 | } 253 | 254 | @Override 255 | public void enterImmediate(JimpleParser.ImmediateContext ctx) { 256 | if (ctx.local != null) { 257 | currentLocalPositionList.add( 258 | Pair.of( 259 | JimpleConverterUtil.buildPositionFromCtx(ctx.local), 260 | Jimple.unescape(ctx.local.getText()))); 261 | } 262 | super.enterImmediate(ctx); 263 | } 264 | 265 | @Override 266 | public void enterImportItem(JimpleParser.ImportItemContext ctx) { 267 | util.addImport(ctx); 268 | super.enterImportItem(ctx); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/provider/SyntaxHighlightingProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp.provider; 2 | 3 | import org.antlr.v4.runtime.ParserRuleContext; 4 | import org.antlr.v4.runtime.Token; 5 | import org.antlr.v4.runtime.tree.ParseTree; 6 | import org.eclipse.lsp4j.SemanticTokenTypes; 7 | import org.eclipse.lsp4j.SemanticTokens; 8 | import org.eclipse.lsp4j.SemanticTokensLegend; 9 | import sootup.jimple.JimpleBaseVisitor; 10 | import sootup.jimple.JimpleParser; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.io.IOException; 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | 17 | /** @author Markus Schmidt */ 18 | public class SyntaxHighlightingProvider { 19 | 20 | @Nonnull 21 | static final SemanticTokensLegend legend = 22 | new SemanticTokensLegend( 23 | Arrays.asList( 24 | SemanticTokenTypes.Keyword, 25 | SemanticTokenTypes.Comment, 26 | SemanticTokenTypes.Modifier, 27 | SemanticTokenTypes.Class, 28 | SemanticTokenTypes.Method, 29 | SemanticTokenTypes.Type, 30 | SemanticTokenTypes.Variable, 31 | SemanticTokenTypes.Parameter, 32 | SemanticTokenTypes.String, 33 | SemanticTokenTypes.Number, 34 | SemanticTokenTypes.Operator, 35 | SemanticTokenTypes.Interface), 36 | Collections.emptyList()); 37 | 38 | public static SemanticTokensLegend getLegend() { 39 | return legend; 40 | } 41 | 42 | public static SemanticTokens paintbrush(@Nonnull ParseTree parseTree) throws IOException { 43 | SemanticTokenManager semanticTokenManager = new SemanticTokenManager(legend); 44 | new SyntaxHighlightingVisitor(semanticTokenManager).visit(parseTree); 45 | return new SemanticTokens(semanticTokenManager.getCanvas()); 46 | } 47 | 48 | /** 'Cause colors make the world go round */ 49 | private static class SyntaxHighlightingVisitor extends JimpleBaseVisitor { 50 | 51 | @Nonnull private final SemanticTokenManager semanticTokenManager; 52 | 53 | private SyntaxHighlightingVisitor(@Nonnull SemanticTokenManager semanticTokenManager) { 54 | this.semanticTokenManager = semanticTokenManager; 55 | } 56 | 57 | private void paint(@Nonnull String tokentype, @Nonnull Token token) { 58 | // TODO: add tokenModifier 59 | // zero offset line/column 60 | semanticTokenManager.paintText( 61 | tokentype, 62 | "", 63 | token.getLine() - 1, 64 | token.getCharPositionInLine(), 65 | token.getText().length()); 66 | } 67 | 68 | private void paint(@Nonnull String tokentype, @Nonnull ParserRuleContext ctx) { 69 | // TODO: add tokenModifier 70 | // zero offset line/column 71 | String tokenText = ctx.getText(); 72 | 73 | semanticTokenManager.paintText( 74 | tokentype, 75 | "", 76 | ctx.start.getLine() - 1, 77 | ctx.start.getCharPositionInLine(), 78 | ctx.getText().length()); 79 | } 80 | 81 | @Override 82 | public SemanticTokenManager visitFile(JimpleParser.FileContext ctx) { 83 | ctx.class_modifier().forEach(x -> paint(SemanticTokenTypes.Modifier, x)); 84 | visitFile_type(ctx.file_type()); 85 | if (ctx.file_type().getText().charAt(0) == 'c') { 86 | paint(SemanticTokenTypes.Class, ctx.classname); 87 | } else { 88 | paint(SemanticTokenTypes.Interface, ctx.classname); 89 | } 90 | 91 | JimpleParser.Extends_clauseContext extendsClauseCtx = ctx.extends_clause(); 92 | if (extendsClauseCtx != null) { 93 | visitExtends_clause(extendsClauseCtx); 94 | } 95 | 96 | JimpleParser.Implements_clauseContext implements_clauseContext = ctx.implements_clause(); 97 | if (implements_clauseContext != null) { 98 | visitImplements_clause(implements_clauseContext); 99 | } 100 | ctx.member().forEach(this::visitMember); 101 | return semanticTokenManager; 102 | } 103 | 104 | @Override 105 | public SemanticTokenManager visitFile_type(JimpleParser.File_typeContext ctx) { 106 | paint(SemanticTokenTypes.Keyword, ctx); 107 | return semanticTokenManager; 108 | } 109 | 110 | @Override 111 | public SemanticTokenManager visitImportItem(JimpleParser.ImportItemContext ctx) { 112 | paint(SemanticTokenTypes.Keyword, ctx.start); 113 | return semanticTokenManager; 114 | } 115 | 116 | @Override 117 | public SemanticTokenManager visitExtends_clause(JimpleParser.Extends_clauseContext ctx) { 118 | paint(SemanticTokenTypes.Keyword, ctx.start); 119 | paint(SemanticTokenTypes.Type, ctx.identifier()); 120 | return semanticTokenManager; 121 | } 122 | 123 | @Override 124 | public SemanticTokenManager visitImplements_clause(JimpleParser.Implements_clauseContext ctx) { 125 | paint(SemanticTokenTypes.Keyword, ctx.start); 126 | JimpleParser.Type_listContext typeList = ctx.type_list(); 127 | if (typeList != null) { 128 | visitType_list(typeList); 129 | } 130 | return semanticTokenManager; 131 | } 132 | 133 | @Override 134 | public SemanticTokenManager visitField(JimpleParser.FieldContext ctx) { 135 | ctx.field_modifier().forEach( modctx -> { 136 | paint(SemanticTokenTypes.Modifier, modctx); 137 | }); 138 | paint(SemanticTokenTypes.Type, ctx.type()); 139 | paint(SemanticTokenTypes.Variable, ctx.identifier()); 140 | return semanticTokenManager; 141 | } 142 | 143 | @Override 144 | public SemanticTokenManager visitMethod(JimpleParser.MethodContext ctx) { 145 | ctx.method_modifier().forEach( modctx -> { 146 | paint(SemanticTokenTypes.Modifier, modctx); 147 | }); 148 | visitMethod_subsignature(ctx.method_subsignature()); 149 | JimpleParser.Throws_clauseContext throws_clauseContext = ctx.throws_clause(); 150 | if (throws_clauseContext != null) { 151 | paint(SemanticTokenTypes.Keyword, throws_clauseContext.start); // fix .g4 to use THROWS 152 | visitType_list(throws_clauseContext.type_list()); 153 | } 154 | visitMethod_body(ctx.method_body()); 155 | return semanticTokenManager; 156 | } 157 | // paint(SemanticTokenTypes.Modifier, ctx); 158 | 159 | @Override 160 | public SemanticTokenManager visitTrap_clause(JimpleParser.Trap_clauseContext ctx) { 161 | paint(SemanticTokenTypes.Keyword, ctx.CATCH().getSymbol()); 162 | paint(SemanticTokenTypes.Type, ctx.exceptiontype); 163 | paint(SemanticTokenTypes.Keyword, ctx.FROM().getSymbol()); 164 | paint(SemanticTokenTypes.Keyword, ctx.TO().getSymbol()); 165 | paint(SemanticTokenTypes.Keyword, ctx.WITH().getSymbol()); 166 | 167 | return semanticTokenManager; 168 | } 169 | 170 | @Override 171 | public SemanticTokenManager visitType(JimpleParser.TypeContext ctx) { 172 | paint(SemanticTokenTypes.Type, ctx.start); 173 | return semanticTokenManager; 174 | } 175 | 176 | @Override 177 | public SemanticTokenManager visitMethod_signature(JimpleParser.Method_signatureContext ctx) { 178 | // paint(SemanticTokenTypes.Variable, ctx.identifier()); 179 | paint(SemanticTokenTypes.Type, ctx.class_name); 180 | visitMethod_subsignature(ctx.method_subsignature()); 181 | return semanticTokenManager; 182 | } 183 | 184 | @Override 185 | public SemanticTokenManager visitMethod_subsignature( 186 | JimpleParser.Method_subsignatureContext ctx) { 187 | visitType(ctx.type()); 188 | paint(SemanticTokenTypes.Method, ctx.method_name()); 189 | if (ctx.type_list() != null) { 190 | visitType_list(ctx.type_list()); 191 | } 192 | return semanticTokenManager; 193 | } 194 | 195 | @Override 196 | public SemanticTokenManager visitField_signature(JimpleParser.Field_signatureContext ctx) { 197 | paint(SemanticTokenTypes.Type, ctx.classname); 198 | paint(SemanticTokenTypes.Type, ctx.type()); 199 | paint(SemanticTokenTypes.Variable, ctx.fieldname); 200 | return semanticTokenManager; 201 | } 202 | 203 | @Override 204 | public SemanticTokenManager visitReference(JimpleParser.ReferenceContext ctx) { 205 | JimpleParser.IdentifierContext identifier = ctx.identifier(); 206 | if (identifier != null) { 207 | paint(SemanticTokenTypes.Variable, identifier); 208 | final JimpleParser.Array_descriptorContext arrayDescrCtx = ctx.array_descriptor(); 209 | if (arrayDescrCtx != null) { 210 | visitArray_descriptor(arrayDescrCtx); 211 | } 212 | } 213 | 214 | final JimpleParser.Field_signatureContext ctx1 = ctx.field_signature(); 215 | if (ctx1 != null) { 216 | visitField_signature(ctx1); 217 | } 218 | 219 | return semanticTokenManager; 220 | } 221 | 222 | @Override 223 | public SemanticTokenManager visitImmediate(JimpleParser.ImmediateContext ctx) { 224 | if (ctx.local != null) { 225 | paint(SemanticTokenTypes.Variable, ctx); 226 | } else { 227 | visitConstant(ctx.constant()); 228 | } 229 | return semanticTokenManager; 230 | } 231 | 232 | @Override 233 | public SemanticTokenManager visitConstant(JimpleParser.ConstantContext ctx) { 234 | String text = ctx.getText(); 235 | if (text.charAt(0) == '"' || text.charAt(0) == '\'') { 236 | paint(SemanticTokenTypes.String, ctx); 237 | } else if (ctx.CLASS() != null) { 238 | paint(SemanticTokenTypes.Keyword, ctx.CLASS().getSymbol()); 239 | // FIXME: what is this now? paint(SemanticTokenTypes.Type, ctx.identifier()); 240 | } else { 241 | // number, boolean, .. 242 | paint(SemanticTokenTypes.Number, ctx); 243 | } 244 | return semanticTokenManager; 245 | } 246 | 247 | @Override 248 | public SemanticTokenManager visitStmt(JimpleParser.StmtContext ctx) { 249 | if (ctx.IF() != null 250 | || ctx.RETURN() != null 251 | || ctx.ENTERMONITOR() != null 252 | || ctx.EXITMONITOR() != null 253 | || ctx.BREAKPOINT() != null 254 | || ctx.THROW() != null 255 | || ctx.NOP() != null) { 256 | paint(SemanticTokenTypes.Keyword, ctx.start); 257 | if (ctx.immediate() != null) { 258 | visitImmediate(ctx.immediate()); 259 | } 260 | } else if (ctx.assignments() != null) { 261 | visitAssignments(ctx.assignments()); 262 | } else if (ctx.goto_stmt() != null) { 263 | visitGoto_stmt(ctx.goto_stmt()); 264 | } else if (ctx.SWITCH() != null) { 265 | paint(SemanticTokenTypes.Keyword, ctx.start); 266 | ctx.case_stmt().forEach(this::visitCase_stmt); 267 | } 268 | 269 | return semanticTokenManager; 270 | } 271 | 272 | @Override 273 | public SemanticTokenManager visitCase_stmt(JimpleParser.Case_stmtContext ctx) { 274 | paint(SemanticTokenTypes.Keyword, ctx.start); 275 | return semanticTokenManager; 276 | } 277 | 278 | @Override 279 | public SemanticTokenManager visitCase_label(JimpleParser.Case_labelContext ctx) { 280 | paint(SemanticTokenTypes.Keyword, ctx.start); 281 | return semanticTokenManager; 282 | } 283 | 284 | @Override 285 | public SemanticTokenManager visitDeclaration(JimpleParser.DeclarationContext ctx) { 286 | paint(SemanticTokenTypes.Type, ctx.type()); 287 | visitArg_list(ctx.arg_list()); 288 | return semanticTokenManager; 289 | } 290 | 291 | @Override 292 | public SemanticTokenManager visitAssignments(JimpleParser.AssignmentsContext ctx) { 293 | JimpleParser.Identity_refContext identity_refContext = ctx.identity_ref(); 294 | if (identity_refContext != null) { 295 | paint(SemanticTokenTypes.Variable, ctx.identifier()); 296 | paint(SemanticTokenTypes.Keyword, identity_refContext.start); 297 | if (identity_refContext.DEC_CONSTANT() != null) { 298 | paint(SemanticTokenTypes.Keyword, identity_refContext.DEC_CONSTANT().getSymbol()); 299 | } 300 | } else { 301 | 302 | if (ctx.reference() != null) { 303 | visitReference(ctx.reference()); 304 | } else { 305 | paint(SemanticTokenTypes.Variable, ctx.identifier()); 306 | } 307 | visitValue(ctx.value()); 308 | } 309 | return semanticTokenManager; 310 | } 311 | 312 | @Override 313 | public SemanticTokenManager visitGoto_stmt(JimpleParser.Goto_stmtContext ctx) { 314 | paint(SemanticTokenTypes.Keyword, ctx.start); 315 | return semanticTokenManager; 316 | } 317 | 318 | @Override 319 | public SemanticTokenManager visitArg_list(JimpleParser.Arg_listContext ctx) { 320 | for (JimpleParser.ImmediateContext immediateContext : ctx.immediate()) { 321 | paint(SemanticTokenTypes.Variable, immediateContext); 322 | } 323 | return semanticTokenManager; 324 | } 325 | 326 | @Override 327 | public SemanticTokenManager visitInvoke_expr(JimpleParser.Invoke_exprContext ctx) { 328 | paint(SemanticTokenTypes.Keyword, ctx.start); 329 | 330 | if (ctx.identifier() != null) { 331 | paint(SemanticTokenTypes.Variable, ctx.identifier()); 332 | } 333 | 334 | if (ctx.DYNAMICINVOKE() != null) { 335 | 336 | if (ctx.type() != null) { 337 | visitType(ctx.type()); 338 | } 339 | if (ctx.type_list() != null) { 340 | visitType_list(ctx.type_list()); 341 | } 342 | if (ctx.dyn_args != null) { 343 | ctx.dyn_args.immediate().forEach(this::visitImmediate); 344 | } 345 | 346 | visitMethod_signature(ctx.method_signature()); 347 | 348 | if (ctx.staticargs != null) { 349 | ctx.staticargs.immediate().forEach(this::visitImmediate); 350 | } 351 | } else { 352 | visitMethod_signature(ctx.method_signature()); 353 | 354 | if (ctx.arg_list() != null && !ctx.arg_list().isEmpty()) { 355 | visitArg_list(ctx.arg_list(0)); 356 | } 357 | } 358 | 359 | return semanticTokenManager; 360 | } 361 | 362 | @Override 363 | public SemanticTokenManager visitValue(JimpleParser.ValueContext ctx) { 364 | if (ctx.NEW() != null) { 365 | paint(SemanticTokenTypes.Keyword, ctx.NEW().getSymbol()); 366 | paint(SemanticTokenTypes.Type, ctx.identifier()); 367 | } else if (ctx.NEWARRAY() != null) { 368 | paint(SemanticTokenTypes.Keyword, ctx.NEWARRAY().getSymbol()); 369 | paint(SemanticTokenTypes.Type, ctx.type()); 370 | } else if (ctx.NEWMULTIARRAY() != null) { 371 | paint(SemanticTokenTypes.Keyword, ctx.NEWMULTIARRAY().getSymbol()); 372 | paint(SemanticTokenTypes.Type, ctx.type()); 373 | } else if (ctx.INSTANCEOF() != null) { 374 | paint(SemanticTokenTypes.Variable, ctx.op); 375 | paint(SemanticTokenTypes.Keyword, ctx.INSTANCEOF().getSymbol()); 376 | paint(SemanticTokenTypes.Type, ctx.type()); 377 | } else { 378 | visitChildren(ctx); 379 | } 380 | return semanticTokenManager; 381 | } 382 | 383 | @Override 384 | public SemanticTokenManager visitBinop(JimpleParser.BinopContext ctx) { 385 | paint(SemanticTokenTypes.Operator, ctx); 386 | return semanticTokenManager; 387 | } 388 | 389 | @Override 390 | public SemanticTokenManager visitUnop(JimpleParser.UnopContext ctx) { 391 | paint(SemanticTokenTypes.Operator, ctx); 392 | return semanticTokenManager; 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/JimpleLspServer.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import com.github.swissiety.jimplelsp.provider.SyntaxHighlightingProvider; 4 | import com.google.gson.JsonObject; 5 | import org.antlr.v4.runtime.CharStream; 6 | import org.antlr.v4.runtime.CharStreams; 7 | import org.eclipse.lsp4j.*; 8 | import org.eclipse.lsp4j.services.LanguageClient; 9 | import org.eclipse.lsp4j.services.LanguageServer; 10 | import org.eclipse.lsp4j.services.TextDocumentService; 11 | import org.eclipse.lsp4j.services.WorkspaceService; 12 | import sootup.core.cache.provider.MutableFullCacheProvider; 13 | import sootup.core.frontend.ResolveException; 14 | import sootup.core.frontend.SootClassSource; 15 | import sootup.core.inputlocation.EagerInputLocation; 16 | import sootup.core.model.SootClass; 17 | import sootup.core.model.SourceType; 18 | import sootup.core.types.ClassType; 19 | import sootup.jimple.parser.JimpleConverter; 20 | import sootup.jimple.parser.JimpleView; 21 | 22 | import javax.annotation.Nonnull; 23 | import javax.annotation.Nullable; 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.io.PrintStream; 28 | import java.nio.file.Files; 29 | import java.nio.file.Path; 30 | import java.nio.file.Paths; 31 | import java.util.*; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.stream.Collectors; 36 | import java.util.stream.Stream; 37 | 38 | /** @author Markus Schmidt */ 39 | public class JimpleLspServer implements LanguageServer { 40 | 41 | private final JimpleTextDocumentService textDocumentService; 42 | private final WorkspaceService workspaceService; 43 | LanguageClient client = null; 44 | private ClientCapabilities clientCapabilities; 45 | 46 | @Nonnull 47 | private final Map>> textDocumentClassMapping = 48 | new HashMap<>(); 49 | 50 | private JimpleView view; 51 | private boolean isViewDirty = true; 52 | 53 | // config values 54 | private String sootpath = ""; 55 | private String androidplatform = ""; 56 | 57 | public JimpleLspServer() { 58 | this.textDocumentService = new JimpleTextDocumentService(this); 59 | this.workspaceService = new JimpleWorkspaceService(this); 60 | } 61 | 62 | @Nonnull 63 | public CompletableFuture pool(Callable lambda) { 64 | return CompletableFuture.supplyAsync( 65 | () -> { 66 | try { 67 | return lambda.call(); 68 | } catch (Throwable e) { 69 | client.logMessage(new MessageParams(MessageType.Error, getStringFrom(e))); 70 | } 71 | return null; 72 | }); 73 | } 74 | 75 | static String getStringFrom(Throwable e) { 76 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 77 | final PrintStream printStream = new PrintStream(bos); 78 | e.printStackTrace(printStream); 79 | printStream.flush(); 80 | return bos.toString(); 81 | } 82 | 83 | @Nonnull 84 | ClientCapabilities getClientCapabilities() { 85 | return clientCapabilities; 86 | } 87 | 88 | @Nonnull 89 | public JimpleView getView() { 90 | if (isViewDirty) { 91 | view = recreateView(); 92 | isViewDirty = false; 93 | } 94 | return view; 95 | } 96 | 97 | @Nonnull 98 | private JimpleView recreateView() { 99 | HashMap map = new HashMap<>(); 100 | textDocumentClassMapping.forEach((key, value) -> map.put(value.getClassType(), value)); 101 | JimpleView jimpleView = new JimpleView(Collections.singletonList(new EagerInputLocation()), new MutableFullCacheProvider<>()); 102 | 103 | // FIXME: set list of modified jimple files 104 | 105 | return jimpleView; 106 | } 107 | 108 | public boolean quarantineInputOrUpdate(@Nonnull String uri) throws ResolveException, IOException { 109 | return quarantineInputOrUpdate(uri, CharStreams.fromPath(Util.uriToPath(uri))); 110 | } 111 | 112 | public boolean quarantineInputOrUpdate(@Nonnull String uri, String content) 113 | throws ResolveException { 114 | return quarantineInputOrUpdate(uri, CharStreams.fromString(content)); 115 | } 116 | 117 | private boolean quarantineInputOrUpdate(@Nonnull String uri, @Nonnull CharStream charStream) 118 | throws ResolveException { 119 | 120 | final JimpleConverter jimpleConverter = new JimpleConverter(); 121 | try { 122 | 123 | final Path path = Util.uriToPath(uri); 124 | if (!Files.exists(path)) { 125 | return false; 126 | } 127 | SootClassSource> scs = 128 | jimpleConverter.run(charStream, new EagerInputLocation<>(), path); 129 | // input is clean 130 | final SootClassSource> overriden = 131 | textDocumentClassMapping.put(path, scs); 132 | if (overriden != null) { 133 | // possible optimization: compare if classes are still equal -> set dirty bit only when 134 | // necessary 135 | } 136 | isViewDirty = true; 137 | 138 | // clean up errors in IDE if the file is valid (again) 139 | // FIXME: merge with other diagnostics in magpie 140 | client.publishDiagnostics(new PublishDiagnosticsParams(uri, Collections.emptyList())); 141 | 142 | return true; 143 | } catch (ResolveException e) { 144 | // feed error into diagnostics 145 | final Diagnostic d = 146 | new Diagnostic( 147 | Util.positionToDefRange(e.getRange()), 148 | e.getMessage(), 149 | DiagnosticSeverity.Error, 150 | "JimpleParser"); 151 | client.publishDiagnostics(new PublishDiagnosticsParams(uri, Collections.singletonList(d))); 152 | return false; 153 | } catch (Exception e) { 154 | // feed error into diagnostics 155 | String stackStraceString = getStringFrom(e); 156 | 157 | final Diagnostic d = 158 | new Diagnostic( 159 | new Range(new Position(0, 0), new Position(0, Integer.MAX_VALUE)), 160 | stackStraceString, 161 | DiagnosticSeverity.Error, 162 | "JimpleParser"); 163 | // FIXME: merge with other diagnostics in magpie 164 | client.publishDiagnostics(new PublishDiagnosticsParams(uri, Collections.singletonList(d))); 165 | return false; 166 | } 167 | } 168 | 169 | @Override 170 | public void exit() { 171 | 172 | } 173 | 174 | @Override 175 | public TextDocumentService getTextDocumentService() { 176 | return null; 177 | } 178 | 179 | @Override 180 | public WorkspaceService getWorkspaceService() { 181 | return null; 182 | } 183 | 184 | List workspaceFolders = Collections.emptyList(); 185 | 186 | private boolean jimpleFound = false; 187 | 188 | @Override 189 | public CompletableFuture initialize(InitializeParams params) { 190 | if (params.getWorkspaceFolders() != null) { 191 | workspaceFolders = params.getWorkspaceFolders(); 192 | } 193 | 194 | final InitializeResult initialize = new InitializeResult(); 195 | 196 | final ServerCapabilities capabilities = initialize.getCapabilities(); 197 | capabilities.setWorkspaceSymbolProvider(true); 198 | capabilities.setDocumentSymbolProvider(true); 199 | 200 | capabilities.setImplementationProvider(true); 201 | capabilities.setDefinitionProvider(true); 202 | capabilities.setReferencesProvider(true); 203 | capabilities.setHoverProvider(true); 204 | 205 | capabilities.setTypeDefinitionProvider(true); 206 | capabilities.setFoldingRangeProvider(false); 207 | capabilities.setDocumentHighlightProvider(true); 208 | 209 | // we could announce it even if the client does not support it.. 210 | if (params.getCapabilities().getTextDocument() != null) { 211 | // semantic token config 212 | if (params.getCapabilities().getTextDocument().getSemanticTokens() != null) { 213 | capabilities.setSemanticTokensProvider( 214 | new SemanticTokensWithRegistrationOptions( 215 | SyntaxHighlightingProvider.getLegend(), true)); 216 | } 217 | } 218 | // TODO: check capabilities.setDocumentFormattingProvider(true); 219 | 220 | long startNanos = System.nanoTime(); 221 | pool( 222 | () -> { 223 | List rootpaths = new ArrayList<>(workspaceFolders.size() + 1); 224 | 225 | workspaceFolders.forEach( 226 | f -> { 227 | final Path path = Util.uriToPath(f.getUri()).toAbsolutePath(); 228 | if (rootpaths.stream() 229 | .noneMatch(existingPath -> path.startsWith(existingPath.toAbsolutePath()))) { 230 | // add workspace folder if its not a subdirectory of an already existing path 231 | rootpaths.add(path); 232 | } 233 | }); 234 | 235 | jimpleFound = scanDirectoryForJimple(rootpaths); 236 | 237 | double runtimeMs = (System.nanoTime() - startNanos) / 1e6; 238 | client.logMessage( 239 | new MessageParams(MessageType.Log, "Workspace indexing took " + runtimeMs + " ms")); 240 | return null; 241 | }); 242 | 243 | return CompletableFuture.completedFuture(initialize); 244 | } 245 | 246 | @Override 247 | public void initialized(InitializedParams params) { 248 | 249 | if (!jimpleFound) { 250 | extractFromAPKJAR( 251 | workspaceFolders.stream() 252 | .map(w -> Util.uriToPath(w.getUri())) 253 | .collect(Collectors.toList())); 254 | } else { 255 | indexJimple(textDocumentClassMapping.keySet()); 256 | } 257 | } 258 | 259 | @Override 260 | public CompletableFuture shutdown() { 261 | return null; 262 | } 263 | 264 | private void extractFromAPKJAR(List rootpaths) { 265 | // find apk in top levels/first level subdir 266 | if (clientCapabilities.getWorkspace().getConfiguration() == Boolean.TRUE) { 267 | 268 | List apkJarFiles = new ArrayList<>(); 269 | // get ANDROIDHOME config from client 270 | final ConfigurationItem configurationItem = new ConfigurationItem(); 271 | configurationItem.setSection("JimpleLSP.jimpleextraction"); 272 | final CompletableFuture> configuration = 273 | client.configuration( 274 | new ConfigurationParams(Collections.singletonList(configurationItem))); 275 | 276 | final List configItems; 277 | try { 278 | configItems = configuration.get(); 279 | if (configItems.isEmpty()) { 280 | return; 281 | } 282 | } catch (InterruptedException | ExecutionException e) { 283 | e.printStackTrace(); 284 | return; 285 | } 286 | 287 | /* TODO: if not overriden in configuration try to get info frome $ANDROID_HOME global variable if set 288 | final String androidhomeEnv = System.getenv("ANDROID_HOME"); 289 | // exists? 290 | androidhomeEnv += "/"; 291 | // exists folder? 292 | */ 293 | 294 | final JsonObject o = (JsonObject) configItems.get(0); 295 | androidplatform = o.get("androidplatforms").getAsString(); 296 | sootpath = o.get("sootpath").getAsString(); 297 | 298 | for (Path rootpath : rootpaths) { 299 | // find apk 300 | try (Stream paths = Files.walk(rootpath)) { 301 | paths 302 | .filter( 303 | f -> { 304 | final String filename = f.toString().toLowerCase(); 305 | return filename.endsWith(".apk") || filename.endsWith(".jar"); 306 | }) 307 | .forEach(apkJarFiles::add); 308 | } catch (IOException e) { 309 | e.printStackTrace(); 310 | } 311 | } 312 | 313 | if (apkJarFiles.size() > 0) { 314 | 315 | final MessageActionItem extract_all = new MessageActionItem("Extract all"); 316 | // single / multiple containers found 317 | final ShowMessageRequestParams messageRequestParams = new ShowMessageRequestParams(); 318 | 319 | StringBuilder sb = new StringBuilder(); 320 | List actions = new ArrayList<>(); 321 | if (apkJarFiles.size() > 1) { 322 | sb.append( 323 | "JimpleLSP found multiple APKs/Jars. Do you like to extract Jimple from them?\n"); 324 | actions.add(extract_all); 325 | 326 | for (Path apkJarFile : apkJarFiles) { 327 | sb.append("- ").append(apkJarFile.toString()).append("\n"); 328 | actions.add(new MessageActionItem(apkJarFile.toString())); 329 | } 330 | 331 | actions.add(new MessageActionItem("Cancel")); 332 | 333 | } else { 334 | final Path apkJarFile = apkJarFiles.get(0); 335 | sb.append("Do you wish to extract Jimple from the following file?"); 336 | actions.add(new MessageActionItem(apkJarFile.toString())); 337 | actions.add(new MessageActionItem("No")); 338 | } 339 | 340 | messageRequestParams.setMessage(sb.toString()); 341 | messageRequestParams.setActions(actions); 342 | messageRequestParams.setType(MessageType.Info); 343 | 344 | final CompletableFuture requestFutur = 345 | client.showMessageRequest(messageRequestParams); 346 | 347 | requestFutur.thenApplyAsync( 348 | (MessageActionItem messageActionItem) -> { 349 | if (messageActionItem.equals(extract_all)) { 350 | for (Path apkJarFile : apkJarFiles) { 351 | 352 | final String absoluteFilename = apkJarFile.toAbsolutePath().toString(); 353 | File outputdir = 354 | new File(absoluteFilename.substring(0, absoluteFilename.length() - 4)); 355 | 356 | if (!extractAPKJAR(apkJarFile, outputdir)) { 357 | client.showMessage( 358 | new MessageParams(MessageType.Info, "Extraction of Jimple aborted.")); 359 | break; 360 | } 361 | scanDirectoryForJimple(Collections.singletonList(outputdir.toPath())); 362 | } 363 | client.showMessage( 364 | new MessageParams( 365 | MessageType.Warning, 366 | "You extracted multiple .apk or .jar files into the same workspace. If there are identical fully qualified classnames across these files and hence in the workspace JimpleLSP will have problems resolving the intended class.")); 367 | } else { 368 | // FIXME: security! check if answer is from proposed list 369 | Path target = Paths.get(messageActionItem.getTitle()); 370 | 371 | final String absoluteFilename = target.toAbsolutePath().toString(); 372 | File outputdir = 373 | new File(absoluteFilename.substring(0, absoluteFilename.length() - 4)); 374 | extractAPKJAR(target, outputdir); 375 | 376 | scanDirectoryForJimple(Collections.singletonList(outputdir.toPath())); 377 | } 378 | 379 | return messageActionItem; 380 | }); 381 | } 382 | } 383 | } 384 | 385 | private boolean extractAPKJAR(Path target, File outputdir) { 386 | try { 387 | 388 | if (!Files.exists(Paths.get(sootpath))) { 389 | client.showMessage( 390 | new MessageParams( 391 | MessageType.Error, 392 | "Configured path to the soot executable \"" + sootpath + "\" does not exist.")); 393 | return false; 394 | } 395 | 396 | // dont overwrite 397 | if (outputdir.exists()) { 398 | client.showMessage( 399 | new MessageParams( 400 | MessageType.Error, "Output Directory " + outputdir + " exists already.")); 401 | return false; 402 | } 403 | if (!outputdir.mkdir()) { 404 | client.showMessage( 405 | new MessageParams( 406 | MessageType.Error, 407 | "Can not create directory \"" + outputdir + "\" for extracted files.")); 408 | return false; 409 | } 410 | 411 | String[] options; 412 | if (target.toString().toLowerCase().endsWith("apk")) { 413 | 414 | if (androidplatform.isEmpty()) { 415 | client.showMessage( 416 | new MessageParams( 417 | MessageType.Error, "The Configuration for androidplatform is empty.")); 418 | return false; 419 | } 420 | 421 | if (!Files.exists(Paths.get(androidplatform))) { 422 | client.showMessage( 423 | new MessageParams( 424 | MessageType.Error, 425 | "Configured androidplatform path \"" + androidplatform + "\" does not exist.")); 426 | return false; 427 | } 428 | 429 | // soots arguments for an APK 430 | options = 431 | new String[] { 432 | "-process-dir", 433 | target.toString(), 434 | "-pp", 435 | "-src-prec", 436 | "apk", 437 | "-android-jars", 438 | androidplatform, 439 | "-allow-phantom-refs", 440 | "-d", 441 | outputdir.toString(), 442 | "-output-format", 443 | "J" 444 | }; 445 | 446 | } else { 447 | // soots arguments for a JAR 448 | options = 449 | new String[] { 450 | "-process-dir", 451 | target.toString(), 452 | "-pp", 453 | "-src-prec", 454 | "c", 455 | "-allow-phantom-refs", 456 | "-d", 457 | outputdir.toString(), 458 | "-output-format", 459 | "J" 460 | }; 461 | } 462 | 463 | // Main.v().run(options); 464 | 465 | Runtime rt = Runtime.getRuntime(); 466 | Process pr = 467 | rt.exec( 468 | "java -jar " + sootpath + " soot.Main " + String.join(" ", Arrays.asList(options))); 469 | 470 | final int ret = pr.waitFor(); 471 | if (ret == 0) { 472 | client.showMessage( 473 | new MessageParams(MessageType.Info, "Jimple extracted to \"" + outputdir + "\".")); 474 | } else { 475 | client.showMessage(new MessageParams(MessageType.Error, pr.getErrorStream().toString())); 476 | } 477 | 478 | } catch (Exception e) { 479 | 480 | client.showMessage(new MessageParams(MessageType.Error, getStringFrom(e))); 481 | e.printStackTrace(); 482 | return false; 483 | } 484 | return true; 485 | } 486 | 487 | private boolean scanDirectoryForJimple(Iterable rootpaths) { 488 | // scan workspace all jimple files <-> classes 489 | List jimpleFiles = new ArrayList<>(); 490 | 491 | // scan all workspaces in depth for jimple files 492 | for (Path rootpath : rootpaths) { 493 | // jimple 494 | try (Stream paths = Files.walk(rootpath)) { 495 | paths.filter(f -> f.toString().toLowerCase().endsWith(".jimple")).forEach(jimpleFiles::add); 496 | } catch (IOException e) { 497 | e.printStackTrace(); 498 | } 499 | } 500 | 501 | for (Path jimpleFile : jimpleFiles) { 502 | textDocumentClassMapping.put(jimpleFile, null); 503 | } 504 | 505 | return !jimpleFiles.isEmpty(); 506 | } 507 | 508 | private void indexJimple(Collection jimpleFiles) { 509 | for (Path jimpleFile : jimpleFiles) { 510 | try { 511 | final String uri = Util.pathToUri(jimpleFile); 512 | quarantineInputOrUpdate(uri); 513 | } catch (IOException exception) { 514 | exception.printStackTrace(); 515 | } 516 | } 517 | } 518 | 519 | @Nullable 520 | public ClassType uriToClasstype(@Nonnull String strUri) { 521 | final SootClassSource sootClassSource = textDocumentClassMapping.get(Util.uriToPath(strUri)); 522 | if (sootClassSource == null) { 523 | return null; 524 | } 525 | return sootClassSource.getClassType(); 526 | } 527 | 528 | public void connectClient(LanguageClient remoteProxy) { 529 | client = remoteProxy; 530 | } 531 | 532 | public LanguageClient getClient() { 533 | return client; 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/main/java/com/github/swissiety/jimplelsp/JimpleTextDocumentService.java: -------------------------------------------------------------------------------- 1 | package com.github.swissiety.jimplelsp; 2 | 3 | import com.github.swissiety.jimplelsp.provider.JimpleSymbolProvider; 4 | import com.github.swissiety.jimplelsp.provider.SyntaxHighlightingProvider; 5 | import com.github.swissiety.jimplelsp.resolver.LocalPositionResolver; 6 | import com.github.swissiety.jimplelsp.resolver.SignaturePositionResolver; 7 | import com.github.swissiety.jimplelsp.workingtree.WorkingTree; 8 | import org.antlr.v4.runtime.CharStreams; 9 | import org.antlr.v4.runtime.ParserRuleContext; 10 | import org.antlr.v4.runtime.tree.ParseTree; 11 | import org.apache.commons.lang3.tuple.Pair; 12 | import org.eclipse.lsp4j.*; 13 | import org.eclipse.lsp4j.Position; 14 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 15 | import org.eclipse.lsp4j.services.TextDocumentService; 16 | import sootup.core.model.*; 17 | import sootup.core.signatures.FieldSignature; 18 | import sootup.core.signatures.MethodSignature; 19 | import sootup.core.signatures.Signature; 20 | import sootup.core.typehierarchy.ViewTypeHierarchy; 21 | import sootup.core.types.ClassType; 22 | import sootup.core.types.Type; 23 | import sootup.core.util.printer.JimplePrinter; 24 | import sootup.core.views.View; 25 | import sootup.jimple.JimpleParser; 26 | import sootup.jimple.parser.JimpleConverterUtil; 27 | import sootup.jimple.parser.JimpleView; 28 | 29 | import javax.annotation.Nonnull; 30 | import javax.annotation.Nullable; 31 | import java.io.IOException; 32 | import java.io.PrintWriter; 33 | import java.io.StringWriter; 34 | import java.net.URI; 35 | import java.nio.file.Files; 36 | import java.nio.file.Path; 37 | import java.util.*; 38 | import java.util.concurrent.CompletableFuture; 39 | import java.util.function.Function; 40 | import java.util.stream.Collectors; 41 | 42 | /** @author Markus Schmidt */ 43 | public class JimpleTextDocumentService implements TextDocumentService { 44 | 45 | WorkingTree workingTree = new WorkingTree("jimple"); 46 | 47 | private final Map docSignaturePositionResolver = new HashMap<>(); 48 | 49 | private final Map docParseTree = new HashMap<>(); 50 | @Nonnull 51 | private final JimpleLspServer server; 52 | 53 | /** 54 | * Instantiates a new magpie text document service. 55 | * 56 | * @param server the server 57 | */ 58 | public JimpleTextDocumentService(@Nonnull JimpleLspServer server) { 59 | this.server = server; 60 | } 61 | 62 | @Nonnull 63 | JimpleLspServer getServer() { 64 | return server; 65 | } 66 | 67 | /** TODO: refactor into magpiebridge */ 68 | protected void forwardException(@Nonnull Exception e) { 69 | getServer() 70 | .getClient() 71 | .logMessage(new MessageParams(MessageType.Error, JimpleLspServer.getStringFrom(e))); 72 | } 73 | 74 | @Override 75 | public void didOpen(DidOpenTextDocumentParams params) { 76 | if (params == null || params.getTextDocument() == null) { 77 | return; 78 | } 79 | final String uri = params.getTextDocument().getUri(); 80 | if (uri == null) { 81 | return; 82 | } 83 | final String text = params.getTextDocument().getText(); 84 | if (text == null) { 85 | return; 86 | } 87 | workingTree.didOpen(params); 88 | analyzeFile(uri, text); 89 | } 90 | 91 | @Override 92 | public void didChange(DidChangeTextDocumentParams params) { 93 | final String uri = params.getTextDocument().getUri(); 94 | 95 | workingTree.didChange(params); 96 | analyzeFile(params.getTextDocument().getUri(), workingTree.get(uri).getContent()); 97 | } 98 | 99 | @Override 100 | public void didSave(DidSaveTextDocumentParams params) { 101 | if (params == null || params.getTextDocument() == null) { 102 | return; 103 | } 104 | final String uri = params.getTextDocument().getUri(); 105 | if (uri == null) { 106 | return; 107 | } 108 | final String text = params.getText(); 109 | if (text == null) { 110 | return; 111 | } 112 | workingTree.didSave(params); 113 | 114 | // update classes 115 | analyzeFile(uri, text); 116 | } 117 | 118 | @Override 119 | public void didClose(DidCloseTextDocumentParams params) { 120 | TextDocumentIdentifier textDocument = params.getTextDocument(); 121 | if (textDocument == null || textDocument.getUri() == null) { 122 | return; 123 | } 124 | 125 | docParseTree.remove(Util.uriToPath(textDocument.getUri())); 126 | } 127 | 128 | private void analyzeFile(@Nonnull String uri, @Nonnull String text) { 129 | final boolean valid = getServer().quarantineInputOrUpdate(uri, text); 130 | Path path = Util.uriToPath(uri); 131 | if (valid) { 132 | // calculate and cache interesting i.e.signature positions of the opened file 133 | JimpleParser jimpleParser = 134 | JimpleConverterUtil.createJimpleParser(CharStreams.fromString(text), path); 135 | ParseTree parseTree = jimpleParser.file(); 136 | docParseTree.put(path, jimpleParser); 137 | 138 | SignaturePositionResolver sigposresolver = new SignaturePositionResolver(path, parseTree); 139 | docSignaturePositionResolver.put(path, sigposresolver); 140 | } else { 141 | // file is invalid Jimple -> clear cache 142 | docParseTree.remove(path); 143 | docSignaturePositionResolver.remove(path); 144 | } 145 | } 146 | 147 | /* 148 | @Override 149 | public CompletableFuture, List>> 150 | declaration(DeclarationParams params) { 151 | // TODO: handle locals different if local declarations are present 152 | // TODO: handle abstract method declarations 153 | // otherwise its equal to definition 154 | return definition(params); 155 | } 156 | */ 157 | 158 | @Override 159 | public CompletableFuture, List>> 160 | definition(DefinitionParams position) { 161 | if (position == null) { 162 | return null; 163 | } 164 | 165 | // go to definition of Field/Method/Class/Local - if position resolves to a Type -> go to 166 | // typeDefinition 167 | return getServer() 168 | .pool( 169 | () -> { 170 | final String uri = position.getTextDocument().getUri(); 171 | final SignaturePositionResolver resolver = getSignaturePositionResolver(uri); 172 | if (resolver == null) { 173 | return null; 174 | } 175 | final Pair sigInst = resolver.resolve(position.getPosition()); 176 | if (sigInst == null) { 177 | // try if its a Local (which has no Signature!) 178 | 179 | final ClassType classType = getServer().uriToClasstype(uri); 180 | if (classType == null) { 181 | return null; 182 | } 183 | 184 | final Optional> aClass = getServer().getView().getClass(classType); 185 | if (!aClass.isPresent()) { 186 | return null; 187 | } 188 | SootClass sc = aClass.get(); 189 | 190 | // maybe: cache instance for this file like for sigs 191 | Path path = Util.uriToPath(uri); 192 | final JimpleParser parser = getParser(path); 193 | if (parser == null) { 194 | return null; 195 | } 196 | ParseTree parseTree = parser.file(); 197 | if (parseTree == null) { 198 | return null; 199 | } 200 | final LocalPositionResolver localPositionResolver = 201 | new LocalPositionResolver(path, parseTree); 202 | LocationLink locationLink = localPositionResolver.resolveDefinition(sc, position); 203 | if (locationLink == null) { 204 | return null; 205 | } 206 | if (getServer() 207 | .getClientCapabilities() 208 | .getTextDocument() 209 | .getDefinition() 210 | .getLinkSupport() 211 | == Boolean.TRUE) { 212 | return Either.forRight(Collections.singletonList(locationLink)); 213 | } else { 214 | return Either.forLeft( 215 | Collections.singletonList( 216 | new Location( 217 | locationLink.getTargetUri(), locationLink.getTargetRange()))); 218 | } 219 | } 220 | Signature sig = sigInst.getLeft(); 221 | if (sig != null) { 222 | Location definitionLocation = getDefinitionLocation(sig); 223 | if (definitionLocation == null) { 224 | return null; 225 | } 226 | 227 | if (getServer() 228 | .getClientCapabilities() 229 | .getTextDocument() 230 | .getDefinition() 231 | .getLinkSupport() 232 | == Boolean.TRUE) { 233 | return Either.forRight( 234 | Collections.singletonList( 235 | new LocationLink( 236 | definitionLocation.getUri(), 237 | definitionLocation.getRange(), 238 | definitionLocation.getRange(), 239 | sigInst.getRight()))); 240 | } else { 241 | return Either.forLeft(Collections.singletonList(definitionLocation)); 242 | } 243 | } 244 | return null; 245 | }); 246 | } 247 | 248 | @Nullable 249 | private Location getDefinitionLocation(@Nonnull Signature sig) { 250 | if (sig instanceof ClassType) { 251 | final Optional> aClass = getServer().getView().getClass((ClassType) sig); 252 | if (aClass.isPresent()) { 253 | SootClass sc = aClass.get(); 254 | SignaturePositionResolver resolver = 255 | getSignaturePositionResolver(Util.pathToUri(sc.getClassSource().getSourcePath())); 256 | if (resolver == null) { 257 | return null; 258 | } 259 | return resolver.findFirstMatchingSignature(sc.getType(), sc.getPosition()); 260 | } 261 | 262 | } else if (sig instanceof MethodSignature) { 263 | final Optional> aClass = 264 | getServer().getView().getClass(((MethodSignature) sig).getDeclClassType()); 265 | if (aClass.isPresent()) { 266 | SootClass sc = aClass.get(); 267 | final Optional methodOpt = 268 | sc.getMethod((((MethodSignature) sig).getSubSignature())); 269 | if (methodOpt.isPresent()) { 270 | final SootMethod method = methodOpt.get(); 271 | SignaturePositionResolver resolver = 272 | getSignaturePositionResolver(Util.pathToUri(sc.getClassSource().getSourcePath())); 273 | if (resolver == null) { 274 | return null; 275 | } 276 | return resolver.findFirstMatchingSignature(method.getSignature(), method.getPosition()); 277 | } 278 | } 279 | 280 | } else if (sig instanceof FieldSignature) { 281 | final Optional> aClass = 282 | getServer().getView().getClass(((FieldSignature) sig).getDeclClassType()); 283 | if (aClass.isPresent()) { 284 | SootClass sc = aClass.get(); 285 | final Optional field = 286 | sc.getField(((FieldSignature) sig).getSubSignature()); 287 | if (field.isPresent()) { 288 | final SootField sf = field.get(); 289 | SignaturePositionResolver resolver = 290 | getSignaturePositionResolver(Util.pathToUri(sc.getClassSource().getSourcePath())); 291 | if (resolver == null) { 292 | return null; 293 | } 294 | return resolver.findFirstMatchingSignature(sf.getSignature(), sf.getPosition()); 295 | } 296 | } 297 | } 298 | return null; 299 | } 300 | 301 | @Override 302 | public CompletableFuture, List>> 303 | implementation(ImplementationParams position) { 304 | if (position == null) { 305 | return null; 306 | } 307 | // resolve position to ClassSignature/MethodSignature and retrieve respective 308 | // subclasses/overriding methods there 309 | return getServer() 310 | .pool( 311 | () -> { 312 | List list = new ArrayList<>(); 313 | 314 | final String uri = position.getTextDocument().getUri(); 315 | final SignaturePositionResolver sigResolver = getSignaturePositionResolver(uri); 316 | if (sigResolver == null) { 317 | return null; 318 | } 319 | final Pair sigInstance = 320 | sigResolver.resolve(position.getPosition()); 321 | if (sigInstance == null) { 322 | return null; 323 | } 324 | Signature sig = sigInstance.getLeft(); 325 | 326 | final View> view = getServer().getView(); 327 | final ViewTypeHierarchy typeHierarchy = new ViewTypeHierarchy(view); 328 | 329 | if (sig instanceof ClassType) { 330 | final Set subClassTypes = typeHierarchy.subtypesOf((ClassType) sig); 331 | 332 | subClassTypes.forEach( 333 | subClassSig -> { 334 | Optional> scOpt = view.getClass(subClassSig); 335 | scOpt.ifPresent( 336 | sootClass -> 337 | list.add( 338 | Util.positionToDefLocation( 339 | Util.pathToUri(sootClass.getClassSource().getSourcePath()), 340 | sootClass.getPosition()))); 341 | }); 342 | 343 | if (getServer() 344 | .getClientCapabilities() 345 | .getTextDocument() 346 | .getDefinition() 347 | .getLinkSupport() 348 | == Boolean.TRUE) { 349 | return Either.forRight( 350 | list.stream() 351 | .map( 352 | i -> 353 | new LocationLink( 354 | i.getUri(), 355 | i.getRange(), 356 | i.getRange(), 357 | sigInstance.getRight())) 358 | .collect(Collectors.toList())); 359 | } else { 360 | return Either.forLeft(list); 361 | } 362 | 363 | } else if (sig instanceof MethodSignature) { 364 | final Set classTypes = 365 | typeHierarchy.subtypesOf(((MethodSignature) sig).getDeclClassType()); 366 | classTypes.forEach( 367 | csig -> { 368 | Optional> scOpt = view.getClass(csig); 369 | if (scOpt.isPresent()) { 370 | final SootClass sc = scOpt.get(); 371 | final Optional methodOpt = 372 | sc.getMethod(((MethodSignature) sig).getSubSignature()); 373 | final String methodsClassUri = 374 | Util.pathToUri(sc.getClassSource().getSourcePath()); 375 | methodOpt.ifPresent( 376 | method -> 377 | list.add( 378 | Util.positionToDefLocation( 379 | methodsClassUri, method.getPosition()))); 380 | } 381 | }); 382 | 383 | if (getServer() 384 | .getClientCapabilities() 385 | .getTextDocument() 386 | .getDefinition() 387 | .getLinkSupport() 388 | == Boolean.TRUE) { 389 | return Either.forRight( 390 | list.stream() 391 | .map( 392 | i -> 393 | new LocationLink( 394 | i.getUri(), 395 | i.getRange(), 396 | i.getRange(), 397 | sigInstance.getRight())) 398 | .collect(Collectors.toList())); 399 | } else { 400 | return Either.forLeft(list); 401 | } 402 | } 403 | 404 | return null; 405 | }); 406 | } 407 | 408 | @Override 409 | public CompletableFuture> references(ReferenceParams params) { 410 | if (params == null) { 411 | return null; 412 | } 413 | // find usages of FieldSignaturesy|MethodSignatures|Classtypes 414 | return getServer() 415 | .pool( 416 | () -> { 417 | List list = new ArrayList<>(); 418 | final String uri = params.getTextDocument().getUri(); 419 | final SignaturePositionResolver resolver = getSignaturePositionResolver(uri); 420 | if (resolver == null) { 421 | return null; 422 | } 423 | final Pair sigInstance = resolver.resolve(params.getPosition()); 424 | 425 | if (sigInstance == null) { 426 | // maybe its a Local? 427 | 428 | final ClassType classType = getServer().uriToClasstype(uri); 429 | if (classType == null) { 430 | return null; 431 | } 432 | final JimpleView view = getServer().getView(); 433 | final Optional> aClass = view.getClass(classType); 434 | if (aClass.isPresent()) { 435 | SootClass sc = aClass.get(); 436 | Path path = Util.uriToPath(uri); 437 | final JimpleParser parser = getParser(path); 438 | if (parser == null) { 439 | return null; 440 | } 441 | ParseTree parseTree = parser.file(); 442 | if (parseTree == null) { 443 | return null; 444 | } 445 | final LocalPositionResolver localPositionResolver = 446 | new LocalPositionResolver(path, parseTree); 447 | list.addAll(localPositionResolver.resolveReferences(sc, params)); 448 | return list; 449 | } 450 | 451 | return null; 452 | } 453 | Signature sig = sigInstance.getLeft(); 454 | 455 | boolean includeDef = 456 | params.getContext() != null && params.getContext().isIncludeDeclaration(); 457 | final Location definitionLocation = includeDef ? null : getDefinitionLocation(sig); 458 | 459 | final Collection> classes = getServer().getView().getClasses(); 460 | for (SootClass sc : classes) { 461 | final Path scPath = sc.getClassSource().getSourcePath(); 462 | final SignaturePositionResolver sigresolver = getSignaturePositionResolver(scPath); 463 | if (sigresolver == null) { 464 | continue; 465 | } 466 | final List resolvedList = sigresolver.resolve(sig); 467 | 468 | if (resolvedList != null) { 469 | // remove definition if requested 470 | if (!includeDef) { 471 | resolvedList.removeIf(loc -> loc.equals(definitionLocation)); 472 | } 473 | list.addAll(resolvedList); 474 | } 475 | } 476 | 477 | return list; 478 | }); 479 | } 480 | 481 | @Nullable 482 | public SignaturePositionResolver getSignaturePositionResolver(@Nonnull String uri) { 483 | return getSignaturePositionResolver(Util.uriToPath(uri)); 484 | } 485 | 486 | @Nullable 487 | private SignaturePositionResolver getSignaturePositionResolver(@Nonnull Path path) { 488 | return docSignaturePositionResolver.computeIfAbsent( 489 | path, 490 | k -> { 491 | try { 492 | final JimpleParser parser = getParser(path); 493 | if (parser == null) { 494 | return null; 495 | } 496 | ParseTree parseTree = parser.file(); 497 | if (parseTree == null) { 498 | return null; 499 | } 500 | return new SignaturePositionResolver(path, parseTree); 501 | } catch (IllegalStateException e) { 502 | forwardException(e); 503 | } 504 | return null; 505 | }); 506 | } 507 | 508 | /** 509 | * onlyValid: flag to specify whether a partial parsetree ie of invalid or incomplete jimple files 510 | * are mandatory 511 | */ 512 | @Nullable 513 | private JimpleParser getParser(@Nonnull Path path) { 514 | return docParseTree.computeIfAbsent( 515 | path, 516 | k -> { 517 | try { 518 | return JimpleConverterUtil.createJimpleParser(CharStreams.fromPath(path), path); 519 | } catch (IOException e) { 520 | forwardException(e); 521 | return null; 522 | } 523 | }); 524 | } 525 | 526 | @Override 527 | public CompletableFuture, List>> 528 | typeDefinition(TypeDefinitionParams position) { 529 | if (position == null) { 530 | return null; 531 | } 532 | 533 | // method-> returntype; field -> type; local -> type 534 | return getServer() 535 | .pool( 536 | () -> { 537 | final String uri = position.getTextDocument().getUri(); 538 | final SignaturePositionResolver resolver = getSignaturePositionResolver(uri); 539 | if (resolver == null) { 540 | return null; 541 | } 542 | final Pair sigInst = resolver.resolve(position.getPosition()); 543 | if (sigInst == null) { 544 | // try whether its a Local (which has no Signature!) 545 | final ClassType classType = getServer().uriToClasstype(uri); 546 | if (classType == null) { 547 | return null; 548 | } 549 | 550 | final Optional> aClass = getServer().getView().getClass(classType); 551 | if (!aClass.isPresent()) { 552 | return null; 553 | } 554 | SootClass sc = aClass.get(); 555 | 556 | // maybe: cache instance for this file like for sigs 557 | Path path = Util.uriToPath(uri); 558 | final JimpleParser parser = getParser(path); 559 | if (parser == null) { 560 | return null; 561 | } 562 | ParseTree parseTree = parser.file(); 563 | if (parseTree == null) { 564 | return null; 565 | } 566 | final LocalPositionResolver localPositionResolver = 567 | new LocalPositionResolver(path, parseTree); 568 | final Type type = 569 | localPositionResolver.resolveTypeDefinition(sc, position.getPosition()); 570 | 571 | if (!(type instanceof ClassType)) { 572 | return null; 573 | } 574 | final Optional> typeClass = 575 | getServer().getView().getClass((ClassType) type); 576 | if (typeClass.isPresent()) { 577 | final SootClass sootClass = typeClass.get(); 578 | return Util.positionToLocationList( 579 | Util.pathToUri(sootClass.getClassSource().getSourcePath()), 580 | sootClass.getPosition()); 581 | } 582 | return null; 583 | } 584 | Signature sig = sigInst.getLeft(); 585 | 586 | if (sig instanceof ClassType) { 587 | return null; 588 | } else if (sig instanceof MethodSignature) { 589 | final Type type = ((MethodSignature) sig).getType(); 590 | if (!(type instanceof ClassType)) { 591 | return null; 592 | } 593 | final Optional> aClass = 594 | getServer().getView().getClass((ClassType) type); 595 | if (aClass.isPresent()) { 596 | SootClass sc = aClass.get(); 597 | final Optional method = 598 | sc.getMethod(((MethodSignature) sig).getSubSignature()); 599 | if (method.isPresent()) { 600 | return Util.positionToLocationList( 601 | Util.pathToUri(sc.getClassSource().getSourcePath()), 602 | method.get().getPosition()); 603 | } 604 | } 605 | 606 | } else if (sig instanceof FieldSignature) { 607 | final Type type = ((FieldSignature) sig).getType(); 608 | if (!(type instanceof ClassType)) { 609 | return null; 610 | } 611 | final Optional> aClass = 612 | getServer().getView().getClass((ClassType) type); 613 | if (aClass.isPresent()) { 614 | SootClass sc = aClass.get(); 615 | final Optional field = 616 | sc.getField(((FieldSignature) sig).getSubSignature()); 617 | if (field.isPresent()) { 618 | return Util.positionToLocationList( 619 | Util.pathToUri(sc.getClassSource().getSourcePath()), 620 | field.get().getPosition()); 621 | } 622 | } 623 | } 624 | 625 | return null; 626 | }); 627 | } 628 | 629 | @Override 630 | public CompletableFuture hover(HoverParams position) { 631 | if (position == null) { 632 | return null; 633 | } 634 | 635 | return getServer() 636 | .pool( 637 | () -> { 638 | final String uri = position.getTextDocument().getUri(); 639 | final SignaturePositionResolver sigResolver = getSignaturePositionResolver(uri); 640 | if (sigResolver == null) { 641 | return null; 642 | } 643 | final Pair sigInstance = 644 | sigResolver.resolve(position.getPosition()); 645 | if (sigInstance == null) { 646 | return null; 647 | } 648 | Signature sig = sigInstance.getLeft(); 649 | 650 | String str = null; 651 | if (sig instanceof ClassType) { 652 | final Optional> aClass = 653 | getServer().getView().getClass((ClassType) sig); 654 | if (aClass.isPresent()) { 655 | SootClass sc = aClass.get(); 656 | str = ClassModifier.toString(sc.getModifiers()) + " " + sc; 657 | Optional superclass = sc.getSuperclass(); 658 | if (superclass.isPresent()) { 659 | str += "\n extends " + superclass.get(); 660 | } 661 | 662 | Iterator interfaceIt = sc.getInterfaces().iterator(); 663 | if (interfaceIt.hasNext()) { 664 | str += " implements " + interfaceIt.next(); 665 | while (interfaceIt.hasNext()) { 666 | str += ", " + interfaceIt.next(); 667 | } 668 | } 669 | } 670 | } else if (sig instanceof MethodSignature) { 671 | final Optional> aClass = 672 | getServer().getView().getClass(((MethodSignature) sig).getDeclClassType()); 673 | if (aClass.isPresent()) { 674 | SootClass sc = aClass.get(); 675 | final Optional aMethod = 676 | sc.getMethod(((MethodSignature) sig).getSubSignature()); 677 | if (aMethod.isPresent()) { 678 | final SootMethod sootMethod = aMethod.get(); 679 | str = MethodModifier.toString(sootMethod.getModifiers()) + " " + sootMethod; 680 | } 681 | } 682 | } else if (sig instanceof FieldSignature) { 683 | final Optional> aClass = 684 | getServer().getView().getClass(((FieldSignature) sig).getDeclClassType()); 685 | if (aClass.isPresent()) { 686 | SootClass sc = aClass.get(); 687 | final Optional aField = 688 | sc.getField(((FieldSignature) sig).getSubSignature()); 689 | if (aField.isPresent()) { 690 | final SootField sootField = aField.get(); 691 | str = FieldModifier.toString(sootField.getModifiers()) + " " + sootField; 692 | } 693 | } 694 | } 695 | 696 | if (str != null) { 697 | return new Hover( 698 | new MarkupContent(MarkupKind.PLAINTEXT, str), sigInstance.getRight()); 699 | } 700 | return null; 701 | }); 702 | } 703 | 704 | @Override 705 | public CompletableFuture> documentHighlight( 706 | DocumentHighlightParams position) { 707 | if (position == null) { 708 | return null; 709 | } 710 | 711 | // local references 712 | return getServer() 713 | .pool( 714 | () -> { 715 | final String uri = position.getTextDocument().getUri(); 716 | Path path = Util.uriToPath(uri); 717 | final JimpleParser parser = getParser(path); 718 | if (parser == null) { 719 | return null; 720 | } 721 | ParseTree parseTree = parser.file(); 722 | if (parseTree == null) { 723 | return null; 724 | } 725 | 726 | final LocalPositionResolver resolver = new LocalPositionResolver(path, parseTree); 727 | 728 | final ClassType classType = getServer().uriToClasstype(uri); 729 | if (classType == null) { 730 | return null; 731 | } 732 | final JimpleView view = getServer().getView(); 733 | final Optional> aClass = view.getClass(classType); 734 | if (aClass.isPresent()) { 735 | SootClass sc = aClass.get(); 736 | return resolver.resolveReferences(sc, position).stream() 737 | .map(ref -> new DocumentHighlight(ref.getRange(), DocumentHighlightKind.Text)) 738 | .collect(Collectors.toList()); 739 | } 740 | 741 | return null; 742 | }); 743 | } 744 | 745 | @Override 746 | public CompletableFuture> formatting(DocumentFormattingParams params) { 747 | if (params == null) { 748 | return null; 749 | } 750 | final TextDocumentIdentifier textDocument = params.getTextDocument(); 751 | if (textDocument == null) { 752 | return null; 753 | } 754 | final String uri = textDocument.getUri(); 755 | if (uri == null) { 756 | return null; 757 | } 758 | 759 | return getServer() 760 | .pool( 761 | () -> { 762 | // warning: removes comments! 763 | final ClassType classType = getServer().uriToClasstype(uri); 764 | if (classType == null) { 765 | return null; 766 | } 767 | final JimpleView view = getServer().getView(); 768 | final Optional> aClass = view.getClass(classType); 769 | if (aClass.isPresent()) { 770 | SootClass sc = aClass.get(); 771 | 772 | final StringWriter out = new StringWriter(); 773 | PrintWriter writer = new PrintWriter(out); 774 | sootup.core.util.printer.JimplePrinter printer = new JimplePrinter(); 775 | printer.printTo(sc, writer); 776 | writer.close(); 777 | final String newText = out.toString(); 778 | return Collections.singletonList( 779 | new TextEdit( 780 | new Range( 781 | new Position(0, 0), 782 | new Position( 783 | sc.getPosition().getLastLine(), sc.getPosition().getLastCol())), 784 | newText)); 785 | } 786 | return null; 787 | }); 788 | } 789 | 790 | @Override 791 | public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { 792 | // possibilities: fold imports | fold multiline comments 793 | if (params == null) { 794 | return null; 795 | } 796 | 797 | return getServer() 798 | .pool( 799 | () -> { 800 | final ClassType classType = 801 | getServer().uriToClasstype(params.getTextDocument().getUri()); 802 | if (classType == null) { 803 | return null; 804 | } 805 | final Optional> aClass = getServer().getView().getClass(classType); 806 | if (aClass.isPresent()) { 807 | SootClass sc = aClass.get(); 808 | List frList = new ArrayList<>(); 809 | sc.getMethods() 810 | .forEach( 811 | m -> { 812 | final FoldingRange fr = 813 | new FoldingRange( 814 | m.getPosition().getFirstLine(), m.getPosition().getLastLine()); 815 | fr.setKind("region"); 816 | frList.add(fr); 817 | }); 818 | return frList; 819 | } 820 | 821 | return null; 822 | }); 823 | } 824 | 825 | @Override 826 | public CompletableFuture>> documentSymbol( 827 | DocumentSymbolParams params) { 828 | if (params == null) { 829 | return null; 830 | } 831 | 832 | return getServer() 833 | .pool( 834 | () -> { 835 | final TextDocumentClientCapabilities textDocumentCap = 836 | getServer().getClientCapabilities().getTextDocument(); 837 | if (textDocumentCap == null) { 838 | return null; 839 | } 840 | final DocumentSymbolCapabilities documentSymbol = textDocumentCap.getDocumentSymbol(); 841 | if (documentSymbol == null) { 842 | return null; 843 | } 844 | final SymbolKindCapabilities symbolKind = documentSymbol.getSymbolKind(); 845 | if (symbolKind == null) { 846 | return null; 847 | } 848 | final TextDocumentIdentifier textDoc = params.getTextDocument(); 849 | if (textDoc == null) { 850 | return null; 851 | } 852 | final String uri = textDoc.getUri(); 853 | if (uri == null) { 854 | return null; 855 | } 856 | final ClassType classType = getServer().uriToClasstype(uri); 857 | if (classType == null) { 858 | return null; 859 | } 860 | final Optional> aClass = getServer().getView().getClass(classType); 861 | if (!aClass.isPresent()) { 862 | return null; 863 | } 864 | 865 | SootClass sc = aClass.get(); 866 | List list = new ArrayList<>(); 867 | int limit = Integer.MAX_VALUE; 868 | JimpleSymbolProvider.retrieveAndFilterSymbolsFromClass( 869 | list, null, sc, getSignaturePositionResolver(uri), symbolKind, limit); 870 | 871 | return list.stream() 872 | .>map(Either::forLeft) 873 | .collect(Collectors.toCollection(() -> new ArrayList<>(list.size()))); 874 | }); 875 | } 876 | 877 | @Nullable 878 | ParserRuleContext tryParse( 879 | @Nonnull JimpleParser jp, @Nonnull List> levels) { 880 | 881 | ParserRuleContext tree; 882 | for (Function level : levels) { 883 | try { 884 | tree = level.apply(jp); 885 | if (jp.getNumberOfSyntaxErrors() > 0) { 886 | jp.reset(); 887 | continue; 888 | } 889 | } catch (Exception e) { 890 | jp.reset(); 891 | continue; 892 | } 893 | return tree; 894 | } 895 | 896 | return null; 897 | } 898 | 899 | @Override 900 | public CompletableFuture semanticTokensFull(SemanticTokensParams params) { 901 | final TextDocumentIdentifier textDoc = params.getTextDocument(); 902 | if (textDoc == null) { 903 | return null; 904 | } 905 | final Path path = Util.uriToPath(textDoc.getUri()); 906 | return getServer() 907 | .pool( 908 | () -> { 909 | final JimpleParser parser = getParser(path); 910 | if (parser == null) { 911 | // e.g. file not found 912 | return null; 913 | } 914 | 915 | List> levels = new ArrayList<>(); 916 | levels.add(JimpleParser::file); 917 | levels.add(JimpleParser::member); 918 | levels.add(JimpleParser::method_body); 919 | 920 | levels.add(JimpleParser::declarations); 921 | levels.add(JimpleParser::statements); 922 | levels.add(JimpleParser::trap_clauses); 923 | 924 | levels.add(JimpleParser::field_signature); 925 | levels.add(JimpleParser::method_signature); 926 | levels.add(JimpleParser::method_subsignature); 927 | levels.add(JimpleParser::immediate); 928 | 929 | final ParserRuleContext parseTree = tryParse(parser, levels); 930 | if (parseTree == null) { 931 | return null; 932 | } 933 | 934 | return SyntaxHighlightingProvider.paintbrush(parseTree); 935 | }); 936 | } 937 | 938 | public JimpleParser getDocParseTree(Path path) { 939 | return docParseTree.computeIfAbsent( 940 | path, 941 | p -> { 942 | try { 943 | analyzeFile(Util.pathToUri(path), Arrays.toString(Files.readAllBytes(path))); 944 | return docParseTree.get(path); 945 | } catch (IOException e) { 946 | e.printStackTrace(); 947 | return null; 948 | } 949 | }); 950 | } 951 | 952 | } 953 | --------------------------------------------------------------------------------