| /* |
| * 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 static com.android.SdkConstants.CLASS_FRAGMENT; |
| import static com.android.SdkConstants.CLASS_V4_FRAGMENT; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.client.api.JavaEvaluator; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Detector.JavaPsiScanner; |
| 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.Location; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.intellij.psi.PsiAnonymousClass; |
| import com.intellij.psi.PsiClass; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiMethod; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Checks that Fragment subclasses can be instantiated via |
| * {link {@link Class#newInstance()}}: the class is public, static, and has |
| * a public null constructor. |
| * <p> |
| * This helps track down issues like |
| * http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate |
| * (and countless duplicates) |
| */ |
| public class FragmentDetector extends Detector implements JavaPsiScanner { |
| /** Are fragment subclasses instantiatable? */ |
| public static final Issue ISSUE = Issue.create( |
| "ValidFragment", //$NON-NLS-1$ |
| "Fragment not instantiatable", |
| |
| "From the Fragment documentation:\n" + |
| "*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()`.", |
| |
| Category.CORRECTNESS, |
| 6, |
| Severity.FATAL, |
| new Implementation( |
| FragmentDetector.class, |
| Scope.JAVA_FILE_SCOPE) |
| ).addMoreInfo( |
| "http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$ |
| |
| |
| /** Constructs a new {@link FragmentDetector} */ |
| public FragmentDetector() { |
| } |
| |
| // ---- Implements JavaScanner ---- |
| |
| @Nullable |
| @Override |
| public List<String> applicableSuperClasses() { |
| return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT); |
| } |
| |
| @Override |
| public void checkClass(@NonNull JavaContext context, @NonNull PsiClass node) { |
| if (node instanceof PsiAnonymousClass) { |
| String message = "Fragments should be static such that they can be re-instantiated by " + |
| "the system, and anonymous classes are not static"; |
| PsiElement locationNode = JavaContext.findNameElement(node); |
| if (locationNode == null) { |
| locationNode = node; |
| } |
| context.report(ISSUE, locationNode, context.getLocation(locationNode), message); |
| return; |
| } |
| |
| JavaEvaluator evaluator = context.getEvaluator(); |
| if (evaluator.isAbstract(node)) { |
| return; |
| } |
| |
| if (!evaluator.isPublic(node)) { |
| String message = String.format("This fragment class should be public (%1$s)", |
| node.getQualifiedName()); |
| context.report(ISSUE, node, context.getNameLocation(node), message); |
| return; |
| } |
| |
| if (node.getContainingClass() != null && !evaluator.isStatic(node)) { |
| String message = String.format( |
| "This fragment inner class should be static (%1$s)", node.getQualifiedName()); |
| context.report(ISSUE, node, context.getNameLocation(node), message); |
| return; |
| } |
| |
| boolean hasDefaultConstructor = false; |
| boolean hasConstructor = false; |
| for (PsiMethod constructor : node.getConstructors()) { |
| hasConstructor = true; |
| if (constructor.getParameterList().getParametersCount() == 0) { |
| if (evaluator.isPublic(constructor)) { |
| hasDefaultConstructor = true; |
| } else { |
| Location location = context.getNameLocation(constructor); |
| context.report(ISSUE, constructor, location, |
| "The default constructor must be public"); |
| // Also mark that we have a constructor so we don't complain again |
| // below since we've already emitted a more specific error related |
| // to the default constructor |
| hasDefaultConstructor = true; |
| } |
| } else { |
| Location location = context.getNameLocation(constructor); |
| // TODO: Use separate issue for this which isn't an error |
| String 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) { |
| String message = String.format( |
| "This fragment should provide a default constructor (a public " + |
| "constructor with no arguments) (`%1$s`)", |
| node.getQualifiedName()); |
| context.report(ISSUE, node, context.getNameLocation(node), message); |
| } |
| } |
| } |