Source links via CU DEclaration AST

This commit is contained in:
aboyko
2023-04-05 10:12:35 -04:00
parent 3e39181e2b
commit ebc2b461d3
6 changed files with 269 additions and 7 deletions

View File

@@ -0,0 +1,189 @@
package org.springframework.ide.vscode.boot.java.links;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Stack;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaModuleData;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.text.Region;
public abstract class AbstractSourceLinks2 implements SourceLinks {
private static final Logger log = LoggerFactory.getLogger(AbstractSourceLinks2.class);
private CompilationUnitCache cuCache;
private JavaProjectFinder projectFinder;
protected AbstractSourceLinks2(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) {
this.cuCache = cuCache;
this.projectFinder = projectFinder;
}
@Override
public Optional<String> sourceLinkUrlForFQName(IJavaProject project, String fqName) {
Optional<String> url = project == null ? Optional.empty() : getSourceLinkUrlForFQName(project, fqName);
if (!url.isPresent()) {
for (IJavaProject jp : projectFinder.all()) {
if (jp != project) {
url = getSourceLinkUrlForFQName(jp, fqName);
if (url.isPresent()) {
break;
}
}
}
}
return url;
}
private Optional<String> getSourceLinkUrlForFQName(IJavaProject project, String fqName) {
IJavaModuleData classpathResource = project.getIndex().findClasspathResourceContainer(fqName);
if (classpathResource != null) {
File file = classpathResource.getContainer();
if (file.isDirectory()) {
return javaSourceLinkUrl(project, fqName, classpathResource);
} else {
return jarSourceLinkUrl(project, fqName, classpathResource);
}
}
return Optional.empty();
}
@Override
public Optional<String> sourceLinkUrlForClasspathResource(String path) {
return SourceLinks.sourceLinkUrlForClasspathResource(this, projectFinder, path);
}
private Optional<String> javaSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData folderModuleData) {
IClasspath classpath = project.getClasspath();
return SourceLinks.sourceFromSourceFolder(fqName, classpath)
.map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName));
}
private String javaSourceLinkUrl(IJavaProject project, Path sourcePath, String fqName) {
Optional<String> linkOptional = sourceLinkForResourcePath(sourcePath);
if (linkOptional.isPresent()) {
Optional<String> positionLink = findCU(project, sourcePath.toUri()).map(cu -> positionLink(cu, fqName));
return positionLink.isPresent() ? linkOptional.get() + positionLink.get() : linkOptional.get();
}
return null;
}
abstract protected String positionLink(CompilationUnitDeclaration cu, String fqName);
private Optional<CompilationUnitDeclaration> findCU(IJavaProject project, URI uri) {
return cuCache == null ? Optional.empty() : cuCache.withCompilationUnitDeclaration(project, uri, compilationUnit -> Optional.ofNullable(compilationUnit));
}
abstract protected Optional<String> jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData);
private Optional<String> jarSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) {
return jarLinkUrl(project, fqName, jarModuleData).map(sourceUrl -> {
Optional<String> positionLink = findCUForFQNameFromJar(project, jarModuleData, fqName).map(cu -> positionLink(cu, fqName));
return positionLink.isPresent() ? sourceUrl + positionLink.get() : sourceUrl;
});
}
private Optional<CompilationUnitDeclaration> findCUForFQNameFromJar(IJavaProject project, IJavaModuleData jarModuleData, String fqName) {
return IClasspathUtil.sourceContainer(project.getClasspath(), jarModuleData.getContainer()).map(url -> {
try {
return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName, jarModuleData.getModule());
} catch (Exception e) {
log.warn("Failed to determine source URL from url={} fqName={}", url, fqName, e);
return null;
}
}).map(sourceUrl -> {
try {
return sourceUrl.toURI();
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}).map(sourcePath -> findCU(project, sourcePath).orElse(null));
}
protected Region findTypeRegion(CompilationUnitDeclaration cu, String fqName) {
if (cu == null) {
return null;
}
int[] values = new int[] {0, -1};
int lastDotIndex = fqName.lastIndexOf('.');
String packageName = fqName.substring(0, lastDotIndex);
String typeName = fqName.substring(lastDotIndex + 1);
String[] tokens = new String[cu.currentPackage.tokens.length];
for (int i = 0; i < cu.currentPackage.tokens.length; i++) {
tokens[i] = new String(cu.currentPackage.tokens[i]);
}
String cuPackageName = String.join(".", tokens);
if (packageName.equals(cuPackageName)) {
Stack<String> visitedType = new Stack<>();
cu.traverse(new ASTVisitor() {
private boolean visitDeclaration(TypeDeclaration node) {
visitedType.push(new String(node.name));
if (values[1] < 0) {
if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) {
values[0] = node.sourceStart;
values[1] = node.sourceEnd;
}
}
return values[1] < 0;
}
@Override
public boolean visit(TypeDeclaration localTypeDeclaration, BlockScope scope) {
return visitDeclaration(localTypeDeclaration);
}
@Override
public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
return visitDeclaration(memberTypeDeclaration);
}
@Override
public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
return visitDeclaration(typeDeclaration);
}
@Override
public void endVisit(TypeDeclaration localTypeDeclaration, BlockScope scope) {
visitedType.pop();
super.endVisit(localTypeDeclaration, scope);
}
@Override
public void endVisit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
visitedType.pop();
super.endVisit(memberTypeDeclaration, scope);
}
@Override
public void endVisit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
visitedType.pop();
super.endVisit(typeDeclaration, scope);
}
}, new CompilationUnitScope(cu, new CompilerOptions(CompilationUnitCache.createCompilerOptions())), false);
}
return values[1] < 0 ? null : new Region(values[0], values[1]);
}
}

