blob: 6921d229f5a5bedf5fda26a02b107aa996520f99 [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.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.ResourceRepository;
import com.android.ide.common.util.PathString;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.ResourceReference;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
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.Lint;
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.SourceCodeScanner;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.PsiMethod;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UastLiteralUtils;
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;
/** Looks for layout inflation calls passing null as the view root */
public class LayoutInflationDetector extends LayoutDetector implements SourceCodeScanner {
@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",
"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() {}
@Override
public void afterCheckRootProject(@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(Lint.getBaseName(context.file.getName()));
break;
}
}
}
}
// ---- implements SourceCodeScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(INFLATE);
}
@Override
public void visitMethodCall(
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull PsiMethod method) {
assert method.getName().equals(INFLATE);
if (call.getReceiver() == null) {
return;
}
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() < 2) {
return;
}
UExpression second = arguments.get(1);
if (!UastLiteralUtils.isNullLiteral(second)) {
return;
}
UExpression first = arguments.get(0);
ResourceReference reference = ResourceReference.get(first);
if (reference == null) {
return;
}
String layoutName = reference.getName();
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, call)) {
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, call, context.getLocation(second), ERROR_MESSAGE);
}
}
private static boolean hasLayoutParams(@NonNull JavaContext context, String name) {
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return true; // not certain
}
Project project = context.getProject();
ResourceRepository resources = client.getResourceRepository(project, true, false);
if (resources == null) {
return true; // not certain
}
List<ResourceItem> items =
resources.getResources(ResourceNamespace.TODO(), ResourceType.LAYOUT, name);
for (ResourceItem item : items) {
PathString source = item.getSource();
if (source == null) {
return true; // Not certain.
}
try {
XmlPullParser parser = client.createXmlPullParser(source);
if (parser != null && hasLayoutParams(parser)) {
return true;
}
} catch (XmlPullParserException | IOException e) {
context.log(e, "Could not read/parse inflated layout");
return true; // not certain
}
}
return false;
}
@VisibleForTesting
static boolean hasLayoutParams(@NonNull XmlPullParser parser)
throws XmlPullParserException, IOException {
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 || event == XmlPullParser.END_TAG) {
return false;
}
}
}
}