Fixed: Incomplete classpath provided to Java LSP

This commit is contained in:
Akash Yadav
2022-08-16 13:25:11 +05:30
parent f8c94f2f83
commit c87538fc5a
14 changed files with 163 additions and 94 deletions
Binary file not shown.
@@ -32,4 +32,7 @@ class DefaultLibrary : Library, Serializable {
override var lintJar: File? = null
override var projectInfo: DefaultProjectInfo? = null
override var type: LibraryType = ANDROID_LIBRARY
/** Dependencies of this library. */
val dependencies = mutableSetOf<String>()
}
@@ -61,7 +61,10 @@ import java.io.File
* @param viewBindingOptions The view binding options of this module.
* @param bootClassPaths The boot class paths of the project. Usually contains the path to the
* `android.jar` file.
* @param libraries The list of libraries for the debug variant.
* @param libraries The list of libraries for the debug variant. Values must be values in the
* [libraryMap].
* @param libraryMap The map of libraries. Keys are the key field obtained from the GraphItem
* instance in the Tooling API implementation.
* @param dynamicFeatures The dynamic features.
* @param lintCheckJars The lint check jar files.
* @param modelSyncFiles The model sync files.
@@ -86,20 +89,26 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
val javaCompileOptions: DefaultJavaCompileOptions,
val viewBindingOptions: DefaultViewBindingOptions,
val bootClassPaths: Collection<File>,
val libraries: List<DefaultLibrary>,
val libraries: Set<String>,
val libraryMap: Map<String, DefaultLibrary>,
val dynamicFeatures: Collection<String>?,
val lintCheckJars: List<File>,
val modelSyncFiles: List<DefaultModelSyncFile>,
val variants: List<SimpleVariantData> = listOf()
) :
ModuleProject(name, description, path, projectDir, buildDir, buildScript, tasks, javaCompileOptions), WithModuleData {
ModuleProject(
name,
description,
path,
projectDir,
buildDir,
buildScript,
tasks,
javaCompileOptions
),
WithModuleData {
companion object {
const val UNKNOWN_PACKAGE = com.itsaky.androidide.tooling.api.model.AndroidModule.UNKNOWN_PACKAGE;
}
private val log = ILogger.newInstance(javaClass.simpleName)
private var cachedLibraries: Map<String, List<DefaultLibrary>> = mutableMapOf()
override var moduleData: SimpleModuleData? = null
init {
@@ -174,56 +183,53 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
}
override fun getCompileClasspaths(): Set<File> {
val compileLibraries = getCompileLibraries()
val classpaths = getModuleClasspaths().toMutableSet()
getCompileModuleProjects(compileLibraries).forEach {
classpaths.addAll(it.getModuleClasspaths())
}
getCompileAndroidOrJavaLibraries(compileLibraries).forEach {
if (it.type == ANDROID_LIBRARY) {
classpaths.addAll(it.androidLibraryData!!.compileJarFiles)
} else {
classpaths.add(it.artifact!!)
val root = ProjectManager.rootProject ?: return emptySet()
val result = mutableSetOf<File>()
result.addAll(getModuleClasspaths())
collectLibraries(root, this.libraries, result)
return result
}
private fun collectLibraries(root: Project, libraries: Set<String>, result: MutableSet<File>) {
for (library in libraries) {
val lib = this.libraryMap[library] ?: continue
if (lib.type == PROJECT) {
val module = root.findByPath(lib.projectInfo!!.projectPath) ?: continue
if (module !is ModuleProject) {
continue
}
result.addAll(module.getCompileClasspaths())
} else if (lib.type == ANDROID_LIBRARY) {
result.addAll(lib.androidLibraryData!!.compileJarFiles)
} else if (lib.type == JAVA_LIBRARY) {
result.add(lib.artifact!!)
}
collectLibraries(root, lib.dependencies, result)
}
return classpaths
}
override fun getCompileModuleProjects(): List<ModuleProject> {
return getCompileModuleProjects(getCompileLibraries())
}
fun getCompileAndroidOrJavaLibraries(): List<DefaultLibrary> {
return getCompileAndroidOrJavaLibraries(getCompileLibraries())
}
/**
* Get all the libraries of this project with compile scope. This includes libraries of all
* [com.android.builder.model.v2.ide.LibraryType]s.
*/
fun getCompileLibraries(): List<DefaultLibrary> {
return this.libraries.filter {
when (it.type) {
PROJECT -> it.projectInfo!!.attributes[PROP_USAGE] == USAGE_API
ANDROID_LIBRARY,
JAVA_LIBRARY -> it.libraryInfo!!.attributes[PROP_USAGE] == USAGE_API
else -> false
}
}
}
private fun getCompileModuleProjects(libs: List<DefaultLibrary>): List<ModuleProject> {
val root = ProjectManager.rootProject ?: return emptyList()
return libs
.filter { it.type == PROJECT }
.mapNotNull { root.findByPath(it.projectInfo!!.projectPath) }
.filterIsInstance(ModuleProject::class.java)
}
val result = mutableListOf<ModuleProject>()
private fun getCompileAndroidOrJavaLibraries(libs: List<DefaultLibrary>): List<DefaultLibrary> {
return libs.filter {
(it.type == ANDROID_LIBRARY || it.type == JAVA_LIBRARY) &&
it.libraryInfo!!.attributes[PROP_USAGE] == USAGE_API
for (library in this.libraries) {
val lib = this.libraryMap[library] ?: continue
if (lib.type != PROJECT) {
continue
}
val module = root.findByPath(lib.projectInfo!!.projectPath) ?: continue
if (module !is ModuleProject) {
continue
}
result.add(module)
result.addAll(module.getCompileModuleProjects())
}
return result
}
}
@@ -17,7 +17,6 @@
package com.itsaky.androidide.projects.util
import com.android.builder.model.v2.ide.LibraryType.PROJECT
import com.android.builder.model.v2.ide.ProjectType.LIBRARY
import com.itsaky.androidide.builder.model.DefaultViewBindingOptions
import com.itsaky.androidide.projects.api.AndroidModule
@@ -61,7 +60,9 @@ class ProjectTransformer {
}
}
fun transform(project: com.itsaky.androidide.tooling.api.model.AndroidModule): AndroidModule {
private fun transform(
project: com.itsaky.androidide.tooling.api.model.AndroidModule
): AndroidModule {
return AndroidModule(
name = project.name,
description = project.description ?: "",
@@ -81,10 +82,8 @@ class ProjectTransformer {
javaCompileOptions = project.javaCompileOptions,
viewBindingOptions = project.viewBindingOptions ?: DefaultViewBindingOptions(),
bootClassPaths = project.bootClassPaths,
libraries =
project.libraries.filter {
!(it.type == PROJECT && it.projectInfo!!.projectPath == project.projectPath)
},
libraries = project.libraries,
libraryMap = project.libraryMap,
dynamicFeatures = project.dynamicFeatures,
lintCheckJars = project.lintChecksJars,
modelSyncFiles = project.modelSyncFiles,
@@ -92,7 +91,7 @@ class ProjectTransformer {
)
}
fun transform(project: com.itsaky.androidide.tooling.api.model.JavaModule): JavaModule {
private fun transform(project: com.itsaky.androidide.tooling.api.model.JavaModule): JavaModule {
return JavaModule(
name = project.name,
description = project.description ?: "",
@@ -110,12 +109,12 @@ class ProjectTransformer {
private fun transform(modules: MutableList<SimpleModuleData>, root: IProject): List<Project> {
return mutableListOf<Project>().apply {
for (module in modules) {
add(createProject(module, root)!!)
add(createProject(module, root))
}
}
}
private fun createProject(module: SimpleModuleData, root: IProject): Project? {
private fun createProject(module: SimpleModuleData, root: IProject): Project {
val project =
root.findByPath(module.path).get()
?: throw java.lang.IllegalStateException("Invalid module data")
@@ -21,22 +21,19 @@ import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.SourceClassTrie.SourceNode
import java.io.File
import java.nio.file.Files
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
import java.nio.file.Files
/** @author Akash Yadav */
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.DEFAULT_VALUE_STRING)
class ModuleProjectTest {
private val log = ILogger.newInstance(javaClass.simpleName)
@Test
fun test() {
val (server, project) = ToolingApiTestLauncher().launchServer()
@@ -22,6 +22,8 @@ import static com.itsaky.androidide.utils.ILogger.MSG_SEPARATOR;
import static java.util.Collections.emptyList;
import com.android.builder.model.ModelBuilderParameter;
import com.android.builder.model.v2.ide.GraphItem;
import com.android.builder.model.v2.ide.Library;
import com.android.builder.model.v2.ide.ProjectType;
import com.android.builder.model.v2.models.AndroidProject;
import com.android.builder.model.v2.models.BasicAndroidProject;
@@ -62,11 +64,17 @@ import org.gradle.tooling.model.idea.IdeaSingleEntryLibraryDependency;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import kotlin.Pair;
/**
* @author Akash Yadav
*/
@@ -264,12 +272,9 @@ public class ProjectReader {
ModelBuilderParameter.class,
it -> it.setVariantName("debug"));
final var libraries = new ArrayList<DefaultLibrary>();
variantDependencies
.getLibraries()
.forEach((s, library) -> libraries.add(AndroidModulePropertyCopier.INSTANCE.copy(library)));
module.setLibraries(libraries);
final var dependencies = getAllCompileDependencies(variantDependencies);
module.setLibraryMap(dependencies.getFirst());
module.setLibraries(dependencies.getSecond());
log("Fetching project sync issues...");
final var issues = controller.findModel(gradle, ProjectSyncIssues.class);
@@ -285,6 +290,45 @@ public class ProjectReader {
return new ModelInfoContainer(module, syncIssues);
}
private static Pair<Map<String, DefaultLibrary>, Set<String>> getAllCompileDependencies(
final VariantDependencies variantDependencies) {
final var seen = new HashMap<String, DefaultLibrary>();
final var libs = new HashSet<String>();
final var compileDependencies = variantDependencies.getMainArtifact().getCompileDependencies();
final var libraries = variantDependencies.getLibraries();
for (final var dependency : compileDependencies) {
var library = seen.get(dependency.getKey());
if (library == null) {
library = getLibrary(dependency, libraries, seen);
}
if (library == null) {
continue;
}
libs.add(library.getKey());
}
return new Pair<>(seen, libs);
}
private static DefaultLibrary getLibrary(
final GraphItem item,
final Map<String, Library> libraries,
final HashMap<String, DefaultLibrary> seen) {
final var lib = libraries.get(item.getKey());
if (lib == null) {
return null;
}
final var library = AndroidModulePropertyCopier.INSTANCE.copy(lib);
for (final var dependency : item.getDependencies()) {
final DefaultLibrary dep = Objects.requireNonNull(getLibrary(dependency, libraries, seen));
library.getDependencies().add(dep.getKey());
}
seen.put(item.getKey(), library);
return library;
}
private static JavaModule buildJavaModuleProject(
GradleProject gradle, final IdeaProject ideaProject, IdeaModule idea) {
final var builder = new ProjectBuilder();
@@ -20,6 +20,7 @@ package com.itsaky.androidide.tooling.impl
import com.android.builder.model.v2.ide.LibraryType.PROJECT
import com.android.builder.model.v2.ide.ProjectType.APPLICATION
import com.google.common.truth.Truth.assertThat
import com.google.gson.GsonBuilder
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage
@@ -27,6 +28,7 @@ import com.itsaky.androidide.tooling.api.model.AndroidModule
import com.itsaky.androidide.tooling.api.model.JavaModule
import com.itsaky.androidide.tooling.api.model.JavaModuleExternalDependency
import com.itsaky.androidide.tooling.api.model.JavaModuleProjectDependency
import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import java.io.File
import org.junit.Test
@@ -57,6 +59,11 @@ class ToolingApiImplTest {
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(AndroidModule::class.java)
GsonBuilder().apply {
ToolingApiLauncher.configureGson(this)
println(create().toJson(app))
}
assertThat((app as AndroidModule).javaCompileOptions).isNotNull()
assertThat(app.javaCompileOptions.sourceCompatibility).isEqualTo("11")
assertThat(app.javaCompileOptions.targetCompatibility).isEqualTo("11")
@@ -77,11 +84,11 @@ class ToolingApiImplTest {
assertThat(app.libraries).isNotEmpty()
// At least one project library
assertThat(app.libraries.filter { it.type == PROJECT }).isNotEmpty()
assertThat(app.libraryMap.values.filter { it.type == PROJECT }).isNotEmpty()
// :app module includes :java-library as a dependency. But it is not transitive
assertThat(
app.libraries.firstOrNull {
app.libraryMap.values.firstOrNull {
it.type == PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
@@ -96,7 +103,7 @@ class ToolingApiImplTest {
// :java-library project which further includes :another-java-libraries project with 'api'
// configuration
assertThat(
(androidLib as AndroidModule).libraries.firstOrNull {
(androidLib as AndroidModule).libraryMap.values.firstOrNull {
it.type == PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
@@ -30,11 +30,11 @@ import com.itsaky.androidide.builder.model.IJavaCompilerSettings
import com.itsaky.androidide.tooling.api.IProject.Type
import com.itsaky.androidide.tooling.api.IProject.Type.Android
import com.itsaky.androidide.tooling.api.messages.result.SimpleVariantData
import org.eclipse.lemminx.dom.DOMParser
import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager
import java.io.File
import java.io.Serializable
import java.util.concurrent.*
import java.util.concurrent.CompletableFuture
import org.eclipse.lemminx.dom.DOMParser
import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager
/**
* Default implementation of [AndroidProject].
@@ -78,8 +78,9 @@ open class AndroidModule(
}
var bootClassPaths: Collection<File> = emptyList()
var mainSourceSet: DefaultSourceSetContainer? = null
var libraries: List<DefaultLibrary> = emptyList()
var libraries = mutableSetOf<String>()
var libraryMap = mutableMapOf<String, DefaultLibrary>()
override var compilerSettings: IJavaCompilerSettings = javaCompileOptions
@Suppress("unused")
@@ -236,7 +236,7 @@ public class JavaLanguageServer implements ILanguageServer {
return DiagnosticResult.NO_UPDATE;
}
return this.diagnosticProvider.analyze(compiler, file);
return this.diagnosticProvider.analyze(file);
}
@NonNull
@@ -38,7 +38,7 @@ class DefaultCompilationTaskProcessor : CompilationTaskProcessor {
trees.forEach(processCompilationUnit::accept)
watch.lapFromLast("Processed trees")
JavacTaskUtil.analyze(task, JavacTaskUtil.enterTrees(task, trees))
task.analyze()
watch.lapFromLast("Analyzed all trees")
}
}
@@ -18,6 +18,9 @@
package com.itsaky.androidide.lsp.java.compiler;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -83,25 +86,29 @@ public class SourceFileManager extends ForwardingJavaFileManager<JavacFileManage
setLocationLogError(StandardLocation.SOURCE_PATH, module.getCompileSourceDirectories());
final Set<File> classpaths = getClasspaths(module);
final Set<File> classpaths = configureClasspaths(module);
setLocationLogError(StandardLocation.CLASS_PATH, classpaths);
listLocations(EnumSet.of(StandardLocation.CLASS_PATH));
listLocations(EnumSet.of(StandardLocation.CLASS_PATH, StandardLocation.PLATFORM_CLASS_PATH));
}
@NonNull
private Set<File> getClasspaths(final ModuleProject module) {
private Set<File> configureClasspaths(final ModuleProject module) {
if (module == null) {
return Collections.singleton(Environment.ANDROID_JAR);
setFallbackPlatformClasspath();
return emptySet();
}
final Set<File> classpaths = module.getCompileClasspaths();
if (module instanceof AndroidModule) {
classpaths.addAll(((AndroidModule) module).getBootClassPaths());
} else {
classpaths.add(Environment.ANDROID_JAR);
if (!(module instanceof AndroidModule)) {
setFallbackPlatformClasspath();
return emptySet();
}
final AndroidModule androidModule = (AndroidModule) module;
classpaths.addAll(androidModule.getBootClassPaths());
setLocationLogError(StandardLocation.PLATFORM_CLASS_PATH, androidModule.getBootClassPaths());
return classpaths;
}
@@ -28,7 +28,7 @@ import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import java.nio.file.Path
import java.time.Instant
import java.util.concurrent.atomic.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* Code analyzer for java source code.
@@ -43,7 +43,7 @@ class JavaDiagnosticProvider {
private var analyzing = AtomicBoolean(false)
private var analyzingThread: AnalyzingThread? = null
fun analyze(c: JavaCompilerService, file: Path): DiagnosticResult {
fun analyze(file: Path): DiagnosticResult {
val module = ProjectManager.findModuleForFile(file) ?: return DiagnosticResult.NO_UPDATE
val compiler = JavaCompilerService(module)
@@ -36,10 +36,15 @@ import com.sun.tools.javac.tree.JCTree.JCVariableDecl
import com.sun.tools.javac.tree.TreeScanner
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import javax.lang.model.type.ArrayType
/** @author Akash Yadav */
class PartialReparserImplTest {
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.DEFAULT_VALUE_STRING)
class PartialReparserImplTest {
@Before
fun setup() {
Binary file not shown.