/* * 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 static com.android.tools.lint.client.api.JavaParser.ResolvedClass; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.JavaScanner; 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.android.tools.lint.detector.api.Speed; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; import lombok.ast.ClassDeclaration; import lombok.ast.ConstructorDeclaration; import lombok.ast.Node; import lombok.ast.NormalTypeBody; import lombok.ast.TypeMember; /** * 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 JavaScanner { /** 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() { } @NonNull @Override public Speed getSpeed() { return Speed.FAST; } // ---- Implements JavaScanner ---- @Nullable @Override public List<String> applicableSuperClasses() { return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT); } @Override public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration node, @NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) { if (node == null) { return; } int flags = node.astModifiers().getEffectiveModifierFlags(); if ((flags & Modifier.ABSTRACT) != 0) { return; } if ((flags & Modifier.PUBLIC) == 0) { String message = String.format("This fragment class should be public (%1$s)", cls.getName()); context.report(ISSUE, node, context.getLocation(node.astName()), message); return; } if (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) { String message = String.format( "This fragment inner class should be static (%1$s)", cls.getName()); context.report(ISSUE, node, context.getLocation(node.astName()), message); return; } boolean hasDefaultConstructor = false; boolean hasConstructor = false; NormalTypeBody body = node.astBody(); if (body != null) { for (TypeMember member : body.astMembers()) { if (member instanceof ConstructorDeclaration) { hasConstructor = true; ConstructorDeclaration constructor = (ConstructorDeclaration) member; if (constructor.astParameters().isEmpty()) { // The constructor must be public if (constructor.astModifiers().isPublic()) { hasDefaultConstructor = true; } else { Location location = context.getLocation( constructor.astTypeName()); 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.getLocation(constructor.astTypeName()); // 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`)", cls.getName()); context.report(ISSUE, node, context.getLocation(node.astName()), message); } } }