001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import java.util.ArrayList; 023import java.util.BitSet; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 033 034/** 035 * Checks for multiple occurrences of the same string literal within a 036 * single file. 037 * 038 * @author Daniel Grenner 039 */ 040public class MultipleStringLiteralsCheck extends AbstractCheck { 041 042 /** 043 * A key is pointing to the warning message text in "messages.properties" 044 * file. 045 */ 046 public static final String MSG_KEY = "multiple.string.literal"; 047 048 /** 049 * The found strings and their positions. 050 * {@code <String, ArrayList>}, with the ArrayList containing StringInfo 051 * objects. 052 */ 053 private final Map<String, List<StringInfo>> stringMap = new HashMap<>(); 054 055 /** 056 * Marks the TokenTypes where duplicate strings should be ignored. 057 */ 058 private final BitSet ignoreOccurrenceContext = new BitSet(); 059 060 /** 061 * The allowed number of string duplicates in a file before an error is 062 * generated. 063 */ 064 private int allowedDuplicates = 1; 065 066 /** 067 * Pattern for matching ignored strings. 068 */ 069 private Pattern ignoreStringsRegexp; 070 071 /** 072 * Construct an instance with default values. 073 */ 074 public MultipleStringLiteralsCheck() { 075 setIgnoreStringsRegexp(Pattern.compile("^\"\"$")); 076 ignoreOccurrenceContext.set(TokenTypes.ANNOTATION); 077 } 078 079 /** 080 * Sets the maximum allowed duplicates of a string. 081 * @param allowedDuplicates The maximum number of duplicates. 082 */ 083 public void setAllowedDuplicates(int allowedDuplicates) { 084 this.allowedDuplicates = allowedDuplicates; 085 } 086 087 /** 088 * Sets regular expression pattern for ignored strings. 089 * @param ignoreStringsRegexp 090 * regular expression pattern for ignored strings 091 */ 092 public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) { 093 if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) { 094 this.ignoreStringsRegexp = null; 095 } 096 else { 097 this.ignoreStringsRegexp = ignoreStringsRegexp; 098 } 099 } 100 101 /** 102 * Adds a set of tokens the check is interested in. 103 * @param strRep the string representation of the tokens interested in 104 */ 105 public final void setIgnoreOccurrenceContext(String... strRep) { 106 ignoreOccurrenceContext.clear(); 107 for (final String s : strRep) { 108 final int type = TokenUtils.getTokenId(s); 109 ignoreOccurrenceContext.set(type); 110 } 111 } 112 113 @Override 114 public int[] getDefaultTokens() { 115 return getAcceptableTokens(); 116 } 117 118 @Override 119 public int[] getAcceptableTokens() { 120 return new int[] {TokenTypes.STRING_LITERAL}; 121 } 122 123 @Override 124 public int[] getRequiredTokens() { 125 return getAcceptableTokens(); 126 } 127 128 @Override 129 public void visitToken(DetailAST ast) { 130 if (!isInIgnoreOccurrenceContext(ast)) { 131 final String currentString = ast.getText(); 132 if (ignoreStringsRegexp == null || !ignoreStringsRegexp.matcher(currentString).find()) { 133 List<StringInfo> hitList = stringMap.get(currentString); 134 if (hitList == null) { 135 hitList = new ArrayList<>(); 136 stringMap.put(currentString, hitList); 137 } 138 final int line = ast.getLineNo(); 139 final int col = ast.getColumnNo(); 140 hitList.add(new StringInfo(line, col)); 141 } 142 } 143 } 144 145 /** 146 * Analyses the path from the AST root to a given AST for occurrences 147 * of the token types in {@link #ignoreOccurrenceContext}. 148 * 149 * @param ast the node from where to start searching towards the root node 150 * @return whether the path from the root node to ast contains one of the 151 * token type in {@link #ignoreOccurrenceContext}. 152 */ 153 private boolean isInIgnoreOccurrenceContext(DetailAST ast) { 154 for (DetailAST token = ast; 155 token.getParent() != null; 156 token = token.getParent()) { 157 final int type = token.getType(); 158 if (ignoreOccurrenceContext.get(type)) { 159 return true; 160 } 161 } 162 return false; 163 } 164 165 @Override 166 public void beginTree(DetailAST rootAST) { 167 super.beginTree(rootAST); 168 stringMap.clear(); 169 } 170 171 @Override 172 public void finishTree(DetailAST rootAST) { 173 for (Map.Entry<String, List<StringInfo>> stringListEntry : stringMap.entrySet()) { 174 final List<StringInfo> hits = stringListEntry.getValue(); 175 if (hits.size() > allowedDuplicates) { 176 final StringInfo firstFinding = hits.get(0); 177 final int line = firstFinding.getLine(); 178 final int col = firstFinding.getCol(); 179 log(line, col, MSG_KEY, stringListEntry.getKey(), hits.size()); 180 } 181 } 182 } 183 184 /** 185 * This class contains information about where a string was found. 186 */ 187 private static final class StringInfo { 188 /** 189 * Line of finding. 190 */ 191 private final int line; 192 /** 193 * Column of finding. 194 */ 195 private final int col; 196 197 /** 198 * Creates information about a string position. 199 * @param line int 200 * @param col int 201 */ 202 StringInfo(int line, int col) { 203 this.line = line; 204 this.col = col; 205 } 206 207 /** 208 * The line where a string was found. 209 * @return int Line of the string. 210 */ 211 private int getLine() { 212 return line; 213 } 214 215 /** 216 * The column where a string was found. 217 * @return int Column of the string. 218 */ 219 private int getCol() { 220 return col; 221 } 222 } 223 224}