1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.checkcolor.lint; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.NonNull; 21 import com.android.annotations.Nullable; 22 import com.android.ide.common.resources.ResourceUrl; 23 import com.android.resources.ResourceFolderType; 24 import com.android.resources.ResourceType; 25 import com.android.tools.lint.detector.api.Category; 26 import com.android.tools.lint.detector.api.Context; 27 import com.android.tools.lint.detector.api.Implementation; 28 import com.android.tools.lint.detector.api.Issue; 29 import com.android.tools.lint.detector.api.LintUtils; 30 import com.android.tools.lint.detector.api.Location; 31 import com.android.tools.lint.detector.api.ResourceXmlDetector; 32 import com.android.tools.lint.detector.api.Scope; 33 import com.android.tools.lint.detector.api.Severity; 34 import com.android.tools.lint.detector.api.XmlContext; 35 import com.google.common.collect.ArrayListMultimap; 36 import com.google.common.collect.Multimap; 37 38 import org.w3c.dom.Attr; 39 import org.w3c.dom.Element; 40 import org.w3c.dom.Node; 41 import org.w3c.dom.NodeList; 42 43 import java.util.Arrays; 44 import java.util.Collection; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Set; 48 49 import static com.android.SdkConstants.TAG_COLOR; 50 import static com.android.SdkConstants.TAG_ITEM; 51 import static com.android.SdkConstants.TAG_STYLE; 52 53 /** 54 * It contains two phases to detect the hardcode colors 55 * 56 * Phase 1: 57 * 1. Check all the direct hardcode color(#ffffff) 58 * 2. Store all the potential indirect hardcode color(Hopefully none) 59 * 60 * Phase 2: 61 * 1. Go through colors.xml, recheck all the indirect hardcoded color 62 */ 63 public class HardcodedColorDetector extends ResourceXmlDetector { 64 private static final Implementation IMPLEMENTATION = new Implementation( 65 HardcodedColorDetector.class, 66 Scope.RESOURCE_FILE_SCOPE); 67 68 public static final Issue ISSUE = Issue.create( 69 "HardcodedColor", 70 "Using hardcoded color", 71 "Hardcoded color values are bad because theme changes cannot be uniformly applied." + 72 "Instead use the theme specific colors such as `?android:attr/textColorPrimary` in " + 73 "attributes.\n" + 74 "This ensures that a theme change from a light to a dark theme can be uniformly" + 75 "applied across the app.", 76 Category.CORRECTNESS, 77 4, 78 Severity.ERROR, 79 IMPLEMENTATION); 80 81 private static final String ERROR_MESSAGE = "Using hardcoded colors is not allowed"; 82 83 private Multimap<String, Location.Handle> indirectColorMultiMap; 84 private Set<String> hardcodedColorSet; 85 private Set<String> skipAttributes; 86 HardcodedColorDetector()87 public HardcodedColorDetector() { 88 indirectColorMultiMap = ArrayListMultimap.create(); 89 skipAttributes = new HashSet<>(); 90 hardcodedColorSet = new HashSet<>(); 91 92 skipAttributes.add("fillColor"); 93 skipAttributes.add("strokeColor"); 94 skipAttributes.add("text"); 95 } 96 97 @Override appliesTo(@onNull ResourceFolderType folderType)98 public boolean appliesTo(@NonNull ResourceFolderType folderType) { 99 return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES 100 || folderType == ResourceFolderType.DRAWABLE; 101 } 102 103 @Override getApplicableAttributes()104 public Collection<String> getApplicableAttributes() { 105 return ALL; 106 } 107 108 @Override 109 @Nullable getApplicableElements()110 public Collection<String> getApplicableElements() { 111 return Arrays.asList(TAG_STYLE, TAG_COLOR); 112 } 113 114 @Override visitAttribute(@onNull XmlContext context, @NonNull Attr attribute)115 public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { 116 if (!LintUtils.isEnglishResource(context, true)) { 117 return; 118 } 119 120 final String value = attribute.getValue(); 121 final ResourceUrl resUrl = ResourceUrl.parse(value); 122 if (!skipAttributes.contains(attribute.getLocalName()) && resUrl == null 123 && isHardcodedColor(value)) { 124 // TODO: check whether the attr is valid to store the color 125 if (context.isEnabled(ISSUE)) { 126 context.report(ISSUE, attribute, context.getLocation(attribute), 127 ERROR_MESSAGE); 128 } 129 } else if (resUrl != null && resUrl.type == ResourceType.COLOR && !resUrl.theme) { 130 addIndirectColor(context, value, attribute); 131 } 132 } 133 134 @Override visitElement(@onNull XmlContext context, @NonNull Element element)135 public void visitElement(@NonNull XmlContext context, @NonNull Element element) { 136 if (context.getResourceFolderType() != ResourceFolderType.VALUES) { 137 return; 138 } 139 140 if (!LintUtils.isEnglishResource(context, true)) { 141 return; 142 } 143 144 final int phase = context.getPhase(); 145 final String tagName = element.getTagName(); 146 if (tagName.equals(TAG_STYLE)) { 147 final List<Element> itemNodes = LintUtils.getChildren(element); 148 for (Element childElement : itemNodes) { 149 if (childElement.getNodeType() == Node.ELEMENT_NODE && 150 TAG_ITEM.equals(childElement.getNodeName())) { 151 final NodeList childNodes = childElement.getChildNodes(); 152 for (int i = 0, n = childNodes.getLength(); i < n; i++) { 153 final Node child = childNodes.item(i); 154 if (child.getNodeType() != Node.TEXT_NODE) { 155 break; 156 } 157 158 final String value = child.getNodeValue(); 159 final ResourceUrl resUrl = ResourceUrl.parse(value); 160 if (resUrl == null && isHardcodedColor(value)) { 161 // TODO: check whether the node is valid to store the color 162 context.report(ISSUE, childElement, context.getLocation(child), 163 ERROR_MESSAGE); 164 } else if (resUrl != null && resUrl.type == ResourceType.COLOR 165 && !resUrl.theme) { 166 addIndirectColor(context, value, child); 167 } 168 } 169 } 170 } 171 } else if (tagName.equals(TAG_COLOR)) { 172 final String name = element.getAttribute(SdkConstants.ATTR_NAME); 173 final String value = element.getFirstChild().getNodeValue(); 174 if (isHardcodedColor(value) && context.isEnabled(ISSUE)) { 175 context.report(ISSUE, element, context.getLocation(element), 176 ERROR_MESSAGE); 177 178 final String fullColorName = "@color/" + name; 179 hardcodedColorSet.add(fullColorName); 180 } 181 } 182 } 183 184 @Override afterCheckProject(@onNull Context context)185 public void afterCheckProject(@NonNull Context context) { 186 super.afterCheckProject(context); 187 188 if (context.isEnabled(ISSUE)) { 189 for (final String fullColorName : hardcodedColorSet) { 190 if (indirectColorMultiMap.containsKey(fullColorName)) { 191 for (Location.Handle handle : indirectColorMultiMap.get(fullColorName)) { 192 context.report(ISSUE, handle.resolve(), ERROR_MESSAGE); 193 } 194 } 195 } 196 } 197 } 198 199 /** 200 * Test whether {@paramref color} is the hardcoded color using the regex match. 201 * The hex hardcoded color has three types. 202 * 1. #RGB e.g #fff 203 * 2. #RRGGBB e.g #ffffff 204 * 3. #AARRGGBB e.g #ffffffff 205 * 206 * @param color name of the color 207 * @return whether it is hardcoded color 208 */ isHardcodedColor(String color)209 private boolean isHardcodedColor(String color) { 210 return color.matches("#[0-9a-fA-F]{3}") 211 || color.matches("#[0-9a-fA-F]{6}") 212 || color.matches("#[0-9a-fA-F]{8}"); 213 } 214 215 /** 216 * Add indirect color for further examination. For example, in layout file we don't know 217 * whether "@color/color_to_examine" is the hardcoded color. So I store the name and location 218 * first 219 * @param context used to create the location handle 220 * @param color name of the indirect color 221 * @param node postion in xml file 222 */ addIndirectColor(XmlContext context, String color, Node node)223 private void addIndirectColor(XmlContext context, String color, Node node) { 224 final Location.Handle handle = context.createLocationHandle(node); 225 handle.setClientData(node); 226 227 indirectColorMultiMap.put(color, handle); 228 } 229 }