Last active
June 22, 2021 19:00
-
-
Save jayrambhia/8d2416f7ec197b418ae3d762d67a7e80 to your computer and use it in GitHub Desktop.
ColorDetector - Lint detector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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