/* * 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.CONSTRUCTOR_NAME; import static com.android.SdkConstants.FRAGMENT; import static com.android.SdkConstants.FRAGMENT_V4; import com.android.annotations.NonNull; import com.android.tools.lint.client.api.LintDriver; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.ClassContext; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.ClassScanner; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.LintUtils; 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 org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; 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 ClassScanner { private static final String FRAGMENT_NAME_SUFFIX = "Fragment"; //$NON-NLS-1$ /** Are fragment subclasses instantiatable? */ public static final Issue ISSUE = Issue.create( "ValidFragment", //$NON-NLS-1$ "Ensures that Fragment subclasses can be instantiated", "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.WARNING, FragmentDetector.class, Scope.CLASS_FILE_SCOPE).setMoreInfo( "http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$ /** Constructs a new {@link FragmentDetector} */ public FragmentDetector() { } @Override public @NonNull Speed getSpeed() { return Speed.FAST; } // ---- Implements ClassScanner ---- @Override public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) { // Ignore abstract classes since they are clearly (and by definition) not intended to // be instantiated. We're looking for accidental non-static or missing constructor // scenarios here. return; } LintDriver driver = context.getDriver(); if (!(driver.isSubclassOf(classNode, FRAGMENT) || driver.isSubclassOf(classNode, FRAGMENT_V4))) { if (!context.getScope().contains(Scope.ALL_JAVA_FILES)) { // Single file checking: Just check that it looks like a fragment class // (since we don't have a full superclass map) if (!classNode.name.endsWith(FRAGMENT_NAME_SUFFIX) || classNode.superName == null) { return; } } else { return; } } if ((classNode.access & Opcodes.ACC_PUBLIC) == 0) { context.report(ISSUE, context.getLocation(classNode), String.format( "This fragment class should be public (%1$s)", ClassContext.createSignature(classNode.name, null, null)), null); return; } if (classNode.name.indexOf('$') != -1 && !LintUtils.isStaticInnerClass(classNode)) { context.report(ISSUE, context.getLocation(classNode), String.format( "This fragment inner class should be static (%1$s)", ClassContext.createSignature(classNode.name, null, null)), null); return; } boolean hasDefaultConstructor = false; @SuppressWarnings("rawtypes") // ASM API List methodList = classNode.methods; for (Object m : methodList) { MethodNode method = (MethodNode) m; if (method.name.equals(CONSTRUCTOR_NAME)) { if (method.desc.equals("()V")) { //$NON-NLS-1$ // The constructor must be public if ((method.access & Opcodes.ACC_PUBLIC) != 0) { hasDefaultConstructor = true; } else { context.report(ISSUE, context.getLocation(method, classNode), "The default constructor must be public", null); // 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 if (!method.desc.contains("()")) { //$NON-NLS-1$ context.report(ISSUE, context.getLocation(method, classNode), "Avoid non-default constructors in fragments: use a default constructor " + "plus Fragment#setArguments(Bundle) instead", null); } } } if (!hasDefaultConstructor) { context.report(ISSUE, context.getLocation(classNode), String.format( "This fragment should provide a default constructor (a public " + "constructor with no arguments) (%1$s)", ClassContext.createSignature(classNode.name, null, null)), null); } } }