View File

@@ -55,7 +55,7 @@ public final class SourceLinkFactory {
switch (LspClient.currentClient()) {
case VSCODE:
case THEIA:
return /*new VSCodeSourceLinks(cuCache);*/server == null ? new VSCodeSourceLinks(cuCache, projectFinder) :new JavaServerSourceLinks(server, projectFinder);
return /*new VSCodeSourceLinks(cuCache);*/server == null ? new VSCodeSourceLinks2(cuCache, projectFinder) :new JavaServerSourceLinks(server, projectFinder);
case ECLIPSE:
return /*new EclipseSourceLinks();*/server == null ? new EclipseSourceLinks(projectFinder) : new JavaServerSourceLinks(server, projectFinder);
case ATOM:

View File

@@ -0,0 +1,48 @@
package org.springframework.ide.vscode.boot.java.links;
import java.nio.file.Path;
import java.util.Optional;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.utils.CuDeclarationUtils;
import org.springframework.ide.vscode.commons.java.IJavaModuleData;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.text.Region;
public class VSCodeSourceLinks2 extends AbstractSourceLinks2 {
public VSCodeSourceLinks2(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) {
super(cuCache, projectFinder);
}
@Override
public Optional<String> sourceLinkForResourcePath(Path path) {
return Optional.of(path.toUri().toASCIIString());
}
@Override
protected String positionLink(CompilationUnitDeclaration cu, String fqName) {
if (cu != null) {
Region region = findTypeRegion(cu, fqName);
if (region != null) {
int line = CuDeclarationUtils.getLineNumber(cu, region.getOffset());
int column = CuDeclarationUtils.getColumn(cu, region.getOffset());
StringBuilder sb = new StringBuilder();
sb.append('#');
sb.append(line);
sb.append(',');
sb.append(column + 1); // 1-based columns?
return sb.toString();
}
}
return null;
}
@Override
protected Optional<String> jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) {
return Optional.ofNullable(JdtJavaDocumentUriProvider.uri(project, fqName)).map(uri -> uri.toASCIIString());
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.ide.vscode.boot.java.utils;
import java.util.Arrays;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
public class CuDeclarationUtils {
public static int getLineNumber(CompilationUnitDeclaration cu, int offset) {
int insertionIndex = Arrays.binarySearch(cu.compilationResult.lineSeparatorPositions, offset);
if (insertionIndex < 0) {
return -insertionIndex;
}
// start lines from 1.
return insertionIndex + 1;
}
public static int getColumn(CompilationUnitDeclaration cu, int offset) {
int line = getLineNumber(cu, offset);
// line start from 1 hence -1 and -1 for getting end of previous line
return line == 0 ? offset : offset - cu.compilationResult.lineSeparatorPositions[line - 2] - 1; // -1 at end because offset is for the line separator on the previous line
}
}

View File

@@ -16,7 +16,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.BootLanguageServerParams;
import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks;
import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks2;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.utils.SymbolCache;
import org.springframework.ide.vscode.boot.java.utils.SymbolCacheVoid;
@@ -66,7 +66,7 @@ public class SourceLinksTestConf {
}
@Bean SourceLinks sourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) {
return new VSCodeSourceLinks(cuCache, projectFinder);
return new VSCodeSourceLinks2(cuCache, projectFinder);
}
@Bean MockProjectObserver mockProjectObserver(BootLanguageServerParams params) {

View File

@@ -24,6 +24,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.gradle.internal.impldep.com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks;
import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks2;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.javadoc.JavaDocProviders;
@@ -61,7 +62,7 @@ public class VSCodeSourceLinksTest {
@Test
void testJavaSourceUrl() throws Exception {
MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app");
Optional<String> url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication");
Optional<String> url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication");
assertTrue(url.isPresent());
Path projectPath = Paths.get(project.pom().getParent());
URI uri = URI.create(url.get());
@@ -78,7 +79,7 @@ public class VSCodeSourceLinksTest {
@Test
void testClasspathResourceOnTomcatUrl() throws Exception {
MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app");
Optional<String> url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), new JavaProjectFinder() {
Optional<String> url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), new JavaProjectFinder() {
@Override
public Optional<IJavaProject> find(TextDocumentIdentifier doc) {
@@ -107,7 +108,7 @@ public class VSCodeSourceLinksTest {
@Test
void testJarUrl() throws Exception {
MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app");
Optional<String> url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication");
Optional<String> url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication");
assertTrue(url.isPresent());
String headerPart = url.get().substring(0, url.get().indexOf('?'));
assertEquals("jdt://contents/spring-boot-autoconfigure-1.5.8.RELEASE.jar/org.springframework.boot.autoconfigure/SpringBootApplication.class", headerPart);
@@ -118,7 +119,7 @@ public class VSCodeSourceLinksTest {
@Test
void testJarUrlInnerType() throws Exception {
MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app");
Optional<String> url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback");
Optional<String> url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback");
assertTrue(url.isPresent());
String headerPart = url.get().substring(0, url.get().indexOf('?'));
assertEquals("jdt://contents/spring-web-4.3.12.RELEASE.jar/org.springframework.web.client/RestTemplate$AcceptHeaderRequestCallback.class", headerPart);