/* * 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.ANDROID_URI; import static com.android.SdkConstants.ATTR_HINT; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LABEL_FOR; import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; import static com.android.SdkConstants.EDIT_TEXT; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix; import com.android.annotations.NonNull; import com.android.annotations.Nullable; 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.LayoutDetector; import com.android.tools.lint.detector.api.Location; 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.collect.Sets; import org.w3c.dom.Attr; import org.w3c.dom.Element; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; /** * Detector which finds unlabeled text fields */ public class LabelForDetector extends LayoutDetector { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "LabelFor", //$NON-NLS-1$ "Missing `labelFor` attribute", "Text fields should be labelled with a `labelFor` attribute, " + "provided your `minSdkVersion` is at least 17.\n" + "\n" + "If your view is labeled but by a label in a different layout which " + "includes this one, just suppress this warning from lint.", Category.A11Y, 2, Severity.WARNING, new Implementation( LabelForDetector.class, Scope.RESOURCE_FILE_SCOPE)); private Set<String> mLabels; private List<Element> mTextFields; /** Constructs a new {@link LabelForDetector} */ public LabelForDetector() { } @NonNull @Override public Speed getSpeed() { return Speed.FAST; } @Override @Nullable public Collection<String> getApplicableAttributes() { return Collections.singletonList(ATTR_LABEL_FOR); } @Override public Collection<String> getApplicableElements() { return Arrays.asList( EDIT_TEXT, AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW ); } @Override public void afterCheckFile(@NonNull Context context) { if (mTextFields != null) { if (mLabels == null) { mLabels = Collections.emptySet(); } for (Element element : mTextFields) { if (element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) { continue; } String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); boolean missing = true; if (mLabels.contains(id)) { missing = false; } else if (id.startsWith(NEW_ID_PREFIX)) { missing = !mLabels.contains(ID_PREFIX + stripIdPrefix(id)); } else if (id.startsWith(ID_PREFIX)) { missing = !mLabels.contains(NEW_ID_PREFIX + stripIdPrefix(id)); } if (missing) { XmlContext xmlContext = (XmlContext) context; Location location = xmlContext.getLocation(element); String message; if (id == null || id.isEmpty()) { message = "No label views point to this text field with a " + "`labelFor` attribute"; } else { message = String.format("No label views point to this text field with " + "an `android:labelFor=\"@+id/%1$s\"` attribute", id); } xmlContext.report(ISSUE, element, location, message); } } } mLabels = null; mTextFields = null; } @Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { if (mLabels == null) { mLabels = Sets.newHashSet(); } mLabels.add(attribute.getValue()); } @Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { // NOTE: This should NOT be checking *minSdkVersion*, but *targetSdkVersion* // or even buildTarget instead. However, there's a risk that this will flag // way too much and make the rule annoying until API 17 support becomes // more widespread, so for now limit the check to those projects *really* // working with 17. When API 17 reaches a certain amount of adoption, change // this to flag all apps supporting 17, including those supporting earlier // versions as well. if (context.getMainProject().getMinSdk() < 17) { return; } if (mTextFields == null) { mTextFields = new ArrayList<Element>(); } mTextFields.add(element); } }