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     * @noinspection WeakerAccess
092     */
093    public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) {
094        if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) {
095            this.ignoreStringsRegexp = null;
096        }
097        else {
098            this.ignoreStringsRegexp = ignoreStringsRegexp;
099        }
100    }
101
102    /**
103     * Adds a set of tokens the check is interested in.
104     * @param strRep the string representation of the tokens interested in
105     */
106    public final void setIgnoreOccurrenceContext(String... strRep) {
107        ignoreOccurrenceContext.clear();
108        for (final String s : strRep) {
109            final int type = TokenUtils.getTokenId(s);
110            ignoreOccurrenceContext.set(type);
111        }
112    }
113
114    @Override
115    public int[] getDefaultTokens() {
116        return getAcceptableTokens();
117    }
118
119    @Override
120    public int[] getAcceptableTokens() {
121        return new int[] {TokenTypes.STRING_LITERAL};
122    }
123
124    @Override
125    public int[] getRequiredTokens() {
126        return getAcceptableTokens();
127    }
128
129    @Override
130    public void visitToken(DetailAST ast) {
131        if (!isInIgnoreOccurrenceContext(ast)) {
132            final String currentString = ast.getText();
133            if (ignoreStringsRegexp == null || !ignoreStringsRegexp.matcher(currentString).find()) {
134                List<StringInfo> hitList = stringMap.get(currentString);
135                if (hitList == null) {
136                    hitList = new ArrayList<>();
137                    stringMap.put(currentString, hitList);
138                }
139                final int line = ast.getLineNo();
140                final int col = ast.getColumnNo();
141                hitList.add(new StringInfo(line, col));
142            }
143        }
144    }
145
146    /**
147     * Analyses the path from the AST root to a given AST for occurrences
148     * of the token types in {@link #ignoreOccurrenceContext}.
149     *
150     * @param ast the node from where to start searching towards the root node
151     * @return whether the path from the root node to ast contains one of the
152     *     token type in {@link #ignoreOccurrenceContext}.
153     */
154    private boolean isInIgnoreOccurrenceContext(DetailAST ast) {
155        boolean isInIgnoreOccurrenceContext = false;
156        for (DetailAST token = ast;
157             token.getParent() != null;
158             token = token.getParent()) {
159            final int type = token.getType();
160            if (ignoreOccurrenceContext.get(type)) {
161                isInIgnoreOccurrenceContext = true;
162                break;
163            }
164        }
165        return isInIgnoreOccurrenceContext;
166    }
167
168    @Override
169    public void beginTree(DetailAST rootAST) {
170        stringMap.clear();
171    }
172
173    @Override
174    public void finishTree(DetailAST rootAST) {
175        for (Map.Entry<String, List<StringInfo>> stringListEntry : stringMap.entrySet()) {
176            final List<StringInfo> hits = stringListEntry.getValue();
177            if (hits.size() > allowedDuplicates) {
178                final StringInfo firstFinding = hits.get(0);
179                final int line = firstFinding.getLine();
180                final int col = firstFinding.getCol();
181                log(line, col, MSG_KEY, stringListEntry.getKey(), hits.size());
182            }
183        }
184    }
185
186    /**
187     * This class contains information about where a string was found.
188     */
189    private static final class StringInfo {
190        /**
191         * Line of finding.
192         */
193        private final int line;
194        /**
195         * Column of finding.
196         */
197        private final int col;
198
199        /**
200         * Creates information about a string position.
201         * @param line int
202         * @param col int
203         */
204        StringInfo(int line, int col) {
205            this.line = line;
206            this.col = col;
207        }
208
209        /**
210         * The line where a string was found.
211         * @return int Line of the string.
212         */
213        private int getLine() {
214            return line;
215        }
216
217        /**
218         * The column where a string was found.
219         * @return int Column of the string.
220         */
221        private int getCol() {
222            return col;
223        }
224    }
225
226}