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.FileStatefulCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
034
035/**
036 * Checks for multiple occurrences of the same string literal within a
037 * single file.
038 *
039 * @author Daniel Grenner
040 */
041@FileStatefulCheck
042public class MultipleStringLiteralsCheck extends AbstractCheck {
043
044    /**
045     * A key is pointing to the warning message text in "messages.properties"
046     * file.
047     */
048    public static final String MSG_KEY = "multiple.string.literal";
049
050    /**
051     * The found strings and their positions.
052     * {@code <String, ArrayList>}, with the ArrayList containing StringInfo
053     * objects.
054     */
055    private final Map<String, List<StringInfo>> stringMap = new HashMap<>();
056
057    /**
058     * Marks the TokenTypes where duplicate strings should be ignored.
059     */
060    private final BitSet ignoreOccurrenceContext = new BitSet();
061
062    /**
063     * The allowed number of string duplicates in a file before an error is
064     * generated.
065     */
066    private int allowedDuplicates = 1;
067
068    /**
069     * Pattern for matching ignored strings.
070     */
071    private Pattern ignoreStringsRegexp;
072
073    /**
074     * Construct an instance with default values.
075     */
076    public MultipleStringLiteralsCheck() {
077        setIgnoreStringsRegexp(Pattern.compile("^\"\"$"));
078        ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
079    }
080
081    /**
082     * Sets the maximum allowed duplicates of a string.
083     * @param allowedDuplicates The maximum number of duplicates.
084     */
085    public void setAllowedDuplicates(int allowedDuplicates) {
086        this.allowedDuplicates = allowedDuplicates;
087    }
088
089    /**
090     * Sets regular expression pattern for ignored strings.
091     * @param ignoreStringsRegexp
092     *        regular expression pattern for ignored strings
093     * @noinspection WeakerAccess
094     */
095    public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) {
096        if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) {
097            this.ignoreStringsRegexp = null;
098        }
099        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}