• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / #3719

pending completion
#3719

push

github actions

adangel
Fix dogfood pmd violations

1 of 1 new or added line in 1 file covered. (100.0%)

66857 of 127267 relevant lines covered (52.53%)

0.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.34
/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.document;
6

7
import java.io.Closeable;
8
import java.io.File;
9
import java.io.IOException;
10
import java.net.URI;
11
import java.nio.charset.Charset;
12
import java.nio.charset.StandardCharsets;
13
import java.nio.file.FileSystem;
14
import java.nio.file.FileSystemAlreadyExistsException;
15
import java.nio.file.FileSystems;
16
import java.nio.file.FileVisitResult;
17
import java.nio.file.Files;
18
import java.nio.file.Path;
19
import java.nio.file.ProviderNotFoundException;
20
import java.nio.file.SimpleFileVisitor;
21
import java.nio.file.attribute.BasicFileAttributes;
22
import java.util.ArrayList;
23
import java.util.Collections;
24
import java.util.Comparator;
25
import java.util.HashSet;
26
import java.util.Iterator;
27
import java.util.List;
28
import java.util.Objects;
29
import java.util.Set;
30

31
import org.slf4j.Logger;
32
import org.slf4j.LoggerFactory;
33

34
import net.sourceforge.pmd.PmdAnalysis;
35
import net.sourceforge.pmd.annotation.InternalApi;
36
import net.sourceforge.pmd.internal.util.AssertionUtil;
37
import net.sourceforge.pmd.lang.Language;
38
import net.sourceforge.pmd.lang.LanguageVersion;
39
import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
40
import net.sourceforge.pmd.util.IOUtil;
41
import net.sourceforge.pmd.util.log.MessageReporter;
42

43
/**
44
 * Collects files to analyse before a PMD run. This API allows opening
45
 * zip files and makes sure they will be closed at the end of a run.
46
 *
47
 * @author Clément Fournier
48
 */
49
@SuppressWarnings("PMD.CloseResource")
50
public final class FileCollector implements AutoCloseable {
51

52
    private static final Logger LOG = LoggerFactory.getLogger(FileCollector.class);
1✔
53

54
    private final List<TextFile> allFilesToProcess = new ArrayList<>();
1✔
55
    private final List<Closeable> resourcesToClose = new ArrayList<>();
1✔
56
    private Charset charset = StandardCharsets.UTF_8;
1✔
57
    private final LanguageVersionDiscoverer discoverer;
58
    private final MessageReporter reporter;
59
    private final List<String> relativizeRoots = new ArrayList<>();
1✔
60
    private boolean closed;
61

62
    // construction
63

64
    private FileCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) {
1✔
65
        this.discoverer = discoverer;
1✔
66
        this.reporter = reporter;
1✔
67
    }
1✔
68

69
    /**
70
     * Internal API: please use {@link PmdAnalysis#files()} instead of
71
     * creating a collector yourself.
72
     */
73
    @InternalApi
74
    public static FileCollector newCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) {
75
        return new FileCollector(discoverer, reporter);
1✔
76
    }
77

78
    /**
79
     * Returns a new collector using the configuration except for the logger.
80
     */
81
    @InternalApi
82
    public FileCollector newCollector(MessageReporter logger) {
83
        FileCollector fileCollector = new FileCollector(discoverer, logger);
1✔
84
        fileCollector.charset = this.charset;
1✔
85
        fileCollector.relativizeRoots.addAll(this.relativizeRoots);
1✔
86
        return fileCollector;
1✔
87
    }
88

89
    // public behaviour
90

91
    /**
92
     * Returns an unmodifiable list of all files that have been collected.
93
     *
94
     * <p>Internal: This might be unstable until PMD 7, but it's internal.
95
     */
96
    @InternalApi
97
    public List<TextFile> getCollectedFiles() {
98
        if (closed) {
1✔
99
            throw new IllegalStateException("Collector was closed!");
×
100
        }
101
        allFilesToProcess.sort(Comparator.comparing(TextFile::getPathId));
1✔
102
        return Collections.unmodifiableList(allFilesToProcess);
1✔
103
    }
