blob: e3bb699c968c03e13757708feb2c77bd036fee75 [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.ATTR_SHOW_AS_ACTION;
import static com.android.SdkConstants.VALUE_ALWAYS;
import static com.android.SdkConstants.VALUE_IF_ROOM;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
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.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.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.android.tools.lint.detector.api.XmlContext;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UReferenceExpression;
import org.w3c.dom.Attr;
/**
* Check which looks for usage of showAsAction="always" in menus (or MenuItem.SHOW_AS_ACTION_ALWAYS
* in code), which is usually a style guide violation. (Use ifRoom instead).
*/
public class AlwaysShowActionDetector extends ResourceXmlDetector implements SourceCodeScanner {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
public static final Issue ISSUE =
Issue.create(
"AlwaysShowAction",
"Usage of `showAsAction=always`",
"Using `showAsAction=\"always\"` in menu XML, or `MenuItem.SHOW_AS_ACTION_ALWAYS` in "
+ "Java code is usually a deviation from the user interface style guide."
+ "Use `ifRoom` or the corresponding `MenuItem.SHOW_AS_ACTION_IF_ROOM` instead.\n"
+ "\n"
+ "If `always` is used sparingly there are usually no problems and behavior is "
+ "roughly equivalent to `ifRoom` but with preference over other `ifRoom` "
+ "items. Using it more than twice in the same menu is a bad idea.\n"
+ "\n"
+ "This check looks for menu XML files that contain more than two `always` "
+ "actions, or some `always` actions and no `ifRoom` actions. In Java code, "
+ "it looks for projects that contain references to `MenuItem.SHOW_AS_ACTION_ALWAYS` "
+ "and no references to `MenuItem.SHOW_AS_ACTION_IF_ROOM`.",
Category.USABILITY,
3,
Severity.WARNING,
new Implementation(
AlwaysShowActionDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/design/patterns/actionbar.html");
/** List of showAsAction attributes appearing in the current menu XML file */
private List<Attr> mFileAttributes;
/** If at least one location has been marked ignore in this file, ignore all */
private boolean mIgnoreFile;
/** List of locations of MenuItem.SHOW_AS_ACTION_ALWAYS references in Java code */
private List<Location> mAlwaysFields;
/** True if references to MenuItem.SHOW_AS_ACTION_IF_ROOM were found */
private boolean mHasIfRoomRefs;
/** Constructs a new {@link AlwaysShowActionDetector} */
public AlwaysShowActionDetector() {}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.MENU;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_SHOW_AS_ACTION);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mFileAttributes = null;
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (mIgnoreFile) {
mFileAttributes = null;
return;
}
if (mFileAttributes != null) {
assert context instanceof XmlContext; // mFileAttributes is only set in XML files
List<Attr> always = new ArrayList<>();
List<Attr> ifRoom = new ArrayList<>();
for (Attr attribute : mFileAttributes) {
String value = attribute.getValue();
if (value.equals(VALUE_ALWAYS)) {
always.add(attribute);
} else if (value.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
} else if (value.indexOf('|') != -1) {
String[] flags = value.split("\\|");
for (String flag : flags) {
if (flag.equals(VALUE_ALWAYS)) {
always.add(attribute);
break;
} else if (flag.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
break;
}
}
}
}
if (!always.isEmpty() && mFileAttributes.size() > 1) {
// Complain if you're using more than one "always", or if you're
// using "always" and aren't using "ifRoom" at all (and provided you
// have more than a single item)
if (always.size() > 2 || ifRoom.isEmpty()) {
XmlContext xmlContext = (XmlContext) context;
Location location = null;
for (int i = always.size() - 1; i >= 0; i--) {
Location next = location;
location = xmlContext.getLocation(always.get(i));
if (next != null) {
location.setSecondary(next);
}
}
if (location != null) {
LintFix fix = fix().replace().pattern("(always)").with("ifRoom").build();
context.report(
ISSUE,
location,
"Prefer \"`ifRoom`\" instead of \"`always`\"",
fix);
}
}
}
}
}
@Override
public void afterCheckRootProject(@NonNull Context context) {
if (mAlwaysFields != null && !mHasIfRoomRefs) {
for (Location location : mAlwaysFields) {
context.report(
ISSUE,
location,
"Prefer \"`SHOW_AS_ACTION_IF_ROOM`\" instead of \"`SHOW_AS_ACTION_ALWAYS`\"");
}
}
}
// ---- Implements XmlScanner ----
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (context.getDriver().isSuppressed(context, ISSUE, attribute)) {
mIgnoreFile = true;
return;
}
if (mFileAttributes == null) {
mFileAttributes = new ArrayList<>();
}
mFileAttributes.add(attribute);
}
// ---- implements SourceCodeScanner ----
@Nullable
@Override
public List<String> getApplicableReferenceNames() {
return Arrays.asList("SHOW_AS_ACTION_IF_ROOM", "SHOW_AS_ACTION_ALWAYS");
}
@Override
public void visitReference(
@NonNull JavaContext context,
@NonNull UReferenceExpression reference,
@NonNull PsiElement resolved) {
if (resolved instanceof PsiField
&& context.getEvaluator()
.isMemberInClass((PsiField) resolved, "android.view.MenuItem")) {
if ("SHOW_AS_ACTION_ALWAYS".equals(reference.getResolvedName())) {
if (context.getDriver().isSuppressed(context, ISSUE, reference)) {
return;
}
if (mAlwaysFields == null) {
mAlwaysFields = new ArrayList<>();
}
mAlwaysFields.add(context.getLocation(reference));
} else {
mHasIfRoomRefs = true;
}
}
}
}