summaryrefslogtreecommitdiff
path: root/android-lint/src
diff options
context:
space:
mode:
authorKun Shen <kunshen@google.com>2021-10-11 11:02:40 -0700
committerKun Shen <kunshen@google.com>2021-10-11 20:00:16 +0000
commitb9ade51b80d570691e8e6db6493323dca3cd9dfb (patch)
tree0072778e924affffb88a3cb0b7532cb245ed8c97 /android-lint/src
parentb8d89e0ae887da5cebf858023dfbab7dcf26bd60 (diff)
downloadidea-b9ade51b80d570691e8e6db6493323dca3cd9dfb.tar.gz
Clean up code
Here, `WrongThreadInterproceduralAction` and `CallGraphAction` are moved to `intellij.android.lint` module. Bug: n/a Test: n/a Change-Id: If8f530de53d9a8fa48034da6eb429950fdce3180
Diffstat (limited to 'android-lint/src')
-rw-r--r--android-lint/src/META-INF/android-lint-plugin.xml5
-rw-r--r--android-lint/src/com/android/tools/idea/lint/actions/CallGraphAction.kt189
-rw-r--r--android-lint/src/com/android/tools/idea/lint/actions/WrongThreadInterproceduralAction.kt81
3 files changed, 275 insertions, 0 deletions
diff --git a/android-lint/src/META-INF/android-lint-plugin.xml b/android-lint/src/META-INF/android-lint-plugin.xml
index 905a6e8cef1..187c6736ba8 100644
--- a/android-lint/src/META-INF/android-lint-plugin.xml
+++ b/android-lint/src/META-INF/android-lint-plugin.xml
@@ -378,4 +378,9 @@
<extensions defaultExtensionNs="com.android.tools.idea.lint.common">
<lintIdeSupport implementation="com.android.tools.idea.lint.AndroidLintIdeSupport"/>
</extensions>
+
+ <actions>
+ <action id="Project.CallGraph" internal="true" text="Contextual Call Paths" class="com.android.tools.idea.lint.actions.CallGraphAction"/>
+ <action id="Project.InterproceduralThreadAnnotations" internal="true" text="Interprocedural Thread Annotation Checker" class="com.android.tools.idea.lint.actions.WrongThreadInterproceduralAction"/>
+ </actions>
</idea-plugin>
diff --git a/android-lint/src/com/android/tools/idea/lint/actions/CallGraphAction.kt b/android-lint/src/com/android/tools/idea/lint/actions/CallGraphAction.kt
new file mode 100644
index 00000000000..2ec711bbdbb
--- /dev/null
+++ b/android-lint/src/com/android/tools/idea/lint/actions/CallGraphAction.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 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 com.android.tools.idea.lint.actions
+
+import com.android.tools.lint.detector.api.interprocedural.*
+import com.google.common.collect.HashMultimap
+import com.google.common.collect.Multimap
+import com.intellij.analysis.AnalysisScope
+import com.intellij.ide.hierarchy.*
+import com.intellij.ide.hierarchy.actions.BrowseHierarchyActionBase
+import com.intellij.ide.hierarchy.call.CallHierarchyNodeDescriptor
+import com.intellij.ide.util.treeView.NodeDescriptor
+import com.intellij.openapi.actionSystem.*
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.components.ServiceManager
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.project.Project
+import com.intellij.psi.*
+import com.intellij.psi.util.PsiTreeUtil
+import com.intellij.ui.PopupHandler
+import org.jetbrains.uast.UFile
+import org.jetbrains.uast.UastContext
+import org.jetbrains.uast.convertWithParent
+import org.jetbrains.uast.visitor.UastVisitor
+import java.util.Comparator
+import javax.swing.JTree
+import kotlin.collections.ArrayList
+
+/** Creates a collection of UFiles from a project and scope. */
+fun UastVisitor.visitAll(project: Project, scope: AnalysisScope): Collection<UFile> {
+ val res = ArrayList<UFile>()
+ val uastContext = ServiceManager.getService(project, UastContext::class.java)
+ scope.accept { virtualFile ->
+ if (!uastContext.isFileSupported(virtualFile.name)) return@accept true
+ val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return@accept true
+ val file = uastContext.convertWithParent<UFile>(psiFile) ?: return@accept true
+ file.accept(this)
+ true
+ }
+ return res
+}
+
+// TODO: Improve node descriptor for lambdas, and show intermediate call expression nodes.
+
+class ContextualCallPathTreeStructure(
+ project: Project,
+ val graph: ContextualCallGraph,
+ element: PsiElement,
+ private val reverseEdges: Boolean
+) :
+ HierarchyTreeStructure(
+ project,
+ CallHierarchyNodeDescriptor(project, null, element, true, false)) {
+
+ private val reachableContextualNodes: Multimap<HierarchyNodeDescriptor, ContextualEdge> = HashMultimap.create()
+
+ init {
+ val initialEdges = graph.contextualNodes
+ .filter { it.node.target.element.psi == element }
+ .map { ContextualEdge(it, it.node.target.element) }
+ reachableContextualNodes.putAll(myBaseDescriptor, initialEdges)
+ }
+
+ override fun buildChildren(descriptor: HierarchyNodeDescriptor): Array<Any> {
+ return reachableContextualNodes[descriptor]
+ .flatMap {
+ // Get neighboring contextual nodes.
+ if (reverseEdges) graph.inEdges(it.contextualNode)
+ else graph.outEdges(it.contextualNode)
+ }
+ .groupBy { it.contextualNode.node }
+ .mapNotNull { (node, contextNodes) ->
+ node.target.element.psi?.let { Pair(it, contextNodes) }
+ }
+ .map { (psi, contextNodes) ->
+ val nbrDescriptor = CallHierarchyNodeDescriptor(myProject, descriptor, psi, false, false)
+ reachableContextualNodes.putAll(nbrDescriptor, contextNodes)
+ nbrDescriptor
+ }
+ .toTypedArray()
+ }
+}
+
+// Note: This class is similar to CallHierarchyBrowser, but supports arbitrary PSI elements (not just PsiMethod).
+open class ContextualCallPathBrowser(
+ project: Project,
+ val graph: ContextualCallGraph,
+ element: PsiElement
+) : CallHierarchyBrowserBase(project, element) {
+
+ override fun createHierarchyTreeStructure(kind: String, psiElement: PsiElement): HierarchyTreeStructure {
+ val reverseEdges = kind == CallHierarchyBrowserBase.CALLER_TYPE
+ return ContextualCallPathTreeStructure(myProject, graph, psiElement, reverseEdges)
+ }
+
+ override fun createTrees(typeToTreeMap: MutableMap<in String, in JTree>) {
+ val group = ActionManager.getInstance().getAction(IdeActions.GROUP_CALL_HIERARCHY_POPUP) as ActionGroup
+ val baseOnThisMethodAction = BaseOnThisMethodAction()
+ val kinds = arrayOf(
+ CallHierarchyBrowserBase.CALLEE_TYPE,
+ CallHierarchyBrowserBase.CALLER_TYPE)
+ for (kind in kinds) {
+ val tree = createTree(false)
+ PopupHandler.installPopupHandler(tree, group, ActionPlaces.CALL_HIERARCHY_VIEW_POPUP, ActionManager.getInstance())
+ baseOnThisMethodAction
+ .registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_CALL_HIERARCHY).shortcutSet, tree)
+ typeToTreeMap[kind] = tree
+ }
+ }
+
+ override fun getElementFromDescriptor(descriptor: HierarchyNodeDescriptor) = descriptor.psiElement
+
+ override fun isApplicableElement(element: PsiElement) = when (element) {
+ is PsiMethod,
+ is PsiLambdaExpression,
+ is PsiClass -> true
+ else -> false
+ }
+
+ override fun getComparator(): Comparator<NodeDescriptor<*>> = JavaHierarchyUtil.getComparator(myProject)
+}
+
+class ContextualCallPathProvider(val graph: ContextualCallGraph) : HierarchyProvider {
+
+ override fun getTarget(dataContext: DataContext): PsiElement? {
+ val element = CommonDataKeys.PSI_ELEMENT.getData(dataContext)
+ return PsiTreeUtil.getNonStrictParentOfType(element,
+ PsiMethod::class.java,
+ PsiLambdaExpression::class.java,
+ PsiClass::class.java)
+ }
+
+ override fun createHierarchyBrowser(target: PsiElement) = ContextualCallPathBrowser(target.project, graph, target)
+
+ override fun browserActivated(hierarchyBrowser: HierarchyBrowser) {
+ (hierarchyBrowser as ContextualCallPathBrowser).changeView(CallHierarchyBrowserBase.CALLEE_TYPE)
+ }
+}
+
+class CallGraphAction : AnAction() {
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ PsiDocumentManager.getInstance(project).commitAllDocuments() // Prevents problems with smart pointers creation.
+
+ ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Building contextual call graph", true) {
+ override fun run(indicator: ProgressIndicator) {
+ ApplicationManager.getApplication().runReadAction {
+ val scope = AnalysisScope(project)
+ val cha = ClassHierarchyVisitor()
+ .apply { visitAll(project, scope) }
+ .classHierarchy
+ val receiverEval = IntraproceduralDispatchReceiverVisitor(cha)
+ .apply { visitAll(project, scope) }
+ .receiverEval
+ val callGraph = CallGraphVisitor(receiverEval, cha)
+ .apply { visitAll(project, scope) }
+ .callGraph
+ val contextualGraph = callGraph.buildContextualCallGraph(receiverEval)
+
+ val provider = ContextualCallPathProvider(contextualGraph)
+ val target = provider.getTarget(e.dataContext) ?: return@runReadAction
+
+ ApplicationManager.getApplication().invokeLater({
+ if (!project.isDisposed) {
+ BrowseHierarchyActionBase.createAndAddToPanel(project, provider, target)
+ }
+ }, ModalityState.NON_MODAL)
+ }
+ }
+ })
+ }
+}
diff --git a/android-lint/src/com/android/tools/idea/lint/actions/WrongThreadInterproceduralAction.kt b/android-lint/src/com/android/tools/idea/lint/actions/WrongThreadInterproceduralAction.kt
new file mode 100644
index 00000000000..0a03b4134b7
--- /dev/null
+++ b/android-lint/src/com/android/tools/idea/lint/actions/WrongThreadInterproceduralAction.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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 com.android.tools.idea.lint.actions
+
+import com.android.tools.idea.lint.common.LintBatchResult
+import com.android.tools.idea.lint.common.LintIdeRequest
+import com.android.tools.idea.lint.common.LintIdeSupport
+import com.android.tools.lint.checks.WrongThreadInterproceduralDetector
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.Scope
+import com.intellij.analysis.AnalysisScope
+import com.intellij.analysis.BaseAnalysisAction
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.vfs.VirtualFile
+import java.util.ArrayList
+import java.util.EnumSet
+import kotlin.system.measureTimeMillis
+
+private val LOG = Logger.getInstance(WrongThreadInterproceduralAction::class.java)
+
+/** An internal action for running the interprocedural thread annotation Lint check. Useful for timing and debugging. */
+class WrongThreadInterproceduralAction : BaseAnalysisAction(ACTION_NAME, ACTION_NAME) {
+
+ companion object {
+ private const val ACTION_NAME = "Wrong Thread (Interprocedural) Action"
+ }
+
+ override fun analyze(project: Project, scope: AnalysisScope) {
+ ProgressManager.getInstance().run(object : Task.Backgroundable(
+ project, "Finding interprocedural thread annotation violations", true) {
+
+ override fun run(indicator: ProgressIndicator) {
+ val time = measureTimeMillis {
+ // The Lint check won't run unless explicitly enabled by default..
+ val wasEnabledByDefault = WrongThreadInterproceduralDetector.ISSUE.isEnabledByDefault()
+ val detectorIssue = WrongThreadInterproceduralDetector.ISSUE.setEnabledByDefault(true)
+ val client = LintIdeSupport.get().createBatchClient(LintBatchResult(project, mutableMapOf(), scope, setOf(detectorIssue)))
+ try {
+ val files = ArrayList<VirtualFile>()
+ scope.accept { files.add(it) }
+ val modules = ModuleManager.getInstance(project).modules.toList()
+ val request = LintIdeRequest(client, project, files, modules, /*incremental*/ false)
+ request.setScope(EnumSet.of(Scope.ALL_JAVA_FILES))
+ val issue = object : IssueRegistry() {
+ override val vendor: Vendor = AOSP_VENDOR
+ override val issues: List<Issue>
+ get() = listOf(WrongThreadInterproceduralDetector.ISSUE)
+ }
+ client.createDriver(request, issue).analyze()
+ }
+ finally {
+ Disposer.dispose(client)
+ WrongThreadInterproceduralDetector.ISSUE.setEnabledByDefault(wasEnabledByDefault)
+ }
+ }
+ LOG.info("Interprocedural thread check: ${time}ms")
+ }
+ })
+ }
+}