diff options
author | Kun Shen <kunshen@google.com> | 2021-10-11 11:02:40 -0700 |
---|---|---|
committer | Kun Shen <kunshen@google.com> | 2021-10-11 20:00:16 +0000 |
commit | b9ade51b80d570691e8e6db6493323dca3cd9dfb (patch) | |
tree | 0072778e924affffb88a3cb0b7532cb245ed8c97 /android-lint/src | |
parent | b8d89e0ae887da5cebf858023dfbab7dcf26bd60 (diff) | |
download | idea-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')
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") + } + }) + } +} |