1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.TokenUtil;
34  
35  /**
36   * Checks for multiple occurrences of the same string literal within a
37   * single file.
38   *
39   */
40  @FileStatefulCheck
41  public class MultipleStringLiteralsCheck extends AbstractCheck {
42  
43      /**
44       * A key is pointing to the warning message text in "messages.properties"
45       * file.
46       */
47      public static final String MSG_KEY = "multiple.string.literal";
48  
49      /**
50       * The found strings and their tokens.
51       */
52      private final Map<String, List<DetailAST>> stringMap = new HashMap<>();
53  
54      /**
55       * Marks the TokenTypes where duplicate strings should be ignored.
56       */
57      private final BitSet ignoreOccurrenceContext = new BitSet();
58  
59      /**
60       * The allowed number of string duplicates in a file before an error is
61       * generated.
62       */
63      private int allowedDuplicates = 1;
64  
65      /**
66       * Pattern for matching ignored strings.
67       */
68      private Pattern ignoreStringsRegexp;
69  
70      /**
71       * Construct an instance with default values.
72       */
73      public MultipleStringLiteralsCheck() {
74          setIgnoreStringsRegexp(Pattern.compile("^\"\"$"));
75          ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
76      }
77  
78      /**
79       * Sets the maximum allowed duplicates of a string.
80       * @param allowedDuplicates The maximum number of duplicates.
81       */
82      public void setAllowedDuplicates(int allowedDuplicates) {
83          this.allowedDuplicates = allowedDuplicates;
84      }
85  
86      /**
87       * Sets regular expression pattern for ignored strings.
88       * @param ignoreStringsRegexp
89       *        regular expression pattern for ignored strings
90       * @noinspection WeakerAccess
91       */
92      public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) {
93          if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) {
94              this.ignoreStringsRegexp = null;
95          }
96          else {
97              this.ignoreStringsRegexp = ignoreStringsRegexp;
98          }
99      }
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 = TokenUtil.getTokenId(s);
109             ignoreOccurrenceContext.set(type);
110         }
111     }
112 
113     @Override
114     public int[] getDefaultTokens() {
115         return getRequiredTokens();
116     }
117 
118     @Override
119     public int[] getAcceptableTokens() {
120         return getRequiredTokens();
121     }
122 
123     @Override
124     public int[] getRequiredTokens() {
125         return new int[] {TokenTypes.STRING_LITERAL};
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<DetailAST> hitList = stringMap.get(currentString);
134                 if (hitList == null) {
135                     hitList = new ArrayList<>();
136                     stringMap.put(currentString, hitList);
137                 }
138                 hitList.add(ast);
139             }
140         }
141     }
142 
143     /**
144      * Analyses the path from the AST root to a given AST for occurrences
145      * of the token types in {@link #ignoreOccurrenceContext}.
146      *
147      * @param ast the node from where to start searching towards the root node
148      * @return whether the path from the root node to ast contains one of the
149      *     token type in {@link #ignoreOccurrenceContext}.
150      */
151     private boolean isInIgnoreOccurrenceContext(DetailAST ast) {
152         boolean isInIgnoreOccurrenceContext = false;
153         for (DetailAST token = ast;
154              token.getParent() != null;
155              token = token.getParent()) {
156             final int type = token.getType();
157             if (ignoreOccurrenceContext.get(type)) {
158                 isInIgnoreOccurrenceContext = true;
159                 break;
160             }
161         }
162         return isInIgnoreOccurrenceContext;
163     }
164 
165     @Override
166     public void beginTree(DetailAST rootAST) {
167         stringMap.clear();
168     }
169 
170     @Override
171     public void finishTree(DetailAST rootAST) {
172         for (Map.Entry<String, List<DetailAST>> stringListEntry : stringMap.entrySet()) {
173             final List<DetailAST> hits = stringListEntry.getValue();
174             if (hits.size() > allowedDuplicates) {
175                 final DetailAST firstFinding = hits.get(0);
176                 log(firstFinding, MSG_KEY, stringListEntry.getKey(), hits.size());
177             }
178         }
179     }
180 
181 }