0
0
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:
Prateek Singh 2021-04-17 20:30:58 +05:30 committed by Arthur Milchior
parent 761459fb7c
commit 11667f9de6
4 changed files with 313 additions and 1 deletions

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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();
}
}