• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.android.tools.lint.detector.api;
18 
19 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML;
20 import static com.android.tools.lint.detector.api.LintConstants.ID_RESOURCE_PREFIX;
21 import static com.android.tools.lint.detector.api.LintConstants.NEW_ID_RESOURCE_PREFIX;
22 
23 import com.android.annotations.NonNull;
24 import com.android.annotations.Nullable;
25 import com.android.resources.FolderTypeRelationship;
26 import com.android.resources.ResourceFolderType;
27 import com.android.resources.ResourceType;
28 import com.android.util.PositionXmlParser;
29 import com.google.common.annotations.Beta;
30 import com.google.common.io.Files;
31 
32 import org.w3c.dom.Element;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35 
36 import java.io.File;
37 import java.io.IOException;
38 import java.io.UnsupportedEncodingException;
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 
43 /**
44  * Useful utility methods related to lint.
45  * <p>
46  * <b>NOTE: This is not a public or final API; if you rely on this be prepared
47  * to adjust your code for the next tools release.</b>
48  */
49 @Beta
50 public class LintUtils {
51     /**
52      * Format a list of strings, and cut of the list at {@code maxItems} if the
53      * number of items are greater.
54      *
55      * @param strings the list of strings to print out as a comma separated list
56      * @param maxItems the maximum number of items to print
57      * @return a comma separated list
58      */
59     @NonNull
formatList(@onNull List<String> strings, int maxItems)60     public static String formatList(@NonNull List<String> strings, int maxItems) {
61         StringBuilder sb = new StringBuilder(20 * strings.size());
62 
63         for (int i = 0, n = strings.size(); i < n; i++) {
64             if (sb.length() > 0) {
65                 sb.append(", "); //$NON-NLS-1$
66             }
67             sb.append(strings.get(i));
68 
69             if (maxItems > 0 && i == maxItems - 1 && n > maxItems) {
70                 sb.append(String.format("... (%1$d more)", n - i - 1));
71                 break;
72             }
73         }
74 
75         return sb.toString();
76     }
77 
78     /**
79      * Determine if the given type corresponds to a resource that has a unique
80      * file
81      *
82      * @param type the resource type to check
83      * @return true if the given type corresponds to a file-type resource
84      */
isFileBasedResourceType(@onNull ResourceType type)85     public static boolean isFileBasedResourceType(@NonNull ResourceType type) {
86         List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type);
87         for (ResourceFolderType folderType : folderTypes) {
88             if (folderType != ResourceFolderType.VALUES) {
89                 if (type == ResourceType.ID) {
90                     return false;
91                 }
92                 return true;
93             }
94         }
95         return false;
96     }
97 
98     /**
99      * Returns true if the given file represents an XML file
100      *
101      * @param file the file to be checked
102      * @return true if the given file is an xml file
103      */
isXmlFile(@onNull File file)104     public static boolean isXmlFile(@NonNull File file) {
105         String string = file.getName();
106         return string.regionMatches(true, string.length() - DOT_XML.length(),
107                 DOT_XML, 0, DOT_XML.length());
108     }
109 
110     /**
111      * Case insensitive ends with
112      *
113      * @param string the string to be tested whether it ends with the given
114      *            suffix
115      * @param suffix the suffix to check
116      * @return true if {@code string} ends with {@code suffix},
117      *         case-insensitively.
118      */
endsWith(@onNull String string, @NonNull String suffix)119     public static boolean endsWith(@NonNull String string, @NonNull String suffix) {
120         return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(),
121                 suffix, 0, suffix.length());
122     }
123 
124     /**
125      * Case insensitive starts with
126      *
127      * @param string the string to be tested whether it starts with the given prefix
128      * @param prefix the prefix to check
129      * @param offset the offset to start checking with
130      * @return true if {@code string} starts with {@code prefix},
131      *         case-insensitively.
132      */
startsWith(@onNull String string, @NonNull String prefix, int offset)133     public static boolean startsWith(@NonNull String string, @NonNull String prefix, int offset) {
134         return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length());
135     }
136 
137     /**
138      * Returns the basename of the given filename, unless it's a dot-file such as ".svn".
139      *
140      * @param fileName the file name to extract the basename from
141      * @return the basename (the filename without the file extension)
142      */
getBaseName(@onNull String fileName)143     public static String getBaseName(@NonNull String fileName) {
144         int extension = fileName.indexOf('.');
145         if (extension > 0) {
146             return fileName.substring(0, extension);
147         } else {
148             return fileName;
149         }
150     }
151 
152     /**
153      * Returns the children elements of the given node
154      *
155      * @param node the parent node
156      * @return a list of element children, never null
157      */
158     @NonNull
getChildren(@onNull Node node)159     public static List<Element> getChildren(@NonNull Node node) {
160         NodeList childNodes = node.getChildNodes();
161         List<Element> children = new ArrayList<Element>(childNodes.getLength());
162         for (int i = 0, n = childNodes.getLength(); i < n; i++) {
163             Node child = childNodes.item(i);
164             if (child.getNodeType() == Node.ELEMENT_NODE) {
165                 children.add((Element) child);
166             }
167         }
168 
169         return children;
170     }
171 
172     /**
173      * Returns the <b>number</b> of children of the given node
174      *
175      * @param node the parent node
176      * @return the count of element children
177      */
getChildCount(@onNull Node node)178     public static int getChildCount(@NonNull Node node) {
179         NodeList childNodes = node.getChildNodes();
180         int childCount = 0;
181         for (int i = 0, n = childNodes.getLength(); i < n; i++) {
182             Node child = childNodes.item(i);
183             if (child.getNodeType() == Node.ELEMENT_NODE) {
184                 childCount++;
185             }
186         }
187 
188         return childCount;
189     }
190 
191     /**
192      * Returns true if the given element is the root element of its document
193      *
194      * @param element the element to test
195      * @return true if the element is the root element
196      */
isRootElement(Element element)197     public static boolean isRootElement(Element element) {
198         return element == element.getOwnerDocument().getDocumentElement();
199     }
200 
201     /**
202      * Returns the given id without an {@code @id/} or {@code @+id} prefix
203      *
204      * @param id the id to strip
205      * @return the stripped id, never null
206      */
207     @NonNull
stripIdPrefix(@ullable String id)208     public static String stripIdPrefix(@Nullable String id) {
209         if (id == null) {
210             return "";
211         } else if (id.startsWith(NEW_ID_RESOURCE_PREFIX)) {
212             return id.substring(NEW_ID_RESOURCE_PREFIX.length());
213         } else if (id.startsWith(ID_RESOURCE_PREFIX)) {
214             return id.substring(ID_RESOURCE_PREFIX.length());
215         }
216 
217         return id;
218     }
219 
220     /**
221      * Returns true if the given two id references match. This is similar to
222      * String equality, but it also considers "{@code @+id/foo == @id/foo}.
223      *
224      * @param id1 the first id to compare
225      * @param id2 the second id to compare
226      * @return true if the two id references refer to the same id
227      */
idReferencesMatch(String id1, String id2)228     public static boolean idReferencesMatch(String id1, String id2) {
229         if (id1.startsWith(NEW_ID_RESOURCE_PREFIX)) {
230             if (id2.startsWith(NEW_ID_RESOURCE_PREFIX)) {
231                 return id1.equals(id2);
232             } else {
233                 assert id2.startsWith(ID_RESOURCE_PREFIX);
234                 return ((id1.length() - id2.length())
235                             == (NEW_ID_RESOURCE_PREFIX.length() - ID_RESOURCE_PREFIX.length()))
236                         && id1.regionMatches(NEW_ID_RESOURCE_PREFIX.length(), id2,
237                                 ID_RESOURCE_PREFIX.length(),
238                                 id2.length() - ID_RESOURCE_PREFIX.length());
239             }
240         } else {
241             assert id1.startsWith(ID_RESOURCE_PREFIX);
242             if (id2.startsWith(ID_RESOURCE_PREFIX)) {
243                 return id1.equals(id2);
244             } else {
245                 assert id2.startsWith(NEW_ID_RESOURCE_PREFIX);
246                 return (id2.length() - id1.length()
247                             == (NEW_ID_RESOURCE_PREFIX.length() - ID_RESOURCE_PREFIX.length()))
248                         && id2.regionMatches(NEW_ID_RESOURCE_PREFIX.length(), id1,
249                                 ID_RESOURCE_PREFIX.length(),
250                                 id1.length() - ID_RESOURCE_PREFIX.length());
251             }
252         }
253     }
254 
255     /**
256      * Computes the edit distance (number of insertions, deletions or substitutions
257      * to edit one string into the other) between two strings. In particular,
258      * this will compute the Levenshtein distance.
259      * <p>
260      * See http://en.wikipedia.org/wiki/Levenshtein_distance for details.
261      *
262      * @param s the first string to compare
263      * @param t the second string to compare
264      * @return the edit distance between the two strings
265      */
editDistance(@onNull String s, @NonNull String t)266     public static int editDistance(@NonNull String s, @NonNull String t) {
267         int m = s.length();
268         int n = t.length();
269         int[][] d = new int[m + 1][n + 1];
270         for (int i = 0; i <= m; i++) {
271             d[i][0] = i;
272         }
273         for (int j = 0; j <= n; j++) {
274             d[0][j] = j;
275         }
276         for (int j = 1; j <= n; j++) {
277             for (int i = 1; i <= m; i++) {
278                 if (s.charAt(i - 1) == t.charAt(j - 1)) {
279                     d[i][j] = d[i - 1][j - 1];
280                 } else {
281                     int deletion = d[i - 1][j] + 1;
282                     int insertion = d[i][j - 1] + 1;
283                     int substitution = d[i - 1][j - 1] + 1;
284                     d[i][j] = Math.min(deletion, Math.min(insertion, substitution));
285                 }
286             }
287         }
288 
289         return d[m][n];
290     }
291 
292     /**
293      * Returns true if assertions are enabled
294      *
295      * @return true if assertions are enabled
296      */
297     @SuppressWarnings("all")
assertionsEnabled()298     public static boolean assertionsEnabled() {
299         boolean assertionsEnabled = false;
300         assert assertionsEnabled = true; // Intentional side-effect
301         return assertionsEnabled;
302     }
303 
304     /**
305      * Returns the layout resource name for the given layout file
306      *
307      * @param layoutFile the file pointing to the layout
308      * @return the layout resource name, not including the {@code @layout}
309      *         prefix
310      */
getLayoutName(File layoutFile)311     public static String getLayoutName(File layoutFile) {
312         String name = layoutFile.getName();
313         int dotIndex = name.indexOf('.');
314         if (dotIndex != -1) {
315             name = name.substring(0, dotIndex);
316         }
317         return name;
318     }
319 
320     /**
321      * Computes the shared parent among a set of files (which may be null).
322      *
323      * @param files the set of files to be checked
324      * @return the closest common ancestor file, or null if none was found
325      */
326     @Nullable
getCommonParent(@onNull List<File> files)327     public static File getCommonParent(@NonNull List<File> files) {
328         int fileCount = files.size();
329         if (fileCount == 0) {
330             return null;
331         } else if (fileCount == 1) {
332             return files.get(0);
333         } else if (fileCount == 2) {
334             return getCommonParent(files.get(0), files.get(1));
335         } else {
336             File common = files.get(0);
337             for (int i = 1; i < fileCount; i++) {
338                 common = getCommonParent(common, files.get(i));
339                 if (common == null) {
340                     return null;
341                 }
342             }
343 
344             return common;
345         }
346     }
347 
348     /**
349      * Computes the closest common parent path between two files.
350      *
351      * @param file1 the first file to be compared
352      * @param file2 the second file to be compared
353      * @return the closest common ancestor file, or null if the two files have
354      *         no common parent
355      */
356     @Nullable
getCommonParent(@onNull File file1, @NonNull File file2)357     public static File getCommonParent(@NonNull File file1, @NonNull File file2) {
358         if (file1.equals(file2)) {
359             return file1;
360         } else if (file1.getPath().startsWith(file2.getPath())) {
361             return file2;
362         } else if (file2.getPath().startsWith(file1.getPath())) {
363             return file1;
364         } else {
365             // Dumb and simple implementation
366             File first = file1.getParentFile();
367             while (first != null) {
368                 File second = file2.getParentFile();
369                 while (second != null) {
370                     if (first.equals(second)) {
371                         return first;
372                     }
373                     second = second.getParentFile();
374                 }
375 
376                 first = first.getParentFile();
377             }
378         }
379         return null;
380     }
381 
382     private static final String UTF_8 = "UTF-8";                 //$NON-NLS-1$
383     private static final String UTF_16 = "UTF_16";               //$NON-NLS-1$
384     private static final String UTF_16LE = "UTF_16LE";           //$NON-NLS-1$
385 
386     /**
387      * Returns the encoded String for the given file. This is usually the
388      * same as {@code Files.toString(file, Charsets.UTF8}, but if there's a UTF byte order mark
389      * (for UTF8, UTF_16 or UTF_16LE), use that instead.
390      *
391      * @param file the file to read from
392      * @return the string
393      * @throws IOException if the file cannot be read properly
394      */
getEncodedString(File file)395     public static String getEncodedString(File file) throws IOException {
396         byte[] bytes = Files.toByteArray(file);
397         if (endsWith(file.getName(), DOT_XML)) {
398             return PositionXmlParser.getXmlString(bytes);
399         }
400 
401         return LintUtils.getEncodedString(bytes);
402     }
403 
404     /**
405      * Returns the String corresponding to the given data. This is usually the
406      * same as {@code new String(data)}, but if there's a UTF byte order mark
407      * (for UTF8, UTF_16 or UTF_16LE), use that instead.
408      * <p>
409      * NOTE: For XML files, there is the additional complication that there
410      * could be a {@code encoding=} attribute in the prologue. For those files,
411      * use {@link PositionXmlParser#getXmlString(byte[])} instead.
412      *
413      * @param data the byte array to construct the string from
414      * @return the string
415      */
getEncodedString(byte[] data)416     public static String getEncodedString(byte[] data) {
417         if (data == null) {
418             return "";
419         }
420 
421         int offset = 0;
422         String defaultCharset = UTF_8;
423         String charset = null;
424         // Look for the byte order mark, to see if we need to remove bytes from
425         // the input stream (and to determine whether files are big endian or little endian) etc
426         // for files which do not specify the encoding.
427         // See http://unicode.org/faq/utf_bom.html#BOM for more.
428         if (data.length > 4) {
429             if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) {
430                 // UTF-8
431                 defaultCharset = charset = UTF_8;
432                 offset += 3;
433             } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) {
434                 //  UTF-16, big-endian
435                 defaultCharset = charset = UTF_16;
436                 offset += 2;
437             } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0
438                     && data[2] == (byte)0xfe && data[3] == (byte)0xff) {
439                 // UTF-32, big-endian
440                 defaultCharset = charset = "UTF_32";    //$NON-NLS-1$
441                 offset += 4;
442             } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe
443                     && data[2] == (byte)0x0 && data[3] == (byte)0x0) {
444                 // UTF-32, little-endian. We must check for this *before* looking for
445                 // UTF_16LE since UTF_32LE has the same prefix!
446                 defaultCharset = charset = "UTF_32LE";  //$NON-NLS-1$
447                 offset += 4;
448             } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) {
449                 //  UTF-16, little-endian
450                 defaultCharset = charset = UTF_16LE;
451                 offset += 2;
452             }
453         }
454         int length = data.length - offset;
455 
456         // Guess encoding by searching for an encoding= entry in the first line.
457         boolean seenOddZero = false;
458         boolean seenEvenZero = false;
459         for (int lineEnd = offset; lineEnd < data.length; lineEnd++) {
460             if (data[lineEnd] == 0) {
461                 if ((lineEnd - offset) % 1 == 0) {
462                     seenEvenZero = true;
463                 } else {
464                     seenOddZero = true;
465                 }
466             } else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') {
467                 break;
468             }
469         }
470 
471         if (charset == null) {
472             charset = seenOddZero ? UTF_16 : seenEvenZero ? UTF_16LE : UTF_8;
473         }
474 
475         String text = null;
476         try {
477             text = new String(data, offset, length, charset);
478         } catch (UnsupportedEncodingException e) {
479             try {
480                 if (charset != defaultCharset) {
481                     text = new String(data, offset, length, defaultCharset);
482                 }
483             } catch (UnsupportedEncodingException u) {
484                 // Just use the default encoding below
485             }
486         }
487         if (text == null) {
488             text = new String(data, offset, length);
489         }
490         return text;
491     }
492 }
493