diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java.as31 b/idea/idea-android/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java.as31 new file mode 100644 index 00000000000..89243e861e2 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java.as31 @@ -0,0 +1,149 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.android.navigation; + +import com.android.resources.ResourceType; +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlElement; +import org.jetbrains.android.dom.AndroidAttributeValue; +import org.jetbrains.android.dom.manifest.Manifest; +import org.jetbrains.android.dom.manifest.ManifestElementWithRequiredName; +import org.jetbrains.android.dom.resources.Attr; +import org.jetbrains.android.dom.resources.DeclareStyleable; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.resourceManagers.LocalResourceManager; +import org.jetbrains.android.resourceManagers.ModuleResourceManagers; +import org.jetbrains.android.resourceManagers.ResourceManager; +import org.jetbrains.android.util.AndroidResourceUtil; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.android.AndroidUtilKt; +import org.jetbrains.kotlin.psi.KtSimpleNameExpression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// TODO: ask for extension point +// this class is mostly copied from org.jetbrains.android.AndroidGotoDeclarationHandler +public class KotlinAndroidGotoDeclarationHandler implements GotoDeclarationHandler { + @Override + public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) { + KtSimpleNameExpression referenceExpression = GotoResourceHelperKt.getReferenceExpression(sourceElement); + if (referenceExpression == null) { + return null; + } + + AndroidFacet facet = AndroidUtilKt.getAndroidFacetForFile(referenceExpression); + if (facet == null) { + return null; + } + + AndroidResourceUtil.MyReferredResourceFieldInfo info = GotoResourceHelperKt.getInfo(referenceExpression, facet); + + if (info == null) return null; + + String nestedClassName = info.getClassName(); + String fieldName = info.getFieldName(); + List resourceList = new ArrayList(); + + if (info.isFromManifest()) { + collectManifestElements(nestedClassName, fieldName, facet, resourceList); + } + else { + ModuleResourceManagers managers = ModuleResourceManagers.getInstance(facet); + ResourceManager manager = info.isSystem() + ? managers.getSystemResourceManager(false) + : managers.getLocalResourceManager(); + if (manager == null) { + return null; + } + manager.collectLazyResourceElements(nestedClassName, fieldName, false, referenceExpression, resourceList); + + if (manager instanceof LocalResourceManager) { + LocalResourceManager lrm = (LocalResourceManager) manager; + + if (nestedClassName.equals(ResourceType.ATTR.getName())) { + for (Attr attr : lrm.findAttrs(fieldName)) { + resourceList.add(attr.getName().getXmlAttributeValue()); + } + } + else if (nestedClassName.equals(ResourceType.STYLEABLE.getName())) { + for (DeclareStyleable styleable : lrm.findStyleables(fieldName)) { + resourceList.add(styleable.getName().getXmlAttributeValue()); + } + + for (Attr styleable : lrm.findStyleableAttributesByFieldName(fieldName)) { + resourceList.add(styleable.getName().getXmlAttributeValue()); + } + } + } + } + + if (resourceList.size() > 1) { + // Sort to ensure the output is stable, and to prefer the base folders + Collections.sort(resourceList, AndroidResourceUtil.RESOURCE_ELEMENT_COMPARATOR); + } + + return resourceList.toArray(new PsiElement[resourceList.size()]); + } + + private static void collectManifestElements( + @NotNull String nestedClassName, + @NotNull String fieldName, + @NotNull AndroidFacet facet, + @NotNull List result + ) { + Manifest manifest = facet.getManifest(); + + if (manifest == null) { + return; + } + List list; + + if ("permission".equals(nestedClassName)) { + list = manifest.getPermissions(); + } + else if ("permission_group".equals(nestedClassName)) { + list = manifest.getPermissionGroups(); + } + else { + return; + } + for (ManifestElementWithRequiredName domElement : list) { + AndroidAttributeValue nameAttribute = domElement.getName(); + String name = nameAttribute.getValue(); + + if (AndroidUtils.equal(name, fieldName, false)) { + XmlElement psiElement = nameAttribute.getXmlAttributeValue(); + + if (psiElement != null) { + result.add(psiElement); + } + } + } + } + + @Override + public String getActionText(DataContext context) { + return null; + } +} diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleInspectionTest.kt.as31 b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleInspectionTest.kt.as31 new file mode 100644 index 00000000000..1912b347856 --- /dev/null +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleInspectionTest.kt.as31 @@ -0,0 +1,363 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.codeInsight.gradle + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemDescriptorBase +import com.intellij.openapi.util.Ref +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.InspectionTestUtil +import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl +import org.jetbrains.kotlin.idea.inspections.gradle.DeprecatedGradleDependencyInspection +import org.jetbrains.kotlin.idea.inspections.gradle.DifferentKotlinGradleVersionInspection +import org.jetbrains.kotlin.idea.inspections.gradle.DifferentStdlibGradleVersionInspection +import org.jetbrains.kotlin.idea.inspections.runInspection +import org.junit.Assert +import org.junit.Test + +class GradleInspectionTest : GradleImportingTestCase() { + @Test + fun testDifferentStdlibGradleVersion() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.2") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.3" + } + """ + ) + importProject() + + val tool = DifferentStdlibGradleVersionInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals("Plugin version (1.0.2) is not the same as library version (1.0.3)", problems.single()) + } + + @Test + fun testDifferentStdlibGradleVersionWithImplementation() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.2") + } + } + + apply plugin: 'kotlin' + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.0.3" + } + """ + ) + importProject() + + val tool = DifferentStdlibGradleVersionInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals("Plugin version (1.0.2) is not the same as library version (1.0.3)", problems.single()) + } + + @Test + fun testDifferentStdlibJre7GradleVersion() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + maven { + url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' + } + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.0-beta-17") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.0-beta-22" + } + """ + ) + importProject() + + val tool = DifferentStdlibGradleVersionInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals("Plugin version (1.1.0-beta-17) is not the same as library version (1.1.0-beta-22)", problems.single()) + } + + @Test + fun testDifferentStdlibJdk7GradleVersion() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + maven { + url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' + } + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.0-beta-17") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.1.0-beta-22" + } + """ + ) + importProject() + + val tool = DifferentStdlibGradleVersionInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals("Plugin version (1.1.0-beta-17) is not the same as library version (1.1.0-beta-22)", problems.single()) + } + + + @Test + fun testDifferentStdlibGradleVersionWithVariables() { + createProjectSubFile( + "gradle.properties", """ + |kotlin=1.0.1 + |lib_version=1.0.3""".trimMargin() + ) + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${'$'}kotlin") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: lib_version + } + """ + ) + importProject() + + val tool = DifferentStdlibGradleVersionInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals("Plugin version (1.0.1) is not the same as library version (1.0.3)", problems.single()) + } + + @Test + fun testDifferentKotlinGradleVersion() { + createProjectSubFile("gradle.properties", """test=1.0.1""") + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${'$'}{test}") + } + } + + apply plugin: 'kotlin' + """ + ) + importProject() + + val tool = DifferentKotlinGradleVersionInspection() + tool.testVersionMessage = "\$PLUGIN_VERSION" + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals( + "Kotlin version that is used for building with Gradle (1.0.1) differs from the one bundled into the IDE plugin (\$PLUGIN_VERSION)", + problems.single() + ) + } + + @Test + fun testJreInOldVersion() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.60") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:1.1.60" + } + """ + ) + importProject() + + val tool = DeprecatedGradleDependencyInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.isEmpty()) + } + + @Test + fun testJreIsDeprecated() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.60") + } + } + + apply plugin: 'kotlin' + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.0" + } + """ + ) + importProject() + + val tool = DeprecatedGradleDependencyInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals( + "kotlin-stdlib-jre7 is deprecated since 1.2.0 and should be replaced with kotlin-stdlib-jdk7", + problems.single() + ) + } + + @Test + fun testJreIsDeprecatedWithImplementation() { + val localFile = createProjectSubFile( + "build.gradle", """ + group 'Again' + version '1.0-SNAPSHOT' + + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.0") + } + } + + apply plugin: 'kotlin' + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.0" + } + """ + ) + importProject() + + val tool = DeprecatedGradleDependencyInspection() + val problems = getInspectionResult(tool, localFile) + + Assert.assertTrue(problems.size == 1) + Assert.assertEquals( + "kotlin-stdlib-jre7 is deprecated since 1.2.0 and should be replaced with kotlin-stdlib-jdk7", + problems.single() + ) + } + + fun getInspectionResult(tool: LocalInspectionTool, file: VirtualFile): List { + val resultRef = Ref>() + invokeTestRunnable { + val presentation = runInspection(tool, myProject, listOf(file)) + + val foundProblems = presentation.problemElements + .values + .mapNotNull { it as? ProblemDescriptorBase } + .map { it.descriptionTemplate } + + resultRef.set(foundProblems) + } + + return resultRef.get() + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/framework/JavaRuntimePresentationProvider.java.as31 b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/framework/JavaRuntimePresentationProvider.java.as31 new file mode 100644 index 00000000000..1ce931b7a12 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/framework/JavaRuntimePresentationProvider.java.as31 @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.framework; + +import com.intellij.framework.library.LibraryVersionProperties; +import com.intellij.openapi.roots.libraries.LibraryPresentationProvider; +import com.intellij.openapi.roots.libraries.LibraryProperties; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.idea.KotlinIcons; + +import javax.swing.*; +import java.util.List; + +public class JavaRuntimePresentationProvider extends LibraryPresentationProvider { + public static JavaRuntimePresentationProvider getInstance() { + return LibraryPresentationProvider.EP_NAME.findExtension(JavaRuntimePresentationProvider.class); + } + + protected JavaRuntimePresentationProvider() { + super(JavaRuntimeLibraryDescription.Companion.getKOTLIN_JAVA_RUNTIME_KIND()); + } + + @Nullable + @Override + public Icon getIcon(@Nullable LibraryProperties properties) { + return KotlinIcons.SMALL_LOGO; + } + + @Nullable + @Override + public LibraryVersionProperties detect(@NotNull List classesRoots) { + String version = JavaRuntimeDetectionUtil.getJavaRuntimeVersion(classesRoots); + return version == null ? null : new LibraryVersionProperties(version); + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt.as31 b/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt.as31 new file mode 100644 index 00000000000..d35ad586969 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt.as31 @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.reporter + +import com.intellij.diagnostic.ITNReporter +import com.intellij.openapi.diagnostic.IdeaLoggingEvent +import com.intellij.openapi.diagnostic.SubmittedReportInfo +import com.intellij.openapi.ui.Messages +import com.intellij.util.Consumer +import org.jetbrains.kotlin.idea.KotlinPluginUpdater +import org.jetbrains.kotlin.idea.KotlinPluginUtil +import org.jetbrains.kotlin.idea.PluginUpdateStatus +import org.jetbrains.kotlin.idea.actions.internal.KotlinInternalMode +import java.awt.Component + +/** + * We need to wrap ITNReporter for force showing or errors from kotlin plugin even from released version of IDEA. + */ +class KotlinReportSubmitter : ITNReporter() { + private var hasUpdate = false + private var hasLatestVersion = false + + override fun showErrorInRelease(event: IdeaLoggingEvent): Boolean { + val notificationEnabled = "disabled" != System.getProperty("kotlin.fatal.error.notification", "enabled") + return notificationEnabled && (!hasUpdate || KotlinInternalMode.enabled) + } + + override fun submit(events: Array, additionalInfo: String?, parentComponent: Component?, consumer: Consumer): Boolean { + if (hasUpdate) { + if (KotlinInternalMode.enabled) { + return super.submit(events, additionalInfo, parentComponent, consumer) + } + return true + } + + if (hasLatestVersion) { + return super.submit(events, additionalInfo, parentComponent, consumer) + } + + KotlinPluginUpdater.getInstance().runUpdateCheck { status -> + if (status is PluginUpdateStatus.Update) { + hasUpdate = true + if (parentComponent != null) { + + if (KotlinInternalMode.enabled) { + super.submit(events, additionalInfo, parentComponent, consumer) + } + + val rc = Messages.showDialog(parentComponent, + "You're running Kotlin plugin version ${KotlinPluginUtil.getPluginVersion()}, " + + "while the latest version is ${status.pluginDescriptor.version}", + "Update Kotlin Plugin", + arrayOf("Update", "Ignore"), + 0, Messages.getInformationIcon()) + if (rc == 0) { + KotlinPluginUpdater.getInstance().installPluginUpdate(status) + } + } + } + else { + hasLatestVersion = true + super.submit(events, additionalInfo, parentComponent, consumer) + } + false + } + return true + } +} diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java.as31 b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java.as31 new file mode 100644 index 00000000000..b01989a5a37 --- /dev/null +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java.as31 @@ -0,0 +1,863 @@ +package org.jetbrains.android.inspections.klint; + +import com.android.annotations.NonNull; +import com.android.builder.model.AndroidProject; +import com.android.builder.model.LintOptions; +import com.android.ide.common.repository.ResourceVisibilityLookup; +import com.android.ide.common.res2.AbstractResourceRepository; +import com.android.ide.common.res2.ResourceFile; +import com.android.ide.common.res2.ResourceItem; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.tools.idea.project.AndroidProjectInfo; +import com.android.tools.idea.project.AndroidProjectInfo; +import com.android.tools.idea.res.AppResourceRepository; +import com.android.tools.idea.res.LocalResourceRepository; +import com.android.tools.idea.res.ModuleResourceRepository; +import com.android.tools.idea.res.ProjectResourceRepository; +import com.android.tools.idea.sdk.IdeSdks; +import com.android.tools.klint.checks.ApiLookup; +import com.android.tools.klint.client.api.*; +import com.android.tools.klint.detector.api.*; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; +import com.intellij.analysis.AnalysisScope; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.event.DocumentListener; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.PathUtil; +import com.intellij.util.containers.HashMap; +import com.intellij.util.lang.UrlClassLoader; +import com.intellij.util.net.HttpConfigurable; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.sdk.AndroidSdkData; +import org.jetbrains.android.sdk.AndroidSdkType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.*; + +import static com.android.tools.klint.detector.api.TextFormat.RAW; +import static org.jetbrains.android.inspections.klint.IntellijLintIssueRegistry.CUSTOM_ERROR; +import static org.jetbrains.android.inspections.klint.IntellijLintIssueRegistry.CUSTOM_WARNING; + +/** + * Implementation of the {@linkplain LintClient} API for executing lint within the IDE: + * reading files, reporting issues, logging errors, etc. + */ +public class IntellijLintClient extends LintClient implements Disposable { + protected static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.IntellijLintClient"); + + @NonNull protected Project myProject; + @Nullable protected Map myModuleMap; + + public IntellijLintClient(@NonNull Project project) { + super(CLIENT_STUDIO); + myProject = project; + } + + /** Creates a lint client for batch inspections */ + public static IntellijLintClient forBatch(@NotNull Project project, + @NotNull Map>> problemMap, + @NotNull AnalysisScope scope, + @NotNull List issues) { + return new BatchLintClient(project, problemMap, scope, issues); + } + + /** + * Returns an {@link ApiLookup} service. + * + * @param project the project to use for locating the Android SDK + * @return an API lookup if one can be found + */ + @Nullable + public static ApiLookup getApiLookup(@NotNull Project project) { + return ApiLookup.get(new IntellijLintClient(project)); + } + + /** + * Creates a lint client used for in-editor single file lint analysis (e.g. background checking while user is editing.) + */ + public static IntellijLintClient forEditor(@NotNull State state) { + return new EditorLintClient(state); + } + + @Nullable + protected Module findModuleForLintProject(@NotNull Project project, + @NotNull com.android.tools.klint.detector.api.Project lintProject) { + if (myModuleMap != null) { + Module module = myModuleMap.get(lintProject); + if (module != null) { + return module; + } + } + final File dir = lintProject.getDir(); + final VirtualFile vDir = LocalFileSystem.getInstance().findFileByIoFile(dir); + return vDir != null ? ModuleUtilCore.findModuleForFile(vDir, project) : null; + } + + void setModuleMap(@Nullable Map moduleMap) { + myModuleMap = moduleMap; + } + + @NonNull + @Override + public Configuration getConfiguration(@NonNull com.android.tools.klint.detector.api.Project project, @Nullable final LintDriver driver) { + if (project.isGradleProject() && project.isAndroidProject() && !project.isLibrary()) { + AndroidProject model = project.getGradleProjectModel(); + if (model != null) { + try { + LintOptions lintOptions = model.getLintOptions(); + final Map overrides = lintOptions.getSeverityOverrides(); + if (overrides != null && !overrides.isEmpty()) { + return new DefaultConfiguration(this, project, null) { + @NonNull + @Override + public Severity getSeverity(@NonNull Issue issue) { + Integer severity = overrides.get(issue.getId()); + if (severity != null) { + switch (severity.intValue()) { + case LintOptions.SEVERITY_FATAL: + return Severity.FATAL; + case LintOptions.SEVERITY_ERROR: + return Severity.ERROR; + case LintOptions.SEVERITY_WARNING: + return Severity.WARNING; + case LintOptions.SEVERITY_INFORMATIONAL: + return Severity.INFORMATIONAL; + case LintOptions.SEVERITY_IGNORE: + default: + return Severity.IGNORE; + } + } + + // This is a LIST lookup. I should make this faster! + if (!getIssues().contains(issue) && (driver == null || !driver.isCustomIssue(issue))) { + return Severity.IGNORE; + } + + return super.getSeverity(issue); + } + }; + } + } catch (Exception e) { + LOG.error(e); + } + } + } + return new DefaultConfiguration(this, project, null) { + @Override + public boolean isEnabled(@NonNull Issue issue) { + if (getIssues().contains(issue) && super.isEnabled(issue)) { + return true; + } + + return driver != null && driver.isCustomIssue(issue); + } + }; + } + + @Override + public void report(@NonNull Context context, + @NonNull Issue issue, + @NonNull Severity severity, + @NonNull Location location, + @NonNull String message, + @NonNull TextFormat format) { + assert false : message; + } + + @NonNull protected List getIssues() { + return Collections.emptyList(); + } + + @Nullable + protected Module getModule() { + return null; + } + + /** + * Recursively calls {@link #report} on the secondary location of this error, if any, which in turn may call it on a third + * linked location, and so on.This is necessary since IntelliJ problems don't have secondary locations; instead, we create one + * problem for each location associated with the lint error. + */ + protected void reportSecondary(@NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, @NonNull Location location, + @NonNull String message, @NonNull TextFormat format) { + Location secondary = location.getSecondary(); + if (secondary != null) { + if (secondary.getMessage() != null) { + message = message + " (" + secondary.getMessage() + ")"; + } + report(context, issue, severity, secondary, message, format); + } + } + + @Override + public void log(@NonNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args) { + if (severity == Severity.ERROR || severity == Severity.FATAL) { + if (format != null) { + LOG.error(String.format(format, args), exception); + } else if (exception != null) { + LOG.error(exception); + } + } else if (severity == Severity.WARNING) { + if (format != null) { + LOG.warn(String.format(format, args), exception); + } else if (exception != null) { + LOG.warn(exception); + } + } else { + if (format != null) { + LOG.info(String.format(format, args), exception); + } else if (exception != null) { + LOG.info(exception); + } + } + } + + @Override + public XmlParser getXmlParser() { + return new DomPsiParser(this); + } + + @Nullable + @Override + public JavaParser getJavaParser(@Nullable com.android.tools.klint.detector.api.Project project) { + return new IdeaJavaParser(this, myProject); + } + + @NonNull + @Override + public List getJavaClassFolders(@NonNull com.android.tools.klint.detector.api.Project project) { + // todo: implement when class files checking detectors will be available + return Collections.emptyList(); + } + + @NonNull + @Override + public List getJavaLibraries(@NonNull com.android.tools.klint.detector.api.Project project, boolean includeProvided) { + // todo: implement + return Collections.emptyList(); + } + + @Override + @NonNull + public String readFile(@NonNull final File file) { + final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); + if (vFile == null) { + LOG.debug("Cannot find file " + file.getPath() + " in the VFS"); + return ""; + } + + return ApplicationManager.getApplication().runReadAction(new Computable() { + @Nullable + @Override + public String compute() { + final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(vFile); + if (psiFile == null) { + LOG.info("Cannot find file " + file.getPath() + " in the PSI"); + return null; + } + else { + return psiFile.getText(); + } + } + }); + } + + @Override + public void dispose() { + } + + @Nullable + @Override + public File getSdkHome() { + Module module = getModule(); + if (module != null) { + Sdk moduleSdk = ModuleRootManager.getInstance(module).getSdk(); + if (moduleSdk != null && moduleSdk.getSdkType() instanceof AndroidSdkType) { + String path = moduleSdk.getHomePath(); + if (path != null) { + File home = new File(path); + if (home.exists()) { + return home; + } + } + } + } + + File sdkHome = super.getSdkHome(); + if (sdkHome != null) { + return sdkHome; + } + + for (Module m : ModuleManager.getInstance(myProject).getModules()) { + Sdk moduleSdk = ModuleRootManager.getInstance(m).getSdk(); + if (moduleSdk != null) { + if (moduleSdk.getSdkType() instanceof AndroidSdkType) { + String path = moduleSdk.getHomePath(); + if (path != null) { + File home = new File(path); + if (home.exists()) { + return home; + } + } + } + } + } + + return IdeSdks.getInstance().getAndroidSdkPath(); + } + + @Nullable + @Override + public AndroidSdkHandler getSdk() { + if (mSdk == null) { + Module module = getModule(); + AndroidSdkHandler sdk = getLocalSdk(module); + if (sdk != null) { + mSdk = sdk; + } else { + for (Module m : ModuleManager.getInstance(myProject).getModules()) { + sdk = getLocalSdk(m); + if (sdk != null) { + mSdk = sdk; + break; + } + } + + if (mSdk == null) { + mSdk = super.getSdk(); + } + } + } + + return mSdk; + } + + @Nullable + private static AndroidSdkHandler getLocalSdk(@Nullable Module module) { + if (module != null) { + AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet != null) { + AndroidSdkData sdkData = AndroidSdkData.getSdkData(facet); // AS24 AndroidSdkData.getSdkData() + if (sdkData != null) { + return sdkData.getSdkHandler(); + } + } + } + + return null; + } + + @Override + public boolean isGradleProject(com.android.tools.klint.detector.api.Project project) { + Module module = getModule(); + if (module != null) { + AndroidFacet facet = AndroidFacet.getInstance(module); + return facet != null && facet.requiresAndroidModel(); + } + return AndroidProjectInfo.getInstance(this.myProject).requiresAndroidModel(); + } + + // Overridden such that lint doesn't complain about missing a bin dir property in the event + // that no SDK is configured + @Override + @Nullable + public File findResource(@NonNull String relativePath) { + File top = getSdkHome(); + if (top != null) { + File file = new File(top, relativePath); + if (file.exists()) { + return file; + } + } + + return null; + } + + @Nullable private static volatile String ourSystemPath; + + @Override + @Nullable + public File getCacheDir(boolean create) { + final String path = ourSystemPath != null ? ourSystemPath : (ourSystemPath = PathUtil.getCanonicalPath(PathManager.getSystemPath())); + File lint = new File(path, "lint"); + if (create && !lint.exists()) { + lint.mkdirs(); + } + return lint; + } + + @Override + public boolean isProjectDirectory(@NonNull File dir) { + return new File(dir, Project.DIRECTORY_STORE_FOLDER).exists(); + } + + private static List ourReportedCustomIssues; + + private static void recordCustomIssue(@NonNull Issue issue) { + if (ourReportedCustomIssues == null) { + ourReportedCustomIssues = Lists.newArrayList(); + } else if (ourReportedCustomIssues.contains(issue)) { + return; + } + ourReportedCustomIssues.add(issue); + } + + @Nullable + public static Issue findCustomIssue(@NonNull String errorMessage) { + if (ourReportedCustomIssues != null) { + // We stash the original id into the error message such that we can + // find it later + int begin = errorMessage.lastIndexOf('['); + int end = errorMessage.lastIndexOf(']'); + if (begin < end && begin != -1) { + String id = errorMessage.substring(begin + 1, end); + for (Issue issue : ourReportedCustomIssues) { + if (id.equals(issue.getId())) { + return issue; + } + } + } + } + + return null; + } + + /** + * A lint client used for in-editor single file lint analysis (e.g. background checking while user is editing.) + *

