Skip to content

Commit f92cea6

Browse files
committed
Android Recycle Rule. Closes #212
1 parent 5065d09 commit f92cea6

File tree

4 files changed

+591
-0
lines changed

4 files changed

+591
-0
lines changed

plugin/src/main/java/org/autorefactor/refactoring/rules/AllRefactoringRules.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public static List<RefactoringRule> getAllRefactoringRules() {
116116
new ReplaceQualifiedNamesBySimpleNamesRefactoring(),
117117
new RemoveEmptyLinesRefactoring(),
118118
new AndroidWakeLockRefactoring(),
119+
new AndroidRecycleRefactoring(),
119120
new SwitchRefactoring());
120121
}
121122

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/*
2+
* AutoRefactor - Eclipse plugin to automatically refactor Java code bases.
3+
*
4+
* Copyright (C) 2016 Luis Cruz - Android Refactoring Rules
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program under LICENSE-GNUGPL. If not, see
18+
* <http://www.gnu.org/licenses/>.
19+
*
20+
*
21+
* All rights reserved. This program and the accompanying materials
22+
* are made available under the terms of the Eclipse Public License v1.0
23+
* which accompanies this distribution under LICENSE-ECLIPSE, and is
24+
* available at http://www.eclipse.org/legal/epl-v10.html
25+
*/
26+
package org.autorefactor.refactoring.rules;
27+
28+
import org.eclipse.jdt.core.dom.ASTNode;
29+
import org.eclipse.jdt.core.dom.ASTVisitor;
30+
import org.eclipse.jdt.core.dom.Assignment;
31+
import org.eclipse.jdt.core.dom.Block;
32+
import org.eclipse.jdt.core.dom.ExpressionStatement;
33+
import org.eclipse.jdt.core.dom.IMethodBinding;
34+
import org.eclipse.jdt.core.dom.ITypeBinding;
35+
import org.eclipse.jdt.core.dom.MethodInvocation;
36+
import org.eclipse.jdt.core.dom.SimpleName;
37+
import org.eclipse.jdt.core.dom.Statement;
38+
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
39+
import static org.autorefactor.refactoring.ASTHelper.*;
40+
import static org.eclipse.jdt.core.dom.ASTNode.*;
41+
import org.autorefactor.refactoring.ASTBuilder;
42+
import org.autorefactor.refactoring.Refactorings;
43+
44+
/*
45+
* TODO when the last use of resource is as arg of a method invocation,
46+
* it should be assumed that the given method will take care of the release.
47+
* TODO Track local variables. E.g., when a TypedArray a is assigned to variable b,
48+
* release() should be called only in one variable.
49+
* TODO (low priority) check whether resources are being used after release.
50+
* TODO add support for FragmentTransaction.beginTransaction(). It can use method
51+
* chaining (which means local variable might not be present) and it can be released
52+
* by two methods: commit() and commitAllowingStateLoss().
53+
*/
54+
55+
/** See {@link #getDescription()} method. */
56+
public class AndroidRecycleRefactoring extends AbstractRefactoringRule {
57+
58+
@Override
59+
public String getDescription() {
60+
return "Many resources, such as TypedArrays, VelocityTrackers, etc., should be "
61+
+ "recycled (with a recycle()/close() call) after use. " + "Inspired from "
62+
+ "https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-checks/src/main/"
63+
+ "java/com/android/tools/lint/checks/CleanupDetector.java";
64+
}
65+
66+
@Override
67+
public String getName() {
68+
return "RecycleRefactoring";
69+
}
70+
71+
private static boolean isMethodIgnoringParameters(MethodInvocation node, String typeQualifiedName,
72+
String methodName) {
73+
if (node == null) {
74+
return false;
75+
}
76+
final IMethodBinding methodBinding = node.resolveMethodBinding();
77+
if (methodBinding == null || !methodName.equals(methodBinding.getName())) {
78+
return false;
79+
}
80+
final ITypeBinding declaringClazz = methodBinding.getDeclaringClass();
81+
return instanceOf(declaringClazz, typeQualifiedName);
82+
}
83+
84+
private static boolean isMethodIgnoringParameters(MethodInvocation node, String typeQualifiedName,
85+
String... methodNames) {
86+
for (String methodName : methodNames) {
87+
if (isMethodIgnoringParameters(node, typeQualifiedName, methodName)) {
88+
return true;
89+
}
90+
}
91+
return false;
92+
}
93+
94+
private String methodNameToCleanupResource(MethodInvocation node) {
95+
if (isMethodIgnoringParameters(
96+
node,
97+
"android.database.sqlite.SQLiteDatabase",
98+
"query", "rawQuery", "queryWithFactory", "rawQueryWithFactory")
99+
) {
100+
return "close";
101+
} else if (isMethodIgnoringParameters(
102+
node,
103+
"android.content.ContentProvider",
104+
"query" , "rawQuery", "queryWithFactory", "rawQueryWithFactory")
105+
) {
106+
return "close";
107+
} else if (isMethodIgnoringParameters(
108+
node,
109+
"android.content.ContentResolver",
110+
"query", "rawQuery", "queryWithFactory", "rawQueryWithFactory")
111+
) {
112+
return "close";
113+
} else if (isMethodIgnoringParameters(
114+
node,
115+
"android.content.ContentProviderClient",
116+
"query", "rawQuery", "queryWithFactory", "rawQueryWithFactory")
117+
) {
118+
return "close";
119+
} else if (isMethodIgnoringParameters(
120+
node,
121+
"android.content.Context",
122+
"obtainStyledAttributes")
123+
) {
124+
return "recycle";
125+
} else if (isMethodIgnoringParameters(
126+
node,
127+
"android.content.res.Resources",
128+
"obtainTypedArray", "obtainAttributes", "obtainStyledAttributes")
129+
) {
130+
return "recycle";
131+
} else if (isMethod(
132+
node,
133+
"android.view.VelocityTracker",
134+
"obtain")
135+
) {
136+
return "recycle";
137+
} else if (isMethodIgnoringParameters(
138+
node,
139+
"android.os.Handler",
140+
"obtainMessage")
141+
) {
142+
return "recycle";
143+
} else if (isMethodIgnoringParameters(
144+
node,
145+
"android.os.Message",
146+
"obtain")
147+
) {
148+
return "recycle";
149+
} else if (isMethod(
150+
node,
151+
"android.view.MotionEvent",
152+
"obtainNoHistory", "android.view.MotionEvent")
153+
) {
154+
return "recycle";
155+
} else if (isMethodIgnoringParameters(
156+
node,
157+
"android.view.MotionEvent",
158+
"obtain")
159+
) {
160+
return "recycle";
161+
} else if (isMethod(
162+
node,
163+
"android.os.Parcel",
164+
"obtain")
165+
) {
166+
return "recycle";
167+
} else if (isMethodIgnoringParameters(
168+
node,
169+
"android.content.ContentResolver",
170+
"acquireContentProviderClient")
171+
) {
172+
return "release";
173+
}
174+
return null;
175+
}
176+
177+
@Override
178+
public boolean visit(MethodInvocation node) {
179+
String recycleMethodName = methodNameToCleanupResource(node);
180+
if (recycleMethodName != null) {
181+
SimpleName cursorExpression;
182+
ASTNode variableAssignmentNode;
183+
VariableDeclarationFragment variableDeclarationFragment =
184+
(VariableDeclarationFragment) getFirstAncestorOrNull(node, VariableDeclarationFragment.class);
185+
if (variableDeclarationFragment != null) {
186+
cursorExpression = variableDeclarationFragment.getName();
187+
variableAssignmentNode = variableDeclarationFragment;
188+
} else {
189+
Assignment variableAssignment = getAncestor(node, Assignment.class);
190+
cursorExpression = (SimpleName) variableAssignment.getLeftHandSide();
191+
variableAssignmentNode = variableAssignment;
192+
}
193+
// Check whether it has been closed
194+
ClosePresenceChecker closePresenceChecker = new ClosePresenceChecker(cursorExpression, recycleMethodName);
195+
VisitorDecorator visitor = new VisitorDecorator(variableAssignmentNode, cursorExpression,
196+
closePresenceChecker);
197+
Block block = getAncestor(node, Block.class);
198+
block.accept(visitor);
199+
if (!closePresenceChecker.isClosePresent()) {
200+
Statement lastCursorAccess = closePresenceChecker.getLastCursorStatementInBlock(block);
201+
if (lastCursorAccess.getNodeType() != ASTNode.RETURN_STATEMENT) {
202+
final ASTBuilder b = this.ctx.getASTBuilder();
203+
final Refactorings r = this.ctx.getRefactorings();
204+
MethodInvocation closeInvocation = b.invoke(b.copy(cursorExpression), recycleMethodName);
205+
ExpressionStatement expressionStatement = b.getAST().newExpressionStatement(closeInvocation);
206+
r.insertAfter(expressionStatement, lastCursorAccess);
207+
return DO_NOT_VISIT_SUBTREE;
208+
}
209+
}
210+
}
211+
return VISIT_SUBTREE;
212+
}
213+
214+
private class ClosePresenceChecker extends ASTVisitor {
215+
private boolean closePresent;
216+
private SimpleName lastCursorUse;
217+
private SimpleName cursorSimpleName;
218+
private String recycleMethodName;
219+
220+
/**
221+
* @param cursorSimpleName Variable name of cursor
222+
* @param recycleMethodName Recycle method name
223+
*/
224+
public ClosePresenceChecker(SimpleName cursorSimpleName, String recycleMethodName) {
225+
this.cursorSimpleName = cursorSimpleName;
226+
this.recycleMethodName = recycleMethodName;
227+
}
228+
229+
public boolean isClosePresent() {
230+
return closePresent;
231+
}
232+
233+
/**
234+
* Returns the last statement in the block where the cursor was accessed
235+
* before being assigned again or destroyed.
236+
* @param block Block with the Statement that we want to find
237+
* @return
238+
*/
239+
public Statement getLastCursorStatementInBlock(Block block) {
240+
ASTNode lastCursorStatement = this.lastCursorUse.getParent();
241+
while (lastCursorStatement != null && !block.statements().contains(lastCursorStatement)) {
242+
lastCursorStatement = getAncestor(lastCursorStatement, Statement.class);
243+
}
244+
return (Statement) lastCursorStatement;
245+
}
246+
247+
@Override
248+
public boolean visit(MethodInvocation node) {
249+
if (isSameLocalVariable(cursorSimpleName, node.getExpression())) {
250+
if (this.recycleMethodName.equals(node.getName().getIdentifier())) {
251+
this.closePresent = true;
252+
return DO_NOT_VISIT_SUBTREE;
253+
}
254+
}
255+
return VISIT_SUBTREE;
256+
}
257+
258+
@Override
259+
public boolean visit(SimpleName node) {
260+
if (isSameLocalVariable(node, cursorSimpleName)) {
261+
this.lastCursorUse = node;
262+
}
263+
return VISIT_SUBTREE;
264+
}
265+
266+
@Override
267+
public boolean visit(Assignment node) {
268+
if (isSameLocalVariable(node.getLeftHandSide(), cursorSimpleName)) {
269+
return DO_NOT_VISIT_SUBTREE;
270+
}
271+
return VISIT_SUBTREE;
272+
}
273+
}
274+
275+
/*
276+
* This visitor selects a partial part of the block to make the visit I.e.,
277+
* it will only analyze the visitor from the variable assignment until the
278+
* next assignment or end of the block startNode is a Assignment or
279+
* VariableDeclarationFragment
280+
*/
281+
private class VisitorDecorator extends ASTVisitor {
282+
private ASTVisitor visitor;
283+
private SimpleName cursorSimpleName;
284+
private ASTVisitor specialVisitor;
285+
private ASTNode startNode;
286+
287+
private class NopVisitor extends ASTVisitor {
288+
}
289+
290+
VisitorDecorator(ASTNode startNode, SimpleName cursorSimpleName, ASTVisitor specialVisitor) {
291+
this.cursorSimpleName = cursorSimpleName;
292+
this.specialVisitor = specialVisitor;
293+
this.startNode = startNode;
294+
this.visitor = new NopVisitor();
295+
}
296+
297+
@Override
298+
public boolean visit(Assignment node) {
299+
if (node.equals(startNode)) {
300+
visitor = specialVisitor;
301+
} else if (visitor != null && isSameLocalVariable(node.getLeftHandSide(), cursorSimpleName)) {
302+
visitor = new NopVisitor();
303+
}
304+
return visitor.visit(node);
305+
}
306+
307+
@Override
308+
public boolean visit(VariableDeclarationFragment node) {
309+
if (node.equals(startNode)) {
310+
visitor = specialVisitor;
311+
}
312+
return visitor.visit(node);
313+
}
314+
315+
@Override
316+
public boolean visit(SimpleName node) {
317+
return visitor.visit(node);
318+
}
319+
320+
@Override
321+
public boolean visit(MethodInvocation node) {
322+
return visitor.visit(node);
323+
}
324+
}
325+
}

0 commit comments

Comments
 (0)