104

105

106
    /**
107
     * Returns the reporter for the file collection phase.
108
     */
109
    @InternalApi
110
    public MessageReporter getReporter() {
111
        return reporter;
1✔
112
    }
113

114
    /**
115
     * Close registered resources like zip files.
116
     */
117
    @Override
118
    public void close() {
119
        if (closed) {
1✔
120
            return;
1✔
121
        }
122
        closed = true;
1✔
123
        Exception exception = IOUtil.closeAll(resourcesToClose);
1✔
124
        if (exception != null) {
1✔
125
            reporter.errorEx("Error while closing resources", exception);
×
126
        }
127
    }
1✔
128

129
    // collection
130

131
    /**
132
     * Add a file, language is determined automatically from
133
     * the extension/file patterns. The encoding is the current
134
     * encoding ({@link #setCharset(Charset)}).
135
     *
136
     * @param file File to add
137
     *
138
     * @return True if the file has been added
139
     */
140
    public boolean addFile(Path file) {
141
        if (!Files.isRegularFile(file)) {
1✔
142
            reporter.error("Not a regular file: {0}", file);
1✔
143
            return false;
1✔
144
        }
145
        LanguageVersion languageVersion = discoverLanguage(file.toString());
1✔
146
        if (languageVersion != null) {
1✔
147
            addFileImpl(TextFile.builderForPath(file, charset, languageVersion)
1✔
148
                                .withDisplayName(getDisplayName(file))
1✔
149
                                .build());
1✔
150
            return true;
1✔
151
        }
152
        return false;
1✔
153
    }
154

155
    /**
156
     * Add a file with the given language (which overrides the file patterns).
157
     * The encoding is the current encoding ({@link #setCharset(Charset)}).
158
     *
159
     * @param file     Path to a file
160
     * @param language A language. The language version will be taken to be the
161
     *                 contextual default version.
162
     *
163
     * @return True if the file has been added
164
     */
165
    public boolean addFile(Path file, Language language) {
166
        AssertionUtil.requireParamNotNull("language", language);
1✔
167
        if (!Files.isRegularFile(file)) {
1✔
168
            reporter.error("Not a regular file: {0}", file);
×
169
            return false;
×
170
        }
171
        LanguageVersion lv = discoverer.getDefaultLanguageVersion(language);
1✔
172
        Objects.requireNonNull(lv);
1✔
173
        addFileImpl(TextFile.builderForPath(file, charset, lv)
1✔
174
                            .withDisplayName(getDisplayName(file))
1✔
175
                            .build());
1✔
176
        return true;
1✔
177
    }
178

179
    /**
180
     * Add a pre-configured text file. The language version will be checked
181
     * to match the contextual default for the language (the file cannot be added
182
     * if it has a different version).
183
     *
184
     * @return True if the file has been added
185
     */
186
    public boolean addFile(TextFile textFile) {
187
        AssertionUtil.requireParamNotNull("textFile", textFile);
1✔
188
        if (checkContextualVersion(textFile)) {
1✔
189
            addFileImpl(textFile);
1✔
190
            return true;
1✔
191
        }
192
        return false;
×
193
    }
194

195
    /**
196
     * Add a text file given its contents and a name. The language version
197
     * will be determined from the name as usual.
198
     *
199
     * @return True if the file has been added
200
     */
201
    public boolean addSourceFile(String pathId, String sourceContents) {
202
        AssertionUtil.requireParamNotNull("sourceContents", sourceContents);
1✔
203
        AssertionUtil.requireParamNotNull("pathId", pathId);
1✔
204

205
        LanguageVersion version = discoverLanguage(pathId);
1✔
206
        if (version != null) {
1✔
207
            addFileImpl(TextFile.builderForCharSeq(sourceContents, pathId, version).build());
1✔
208
            return true;
1✔
209
        }
210

211
        return false;
×
212
    }
