| /* |
| * 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; |
| } |
| } |
| } |
| } |