blob: bb5453c8df7f68208c24d62e1f5133a367881b9b [file] [log] [blame]
/*
* Copyright (C) 2012 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.SdkConstants.CLASS_FRAGMENT
import com.android.SdkConstants.CLASS_V4_FRAGMENT
import com.android.tools.lint.detector.api.Category
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 org.jetbrains.uast.UAnonymousClass
import org.jetbrains.uast.UClass
/**
* Checks that Fragment subclasses can be instantiated via Class.newInstance: the
* class is public, static, and has a public null constructor.
*
* This helps track down issues like
* http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate
* (and countless duplicates)
*/
class FragmentDetector : Detector(), SourceCodeScanner {
companion object {
/** Are fragment subclasses instantiatable? */
@JvmField
val ISSUE = Issue.create(
id = "ValidFragment",
briefDescription = "Fragment not instantiatable",
explanation = """
From the Fragment documentation:
**Every** fragment must have an empty constructor, so it can be instantiated \
when restoring its activity's state. It is strongly recommended that subclasses \
do not have other constructors with parameters, since these constructors will \
not be called when the fragment is re-instantiated; instead, arguments can be \
supplied by the caller with `setArguments(Bundle)` and later retrieved by the \
Fragment with `getArguments()`.
Note that this is no longer true when you are using \
`androidx.fragment.app.Fragment`; with the `FragmentFactory` you can supply \
any arguments you want (as of version androidx version 1.1).
""",
category = Category.CORRECTNESS,
androidSpecific = true,
priority = 6,
severity = Severity.ERROR,
moreInfo = "https://developer.android.com/reference/android/app/Fragment.html#Fragment()",
implementation = Implementation(FragmentDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}
// ---- implements SourceCodeScanner ----
override fun applicableSuperClasses(): List<String>? {
// Note: We are deliberately NOT including: CLASS_V4_FRAGMENT.newName() here:
// androidx Fragments are allowed to use non-default constructors (see issue 119675579)
return listOf(CLASS_FRAGMENT, CLASS_V4_FRAGMENT.oldName())
}
override fun visitClass(context: JavaContext, declaration: UClass) {
if (declaration is UAnonymousClass) {
context.report(
ISSUE, declaration, context.getNameLocation(declaration),
"Fragments should be static such that they can be re-instantiated by the system, and anonymous classes are not static"
)
return
}
val evaluator = context.evaluator
if (evaluator.isAbstract(declaration)) {
return
}
if (!evaluator.isPublic(declaration)) {
context.report(
ISSUE, declaration, context.getNameLocation(declaration),
"This fragment class should be public (${declaration.qualifiedName})"
)
return
}
if (declaration.containingClass != null && !evaluator.isStatic(declaration)) {
context.report(
ISSUE, declaration, context.getNameLocation(declaration),
"This fragment inner class should be static (${declaration.qualifiedName})"
)
return
}
var hasDefaultConstructor = false
var hasConstructor = false
for (constructor in declaration.constructors) {
hasConstructor = true
if (constructor.parameterList.parametersCount == 0) {
if (evaluator.isPublic(constructor)) {
hasDefaultConstructor = true
} else {
val location = context.getNameLocation(constructor)
context.report(
ISSUE, constructor, location, "The default constructor must be public"
)
return
}
} else {
val location = context.getNameLocation(constructor)
// TODO: Use separate issue for this which isn't an error
val message =
"Avoid non-default constructors in fragments: use a default constructor plus `Fragment#setArguments(Bundle)` instead"
context.report(ISSUE, constructor, location, message)
}
}
if (!hasDefaultConstructor && hasConstructor) {
val message =
"This fragment should provide a default constructor (a public constructor with no arguments) (`${declaration.qualifiedName}`)"
context.report(ISSUE, declaration, context.getNameLocation(declaration), message)
}
}
}