213

214
    private void addFileImpl(TextFile textFile) {
215
        LOG.trace("Adding file {} (lang: {}) ", textFile.getPathId(), textFile.getLanguageVersion().getTerseName());
1✔
216
        allFilesToProcess.add(textFile);
1✔
217
    }
1✔
218

219
    private LanguageVersion discoverLanguage(String file) {
220
        if (discoverer.getForcedVersion() != null) {
1✔
221
            return discoverer.getForcedVersion();
1✔
222
        }
223
        List<Language> languages = discoverer.getLanguagesForFile(file);
1✔
224

225
        if (languages.isEmpty()) {
1✔
226
            LOG.trace("File {} matches no known language, ignoring", file);
1✔
227
            return null;
1✔
228
        }
229
        Language lang = languages.get(0);
1✔
230
        if (languages.size() > 1) {
1✔
231
            LOG.trace("File {} matches multiple languages ({}), selecting {}", file, languages, lang);
×
232
        }
233
        return discoverer.getDefaultLanguageVersion(lang);
1✔
234
    }
235

236
    /**
237
     * Whether the LanguageVersion of the file matches the one set in
238
     * the {@link LanguageVersionDiscoverer}. This is required to ensure
239
     * that all files for a given language have the same language version.
240
     */
241
    private boolean checkContextualVersion(TextFile textFile) {
242
        LanguageVersion fileVersion = textFile.getLanguageVersion();
1✔
243
        Language language = fileVersion.getLanguage();
1✔
244
        LanguageVersion contextVersion = discoverer.getDefaultLanguageVersion(language);
1✔
245
        if (!fileVersion.equals(contextVersion)) {
1✔
246
            reporter.error(
×
247
                "Cannot add file {0}: version ''{1}'' does not match ''{2}''",
248
                textFile.getPathId(),
×
249
                fileVersion,
250
                contextVersion
251
            );
252
            return false;
×
253
        }
254
        return true;
1✔
255
    }
256

257
    private String getDisplayName(Path file) {
258
        return getDisplayName(file, relativizeRoots);
1✔
259
    }
260

261
    /**
262
     * Return the textfile's display name.
263
     * test only
264
     */
265
    static String getDisplayName(Path file, List<String> relativizeRoots) {
266
        String fileName = file.toString();
1✔
267
        if ("jar".equals(file.toUri().getScheme())) {
1✔
268
            fileName = new File(URI.create(file.toUri().getSchemeSpecificPart()).getPath()).toString();
1✔
269
        }
270
        for (String root : relativizeRoots) {
1✔
271
            if (file.startsWith(root)) {
1✔
272
                if (fileName.startsWith(File.separator, root.length())) {
1✔
273
                    // remove following '/'
274
                    return fileName.substring(root.length() + 1);
1✔
275
                }
276
                return fileName.substring(root.length());
×
277
            }
278
        }
×
279
        return fileName;
1✔
280
    }
281

282

283
    /**
284
     * Add a directory recursively using {@link #addFile(Path)} on
285
     * all regular files.
286
     *
287
     * @param dir Directory path
288
     *
289
     * @return True if the directory has been added
290
     */
291
    public boolean addDirectory(Path dir) throws IOException {
292
        if (!Files.isDirectory(dir)) {
1✔
293
            reporter.error("Not a directory {0}", dir);
×
294
            return false;
×
295
        }
296
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
1✔
297
            @Override
298
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
299
                if (attrs.isRegularFile()) {
1✔
300
                    FileCollector.this.addFile(file);
1✔
301
                }
302
                return super.visitFile(file, attrs);
1✔
303
            }
304
        });
305
        return true;
1✔
306
    }
307

308

309
    /**
310
     * Add a file or directory recursively. Language is determined automatically
311
     * from the extension/file patterns.
312
     *
313
     * @return True if the file or directory has been added
314
     */
