blob: 767df50439460e25c56f48a573b053c8e7e0a098 [file] [log] [blame]
/*
* Copyright (C) 2012, 2017 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_DRAWABLE_END;
import static com.android.SdkConstants.ATTR_DRAWABLE_LEFT;
import static com.android.SdkConstants.ATTR_DRAWABLE_RIGHT;
import static com.android.SdkConstants.ATTR_DRAWABLE_START;
import static com.android.SdkConstants.ATTR_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_END_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_START_OF;
import static com.android.SdkConstants.ATTR_LIST_PREFERRED_ITEM_PADDING_END;
import static com.android.SdkConstants.ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_LIST_PREFERRED_ITEM_PADDING_START;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_END;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_START;
import static com.android.SdkConstants.ATTR_TEXT_ALIGNMENT;
import static com.android.SdkConstants.GRAVITY_VALUE_CENTER;
import static com.android.SdkConstants.GRAVITY_VALUE_CENTER_HORIZONTAL;
import static com.android.SdkConstants.GRAVITY_VALUE_CENTER_VERTICAL;
import static com.android.SdkConstants.GRAVITY_VALUE_END;
import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
import static com.android.SdkConstants.GRAVITY_VALUE_LEFT;
import static com.android.SdkConstants.GRAVITY_VALUE_RIGHT;
import static com.android.SdkConstants.GRAVITY_VALUE_START;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TextAlignment;
import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.utils.XmlUtils.getFirstSubTagByName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.UElementHandler;
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.JavaContext;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Lint;
import com.android.tools.lint.detector.api.LintFix;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/** Check which looks for RTL issues (right-to-left support) in layouts */
public class RtlDetector extends LayoutDetector implements SourceCodeScanner {
private static final Implementation IMPLEMENTATION =
new Implementation(
RtlDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.JAVA_FILE, Scope.MANIFEST),
Scope.RESOURCE_FILE_SCOPE,
Scope.JAVA_FILE_SCOPE,
Scope.MANIFEST_SCOPE);
public static final Issue USE_START =
Issue.create(
"RtlHardcoded",
"Using left/right instead of start/end attributes",
"Using `Gravity#LEFT` and `Gravity#RIGHT` can lead to problems when a layout is "
+ "rendered in locales where text flows from right to left. Use `Gravity#START` "
+ "and `Gravity#END` instead. Similarly, in XML `gravity` and `layout_gravity` "
+ "attributes, use `start` rather than `left`.\n"
+ "\n"
+ "For XML attributes such as paddingLeft and `layout_marginLeft`, use `paddingStart` "
+ "and `layout_marginStart`. **NOTE**: If your `minSdkVersion` is less than 17, you should "
+ "add **both** the older left/right attributes **as well as** the new start/end "
+ "attributes. On older platforms, where RTL is not supported and the start/end "
+ "attributes are unknown and therefore ignored, you need the older left/right "
+ "attributes. There is a separate lint check which catches that type of error.\n"
+ "\n"
+ "(Note: For `Gravity#LEFT` and `Gravity#START`, you can use these constants even "
+ "when targeting older platforms, because the `start` bitmask is a superset of the "
+ "`left` bitmask. Therefore, you can use `gravity=\"start\"` rather than "
+ "`gravity=\"left|start\"`.)",
Category.RTL,
5,
Severity.WARNING,
IMPLEMENTATION);
public static final Issue COMPAT =
Issue.create(
"RtlCompat",
"Right-to-left text compatibility issues",
"API 17 adds a `textAlignment` attribute to specify text alignment. However, "
+ "if you are supporting older versions than API 17, you must **also** specify a "
+ "gravity or layout_gravity attribute, since older platforms will ignore the "
+ "`textAlignment` attribute.",
Category.RTL,
6,
Severity.ERROR,
IMPLEMENTATION);
public static final Issue SYMMETRY =
Issue.create(
"RtlSymmetry",
"Padding and margin symmetry",
"If you specify padding or margin on the left side of a layout, you should "
+ "probably also specify padding on the right side (and vice versa) for "
+ "right-to-left layout symmetry.",
Category.RTL,
6,
Severity.WARNING,
IMPLEMENTATION);
public static final Issue ENABLED =
Issue.create(
"RtlEnabled",
"Using RTL attributes without enabling RTL support",
"To enable right-to-left support, when running on API 17 and higher, you must "
+ "set the `android:supportsRtl` attribute in the manifest `<application>` element.\n"
+ "\n"
+ "If you have started adding RTL attributes, but have not yet finished the "
+ "migration, you can set the attribute to false to satisfy this lint check.",
Category.RTL,
3,
Severity.WARNING,
IMPLEMENTATION);
/* TODO:
public static final Issue FIELD = Issue.create(
"RtlFieldAccess",
"Accessing margin and padding fields directly",
"Modifying the padding and margin constants in view objects directly is " +
"problematic when using RTL support, since it can lead to inconsistent states. You " +
"**must** use the corresponding setter methods instead (`View#setPadding` etc).",
Category.RTL, 3, Severity.WARNING, IMPLEMENTATION).setEnabledByDefault(false);
public static final Issue AWARE = Issue.create(
"RtlAware",
"View code not aware of RTL APIs",
"When manipulating views, and especially when implementing custom layouts, " +
"the code may need to be aware of RTL APIs. This lint check looks for usages of " +
"APIs that frequently require adjustments for right-to-left text, and warns if it " +
"does not also see text direction look-ups indicating that the code has already " +
"been updated to handle RTL layouts.",
Category.RTL, 3, Severity.WARNING, IMPLEMENTATION).setEnabledByDefault(false);
*/
private static final String RIGHT_FIELD = "RIGHT";
private static final String LEFT_FIELD = "LEFT";
private static final String FQCN_GRAVITY = "android.view.Gravity";
static final String ATTR_SUPPORTS_RTL = "supportsRtl";
/** API version in which RTL support was added */
private static final int RTL_API = 17;
private static final String LEFT = "Left";
private static final String START = "Start";
private static final String RIGHT = "Right";
private static final String END = "End";
private Boolean mEnabledRtlSupport;
private boolean mUsesRtlAttributes;
private boolean mSpecifiesRtlMode;
/** Constructs a new {@link RtlDetector} */
public RtlDetector() {}
private boolean rtlApplies(@NonNull Context context) {
Project project = context.getMainProject();
if (project.getTargetSdk() < RTL_API) {
return false;
}
int buildTarget = project.getBuildSdk();
if (buildTarget != -1 && buildTarget < RTL_API) {
return false;
}
if (mEnabledRtlSupport != null && !mEnabledRtlSupport) {
return false;
}
// Enabled RTL support not set? Look for it in the merged manifest
if (mEnabledRtlSupport == null) {
mEnabledRtlSupport = getMergedManifestRtlSetting(context);
if (mEnabledRtlSupport == null) {
mEnabledRtlSupport = false;
}
}
return true;
}
@Nullable
private static Boolean getMergedManifestRtlSetting(@NonNull Context context) {
Project mainProject = context.getMainProject();
Document mergedManifest = mainProject.getMergedManifest();
if (mergedManifest == null) {
return null;
}
Element root = mergedManifest.getDocumentElement();
if (root == null) {
return null;
}
Element application = getFirstSubTagByName(root, TAG_APPLICATION);
if (application == null) {
return null;
}
String rtlSupported = application.getAttributeNS(ANDROID_URI, ATTR_SUPPORTS_RTL);
return !VALUE_FALSE.equals(rtlSupported);
}
@Override
public void afterCheckRootProject(@NonNull Context context) {
if (mUsesRtlAttributes && !mSpecifiesRtlMode && rtlApplies(context)) {
List<File> manifestFile = context.getMainProject().getManifestFiles();
if (!manifestFile.isEmpty()) {
Location location = Location.create(manifestFile.get(0));
context.report(
ENABLED,
location,
"The project references RTL attributes, but does not explicitly enable "
+ "or disable RTL support with `android:supportsRtl` in the manifest");
}
}
}
// ---- Implements XmlDetector ----
@VisibleForTesting
static final String[] ATTRIBUTES =
new String[] {
// Pairs, from left/right constants to corresponding start/end constants
ATTR_LAYOUT_ALIGN_PARENT_LEFT, ATTR_LAYOUT_ALIGN_PARENT_START,
ATTR_LAYOUT_ALIGN_PARENT_RIGHT, ATTR_LAYOUT_ALIGN_PARENT_END,
ATTR_LAYOUT_MARGIN_LEFT, ATTR_LAYOUT_MARGIN_START,
ATTR_LAYOUT_MARGIN_RIGHT, ATTR_LAYOUT_MARGIN_END,
ATTR_PADDING_LEFT, ATTR_PADDING_START,
ATTR_PADDING_RIGHT, ATTR_PADDING_END,
ATTR_DRAWABLE_LEFT, ATTR_DRAWABLE_START,
ATTR_DRAWABLE_RIGHT, ATTR_DRAWABLE_END,
ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT, ATTR_LIST_PREFERRED_ITEM_PADDING_START,
ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT, ATTR_LIST_PREFERRED_ITEM_PADDING_END,
// RelativeLayout
ATTR_LAYOUT_TO_LEFT_OF, ATTR_LAYOUT_TO_START_OF,
ATTR_LAYOUT_TO_RIGHT_OF, ATTR_LAYOUT_TO_END_OF,
ATTR_LAYOUT_ALIGN_LEFT, ATTR_LAYOUT_ALIGN_START,
ATTR_LAYOUT_ALIGN_RIGHT, ATTR_LAYOUT_ALIGN_END,
};
static {
if (Lint.assertionsEnabled()) {
for (int i = 0; i < ATTRIBUTES.length; i += 2) {
String replace = ATTRIBUTES[i];
String with = ATTRIBUTES[i + 1];
assert with.equals(convertOldToNew(replace));
assert replace.equals(convertNewToOld(with));
}
}
}
public static boolean isRtlAttributeName(@NonNull String attribute) {
for (int i = 1; i < ATTRIBUTES.length; i += 2) {
if (attribute.equals(ATTRIBUTES[i])) {
return true;
}
}
return false;
}
@VisibleForTesting
static String convertOldToNew(String attribute) {
if (attribute.contains(LEFT)) {
return attribute.replace(LEFT, START);
} else {
return attribute.replace(RIGHT, END);
}
}
@VisibleForTesting
static String convertNewToOld(String attribute) {
if (attribute.contains(START)) {
return attribute.replace(START, LEFT);
} else {
return attribute.replace(END, RIGHT);
}
}
@VisibleForTesting
static String convertToOppositeDirection(String attribute) {
if (attribute.contains(LEFT)) {
return attribute.replace(LEFT, RIGHT);
} else if (attribute.contains(RIGHT)) {
return attribute.replace(RIGHT, LEFT);
} else if (attribute.contains(START)) {
return attribute.replace(START, END);
} else {
return attribute.replace(END, START);
}
}
@Nullable
static String getTextAlignmentToGravity(String attribute) {
if (attribute.equals(TextAlignment.CENTER)) {
return GRAVITY_VALUE_CENTER_HORIZONTAL;
} else if (attribute.endsWith(START)) { // textStart, viewStart, ...
return GRAVITY_VALUE_START;
} else if (attribute.endsWith(END)) { // textEnd, viewEnd, ...
return GRAVITY_VALUE_END;
} else {
return null; // inherit, others
}
}
@Override
public Collection<String> getApplicableAttributes() {
int size = ATTRIBUTES.length + 4;
List<String> attributes = new ArrayList<>(size);
// For detecting whether RTL support is enabled
attributes.add(ATTR_SUPPORTS_RTL);
// For detecting left/right attributes which should probably be
// migrated to start/end
attributes.add(ATTR_GRAVITY);
attributes.add(ATTR_LAYOUT_GRAVITY);
// For detecting existing attributes which indicate an attempt to
// use RTL
attributes.add(ATTR_TEXT_ALIGNMENT);
// Add conversion attributes: left/right attributes to nominate
// attributes that should be added as start/end, and start/end
// attributes to use to look up elements that should have compatibility
// left/right ones as well
Collections.addAll(attributes, ATTRIBUTES);
assert attributes.size() == size : attributes.size();
return attributes;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
Project project = context.getMainProject();
String value = attribute.getValue();
if (!ANDROID_URI.equals(attribute.getNamespaceURI())) {
// Layout attribute not in the Android namespace (or a custom namespace).
// This is likely an application error (which should get caught by
// the MissingPrefixDetector)
return;
}
String name = attribute.getLocalName();
assert name != null : attribute.getName();
if (name.equals(ATTR_SUPPORTS_RTL)) {
mSpecifiesRtlMode = true;
mEnabledRtlSupport = Boolean.valueOf(value);
if (!attribute.getOwnerElement().getTagName().equals(TAG_APPLICATION)) {
context.report(
ENABLED,
attribute,
context.getLocation(attribute),
String.format(
"Wrong declaration: `%1$s` should be defined on the `<application>` element",
attribute.getName()));
}
int targetSdk = project.getTargetSdk();
if (mEnabledRtlSupport && targetSdk < RTL_API) {
String message =
String.format(
Locale.getDefault(),
"You must set `android:targetSdkVersion` to at least %1$d when "
+ "enabling RTL support (is %2$d)",
RTL_API,
project.getTargetSdk());
context.report(ENABLED, attribute, context.getValueLocation(attribute), message);
}
return;
}
if (!rtlApplies(context)) {
return;
}
if (name.equals(ATTR_TEXT_ALIGNMENT)) {
if (context.getProject().getReportIssues()) {
mUsesRtlAttributes = true;
}
Element element = attribute.getOwnerElement();
final String gravitySpec;
final Attr gravityNode;
if (element.hasAttributeNS(ANDROID_URI, ATTR_GRAVITY)) {
gravityNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_GRAVITY);
gravitySpec = gravityNode.getValue();
} else if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY)) {
gravityNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
gravitySpec = gravityNode.getValue();
} else if (project.getMinSdk() < RTL_API) {
int folderVersion = context.getFolderVersion();
if (folderVersion < RTL_API && context.isEnabled(COMPAT)) {
String expectedGravity = getTextAlignmentToGravity(value);
if (expectedGravity != null) {
String message =
String.format(
Locale.getDefault(),
"To support older versions than API 17 (project specifies %1$d) "
+ "you must **also** specify `gravity` or `layout_gravity=\"%2$s\"`",
project.getMinSdk(),
expectedGravity);
LintFix fix1 =
fix().set(ANDROID_URI, ATTR_GRAVITY, expectedGravity).build();
LintFix fix2 =
fix().set(ANDROID_URI, ATTR_LAYOUT_GRAVITY, expectedGravity)
.build();
LintFix fix = fix().alternatives(fix1, fix2);
context.report(
COMPAT,
attribute,
context.getNameLocation(attribute),
message,
fix);
}
}
return;
} else {
return;
}
String expectedGravity = getTextAlignmentToGravity(value);
if (expectedGravity != null && context.isEnabled(COMPAT)) {
List<String> gravities = new ArrayList<>();
for (String g : gravitySpec.split("\\|")) {
g = g.trim();
gravities.add(g);
if (g.equals(GRAVITY_VALUE_CENTER)) {
gravities.add(GRAVITY_VALUE_CENTER_HORIZONTAL);
gravities.add(GRAVITY_VALUE_CENTER_VERTICAL);
}
if (g.equals(GRAVITY_VALUE_FILL)) {
gravities.add(GRAVITY_VALUE_FILL_HORIZONTAL);
gravities.add(GRAVITY_VALUE_FILL_VERTICAL);
}
}
if (gravities.stream().noneMatch(g -> g.equals(expectedGravity))) {
String message =
String.format(
"Inconsistent alignment specification between `textAlignment` and "
+ "`gravity` attributes: was `%1$s`, expected `%2$s`",
gravitySpec, expectedGravity);
Location location = context.getValueLocation(attribute);
context.report(COMPAT, attribute, location, message);
Location secondary = context.getValueLocation(gravityNode);
secondary.setMessage("Incompatible direction here");
location.setSecondary(secondary);
}
}
}
if (name.equals(ATTR_GRAVITY) || name.equals(ATTR_LAYOUT_GRAVITY)) {
boolean isLeft = value.contains(GRAVITY_VALUE_LEFT);
boolean isRight = value.contains(GRAVITY_VALUE_RIGHT);
if (!isLeft && !isRight) {
if ((value.contains(GRAVITY_VALUE_START) || value.contains(GRAVITY_VALUE_END))
&& context.getProject().getReportIssues()) {
mUsesRtlAttributes = true;
}
return;
}
String message =
String.format(
"Use \"`%1$s`\" instead of \"`%2$s`\" to ensure correct behavior in "
+ "right-to-left locales",
isLeft ? GRAVITY_VALUE_START : GRAVITY_VALUE_END,
isLeft ? GRAVITY_VALUE_LEFT : GRAVITY_VALUE_RIGHT);
if (context.isEnabled(USE_START)) {
context.report(USE_START, attribute, context.getValueLocation(attribute), message);
}
return;
}
// Some other left/right/start/end attribute
int targetSdk = project.getTargetSdk();
// TODO: If attribute is drawableLeft or drawableRight, add note that you might
// want to consider adding a specialized image in the -ldrtl folder as well
Element element = attribute.getOwnerElement();
boolean isPaddingAttribute = isPaddingAttribute(name);
if (isPaddingAttribute || isMarginAttribute(name)) {
String opposite = convertToOppositeDirection(name);
if (element.hasAttributeNS(ANDROID_URI, opposite)) {
String oldValue = element.getAttributeNS(ANDROID_URI, opposite);
if (value.equals(oldValue)) {
return;
}
} else if (isPaddingAttribute
&& !element.hasAttributeNS(
ANDROID_URI,
isOldAttribute(opposite)
? convertOldToNew(opposite)
: convertNewToOld(opposite))
&& context.isEnabled(SYMMETRY)) {
String message =
String.format(
"When you define `%1$s` you should probably also define `%2$s` for "
+ "right-to-left symmetry",
name, opposite);
context.report(SYMMETRY, attribute, context.getNameLocation(attribute), message);
}
}
boolean isOld = isOldAttribute(name);
if (isOld) {
if (!context.isEnabled(USE_START)) {
return;
}
String rtl = convertOldToNew(name);
if (element.hasAttributeNS(ANDROID_URI, rtl)) {
if (project.getMinSdk() >= RTL_API || context.getFolderVersion() >= RTL_API) {
// Warn that left/right isn't needed
String message =
String.format(
"Redundant attribute `%1$s`; already defining `%2$s` with "
+ "`targetSdkVersion` %3$s",
name, rtl, targetSdk);
LintFix fix = fix().unset(ANDROID_URI, name).autoFix().build();
context.report(
USE_START, attribute, context.getNameLocation(attribute), message, fix);
}
} else {
String message;
LintFix lintFix;
if (project.getMinSdk() >= RTL_API || context.getFolderVersion() >= RTL_API) {
message =
String.format(
"Consider replacing `%1$s` with `%2$s:%3$s=\"%4$s\"` to better support "
+ "right-to-left layouts",
attribute.getName(), attribute.getPrefix(), rtl, value);
lintFix =
fix().replace()
.name(
String.format(
"Replace with %1$s:%2$s=\"%3$s\"",
attribute.getPrefix(), rtl, value))
.text(name)
.with(rtl)
.build();
} else {
message =
String.format(
"Consider adding `%1$s:%2$s=\"%3$s\"` to better support "
+ "right-to-left layouts",
attribute.getPrefix(), rtl, value);
lintFix =
fix().name(
String.format(
"Add %1$s:%2$s=\"%3$s\"",
attribute.getPrefix(), rtl, value))
.set(attribute.getNamespaceURI(), rtl, attribute.getValue())
.build();
}
context.report(
USE_START, attribute, context.getNameLocation(attribute), message, lintFix);
}
} else {
if (project.getMinSdk() >= RTL_API || !context.isEnabled(COMPAT)) {
// Only supporting 17+: no need to define older attributes
return;
}
int folderVersion = context.getFolderVersion();
if (folderVersion >= RTL_API) {
// In a -v17 folder or higher: no need to define older attributes
return;
}
String old = convertNewToOld(name);
if (element.hasAttributeNS(ANDROID_URI, old)) {
return;
}
String oldValue = convertNewToOld(value);
String message =
String.format(
Locale.getDefault(),
"To support older versions than API 17 (project specifies %1$d) "
+ "you should **also** add `%2$s:%3$s=\"%4$s\"`",
project.getMinSdk(),
attribute.getPrefix(),
old,
oldValue);
LintFix fix = fix().set(attribute.getNamespaceURI(), old, oldValue).build();
context.report(COMPAT, attribute, context.getNameLocation(attribute), message, fix);
}
}
private static boolean isOldAttribute(String name) {
return name.contains(LEFT) || name.contains(RIGHT);
}
private static boolean isMarginAttribute(@NonNull String name) {
return name.startsWith(ATTR_LAYOUT_MARGIN);
}
private static boolean isPaddingAttribute(@NonNull String name) {
return name.startsWith(ATTR_PADDING);
}
// ---- implements SourceCodeScanner ----
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.singletonList(USimpleNameReferenceExpression.class);
}
@Nullable
@Override
public UElementHandler createUastHandler(@NonNull JavaContext context) {
if (rtlApplies(context)) {
return new IdentifierChecker(context);
}
return null;
}
private static class IdentifierChecker extends UElementHandler {
private final JavaContext context;
public IdentifierChecker(JavaContext context) {
this.context = context;
}
@Override
public void visitSimpleNameReferenceExpression(
@NonNull USimpleNameReferenceExpression element) {
String identifier = element.getIdentifier();
boolean isLeft = LEFT_FIELD.equals(identifier);
boolean isRight = RIGHT_FIELD.equals(identifier);
if (!isLeft && !isRight) {
return;
}
PsiElement resolved = element.resolve();
if (!(resolved instanceof PsiField)) {
return;
} else {
PsiField field = (PsiField) resolved;
if (!context.getEvaluator().isMemberInClass(field, FQCN_GRAVITY)) {
return;
}
}
String message =
String.format(
"Use \"`Gravity.%1$s`\" instead of \"`Gravity.%2$s`\" to ensure correct "
+ "behavior in right-to-left locales",
(isLeft ? GRAVITY_VALUE_START : GRAVITY_VALUE_END)
.toUpperCase(Locale.US),
(isLeft ? GRAVITY_VALUE_LEFT : GRAVITY_VALUE_RIGHT)
.toUpperCase(Locale.US));
Location location = context.getLocation(element);
context.report(USE_START, element, location, message);
}
}
}