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.api;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.grammar.CommentListener;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
032
033/**
034 * Represents the contents of a file.
035 *
036 */
037public final class FileContents implements CommentListener {
038
039    /**
040     * The pattern to match a single line comment containing only the comment
041     * itself -- no code.
042     */
043    private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
044    /** Compiled regexp to match a single-line comment line. */
045    private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
046            .compile(MATCH_SINGLELINE_COMMENT_PAT);
047
048    /** The file name. */
049    private final String fileName;
050
051    /** The text. */
052    private final FileText text;
053
054    /**
055     * Map of the Javadoc comments indexed on the last line of the comment.
056     * The hack is it assumes that there is only one Javadoc comment per line.
057     */
058    private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
059    /** Map of the C++ comments indexed on the first line of the comment. */
060    private final Map<Integer, TextBlock> cppComments = new HashMap<>();
061
062    /**
063     * Map of the C comments indexed on the first line of the comment to a list
064     * of comments on that line.
065     */
066    private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
067
068    /**
069     * Creates a new {@code FileContents} instance.
070     *
071     * @param text the contents of the file
072     */
073    public FileContents(FileText text) {
074        fileName = text.getFile().toString();
075        this.text = new FileText(text);
076    }
077
078    @Override
079    public void reportSingleLineComment(String type, int startLineNo,
080            int startColNo) {
081        reportSingleLineComment(startLineNo, startColNo);
082    }
083
084    /**
085     * Report the location of a single line comment.
086     * @param startLineNo the starting line number
087     * @param startColNo the starting column number
088     **/
089    public void reportSingleLineComment(int startLineNo, int startColNo) {
090        final String line = line(startLineNo - 1);
091        final String[] txt = {line.substring(startColNo)};
092        final Comment comment = new Comment(txt, startColNo, startLineNo,
093                line.length() - 1);
094        cppComments.put(startLineNo, comment);
095    }
096
097    @Override
098    public void reportBlockComment(String type, int startLineNo,
099            int startColNo, int endLineNo, int endColNo) {
100        reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
101    }
102
103    /**
104     * Report the location of a block comment.
105     * @param startLineNo the starting line number
106     * @param startColNo the starting column number
107     * @param endLineNo the ending line number
108     * @param endColNo the ending column number
109     **/
110    public void reportBlockComment(int startLineNo, int startColNo,
111            int endLineNo, int endColNo) {
112        final String[] cComment = extractBlockComment(startLineNo, startColNo,
113                endLineNo, endColNo);
114        final Comment comment = new Comment(cComment, startColNo, endLineNo,
115                endColNo);
116
117        // save the comment
118        if (clangComments.containsKey(startLineNo)) {
119            final List<TextBlock> entries = clangComments.get(startLineNo);
120            entries.add(comment);
121        }
122        else {
123            final List<TextBlock> entries = new ArrayList<>();
124            entries.add(comment);
125            clangComments.put(startLineNo, entries);
126        }
127
128        // Remember if possible Javadoc comment
129        final String firstLine = line(startLineNo - 1);
130        if (firstLine.contains("/**") && !firstLine.contains("/**/")) {
131            javadocComments.put(endLineNo - 1, comment);
132        }
133    }
134
135    /**
136     * Returns a map of all the single line comments. The key is a line number,
137     * the value is the comment {@link TextBlock} at the line.
138     * @return the Map of comments
139     */
140    public Map<Integer, TextBlock> getSingleLineComments() {
141        return Collections.unmodifiableMap(cppComments);
142    }
143
144    /**
145     * Returns a map of all block comments. The key is the line number, the
146     * value is a {@link List} of block comment {@link TextBlock}s
147     * that start at that line.
148     * @return the map of comments
149     */
150    public Map<Integer, List<TextBlock>> getBlockComments() {
151        return Collections.unmodifiableMap(clangComments);
152    }
153
154    /**
155     * Returns the specified block comment as a String array.
156     * @param startLineNo the starting line number
157     * @param startColNo the starting column number
158     * @param endLineNo the ending line number
159     * @param endColNo the ending column number
160     * @return block comment as an array
161     **/
162    private String[] extractBlockComment(int startLineNo, int startColNo,
163            int endLineNo, int endColNo) {
164        final String[] returnValue;
165        if (startLineNo == endLineNo) {
166            returnValue = new String[1];
167            returnValue[0] = line(startLineNo - 1).substring(startColNo,
168                    endColNo + 1);
169        }
170        else {
171            returnValue = new String[endLineNo - startLineNo + 1];
172            returnValue[0] = line(startLineNo - 1).substring(startColNo);
173            for (int i = startLineNo; i < endLineNo; i++) {
174                returnValue[i - startLineNo + 1] = line(i);
175            }
176            returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0,
177                    endColNo + 1);
178        }
179        return returnValue;
180    }
181
182    /**
183     * Returns the Javadoc comment before the specified line.
184     * A return value of {@code null} means there is no such comment.
185     * @param lineNoBefore the line number to check before
186     * @return the Javadoc comment, or {@code null} if none
187     **/
188    public TextBlock getJavadocBefore(int lineNoBefore) {
189        // Lines start at 1 to the callers perspective, so need to take off 2
190        int lineNo = lineNoBefore - 2;
191
192        // skip blank lines
193        while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
194            lineNo--;
195        }
196
197        return javadocComments.get(lineNo);
198    }
199
200    /**
201     * Get a single line.
202     * For internal use only, as getText().get(lineNo) is just as
203     * suitable for external use and avoids method duplication.
204     * @param lineNo the number of the line to get
205     * @return the corresponding line, without terminator
206     * @throws IndexOutOfBoundsException if lineNo is invalid
207     */
208    private String line(int lineNo) {
209        return text.get(lineNo);
210    }
211
212    /**
213     * Get the full text of the file.
214     * @return an object containing the full text of the file
215     */
216    public FileText getText() {
217        return new FileText(text);
218    }
219
220    /**
221     * Gets the lines in the file.
222     * @return the lines in the file
223     */
224    public String[] getLines() {
225        return text.toLinesArray();
226    }
227
228    /**
229     * Get the line from text of the file.
230     * @param index index of the line
231     * @return line from text of the file
232     */
233    public String getLine(int index) {
234        return text.get(index);
235    }
236
237    /**
238     * Gets the name of the file.
239     * @return the name of the file
240     */
241    public String getFileName() {
242        return fileName;
243    }
244
245    /**
246     * Checks if the specified line is blank.
247     * @param lineNo the line number to check
248     * @return if the specified line consists only of tabs and spaces.
249     **/
250    public boolean lineIsBlank(int lineNo) {
251        return CommonUtil.isBlank(line(lineNo));
252    }
253
254    /**
255     * Checks if the specified line is a single-line comment without code.
256     * @param lineNo  the line number to check
257     * @return if the specified line consists of only a single line comment
258     *         without code.
259     **/
260    public boolean lineIsComment(int lineNo) {
261        return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
262    }
263
264    /**
265     * Checks if the specified position intersects with a comment.
266     * @param startLineNo the starting line number
267     * @param startColNo the starting column number
268     * @param endLineNo the ending line number
269     * @param endColNo the ending column number
270     * @return true if the positions intersects with a comment.
271     **/
272    public boolean hasIntersectionWithComment(int startLineNo,
273            int startColNo, int endLineNo, int endColNo) {
274        return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
275                || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
276                        endColNo);
277    }
278
279    /**
280     * Checks if the current file is a package-info.java file.
281     * @return true if the package file.
282     */
283    public boolean inPackageInfo() {
284        return fileName.endsWith("package-info.java");
285    }
286
287    /**
288     * Checks if the specified position intersects with a block comment.
289     * @param startLineNo the starting line number
290     * @param startColNo the starting column number
291     * @param endLineNo the ending line number
292     * @param endColNo the ending column number
293     * @return true if the positions intersects with a block comment.
294     */
295    private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
296            int endLineNo, int endColNo) {
297        boolean hasIntersection = false;
298        // Check C comments (all comments should be checked)
299        final Collection<List<TextBlock>> values = clangComments.values();
300        for (final List<TextBlock> row : values) {
301            for (final TextBlock comment : row) {
302                if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) {
303                    hasIntersection = true;
304                    break;
305                }
306            }
307            if (hasIntersection) {
308                break;
309            }
310        }
311        return hasIntersection;
312    }
313
314    /**
315     * Checks if the specified position intersects with a single line comment.
316     * @param startLineNo the starting line number
317     * @param startColNo the starting column number
318     * @param endLineNo the ending line number
319     * @param endColNo the ending column number
320     * @return true if the positions intersects with a single line comment.
321     */
322    private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
323            int endLineNo, int endColNo) {
324        boolean hasIntersection = false;
325        // Check CPP comments (line searching is possible)
326        for (int lineNumber = startLineNo; lineNumber <= endLineNo;
327             lineNumber++) {
328            final TextBlock comment = cppComments.get(lineNumber);
329            if (comment != null && comment.intersects(startLineNo, startColNo,
330                    endLineNo, endColNo)) {
331                hasIntersection = true;
332                break;
333            }
334        }
335        return hasIntersection;
336    }
337
338}