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