diff options
author | Tor Norbye <tnorbye@google.com> | 2012-12-11 12:14:43 -0800 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2012-12-11 12:14:44 -0800 |
commit | 721302b5f750d6579639ba9c049a230fc46ac0d2 (patch) | |
tree | 8ac37ab077735d3f062d7ed8a6d356e4a399b7ac | |
parent | 50490b1089d1152ca0878be37a5fff3d33f5945f (diff) | |
parent | 6c0eb1ff22473910087e0558110a6785baf65b51 (diff) | |
download | sdk-721302b5f750d6579639ba9c049a230fc46ac0d2.zip sdk-721302b5f750d6579639ba9c049a230fc46ac0d2.tar.gz sdk-721302b5f750d6579639ba9c049a230fc46ac0d2.tar.bz2 |
Merge "Add lint recycle detector"
9 files changed, 737 insertions, 19 deletions
diff --git a/lint/cli/.classpath b/lint/cli/.classpath index ade4f41..3278842 100644 --- a/lint/cli/.classpath +++ b/lint/cli/.classpath @@ -5,11 +5,11 @@ <classpathentry combineaccessrules="false" kind="src" path="/common"/> <classpathentry combineaccessrules="false" kind="src" path="/lint-api"/> <classpathentry combineaccessrules="false" kind="src" path="/lint-checks"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-analysis-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-analysis-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src-4.0.zip"/> <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> <classpathentry kind="output" path="bin"/> </classpath> diff --git a/lint/cli/src/test/.classpath b/lint/cli/src/test/.classpath index 178cd8c..e79b65f 100644 --- a/lint/cli/src/test/.classpath +++ b/lint/cli/src/test/.classpath @@ -7,10 +7,10 @@ <classpathentry combineaccessrules="false" kind="src" path="/lint-api"/> <classpathentry combineaccessrules="false" kind="src" path="/lint-checks"/> <classpathentry combineaccessrules="false" kind="src" path="/lint-cli"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src-4.0.zip"/> <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> <classpathentry combineaccessrules="false" kind="src" path="/common"/> <classpathentry combineaccessrules="false" kind="src" path="/testutils"/> diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/RecycleDetectorTest.java b/lint/cli/src/test/java/com/android/tools/lint/checks/RecycleDetectorTest.java new file mode 100644 index 0000000..ebef046 --- /dev/null +++ b/lint/cli/src/test/java/com/android/tools/lint/checks/RecycleDetectorTest.java @@ -0,0 +1,76 @@ +/* + * 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.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class RecycleDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new RecycleDetector(); + } + + public void test() throws Exception { + assertEquals( + "src/test/pkg/RecycleTest.java:56: Warning: This TypedArray should be recycled after use with #recycle() [Recycle]\n" + + " final TypedArray a = getContext().obtainStyledAttributes(attrs,\n" + + " ~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:63: Warning: This TypedArray should be recycled after use with #recycle() [Recycle]\n" + + " final TypedArray a = getContext().obtainStyledAttributes(new int[0]);\n" + + " ~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:79: Warning: This VelocityTracker should be recycled after use with #recycle() [Recycle]\n" + + " VelocityTracker tracker = VelocityTracker.obtain();\n" + + " ~~~~~~\n" + + "src/test/pkg/RecycleTest.java:85: Warning: This Message should be recycled after use with #recycle() [Recycle]\n" + + " Message message1 = getHandler().obtainMessage();\n" + + " ~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:86: Warning: This Message should be recycled after use with #recycle() [Recycle]\n" + + " Message message2 = Message.obtain();\n" + + " ~~~~~~\n" + + "src/test/pkg/RecycleTest.java:92: Warning: This MotionEvent should be recycled after use with #recycle() [Recycle]\n" + + " MotionEvent event1 = MotionEvent.obtain(null);\n" + + " ~~~~~~\n" + + "src/test/pkg/RecycleTest.java:93: Warning: This MotionEvent should be recycled after use with #recycle() [Recycle]\n" + + " MotionEvent event2 = MotionEvent.obtainNoHistory(null);\n" + + " ~~~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:98: Warning: This MotionEvent should be recycled after use with #recycle() [Recycle]\n" + + " MotionEvent event2 = MotionEvent.obtainNoHistory(null); // Not recycled\n" + + " ~~~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:103: Warning: This MotionEvent should be recycled after use with #recycle() [Recycle]\n" + + " MotionEvent event1 = MotionEvent.obtain(null); // Not recycled\n" + + " ~~~~~~\n" + + "src/test/pkg/RecycleTest.java:113: Warning: This MotionEvent has already been recycled [Recycle]\n" + + " int contents2 = event1.describeContents(); // BAD, after recycle\n" + + " ~~~~~~~~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:117: Warning: This TypedArray has already been recycled [Recycle]\n" + + " example = a.getString(R.styleable.MyView_exampleString); // BAD, after recycle\n" + + " ~~~~~~~~~\n" + + "src/test/pkg/RecycleTest.java:129: Warning: This Parcel should be recycled after use with #recycle() [Recycle]\n" + + " Parcel myparcel = Parcel.obtain();\n" + + " ~~~~~~\n" + + "0 errors, 12 warnings\n", + + lintProject( + "apicheck/classpath=>.classpath", + "apicheck/minsdk4.xml=>AndroidManifest.xml", + "project.properties1=>project.properties", + "bytecode/RecycleTest.java.txt=>src/test/pkg/RecycleTest.java", + "bytecode/RecycleTest.class.data=>bin/classes/test/pkg/RecycleTest.class" + )); + } +} diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.class.data Binary files differnew file mode 100644 index 0000000..3bdc829 --- /dev/null +++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.class.data diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.java.txt new file mode 100644 index 0000000..2a026f2 --- /dev/null +++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.java.txt @@ -0,0 +1,159 @@ +package test.pkg; + +import com.unit.test.R; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Message; +import android.os.Parcel; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; + +@SuppressWarnings("unused") +public class RecycleTest extends View { + // ---- Check recycling TypedArrays ---- + + public RecycleTest(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void ok1(AttributeSet attrs, int defStyle) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.MyView, defStyle, 0); + String example = a.getString(R.styleable.MyView_exampleString); + a.recycle(); + } + + public void ok2(AttributeSet attrs, int defStyle) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.MyView, defStyle, 0); + String example = a.getString(R.styleable.MyView_exampleString); + // If there's complicated logic, don't flag + if (something()) { + a.recycle(); + } + } + + public TypedArray ok3(AttributeSet attrs, int defStyle) { + // Value passes out of method: don't flag, caller might be recycling + return getContext().obtainStyledAttributes(attrs, R.styleable.MyView, + defStyle, 0); + } + + private TypedArray myref; + + public void ok4(AttributeSet attrs, int defStyle) { + // Value stored in a field: might be recycled later + TypedArray ref = getContext().obtainStyledAttributes(attrs, + R.styleable.MyView, defStyle, 0); + myref = ref; + } + + public void wrong1(AttributeSet attrs, int defStyle) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.MyView, defStyle, 0); + String example = a.getString(R.styleable.MyView_exampleString); + // a.recycle(); + } + + public void wrong2(AttributeSet attrs, int defStyle) { + final TypedArray a = getContext().obtainStyledAttributes(new int[0]); + // a.recycle(); + } + + public void unknown(AttributeSet attrs, int defStyle) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.MyView, defStyle, 0); + // We don't know what this method is (usually it will be in a different + // class) + // so don't flag it; it might recycle + handle(a); + } + + // ---- Check recycling VelocityTracker ---- + + public void tracker() { + VelocityTracker tracker = VelocityTracker.obtain(); + } + + // ---- Check recycling Message ---- + + public void message() { + Message message1 = getHandler().obtainMessage(); + Message message2 = Message.obtain(); + } + + // ---- Check recycling MotionEvent ---- + + public void motionEvent() { + MotionEvent event1 = MotionEvent.obtain(null); + MotionEvent event2 = MotionEvent.obtainNoHistory(null); + } + + public void motionEvent2() { + MotionEvent event1 = MotionEvent.obtain(null); // OK + MotionEvent event2 = MotionEvent.obtainNoHistory(null); // Not recycled + event1.recycle(); + } + + public void motionEvent3() { + MotionEvent event1 = MotionEvent.obtain(null); // Not recycled + MotionEvent event2 = MotionEvent.obtain(event1); + event2.recycle(); + } + + // ---- Using recycled objects ---- + + public void recycled() { + MotionEvent event1 = MotionEvent.obtain(null); // Not recycled + event1.recycle(); + int contents2 = event1.describeContents(); // BAD, after recycle + final TypedArray a = getContext().obtainStyledAttributes(new int[0]); + String example = a.getString(R.styleable.MyView_exampleString); // OK + a.recycle(); + example = a.getString(R.styleable.MyView_exampleString); // BAD, after recycle + } + + // ---- Check recycling Parcel ---- + + public void parcelOk() { + Parcel myparcel = Parcel.obtain(); + myparcel.createBinderArray(); + myparcel.recycle(); + } + + public void parcelMissing() { + Parcel myparcel = Parcel.obtain(); + myparcel.createBinderArray(); + } + + + // ---- Check suppress ---- + + @SuppressLint("Recycle") + public void recycledSuppress() { + MotionEvent event1 = MotionEvent.obtain(null); // Not recycled + event1.recycle(); + int contents2 = event1.describeContents(); // BAD, after recycle + final TypedArray a = getContext().obtainStyledAttributes(new int[0]); + String example = a.getString(R.styleable.MyView_exampleString); // OK + } + + // ---- Stubs ---- + + static void handle(TypedArray a) { + // Unknown method + } + + protected boolean something() { + return true; + } + + public android.content.res.TypedArray obtainStyledAttributes( + AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { + return null; + } +} diff --git a/lint/libs/lint_api/.classpath b/lint/libs/lint_api/.classpath index 426f226..b9f0a8a 100644 --- a/lint/libs/lint_api/.classpath +++ b/lint/libs/lint_api/.classpath @@ -3,10 +3,10 @@ <classpathentry excluding="Android.mk" kind="src" path="src/main/java"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry combineaccessrules="false" kind="src" path="/common"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src-4.0.zip"/> <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> <classpathentry kind="output" path="bin"/> diff --git a/lint/libs/lint_checks/.classpath b/lint/libs/lint_checks/.classpath index b169864..51eb645 100644 --- a/lint/libs/lint_checks/.classpath +++ b/lint/libs/lint_checks/.classpath @@ -3,11 +3,11 @@ <classpathentry kind="src" path="src/main/java"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry combineaccessrules="false" kind="src" path="/lint-api"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-analysis-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/> - <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-analysis-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src-4.0.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src-4.0.zip"/> <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> <classpathentry combineaccessrules="false" kind="src" path="/common"/> <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java index 254cb02..39a5ae3 100644 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -55,7 +55,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { private static final List<Issue> sIssues; static { - final int initialCapacity = 133; + final int initialCapacity = 134; List<Issue> issues = new ArrayList<Issue>(initialCapacity); issues.add(AccessibilityDetector.ISSUE); @@ -182,6 +182,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(JavaPerformanceDetector.USE_VALUEOF); issues.add(JavaPerformanceDetector.USE_SPARSEARRAY); issues.add(WakelockDetector.ISSUE); + issues.add(RecycleDetector.ISSUE); issues.add(SetJavaScriptEnabledDetector.ISSUE); issues.add(ToastDetector.ISSUE); issues.add(SharedPrefsDetector.ISSUE); diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RecycleDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RecycleDetector.java new file mode 100644 index 0000000..cad354c --- /dev/null +++ b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RecycleDetector.java @@ -0,0 +1,482 @@ +/* + * 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.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Category; +import com.android.tools.lint.detector.api.ClassContext; +import com.android.tools.lint.detector.api.Context; +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.Location; +import com.android.tools.lint.detector.api.Scope; +import com.android.tools.lint.detector.api.Severity; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicValue; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.Interpreter; +import org.objectweb.asm.tree.analysis.Value; + +import java.util.Arrays; +import java.util.List; + +/** + * Checks for missing {@code recycle} calls on resources that encourage it + */ +public class RecycleDetector extends Detector implements ClassScanner { + /** Problems with missing recycle calls */ + public static final Issue ISSUE = Issue.create( + "Recycle", //$NON-NLS-1$ + "Looks for missing recycle() calls on resources", + + "Many resources, such as TypedArrays, VelocityTrackers, etc., " + + "should be recycled (with a `recycle()` call) after use. This lint check looks " + + "for missing `recycle()` calls.", + + Category.PERFORMANCE, + 7, + Severity.WARNING, + RecycleDetector.class, + Scope.CLASS_FILE_SCOPE); + + // Target method names + private static final String RECYCLE = "recycle"; //$NON-NLS-1$ + private static final String OBTAIN = "obtain"; //$NON-NLS-1$ + private static final String OBTAIN_NO_HISTORY = "obtainNoHistory"; //$NON-NLS-1$ + private static final String OBTAIN_MESSAGE = "obtainMessage"; //$NON-NLS-1$ + private static final String OBTAIN_ATTRIBUTES = "obtainAttributes"; //$NON-NLS-1$ + private static final String OBTAIN_TYPED_ARRAY = "obtainTypedArray"; //$NON-NLS-1$ + private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$ + + // Target owners + private static final String VELOCITY_TRACKER_CLS = "android/view/VelocityTracker";//$NON-NLS-1$ + private static final String TYPED_ARRAY_CLS = "android/content/res/TypedArray"; //$NON-NLS-1$ + private static final String CONTEXT_CLS = "android/content/Context"; //$NON-NLS-1$ + private static final String MOTION_EVENT_CLS = "android/view/MotionEvent"; //$NON-NLS-1$ + private static final String MESSAGE_CLS = "android/os/Message"; //$NON-NLS-1$ + private static final String HANDLER_CLS = "android/os/Handler"; //$NON-NLS-1$ + private static final String RESOURCES_CLS = "android/content/res/Resources"; //$NON-NLS-1$ + private static final String PARCEL_CLS = "android/os/Parcel"; //$NON-NLS-1$ + + // Target description signatures + private static final String TYPED_ARRAY_SIG = "Landroid/content/res/TypedArray;"; //$NON-NLS-1$ + private static final String MESSAGE_SIG = "Landroid/os/Message;"; //$NON-NLS-1$ + + private boolean mObtainsTypedArray; + private boolean mRecyclesTypedArray; + private boolean mObtainsTracker; + private boolean mRecyclesTracker; + private boolean mObtainsMessage; + private boolean mRecyclesMessage; + private boolean mObtainsMotionEvent; + private boolean mRecyclesMotionEvent; + private boolean mObtainsParcel; + private boolean mRecyclesParcel; + + /** Constructs a new {@link RecycleDetector} */ + public RecycleDetector() { + } + + @Override + public void afterCheckProject(@NonNull Context context) { + int phase = context.getDriver().getPhase(); + if (phase == 1) { + if (mObtainsTypedArray && !mRecyclesTypedArray + || mObtainsTracker && !mRecyclesTracker + || mObtainsMessage && !mRecyclesMessage + || mObtainsParcel && !mRecyclesParcel + || mObtainsMotionEvent && !mRecyclesMotionEvent) { + context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE); + } + } + } + + // ---- Implements ClassScanner ---- + + @Override + @Nullable + public List<String> getApplicableCallNames() { + return Arrays.asList( + RECYCLE, + OBTAIN_STYLED_ATTRIBUTES, + OBTAIN, + OBTAIN_ATTRIBUTES, + OBTAIN_TYPED_ARRAY, + OBTAIN_MESSAGE, + OBTAIN_NO_HISTORY + ); + } + + @Override + public void checkCall( + @NonNull ClassContext context, + @NonNull ClassNode classNode, + @NonNull MethodNode method, + @NonNull MethodInsnNode call) { + String name = call.name; + String owner = call.owner; + String desc = call.desc; + int phase = context.getDriver().getPhase(); + if (RECYCLE.equals(name) && desc.equals("()V")) { //$NON-NLS-1$ + if (owner.equals(TYPED_ARRAY_CLS)) { + mRecyclesTypedArray = true; + } else if (owner.equals(VELOCITY_TRACKER_CLS)) { + mRecyclesTracker = true; + } else if (owner.equals(MESSAGE_CLS)) { + mRecyclesMessage = true; + } else if (owner.equals(MOTION_EVENT_CLS)) { + mRecyclesMotionEvent = true; + } else if (owner.equals(PARCEL_CLS)) { + mRecyclesParcel = true; + } + } else if (owner.equals(MOTION_EVENT_CLS)) { + if (OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) { + mObtainsMotionEvent = true; + if (phase == 2 && !mRecyclesMotionEvent) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(MOTION_EVENT_CLS), + null); + } else if (phase == 1 + && checkMethodFlow(context, classNode, method, call, MOTION_EVENT_CLS)) { + // Already reported error above; don't do global check + mRecyclesMotionEvent = true; + } + } + } else if (OBTAIN_MESSAGE.equals(name)) { + if (owner.equals(HANDLER_CLS) && desc.endsWith(MESSAGE_SIG)) { + mObtainsMessage = true; + if (phase == 2 && !mRecyclesMessage) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(MESSAGE_CLS), null); + } + } + } else if (OBTAIN.equals(name)) { + if (owner.equals(VELOCITY_TRACKER_CLS)) { + mObtainsTracker = true; + if (phase == 2 && !mRecyclesTracker) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(VELOCITY_TRACKER_CLS), + null); + } + } else if (owner.equals(MESSAGE_CLS) && desc.endsWith(MESSAGE_SIG)) { + // TODO: Handle Message constructor? + mObtainsMessage = true; + if (phase == 2 && !mRecyclesMessage) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(MESSAGE_CLS), + null); + } + } else if (owner.equals(PARCEL_CLS)) { + mObtainsParcel = true; + if (phase == 2 && !mRecyclesParcel) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(PARCEL_CLS), + null); + } else if (phase == 1 + && checkMethodFlow(context, classNode, method, call, PARCEL_CLS)) { + // Already reported error above; don't do global check + mRecyclesParcel = true; + } + } + } else if (OBTAIN_STYLED_ATTRIBUTES.equals(name) + || OBTAIN_ATTRIBUTES.equals(name) + || OBTAIN_TYPED_ARRAY.equals(name)) { + if ((owner.equals(CONTEXT_CLS) || owner.equals(RESOURCES_CLS)) + && desc.endsWith(TYPED_ARRAY_SIG)) { + mObtainsTypedArray = true; + if (phase == 2 && !mRecyclesTypedArray) { + context.report(ISSUE, method, call, context.getLocation(call), + getErrorMessage(TYPED_ARRAY_CLS), + null); + } else if (phase == 1 + && checkMethodFlow(context, classNode, method, call, TYPED_ARRAY_CLS)) { + // Already reported error above; don't do global check + mRecyclesTypedArray = true; + } + } + } + } + + /** Computes an error message for a missing recycle of the given type */ + private static String getErrorMessage(String owner) { + String className = owner.substring(owner.lastIndexOf('/') + 1); + return String.format("This %1$s should be recycled after use with #recycle()", + className); + } + + /** + * Ensures that the given allocate call in the given method has a + * corresponding recycle method, also within the same method, OR, the + * allocated resource flows out of the method (either as a return value, or + * into a field, or into some other method (with some known exceptions; e.g. + * passing a MotionEvent into another MotionEvent's constructor is fine) + * <p> + * Returns true if an error was found + */ + private static boolean checkMethodFlow(ClassContext context, ClassNode classNode, + MethodNode method, MethodInsnNode call, String recycleOwner) { + RecycleTracker interpreter = new RecycleTracker(context, method, call, recycleOwner); + ResourceAnalyzer analyzer = new ResourceAnalyzer(interpreter); + interpreter.setAnalyzer(analyzer); + try { + analyzer.analyze(classNode.name, method); + if (!interpreter.isRecycled() && !interpreter.isEscaped()) { + Location location = context.getLocation(call); + String message = getErrorMessage(recycleOwner); + context.report(ISSUE, method, call, location, message, null); + return true; + } + } catch (AnalyzerException e) { + context.log(e, null); + } + + return false; + } + + /** + * ASM interpreter which tracks the instances of the allocated resource, and + * checks whether it is eventually passed to a {@code recycle()} call. If the + * value flows out of the method (to a field, or a method call), it will + * also consider the resource recycled. + */ + private static class RecycleTracker extends Interpreter { + private final Value INSTANCE = BasicValue.INT_VALUE; // Only identity matters, not value + private final Value RECYCLED = BasicValue.FLOAT_VALUE; + private final Value UNKNOWN = BasicValue.UNINITIALIZED_VALUE; + + private final ClassContext mContext; + private final MethodNode mMethod; + private final MethodInsnNode mObtainNode; + private boolean mIsRecycled; + private boolean mEscapes; + private final String mRecycleOwner; + private ResourceAnalyzer mAnalyzer; + + public RecycleTracker( + @NonNull ClassContext context, + @NonNull MethodNode method, + @NonNull MethodInsnNode obtainNode, + @NonNull String recycleOwner) { + super(Opcodes.ASM4); + mContext = context; + mMethod = method; + mObtainNode = obtainNode; + mRecycleOwner = recycleOwner; + } + + /** + * Sets the analyzer associated with the interpreter, such that it can + * get access to the execution frames + */ + void setAnalyzer(ResourceAnalyzer analyzer) { + mAnalyzer = analyzer; + } + + /** + * Returns whether a recycle call was found for the given method + * + * @return true if the resource was recycled + */ + public boolean isRecycled() { + return mIsRecycled; + } + + /** + * Returns whether the target resource escapes from the method, for + * example as a return value, or a field assignment, or getting passed + * to another method + * + * @return true if the resource escapes + */ + public boolean isEscaped() { + return mEscapes; + } + + @Override + public Value newOperation(AbstractInsnNode node) throws AnalyzerException { + return UNKNOWN; + } + + @Override + public Value newValue(final Type type) { + if (type != null && type.getSort() == Type.VOID) { + return null; + } else { + return UNKNOWN; + } + } + + @Override + public Value copyOperation(AbstractInsnNode node, Value value) throws AnalyzerException { + return value; + } + + @Override + public Value binaryOperation(AbstractInsnNode node, Value value1, Value value2) + throws AnalyzerException { + if (node.getOpcode() == Opcodes.PUTFIELD) { + if (value2 == INSTANCE) { + mEscapes = true; + } + } + return merge(value1, value2); + } + + @Override + public Value naryOperation(AbstractInsnNode node, List values) throws AnalyzerException { + if (node == mObtainNode) { + return INSTANCE; + } + + MethodInsnNode call = null; + if (node.getType() == AbstractInsnNode.METHOD_INSN) { + call = (MethodInsnNode) node; + if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { + if (call.name.equals(RECYCLE) && call.owner.equals(mRecycleOwner)) { + if (values != null && values.size() == 1 && values.get(0) == INSTANCE) { + mIsRecycled = true; + Frame frame = mAnalyzer.getCurrentFrame(); + if (frame != null) { + int localSize = frame.getLocals(); + for (int i = 0; i < localSize; i++) { + Value local = frame.getLocal(i); + if (local == INSTANCE) { + frame.setLocal(i, RECYCLED); + } + } + int stackSize = frame.getStackSize(); + if (stackSize == 1 && frame.getStack(0) == INSTANCE) { + frame.pop(); + frame.push(RECYCLED); + } + } + return RECYCLED; + } + } + } + } + + if (values != null && values.size() >= 1) { + // Skip the first element: method calls *on* the TypedArray are okay + int start = node.getOpcode() == Opcodes.INVOKESTATIC ? 0 : 1; + for (int i = 0, n = values.size(); i < n; i++) { + Object v = values.get(i); + if (v == INSTANCE && i >= start) { + // Known special cases + if (node.getOpcode() == Opcodes.INVOKESTATIC) { + assert call != null; + if (call.name.equals(OBTAIN) && + call.owner.equals(MOTION_EVENT_CLS)) { + return UNKNOWN; + } + } + + // Passing the instance to another method: could leak + // the instance out of this method (for example calling + // a method which recycles it on our behalf, or store it + // in some holder which will recycle it later). In this + // case, just assume that things are okay. + mEscapes = true; + } else if (v == RECYCLED && call != null) { + Location location = mContext.getLocation(call); + String message = String.format("This %1$s has already been recycled", + mRecycleOwner.substring(mRecycleOwner.lastIndexOf('/') + 1)); + mContext.report(ISSUE, mMethod, call, location, message, null); + } + } + } + + return UNKNOWN; + } + + @Override + public Value unaryOperation(AbstractInsnNode node, Value value) throws AnalyzerException { + return value; + } + + @Override + public Value ternaryOperation(AbstractInsnNode node, Value value1, Value value2, + Value value3) throws AnalyzerException { + if (value1 == RECYCLED || value2 == RECYCLED || value3 == RECYCLED) { + return RECYCLED; + } else if (value1 == INSTANCE || value2 == INSTANCE || value3 == INSTANCE) { + return INSTANCE; + } + return UNKNOWN; + } + + @Override + public void returnOperation(AbstractInsnNode node, Value value1, Value value2) + throws AnalyzerException { + if (value1 == INSTANCE || value2 == INSTANCE) { + mEscapes = true; + } + } + + @Override + public Value merge(Value value1, Value value2) { + if (value1 == RECYCLED || value2 == RECYCLED) { + return RECYCLED; + } else if (value1 == INSTANCE || value2 == INSTANCE) { + return INSTANCE; + } + return UNKNOWN; + } + } + + private static class ResourceAnalyzer extends Analyzer { + private Frame mCurrent; + private Frame mFrame1; + private Frame mFrame2; + + public ResourceAnalyzer(Interpreter interpreter) { + super(interpreter); + } + + Frame getCurrentFrame() { + return mCurrent; + } + + @Override + protected void init(String owner, MethodNode m) throws AnalyzerException { + mCurrent = mFrame2; + super.init(owner, m); + } + + @Override + protected Frame newFrame(int nLocals, int nStack) { + // Stash the two most recent frame allocations. When init is called the second + // most recently seen frame is the current frame used during execution, which + // is where we need to replace INSTANCE with RECYCLED when the void + // recycle method is called. + Frame newFrame = super.newFrame(nLocals, nStack); + mFrame2 = mFrame1; + mFrame1 = newFrame; + return newFrame; + } + } +} |