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