blob: 3b4ea125f3a7b51ff15dde5563383c7c9450d974 [file] [log] [blame]
/*
* Copyright (C) 2014 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.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.tools.lint.checks.ViewHolderDetector.INFLATE;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
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.Project;
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.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.kxml2.io.KXmlParser;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.MethodInvocation;
import lombok.ast.NullLiteral;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
/**
* Looks for layout inflation calls passing null as the view root
*/
public class LayoutInflationDetector extends LayoutDetector implements Detector.JavaScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
LayoutInflationDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE);
/** Passing in a null parent to a layout inflater */
public static final Issue ISSUE = Issue.create(
"InflateParams", //$NON-NLS-1$
"Layout Inflation without a Parent",
"When inflating a layout, avoid passing in null as the parent view, since " +
"otherwise any layout parameters on the root of the inflated layout will be ignored.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("http://www.doubleencore.com/2013/05/layout-inflation-as-intended");
private static final String ERROR_MESSAGE =
"Avoid passing `null` as the view root (needed to resolve "
+ "layout parameters on the inflated layout's root element)";
/** Constructs a new {@link LayoutInflationDetector} check */
public LayoutInflationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingErrors != null) {
for (Pair<String,Location> pair : mPendingErrors) {
String inflatedLayout = pair.getFirst();
if (mLayoutsWithRootLayoutParams == null ||
!mLayoutsWithRootLayoutParams.contains(inflatedLayout)) {
// No root layout parameters on the inflated layout: no need to complain
continue;
}
Location location = pair.getSecond();
context.report(ISSUE, location, ERROR_MESSAGE);
}
}
}
// ---- Implements XmlScanner ----
private Set<String> mLayoutsWithRootLayoutParams;
private List<Pair<String,Location>> mPendingErrors;
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null) {
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
if (attribute.getLocalName() != null
&& attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
if (mLayoutsWithRootLayoutParams == null) {
mLayoutsWithRootLayoutParams = Sets.newHashSetWithExpectedSize(20);
}
mLayoutsWithRootLayoutParams.add(LintUtils.getBaseName(context.file.getName()));
break;
}
}
}
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(INFLATE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
assert node.astName().astValue().equals(INFLATE);
if (node.astOperand() == null) {
return;
}
StrictListAccessor<Expression, MethodInvocation> arguments = node.astArguments();
if (arguments.size() < 2) {
return;
}
Iterator<Expression> iterator = arguments.iterator();
Expression first = iterator.next();
Expression second = iterator.next();
if (!(second instanceof NullLiteral) || !(first instanceof Select)) {
return;
}
Select select = (Select) first;
Expression operand = select.astOperand();
if (operand instanceof Select) {
Select rLayout = (Select) operand;
if (rLayout.astIdentifier().astValue().equals(ResourceType.LAYOUT.getName()) &&
rLayout.astOperand().toString().endsWith(SdkConstants.R_CLASS)) {
String layoutName = select.astIdentifier().astValue();
if (context.getScope().contains(Scope.RESOURCE_FILE)) {
// We're doing a full analysis run: we can gather this information
// incrementally
if (!context.getDriver().isSuppressed(context, ISSUE, node)) {
if (mPendingErrors == null) {
mPendingErrors = Lists.newArrayList();
}
Location location = context.getLocation(second);
mPendingErrors.add(Pair.of(layoutName, location));
}
} else if (hasLayoutParams(context, layoutName)) {
context.report(ISSUE, node, context.getLocation(second), ERROR_MESSAGE);
}
}
}
super.visitMethod(context, visitor, node);
}
private static boolean hasLayoutParams(@NonNull JavaContext context, String name) {
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return true; // not certain
}
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return true; // not certain
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.LAYOUT, name);
if (items == null || items.isEmpty()) {
return false;
}
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source == null) {
return true; // not certain
}
File file = source.getFile();
if (file.exists()) {
try {
String s = context.getClient().readFile(file);
if (hasLayoutParams(new StringReader(s))) {
return true;
}
} catch (Exception e) {
context.log(e, "Could not read/parse inflated layout");
return true; // not certain
}
}
}
return false;
}
@VisibleForTesting
static boolean hasLayoutParams(@NonNull Reader reader)
throws XmlPullParserException, IOException {
KXmlParser parser = new KXmlParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(reader);
while (true) {
int event = parser.next();
if (event == XmlPullParser.START_TAG) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
String prefix = parser.getAttributePrefix(i);
if (prefix != null && !prefix.isEmpty() &&
ANDROID_URI.equals(parser.getNamespace(prefix))) {
return true;
}
}
}
return false;
} else if (event == XmlPullParser.END_DOCUMENT) {
return false;
}
}
}
}