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}