aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2012-12-11 12:14:43 -0800
committerGerrit Code Review <noreply-gerritcodereview@google.com>2012-12-11 12:14:44 -0800
commit721302b5f750d6579639ba9c049a230fc46ac0d2 (patch)
tree8ac37ab077735d3f062d7ed8a6d356e4a399b7ac
parent50490b1089d1152ca0878be37a5fff3d33f5945f (diff)
parent6c0eb1ff22473910087e0558110a6785baf65b51 (diff)
downloadsdk-721302b5f750d6579639ba9c049a230fc46ac0d2.zip
sdk-721302b5f750d6579639ba9c049a230fc46ac0d2.tar.gz
sdk-721302b5f750d6579639ba9c049a230fc46ac0d2.tar.bz2
Merge "Add lint recycle detector"
-rw-r--r--lint/cli/.classpath10
-rw-r--r--lint/cli/src/test/.classpath8
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/RecycleDetectorTest.java76
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.class.databin0 -> 4321 bytes
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.java.txt159
-rw-r--r--lint/libs/lint_api/.classpath8
-rw-r--r--lint/libs/lint_checks/.classpath10
-rw-r--r--lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java3
-rw-r--r--lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RecycleDetector.java482
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
new file mode 100644
index 0000000..3bdc829
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/RecycleTest.class.data
Binary files differ
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;
+ }
+ }
+}