blob: be885ad1b62a0e19708b40c484fc2988e2af4929 [file] [log] [blame]
/*
* Copyright (C) 2018 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.lint.checks
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.UastLintUtils
import com.android.tools.lint.detector.api.isKotlin
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMember
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.util.isArrayInitializer
import org.jetbrains.uast.util.isNewArray
/**
* Detector warning about private inner classes and constructors
* which require a synthetic accessor to be generated, thereby
* unnecessarily increasing overhead (methods, extra dispatch).
* Relevant only in large projects and especially libraries.
*/
class SyntheticAccessorDetector : Detector(), SourceCodeScanner {
companion object {
private val IMPLEMENTATION = Implementation(
SyntheticAccessorDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
/** The main issue discovered by this detector */
@JvmField
val ISSUE = Issue.create(
id = "SyntheticAccessor",
briefDescription = "Synthetic Accessor",
explanation = """
A private inner class which is accessed from the outer class will force \
the compiler to insert a synthetic accessor; this means that you are \
causing extra overhead. This is not important in small projects, but is \
important for large apps running up against the 64K method handle limit, \
and especially for **libraries** where you want to make sure your library \
is as small as possible for the cases where your library is used in an \
app running up against the 64K limit.
""",
moreInfo = null,
category = Category.PERFORMANCE,
priority = 2,
severity = Severity.WARNING,
androidSpecific = true,
enabledByDefault = false,
implementation = IMPLEMENTATION
)
// A similar inspection is available in IntelliJ, using several different id's.
// Create a couple of internal issues with the same id's such that we can check
// for suppress directives for those other aliases.
@Suppress("ObjectPropertyName") // underscore prefix required by testing infra
val _ALIAS_1 = Issue.create(
"SyntheticAccessorCall",
"?",
"?",
Category.LINT,
1,
Severity.WARNING,
IMPLEMENTATION
)
@Suppress("ObjectPropertyName") // underscore prefix required by testing infra
val _ALIAS_2 = Issue.create(
"PrivateMemberAccessBetweenOuterAndInnerClass",
"?",
"?",
Category.LINT,
1,
Severity.WARNING,
IMPLEMENTATION
)
}
override fun getApplicableUastTypes(): List<Class<out UElement>>? =
listOf(UCallExpression::class.java, USimpleNameReferenceExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.isNewArray() || node.isArrayInitializer()) {
return
}
val containingClass = node.getContainingUClass() ?: return
val method = node.resolve()
if (method == null) {
// default constructor
val classRef = node.classReference ?: return
val target = classRef.resolve() as? PsiClass ?: return
if (!context.evaluator.isPrivate(target)) {
return
}
if (target.isEquivalentTo(containingClass)) {
return
}
if (!isSameCompilationUnit(target, node)) {
return
}
reportError(context, node, target, target)
} else {
if (!context.evaluator.isPrivate(method)) {
return
}
val aClass = method.containingClass ?: return
if (aClass == containingClass.psi) {
return
}
if (!isSameCompilationUnit(aClass, node)) {
return
}
val from = node.getContainingUClass()
if (from != null && from.name == "Companion") {
// TODO: Companion objects can be named with a different name;
// we need be able to look this up in UAST
// Another way to do it is
// from.psi.modifierList.text.contains("companion")
return
}
// Mention it's an implicit constructor here?
reportError(context, node, method, aClass)
}
}
private fun isSameCompilationUnit(aClass: PsiClass, node: UElement): Boolean {
val file1 = UastLintUtils.getContainingFile(aClass)
val file2 = UastLintUtils.getContainingFile(node.sourcePsi)
return file1 == file2
}
override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
val resolved = node.resolve() ?: return
if (!(resolved is PsiMethod || resolved is PsiField)) {
return
}
val member = resolved as PsiMember
if (!context.evaluator.isPrivate(member)) {
return
}
val constant = ConstantEvaluator.evaluate(context, node)
if (constant != null) {
return // constant expression: inlined by javac or kotlinc
}
val containingClass = node.getContainingUClass() ?: return
val memberClass = member.containingClass
if (memberClass == null || memberClass == containingClass.psi) {
return
}
if (!isSameCompilationUnit(memberClass, node)) {
return
}
reportError(context, node, member, memberClass)
}
}
}
private fun reportError(
context: JavaContext,
node: UElement,
member: PsiMember,
target: PsiClass
) {
val driver = context.driver
if (driver.isSuppressed(context, _ALIAS_1, node) ||
driver.isSuppressed(context, _ALIAS_2, node)
) {
return
}
val location =
if (node is UCallExpression) {
context.getCallLocation(node, true, false)
} else {
context.getLocation(node)
}
val isKotlin = isKotlin(member)
val name = if (isKotlin) "Make internal" else "Make package protected"
val fix = fix().replace()
.name(name)
.sharedName(name)
.range(context.getLocation(member))
.text("private ")
.with(if (isKotlin) "internal " else "")
.autoFix()
.build()
val memberType = if (member is PsiField) {
"field `${member.name}`"
} else if (member is PsiMethod) {
if (member.isConstructor) {
if (context.evaluator.isStatic(member)) {
return
}
if (isKotlin(member)) {
// Sealed class? This will create a private constructor we can't delete
if (context.evaluator.isSealed(member)) {
return
}
if (context.evaluator.isSealed(target)) {
return
}
}
"constructor"
} else {
"method `${member.name}`"
}
} else {
"member"
}
val message =
"Access to `private` $memberType of class `${target.name}` requires synthetic accessor"
context.report(ISSUE, node, location, message, fix)
}
}