001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.TokenUtil;
034
035/**
036 * Checks for multiple occurrences of the same string literal within a
037 * single file.
038 *
039 */
040@FileStatefulCheck
041public class MultipleStringLiteralsCheck extends AbstractCheck {
042
043    /**
044     * A key is pointing to the warning message text in "messages.properties"
045     * file.
046     */
047    public static final String MSG_KEY = "multiple.string.literal";
048
049    /**
050     * The found strings and their tokens.
051     */
052    private final Map<String, List<DetailAST>> stringMap = new HashMap<>();
053
054    /**
055     * Marks the TokenTypes where duplicate strings should be ignored.
056     */
057    private final BitSet ignoreOccurrenceContext = new BitSet();
058
059    /**
060     * The allowed number of string duplicates in a file before an error is
061     * generated.
062     */
063    private int allowedDuplicates = 1;
064
065    /**
066     * Pattern for matching ignored strings.
067     */
068    private Pattern ignoreStringsRegexp;
069
070    /**
071     * Construct an instance with default values.
072     */
073    public MultipleStringLiteralsCheck() {
074        setIgnoreStringsRegexp(Pattern.compile("^\"\"$"));
075        ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
076    }
077
078    /**
079     * Sets the maximum allowed duplicates of a string.
080     * @param allowedDuplicates The maximum number of duplicates.
081     */
082    public void setAllowedDuplicates(int allowedDuplicates) {
083        this.allowedDuplicates = allowedDuplicates;
084    }
085
086    /**
087     * Sets regular expression pattern for ignored strings.
088     * @param ignoreStringsRegexp
089     *        regular expression pattern for ignored strings
090     * @noinspection WeakerAccess
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 = 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}