mirror of
https://github.com/ankidroid/Anki-Android.git
synced 2024-09-20 12:02:16 +02:00
Updated Fixed Title length lint rule
This commit is contained in:
parent
761459fb7c
commit
11667f9de6
@ -12,6 +12,7 @@ import com.ichi2.anki.lint.rules.DirectGregorianInstantiation;
|
||||
import com.ichi2.anki.lint.rules.DirectToastMakeTextUsage;
|
||||
import com.ichi2.anki.lint.rules.DuplicateCrowdInStrings;
|
||||
import com.ichi2.anki.lint.rules.DuplicateTextInPreferencesXml;
|
||||
import com.ichi2.anki.lint.rules.FixedPreferencesTitleLength;
|
||||
import com.ichi2.anki.lint.rules.InconsistentAnnotationUsage;
|
||||
import com.ichi2.anki.lint.rules.NonPublicNonStaticFieldDetector;
|
||||
import com.ichi2.anki.lint.rules.PrintStackTraceUsage;
|
||||
@ -40,6 +41,8 @@ public class IssueRegistry extends com.android.tools.lint.client.api.IssueRegist
|
||||
issues.add(PrintStackTraceUsage.ISSUE);
|
||||
issues.add(NonPublicNonStaticFieldDetector.ISSUE);
|
||||
issues.add(ConstantFieldDetector.ISSUE);
|
||||
issues.add(FixedPreferencesTitleLength.ISSUE_MAX_LENGTH);
|
||||
issues.add(FixedPreferencesTitleLength.ISSUE_TITLE_LENGTH);
|
||||
return issues;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Prateek Singh <prateeksingh3212@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.ichi2.anki.lint.rules;
|
||||
|
||||
import com.android.resources.ResourceFolderType;
|
||||
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.LintFix;
|
||||
import com.android.tools.lint.detector.api.Location;
|
||||
import com.android.tools.lint.detector.api.ResourceXmlDetector;
|
||||
import com.android.tools.lint.detector.api.Scope;
|
||||
import com.android.tools.lint.detector.api.XmlContext;
|
||||
import com.android.tools.lint.detector.api.XmlScanner;
|
||||
import com.android.tools.lint.detector.api.XmlScannerConstants;
|
||||
import com.android.utils.Pair;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.ichi2.anki.lint.utils.Constants;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FixedPreferencesTitleLength extends ResourceXmlDetector implements XmlScanner {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String ID_TITLE_LENGTH = "FixedPreferencesTitleLength";
|
||||
@VisibleForTesting
|
||||
static final String ID_MAX_LENGTH = "PreferencesTitleMaxLengthAttr";
|
||||
private static final int PREFERENCE_TITLE_MAX_LENGTH = 41;
|
||||
@VisibleForTesting
|
||||
static final String DESCRIPTION_TITLE_LENGTH = String.format("Preference titles should be less than %d characters", PREFERENCE_TITLE_MAX_LENGTH);
|
||||
@VisibleForTesting
|
||||
static final String DESCRIPTION_MAX_LENGTH = String.format("Preference titles should contain maxLength=\"%d\" attribute", PREFERENCE_TITLE_MAX_LENGTH);
|
||||
// Around 42 is a hard max on emulators, likely smaller in reality, so use a buffer
|
||||
private static final String EXPLANATION_TITLE_LENGTH = "A title with more than " + PREFERENCE_TITLE_MAX_LENGTH + " characters may fail to display on smaller screens";
|
||||
// Read More: https://support.crowdin.com/file-formats/android-xml/
|
||||
private static final String EXPLANATION_MAX_LENGTH = "Preference Title should contain maxLength attribute " +
|
||||
"because it fixes translated string length";
|
||||
private static final Implementation implementation = new Implementation(FixedPreferencesTitleLength.class, Scope.RESOURCE_FILE_SCOPE);
|
||||
public static final Issue ISSUE_TITLE_LENGTH = Issue.create(
|
||||
ID_TITLE_LENGTH,
|
||||
DESCRIPTION_TITLE_LENGTH,
|
||||
EXPLANATION_TITLE_LENGTH,
|
||||
Constants.ANKI_XML_CATEGORY,
|
||||
Constants.ANKI_XML_PRIORITY,
|
||||
Constants.ANKI_XML_SEVERITY,
|
||||
implementation
|
||||
);
|
||||
public static final Issue ISSUE_MAX_LENGTH = Issue.create(
|
||||
ID_MAX_LENGTH,
|
||||
DESCRIPTION_MAX_LENGTH,
|
||||
EXPLANATION_MAX_LENGTH,
|
||||
Constants.ANKI_XML_CATEGORY,
|
||||
Constants.ANKI_XML_PRIORITY,
|
||||
Constants.ANKI_XML_SEVERITY,
|
||||
implementation
|
||||
);
|
||||
private static final String ATTR_TITLE = "android:title";
|
||||
private static final String ATTR_NAME = "name";
|
||||
private static final String ATTR_MAX_LENGTH = "maxLength";
|
||||
private final Set<String> xmlData = new HashSet<>();
|
||||
private final Map<String, Pair<Element, Location.Handle>> valuesData = new HashMap<>();
|
||||
|
||||
|
||||
public FixedPreferencesTitleLength() {
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Collection<String> getApplicableElements() {
|
||||
return XmlScannerConstants.ALL;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visitElement(XmlContext context, Element element) {
|
||||
/* 1. This condition checks that current file has xml as a parent file.
|
||||
2. Checks that element contains title attribute or not.
|
||||
If both of conditions are true then set the value in xmlSet.
|
||||
*/
|
||||
if ("xml".equals(context.file.getParentFile().getName()) && element.hasAttribute(ATTR_TITLE)) {
|
||||
String stringName = element.getAttribute(ATTR_TITLE).substring(8);
|
||||
xmlData.add(stringName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"values".equals(context.file.getParentFile().getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"10-preferences.xml".equals(context.file.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("resources".equals(element.getTagName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location.Handle handle = context.createLocationHandle(element);
|
||||
handle.setClientData(element);
|
||||
valuesData.put(element.getAttribute(ATTR_NAME), Pair.of(element, handle));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean appliesTo(ResourceFolderType folderType) {
|
||||
return (folderType == ResourceFolderType.XML || folderType == ResourceFolderType.VALUES);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterCheckEachProject(@NotNull Context context) {
|
||||
for (String title : xmlData) {
|
||||
if (!valuesData.containsKey(title)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Pair<Element, Location.Handle> stringData = valuesData.get(title);
|
||||
Element element = stringData.getFirst();
|
||||
|
||||
if (!element.hasAttribute(ATTR_MAX_LENGTH)) {
|
||||
String message = String.format(Locale.ENGLISH, "Preference title '%s' is missing \"maxLength=%d\" attribute", title, PREFERENCE_TITLE_MAX_LENGTH);
|
||||
context.report(ISSUE_MAX_LENGTH, stringData.getSecond().resolve(), message);
|
||||
|
||||
} else if (!element.getAttribute(ATTR_MAX_LENGTH).equals(Integer.toString(PREFERENCE_TITLE_MAX_LENGTH))) {
|
||||
String message = String.format(Locale.ENGLISH, "Preference title '%s' is having maxLength=%s it should contain maxLength=%d", title, element.getAttribute(ATTR_MAX_LENGTH), PREFERENCE_TITLE_MAX_LENGTH);
|
||||
context.report(ISSUE_MAX_LENGTH, stringData.getSecond().resolve(), message);
|
||||
}
|
||||
|
||||
if (element.getTextContent().length() > PREFERENCE_TITLE_MAX_LENGTH) {
|
||||
String message = String.format(Locale.ENGLISH, "Preference title '%s' must be less than %d characters (currently %d)", title, PREFERENCE_TITLE_MAX_LENGTH, element.getTextContent().length());
|
||||
context.report(ISSUE_TITLE_LENGTH, stringData.getSecond().resolve(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -59,4 +59,20 @@ public class Constants {
|
||||
*/
|
||||
public static final Severity ANKI_CODE_STYLE_SEVERITY = Severity.FATAL;
|
||||
|
||||
}
|
||||
/**
|
||||
* A special {@link Category} which groups the Lint issues related to XML as a
|
||||
* sub category for {@link Category#CORRECTNESS}.
|
||||
*/
|
||||
public static final Category ANKI_XML_CATEGORY = create(Category.CORRECTNESS, "XML", 10);
|
||||
|
||||
/**
|
||||
* The priority for the Lint issues used by rules related to XML.
|
||||
*/
|
||||
public static final int ANKI_XML_PRIORITY = 10;
|
||||
|
||||
/**
|
||||
* The severity for the Lint issues used by rules related to XML.
|
||||
*/
|
||||
public static final Severity ANKI_XML_SEVERITY = Severity.FATAL;
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Prateek Singh <prateeksingh3212@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.ichi2.anki.lint.rules;
|
||||
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.junit.Test;
|
||||
|
||||
import static com.android.tools.lint.checks.infrastructure.TestFiles.xml;
|
||||
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
|
||||
|
||||
public class FixedPreferencesTitleLengthTest {
|
||||
|
||||
@Language("XML")
|
||||
public static final String stringsXmlValid = "" +
|
||||
"<resources>\n" +
|
||||
" <string name=\"app_name\" maxLength=\"41\">app_name</string>\n" +
|
||||
" <string name=\"pref_cat_general_summ\">pref_cat_general_summ</string>\n" +
|
||||
" <string name=\"button_sync\" maxLength=\"41\">button_sync</string>\n" +
|
||||
" <string name=\"sync_account\" maxLength=\"41\">sync_account</string>\n" +
|
||||
" <string name=\"sync_account_summ_logged_out\">sync_account_summ</string>\n" +
|
||||
" <string name=\"sync_fetch_missing_media_summ\">sync_fetch_missing</string>\n" +
|
||||
" <string name=\"less_characters_than_limit\" maxLength=\"41\">Less than limit</string>" +
|
||||
"</resources>";
|
||||
|
||||
@Language("XML")
|
||||
public static final String stringsXmlInvalid = "" +
|
||||
"<resources>\n" +
|
||||
"<!--Not contains maxLength=41 attribute-->\n" +
|
||||
" <string name=\"app_name\">app_name</string>\n" +
|
||||
" <string name=\"pref_cat_general_summ\">pref_cat_general_summ</string>\n" +
|
||||
"<!--Not contains maxLength attribute but having value 55-->\n" +
|
||||
" <string name=\"button_sync\" maxLength=\"55\">button_sync</string>\n" +
|
||||
"<!--This is correct because it contains maxLength=41 and character length is less than 42-->\n" +
|
||||
" <string name=\"sync_account\" maxLength=\"41\">sync_account</string>\n" +
|
||||
" <string name=\"sync_account_summ_logged_out\">sync_account_summ</string>\n" +
|
||||
" <string name=\"sync_fetch_missing_media_summ\">sync_fetch_missing</string>\n" +
|
||||
"<!--Not contains maxLength=41 attribute but character length is greater than 32 and contains UTF-8 character-->\n" +
|
||||
" <string name=\"more_characters_than_limit\" maxLength=\"41\">This String contains character more than limit \u00A3</string>\n" +
|
||||
"</resources>";
|
||||
|
||||
@Language("XML")
|
||||
public static final String invalidString = "" +
|
||||
"<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
|
||||
" android:title=\"@string/app_name\"\n" +
|
||||
" android:summary=\"@string/pref_cat_general_summ\" >\n" +
|
||||
" <PreferenceCategory android:title=\"@string/button_sync\" >\n" +
|
||||
" <Preference\n" +
|
||||
" android:dialogTitle=\"@string/sync_account\"\n" +
|
||||
" android:key=\"syncAccount\"\n" +
|
||||
" android:summary=\"@string/sync_account_summ_logged_out\"\n" +
|
||||
" android:title=\"@string/sync_account\" >\n" +
|
||||
" <intent\n" +
|
||||
" android:targetClass=\"com.ichi2.anki.MyAccount\"\n" +
|
||||
" android:targetPackage=\"com.ichi2.anki\" />\n" +
|
||||
" </Preference>\n" +
|
||||
" <CheckBoxPreference\n" +
|
||||
" android:defaultValue=\"true\"\n" +
|
||||
" android:key=\"syncFetchesMedia\"\n" +
|
||||
" android:summary=\"@string/sync_fetch_missing_media_summ\"\n" +
|
||||
" android:title = \"@string/more_characters_than_limit\" />" +
|
||||
" </PreferenceCategory>" +
|
||||
"</PreferenceScreen>";
|
||||
|
||||
|
||||
@Language("XML")
|
||||
public static final String validString = "" +
|
||||
"<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
|
||||
" android:title=\"@string/app_name\"\n" +
|
||||
" android:summary=\"@string/pref_cat_general_summ\" >\n" +
|
||||
" <PreferenceCategory android:title=\"@string/button_sync\" >\n" +
|
||||
" <Preference\n" +
|
||||
" android:dialogTitle=\"@string/sync_account\"\n" +
|
||||
" android:key=\"syncAccount\"\n" +
|
||||
" android:summary=\"@string/sync_account_summ_logged_out\"\n" +
|
||||
" android:title=\"@string/sync_account\" >\n" +
|
||||
" <intent\n" +
|
||||
" android:targetClass=\"com.ichi2.anki.MyAccount\"\n" +
|
||||
" android:targetPackage=\"com.ichi2.anki\" />\n" +
|
||||
" </Preference>\n" +
|
||||
" <CheckBoxPreference\n" +
|
||||
" android:defaultValue=\"true\"\n" +
|
||||
" android:key=\"syncFetchesMedia\"\n" +
|
||||
" android:summary=\"@string/sync_fetch_missing_media_summ\"\n" +
|
||||
" android:title=\"@string/less_characters_than_limit\" />" +
|
||||
" </PreferenceCategory>" +
|
||||
"</PreferenceScreen>";
|
||||
|
||||
|
||||
@Test
|
||||
public void showsErrorForInvalidFile() {
|
||||
lint().allowMissingSdk()
|
||||
.allowCompilationErrors()
|
||||
.files(xml("res/xml/preference_general_invalid.xml", invalidString), xml("res/values/10-preferences.xml", stringsXmlInvalid))
|
||||
.issues(FixedPreferencesTitleLength.ISSUE_TITLE_LENGTH, FixedPreferencesTitleLength.ISSUE_MAX_LENGTH)
|
||||
.run()
|
||||
.expectErrorCount(3)
|
||||
.expect("res/values/10-preferences.xml:12: Error: Preference title 'more_characters_than_limit' must be less than 41 characters (currently 48) [FixedPreferencesTitleLength]\n" +
|
||||
" <string name=\"more_characters_than_limit\" maxLength=\"41\">This String contains character more than limit £</string>\n" +
|
||||
" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" +
|
||||
"res/values/10-preferences.xml:3: Error: Preference title 'app_name' is missing \"maxLength=41\" attribute [PreferencesTitleMaxLengthAttr]\n" +
|
||||
" <string name=\"app_name\">app_name</string>\n" +
|
||||
" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" +
|
||||
"res/values/10-preferences.xml:6: Error: Preference title 'button_sync' is having maxLength=55 it should contain maxLength=41 [PreferencesTitleMaxLengthAttr]\n" +
|
||||
" <string name=\"button_sync\" maxLength=\"55\">button_sync</string>\n" +
|
||||
" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" +
|
||||
"3 errors, 0 warnings");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void showsNoErrorForValidFile() {
|
||||
lint().allowMissingSdk()
|
||||
.allowCompilationErrors()
|
||||
.files(xml("res/xml/preference_general_valid.xml", validString), xml("res/values/10-preferences.xml", stringsXmlValid))
|
||||
.issues(FixedPreferencesTitleLength.ISSUE_MAX_LENGTH , FixedPreferencesTitleLength.ISSUE_TITLE_LENGTH)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user