diff options
author | Tor Norbye <tnorbye@google.com> | 2012-09-17 15:31:34 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-09-17 16:21:36 -0700 |
commit | e97536a52fd5d3ee3d895d6605ccd723f7d5c3f6 (patch) | |
tree | c9341a6dfc33f2a7ecb5ec384ff62d8fb5569209 /lint | |
parent | e4dffb395e5b412583b687305cc8a11a8923bc60 (diff) | |
download | sdk-e97536a52fd5d3ee3d895d6605ccd723f7d5c3f6.zip sdk-e97536a52fd5d3ee3d895d6605ccd723f7d5c3f6.tar.gz sdk-e97536a52fd5d3ee3d895d6605ccd723f7d5c3f6.tar.bz2 |
Add labelFor lint check
Change-Id: If2b905197d5d81c6f98315a0d48ff37091e8282b
Diffstat (limited to 'lint')
7 files changed, 360 insertions, 4 deletions
diff --git a/lint/libs/lint_api/src/com/android/tools/lint/client/api/DefaultSdkInfo.java b/lint/libs/lint_api/src/com/android/tools/lint/client/api/DefaultSdkInfo.java index 1e53834..b5ae26d 100644 --- a/lint/libs/lint_api/src/com/android/tools/lint/client/api/DefaultSdkInfo.java +++ b/lint/libs/lint_api/src/com/android/tools/lint/client/api/DefaultSdkInfo.java @@ -21,6 +21,7 @@ import static com.android.SdkConstants.ABS_LIST_VIEW; import static com.android.SdkConstants.ABS_SEEK_BAR; import static com.android.SdkConstants.ABS_SPINNER; import static com.android.SdkConstants.ADAPTER_VIEW; +import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; import static com.android.SdkConstants.BUTTON; import static com.android.SdkConstants.CHECKED_TEXT_VIEW; import static com.android.SdkConstants.CHECK_BOX; @@ -35,6 +36,7 @@ import static com.android.SdkConstants.IMAGE_BUTTON; import static com.android.SdkConstants.IMAGE_VIEW; import static com.android.SdkConstants.LINEAR_LAYOUT; import static com.android.SdkConstants.LIST_VIEW; +import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; import static com.android.SdkConstants.PROGRESS_BAR; import static com.android.SdkConstants.RADIO_BUTTON; import static com.android.SdkConstants.RADIO_GROUP; @@ -191,6 +193,8 @@ class DefaultSdkInfo extends SdkInfo { PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT); PARENTS.put(GRID_VIEW, ABS_LIST_VIEW); PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT); + PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT); + PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW); PARENTS.put("CheckedTextView", TEXT_VIEW); //$NON-NLS-1$ PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$ @@ -207,12 +211,9 @@ class DefaultSdkInfo extends SdkInfo { PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$ PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$ PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$ - PARENTS.put("AutoCompleteTextView", EDIT_TEXT); //$NON-NLS-1$ PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$ PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$ PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$ - PARENTS.put("MultiAutoCompleteTextView", //$NON-NLS-1$ - "AutoCompleteTextView"); //$NON-NLS-1$ assert PARENTS.size() <= CLASS_COUNT : PARENTS.size(); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java index ca73df0..c5e4e64 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -55,10 +55,11 @@ public class BuiltinIssueRegistry extends IssueRegistry { private static final List<Issue> sIssues; static { - final int initialCapacity = 109; + final int initialCapacity = 110; List<Issue> issues = new ArrayList<Issue>(initialCapacity); issues.add(AccessibilityDetector.ISSUE); + issues.add(LabelForDetector.ISSUE); issues.add(MathDetector.ISSUE); issues.add(FieldGetterDetector.ISSUE); issues.add(SdCardDetector.ISSUE); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/LabelForDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/LabelForDetector.java new file mode 100644 index 0000000..74e510d --- /dev/null +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/LabelForDetector.java @@ -0,0 +1,163 @@ +/* + * 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_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.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$ + "Ensures that text fields are marked with a 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, + LabelForDetector.class, + Scope.RESOURCE_FILE_SCOPE); + + private Set<String> mLabels; + private List<Element> mTextFields; + + /** Constructs a new {@link LabelForDetector} */ + public LabelForDetector() { + } + + @Override + public @NonNull 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) { + 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, null); + } + + } + } + + 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); + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java new file mode 100644 index 0000000..7e139af --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 com.android.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class LabelForDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new LabelForDetector(); + } + + public void test() throws Exception { + assertEquals( + "res/layout/labelfor.xml:54: Warning: No label views point to this text field with an android:labelFor=\"@+id/@+id/editText2\" attribute [LabelFor]\n" + + " <EditText\n" + + " ^\n" + + "res/layout/labelfor.xml:61: Warning: No label views point to this text field with an android:labelFor=\"@+id/@+id/autoCompleteTextView2\" attribute [LabelFor]\n" + + " <AutoCompleteTextView\n" + + " ^\n" + + "res/layout/labelfor.xml:68: Warning: No label views point to this text field with an android:labelFor=\"@+id/@+id/multiAutoCompleteTextView2\" attribute [LabelFor]\n" + + " <MultiAutoCompleteTextView\n" + + " ^\n" + + "0 errors, 3 warnings\n", + + lintProject( + "apicheck/minsdk17.xml=>AndroidManifest.xml", + "res/layout/labelfor.xml" + )); + } + + public void testSuppressed() throws Exception { + assertEquals( + "No warnings.", + + lintProject( + "apicheck/minsdk17.xml=>AndroidManifest.xml", + "res/layout/labelfor_ignore.xml" + )); + } + + + public void testOk() throws Exception { + assertEquals( + "No warnings.", + + lintProject( + "apicheck/minsdk17.xml=>AndroidManifest.xml", + "res/layout/accessibility.xml" + )); + } + + public void testNotApplicable() throws Exception { + assertEquals( + "No warnings.", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "res/layout/labelfor.xml" + )); + } +} + diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/apicheck/minsdk17.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/apicheck/minsdk17.xml new file mode 100644 index 0000000..1837db9 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/apicheck/minsdk17.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="test.bytecode" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk android:minSdkVersion="17" /> + + <application + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" > + <activity + android:name=".BytecodeTestsActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor.xml new file mode 100644 index 0000000..7eec47e --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <TextView + android:id="@+id/textView1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:labelFor="@+id/editText1" + android:text="Medium Text" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <EditText + android:id="@+id/editText1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textPersonName" > + + <requestFocus /> + </EditText> + + <TextView + android:id="@+id/textView2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:labelFor="@+id/autoCompleteTextView1" + android:text="TextView" /> + + <AutoCompleteTextView + android:id="@+id/autoCompleteTextView1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:text="AutoCompleteTextView" /> + + <TextView + android:id="@+id/textView3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:labelFor="@+id/multiAutoCompleteTextView1" + android:text="Large Text" + android:textAppearance="?android:attr/textAppearanceLarge" /> + + <MultiAutoCompleteTextView + android:id="@+id/multiAutoCompleteTextView1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:text="MultiAutoCompleteTextView" /> + + <EditText + android:id="@+id/editText2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textPostalAddress" /> + + <AutoCompleteTextView + android:id="@+id/autoCompleteTextView2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:text="AutoCompleteTextView" /> + + <MultiAutoCompleteTextView + android:id="@+id/multiAutoCompleteTextView2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:text="MultiAutoCompleteTextView" /> + +</LinearLayout>
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor_ignore.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor_ignore.xml new file mode 100644 index 0000000..41d2821 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/labelfor_ignore.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <EditText + android:id="@+id/editText2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textPostalAddress" + tools:ignore="LabelFor"/> + +</LinearLayout> |