001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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    /**
079     * Get the full text of the file.
080     *
081     * @return an object containing the full text of the file
082     */
083    public FileText getText() {
084        return new FileText(text);
085    }
086
087    /**
088     * Gets the lines in the file.
089     *
090     * @return the lines in the file
091     */
092    public String[] getLines() {
093        return text.toLinesArray();
094    }
095
096    /**
097     * Get the line from text of the file.
098     *
099     * @param index index of the line
100     * @return line from text of the file
101     */
102    public String getLine(int index) {
103        return text.get(index);
104    }
105
106    /**
107     * Gets the name of the file.
108     *
109     * @return the name of the file
110     */
111    public String getFileName() {
112        return fileName;
113    }
114
115    @Override
116    public void reportSingleLineComment(String type, int startLineNo,
117            int startColNo) {
118        reportSingleLineComment(startLineNo, startColNo);
119    }
120
121    /**
122     * Report the location of a single line comment.
123     *
124     * @param startLineNo the starting line number
125     * @param startColNo the starting column number
126     **/
127    public void reportSingleLineComment(int startLineNo, int startColNo) {
128        final String line = line(startLineNo - 1);
129        final String[] txt = {line.substring(startColNo)};
130        final Comment comment = new Comment(txt, startColNo, startLineNo,
131                line.length() - 1);
132        cppComments.put(startLineNo, comment);
133    }
134
135    @Override
136    public void reportBlockComment(String type, int startLineNo,
137            int startColNo, int endLineNo, int endColNo) {
138        reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
139    }
140
141    /**
142     * Report the location of a block comment.
143     *
144     * @param startLineNo the starting line number
145     * @param startColNo the starting column number
146     * @param endLineNo the ending line number
147     * @param endColNo the ending column number
148     **/
149    public void reportBlockComment(int startLineNo, int startColNo,
150            int endLineNo, int endColNo) {
151        final String[] cComment = extractBlockComment(startLineNo, startColNo,
152                endLineNo, endColNo);
153        final Comment comment = new Comment(cComment, startColNo, endLineNo,
154                endColNo);
155
156        // save the comment
157        if (clangComments.containsKey(startLineNo)) {
158            final List<TextBlock> entries = clangComments.get(startLineNo);
159            entries.add(comment);
160        }
161        else {
162            final List<TextBlock> entries = new ArrayList<>();
163            entries.add(comment);
164            clangComments.put(startLineNo, entries);
165        }
166
167        // Remember if possible Javadoc comment
168        final String firstLine = line(startLineNo - 1);
169        if (firstLine.contains("/**") && !firstLine.contains("/**/")) {
170            javadocComments.put(endLineNo - 1, comment);
171        }
172    }
173
174    /**
175     * Returns the specified block comment as a String array.
176     *
177     * @param startLineNo the starting line number
178     * @param startColNo the starting column number
179     * @param endLineNo the ending line number
180     * @param endColNo the ending column number
181     * @return block comment as an array
182     **/
183    private String[] extractBlockComment(int startLineNo, int startColNo,
184            int endLineNo, int endColNo) {
185        final String[] returnValue;
186        if (startLineNo == endLineNo) {
187            returnValue = new String[1];
188            returnValue[0] = line(startLineNo - 1).substring(startColNo,
189                    endColNo + 1);
190        }
191        else {
192            returnValue = new String[endLineNo - startLineNo + 1];
193            returnValue[0] = line(startLineNo - 1).substring(startColNo);
194            for (int i = startLineNo; i < endLineNo; i++) {
195                returnValue[i - startLineNo + 1] = line(i);
196            }
197            returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0,
198                    endColNo + 1);
199        }
200        return returnValue;
201    }
202
203    /**
204     * Get a single line.
205     * For internal use only, as getText().get(lineNo) is just as
206     * suitable for external use and avoids method duplication.
207     *
208     * @param lineNo the number of the line to get
209     * @return the corresponding line, without terminator
210     * @throws IndexOutOfBoundsException if lineNo is invalid
211     */
212    private String line(int lineNo) {
213        return text.get(lineNo);
214    }
215
216    /**
217     * Returns the Javadoc comment before the specified line.
218     * A return value of {@code null} means there is no such comment.
219     *
220     * @param lineNoBefore the line number to check before
221     * @return the Javadoc comment, or {@code null} if none
222     **/
223    public TextBlock getJavadocBefore(int lineNoBefore) {
224        // Lines start at 1 to the callers perspective, so need to take off 2
225        int lineNo = lineNoBefore - 2;
226
227        // skip blank lines
228        while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
229            lineNo--;
230        }
231
232        return javadocComments.get(lineNo);
233    }
234
235    /**
236     * Checks if the specified line is blank.
237     *
238     * @param lineNo the line number to check
239     * @return if the specified line consists only of tabs and spaces.
240     **/
241    public boolean lineIsBlank(int lineNo) {
242        return CommonUtil.isBlank(line(lineNo));
243    }
244
245    /**
246     * Checks if the specified line is a single-line comment without code.
247     *
248     * @param lineNo  the line number to check
249     * @return if the specified line consists of only a single line comment
250     *         without code.
251     **/
252    public boolean lineIsComment(int lineNo) {
253        return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
254    }
255
256    /**
257     * Checks if the specified position intersects with a comment.
258     *
259     * @param startLineNo the starting line number
260     * @param startColNo the starting column number
261     * @param endLineNo the ending line number
262     * @param endColNo the ending column number
263     * @return true if the positions intersects with a comment.
264     **/
265    public boolean hasIntersectionWithComment(int startLineNo,
266            int startColNo, int endLineNo, int endColNo) {
267        return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
268                || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
269                        endColNo);
270    }
271
272    /**
273     * Checks if the specified position intersects with a block comment.
274     *
275     * @param startLineNo the starting line number
276     * @param startColNo the starting column number
277     * @param endLineNo the ending line number
278     * @param endColNo the ending column number
279     * @return true if the positions intersects with a block comment.
280     */
281    private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
282            int endLineNo, int endColNo) {
283        boolean hasIntersection = false;
284        // Check C comments (all comments should be checked)
285        final Collection<List<TextBlock>> values = clangComments.values();
286        for (final List<TextBlock> row : values) {
287            for (final TextBlock comment : row) {
288                if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) {
289                    hasIntersection = true;
290                    break;
291                }
292            }
293            if (hasIntersection) {
294                break;
295            }
296        }
297        return hasIntersection;
298    }
299
300    /**
301     * Checks if the specified position intersects with a single line comment.
302     *
303     * @param startLineNo the starting line number
304     * @param startColNo the starting column number
305     * @param endLineNo the ending line number
306     * @param endColNo the ending column number
307     * @return true if the positions intersects with a single line comment.
308     */
309    private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
310            int endLineNo, int endColNo) {
311        boolean hasIntersection = false;
312        // Check CPP comments (line searching is possible)
313        for (int lineNumber = startLineNo; lineNumber <= endLineNo;
314             lineNumber++) {
315            final TextBlock comment = cppComments.get(lineNumber);
316            if (comment != null && comment.intersects(startLineNo, startColNo,
317                    endLineNo, endColNo)) {
318                hasIntersection = true;
319                break;
320            }
321        }
322        return hasIntersection;
323    }
324
325    /**
326     * Returns a map of all the single line comments. The key is a line number,
327     * the value is the comment {@link TextBlock} at the line.
328     *
329     * @return the Map of comments
330     */
331    public Map<Integer, TextBlock> getSingleLineComments() {
332        return Collections.unmodifiableMap(cppComments);
333    }
334
335    /**
336     * Returns a map of all block comments. The key is the line number, the
337     * value is a {@link List} of block comment {@link TextBlock}s
338     * that start at that line.
339     *
340     * @return the map of comments
341     */
342    public Map<Integer, List<TextBlock>> getBlockComments() {
343        return Collections.unmodifiableMap(clangComments);
344    }
345
346    /**
347     * Checks if the current file is a package-info.java file.
348     *
349     * @return true if the package file.
350     */
351    public boolean inPackageInfo() {
352        return fileName.endsWith("package-info.java");
353    }
354
355}