+ * Since this applies only to a given file and module, it can take some shortcuts over what the general + * {@link BatchLintClient} has to do. + * */ + private static class EditorLintClient extends IntellijLintClient { + private final State myState; + + public EditorLintClient(@NotNull State state) { + super(state.getModule().getProject()); + myState = state; + } + + @Nullable + @Override + protected Module getModule() { + return myState.getModule(); + } + + @NonNull + @Override + protected List getIssues() { + return myState.getIssues(); + } + + @Override + public void report(@NonNull Context context, + @NonNull Issue issue, + @NonNull Severity severity, + @NonNull Location location, + @NonNull String message, + @NonNull TextFormat format) { + if (location != null) { + final File file = location.getFile(); + final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); + + if (context.getDriver().isCustomIssue(issue)) { + // Record original issue id in the message (such that we can find + // it later, in #findCustomIssue) + message += " [" + issue.getId() + "]"; + recordCustomIssue(issue); + issue = Severity.WARNING.compareTo(severity) <= 0 ? CUSTOM_WARNING : CUSTOM_ERROR; + } + + if (myState.getMainFile().equals(vFile)) { + final Position start = location.getStart(); + final Position end = location.getEnd(); + + final TextRange textRange = start != null && end != null && start.getOffset() <= end.getOffset() + ? new TextRange(start.getOffset(), end.getOffset()) + : TextRange.EMPTY_RANGE; + + Severity configuredSeverity = severity != issue.getDefaultSeverity() ? severity : null; + message = format.convertTo(message, RAW); + myState.getProblems().add(new ProblemData(issue, message, textRange, configuredSeverity)); + } + + Location secondary = location.getSecondary(); + if (secondary != null && myState.getMainFile().equals(LocalFileSystem.getInstance().findFileByIoFile(secondary.getFile()))) { + reportSecondary(context, issue, severity, location, message, format); + } + } + } + + @Override + @NotNull + public String readFile(@NonNull File file) { + final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); + + if (vFile == null) { + try { + return Files.toString(file, Charsets.UTF_8); + } catch (IOException ioe) { + LOG.debug("Cannot find file " + file.getPath() + " in the VFS"); + return ""; + } + } + final String content = getFileContent(vFile); + + if (content == null) { + LOG.info("Cannot find file " + file.getPath() + " in the PSI"); + return ""; + } + return content; + } + + @Nullable + private String getFileContent(final VirtualFile vFile) { + if (Comparing.equal(myState.getMainFile(), vFile)) { + return myState.getMainFileContent(); + } + + return ApplicationManager.getApplication().runReadAction(new Computable() { + @Nullable + @Override + public String compute() { + final Module module = myState.getModule(); + final Project project = module.getProject(); + if (project.isDisposed()) { + return null; + } + + final PsiFile psiFile = PsiManager.getInstance(project).findFile(vFile); + + if (psiFile == null) { + return null; + } + final Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); + + if (document != null) { + final DocumentListener listener = new DocumentListener() { + @Override + public void beforeDocumentChange(DocumentEvent event) { + } + + @Override + public void documentChanged(DocumentEvent event) { + myState.markDirty(); + } + }; + document.addDocumentListener(listener, EditorLintClient.this); + } + return psiFile.getText(); + } + }); + } + + @NonNull + @Override + public List getJavaSourceFolders(@NonNull com.android.tools.klint.detector.api.Project project) { + final VirtualFile[] sourceRoots = ModuleRootManager.getInstance(myState.getModule()).getSourceRoots(false); + final List result = new ArrayList(sourceRoots.length); + + for (VirtualFile root : sourceRoots) { + result.add(new File(root.getPath())); + } + return result; + } + + @NonNull + @Override + public List getResourceFolders(@NonNull com.android.tools.klint.detector.api.Project project) { + AndroidFacet facet = AndroidFacet.getInstance(myState.getModule()); + if (facet != null) { + return IntellijLintUtils.getResourceDirectories(facet); + } + return super.getResourceFolders(project); + } + } + + /** Lint client used for batch operations */ + private static class BatchLintClient extends IntellijLintClient { + private final Map>> myProblemMap; + private final AnalysisScope myScope; + private final List myIssues; + + public BatchLintClient(@NotNull Project project, + @NotNull Map>> problemMap, + @NotNull AnalysisScope scope, + @NotNull List issues) { + super(project); + myProblemMap = problemMap; + myScope = scope; + myIssues = issues; + } + + @Nullable + @Override + protected Module getModule() { + // No default module + return null; + } + + @NonNull + @Override + protected List getIssues() { + return myIssues; + } + + @Override + public void report(@NonNull Context context, + @NonNull Issue issue, + @NonNull Severity severity, + @NonNull Location location, + @NonNull String message, + @NonNull TextFormat format) { + VirtualFile vFile = null; + File file = null; + + if (location != null) { + file = location.getFile(); + vFile = LocalFileSystem.getInstance().findFileByIoFile(file); + } + else if (context.getProject() != null) { + final Module module = findModuleForLintProject(myProject, context.getProject()); + + if (module != null) { + final AndroidFacet facet = AndroidFacet.getInstance(module); + vFile = facet != null ? AndroidRootUtil.getPrimaryManifestFile(facet) : null; + + if (vFile != null) { + file = new File(vFile.getPath()); + } + } + } + + boolean inScope = vFile != null && myScope.contains(vFile); + // In analysis batch mode, the AnalysisScope contains a specific set of virtual + // files, not directories, so any errors reported against a directory will not + // be considered part of the scope and therefore won't be reported. Correct + // for this. + if (!inScope && vFile != null && vFile.isDirectory()) { + if (myScope.getScopeType() == AnalysisScope.PROJECT) { + inScope = true; + } else if (myScope.getScopeType() == AnalysisScope.MODULE || + myScope.getScopeType() == AnalysisScope.MODULES) { + final Module module = findModuleForLintProject(myProject, context.getProject()); + if (module != null && myScope.containsModule(module)) { + inScope = true; + } + } + } + + if (inScope) { + if (context.getDriver().isCustomIssue(issue)) { + // Record original issue id in the message (such that we can find + // it later, in #findCustomIssue) + message += " [" + issue.getId() + "]"; + recordCustomIssue(issue); + issue = Severity.WARNING.compareTo(severity) <= 0 ? CUSTOM_WARNING : CUSTOM_ERROR; + } + + file = new File(PathUtil.getCanonicalPath(file.getPath())); + + Map> file2ProblemList = myProblemMap.get(issue); + if (file2ProblemList == null) { + file2ProblemList = new HashMap>(); + myProblemMap.put(issue, file2ProblemList); + } + + List problemList = file2ProblemList.get(file); + if (problemList == null) { + problemList = new ArrayList(); + file2ProblemList.put(file, problemList); + } + + TextRange textRange = TextRange.EMPTY_RANGE; + + if (location != null) { + final Position start = location.getStart(); + final Position end = location.getEnd(); + + if (start != null && end != null && start.getOffset() <= end.getOffset()) { + textRange = new TextRange(start.getOffset(), end.getOffset()); + } + } + Severity configuredSeverity = severity != issue.getDefaultSeverity() ? severity : null; + message = format.convertTo(message, RAW); + problemList.add(new ProblemData(issue, message, textRange, configuredSeverity)); + + if (location != null && location.getSecondary() != null) { + reportSecondary(context, issue, severity, location, message, format); + } + } + } + + @NonNull + @Override + public List getJavaSourceFolders(@NonNull com.android.tools.klint.detector.api.Project project) { + final Module module = findModuleForLintProject(myProject, project); + if (module == null) { + return Collections.emptyList(); + } + final VirtualFile[] sourceRoots = ModuleRootManager.getInstance(module).getSourceRoots(false); + final List result = new ArrayList(sourceRoots.length); + + for (VirtualFile root : sourceRoots) { + result.add(new File(root.getPath())); + } + return result; + } + + @NonNull + @Override + public List getResourceFolders(@NonNull com.android.tools.klint.detector.api.Project project) { + final Module module = findModuleForLintProject(myProject, project); + if (module != null) { + AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet != null) { + return IntellijLintUtils.getResourceDirectories(facet); + } + } + return super.getResourceFolders(project); + } + } + + @Override + public boolean checkForSuppressComments() { + return false; + } + + @Override + public boolean supportsProjectResources() { + return true; + } + + @Nullable + @Override + public AbstractResourceRepository getProjectResources(com.android.tools.klint.detector.api.Project project, boolean includeDependencies) { + final Module module = findModuleForLintProject(myProject, project); + if (module != null) { + AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet != null) { + return includeDependencies ? facet.getProjectResources(true) : facet.getModuleResources(true); + } + } + + return null; + } + + @Nullable + @Override + public URLConnection openConnection(@NonNull URL url) throws IOException { + return HttpConfigurable.getInstance().openConnection(url.toExternalForm()); + } + + @Override + public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) { + return UrlClassLoader.build().parent(parent).urls(urls).get(); + } + + @NonNull + @Override + public Location.Handle createResourceItemHandle(@NonNull ResourceItem item) { + XmlTag tag = LocalResourceRepository.getItemTag(myProject, item); + if (tag != null) { + ResourceFile source = item.getSource(); + assert source != null : item; + return new LocationHandle(source.getFile(), tag); + } + return super.createResourceItemHandle(item); + } + + @NonNull + @Override + public ResourceVisibilityLookup.Provider getResourceVisibilityProvider() { + Module module = getModule(); + if (module != null) { + AppResourceRepository appResources = AppResourceRepository.getAppResources(module, true); + if (appResources != null) { + ResourceVisibilityLookup.Provider provider = appResources.getResourceVisibilityProvider(); + if (provider != null) { + return provider; + } + } + } + return super.getResourceVisibilityProvider(); + } + + private static class LocationHandle implements Location.Handle, Computable { + private final File myFile; + private final XmlElement myNode; + private Object myClientData; + + public LocationHandle(File file, XmlElement node) { + myFile = file; + myNode = node; + } + + @NonNull + @Override + public Location resolve() { + if (!ApplicationManager.getApplication().isReadAccessAllowed()) { + return ApplicationManager.getApplication().runReadAction(this); + } + TextRange textRange = myNode.getTextRange(); + + // For elements, don't highlight the entire element range; instead, just + // highlight the element name + if (myNode instanceof XmlTag) { + String tag = ((XmlTag)myNode).getName(); + int index = myNode.getText().indexOf(tag); + if (index != -1) { + int start = textRange.getStartOffset() + index; + textRange = new TextRange(start, start + tag.length()); + } + } + + Position start = new DefaultPosition(-1, -1, textRange.getStartOffset()); + Position end = new DefaultPosition(-1, -1, textRange.getEndOffset()); + return Location.create(myFile, start, end); + } + + @Override + public Location compute() { + return resolve(); + } + + @Override + public void setClientData(@Nullable Object clientData) { + myClientData = clientData; + } + + @Override + @Nullable + public Object getClientData() { + return myClientData; + } + } +} diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java.as31 b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java.as31 new file mode 100644 index 00000000000..b81136262c0 --- /dev/null +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java.as31 @@ -0,0 +1,1132 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.android.inspections.klint; + +import com.android.annotations.NonNull; +import com.android.builder.model.*; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.tools.idea.gradle.project.model.AndroidModuleModel; +import com.android.tools.idea.gradle.util.GradleUtil; +import com.android.tools.idea.model.AndroidModel; +import com.android.tools.idea.model.AndroidModuleInfo; +import com.android.tools.klint.client.api.LintClient; +import com.android.tools.klint.detector.api.Project; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.roots.LibraryOrderEntry; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.OrderEntry; +import com.intellij.openapi.roots.OrderRootType; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ArrayUtil; +import com.intellij.util.graph.Graph; +import org.jetbrains.android.compiler.AndroidDexCompiler; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.facet.IdeaSourceProvider; +import org.jetbrains.android.sdk.AndroidPlatform; +import org.jetbrains.android.util.AndroidCommonUtils; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties; + +import java.io.File; +import java.util.*; + +import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT; +import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT; + +/** + * An {@linkplain IntellijLintProject} represents a lint project, which typically corresponds to a {@link Module}, + * but can also correspond to a library "project" such as an {@link AndroidLibrary}. + */ +class IntellijLintProject extends Project { + /** + * Whether we support running .class file checks. No class file checks are currently registered as inspections. + * Since IntelliJ doesn't perform background compilation (e.g. only parsing, so there are no bytecode checks) + * this might need some work before we enable it. + */ + public static final boolean SUPPORT_CLASS_FILES = false; + + protected AndroidVersion mMinSdkVersion; + protected AndroidVersion mTargetSdkVersion; + + IntellijLintProject(@NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir) { + super(client, dir, referenceDir); + } + + /** Creates a set of projects for the given IntelliJ modules */ + @NonNull + public static List create(@NonNull IntellijLintClient client, @Nullable List files, @NonNull Module... modules) { + List projects = Lists.newArrayList(); + + Map projectMap = Maps.newHashMap(); + Map moduleMap = Maps.newHashMap(); + Map libraryMap = Maps.newHashMap(); + if (files != null && !files.isEmpty()) { + // Wrap list with a mutable list since we'll be removing the files as we see them + files = Lists.newArrayList(files); + } + for (Module module : modules) { + addProjects(client, module, files, moduleMap, libraryMap, projectMap, projects); + } + + client.setModuleMap(projectMap); + + if (projects.size() > 1) { + // Partition the projects up such that we only return projects that aren't + // included by other projects (e.g. because they are library projects) + Set roots = new HashSet(projects); + for (Project project : projects) { + roots.removeAll(project.getAllLibraries()); + } + return Lists.newArrayList(roots); + } else { + return projects; + } + } + + /** + * Creates a project for a single file. Also optionally creates a main project for the file, if applicable. + * + * @param client the lint client + * @param file the file to create a project for + * @param module the module to create a project for + * @return a project for the file, as well as a project (or null) for the main Android module + */ + @NonNull + public static Pair createForSingleFile(@NonNull IntellijLintClient client, @Nullable VirtualFile file, @NonNull Module module) { + // TODO: Can make this method even more lightweight: we don't need to initialize anything in the project (source paths etc) + // other than the metadata necessary for this file's type + LintModuleProject project = createModuleProject(client, module); + LintModuleProject main = null; + Map projectMap = Maps.newHashMap(); + if (project != null) { + project.setDirectLibraries(Collections.emptyList()); + if (file != null) { + project.addFile(VfsUtilCore.virtualToIoFile(file)); + } + projectMap.put(project, module); + + // Supply a main project too, such that when you for example edit a file in a Java library, + // and lint asks for getMainProject().getMinSdk(), we return the min SDK of an application + // using the library, not "1" (the default for a module without a manifest) + if (!project.isAndroidProject()) { + Module androidModule = findAndroidModule(module); + if (androidModule != null) { + main = createModuleProject(client, androidModule); + if (main != null) { + projectMap.put(main, androidModule); + main.setDirectLibraries(Collections.singletonList(project)); + } + } + } + } + client.setModuleMap(projectMap); + + //noinspection ConstantConditions + return Pair.create(project,main); + } + + /** Find an Android module that depends on this module; prefer app modules over library modules */ + @Nullable + private static Module findAndroidModule(@NonNull final Module module) { + // Search for dependencies of this module + Graph graph = ApplicationManager.getApplication().runReadAction(new Computable>() { + @Override + public Graph compute() { + com.intellij.openapi.project.Project project = module.getProject(); + if (project.isDisposed()) { + return null; + } + return ModuleManager.getInstance(project).moduleGraph(); + } + }); + + if (graph == null) { + return null; + } + + Set facets = Sets.newHashSet(); + HashSet seen = Sets.newHashSet(); + seen.add(module); + addAndroidModules(facets, seen, graph, module); + + // Prefer Android app modules + for (AndroidFacet facet : facets) { + if (!facet.isLibraryProject()) { + return facet.getModule(); + } + } + + // Resort to library modules if no app module depends directly on it + if (!facets.isEmpty()) { + return facets.iterator().next().getModule(); + } + + return null; + } + + private static void addAndroidModules(Set androidFacets, Set seen, Graph graph, Module module) { + Iterator iterator = graph.getOut(module); + while (iterator.hasNext()) { + Module dep = iterator.next(); + AndroidFacet facet = AndroidFacet.getInstance(dep); + if (facet != null) { + androidFacets.add(facet); + } + + if (!seen.contains(dep)) { + seen.add(dep); + addAndroidModules(androidFacets, seen, graph, dep); + } + } + } + + /** + * Recursively add lint projects for the given module, and any other module or library it depends on, and also + * populate the reverse maps so we can quickly map from a lint project to a corresponding module/library (used + * by the lint client + */ + private static void addProjects(@NonNull LintClient client, + @NonNull Module module, + @Nullable List files, + @NonNull Map moduleMap, + @NonNull Map libraryMap, + @NonNull Map projectMap, + @NonNull List projects) { + if (moduleMap.containsKey(module)) { + return; + } + + LintModuleProject project = createModuleProject(client, module); + + if (project == null) { + // It's possible for the module to *depend* on Android code, e.g. in a Gradle + // project there will be a top-level non-Android module + List dependentFacets = AndroidUtils.getAllAndroidDependencies(module, false); + for (AndroidFacet dependentFacet : dependentFacets) { + addProjects(client, dependentFacet.getModule(), files, moduleMap, libraryMap, projectMap, projects); + } + return; + } + + projects.add(project); + moduleMap.put(module, project); + projectMap.put(project, module); + + if (processFileFilter(module, files, project)) { + // No need to process dependencies when doing single file analysis + return; + } + + List dependencies = Lists.newArrayList(); + // No, this shouldn't use getAllAndroidDependencies; we may have non-Android dependencies that this won't include + // (e.g. Java-only modules) + List dependentFacets = AndroidUtils.getAllAndroidDependencies(module, true); + for (AndroidFacet dependentFacet : dependentFacets) { + Project p = moduleMap.get(dependentFacet.getModule()); + if (p != null) { + dependencies.add(p); + } else { + addProjects(client, dependentFacet.getModule(), files, moduleMap, libraryMap, projectMap, dependencies); + } + } + + AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet != null) { + AndroidModuleModel androidModuleModel = AndroidModuleModel.get(facet); + if (androidModuleModel != null) { + addGradleLibraryProjects(client, files, libraryMap, projects, facet, androidModuleModel, project, projectMap, dependencies); + } + } + + project.setDirectLibraries(dependencies); + } + + /** + * Checks whether we have a file filter (e.g. a set of specific files to check in the module rather than all files, + * and if so, and if all the files have been found, returns true) + */ + private static boolean processFileFilter(@NonNull Module module, @Nullable List files, @NonNull LintModuleProject project) { + if (files != null && !files.isEmpty()) { + ListIterator iterator = files.listIterator(); + while (iterator.hasNext()) { + VirtualFile file = iterator.next(); + if (module.getModuleContentScope().accept(file)) { + project.addFile(VfsUtilCore.virtualToIoFile(file)); + iterator.remove(); + } + } + if (files.isEmpty()) { + // We're only scanning a subset of files (typically the current file in the editor); + // in that case, don't initialize all the libraries etc + project.setDirectLibraries(Collections.emptyList()); + return true; + } + } + return false; + } + + /** Creates a new module project */ + @Nullable + private static LintModuleProject createModuleProject(@NonNull LintClient client, @NonNull Module module) { + AndroidFacet facet = AndroidFacet.getInstance(module); + File dir; + + if (facet != null) { + final VirtualFile mainContentRoot = AndroidRootUtil.getMainContentRoot(facet); + + if (mainContentRoot == null) { + return null; + } + dir = new File(FileUtil.toSystemDependentName(mainContentRoot.getPath())); + } else { + String moduleDirPath = AndroidRootUtil.getModuleDirPath(module); + if (moduleDirPath == null) { + return null; + } + dir = new File(FileUtil.toSystemDependentName(moduleDirPath)); + } + LintModuleProject project = null; + if (facet == null) { + project = new LintModuleProject(client, dir, dir, module); + AndroidFacet f = findAndroidFacetInProject(module.getProject()); + if (f != null) { + project.mGradleProject = f.requiresAndroidModel(); + } + } + else if (facet.requiresAndroidModel()) { + AndroidModel androidModel = facet.getAndroidModel(); + if (androidModel instanceof AndroidModuleModel) { + project = new LintGradleProject(client, dir, dir, facet, (AndroidModuleModel)androidModel); + } else { + project = new LintAndroidModelProject(client, dir, dir, facet, androidModel); + } + } + else { + project = new LintAndroidProject(client, dir, dir, facet); + } + if (project != null) { + client.registerProject(dir, project); + } + return project; + } + + public static boolean hasAndroidModule(@NonNull com.intellij.openapi.project.Project project) { + return findAndroidFacetInProject(project) != null; + } + + @Nullable + private static AndroidFacet findAndroidFacetInProject(@NonNull com.intellij.openapi.project.Project project) { + ModuleManager moduleManager = ModuleManager.getInstance(project); + for (Module module : moduleManager.getModules()) { + AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet != null) { + return facet; + } + } + + return null; + } + + /** Adds any gradle library projects to the dependency list */ + private static void addGradleLibraryProjects(@NonNull LintClient client, + @Nullable List files, + @NonNull Map libraryMap, + @NonNull List projects, + @NonNull AndroidFacet facet, + @NonNull AndroidModuleModel AndroidModuleModel, + @NonNull LintModuleProject project, + @NonNull Map projectMap, + @NonNull List dependencies) { + Collection libraries = AndroidModuleModel.getMainArtifact().getDependencies().getLibraries(); + for (AndroidLibrary library : libraries) { + Project p = libraryMap.get(library); + if (p == null) { + File dir = library.getFolder(); + p = new LintGradleLibraryProject(client, dir, dir, library); + libraryMap.put(library, p); + projectMap.put(p, facet.getModule()); + projects.add(p); + + if (files != null) { + VirtualFile libraryDir = LocalFileSystem.getInstance().findFileByIoFile(dir); + if (libraryDir != null) { + ListIterator iterator = files.listIterator(); + while (iterator.hasNext()) { + VirtualFile file = iterator.next(); + if (VfsUtilCore.isAncestor(libraryDir, file, false)) { + project.addFile(VfsUtilCore.virtualToIoFile(file)); + iterator.remove(); + } + } + } + if (files.isEmpty()) { + files = null; // No more work in other modules + } + } + } + dependencies.add(p); + } + } + + @Override + protected void initialize() { + // NOT calling super: super performs ADT/ant initialization. Here we want to use + // the gradle data instead + } + + protected static boolean depsDependsOn(@NonNull Project project, @NonNull String artifact) { + // Checks project dependencies only; used when there is no model + for (Project dependency : project.getDirectLibraries()) { + Boolean b = dependency.dependsOn(artifact); + if (b != null && b) { + return true; + } + } + + return false; + } + + private static class LintModuleProject extends IntellijLintProject { + private Module myModule; + + public void setDirectLibraries(List libraries) { + mDirectLibraries = libraries; + } + + private LintModuleProject(@NonNull LintClient client, @NonNull File dir, @NonNull File referenceDir, Module module) { + super(client, dir, referenceDir); + myModule = module; + } + + @Override + public boolean isAndroidProject() { + return false; + } + + @NonNull + @Override + public List getJavaSourceFolders() { + if (mJavaSourceFolders == null) { + VirtualFile[] sourceRoots = ModuleRootManager.getInstance(myModule).getSourceRoots(false); + List dirs = new ArrayList(sourceRoots.length); + for (VirtualFile root : sourceRoots) { + dirs.add(new File(root.getPath())); + } + mJavaSourceFolders = dirs; + } + + return mJavaSourceFolders; + } + + @NonNull + @Override + public List getTestSourceFolders() { + if (mTestSourceFolders == null) { + ModuleRootManager manager = ModuleRootManager.getInstance(myModule); + VirtualFile[] sourceRoots = manager.getSourceRoots(false); + VirtualFile[] sourceAndTestRoots = manager.getSourceRoots(true); + List dirs = new ArrayList(sourceAndTestRoots.length); + for (VirtualFile root : sourceAndTestRoots) { + if (!ArrayUtil.contains(root, sourceRoots)) { + dirs.add(new File(root.getPath())); + } + } + mTestSourceFolders = dirs; + } + return mTestSourceFolders; + } + + @NonNull + @Override + public List getJavaClassFolders() { + if (SUPPORT_CLASS_FILES) { + if (mJavaClassFolders == null) { + VirtualFile folder = AndroidDexCompiler.getOutputDirectoryForDex(myModule); + if (folder != null) { + mJavaClassFolders = Collections.singletonList(VfsUtilCore.virtualToIoFile(folder)); + } else { + mJavaClassFolders = Collections.emptyList(); + } + } + + return mJavaClassFolders; + } + + return Collections.emptyList(); + } + + @NonNull + @Override + public List getJavaLibraries(boolean includeProvided) { + if (SUPPORT_CLASS_FILES) { + if (mJavaLibraries == null) { + mJavaLibraries = Lists.newArrayList(); + + final OrderEntry[] entries = ModuleRootManager.getInstance(myModule).getOrderEntries(); + // loop in the inverse order to resolve dependencies on the libraries, so that if a library + // is required by two higher level libraries it can be inserted in the correct place + + for (int i = entries.length - 1; i >= 0; i--) { + final OrderEntry orderEntry = entries[i]; + if (orderEntry instanceof LibraryOrderEntry) { + LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry)orderEntry; + VirtualFile[] classes = libraryOrderEntry.getRootFiles(OrderRootType.CLASSES); + if (classes != null) { + for (VirtualFile file : classes) { + mJavaLibraries.add(VfsUtilCore.virtualToIoFile(file)); + } + } + } + } + } + + return mJavaLibraries; + } + + return Collections.emptyList(); + } + } + + /** Wraps an Android module */ + private static class LintAndroidProject extends LintModuleProject { + protected final AndroidFacet myFacet; + + private LintAndroidProject(@NonNull LintClient client, @NonNull File dir, @NonNull File referenceDir, @NonNull AndroidFacet facet) { + super(client, dir, referenceDir, facet.getModule()); + myFacet = facet; + + mGradleProject = false; + mLibrary = myFacet.isLibraryProject(); + + AndroidPlatform platform = AndroidPlatform.getInstance(myFacet.getModule()); + if (platform != null) { + mBuildSdk = platform.getApiLevel(); + } + } + + @Override + public boolean isAndroidProject() { + return true; + } + + @NonNull + @Override + public String getName() { + return myFacet.getModule().getName(); + } + + @Override + @NonNull + public List getManifestFiles() { + if (mManifestFiles == null) { + VirtualFile manifestFile = AndroidRootUtil.getPrimaryManifestFile(myFacet); + if (manifestFile != null) { + mManifestFiles = Collections.singletonList(VfsUtilCore.virtualToIoFile(manifestFile)); + } else { + mManifestFiles = Collections.emptyList(); + } + } + + return mManifestFiles; + } + + @NonNull + @Override + public List getProguardFiles() { + if (mProguardFiles == null) { + final JpsAndroidModuleProperties properties = myFacet.getProperties(); + + if (properties.RUN_PROGUARD) { + final List urls = properties.myProGuardCfgFiles; + + if (!urls.isEmpty()) { + mProguardFiles = new ArrayList(); + + for (String osPath : AndroidUtils.urlsToOsPaths(urls, null)) { + if (!osPath.contains(AndroidCommonUtils.SDK_HOME_MACRO)) { + mProguardFiles.add(new File(osPath)); + } + } + } + } + + if (mProguardFiles == null) { + mProguardFiles = Collections.emptyList(); + } + } + + return mProguardFiles; + } + + @NonNull + @Override + public List getResourceFolders() { + if (mResourceFolders == null) { + List folders = myFacet.getResourceFolderManager().getFolders(); + List dirs = Lists.newArrayListWithExpectedSize(folders.size()); + for (VirtualFile folder : folders) { + dirs.add(VfsUtilCore.virtualToIoFile(folder)); + } + mResourceFolders = dirs; + } + + return mResourceFolders; + } + + @Nullable + @Override + public Boolean dependsOn(@NonNull String artifact) { + if (SUPPORT_LIB_ARTIFACT.equals(artifact)) { + if (mSupportLib == null) { + final OrderEntry[] entries = ModuleRootManager.getInstance(myFacet.getModule()).getOrderEntries(); + libraries: + for (int i = entries.length - 1; i >= 0; i--) { + final OrderEntry orderEntry = entries[i]; + if (orderEntry instanceof LibraryOrderEntry) { + LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry)orderEntry; + VirtualFile[] classes = libraryOrderEntry.getRootFiles(OrderRootType.CLASSES); + if (classes != null) { + for (VirtualFile file : classes) { + if (file.getName().equals("android-support-v4.jar")) { + mSupportLib = true; + break libraries; + + } + } + } + } + } + if (mSupportLib == null) { + mSupportLib = depsDependsOn(this, artifact); + } + } + return mSupportLib; + } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { + if (mSupportLib == null) { + final OrderEntry[] entries = ModuleRootManager.getInstance(myFacet.getModule()).getOrderEntries(); + libraries: + for (int i = entries.length - 1; i >= 0; i--) { + final OrderEntry orderEntry = entries[i]; + if (orderEntry instanceof LibraryOrderEntry) { + LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry)orderEntry; + VirtualFile[] classes = libraryOrderEntry.getRootFiles(OrderRootType.CLASSES); + if (classes != null) { + for (VirtualFile file : classes) { + if (file.getName().equals("appcompat-v7.jar")) { + mSupportLib = true; + break libraries; + + } + } + } + } + } + if (mSupportLib == null) { + mSupportLib = depsDependsOn(this, artifact); + } + } + return mSupportLib; + } else { + return super.dependsOn(artifact); + } + } + } + + private static class LintAndroidModelProject extends LintAndroidProject { + private final AndroidModel myAndroidModel; + + private LintAndroidModelProject( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidFacet facet, + @NonNull AndroidModel androidModel) { + super(client, dir, referenceDir, facet); + myAndroidModel = androidModel; + } + + @Nullable + @Override + public String getPackage() { + String manifestPackage = super.getPackage(); + // For now, lint only needs the manifest package; not the potentially variant specific + // package. As part of the Gradle work on the Lint API we should make two separate + // package lookup methods -- one for the manifest package, one for the build package + if (manifestPackage != null) { + return manifestPackage; + } + + return myAndroidModel.getApplicationId(); + } + + @NonNull + @Override + public AndroidVersion getMinSdkVersion() { + if (mMinSdkVersion == null) { + mMinSdkVersion = AndroidModuleInfo.getInstance(myFacet).getMinSdkVersion(); // AS24 getInstance() + } + return mMinSdkVersion; + } + + @NonNull + @Override + public AndroidVersion getTargetSdkVersion() { + if (mTargetSdkVersion == null) { + mTargetSdkVersion = AndroidModuleInfo.getInstance(myFacet).getTargetSdkVersion(); // AS24 getInstance() + } + + return mTargetSdkVersion; + } + } + + private static class LintGradleProject extends LintAndroidModelProject { + private final AndroidModuleModel myAndroidModuleModel; + + /** + * Creates a new Project. Use one of the factory methods to create. + */ + private LintGradleProject( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidFacet facet, + @NonNull AndroidModuleModel AndroidModuleModel) { + super(client, dir, referenceDir, facet, AndroidModuleModel); + mGradleProject = true; + mMergeManifests = true; + myAndroidModuleModel = AndroidModuleModel; + } + + @NonNull + @Override + public List getManifestFiles() { + if (mManifestFiles == null) { + mManifestFiles = Lists.newArrayList(); + File mainManifest = myFacet.getMainSourceProvider().getManifestFile(); + if (mainManifest.exists()) { + mManifestFiles.add(mainManifest); + } + + List flavorSourceProviders = myAndroidModuleModel.getFlavorSourceProviders(); + if (flavorSourceProviders != null) { + for (SourceProvider provider : flavorSourceProviders) { + File manifestFile = provider.getManifestFile(); + if (manifestFile.exists()) { + mManifestFiles.add(manifestFile); + } + } + } + + SourceProvider multiProvider = myAndroidModuleModel.getMultiFlavorSourceProvider(); + if (multiProvider != null) { + File manifestFile = multiProvider.getManifestFile(); + if (manifestFile.exists()) { + mManifestFiles.add(manifestFile); + } + } + + SourceProvider buildTypeSourceProvider = myAndroidModuleModel.getBuildTypeSourceProvider(); + if (buildTypeSourceProvider != null) { + File manifestFile = buildTypeSourceProvider.getManifestFile(); + if (manifestFile.exists()) { + mManifestFiles.add(manifestFile); + } + } + + SourceProvider variantProvider = myAndroidModuleModel.getVariantSourceProvider(); + if (variantProvider != null) { + File manifestFile = variantProvider.getManifestFile(); + if (manifestFile.exists()) { + mManifestFiles.add(manifestFile); + } + } + } + + return mManifestFiles; + } + + @NonNull + @Override + public List getAssetFolders() { + if (mAssetFolders == null) { + mAssetFolders = Lists.newArrayList(); + for (SourceProvider provider : IdeaSourceProvider.getAllSourceProviders(myFacet)) { + Collection dirs = provider.getAssetsDirectories(); + for (File dir : dirs) { + if (dir.exists()) { // model returns path whether or not it exists + mAssetFolders.add(dir); + } + } + } + } + + return mAssetFolders; + } + + @NonNull + @Override + public List getProguardFiles() { + if (mProguardFiles == null) { + if (myFacet.requiresAndroidModel()) { + // TODO: b/22928250 + AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet); + if (androidModel != null) { + ProductFlavor flavor = androidModel.getAndroidProject().getDefaultConfig().getProductFlavor(); + mProguardFiles = Lists.newArrayList(); + for (File file : flavor.getProguardFiles()) { + if (file.exists()) { + mProguardFiles.add(file); + } + } + try { + for (File file : flavor.getConsumerProguardFiles()) { + if (file.exists()) { + mProguardFiles.add(file); + } + } + } catch (Throwable t) { + // On some models, this threw + // org.gradle.tooling.model.UnsupportedMethodException: Unsupported method: BaseConfig.getConsumerProguardFiles(). + // Playing it safe for a while. + } + } + } + + if (mProguardFiles == null) { + mProguardFiles = Collections.emptyList(); + } + } + + return mProguardFiles; + } + + @NonNull + @Override + public List getJavaClassFolders() { + if (SUPPORT_CLASS_FILES) { + if (mJavaClassFolders == null) { + // Overridden because we don't synchronize the gradle output directory to + // the AndroidDexCompiler settings the way java source roots are mapped into + // the module content root settings + File dir = myAndroidModuleModel.getMainArtifact().getClassesFolder(); + if (dir != null) { + mJavaClassFolders = Collections.singletonList(dir); + } else { + mJavaClassFolders = Collections.emptyList(); + } + } + + return mJavaClassFolders; + } + + return Collections.emptyList(); + } + + private static boolean sProvidedAvailable = true; + + @NonNull + @Override + public List getJavaLibraries(boolean includeProvided) { + if (SUPPORT_CLASS_FILES) { + if (mJavaLibraries == null) { + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + Collection libs = myAndroidModuleModel.getMainArtifact().getDependencies().getJavaLibraries(); + mJavaLibraries = Lists.newArrayListWithExpectedSize(libs.size()); + for (JavaLibrary lib : libs) { + if (!includeProvided) { + if (sProvidedAvailable) { + // Method added in 1.4-rc1; gracefully handle running with + // older plugins + try { + if (lib.isProvided()) { + continue; + } + } + catch (Throwable t) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + sProvidedAvailable = false; // don't try again + } + } + } + + File jar = lib.getJarFile(); + if (jar.exists()) { + mJavaLibraries.add(jar); + } + } + } else { + mJavaLibraries = super.getJavaLibraries(includeProvided); + } + } + return mJavaLibraries; + } + + return Collections.emptyList(); + } + + @Override + public int getBuildSdk() { + // TODO: b/22928250 + AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet); + if (androidModel != null) { + String compileTarget = androidModel.getAndroidProject().getCompileTarget(); + AndroidVersion version = AndroidTargetHash.getPlatformVersion(compileTarget); + if (version != null) { + return version.getFeatureLevel(); + } + } + + AndroidPlatform platform = AndroidPlatform.getInstance(myFacet.getModule()); + if (platform != null) { + return platform.getApiVersion().getFeatureLevel(); + } + + return super.getBuildSdk(); + } + + @Nullable + @Override + public AndroidProject getGradleProjectModel() { + // TODO: b/22928250 + AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet); + if (androidModel != null) { + return androidModel.getAndroidProject(); + } + + return null; + } + + @Nullable + @Override + public Variant getCurrentVariant() { + // TODO: b/22928250 + AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet); + if (androidModel != null) { + return androidModel.getSelectedVariant(); + } + + return null; + } + + @Nullable + @Override + public AndroidLibrary getGradleLibraryModel() { + return null; + } + + @Nullable + @Override + public Boolean dependsOn(@NonNull String artifact) { + // TODO: b/22928250 + AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet); + + if (SUPPORT_LIB_ARTIFACT.equals(artifact)) { + if (mSupportLib == null) { + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + mSupportLib = GradleUtil.dependsOn(androidModel, artifact); + } else { + mSupportLib = depsDependsOn(this, artifact); + } + } + return mSupportLib; + } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { + if (mAppCompat == null) { + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + mAppCompat = GradleUtil.dependsOn(androidModel, artifact); + } else { + mAppCompat = depsDependsOn(this, artifact); + } + } + return mAppCompat; + } else { + // Some other (not yet directly cached result) + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null + && GradleUtil.dependsOn(androidModel, artifact)) { + return true; + } + + return super.dependsOn(artifact); + } + } + } + + private static class LintGradleLibraryProject extends IntellijLintProject { + private final AndroidLibrary myLibrary; + + private LintGradleLibraryProject(@NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidLibrary library) { + super(client, dir, referenceDir); + myLibrary = library; + + mLibrary = true; + mMergeManifests = true; + mReportIssues = false; + mGradleProject = true; + mDirectLibraries = Collections.emptyList(); + } + + @NonNull + @Override + public List getManifestFiles() { + if (mManifestFiles == null) { + File manifest = myLibrary.getManifest(); + if (manifest.exists()) { + mManifestFiles = Collections.singletonList(manifest); + } else { + mManifestFiles = Collections.emptyList(); + } + } + + return mManifestFiles; + } + + @NonNull + @Override + public List getProguardFiles() { + if (mProguardFiles == null) { + File proguardRules = myLibrary.getProguardRules(); + if (proguardRules.exists()) { + mProguardFiles = Collections.singletonList(proguardRules); + } else { + mProguardFiles = Collections.emptyList(); + } + } + + return mProguardFiles; + } + + @NonNull + @Override + public List getResourceFolders() { + if (mResourceFolders == null) { + File folder = myLibrary.getResFolder(); + if (folder.exists()) { + mResourceFolders = Collections.singletonList(folder); + } else { + mResourceFolders = Collections.emptyList(); + } + } + + return mResourceFolders; + } + + @NonNull + @Override + public List getJavaSourceFolders() { + return Collections.emptyList(); + } + + @NonNull + @Override + public List getJavaClassFolders() { + return Collections.emptyList(); + } + + private static boolean sOptionalAvailable = true; + + @NonNull + @Override + public List getJavaLibraries(boolean includeProvided) { + if (SUPPORT_CLASS_FILES) { + if (!includeProvided) { + if (sOptionalAvailable) { + // Method added in 1.4-rc1; gracefully handle running with + // older plugins + try { + if (myLibrary.isOptional()) { + return Collections.emptyList(); + } + } + catch (Throwable t) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + sOptionalAvailable = false; // don't try again + } + } + } + + if (mJavaLibraries == null) { + mJavaLibraries = Lists.newArrayList(); + File jarFile = myLibrary.getJarFile(); + if (jarFile.exists()) { + mJavaLibraries.add(jarFile); + } + + for (File local : myLibrary.getLocalJars()) { + if (local.exists()) { + mJavaLibraries.add(local); + } + } + } + + return mJavaLibraries; + } + + return Collections.emptyList(); + } + + @Nullable + @Override + public AndroidProject getGradleProjectModel() { + return null; + } + + @Nullable + @Override + public AndroidLibrary getGradleLibraryModel() { + return myLibrary; + } + + @Nullable + @Override + public Boolean dependsOn(@NonNull String artifact) { + if (SUPPORT_LIB_ARTIFACT.equals(artifact)) { + if (mSupportLib == null) { + mSupportLib = GradleUtil.dependsOn(myLibrary, artifact, true); + } + return mSupportLib; + } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { + if (mAppCompat == null) { + mAppCompat = GradleUtil.dependsOn(myLibrary, artifact, true); + } + return mAppCompat; + } else { + // Some other (not yet directly cached result) + if (GradleUtil.dependsOn(myLibrary, artifact, true)) { + return true; + } + + return super.dependsOn(artifact); + } + } + } +}