blob: acb7dec704da4ba185176c7ff9858a04470df9cb [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.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);
}
}