blob: f633c6cf53196c0a8873cb42e517a7e29a09f966 [file] [log] [blame]
/*
* 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.ATTR_ON_CLICK;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
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.Context;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
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 com.android.tools.lint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks for missing onClick handlers
*/
public class OnClickDetector extends LayoutDetector implements ClassScanner {
/** Missing onClick handlers */
public static final Issue ISSUE = Issue.create(
"OnClick", //$NON-NLS-1$
"`onClick` method does not exist",
"The `onClick` attribute value should be the name of a method in this View's context " +
"to invoke when the view is clicked. This name must correspond to a public method " +
"that takes exactly one parameter of type `View`.\n" +
"\n" +
"Must be a string value, using '\\;' to escape characters such as '\\n' or " +
"'\\uxxxx' for a unicode character.",
Category.CORRECTNESS,
10,
Severity.ERROR,
new Implementation(
OnClickDetector.class,
Scope.CLASS_AND_ALL_RESOURCE_FILES));
private Map<String, Location.Handle> mNames;
private Map<String, List<String>> mSimilar;
private boolean mHaveBytecode;
/** Constructs a new {@link OnClickDetector} */
public OnClickDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mNames != null && !mNames.isEmpty() && mHaveBytecode) {
List<String> names = new ArrayList<String>(mNames.keySet());
Collections.sort(names);
LintDriver driver = context.getDriver();
for (String name : names) {
Handle handle = mNames.get(name);
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (driver.isSuppressed(null, ISSUE, (Node) clientData)) {
continue;
}
}
Location location = handle.resolve();
String message = String.format(
"Corresponding method handler '`public void %1$s(android.view.View)`' not found",
name);
List<String> similar = mSimilar != null ? mSimilar.get(name) : null;
if (similar != null) {
Collections.sort(similar);
message += String.format(" (did you mean `%1$s` ?)", Joiner.on(", ").join(similar));
}
context.report(ISSUE, location, message);
}
}
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ON_CLICK);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getValue();
if (value.isEmpty() || value.trim().isEmpty()) {
context.report(ISSUE, attribute, context.getLocation(attribute),
"`onClick` attribute value cannot be empty");
} else if (!value.equals(value.trim())) {
context.report(ISSUE, attribute, context.getLocation(attribute),
"There should be no whitespace around attribute values");
} else if (!value.startsWith(PREFIX_RESOURCE_REF)) { // Not resolved
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mNames == null) {
mNames = new HashMap<String, Location.Handle>();
}
Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
// Replace unicode characters with the actual value since that's how they
// appear in the ASM signatures
if (value.contains("\\u")) { //$NON-NLS-1$
Pattern pattern = Pattern.compile("\\\\u(\\d\\d\\d\\d)"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(value);
StringBuilder sb = new StringBuilder(value.length());
int remainder = 0;
while (matcher.find()) {
sb.append(value.substring(0, matcher.start()));
String unicode = matcher.group(1);
int hex = Integer.parseInt(unicode, 16);
sb.append((char) hex);
remainder = matcher.end();
}
sb.append(value.substring(remainder));
value = sb.toString();
}
mNames.put(value, handle);
}
}
// ---- Implements ClassScanner ----
@SuppressWarnings("rawtypes")
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
if (mNames == null) {
// No onClick attributes in the XML files
return;
}
mHaveBytecode = true;
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
boolean rightArguments = method.desc.equals("(Landroid/view/View;)V"); //$NON-NLS-1$
if (!mNames.containsKey(method.name)) {
if (rightArguments) {
// See if there's a possible typo instead
for (String n : mNames.keySet()) {
if (LintUtils.editDistance(n, method.name) <= 2) {
recordSimilar(n, classNode, method);
break;
}
}
}
continue;
}
// TODO: Validate class hierarchy: should extend a context method
// Longer term, also validate that it's in a layout that corresponds to
// the given activity
if (rightArguments){
// Found: remove from list to be checked
mNames.remove(method.name);
// Make sure the method is public
if ((method.access & Opcodes.ACC_PUBLIC) == 0) {
Location location = context.getLocation(method, classNode);
String message = String.format(
"On click handler `%1$s(View)` must be public",
method.name);
context.report(ISSUE, location, message);
} else if ((method.access & Opcodes.ACC_STATIC) != 0) {
Location location = context.getLocation(method, classNode);
String message = String.format(
"On click handler `%1$s(View)` should not be static",
method.name);
context.report(ISSUE, location, message);
}
if (mNames.isEmpty()) {
mNames = null;
return;
}
}
}
}
private void recordSimilar(String name, ClassNode classNode, MethodNode method) {
if (mSimilar == null) {
mSimilar = new HashMap<String, List<String>>();
}
List<String> list = mSimilar.get(name);
if (list == null) {
list = new ArrayList<String>();
mSimilar.put(name, list);
}
String signature = ClassContext.createSignature(classNode.name, method.name, method.desc);
list.add(signature);
}
}