1 package org.codehaus.mojo.taglist;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.LineNumberReader;
27 import java.io.Reader;
28 import java.nio.file.Files;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.List;
32 import java.util.Locale;
33
34 import org.apache.commons.lang3.StringUtils;
35 import org.apache.maven.plugin.logging.Log;
36 import org.codehaus.mojo.taglist.beans.FileReport;
37 import org.codehaus.mojo.taglist.beans.TagReport;
38 import org.codehaus.mojo.taglist.tags.TagClass;
39 import org.codehaus.plexus.util.FileUtils;
40
41 /**
42 * Class that analyzes a file with a special comment tag. For instance:
43 *
44 * <pre>
45 * // TODO: Example of an Eclipse/IntelliJ-like "todo" tag
46 * </pre>
47 *
48 * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard </a>
49 * @todo : This is another example of "todo" tag
50 */
51 public class FileAnalyser {
52 /**
53 * String that is used for beginning a comment line.
54 */
55 private static final String STAR_COMMENT = "*";
56
57 /**
58 * String that is used for beginning a comment line.
59 */
60 private static final String SLASH_COMMENT = "//";
61
62 /**
63 * Maximum length of a comment.
64 */
65 private static final int MAX_COMMENT_CHARACTERS = 99999;
66
67 /**
68 * The character encoding of the files to analyze.
69 */
70 private final String encoding;
71
72 /**
73 * The Locale of the files to analyze.
74 */
75 private final Locale sourceFileLocale;
76
77 /**
78 * The directories to analyze.
79 */
80 private final Collection<String> sourceDirs;
81
82 /**
83 * The files to include, as a comma separated list of patterns.
84 */
85 private final String includes;
86
87 /**
88 * The files top exclude, as a comma separated list of patterns.
89 */
90 private final String excludes;
91
92 /**
93 * Log for debug output.
94 */
95 private final Log log;
96
97 /**
98 * Set to true if the analyzer should look for multiple line comments.
99 */
100 private final boolean multipleLineCommentsOn;
101
102 /**
103 * Set to true if the analyzer should look for tags without comments.
104 */
105 private final boolean emptyCommentsOn;
106
107 /**
108 * ArrayList of tag classes.
109 */
110 private final List<TagClass> tagClasses;
111
112 /**
113 * Constructor.
114 *
115 * @param report the MOJO that is using this analyzer.
116 * @param tagClasses the array of tag classes to use for searching
117 */
118 public FileAnalyser(TagListReport report, List<TagClass> tagClasses) {
119 // TODO - direct class TagListReport should not be used,
120 // we can add a separate args or new class/interface for group of args
121 multipleLineCommentsOn = report.isMultipleLineComments();
122 emptyCommentsOn = report.isEmptyComments();
123 log = report.getLog();
124 sourceDirs = report.getSourceDirs();
125 encoding = report.getInputEncoding();
126 sourceFileLocale = report.getSourceFileLocale();
127 this.tagClasses = tagClasses;
128 this.includes = report.getIncludesCommaSeparated();
129 this.excludes = report.getExcludesCommaSeparated();
130 }
131
132 /**
133 * Execute the analysis for the configuration given by the TagListReport.
134 *
135 * @return a collection of TagReport objects.
136 */
137 public Collection<TagReport> execute() throws IOException {
138 List<File> fileList = findFilesToScan();
139
140 for (File file : fileList) {
141 if (file.exists()) {
142 scanFile(file);
143 }
144 }
145
146 // Get the tag reports from each of the tag classes.
147 Collection<TagReport> tagReports = new ArrayList<>();
148 for (TagClass tc : tagClasses) {
149 tagReports.add(tc.getTagReport());
150 }
151
152 return tagReports;
153 }
154
155 /**
156 * Gives the list of files to scan.
157 *
158 * @return a List of File objects.
159 */
160 private List<File> findFilesToScan() throws IOException {
161 List<File> filesList = new ArrayList<>();
162 for (String sourceDir : sourceDirs) {
163 filesList.addAll(FileUtils.getFiles(new File(sourceDir), includes, excludes));
164 }
165 return filesList;
166 }
167
168 /**
169 * Access an input reader that uses the current file encoding.
170 *
171 * @param file the file to open in the reader.
172 * @return a reader with the current file encoding.
173 * @throws IOException the IO exception.
174 */
175 private Reader getReader(File file) throws IOException {
176 InputStream in = Files.newInputStream(file.toPath());
177 return (encoding == null) ? new InputStreamReader(in) : new InputStreamReader(in, encoding);
178 }
179
180 /**
181 * Scans a file to look for task tags.
182 *
183 * @param file the file to scan.
184 */
185 public void scanFile(File file) {
186 try (LineNumberReader reader = new LineNumberReader(getReader(file))) {
187
188 String currentLine = reader.readLine();
189 while (currentLine != null) {
190 int index;
191 // look for a tag on this line
192 for (TagClass tagClass : tagClasses) {
193 index = tagClass.tagMatchContains(currentLine, sourceFileLocale);
194 if (index != TagClass.NO_MATCH) {
195 // there's a tag on this line
196 String commentType = extractCommentType(currentLine, index);
197 if (commentType == null) {
198 // this is not a valid comment tag: skip other tag classes and
199 // go to the next line
200 break;
201 }
202
203 int tagLength = tagClass.getLastTagMatchStringLength();
204 int commentStartIndex = reader.getLineNumber();
205 StringBuilder comment = new StringBuilder();
206
207 String firstLine = StringUtils.strip(currentLine.substring(index + tagLength));
208 firstLine = StringUtils.removeEnd(firstLine, "*/"); // MTAGLIST-35
209 if (firstLine.isEmpty() || ":".equals(firstLine)) {
210 // this is not a valid comment tag: nothing is written there
211 if (!emptyCommentsOn) {
212 continue;
213 }
214 } else {
215 // this tag has a comment
216 if (firstLine.charAt(0) == ':') {
217 comment.append(firstLine.substring(1).trim());
218 } else {
219 comment.append(firstLine);
220 }
221
222 if (multipleLineCommentsOn) {
223 // Mark the current position, set the read forward limit to
224 // a large number that should not be met.
225 reader.mark(MAX_COMMENT_CHARACTERS);
226
227 // next line
228 String futureLine = reader.readLine();
229
230 // we're looking for multiple line comments
231 while (futureLine != null
232 && futureLine.trim().startsWith(commentType)
233 && !futureLine.contains(tagClass.getLastTagMatchString())) {
234 String currentComment = futureLine
235 .substring(futureLine.indexOf(commentType) + commentType.length())
236 .trim();
237 if (currentComment.startsWith("@")
238 || currentComment.isEmpty()
239 || "/".equals(currentComment)) {
240 // the comment is finished
241 break;
242 }
243 // try to look if the next line is not a new tag
244 boolean newTagFound = false;
245 for (TagClass tc : tagClasses) {
246 if (tc.tagMatchStartsWith(currentComment, sourceFileLocale)) {
247 newTagFound = true;
248 break;
249 }
250 }
251 if (newTagFound) {
252 // this is a new comment: stop here the current comment
253 break;
254 }
255 // nothing was found: this means the comment is going on this line
256 comment.append(" ");
257 comment.append(currentComment);
258 futureLine = reader.readLine();
259 }
260
261 // Reset the reader to the marked position before the multi
262 // line check was performed.
263 reader.reset();
264 }
265 }
266 TagReport tagReport = tagClass.getTagReport();
267 FileReport fileReport = tagReport.getFileReport(file, encoding);
268 fileReport.addComment(comment.toString(), commentStartIndex);
269 }
270 }
271 currentLine = reader.readLine();
272 }
273 } catch (IOException e) {
274 log.error("Error while scanning the file " + file.getPath(), e);
275 }
276 }
277
278 /**
279 * Finds the type of comment the tag is in.
280 *
281 * @param currentLine the line to analyze.
282 * @param index the index of the tag in the line.
283 * @return "*" or "//" or null.
284 */
285 private String extractCommentType(String currentLine, int index) {
286 String commentType = null;
287 String beforeTag = currentLine.substring(0, index).trim();
288 if (beforeTag.endsWith(SLASH_COMMENT)) {
289 commentType = SLASH_COMMENT;
290 } else if (beforeTag.endsWith(STAR_COMMENT)) {
291 commentType = STAR_COMMENT;
292 }
293 return commentType;
294 }
295 }