315
    public boolean addFileOrDirectory(Path file) throws IOException {
316
        if (Files.isDirectory(file)) {
1✔
317
            return addDirectory(file);
1✔
318
        } else if (Files.isRegularFile(file)) {
×
319
            return addFile(file);
×
320
        } else {
321
            reporter.error("Not a file or directory {0}", file);
×
322
            return false;
×
323
        }
324
    }
325

326
    /**
327
     * Opens a zip file and returns a FileSystem for its contents, so
328
     * it can be explored with the {@link Path} API. You can then call
329
     * {@link #addFile(Path)} and such. The zip file is registered as
330
     * a resource to close at the end of analysis.
331
     */
332
    public FileSystem addZipFile(Path zipFile) {
333
        if (!Files.isRegularFile(zipFile)) {
1✔
334
            throw new IllegalArgumentException("Not a regular file: " + zipFile);
×
335
        }
336
        URI zipUri = URI.create("jar:" + zipFile.toUri());
1✔
337
        try {
338
            FileSystem fs = FileSystems.newFileSystem(zipUri, Collections.<String, Object>emptyMap());
1✔
339
            resourcesToClose.add(fs);
1✔
340
            return fs;
1✔
341
        } catch (FileSystemAlreadyExistsException | ProviderNotFoundException | IOException e) {
×
342
            reporter.errorEx("Cannot open zip file " + zipFile, e);
×
343
            return null;
×
344
        }
345
    }
346

347
    // configuration
348

349
    /**
350
     * Sets the charset to use for subsequent calls to {@link #addFile(Path)}
351
     * and other overloads using a {@link Path}.
352
     *
353
     * @param charset A charset
354
     */
355
    public void setCharset(Charset charset) {
356
        this.charset = Objects.requireNonNull(charset);
1✔
357
    }
1✔
358

359
    /**
360
     * Add a prefix that is used to relativize file paths as their display name.
361
     * For instance, when adding a file {@code /tmp/src/main/java/org/foo.java},
362
     * and relativizing with {@code /tmp/src/}, the registered {@link  TextFile}
363
     * will have a path id of {@code /tmp/src/main/java/org/foo.java}, and a
364
     * display name of {@code main/java/org/foo.java}.
365
     *
366
     * This only matters for files added from a {@link Path} object.
367
     *
368
     * @param prefix Prefix to relativize (if a directory, include a trailing slash)
369
     */
370
    public void relativizeWith(String prefix) {
371
        this.relativizeRoots.add(Objects.requireNonNull(prefix));
1✔
372
    }
1✔
373

374
    // filtering
375

376
    /**
377
     * Remove all files collected by the given collector from this one.
378
     */
379
    public void exclude(FileCollector excludeCollector) {
380
        Set<TextFile> toExclude = new HashSet<>(excludeCollector.allFilesToProcess);
1✔
381
        for (Iterator<TextFile> iterator = allFilesToProcess.iterator(); iterator.hasNext();) {
1✔
382
            TextFile file = iterator.next();
1✔
383
            if (toExclude.contains(file)) {
1✔
384
                LOG.trace("Excluding file {}", file.getPathId());
1✔
385
                iterator.remove();
1✔
386
            }
387
        }
1✔
388
    }
1✔
389

390
    /**
391
     * Exclude all collected files whose language is not part of the given
392
     * collection.
393
     */
394
    public void filterLanguages(Set<Language> languages) {
395
        for (Iterator<TextFile> iterator = allFilesToProcess.iterator(); iterator.hasNext();) {
1✔
396
            TextFile file = iterator.next();
1✔
397
            Language lang = file.getLanguageVersion().getLanguage();
1✔
398
            if (!languages.contains(lang)) {
1✔
399
                LOG.trace("Filtering out {}, no rules for language {}", file.getPathId(), lang);
×
400
                iterator.remove();
×
401
            }
402
        }
1✔
403
    }
1✔
404

405
    @Override
406
    public String toString() {
407
        return "FileCollector{filesToProcess=" + allFilesToProcess + '}';
×
408
    }
409
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc