Skip to content

Instantly share code, notes, and snippets.

@jayrambhia
Last active June 22, 2021 19:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jayrambhia/8d2416f7ec197b418ae3d762d67a7e80 to your computer and use it in GitHub Desktop.
Save jayrambhia/8d2416f7ec197b418ae3d762d67a7e80 to your computer and use it in GitHub Desktop.
ColorDetector - Lint detector
public class ColorDetector extends ResourceXmlDetector {
private static final String ID = "CustomColors";
private static final String DESCRIPTION = "Custom colors used";
private static final String EXPLANATION = "Use pre-defined (allowed) colors only";
private static final Category CATEGORY = Category.CORRECTNESS;
private static final int PRIORITY = 6;
private static final Severity SEVERITY = Severity.ERROR;
public static final Issue ISSUE = Issue.create(
ID,
DESCRIPTION,
EXPLANATION,
CATEGORY,
PRIORITY,
SEVERITY,
new Implementation(ColorDetector.class, Scope.RESOURCE_FILE_SCOPE)
);
private Set<String> predefinedColors = new HashSet<>();
private Set<String> allowedColors = new HashSet<>();
private List<Pair<Attr, Location>> colorUsages = new ArrayList<>();
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList("color");
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList("color", "textColor", "background");
}
@Override
public void beforeCheckProject(Context context) {
// set-up the project
allowedColors.clear();
colorUsages.clear();
// add predefined colors.
// predefinedColors.add("my_awesome_color");
}
@Override
public void visitElement(XmlContext context, Element element) {
String value = null;
NodeList nodes = element.getChildNodes();
if (nodes.getLength() == 1) {
// <color> element should have only one node
Node node = nodes.item(0);
// This gives us the color, eg. @color/my_awesome_color, #DDFFFF, etc
value = node.getNodeValue();
}
if (value == null) {
// Ideally, this should not happen.
return;
}
if (value.startsWith("@color/")) {
// check if the color name exists in predefined colors.
if (predefinedColors.contains(value.substring(7))) {
allowedColors.add(element.getAttribute("name"));
return;
}
}
context.report(ISSUE, context.getLocation(element), "custom colors should refer to predefined colors");
}
@Override
public void visitAttribute(XmlContext context, Attr attribute) {
String content = attribute.getTextContent();
if (content.startsWith("@color/")) {
if (!predefinedColors.contains(content.substring(7))) {
// the color is a custom color
colorUsages.add(Pair.of(attribute, context.getLocation(attribute)));
}
} else if (content.startsWith("#")) {
context.report(ISSUE, context.getLocation(attribute), "Do not use hardcoded colors");
}
}
@Override
public void afterCheckProject(Context context) {
// filter and report
for (Pair<Attr, Location> usage: colorUsages) {
String content = usage.getFirst().getTextContent();
if (content.startsWith("@color/")) {
if (!allowedColors.contains(content.substring(7))) {
context.report(ISSUE, usage.getSecond(), "custom color does not refer to predefined colors");
}
} else if (content.startsWith("#")) {
context.report(ISSUE, usage.getSecond(), "Do not use hardcoded colors");
}
}
// clean-up
allowedColors.clear();
colorUsages.clear();
predefinedColors.clear();
}
}
public class ColorDetector extends ResourceXmlDetector {
private static final String ID = "CustomColors";
private static final String DESCRIPTION = "Custom colors used";
private static final String EXPLANATION = "Use pre-defined (allowed) colors only";
private static final Category CATEGORY = Category.CORRECTNESS;
private static final int PRIORITY = 6;
private static final Severity SEVERITY = Severity.ERROR;
public static final Issue ISSUE = Issue.create(
ID,
DESCRIPTION,
EXPLANATION,
CATEGORY,
PRIORITY,
SEVERITY,
new Implementation(ColorDetector.class, Scope.RESOURCE_FILE_SCOPE)
);
private static final String TOOLS_SCHEMA = "http://schemas.android.com/tools";
private Set<String> predefinedColors = new HashSet<>();
private Set<String> allowedColors = new HashSet<>();
// If the user has used tools:ingore, we don't want to raise issues for its usage.
private Set<String> ignoredColors = new HashSet<>();
private List<Pair<Attr, Location>> colorUsages = new ArrayList<>();
// For res/color/ files.
private int errorInAFile;
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList("color");
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList("color", "textColor", "background");
}
@Override
public void beforeCheckProject(Context context) {
// set-up the project
allowedColors.clear();
colorUsages.clear();
// add predefined colors.
// predefinedColors.add("my_awesome_color");
}
@Override
public void beforeCheckFile(Context context) {
errorsInAFile = 0;
}
@Override
public void visitElement(XmlContext context, Element element) {
String value = null;
NodeList nodes = element.getChildNodes();
if (nodes.getLength() == 1) {
// <color> element should have only one node
Node node = nodes.item(0);
// This gives us the color, eg. @color/my_awesome_color, #DDFFFF, etc
value = node.getNodeValue();
}
if (value == null) {
// Ideally, this should not happen.
return;
}
if (value.startsWith("@color/")) {
// check if the color name exists in predefined colors.
if (predefinedColors.contains(value.substring(7))) {
allowedColors.add(element.getAttribute("name"));
return;
}
errorsInAFile++;
String ignored = element.getAttributeNS(TOOLS_SCHEMA, "ignore");
if (ignored != null && ignored.contains(ID)) {
ignoredColors.add(element.getAttribute("name"));
}
}
context.report(ISSUE, context.getLocation(element), "custom colors should refer to predefined colors");
}
@Override
public void visitAttribute(XmlContext context, Attr attribute) {
String content = attribute.getTextContent();
if (content.startsWith("@color/")) {
if (!predefinedColors.contains(content.substring(7))) {
// the color is a custom color
colorUsages.add(Pair.of(attribute, context.getLocation(attribute)));
}
} else if (content.startsWith("#")) {
String ignored = attribute.getOwnerElement().getAttributeNS(TOOLS_SCHEMA, "ignore");
if (ignored == null || !ignored.contains(ID)) {
errrosInAFile++;
}
context.report(ISSUE, context.getLocation(attribute), "Do not use hardcoded colors");
}
}
@Override
public void afterCheckFile(Context context) {
if (errorsInAFile == 0) {
if (context.file.getPath().contains("/res/color")) {
allowedColors.add(context.file.getName().split("\\.")[0]);
}
}
}
@Override
public void afterCheckProject(Context context) {
// filter and report
for (Pair<Attr, Location> usage: colorUsages) {
String content = usage.getFirst().getTextContent();
if (content.startsWith("@color/")) {
if (!allowedColors.contains(content.substring(7))) {
context.report(ISSUE, usage.getSecond(), "custom color does not refer to predefined colors");
}
} else if (content.startsWith("#")) {
context.report(ISSUE, usage.getSecond(), "Do not use hardcoded colors");
}
}
// clean-up
allowedColors.clear();
colorUsages.clear();
predefinedColors.clear();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment