View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.ArrayList;
23  import java.util.BitSet;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.regex.Pattern;
28  
29  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
34  
35  /**
36   * Checks for multiple occurrences of the same string literal within a
37   * single file.
38   *
39   * @author Daniel Grenner
40   */
41  @FileStatefulCheck
42  public class MultipleStringLiteralsCheck extends AbstractCheck {
43  
44      /**
45       * A key is pointing to the warning message text in "messages.properties"
46       * file.
47       */
48      public static final String MSG_KEY = "multiple.string.literal";
49  
50      /**
51       * The found strings and their positions.
52       * {@code <String, ArrayList>}, with the ArrayList containing StringInfo
53       * objects.
54       */
55      private final Map<String, List<StringInfo>> stringMap = new HashMap<>();
56  
57      /**
58       * Marks the TokenTypes where duplicate strings should be ignored.
59       */
60      private final BitSet ignoreOccurrenceContext = new BitSet();
61  
62      /**
63       * The allowed number of string duplicates in a file before an error is
64       * generated.
65       */
66      private int allowedDuplicates = 1;
67  
68      /**
69       * Pattern for matching ignored strings.
70       */
71      private Pattern ignoreStringsRegexp;
72  
73      /**
74       * Construct an instance with default values.
75       */
76      public MultipleStringLiteralsCheck() {
77          setIgnoreStringsRegexp(Pattern.compile("^\"\"$"));
78          ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
79      }
80  
81      /**
82       * Sets the maximum allowed duplicates of a string.
83       * @param allowedDuplicates The maximum number of duplicates.
84       */
85      public void setAllowedDuplicates(int allowedDuplicates) {
86          this.allowedDuplicates = allowedDuplicates;
87      }
88  
89      /**
90       * Sets regular expression pattern for ignored strings.
91       * @param ignoreStringsRegexp
92       *        regular expression pattern for ignored strings
93       * @noinspection WeakerAccess
94       */
95      public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) {
96          if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) {
97              this.ignoreStringsRegexp = null;
98          }
99          else {
100             this.ignoreStringsRegexp = ignoreStringsRegexp;
101         }
102     }
103 
104     /**
105      * Adds a set of tokens the check is interested in.
106      * @param strRep the string representation of the tokens interested in
107      */
108     public final void setIgnoreOccurrenceContext(String... strRep) {
109         ignoreOccurrenceContext.clear();
110         for (final String s : strRep) {
111             final int type = TokenUtils.getTokenId(s);
112             ignoreOccurrenceContext.set(type);
113         }
114     }
115 
116     @Override
117     public int[] getDefaultTokens() {
118         return getRequiredTokens();
119     }
120 
121     @Override
122     public int[] getAcceptableTokens() {
123         return getRequiredTokens();
124     }
125 
126     @Override
127     public int[] getRequiredTokens() {
128         return new int[] {TokenTypes.STRING_LITERAL};
129     }
130 
131     @Override
132     public void visitToken(DetailAST ast) {
133         if (!isInIgnoreOccurrenceContext(ast)) {
134             final String currentString = ast.getText();
135             if (ignoreStringsRegexp == null || !ignoreStringsRegexp.matcher(currentString).find()) {
136                 List<StringInfo> hitList = stringMap.get(currentString);
137                 if (hitList == null) {
138                     hitList = new ArrayList<>();
139                     stringMap.put(currentString, hitList);
140                 }
141                 final int line = ast.getLineNo();
142                 final int col = ast.getColumnNo();
143                 hitList.add(new StringInfo(line, col));
144             }
145         }
146     }
147 
148     /**
149      * Analyses the path from the AST root to a given AST for occurrences
150      * of the token types in {@link #ignoreOccurrenceContext}.
151      *
152      * @param ast the node from where to start searching towards the root node
153      * @return whether the path from the root node to ast contains one of the
154      *     token type in {@link #ignoreOccurrenceContext}.
155      */
156     private boolean isInIgnoreOccurrenceContext(DetailAST ast) {
157         boolean isInIgnoreOccurrenceContext = false;
158         for (DetailAST token = ast;
159              token.getParent() != null;
160              token = token.getParent()) {
161             final int type = token.getType();
162             if (ignoreOccurrenceContext.get(type)) {
163                 isInIgnoreOccurrenceContext = true;
164                 break;
165             }
166         }
167         return isInIgnoreOccurrenceContext;
168     }
169 
170     @Override
171     public void beginTree(DetailAST rootAST) {
172         stringMap.clear();
173     }
174 
175     @Override
176     public void finishTree(DetailAST rootAST) {
177         for (Map.Entry<String, List<StringInfo>> stringListEntry : stringMap.entrySet()) {
178             final List<StringInfo> hits = stringListEntry.getValue();
179             if (hits.size() > allowedDuplicates) {
180                 final StringInfo firstFinding = hits.get(0);
181                 final int line = firstFinding.getLine();
182                 final int col = firstFinding.getCol();
183                 log(line, col, MSG_KEY, stringListEntry.getKey(), hits.size());
184             }
185         }
186     }
187 
188     /**
189      * This class contains information about where a string was found.
190      */
191     private static final class StringInfo {
192         /**
193          * Line of finding.
194          */
195         private final int line;
196         /**
197          * Column of finding.
198          */
199         private final int col;
200 
201         /**
202          * Creates information about a string position.
203          * @param line int
204          * @param col int
205          */
206         StringInfo(int line, int col) {
207             this.line = line;
208             this.col = col;
209         }
210 
211         /**
212          * The line where a string was found.
213          * @return int Line of the string.
214          */
215         private int getLine() {
216             return line;
217         }
218 
219         /**
220          * The column where a string was found.
221          * @return int Column of the string.
222          */
223         private int getCol() {
224             return col;
225         }
226     }
227 
228 }