diff --git a/.all-contributorsrc b/.all-contributorsrc index 531090e31d..7d94f872d8 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -6539,6 +6539,15 @@ "code" ] }, + { + "login": "filiprafalowicz", + "name": "filiprafalowicz", + "avatar_url": "https://avatars.githubusercontent.com/u/24355557?v=4", + "profile": "https://github.com/filiprafalowicz", + "contributions": [ + "code" + ] + }, { "login": "JerritEic", "name": "JerritEic", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87417153a5..b4b787a283 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: os: [ ubuntu-latest, windows-latest, macos-latest ] if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 2 - uses: actions/cache@v2 diff --git a/.github/workflows/git-repo-sync.yml b/.github/workflows/git-repo-sync.yml index 2219f2330a..250dd00933 100644 --- a/.github/workflows/git-repo-sync.yml +++ b/.github/workflows/git-repo-sync.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 100 - name: Setup Environment diff --git a/.github/workflows/troubleshooting.yml b/.github/workflows/troubleshooting.yml index d58e0f2aa1..4554ddedf7 100644 --- a/.github/workflows/troubleshooting.yml +++ b/.github/workflows/troubleshooting.yml @@ -12,7 +12,7 @@ jobs: os: [ ubuntu-latest ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | diff --git a/docs/pages/pmd/projectdocs/credits.md b/docs/pages/pmd/projectdocs/credits.md index c2d45320d9..fdac14b9c1 100644 --- a/docs/pages/pmd/projectdocs/credits.md +++ b/docs/pages/pmd/projectdocs/credits.md @@ -775,163 +775,164 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
ekkirala

πŸ›
emersonmoura

πŸ›
fairy

πŸ› +
filiprafalowicz

πŸ’»
foxmason

πŸ› -
frankegabor

πŸ› +
frankegabor

πŸ›
frankl

πŸ›
freafrea

πŸ›
fsapatin

πŸ›
gracia19

πŸ›
guo fei

πŸ›
gurmsc5

πŸ› -
gwilymatgearset

πŸ’» πŸ› +
gwilymatgearset

πŸ’» πŸ›
haigsn

πŸ›
hemanshu070

πŸ›
henrik242

πŸ›
hongpuwu

πŸ›
hvbtup

πŸ’» πŸ›
igniti GmbH

πŸ› -
ilovezfs

πŸ› +
ilovezfs

πŸ›
itaigilo

πŸ›
jakivey32

πŸ›
jbennett2091

πŸ›
jcamerin

πŸ›
jkeener1

πŸ›
jmetertea

πŸ› -
johnra2

πŸ’» +
johnra2

πŸ’»
josemanuelrolon

πŸ’» πŸ›
kabroxiko

πŸ’» πŸ›
karwer

πŸ›
kaulonline

πŸ›
kdaemonv

πŸ›
kenji21

πŸ’» πŸ› -
kfranic

πŸ› +
kfranic

πŸ›
khalidkh

πŸ›
krzyk

πŸ›
lasselindqvist

πŸ›
lihuaib

πŸ›
lonelyma1021

πŸ›
lpeddy

πŸ› -
lujiefsi

πŸ’» +
lujiefsi

πŸ’»
lyriccoder

πŸ›
marcelmore

πŸ›
matchbox

πŸ›
matthiaskraaz

πŸ›
meandonlyme

πŸ›
mikesive

πŸ› -
milossesic

πŸ› +
milossesic

πŸ›
mriddell95

πŸ›
mrlzh

πŸ›
msloan

πŸ›
mucharlaravalika

πŸ›
mvenneman

πŸ›
nareshl119

πŸ› -
nicolas-harraudeau-sonarsource

πŸ› +
nicolas-harraudeau-sonarsource

πŸ›
noerremark

πŸ›
novsirion

πŸ›
oggboy

πŸ›
oinume

πŸ›
orimarko

πŸ’» πŸ›
pallavi agarwal

πŸ› -
parksungrin

πŸ› +
parksungrin

πŸ›
patpatpat123

πŸ›
patriksevallius

πŸ›
pbrajesh1

πŸ›
phoenix384

πŸ›
piotrszymanski-sc

πŸ’»
plan3d

πŸ› -
poojasix

πŸ› +
poojasix

πŸ›
prabhushrikant

πŸ›
pujitha8783

πŸ›
r-r-a-j

πŸ›
raghujayjunk

πŸ›
rajeshveera

πŸ›
rajeswarreddy88

πŸ› -
recdevs

πŸ› +
recdevs

πŸ›
reudismam

πŸ’» πŸ›
rijkt

πŸ›
rillig-tk

πŸ›
rmohan20

πŸ’» πŸ›
rxmicro

πŸ›
ryan-gustafson

πŸ’» πŸ› -
sabi0

πŸ› +
sabi0

πŸ›
scais

πŸ›
sebbASF

πŸ›
sergeygorbaty

πŸ’»
shilko2013

πŸ›
simeonKondr

πŸ›
snajberk

πŸ› -
sniperrifle2004

πŸ› +
sniperrifle2004

πŸ›
snuyanzin

πŸ› πŸ’»
sratz

πŸ›
stonio

πŸ›
sturton

πŸ’» πŸ›
sudharmohan

πŸ›
suruchidawar

πŸ› -
svenfinitiv

πŸ› +
svenfinitiv

πŸ›
tashiscool

πŸ›
test-git-hook

πŸ›
testation21

πŸ’» πŸ›
thanosa

πŸ›
tiandiyixian

πŸ›
tobwoerk

πŸ› -
tprouvot

πŸ› +
tprouvot

πŸ›
trentchilders

πŸ›
triandicAnt

πŸ›
trishul14

πŸ›
tsui

πŸ›
winhkey

πŸ›
witherspore

πŸ› -
wjljack

πŸ› +
wjljack

πŸ›
wuchiuwong

πŸ›
xingsong

πŸ›
xioayuge

πŸ›
xnYi9wRezm

πŸ’» πŸ›
xuanuy

πŸ›
xyf0921

πŸ› -
yalechen-cyw3

πŸ› +
yalechen-cyw3

πŸ›
yasuharu-sato

πŸ›
zenglian

πŸ›
zgrzyt93

πŸ’» πŸ›
zh3ng

πŸ›
zt_soft

πŸ›
ztt79

πŸ› -
zzzzfeng

πŸ› +
zzzzfeng

πŸ›
ÁrpÑd MagosÑnyi

πŸ›
任贡杰

πŸ› diff --git a/docs/pages/pmd/userdocs/tools/java-api.md b/docs/pages/pmd/userdocs/tools/java-api.md index f313ba54dc..36ebb017ea 100644 --- a/docs/pages/pmd/userdocs/tools/java-api.md +++ b/docs/pages/pmd/userdocs/tools/java-api.md @@ -66,11 +66,11 @@ public class PmdExample { public static void main(String[] args) { PMDConfiguration configuration = new PMDConfiguration(); configuration.setInputPaths("/home/workspace/src/main/java/code"); - configuration.setRuleSets("rulesets/java/quickstart.xml"); + configuration.addRuleSet("rulesets/java/quickstart.xml"); configuration.setReportFormat("xml"); configuration.setReportFile("/home/workspace/pmd-report.xml"); - PMD.runPMD(configuration); + PMD.runPmd(configuration); } } ``` @@ -80,66 +80,75 @@ public class PmdExample { This gives you more control over which files are processed, but is also more complicated. You can also provide your own custom renderers. -1. First we create a `PMDConfiguration`. This is currently the only way to specify a ruleset: +1. First we create a `PMDConfiguration` and configure it, first the rules: ```java PMDConfiguration configuration = new PMDConfiguration(); configuration.setMinimumPriority(RulePriority.MEDIUM); - configuration.setRuleSets("rulesets/java/quickstart.xml"); + configuration.addRuleSet("rulesets/java/quickstart.xml"); ``` -2. In order to support type resolution, PMD needs to have access to the compiled classes and dependencies - as well. This is called "auxclasspath" and is also configured here. +2. Then we configure, which paths to analyze: + + ```java + configuration.setInputPaths("/home/workspace/src/main/java/code"); + ``` + +3. The we configure the default language version for Java. And in order to support type resolution, + PMD needs to have access to the compiled classes and dependencies as well. This is called + "auxclasspath" and is also configured here. + Note: you can specify multiple class paths separated by `:` on Unix-systems or `;` under Windows. ```java - configuration.prependClasspath("/home/workspace/target/classes:/home/.m2/repository/my/dependency.jar"); + configuration.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11")); + configuration.prependAuxClasspath("/home/workspace/target/classes:/home/.m2/repository/my/dependency.jar"); ``` -3. Then we need to load the rulesets. This is done by using the configuration, taking the minimum priority into - account: +4. Then we configure the reporting. Configuring the report file is optional. If not specified, the report + will be written to `stdout`. ```java - RuleSetLoader ruleSetLoader = RuleSetLoader.fromPmdConfig(configuration); - List ruleSets = ruleSetLoader.loadFromResources(Arrays.asList(configuration.getRuleSets().split(","))); + configuration.setReportFormat("xml"); + configuration.setReportFile("/home/workspace/pmd-report.xml"); ``` -4. PMD operates on a list of `DataSource`. You can assemble a own list of `FileDataSource`, e.g. +5. Now an optional step: If you want to use additional renderers as in the example, set them up before + calling PMD. You can use a built-in renderer, e.g. `XMLRenderer` or a custom renderer implementing + `Renderer`. Note, that you must manually initialize the renderer by setting a suitable `Writer`: ```java - List files = Arrays.asList(new FileDataSource(new File("/path/to/src/MyClass.java"))); - ``` - -5. For reporting, you can use `GlobalAnalysisListener`, which receives events like violations and errors. - Useful implementations are provided by `Renderer` instances. To use a renderer, eg the built-in `XMLRenderer`, - create it and configure it with a suitable `Writer`. + Writer rendererOutput = new StringWriter(); + Renderer renderer = createRenderer(rendererOutput); - ```java - StringWriter rendererOutput = new StringWriter(); - Renderer xmlRenderer = new XMLRenderer("UTF-8"); - xmlRenderer.setWriter(rendererOutput); - // The listener is created from the renderer in the next listing + // ... + private static Renderer createRenderer(Writer writer) { + XMLRenderer xml = new XMLRenderer("UTF-8"); + xml.setWriter(writer); + return xml; + } ``` -6. Now, all the preparations are done, and PMD can be executed. This is done by calling - `PMD.processFiles(...)`. This method call takes the configuration, the rulesets, the files - to process, and the list of renderers. Provide an empty list, if you don't want to use - any renderer. Note: The auxclasspath needs to be closed explicitly. Otherwise the class or jar files may - remain open and file resources are leaked. +6. Finally we can start the PMD analysis. There is the possibility to fine-tune the configuration + by adding additional files to analyze or adding additional rulesets or renderers: ```java - try (GlobalAnalysisListener listener = xmlRenderer.newListener()) { - PMD.processFiles(configuration, ruleSets, files, listener); - } finally { - ClassLoader auxiliaryClassLoader = configuration.getClassLoader(); - if (auxiliaryClassLoader instanceof ClasspathClassLoader) { - ((ClasspathClassLoader) auxiliaryClassLoader).close(); - } + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + // optional: add more rulesets + pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml")); + // optional: add more files + pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java")); + // optional: add more renderers + pmd.addRenderer(renderer); + + // or just call PMD + pmd.performAnalysis(); } ``` -7. After the call, the renderer will have been flushed by PMD (through its `GlobalAnalysisListener`). - Then you can check the rendered output. + The renderer will be automatically flushed and closed at the end of the analysis. + +7. Then you can check the rendered output. ``` java System.out.println("Rendered Report:"); @@ -152,51 +161,43 @@ Here is a complete example: import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.nio.file.Paths; -import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; +import net.sourceforge.pmd.PmdAnalysis; import net.sourceforge.pmd.RulePriority; -import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetLoader; +import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.renderers.XMLRenderer; -import net.sourceforge.pmd.util.ClasspathClassLoader; -import net.sourceforge.pmd.util.datasource.DataSource; -import net.sourceforge.pmd.util.datasource.FileDataSource; public class PmdExample2 { public static void main(String[] args) throws IOException { PMDConfiguration configuration = new PMDConfiguration(); configuration.setMinimumPriority(RulePriority.MEDIUM); - configuration.setRuleSets("rulesets/java/quickstart.xml"); - configuration.prependClasspath("/home/workspace/target/classes"); - RuleSetLoader ruleSetLoader = RuleSetLoader.fromPmdConfig(configuration); - List ruleSets = ruleSetLoader.loadFromResources(Arrays.asList(configuration.getRuleSets().split(","))); + configuration.addRuleSet("rulesets/java/quickstart.xml"); - List files = determineFiles("/home/workspace/src/main/java/code"); + configuration.setInputPaths("/home/workspace/src/main/java/code"); + + configuration.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11")); + configuration.prependAuxClasspath("/home/workspace/target/classes"); + + configuration.setReportFormat("xml"); + configuration.setReportFile("/home/workspace/pmd-report.xml"); Writer rendererOutput = new StringWriter(); Renderer renderer = createRenderer(rendererOutput); - try (GlobalAnalysisListener listener = renderer.newListener()) { - PMD.processFiles(configuration, ruleSets, files, listener); - } finally { - ClassLoader auxiliaryClassLoader = configuration.getClassLoader(); - if (auxiliaryClassLoader instanceof ClasspathClassLoader) { - ((ClasspathClassLoader) auxiliaryClassLoader).close(); - } + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + // optional: add more rulesets + pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml")); + // optional: add more files + pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java")); + // optional: add more renderers + pmd.addRenderer(renderer); + + // or just call PMD + pmd.performAnalysis(); } System.out.println("Rendered Report:"); @@ -208,28 +209,6 @@ public class PmdExample2 { xml.setWriter(writer); return xml; } - - private static List determineFiles(String basePath) throws IOException { - Path dirPath = FileSystems.getDefault().getPath(basePath); - final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java"); - - final List files = new ArrayList<>(); - - Files.walkFileTree(dirPath, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { - if (matcher.matches(path.getFileName())) { - System.out.printf("Using %s%n", path); - files.add(new FileDataSource(path.toFile())); - } else { - System.out.printf("Ignoring %s%n", path); - } - return super.visitFile(path, attrs); - } - }); - System.out.printf("Analyzing %d files in %s%n", files.size(), basePath); - return files; - } } ``` diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 8abb75f161..31b9997389 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,11 +19,83 @@ This is a {{ site.pmd.release_type }} release. ### New and noteworthy + +#### New programmatic API + +This release introduces a new programmatic API to replace the inflexible {% jdoc core::PMD %} class. +Programmatic execution of PMD should now be done with a {% jdoc core::PMDConfiguration %} +and a {% jdoc core::PmdAnalysis %}, for instance: + +```java +PMDConfiguration config = new PMDConfiguration(); +config.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11")); +config.setInputPaths("src/main/java"); +config.prependAuxClasspath("target/classes"); +config.setMinimumPriority(RulePriority.HIGH); +config.addRuleSet("rulesets/java/quickstart.xml"); +config.setReportFormat("xml"); +config.setReportFile("target/pmd-report.xml"); + +try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + // note: don't use `config` once a PmdAnalysis has been created. + // optional: add more rulesets + pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml")); + // optional: add more files + pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java")); + // optional: add more renderers + pmd.addRenderer(renderer); + + // or just call PMD + pmd.performAnalysis(); +} +``` + +The `PMD` class still supports methods related to CLI execution: `runPmd` and `main`. +All other members are now deprecated for removal. +The CLI itself remains compatible, if you run PMD via command-line, no action is required on your part. + ### Fixed Issues +* apex-performance + * [#3773](https://github.com/pmd/pmd/pull/3773): \[apex] EagerlyLoadedDescribeSObjectResult false positives with SObjectField.getDescribe() +* core + * [#3299](https://github.com/pmd/pmd/issues/3299): \[core] Deprecate system properties of PMDCommandLineInterface + ### API Changes +#### Deprecated API + +* Several members of {% jdoc core::PMD %} have been newly deprecated, including: + - `PMD#EOL`: use `System#lineSeparator()` + - `PMD#SUPPRESS_MARKER`: use {% jdoc core::PMDConfiguration#DEFAULT_SUPPRESS_MARKER %} + - `PMD#processFiles`: use the [new programmatic API](#new-programmatic-api) + - `PMD#getApplicableFiles`: is internal +* {% jdoc !!core::PMDConfiguration#prependClasspath(java.lang.String) %} is deprecated + in favour of {% jdoc core::PMDConfiguration#prependAuxClasspath(java.lang.String) %}. +* {% jdoc !!core::PMDConfiguration#setRuleSets(java.lang.String) %} and + {% jdoc core::PMDConfiguration#getRuleSets() %} are deprecated. Use instead + {% jdoc core::PMDConfiguration#setRuleSets(java.util.List) %}, + {% jdoc core::PMDConfiguration#addRuleSet(java.lang.String) %}, + and {% jdoc core::PMDConfiguration#getRuleSetPaths() %}. +* Several members of {% jdoc test::cli.BaseCLITest %} have been deprecated with replacements. +* Several members of {% jdoc core::cli.PMDCommandLineInterface %} have been explicitly deprecated. + The whole class however was deprecated long ago already with 6.30.0. It is internal API and should + not be used. + +#### Experimental APIs + +* Together with the [new programmatic API](#new-programmatic-api) the interface + {% jdoc core::lang.document.TextFile %} has been added as *experimental*. It intends + to replace {% jdoc core::util.datasource.DataSource %} and {% jdoc core::cpd.SourceCode %} in the long term. + + This interface will change in PMD 7 to support read/write operations + and other things. You don't need to use it in PMD 6, as {% jdoc core::lang.document.FileCollector %} + decouples you from this. A file collector is available through {% jdoc !!core::PmdAnalysis#files() %}. + + ### External Contributions +* [#3773](https://github.com/pmd/pmd/pull/3773): \[apex] EagerlyLoadedDescribeSObjectResult false positives with SObjectField.getDescribe() - [@filiprafalowicz](https://github.com/filiprafalowicz) + {% endtocmaker %} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTReferenceExpression.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTReferenceExpression.java index 147c1ca998..b78802194f 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTReferenceExpression.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTReferenceExpression.java @@ -56,4 +56,10 @@ public final class ASTReferenceExpression extends AbstractApexNode identifiers = node.getNames(); + return identifiers != null + && identifiers.stream().anyMatch(id -> "sobjecttype".equalsIgnoreCase(id.getValue())); + } } diff --git a/pmd-apex/src/main/resources/category/apex/performance.xml b/pmd-apex/src/main/resources/category/apex/performance.xml index 731a8e1aa3..50f749dd88 100644 --- a/pmd-apex/src/main/resources/category/apex/performance.xml +++ b/pmd-apex/src/main/resources/category/apex/performance.xml @@ -146,23 +146,26 @@ public class Something { This rule finds `DescribeSObjectResult`s which could have been loaded eagerly via `SObjectType.getDescribe()`. -When using `SObjectType.getDescribe()` or `Schema.describeSObjects()` without supplying a `SObjectDescribeOptions`, implicitely it will be using `SObjectDescribeOptions.DEFAULT` then all +When using `SObjectType.getDescribe()` or `Schema.describeSObjects()` without supplying a `SObjectDescribeOptions`, +implicitly it will be using `SObjectDescribeOptions.DEFAULT` and then all child relationships will be loaded eagerly regardless whether this information is needed or not. This has a potential negative performance impact. Instead [`SObjectType.getDescribe(options)`](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Schema_SObjectType.htm#unique_346834793) -or [`Schema.describeSObjects(SObjectTypes, options)`](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_schema.htm#apex_System_Schema_describeSObjects) should be used and a `SObjectDescribeOptions` should be supplied. By using +or [`Schema.describeSObjects(SObjectTypes, options)`](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_schema.htm#apex_System_Schema_describeSObjects) +should be used and a `SObjectDescribeOptions` should be supplied. By using `SObjectDescribeOptions.DEFERRED` the describe attributes will be lazily initialized at first use. -Lazy loading `DescribeSObjectResult` on picklist fields is not recommended. The lazy loaded +Lazy loading `DescribeSObjectResult` on picklist fields is not always recommended. The lazy loaded describe objects might not be 100% accurate. It might be safer to explicitly use -`SObjectDescribeOptions.FULL` in such a case. The same applies when you need the same `DescribeSObjectResult` to be consistent -accross different contexts and API versions. +`SObjectDescribeOptions.FULL` in such a case. The same applies when you need the same `DescribeSObjectResult` +to be consistent across different contexts and API versions. Properties: * `noDefault`: The behavior of `SObjectDescribeOptions.DEFAULT` changes from API Version 43 to 44: With API Version 43, the attributes are loaded eagerly. With API Version 44, they are loaded lazily. Simply using `SObjectDescribeOptions.DEFAULT` doesn't automatically make use of lazy loading. - (unless "Use Improved Schema Caching" critical update is applied, `SObjectDescribeOptions.DEFAULT` do fallback to lazy loading) + (unless "Use Improved Schema Caching" critical update is applied, `SObjectDescribeOptions.DEFAULT` does fallback + to lazy loading) With this property enabled, such usages are found. You might ignore this, if you can make sure, that you don't run a mix of API Versions. @@ -173,8 +176,20 @@ Properties: diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SafeNavigationOperator.txt b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SafeNavigationOperator.txt index 3915093d23..db0bb1da49 100644 --- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SafeNavigationOperator.txt +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SafeNavigationOperator.txt @@ -9,7 +9,7 @@ | +- ModifierNode[@Abstract = "false", @ApexVersion = "54.0", @DefiningType = "Foo", @DeprecatedTestMethod = "false", @Final = "false", @Global = "false", @InheritedSharing = "false", @Location = "no location", @Modifiers = "0", @Namespace = "", @Override = "false", @Private = "false", @Protected = "false", @Public = "false", @RealLoc = "false", @Static = "false", @Test = "false", @TestOrTestSetup = "false", @Transient = "false", @Virtual = "false", @WebService = "false", @WithSharing = "false", @WithoutSharing = "false"] | +- FieldDeclaration[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "anIntegerField", @Location = "(5, 13, 198, 199)", @Name = "anIntegerField", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "anIntegerField", @Location = "(5, 27, 212, 226)", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "anObject", @Location = "(5, 17, 202, 210)", @Namespace = "", @RealLoc = "true"] | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "x", @Location = "(5, 13, 198, 199)", @Namespace = "", @RealLoc = "true"] @@ -18,9 +18,9 @@ | +- ModifierNode[@Abstract = "false", @ApexVersion = "54.0", @DefiningType = "Foo", @DeprecatedTestMethod = "false", @Final = "false", @Global = "false", @InheritedSharing = "false", @Location = "no location", @Modifiers = "0", @Namespace = "", @Override = "false", @Private = "false", @Protected = "false", @Public = "false", @RealLoc = "false", @Static = "false", @Test = "false", @TestOrTestSetup = "false", @Transient = "false", @Virtual = "false", @WebService = "false", @WithSharing = "false", @WithoutSharing = "false"] | +- FieldDeclaration[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "profileUrl", @Location = "(8, 12, 365, 375)", @Name = "profileUrl", @Namespace = "", @RealLoc = "true"] | +- MethodCallExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @FullMethodName = "toExternalForm", @InputParametersSize = "0", @Location = "(8, 47, 400, 414)", @MethodName = "toExternalForm", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SafeNav = "true"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SObjectType = "false", @SafeNav = "true"] | | +- MethodCallExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @FullMethodName = "user.getProfileUrl", @InputParametersSize = "0", @Location = "(8, 30, 383, 396)", @MethodName = "getProfileUrl", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Image = "user", @Location = "(8, 25, 378, 382)", @Namespace = "", @RealLoc = "true", @ReferenceType = "METHOD", @SafeNav = "false"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Image = "user", @Location = "(8, 25, 378, 382)", @Namespace = "", @RealLoc = "true", @ReferenceType = "METHOD", @SObjectType = "false", @SafeNav = "false"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "profileUrl", @Location = "(8, 12, 365, 375)", @Namespace = "", @RealLoc = "true"] | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] +- Method[@ApexVersion = "54.0", @Arity = "1", @CanonicalName = "bar1", @Constructor = "false", @DefiningType = "Foo", @Image = "bar1", @Location = "(10, 17, 435, 439)", @Namespace = "", @RealLoc = "true", @ReturnType = "void", @Synthetic = "false"] @@ -30,15 +30,15 @@ | +- BlockStatement[@ApexVersion = "54.0", @CurlyBrace = "true", @DefiningType = "Foo", @Location = "(10, 32, 450, 538)", @Namespace = "", @RealLoc = "true"] | +- ExpressionStatement[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(11, 12, 463, 465)", @Namespace = "", @RealLoc = "true"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "b", @Location = "(11, 12, 463, 464)", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "a", @Location = "(11, 9, 460, 461)", @Namespace = "", @RealLoc = "true"] | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] | +- ExpressionStatement[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(12, 22, 527, 532)", @Namespace = "", @RealLoc = "true"] | +- MethodCallExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @FullMethodName = "c1", @InputParametersSize = "0", @Location = "(12, 22, 527, 529)", @MethodName = "c1", @Namespace = "", @RealLoc = "true"] - | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SafeNav = "true"] + | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SObjectType = "false", @SafeNav = "true"] | +- CastExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(12, 10, 515, 518)", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "b1", @Location = "(12, 17, 522, 524)", @Namespace = "", @RealLoc = "true"] - | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "a1", @Location = "(12, 13, 518, 520)", @Namespace = "", @RealLoc = "true"] | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] +- Method[@ApexVersion = "54.0", @Arity = "2", @CanonicalName = "bar2", @Constructor = "false", @DefiningType = "Foo", @Image = "bar2", @Location = "(15, 17, 556, 560)", @Namespace = "", @RealLoc = "true", @ReturnType = "void", @Synthetic = "false"] @@ -50,9 +50,9 @@ | +- BlockStatement[@ApexVersion = "54.0", @CurlyBrace = "true", @DefiningType = "Foo", @Location = "(15, 41, 580, 688)", @Namespace = "", @RealLoc = "true"] | +- ExpressionStatement[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(16, 25, 606, 613)", @Namespace = "", @RealLoc = "true"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "aField", @Location = "(16, 25, 606, 612)", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "false"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "false"] | | +- MethodCallExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @FullMethodName = "aMethod", @InputParametersSize = "0", @Location = "(16, 15, 596, 603)", @MethodName = "aMethod", @Namespace = "", @RealLoc = "true"] - | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SafeNav = "true"] + | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SObjectType = "false", @SafeNav = "true"] | | +- ArrayLoadExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(16, 9, 590, 591)", @Namespace = "", @RealLoc = "true"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "a", @Location = "(16, 9, 590, 591)", @Namespace = "", @RealLoc = "true"] | | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] @@ -60,9 +60,9 @@ | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] | +- ExpressionStatement[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(17, 25, 675, 682)", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "aField", @Location = "(17, 25, 675, 681)", @Namespace = "", @RealLoc = "true"] - | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | +- MethodCallExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @FullMethodName = "aMethod", @InputParametersSize = "0", @Location = "(17, 14, 664, 671)", @MethodName = "aMethod", @Namespace = "", @RealLoc = "true"] - | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SafeNav = "false"] + | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "METHOD", @SObjectType = "false", @SafeNav = "false"] | +- ArrayLoadExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(17, 9, 659, 660)", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "a", @Location = "(17, 9, 659, 660)", @Namespace = "", @RealLoc = "true"] | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] @@ -77,14 +77,14 @@ | | +- ModifierNode[@Abstract = "false", @ApexVersion = "54.0", @DefiningType = "Foo", @DeprecatedTestMethod = "false", @Final = "false", @Global = "false", @InheritedSharing = "false", @Location = "no location", @Modifiers = "0", @Namespace = "", @Override = "false", @Private = "false", @Protected = "false", @Public = "false", @RealLoc = "false", @Static = "false", @Test = "false", @TestOrTestSetup = "false", @Transient = "false", @Virtual = "false", @WebService = "false", @WithSharing = "false", @WithoutSharing = "false"] | | +- VariableDeclaration[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "s", @Location = "(21, 16, 744, 745)", @Namespace = "", @RealLoc = "true", @Type = "String"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "BillingCity", @Location = "(21, 37, 765, 776)", @Namespace = "", @RealLoc = "true"] - | | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "Account", @Location = "(21, 28, 756, 763)", @Namespace = "", @RealLoc = "true"] - | | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Image = "contact", @Location = "(21, 20, 748, 755)", @Namespace = "", @RealLoc = "true", @ReferenceType = "LOAD", @SafeNav = "false"] + | | | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Image = "contact", @Location = "(21, 20, 748, 755)", @Namespace = "", @RealLoc = "true", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "false"] | | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "s", @Location = "(21, 16, 744, 745)", @Namespace = "", @RealLoc = "true"] | | +- EmptyReferenceExpression[@ApexVersion = "54.0", @DefiningType = null, @Location = "no location", @Namespace = null, @RealLoc = "false"] | +- ReturnStatement[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(23, 9, 841, 899)", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "Name", @Location = "(23, 62, 894, 898)", @Namespace = "", @RealLoc = "true"] - | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SafeNav = "true"] + | +- ReferenceExpression[@ApexVersion = "54.0", @Context = null, @DefiningType = "Foo", @Location = "no location", @Namespace = "", @RealLoc = "false", @ReferenceType = "LOAD", @SObjectType = "false", @SafeNav = "true"] | +- SoqlExpression[@ApexVersion = "54.0", @CanonicalQuery = "SELECT Name FROM Account WHERE Id = :tmpVar1", @DefiningType = "Foo", @Location = "(23, 16, 848, 892)", @Namespace = "", @Query = "SELECT Name FROM Account WHERE Id = :accId", @RealLoc = "true"] | +- BindExpressions[@ApexVersion = "54.0", @DefiningType = "Foo", @Location = "(23, 16, 848, 892)", @Namespace = "", @RealLoc = "true"] | +- VariableExpression[@ApexVersion = "54.0", @DefiningType = "Foo", @Image = "accId", @Location = "(23, 54, 886, 891)", @Namespace = "", @RealLoc = "true"] diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/performance/xml/EagerlyLoadedDescribeSObjectResult.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/performance/xml/EagerlyLoadedDescribeSObjectResult.xml index c8f7aeb7ad..2e878dfa80 100644 --- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/performance/xml/EagerlyLoadedDescribeSObjectResult.xml +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/performance/xml/EagerlyLoadedDescribeSObjectResult.xml @@ -7,6 +7,7 @@ No describer options 1 + 3 accounts) { @@ -21,6 +22,7 @@ public class Foo { No describer options using Schema class 1 + 3 accounts) { @@ -89,4 +91,54 @@ public class Foo { ]]> + + False positive with no describer options on SObjectField + 0 + + + + + + False positive on SObjectField with FieldDescribeOptions.FULL_DESCRIBE + 0 + + + + + False positive on SObjectField with FieldDescribeOptions.DEFAULT + 0 + + + + + False positive on SObjectField with FieldDescribeOptions.DEFAULT with noDefault=true + true + 0 + + diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java index eb8fcb2381..9c1b261c88 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java @@ -6,21 +6,14 @@ package net.sourceforge.pmd; import static net.sourceforge.pmd.util.CollectionUtil.listOf; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FilenameFilter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; -import java.net.URISyntaxException; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; +import java.util.Comparator; import java.util.List; import java.util.Map.Entry; -import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +22,6 @@ import org.slf4j.event.Level; import net.sourceforge.pmd.Report.GlobalReportBuilderListener; import net.sourceforge.pmd.benchmark.TextTimingReportRenderer; import net.sourceforge.pmd.benchmark.TimeTracker; -import net.sourceforge.pmd.benchmark.TimedOperation; -import net.sourceforge.pmd.benchmark.TimedOperationCategory; import net.sourceforge.pmd.benchmark.TimingReport; import net.sourceforge.pmd.benchmark.TimingReportRenderer; import net.sourceforge.pmd.cache.NoopAnalysisCache; @@ -38,38 +29,19 @@ import net.sourceforge.pmd.cli.PMDCommandLineInterface; import net.sourceforge.pmd.cli.PmdParametersParseResult; import net.sourceforge.pmd.cli.internal.CliMessages; import net.sourceforge.pmd.internal.Slf4jSimpleConfiguration; -import net.sourceforge.pmd.internal.util.AssertionUtil; -import net.sourceforge.pmd.lang.Language; -import net.sourceforge.pmd.lang.LanguageFilenameFilter; -import net.sourceforge.pmd.lang.LanguageVersion; -import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; -import net.sourceforge.pmd.processor.AbstractPMDProcessor; import net.sourceforge.pmd.renderers.Renderer; -import net.sourceforge.pmd.reporting.GlobalAnalysisListener; -import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener; -import net.sourceforge.pmd.util.ClasspathClassLoader; -import net.sourceforge.pmd.util.FileUtil; -import net.sourceforge.pmd.util.IOUtil; -import net.sourceforge.pmd.util.database.DBMSMetadata; -import net.sourceforge.pmd.util.database.DBURI; -import net.sourceforge.pmd.util.database.SourceObject; +import net.sourceforge.pmd.reporting.ReportStats; +import net.sourceforge.pmd.reporting.ReportStatsListener; import net.sourceforge.pmd.util.datasource.DataSource; -import net.sourceforge.pmd.util.datasource.ReaderDataSource; +import net.sourceforge.pmd.util.log.MessageReporter; +import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter; /** - * This is the main class for interacting with PMD. The primary flow of all Rule - * process is controlled via interactions with this class. A command line - * interface is supported, as well as a programmatic API for integrating PMD - * with other software such as IDEs and Ant. - * - *

Main entrypoints are: - *

    - *
  • {@link #main(String[])} which exits the java process
  • - *
  • {@link #runPmd(String...)} which returns a {@link StatusCode}
  • - *
  • {@link #runPmd(PMDConfiguration)}
  • - *
  • {@link #processFiles(PMDConfiguration, List, Collection, List)}
  • - *
- * + * Entry point for PMD's CLI. Use {@link #runPmd(PMDConfiguration)} + * or {@link #runPmd(String...)} to mimic a CLI run. This class is + * not a supported programmatic API anymore, use {@link PmdAnalysis} + * for fine control over the PMD integration and execution. + * *

Warning: This class is not intended to be instantiated or subclassed. It will * be made final in PMD7. */ @@ -81,158 +53,60 @@ public final class PMD { /** * The line delimiter used by PMD in outputs. Usually the platform specific * line separator. + * + * @deprecated Use {@link System#lineSeparator()} */ - public static final String EOL = System.getProperty("line.separator", "\n"); + @Deprecated + public static final String EOL = System.lineSeparator(); - /** The default suppress marker string. */ - public static final String SUPPRESS_MARKER = "NOPMD"; + /** + * The default suppress marker string. + * + * @deprecated Use {@link PMDConfiguration#DEFAULT_SUPPRESS_MARKER} + */ + @Deprecated + public static final String SUPPRESS_MARKER = PMDConfiguration.DEFAULT_SUPPRESS_MARKER; private PMD() { } - /** - * Parses the given string as a database uri and returns a list of - * datasources. - * - * @param uriString the URI to parse - * - * @return list of data sources - * - * @throws IOException if the URI couldn't be parsed - * @see DBURI - * - * @deprecated Will be hidden as part of the parsing of {@link PMD#getApplicableFiles(PMDConfiguration, Set)} - */ - @Deprecated - public static List getURIDataSources(String uriString) throws IOException { - List dataSources = new ArrayList<>(); + private static ReportStats runAndReturnStats(PmdAnalysis pmd) { + if (pmd.getRulesets().isEmpty()) { + return ReportStats.empty(); + } + + @SuppressWarnings("PMD.CloseResource") + ReportStatsListener listener = new ReportStatsListener(); + + pmd.addListener(listener); try { - DBURI dbUri = new DBURI(uriString); - DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri); - log.debug("DBMSMetadata retrieved"); - List sourceObjectList = dbmsMetadata.getSourceObjectList(); - log.debug("Located {} database source objects", sourceObjectList.size()); - for (SourceObject sourceObject : sourceObjectList) { - String falseFilePath = sourceObject.getPseudoFileName(); - log.trace("Adding database source object {}", falseFilePath); - - try { - dataSources.add(new ReaderDataSource(dbmsMetadata.getSourceCode(sourceObject), falseFilePath)); - } catch (SQLException ex) { - log.warn("Cannot get SourceCode for {} - skipping ...", falseFilePath, ex); - } - } - } catch (URISyntaxException e) { - throw new IOException("Cannot get DataSources from DBURI - \"" + uriString + "\"", e); - } catch (SQLException e) { - throw new IOException("Cannot get DataSources from DBURI, couldn't access the database - \"" + uriString + "\"", e); - } catch (ClassNotFoundException e) { - throw new IOException("Cannot get DataSources from DBURI, probably missing database jdbc driver - \"" + uriString + "\"", e); + pmd.performAnalysis(); } catch (Exception e) { - throw new IOException("Encountered unexpected problem with URI \"" + uriString + "\"", e); + pmd.getReporter().errorEx("Exception during processing", e); + ReportStats stats = listener.getResult(); + printErrorDetected(1 + stats.getNumErrors()); + return stats; // should have been closed } - return dataSources; - } + ReportStats stats = listener.getResult(); - /** - * This method is the main entry point for command line usage. - * - * @param configuration the configuration to use - * - * @return number of violations found. - * - * @deprecated Use {@link #runPmd(PMDConfiguration)}. - */ - @Deprecated - public static int doPMD(final PMDConfiguration configuration) { - - // Load the RuleSets - final RuleSetLoader ruleSetFactory = RuleSetLoader.fromPmdConfig(configuration); - final List ruleSets = getRuleSetsWithBenchmark(configuration.getRuleSetPaths(), ruleSetFactory); - - final Set languages = getApplicableLanguages(configuration, ruleSets); - - try { - - final List files = getApplicableFiles(configuration, languages); - Renderer renderer = configuration.createRenderer(true); - - @SuppressWarnings("PMD.CloseResource") - ViolationCounterListener violationCounter = new ViolationCounterListener(); - @SuppressWarnings("PMD.CloseResource") - GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener(); - - List allListeners = listOf( - reportBuilder, - renderer.newListener(), - violationCounter); - try (GlobalAnalysisListener listener = GlobalAnalysisListener.tee(allListeners)) { - try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) { - processFiles(configuration, ruleSets, files, listener); - } - } - - Report report = reportBuilder.getResult(); - if (!report.getProcessingErrors().isEmpty()) { - printErrorDetected(report.getProcessingErrors().size()); - } - - return violationCounter.getResult(); - } catch (final Exception e) { - log.error("Exception during processing: {}", e.toString()); // only exception without stacktrace - log.debug("Exception during processing", e); // with stacktrace - printErrorDetected(1); - return PMDCommandLineInterface.NO_ERRORS_STATUS; // fixme? - } finally { - /* - * Make sure it's our own classloader before attempting to close it.... - * Maven + Jacoco provide us with a cloaseable classloader that if closed - * will throw a ClassNotFoundException. - */ - if (configuration.getClassLoader() instanceof ClasspathClassLoader) { - IOUtil.tryCloseClassLoader(configuration.getClassLoader()); - } + if (stats.getNumErrors() > 0) { + printErrorDetected(stats.getNumErrors()); } + + return stats; } - private static List getRuleSetsWithBenchmark(List rulesetPaths, RuleSetLoader factory) { - try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.LOAD_RULES)) { - List ruleSets; - try { - ruleSets = factory.loadFromResources(rulesetPaths); - printRuleNamesInDebug(ruleSets); - if (isEmpty(ruleSets)) { - String msg = "No rules found. Maybe you misspelled a rule name? (" - + String.join(",", rulesetPaths) + ')'; - log.error(msg); - throw new IllegalArgumentException(msg); - } - } catch (RuleSetLoadException rsnfe) { - log.error("Ruleset not found", rsnfe); - throw rsnfe; - } - return ruleSets; - } - } - private static boolean isEmpty(List rsets) { - return rsets.stream().noneMatch(it -> it.size() > 0); - } - - /** - * If in debug modus, print the names of the rules. - * - * @param rulesets the RuleSets to print - */ - private static void printRuleNamesInDebug(List rulesets) { - if (log.isDebugEnabled()) { - for (RuleSet rset : rulesets) { - for (Rule r : rset.getRules()) { - log.debug("Loaded rule {}", r.getName()); - } - } + static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) { + if (!configuration.isIgnoreIncrementalAnalysis() + && configuration.getAnalysisCache() instanceof NoopAnalysisCache + && log.isWarnEnabled()) { + final String version = + PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-" + PMDVersion.VERSION; + log.warn("This analysis could be faster, please consider using Incremental Analysis: " + + "https://pmd.github.io/{}/pmd_userdocs_incremental_analysis.html", version); } } @@ -249,227 +123,25 @@ public final class PMD { * @return Report in which violations are accumulated * * @throws Exception If there was a problem when opening or closing the renderers + * + * @deprecated Use {@link PmdAnalysis} */ - @SuppressWarnings("PMD.CloseResource") + @Deprecated public static Report processFiles(PMDConfiguration configuration, List ruleSets, Collection files, List renderers) throws Exception { - - GlobalAnalysisListener rendererListeners = createComposedRendererListener(renderers); - GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener(); - - List allListeners = listOf(reportBuilder, rendererListeners); - - try (GlobalAnalysisListener listener = GlobalAnalysisListener.tee(allListeners)) { - processFiles(configuration, ruleSets, new ArrayList<>(files), listener); + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + pmd.addRuleSets(ruleSets); + pmd.addRenderers(renderers); + @SuppressWarnings("PMD.CloseResource") + GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener(); + List sortedFiles = new ArrayList<>(files); + sortedFiles.sort(Comparator.comparing(ds -> ds.getNiceFileName(false, ""))); + pmd.performAnalysisImpl(listOf(reportBuilder), sortedFiles); + return reportBuilder.getResult(); } - - return reportBuilder.getResult(); - } - - private static GlobalAnalysisListener createComposedRendererListener(List renderers) throws Exception { - if (renderers.isEmpty()) { - return GlobalAnalysisListener.noop(); - } - - List rendererListeners = new ArrayList<>(renderers.size()); - for (Renderer renderer : renderers) { - try { - rendererListeners.add(renderer.newListener()); - } catch (IOException ioe) { - // close listeners so far, throw their close exception or the ioe - IOUtil.ensureClosed(rendererListeners, ioe); - throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown"); - } - } - return GlobalAnalysisListener.tee(rendererListeners); - } - - - /** - * Run PMD using the given configuration. This replaces the other overload. - * - * @param configuration Configuration for the run. Note that the files, - * and rulesets, are ignored, as they are supplied - * as parameters - * @param ruleSets Parsed rulesets - * @param files Files to process, will be closed by this method. - * @param listener Listener to which analysis events are forwarded. - * The listener is NOT closed by this routine and should - * be closed by the caller. - * - * @throws Exception If there was a problem when opening or closing the renderers - */ - public static void processFiles(PMDConfiguration configuration, - List ruleSets, - List files, - GlobalAnalysisListener listener) throws Exception { - - final RuleSets rs = new RuleSets(ruleSets); - - // todo Just like we throw for invalid properties, "broken rules" - // shouldn't be a "config error". This is the only instance of - // config errors... - - for (final Rule rule : removeBrokenRules(rs)) { - listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason())); - } - - encourageToUseIncrementalAnalysis(configuration); - - List sortedFiles = sortFiles(configuration, files); - - // Make sure the cache is listening for analysis results - listener = GlobalAnalysisListener.tee(listOf(listener, configuration.getAnalysisCache())); - - configuration.getAnalysisCache().checkValidity(rs, configuration.getClassLoader()); - - Exception ex = null; - try (AbstractPMDProcessor processor = AbstractPMDProcessor.newFileProcessor(configuration)) { - processor.processFiles(rs, sortedFiles, listener); - } catch (Exception e) { - ex = e; - } finally { - configuration.getAnalysisCache().persist(); - IOUtil.ensureClosed(sortedFiles, ex); - } - } - - - /** - * Remove and return the misconfigured rules from the rulesets and log them - * for good measure. - * - * @param ruleSets RuleSets to prune of broken rules. - * - * @return Set - */ - private static Set removeBrokenRules(final RuleSets ruleSets) { - final Set brokenRules = new HashSet<>(); - ruleSets.removeDysfunctionalRules(brokenRules); - - for (final Rule rule : brokenRules) { - log.warn("Removed misconfigured rule: {} cause: {}", rule.getName(), rule.dysfunctionReason()); - } - - return brokenRules; - } - - - private static List sortFiles(final PMDConfiguration configuration, Collection files) { - // the input collection may be unmodifiable - List result = new ArrayList<>(files); - - if (configuration.isStressTest()) { - // randomize processing order - Collections.shuffle(result); - } else { - final boolean useShortNames = configuration.isReportShortNames(); - final String inputPaths = configuration.getInputPaths(); - result.sort((left, right) -> { - String leftString = left.getNiceFileName(useShortNames, inputPaths); - String rightString = right.getNiceFileName(useShortNames, inputPaths); - return leftString.compareTo(rightString); - }); - } - - return result; - } - - private static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) { - if (!configuration.isIgnoreIncrementalAnalysis() - && configuration.getAnalysisCache() instanceof NoopAnalysisCache - && log.isWarnEnabled()) { - final String version = - PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-" + PMDVersion.VERSION; - log.warn("This analysis could be faster, please consider using Incremental Analysis: " - + "https://pmd.github.io/{}/pmd_userdocs_incremental_analysis.html", version); - } - } - - /** - * Determines all the files, that should be analyzed by PMD. - * - * @param configuration - * contains either the file path or the DB URI, from where to - * load the files - * @param languages - * used to filter by file extension - * @return List of {@link DataSource} of files - */ - public static List getApplicableFiles(PMDConfiguration configuration, Set languages) throws IOException { - try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.COLLECT_FILES)) { - return internalGetApplicableFiles(configuration, languages); - } - } - - private static List internalGetApplicableFiles(PMDConfiguration configuration, - Set languages) throws IOException { - FilenameFilter fileSelector = configuration.isForceLanguageVersion() ? new AcceptAllFilenames() : new LanguageFilenameFilter(languages); - List files = new ArrayList<>(); - - if (null != configuration.getInputPaths()) { - files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector)); - } - - if (null != configuration.getInputUri()) { - String uriString = configuration.getInputUri(); - files.addAll(getURIDataSources(uriString)); - } - - if (null != configuration.getInputFilePath()) { - String inputFilePath = configuration.getInputFilePath(); - File file = new File(inputFilePath); - if (!file.exists()) { - throw new FileNotFoundException(inputFilePath); - } - - try { - String filePaths = FileUtil.readFilelist(file); - files.addAll(FileUtil.collectFiles(filePaths, fileSelector)); - } catch (IOException ex) { - throw new IOException("Problem with Input File Path: " + inputFilePath, ex); - } - - } - - if (null != configuration.getIgnoreFilePath()) { - String ignoreFilePath = configuration.getIgnoreFilePath(); - File file = new File(ignoreFilePath); - if (!file.exists()) { - throw new FileNotFoundException(ignoreFilePath); - } - - try { - String filePaths = FileUtil.readFilelist(file); - files.removeAll(FileUtil.collectFiles(filePaths, fileSelector)); - } catch (IOException ex) { - log.error("Problem with Ignore File", ex); - throw new RuntimeException("Problem with Ignore File Path: " + ignoreFilePath, ex); - } - } - return files; - } - - private static Set getApplicableLanguages(final PMDConfiguration configuration, final List ruleSets) { - final Set languages = new HashSet<>(); - final LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer(); - - for (final RuleSet ruleSet : ruleSets) { - for (Rule rule : ruleSet.getRules()) { - final Language ruleLanguage = rule.getLanguage(); - if (!languages.contains(ruleLanguage)) { - final LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage); - if (RuleSet.applies(rule, version)) { - languages.add(ruleLanguage); - log.debug("Using {} version: {}", ruleLanguage.getShortName(), version.getShortName()); - } - } - } - } - return languages; } /** @@ -483,23 +155,6 @@ public final class PMD { PMDCommandLineInterface.setStatusCodeOrExit(exitCode.toInt()); } - /** - * Parses the command line arguments and executes PMD. Returns the - * exit code without exiting the VM. - * - * @param args command line arguments - * - * @return the exit code, where 0 means successful execution, - * 1 means error, 4 means there have been - * violations found. - * - * @deprecated Use {@link #runPmd(String...)}. - */ - @Deprecated - public static int run(final String[] args) { - return runPmd(args).toInt(); - } - /** * Parses the command line arguments and executes PMD. Returns the * status code without exiting the VM. Note that the arguments parsing @@ -515,6 +170,7 @@ public final class PMD { public static StatusCode runPmd(String... args) { PmdParametersParseResult parseResult = PmdParametersParseResult.extractParameters(args); + // todo these warnings/errors should be output on a PmdRenderer if (!parseResult.getDeprecatedOptionsUsed().isEmpty()) { Entry first = parseResult.getDeprecatedOptionsUsed().entrySet().iterator().next(); log.warn("Some deprecated options were used on the command-line, including {}", first.getKey()); @@ -560,44 +216,68 @@ public final class PMD { // only reconfigure logging, if debug flag was used on command line // otherwise just use whatever is in conf/simplelogger.properties which happens automatically if (configuration.isDebug()) { - Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(Level.DEBUG); + Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(Level.TRACE); // need to reload the logger with the new configuration log = LoggerFactory.getLogger(PMD.class); } + // create a top-level reporter + // TODO CLI errors should also be reported through this + // TODO this should not use the logger as backend, otherwise without + // slf4j implementation binding, errors are entirely ignored. + MessageReporter pmdReporter = new SimpleMessageReporter(log); // always install java.util.logging to slf4j bridge Slf4jSimpleConfiguration.installJulBridge(); // logging, mostly for testing purposes Level defaultLogLevel = Slf4jSimpleConfiguration.getDefaultLogLevel(); - log.atLevel(defaultLogLevel).log("Log level is at {}", defaultLogLevel); + log.info("Log level is at {}", defaultLogLevel); - StatusCode status; try { - int violations = PMD.doPMD(configuration); - if (violations > 0 && configuration.isFailOnViolation()) { - status = StatusCode.VIOLATIONS_FOUND; - } else { - status = StatusCode.OK; + PmdAnalysis pmd; + try { + pmd = PmdAnalysis.create(configuration, pmdReporter); + } catch (Exception e) { + pmdReporter.errorEx("Could not initialize analysis", e); + return StatusCode.ERROR; } - } catch (Exception e) { - System.err.println(e.getMessage()); - status = StatusCode.ERROR; - } finally { - if (configuration.isBenchmark()) { - final TimingReport timingReport = TimeTracker.stopGlobalTracking(); - - // TODO get specified report format from config - final TimingReportRenderer renderer = new TextTimingReportRenderer(); - try { - // Don't close this writer, we don't want to close stderr - @SuppressWarnings("PMD.CloseResource") - final Writer writer = new OutputStreamWriter(System.err); - renderer.render(timingReport, writer); - } catch (final IOException e) { - System.err.println(e.getMessage()); + try { + ReportStats stats; + stats = PMD.runAndReturnStats(pmd); + if (pmdReporter.numErrors() > 0) { + // processing errors are ignored + return StatusCode.ERROR; + } else if (stats.getNumViolations() > 0 && configuration.isFailOnViolation()) { + return StatusCode.VIOLATIONS_FOUND; + } else { + return StatusCode.OK; } + } finally { + pmd.close(); + } + + } catch (Exception e) { + pmdReporter.errorEx("Exception while running PMD.", e); + printErrorDetected(1); + return StatusCode.ERROR; + } finally { + finishBenchmarker(configuration); + } + } + + private static void finishBenchmarker(PMDConfiguration configuration) { + if (configuration.isBenchmark()) { + final TimingReport timingReport = TimeTracker.stopGlobalTracking(); + + // TODO get specified report format from config + final TimingReportRenderer renderer = new TextTimingReportRenderer(); + try { + // Don't close this writer, we don't want to close stderr + @SuppressWarnings("PMD.CloseResource") + final Writer writer = new OutputStreamWriter(System.err); + renderer.render(timingReport, writer); + } catch (final IOException e) { + System.err.println(e.getMessage()); } } - return status; } /** @@ -633,10 +313,4 @@ public final class PMD { } - private static final class AcceptAllFilenames implements FilenameFilter { - @Override - public boolean accept(File dir, String name) { - return true; - } - } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java index 1b9931f537..bc6e618aa2 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java @@ -13,12 +13,14 @@ import java.util.Objects; import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import net.sourceforge.pmd.annotation.DeprecatedUntil700; import net.sourceforge.pmd.cache.AnalysisCache; import net.sourceforge.pmd.cache.FileAnalysisCache; import net.sourceforge.pmd.cache.NoopAnalysisCache; import net.sourceforge.pmd.cli.PmdParametersParseResult; +import net.sourceforge.pmd.internal.util.AssertionUtil; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; @@ -44,7 +46,7 @@ import net.sourceforge.pmd.util.ClasspathClassLoader; * {@link #getClassLoader()} *

  • A means to configure a ClassLoader using a prepended classpath String, * instead of directly setting it programmatically. - * {@link #prependClasspath(String)}
  • + * {@link #prependAuxClasspath(String)} *
  • A LanguageVersionDiscoverer instance, which defaults to using the default * LanguageVersion of each Language. Means are provided to change the * LanguageVersion for each Language. @@ -88,15 +90,19 @@ import net.sourceforge.pmd.util.ClasspathClassLoader; * */ public class PMDConfiguration extends AbstractConfiguration { + + /** The default suppress marker string. */ + public static final String DEFAULT_SUPPRESS_MARKER = "NOPMD"; + // General behavior options - private String suppressMarker = PMD.SUPPRESS_MARKER; + private String suppressMarker = DEFAULT_SUPPRESS_MARKER; private int threads = Runtime.getRuntime().availableProcessors(); private ClassLoader classLoader = getClass().getClassLoader(); private LanguageVersionDiscoverer languageVersionDiscoverer = new LanguageVersionDiscoverer(); private LanguageVersion forceLanguageVersion; // Rule and source file options - private List ruleSets; + private List ruleSets = new ArrayList<>(); private RulePriority minimumPriority = RulePriority.LOW; private String inputPaths; private String inputUri; @@ -197,13 +203,46 @@ public class PMDConfiguration extends AbstractConfiguration { * if the given classpath is invalid (e.g. does not exist) * @see PMDConfiguration#setClassLoader(ClassLoader) * @see ClasspathClassLoader + * + * @deprecated Use {@link #prependAuxClasspath(String)}, which doesn't + * throw a checked {@link IOException} */ + @Deprecated public void prependClasspath(String classpath) throws IOException { - if (classLoader == null) { - classLoader = PMDConfiguration.class.getClassLoader(); + try { + prependAuxClasspath(classpath); + } catch (IllegalArgumentException e) { + throw new IOException(e); } - if (classpath != null) { - classLoader = new ClasspathClassLoader(classpath, classLoader); + } + + /** + * Prepend the specified classpath like string to the current ClassLoader of + * the configuration. If no ClassLoader is currently configured, the + * ClassLoader used to load the {@link PMDConfiguration} class will be used + * as the parent ClassLoader of the created ClassLoader. + * + *

    If the classpath String looks like a URL to a file (i.e. starts with + * file://) the file will be read with each line representing + * an entry on the classpath.

    + * + * @param classpath The prepended classpath. + * + * @throws IllegalArgumentException if the given classpath is invalid (e.g. does not exist) + * @see PMDConfiguration#setClassLoader(ClassLoader) + */ + public void prependAuxClasspath(String classpath) { + try { + if (classLoader == null) { + classLoader = PMDConfiguration.class.getClassLoader(); + } + if (classpath != null) { + classLoader = new ClasspathClassLoader(classpath, classLoader); + } + } catch (IOException e) { + // Note: IOExceptions shouldn't appear anymore, they should already be converted + // to IllegalArgumentException in ClasspathClassLoader. + throw new IllegalArgumentException(e); } } @@ -244,6 +283,7 @@ public class PMDConfiguration extends AbstractConfiguration { */ public void setForceLanguageVersion(LanguageVersion forceLanguageVersion) { this.forceLanguageVersion = forceLanguageVersion; + languageVersionDiscoverer.setForcedVersion(forceLanguageVersion); } /** @@ -253,6 +293,7 @@ public class PMDConfiguration extends AbstractConfiguration { * the LanguageVersion */ public void setDefaultLanguageVersion(LanguageVersion languageVersion) { + Objects.requireNonNull(languageVersion); setDefaultLanguageVersions(Arrays.asList(languageVersion)); } @@ -265,6 +306,7 @@ public class PMDConfiguration extends AbstractConfiguration { */ public void setDefaultLanguageVersions(List languageVersions) { for (LanguageVersion languageVersion : languageVersions) { + Objects.requireNonNull(languageVersion); languageVersionDiscoverer.setDefaultLanguageVersion(languageVersion); } } @@ -310,7 +352,10 @@ public class PMDConfiguration extends AbstractConfiguration { */ @Deprecated @DeprecatedUntil700 - public String getRuleSets() { + public @Nullable String getRuleSets() { + if (ruleSets.isEmpty()) { + return null; + } return String.join(",", ruleSets); } @@ -319,17 +364,34 @@ public class PMDConfiguration extends AbstractConfiguration { * * @see RuleSetLoader#loadFromResource(String) */ - public List getRuleSetPaths() { + public @NonNull List<@NonNull String> getRuleSetPaths() { return ruleSets; } /** - * Sets the rulesets. + * Sets the list of ruleset paths to load when starting the analysis. + * + * @param ruleSetPaths A list of ruleset paths, understandable by {@link RuleSetLoader#loadFromResource(String)}. * * @throws NullPointerException If the parameter is null */ - public void setRuleSets(@NonNull List ruleSets) { - this.ruleSets = new ArrayList<>(ruleSets); + public void setRuleSets(@NonNull List<@NonNull String> ruleSetPaths) { + AssertionUtil.requireParamNotNull("ruleSetPaths", ruleSetPaths); + AssertionUtil.requireContainsNoNullValue("ruleSetPaths", ruleSetPaths); + this.ruleSets = new ArrayList<>(ruleSetPaths); + } + + /** + * Add a new ruleset paths to load when starting the analysis. + * This list is initially empty. + * + * @param rulesetPath A ruleset path, understandable by {@link RuleSetLoader#loadFromResource(String)}. + * + * @throws NullPointerException If the parameter is null + */ + public void addRuleSet(@NonNull String rulesetPath) { + AssertionUtil.requireParamNotNull("rulesetPath", rulesetPath); + this.ruleSets.add(rulesetPath); } /** @@ -337,12 +399,16 @@ public class PMDConfiguration extends AbstractConfiguration { * * @param ruleSets the rulesets to set * - * @deprecated Use {@link #setRuleSets(List)} + * @deprecated Use {@link #setRuleSets(List)} or {@link #addRuleSet(String)}. */ @Deprecated @DeprecatedUntil700 - public void setRuleSets(String ruleSets) { - this.ruleSets = Arrays.asList(ruleSets.split(",")); + public void setRuleSets(@Nullable String ruleSets) { + if (ruleSets == null) { + this.ruleSets = new ArrayList<>(); + } else { + this.ruleSets = new ArrayList<>(Arrays.asList(ruleSets.split(","))); + } } /** diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java b/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java new file mode 100644 index 0000000000..1b74fa5ab8 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java @@ -0,0 +1,403 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd; + +import static net.sourceforge.pmd.util.CollectionUtil.listOf; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sourceforge.pmd.Report.GlobalReportBuilderListener; +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.benchmark.TimeTracker; +import net.sourceforge.pmd.benchmark.TimedOperation; +import net.sourceforge.pmd.benchmark.TimedOperationCategory; +import net.sourceforge.pmd.cache.AnalysisCacheListener; +import net.sourceforge.pmd.internal.util.AssertionUtil; +import net.sourceforge.pmd.internal.util.FileCollectionUtil; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; +import net.sourceforge.pmd.lang.document.FileCollector; +import net.sourceforge.pmd.processor.AbstractPMDProcessor; +import net.sourceforge.pmd.renderers.Renderer; +import net.sourceforge.pmd.reporting.GlobalAnalysisListener; +import net.sourceforge.pmd.util.ClasspathClassLoader; +import net.sourceforge.pmd.util.IOUtil; +import net.sourceforge.pmd.util.datasource.DataSource; +import net.sourceforge.pmd.util.log.MessageReporter; +import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter; + +/** + * Main programmatic API of PMD. Create and configure a {@link PMDConfiguration}, + * then use {@link #create(PMDConfiguration)} to obtain an instance. + * You can perform additional configuration on the instance, eg adding + * files to process, or additional rulesets and renderers. Then, call + * {@link #performAnalysis()}. Example: + *
    {@code
    + *   PMDConfiguration config = new PMDConfiguration();
    + *   config.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11"));
    + *   config.setInputPaths("src/main/java");
    + *   config.prependClasspath("target/classes");
    + *   config.setMinimumPriority(RulePriority.HIGH);
    + *   config.addRuleSet("rulesets/java/quickstart.xml");
    + *   config.setReportFormat("xml");
    + *   config.setReportFile("target/pmd-report.xml");
    + *
    + *   try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
    + *     // note: don't use `config` once a PmdAnalysis has been created.
    + *     // optional: add more rulesets
    + *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
    + *     // optional: add more files
    + *     pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java"));
    + *     // optional: add more renderers
    + *     pmd.addRenderer(renderer);
    + *
    + *     pmd.performAnalysis();
    + *   }
    + * }
    + * + */ +public final class PmdAnalysis implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(PmdAnalysis.class); + + private final FileCollector collector; + private final List renderers = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private final List ruleSets = new ArrayList<>(); + private final PMDConfiguration configuration; + private final MessageReporter reporter; + + private boolean closed; + + /** + * Constructs a new instance. The files paths (input files, filelist, + * exclude list, etc) given in the configuration are collected into + * the file collector ({@link #files()}), but more can be added + * programmatically using the file collector. + */ + private PmdAnalysis(PMDConfiguration config, MessageReporter reporter) { + this.configuration = config; + this.reporter = reporter; + this.collector = FileCollector.newCollector( + config.getLanguageVersionDiscoverer(), + reporter + ); + } + + /** + * Constructs a new instance from a configuration. + * + *
      + *
    • The files paths (input files, filelist, + * exclude list, etc) are explored and the files to analyse are + * collected into the file collector ({@link #files()}). + * More can be added programmatically using the file collector. + *
    • The rulesets given in the configuration are loaded ({@link PMDConfiguration#getRuleSets()}) + *
    • A renderer corresponding to the parameters of the configuration + * is created and added (but not started). + *
    + */ + public static PmdAnalysis create(PMDConfiguration config) { + return create( + config, + new SimpleMessageReporter(LoggerFactory.getLogger(PmdAnalysis.class)) + ); + } + + @InternalApi + static PmdAnalysis create(PMDConfiguration config, MessageReporter reporter) { + PmdAnalysis pmd = new PmdAnalysis(config, reporter); + + // note: do not filter files by language + // they could be ignored later. The problem is if you call + // addRuleSet later, then you could be enabling new languages + // So the files should not be pruned in advance + FileCollectionUtil.collectFiles(config, pmd.files()); + + if (config.getReportFormat() != null) { + Renderer renderer = config.createRenderer(true); + pmd.addRenderer(renderer); + } + + if (!config.getRuleSetPaths().isEmpty()) { + final RuleSetLoader ruleSetLoader = pmd.newRuleSetLoader(); + final List ruleSets = ruleSetLoader.loadRuleSetsWithoutException(config.getRuleSetPaths()); + pmd.addRuleSets(ruleSets); + } + return pmd; + } + + // test only + List rulesets() { + return ruleSets; + } + + // test only + List renderers() { + return renderers; + } + + + /** + * Returns the file collector for the analysed sources. + */ + public FileCollector files() { + return collector; // todo user can close collector programmatically + } + + /** + * Returns a new ruleset loader, which can be used to create new + * rulesets (add them then with {@link #addRuleSet(RuleSet)}). + * + *
    {@code
    +     * try (PmdAnalysis pmd = create(config)) {
    +     *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
    +     * }
    +     * }
    + */ + public RuleSetLoader newRuleSetLoader() { + RuleSetLoader loader = RuleSetLoader.fromPmdConfig(configuration); + loader.setReporter(this.reporter); + return loader; + } + + /** + * Add a new renderer. The given renderer must not already be started, + * it will be started by {@link #performAnalysis()}. + * + * @throws NullPointerException If the parameter is null + */ + public void addRenderer(Renderer renderer) { + AssertionUtil.requireParamNotNull("renderer", renderer); + this.renderers.add(renderer); + } + + /** + * Add several renderers at once. + * + * @throws NullPointerException If the parameter is null, or any of its items is null. + */ + public void addRenderers(Collection renderers) { + renderers.forEach(this::addRenderer); + } + + /** + * Add a new listener. As per the contract of {@link GlobalAnalysisListener}, + * this object must be ready for interaction. However, nothing will + * be done with the listener until {@link #performAnalysis()} is called. + * The listener will be closed by {@link #performAnalysis()}, or + * {@link #close()}, whichever happens first. + * + * @throws NullPointerException If the parameter is null + */ + public void addListener(GlobalAnalysisListener listener) { + AssertionUtil.requireParamNotNull("listener", listener); + this.listeners.add(listener); + } + + /** + * Add several listeners at once. + * + * @throws NullPointerException If the parameter is null, or any of its items is null. + * @see #addListener(GlobalAnalysisListener) + */ + public void addListeners(Collection listeners) { + listeners.forEach(this::addListener); + } + + /** + * Add a new ruleset. + * + * @throws NullPointerException If the parameter is null + */ + public void addRuleSet(RuleSet ruleSet) { + AssertionUtil.requireParamNotNull("rule set", ruleSet); + this.ruleSets.add(ruleSet); + } + + /** + * Add several rulesets at once. + * + * @throws NullPointerException If the parameter is null, or any of its items is null. + */ + public void addRuleSets(Collection ruleSets) { + ruleSets.forEach(this::addRuleSet); + } + + /** + * Returns an unmodifiable view of the ruleset list. That will be + * processed. + */ + public List getRulesets() { + return Collections.unmodifiableList(ruleSets); + } + + + /** + * Run PMD with the current state of this instance. This will start + * and finish the registered renderers, and close all + * {@linkplain #addListener(GlobalAnalysisListener) registered listeners}. + * All files collected in the {@linkplain #files() file collector} are + * processed. This does not return a report, as the analysis results + * are consumed by {@link GlobalAnalysisListener} instances (of which + * Renderers are a special case). Note that this does + * not throw, errors are instead accumulated into a {@link MessageReporter}. + */ + public void performAnalysis() { + performAnalysisImpl(Collections.emptyList()); + } + + /** + * Run PMD with the current state of this instance. This will start + * and finish the registered renderers. All files collected in the + * {@linkplain #files() file collector} are processed. Returns the + * output report. Note that this does not throw, errors are instead + * accumulated into a {@link MessageReporter}. + */ + public Report performAnalysisAndCollectReport() { + try (GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener()) { + performAnalysisImpl(listOf(reportBuilder)); // closes the report builder + return reportBuilder.getResultImpl(); + } + } + + void performAnalysisImpl(List extraListeners) { + try (FileCollector files = collector) { + files.filterLanguages(getApplicableLanguages()); + List dataSources = FileCollectionUtil.collectorToDataSource(files); + performAnalysisImpl(extraListeners, dataSources); + } + } + + void performAnalysisImpl(List extraListeners, List dataSources) { + RuleSets rulesets = new RuleSets(this.ruleSets); + + GlobalAnalysisListener listener; + try { + @SuppressWarnings("PMD.CloseResource") AnalysisCacheListener cacheListener = new AnalysisCacheListener(configuration.getAnalysisCache(), rulesets, configuration.getClassLoader()); + listener = GlobalAnalysisListener.tee(listOf(createComposedRendererListener(renderers), GlobalAnalysisListener.tee(listeners), GlobalAnalysisListener.tee(extraListeners), cacheListener)); + } catch (Exception e) { + reporter.errorEx("Exception while initializing analysis listeners", e); + throw new RuntimeException("Exception while initializing analysis listeners", e); + } + + try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) { + + for (final Rule rule : removeBrokenRules(rulesets)) { + // todo Just like we throw for invalid properties, "broken rules" + // shouldn't be a "config error". This is the only instance of + // config errors... + listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason())); + } + + PMD.encourageToUseIncrementalAnalysis(configuration); + try (AbstractPMDProcessor processor = AbstractPMDProcessor.newFileProcessor(configuration)) { + processor.processFiles(rulesets, dataSources, listener); + } + } finally { + try { + listener.close(); + } catch (Exception e) { + reporter.errorEx("Exception while initializing analysis listeners", e); + // todo better exception + throw new RuntimeException("Exception while initializing analysis listeners", e); + } + } + } + + + private static GlobalAnalysisListener createComposedRendererListener(List renderers) throws Exception { + if (renderers.isEmpty()) { + return GlobalAnalysisListener.noop(); + } + + List rendererListeners = new ArrayList<>(renderers.size()); + for (Renderer renderer : renderers) { + try { + @SuppressWarnings("PMD.CloseResource") + GlobalAnalysisListener listener = + Objects.requireNonNull(renderer.newListener(), "Renderer should provide non-null listener"); + rendererListeners.add(listener); + } catch (Exception ioe) { + // close listeners so far, throw their close exception or the ioe + IOUtil.ensureClosed(rendererListeners, ioe); + throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown"); + } + } + return GlobalAnalysisListener.tee(rendererListeners); + } + + private Set getApplicableLanguages() { + final Set languages = new HashSet<>(); + final LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer(); + + for (RuleSet ruleSet : ruleSets) { + for (final Rule rule : ruleSet.getRules()) { + final Language ruleLanguage = rule.getLanguage(); + if (!languages.contains(ruleLanguage)) { + final LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage); + if (RuleSet.applies(rule, version)) { + languages.add(ruleLanguage); + LOG.trace("Using {} version ''{}''", version.getLanguage().getName(), version.getTerseName()); + } + } + } + } + return languages; + } + + /** + * Remove and return the misconfigured rules from the rulesets and log them + * for good measure. + */ + private Set removeBrokenRules(final RuleSets ruleSets) { + final Set brokenRules = new HashSet<>(); + ruleSets.removeDysfunctionalRules(brokenRules); + + for (final Rule rule : brokenRules) { + reporter.warn("Removed misconfigured rule: {} cause: {}", + rule.getName(), rule.dysfunctionReason()); + } + + return brokenRules; + } + + + public MessageReporter getReporter() { + return reporter; + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + collector.close(); + + // close listeners if analysis is not run. + IOUtil.closeAll(listeners); + + /* + * Make sure it's our own classloader before attempting to close it.... + * Maven + Jacoco provide us with a cloaseable classloader that if closed + * will throw a ClassNotFoundException. + */ + if (configuration.getClassLoader() instanceof ClasspathClassLoader) { + IOUtil.tryCloseClassLoader(configuration.getClassLoader()); + } + } + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java index 9513f5901e..78f4861cc4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java @@ -28,7 +28,7 @@ import net.sourceforge.pmd.util.datasource.DataSource; * and configuration errors. * *

    A report may be created by a {@link GlobalReportBuilderListener} that you - * use as the {@link GlobalAnalysisListener} in {@linkplain PMD#processFiles(PMDConfiguration, List, List, GlobalAnalysisListener) PMD's entry point}. + * use as the {@linkplain GlobalAnalysisListener} in {@link PmdAnalysis#performAnalysisAndCollectReport() PMD's entry point}. * You can also create one manually with {@link #buildReport(Consumer)}. */ public final class Report { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java index e177ee4eb6..1a75e5d3a5 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java @@ -16,11 +16,16 @@ import java.util.Properties; import org.apache.commons.lang3.StringUtils; import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.util.CollectionUtil; import net.sourceforge.pmd.util.ResourceLoader; +import net.sourceforge.pmd.util.log.MessageReporter; +import net.sourceforge.pmd.util.log.internal.NoopReporter; /** * Configurable object to load rulesets from XML resources. @@ -29,12 +34,26 @@ import net.sourceforge.pmd.util.ResourceLoader; * or some such overload. */ public final class RuleSetLoader { + private static final Logger LOG = LoggerFactory.getLogger(RuleSetLoader.class); private ResourceLoader resourceLoader = new ResourceLoader(RuleSetLoader.class.getClassLoader()); private RulePriority minimumPriority = RulePriority.LOW; private boolean warnDeprecated = true; private @NonNull RuleSetFactoryCompatibility compatFilter = RuleSetFactoryCompatibility.DEFAULT; private boolean includeDeprecatedRuleReferences = false; + private MessageReporter reporter = new NoopReporter(); // non-null + + /** + * Create a new RuleSetLoader with a default configuration. + * The defaults are described on each configuration method of this class. + */ + public RuleSetLoader() { // NOPMD UnnecessaryConstructor + // default + } + + void setReporter(MessageReporter reporter) { + this.reporter = reporter; + } /** * Specify that the given classloader should be used to resolve @@ -145,7 +164,7 @@ public final class RuleSetLoader { * * @throws RuleSetLoadException If any error occurs (eg, invalid syntax) */ - public RuleSet loadFromString(String filename, String rulesetXmlContent) { + public RuleSet loadFromString(String filename, final String rulesetXmlContent) { return loadFromResource(new RuleSetReferenceId(filename) { @Override public InputStream getInputStream(ResourceLoader rl) { @@ -171,6 +190,52 @@ public final class RuleSetLoader { return ruleSets; } + /** + * Loads a list of rulesets, if any has an error, report it on the contextual + * error reporter instead of aborting, and continue loading the rest. + * + *

    Internal API: might be published later, or maybe in PMD 7 this + * will be the default behaviour of every method of this class. + */ + @InternalApi + public List loadRuleSetsWithoutException(List rulesetPaths) { + List ruleSets = new ArrayList<>(rulesetPaths.size()); + boolean anyRules = false; + for (String path : rulesetPaths) { + try { + RuleSet ruleset = this.loadFromResource(path); + anyRules |= !ruleset.getRules().isEmpty(); + printRulesInDebug(path, ruleset); + ruleSets.add(ruleset); + } catch (RuleSetLoadException e) { + if (e.getCause() != null) { + // eg RuleSetNotFoundException + reporter.errorEx("Cannot load ruleset {0}", new Object[] { path }, e.getCause()); + } else { + reporter.errorEx("Cannot load ruleset {0}", new Object[] { path }, e); + } + } + } + if (!anyRules) { + reporter.warn("No rules found. Maybe you misspelled a rule name? ({})", + StringUtils.join(rulesetPaths, ',')); + } + return ruleSets; + } + + void printRulesInDebug(String path, RuleSet ruleset) { + if (LOG.isDebugEnabled()) { + LOG.debug("Rules loaded from {}:", path); + for (Rule rule : ruleset.getRules()) { + LOG.debug("- {} ({})", rule.getName(), rule.getLanguage().getName()); + } + } + if (ruleset.getRules().isEmpty()) { + reporter.warn("No rules found in ruleset {}", path); + } + + } + /** * Parses several resources into a list of rulesets. * diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java index 0f3f8f3114..4117010f49 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java @@ -4,10 +4,9 @@ package net.sourceforge.pmd.ant.internal; -import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.StringJoiner; @@ -22,12 +21,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; -import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; -import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.PmdAnalysis; import net.sourceforge.pmd.RulePriority; -import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetLoadException; import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.ant.Formatter; import net.sourceforge.pmd.ant.PMDTask; @@ -38,11 +34,11 @@ import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.reporting.FileAnalysisListener; import net.sourceforge.pmd.reporting.GlobalAnalysisListener; -import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener; +import net.sourceforge.pmd.reporting.ReportStats; +import net.sourceforge.pmd.reporting.ReportStatsListener; import net.sourceforge.pmd.util.ClasspathClassLoader; import net.sourceforge.pmd.util.IOUtil; import net.sourceforge.pmd.util.datasource.DataSource; -import net.sourceforge.pmd.util.datasource.FileDataSource; public class PMDTaskImpl { @@ -51,7 +47,7 @@ public class PMDTaskImpl { private final List formatters = new ArrayList<>(); private final List filesets = new ArrayList<>(); private final PMDConfiguration configuration = new PMDConfiguration(); - private final String rulesetPaths; + private boolean failOnError; private boolean failOnRuleViolation; private int maxRuleViolations = 0; private String failuresPropertyName; @@ -62,12 +58,16 @@ public class PMDTaskImpl { if (task.getSuppressMarker() != null) { configuration.setSuppressMarker(task.getSuppressMarker()); } + this.failOnError = task.isFailOnError(); this.failOnRuleViolation = task.isFailOnRuleViolation(); this.maxRuleViolations = task.getMaxRuleViolations(); if (this.maxRuleViolations > 0) { this.failOnRuleViolation = true; } - this.rulesetPaths = task.getRulesetFiles() == null ? "" : task.getRulesetFiles(); + if (task.getRulesetFiles() != null) { + configuration.setRuleSets(Arrays.asList(task.getRulesetFiles().split(","))); + } + configuration.setRuleSetFactoryCompatibilityEnabled(!task.isNoRuleSetCompatibility()); if (task.getEncoding() != null) { configuration.setSourceEncoding(task.getEncoding()); @@ -100,46 +100,49 @@ public class PMDTaskImpl { private void doTask() { setupClassLoader(); - // Setup RuleSetFactory and validate RuleSets - RuleSetLoader rulesetLoader = RuleSetLoader.fromPmdConfig(configuration) - .loadResourcesWith(setupResourceLoader()); - - List rules = loadRulesets(rulesetLoader); - if (configuration.getSuppressMarker() != null) { project.log("Setting suppress marker to be " + configuration.getSuppressMarker(), Project.MSG_VERBOSE); } - @SuppressWarnings("PMD.CloseResource") - ViolationCounterListener reportSizeListener = new ViolationCounterListener(); - - final List files = new ArrayList<>(); - final List reportShortNamesPaths = new ArrayList<>(); + @SuppressWarnings("PMD.CloseResource") final List reportShortNamesPaths = new ArrayList<>(); StringJoiner fullInputPath = new StringJoiner(","); - for (FileSet fs : filesets) { - DirectoryScanner ds = fs.getDirectoryScanner(project); - for (String srcFile : ds.getIncludedFiles()) { - File file = new File(ds.getBasedir() + File.separator + srcFile); - files.add(new FileDataSource(file)); + List ruleSetPaths = expandRuleSetPaths(configuration.getRuleSetPaths()); + // don't let PmdAnalysis.create create rulesets itself. + configuration.setRuleSets(Collections.emptyList()); + + ReportStats stats; + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + RuleSetLoader rulesetLoader = + pmd.newRuleSetLoader().loadResourcesWith(setupResourceLoader()); + pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(ruleSetPaths)); + + for (FileSet fileset : filesets) { + DirectoryScanner ds = fileset.getDirectoryScanner(project); + for (String srcFile : ds.getIncludedFiles()) { + pmd.files().addFile(ds.getBasedir().toPath().resolve(srcFile)); + } + + final String commonInputPath = ds.getBasedir().getPath(); + fullInputPath.add(commonInputPath); + if (configuration.isReportShortNames()) { + reportShortNamesPaths.add(commonInputPath); + } } - final String commonInputPath = ds.getBasedir().getPath(); - fullInputPath.add(commonInputPath); - if (configuration.isReportShortNames()) { - reportShortNamesPaths.add(commonInputPath); + @SuppressWarnings("PMD.CloseResource") + ReportStatsListener reportStatsListener = new ReportStatsListener(); + pmd.addListener(getListener(reportStatsListener, reportShortNamesPaths, fullInputPath.toString())); + + pmd.performAnalysis(); + stats = reportStatsListener.getResult(); + if (failOnError && pmd.getReporter().numErrors() > 0) { + throw new BuildException("Some errors occurred while running PMD"); } } - configuration.setInputPaths(fullInputPath.toString()); - try (GlobalAnalysisListener listener = getListener(reportSizeListener, reportShortNamesPaths)) { - PMD.processFiles(configuration, rules, files, listener); - } catch (Exception e) { - throw new BuildException("Exception while closing data sources", e); - } - - int problemCount = reportSizeListener.getResult(); + int problemCount = stats.getNumViolations(); project.log(problemCount + " problems found", Project.MSG_VERBOSE); if (failuresPropertyName != null && problemCount > 0) { @@ -152,34 +155,27 @@ public class PMDTaskImpl { } } - private List loadRulesets(RuleSetLoader rulesetLoader) { - try { - // This is just used to validate and display rules. Each thread will create its own ruleset - // Substitute env variables/properties - String ruleSetString = project.replaceProperties(rulesetPaths); - - List rulesets = Arrays.asList(ruleSetString.split(",")); - List rulesetList = rulesetLoader.loadFromResources(rulesets); - if (rulesetList.isEmpty()) { - throw new BuildException("No rulesets"); - } - logRulesUsed(rulesetList); - return rulesetList; - } catch (RuleSetLoadException e) { - throw new BuildException(e.getMessage(), e); + private List expandRuleSetPaths(List ruleSetPaths) { + List paths = new ArrayList<>(ruleSetPaths); + for (int i = 0; i < paths.size(); i++) { + paths.set(i, project.replaceProperties(paths.get(i))); } + return paths; } - private @NonNull GlobalAnalysisListener getListener(ViolationCounterListener reportSizeListener, List reportShortNamesPaths) { + private @NonNull GlobalAnalysisListener getListener(ReportStatsListener reportSizeListener, + List reportShortNamesPaths, + String inputPaths) { List renderers = new ArrayList<>(formatters.size() + 1); try { - renderers.add(makeLogListener(configuration.getInputPaths())); + renderers.add(makeLogListener(inputPaths)); renderers.add(reportSizeListener); for (Formatter formatter : formatters) { project.log("Sending a report to " + formatter, Project.MSG_VERBOSE); renderers.add(formatter.newListener(project, reportShortNamesPaths)); } - } catch (IOException e) { + return GlobalAnalysisListener.tee(renderers); + } catch (Exception e) { // close those opened so far Exception e2 = IOUtil.closeAll(renderers); if (e2 != null) { @@ -187,8 +183,6 @@ public class PMDTaskImpl { } throw new BuildException("Exception while initializing renderers", e); } - - return GlobalAnalysisListener.tee(renderers); } private GlobalAnalysisListener makeLogListener(String commonInputPath) { @@ -233,9 +227,9 @@ public class PMDTaskImpl { try { if (auxClasspath != null) { project.log("Using auxclasspath: " + auxClasspath, Project.MSG_VERBOSE); - configuration.prependClasspath(auxClasspath.toString()); + configuration.prependAuxClasspath(auxClasspath.toString()); } - } catch (IOException ioe) { + } catch (IllegalArgumentException ioe) { throw new BuildException(ioe.getMessage(), ioe); } } @@ -257,13 +251,4 @@ public class PMDTaskImpl { } } - private void logRulesUsed(List rulesets) { - project.log("Using these rulesets: " + rulesetPaths, Project.MSG_VERBOSE); - - for (RuleSet ruleSet : rulesets) { - for (Rule rule : ruleSet.getRules()) { - project.log("Using rule " + rule.getName(), Project.MSG_VERBOSE); - } - } - } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java index 9089eb05bd..624515579c 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sourceforge.pmd.PMDVersion; +import net.sourceforge.pmd.Report.ProcessingError; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.annotation.InternalApi; @@ -210,13 +211,26 @@ public abstract class AbstractAnalysisCache implements AnalysisCache { } @Override - public FileAnalysisListener startFileAnalysis(DataSource filename) { - return violation -> { - final AnalysisResult analysisResult = - updatedResultsCache.get(violation.getFilename()); + public FileAnalysisListener startFileAnalysis(DataSource dataSource) { + String fileName = dataSource.getNiceFileName(false, ""); + File sourceFile = new File(fileName); + AnalysisResult analysisResult = updatedResultsCache.get(fileName); + if (analysisResult == null) { + analysisResult = new AnalysisResult(sourceFile); + } + final AnalysisResult nonNullAnalysisResult = analysisResult; - synchronized (analysisResult) { - analysisResult.addViolation(violation); + return new FileAnalysisListener() { + @Override + public void onRuleViolation(RuleViolation violation) { + synchronized (nonNullAnalysisResult) { + nonNullAnalysisResult.addViolation(violation); + } + } + + @Override + public void onError(ProcessingError error) { + analysisFailed(sourceFile); } }; } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java index ea1f6f5273..f16490263a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.cache; import java.io.File; +import java.io.IOException; import java.util.List; import net.sourceforge.pmd.RuleSets; @@ -12,6 +13,7 @@ import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.reporting.FileAnalysisListener; import net.sourceforge.pmd.reporting.GlobalAnalysisListener; +import net.sourceforge.pmd.util.datasource.DataSource; /** * An analysis cache for incremental analysis. @@ -22,12 +24,12 @@ import net.sourceforge.pmd.reporting.GlobalAnalysisListener; */ @Deprecated @InternalApi -public interface AnalysisCache extends GlobalAnalysisListener { +public interface AnalysisCache { /** * Persists the updated analysis results on whatever medium is used by the cache. */ - void persist(); + void persist() throws IOException; /** * Checks if a given file is up to date in the cache and can be skipped from analysis. @@ -59,8 +61,16 @@ public interface AnalysisCache extends GlobalAnalysisListener { * cache is invalidated. This needs to be called before analysis, as it * conditions the good behaviour of {@link #isUpToDate(File)}. * - * @param ruleSets The rulesets configured for this analysis. + * @param ruleSets The rulesets configured for this analysis. * @param auxclassPathClassLoader The class loader for auxclasspath configured for this analysis. */ void checkValidity(RuleSets ruleSets, ClassLoader auxclassPathClassLoader); + + /** + * Returns a listener that will be used like in {@link GlobalAnalysisListener#startFileAnalysis(DataSource)}. + * This should record violations, and call {@link #analysisFailed(File)} + * upon error. + */ + FileAnalysisListener startFileAnalysis(DataSource file); + } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java new file mode 100644 index 0000000000..f6c4d12558 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java @@ -0,0 +1,40 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.cache; + +import java.io.IOException; + +import net.sourceforge.pmd.RuleSets; +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.reporting.FileAnalysisListener; +import net.sourceforge.pmd.reporting.GlobalAnalysisListener; +import net.sourceforge.pmd.util.datasource.DataSource; + +/** + * Adapter to wrap {@link AnalysisCache} behaviour in a {@link GlobalAnalysisListener}. + */ +@Deprecated +@InternalApi +public class AnalysisCacheListener implements GlobalAnalysisListener { + + private final AnalysisCache cache; + + public AnalysisCacheListener(AnalysisCache cache, RuleSets ruleSets, ClassLoader classLoader) { + this.cache = cache; + cache.checkValidity(ruleSets, classLoader); + } + + + @Override + public FileAnalysisListener startFileAnalysis(DataSource file) { + return cache.startFileAnalysis(file); + } + + @Override + public void close() throws IOException { + cache.persist(); + } + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java index 5c1f29f4db..d2739cfc92 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java @@ -151,11 +151,6 @@ public class FileAnalysisCache extends AbstractAnalysisCache { } } - @Override - public void close() throws Exception { - // nothing to do, PMD calls persist explicitly - } - @Override protected boolean cacheExists() { return cacheFile.exists() && cacheFile.isFile() && cacheFile.length() > 0; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java index 56127ea4a7..3a3dee4470 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java @@ -53,8 +53,4 @@ public class NoopAnalysisCache implements AnalysisCache { return FileAnalysisListener.noop(); } - @Override - public void close() throws Exception { - // noop - } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java index 7419d11afd..5e90676c06 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java @@ -8,6 +8,7 @@ import java.util.Properties; import java.util.stream.Collectors; import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.PMD.StatusCode; import net.sourceforge.pmd.PMDVersion; import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.Language; @@ -28,13 +29,39 @@ import com.beust.jcommander.ParameterException; @InternalApi public final class PMDCommandLineInterface { + @Deprecated public static final String PROG_NAME = "pmd"; + /** + * @deprecated This is used for testing, but support for it will be removed in PMD 7. + * Use {@link PMD#runPmd(String...)} or an overload to avoid exiting the VM. In PMD 7, + * {@link PMD#main(String[])} will call {@link System#exit(int)} always. + */ + @Deprecated public static final String NO_EXIT_AFTER_RUN = "net.sourceforge.pmd.cli.noExit"; + + /** + * @deprecated This is used for testing, but support for it will be removed in PMD 7. + * Use {@link PMD#runPmd(String...)} or an overload to avoid exiting the VM. In PMD 7, + * {@link PMD#main(String[])} will call {@link System#exit(int)} always. + */ + @Deprecated public static final String STATUS_CODE_PROPERTY = "net.sourceforge.pmd.cli.status"; + /** + * @deprecated Use {@link StatusCode#OK} + */ + @Deprecated public static final int NO_ERRORS_STATUS = 0; + /** + * @deprecated Use {@link StatusCode#ERROR} + */ + @Deprecated public static final int ERROR_STATUS = 1; + /** + * @deprecated Use {@link StatusCode#VIOLATIONS_FOUND} + */ + @Deprecated public static final int VIOLATIONS_FOUND = 4; private PMDCommandLineInterface() { } @@ -126,7 +153,10 @@ public final class PMDCommandLineInterface { * For testing purpose only... * * @param args + * + * @deprecated Use {@link PMD#runPmd(String...)} */ + @Deprecated public static void main(String[] args) { System.out.println(PMDCommandLineInterface.buildUsageText()); } @@ -160,14 +190,6 @@ public final class PMDCommandLineInterface { return buf.toString(); } - /** - * @deprecated Use {@link PMD#main(String[])} - */ - @Deprecated - public static void run(String[] args) { - setStatusCodeOrExit(PMD.run(args)); - } - public static void setStatusCodeOrExit(int status) { if (isExitAfterRunSet()) { System.exit(status); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java index 892093af24..7cea583a26 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java @@ -4,7 +4,6 @@ package net.sourceforge.pmd.cli; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -235,8 +234,8 @@ public class PMDParameters { } try { - configuration.prependClasspath(this.getAuxclasspath()); - } catch (IOException e) { + configuration.prependAuxClasspath(this.getAuxclasspath()); + } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid auxiliary classpath: " + e.getMessage(), e); } return configuration; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java index b1034a6252..fca762b178 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java @@ -25,10 +25,12 @@ public final class AssertionUtil { /** @throws NullPointerException if $name */ public static void requireContainsNoNullValue(String name, Collection c) { + int i = 0; for (Object o : c) { if (o == null) { - throw new NullPointerException(name + " contains null elements"); + throw new NullPointerException(name + " contains a null element at index " + i); } + i++; } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java new file mode 100644 index 0000000000..697d16f927 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java @@ -0,0 +1,183 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.internal.util; + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sourceforge.pmd.PMDConfiguration; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.document.FileCollector; +import net.sourceforge.pmd.lang.document.TextFile; +import net.sourceforge.pmd.util.FileUtil; +import net.sourceforge.pmd.util.database.DBMSMetadata; +import net.sourceforge.pmd.util.database.DBURI; +import net.sourceforge.pmd.util.database.SourceObject; +import net.sourceforge.pmd.util.datasource.DataSource; +import net.sourceforge.pmd.util.log.MessageReporter; +import net.sourceforge.pmd.util.log.internal.ErrorsAsWarningsReporter; + +/** + * @author ClΓ©ment Fournier + */ +public final class FileCollectionUtil { + + private static final Logger LOG = LoggerFactory.getLogger(FileCollectionUtil.class); + + private FileCollectionUtil() { + + } + + public static List collectorToDataSource(FileCollector collector) { + List result = new ArrayList<>(); + for (TextFile file : collector.getCollectedFiles()) { + result.add(file.toDataSourceCompat()); + } + return result; + } + + public static FileCollector collectFiles(PMDConfiguration configuration, Set languages, MessageReporter reporter) { + FileCollector collector = collectFiles(configuration, reporter); + collector.filterLanguages(languages); + return collector; + } + + private static FileCollector collectFiles(PMDConfiguration configuration, MessageReporter reporter) { + FileCollector collector = FileCollector.newCollector( + configuration.getLanguageVersionDiscoverer(), + reporter + ); + collectFiles(configuration, collector); + return collector; + } + + public static void collectFiles(PMDConfiguration configuration, FileCollector collector) { + if (configuration.getSourceEncoding() != null) { + collector.setCharset(configuration.getSourceEncoding()); + } + + if (configuration.getInputPaths() != null) { + collectFiles(collector, configuration.getInputPaths()); + } + + if (configuration.getInputUri() != null) { + collectDB(collector, configuration.getInputUri()); + } + + if (configuration.getInputFilePath() != null) { + collectFileList(collector, configuration.getInputFilePath()); + } + + if (configuration.getIgnoreFilePath() != null) { + // This is to be able to interpret the log (will report 'adding' xxx) + LOG.debug("Now collecting files to exclude."); + // errors like "excluded file does not exist" are reported as warnings. + // todo better reporting of *where* exactly the path is + MessageReporter mutedLog = new ErrorsAsWarningsReporter(collector.getReporter()); + try (FileCollector excludeCollector = collector.newCollector(mutedLog)) { + collectFileList(excludeCollector, configuration.getIgnoreFilePath()); + collector.exclude(excludeCollector); + } + } + } + + + public static void collectFiles(FileCollector collector, String fileLocations) { + for (String rootLocation : fileLocations.split(",")) { + try { + collector.relativizeWith(rootLocation); + addRoot(collector, rootLocation); + } catch (IOException e) { + collector.getReporter().errorEx("Error collecting " + rootLocation, e); + } + } + } + + public static void collectFileList(FileCollector collector, String fileListLocation) { + LOG.debug("Reading file list {}.", fileListLocation); + Path path = Paths.get(fileListLocation); + if (!Files.exists(path)) { + collector.getReporter().error("No such file {}", fileListLocation); + return; + } + + String filePaths; + try { + filePaths = FileUtil.readFilelist(path.toFile()); + } catch (IOException e) { + collector.getReporter().errorEx("Error reading {}", new Object[] { fileListLocation }, e); + return; + } + collectFiles(collector, filePaths); + } + + private static void addRoot(FileCollector collector, String rootLocation) throws IOException { + Path path = Paths.get(rootLocation); + if (!Files.exists(path)) { + collector.getReporter().error("No such file {}", path); + return; + } + + if (Files.isDirectory(path)) { + LOG.debug("Adding directory {}.", path); + collector.addDirectory(path); + } else if (rootLocation.endsWith(".zip") || rootLocation.endsWith(".jar")) { + LOG.debug("Adding zip file {}.", path); + @SuppressWarnings("PMD.CloseResource") + FileSystem fs = collector.addZipFile(path); + if (fs == null) { + return; + } + for (Path zipRoot : fs.getRootDirectories()) { + collector.addFileOrDirectory(zipRoot); + } + } else if (Files.isRegularFile(path)) { + LOG.debug("Adding regular file {}.", path); + collector.addFile(path); + } else { + LOG.debug("Ignoring {}: not a regular file or directory", path); + } + } + + public static void collectDB(FileCollector collector, String uriString) { + try { + LOG.debug("Connecting to {}", uriString); + DBURI dbUri = new DBURI(uriString); + DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri); + LOG.trace("DBMSMetadata retrieved"); + List sourceObjectList = dbmsMetadata.getSourceObjectList(); + LOG.trace("Located {} database source objects", sourceObjectList.size()); + for (SourceObject sourceObject : sourceObjectList) { + String falseFilePath = sourceObject.getPseudoFileName(); + LOG.trace("Adding database source object {}", falseFilePath); + + try (Reader sourceCode = dbmsMetadata.getSourceCode(sourceObject)) { + String source = IOUtils.toString(sourceCode); + collector.addSourceFile(source, falseFilePath); + } catch (SQLException ex) { + collector.getReporter().warnEx("Cannot get SourceCode for {} - skipping ...", + new Object[] { falseFilePath }, + ex); + } + } + } catch (ClassNotFoundException e) { + collector.getReporter().errorEx("Cannot get files from DB - probably missing database JDBC driver", e); + } catch (Exception e) { + collector.getReporter().errorEx("Cannot get files from DB - ''{}''", new Object[] { uriString }, e); + } + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java index 4413c302ca..4491ff2a42 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java @@ -154,7 +154,7 @@ public abstract class BaseLanguageModule implements Language { @Override public String toString() { - return "LanguageModule:" + name + '(' + this.getClass().getSimpleName() + ')'; + return getTerseName(); } @Override diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java index 94bf68af88..b1ac43e348 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java @@ -35,7 +35,7 @@ public class LanguageVersion implements Comparable { private final Language language; private final String version; - private final LanguageVersionHandler languageVersionHandler; + private final LanguageVersionHandler languageVersionHandler; // note: this is null if this is a cpd-only language... /** * @deprecated Use {@link Language#getVersion(String)}. This is only diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java index 61e997c29c..4e22b03a4f 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java @@ -8,6 +8,11 @@ import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import net.sourceforge.pmd.internal.util.AssertionUtil; /** * This class can discover the LanguageVersion of a source file. Further, every @@ -17,6 +22,23 @@ import java.util.Map; public class LanguageVersionDiscoverer { private Map languageToLanguageVersion = new HashMap<>(); + private LanguageVersion forcedVersion; + + public LanguageVersionDiscoverer() { + this(null); + } + + /** + * Build a new instance. + * + * @param forcedVersion If non-null, all files should be assigned this version. + * The methods of this class still work as usual and do not + * care about the forced language version. + */ + public LanguageVersionDiscoverer(LanguageVersion forcedVersion) { + this.forcedVersion = forcedVersion; + } + /** * Set the given LanguageVersion as the current default for it's Language. * @@ -25,6 +47,7 @@ public class LanguageVersionDiscoverer { * @return The previous default version for the language. */ public LanguageVersion setDefaultLanguageVersion(LanguageVersion languageVersion) { + AssertionUtil.requireParamNotNull("languageVersion", languageVersion); LanguageVersion currentLanguageVersion = languageToLanguageVersion.put(languageVersion.getLanguage(), languageVersion); if (currentLanguageVersion == null) { @@ -41,6 +64,7 @@ public class LanguageVersionDiscoverer { * @return The current default version for the language. */ public LanguageVersion getDefaultLanguageVersion(Language language) { + Objects.requireNonNull(language); LanguageVersion languageVersion = languageToLanguageVersion.get(language); if (languageVersion == null) { languageVersion = language.getDefaultVersion(); @@ -81,6 +105,14 @@ public class LanguageVersionDiscoverer { return languageVersion; } + public LanguageVersion getForcedVersion() { + return forcedVersion; + } + + public void setForcedVersion(LanguageVersion forceLanguageVersion) { + this.forcedVersion = forceLanguageVersion; + } + /** * Get the Languages of a given source file. * @@ -106,11 +138,8 @@ public class LanguageVersionDiscoverer { // Get the extensions from a file private String getExtension(String fileName) { - String extension = null; - int extensionIndex = 1 + fileName.lastIndexOf('.'); - if (extensionIndex > 0) { - extension = fileName.substring(extensionIndex); - } - return extension; + return StringUtils.substringAfterLast(fileName, "."); } + + } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java index 3697ff7ddb..348d104a3a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java @@ -10,7 +10,7 @@ import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; -import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.properties.AbstractPropertySource; import net.sourceforge.pmd.properties.PropertyDescriptor; @@ -72,7 +72,7 @@ public interface Parser { public static final PropertyDescriptor COMMENT_MARKER = PropertyFactory.stringProperty("suppressionCommentMarker") .desc("deprecated! NOPMD") - .defaultValue(PMD.SUPPRESS_MARKER) + .defaultValue(PMDConfiguration.DEFAULT_SUPPRESS_MARKER) .build(); @Deprecated // transitional until language properties are implemented diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java new file mode 100644 index 0000000000..55c7fb5d24 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java @@ -0,0 +1,400 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sourceforge.pmd.PmdAnalysis; +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.internal.util.AssertionUtil; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; +import net.sourceforge.pmd.util.IOUtil; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * Collects files to analyse before a PMD run. This API allows opening + * zip files and makes sure they will be closed at the end of a run. + * + * @author ClΓ©ment Fournier + */ +@SuppressWarnings("PMD.CloseResource") +public final class FileCollector implements AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(FileCollector.class); + + private final List allFilesToProcess = new ArrayList<>(); + private final List resourcesToClose = new ArrayList<>(); + private Charset charset = StandardCharsets.UTF_8; + private final LanguageVersionDiscoverer discoverer; + private final MessageReporter reporter; + private final List relativizeRoots = new ArrayList<>(); + private boolean closed; + + // construction + + private FileCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) { + this.discoverer = discoverer; + this.reporter = reporter; + } + + /** + * Internal API: please use {@link PmdAnalysis#files()} instead of + * creating a collector yourself. + */ + @InternalApi + public static FileCollector newCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) { + return new FileCollector(discoverer, reporter); + } + + /** + * Returns a new collector using the configuration except for the logger. + */ + @InternalApi + public FileCollector newCollector(MessageReporter logger) { + FileCollector fileCollector = new FileCollector(discoverer, logger); + fileCollector.charset = this.charset; + fileCollector.relativizeRoots.addAll(this.relativizeRoots); + return fileCollector; + } + + // public behaviour + + /** + * Returns an unmodifiable list of all files that have been collected. + * + *

    Internal: This might be unstable until PMD 7, but it's internal. + */ + @InternalApi + public List getCollectedFiles() { + if (closed) { + throw new IllegalStateException("Collector was closed!"); + } + allFilesToProcess.sort(Comparator.comparing(TextFile::getPathId)); + return Collections.unmodifiableList(allFilesToProcess); + } + + + /** + * Returns the reporter for the file collection phase. + */ + @InternalApi + public MessageReporter getReporter() { + return reporter; + } + + /** + * Close registered resources like zip files. + */ + @Override + public void close() { + if (closed) { + return; + } + closed = true; + Exception exception = IOUtil.closeAll(resourcesToClose); + if (exception != null) { + reporter.errorEx("Error while closing resources", exception); + } + } + + // collection + + /** + * Add a file, language is determined automatically from + * the extension/file patterns. The encoding is the current + * encoding ({@link #setCharset(Charset)}). + * + * @param file File to add + * + * @return True if the file has been added + */ + public boolean addFile(Path file) { + if (!Files.isRegularFile(file)) { + reporter.error("Not a regular file {}", file); + return false; + } + LanguageVersion languageVersion = discoverLanguage(file.toString()); + if (languageVersion != null) { + addFileImpl(new NioTextFile(file, charset, languageVersion, getDisplayName(file))); + return true; + } + return false; + } + + /** + * Add a file with the given language (which overrides the file patterns). + * The encoding is the current encoding ({@link #setCharset(Charset)}). + * + * @param file Path to a file + * @param language A language. The language version will be taken to be the + * contextual default version. + * + * @return True if the file has been added + */ + public boolean addFile(Path file, Language language) { + AssertionUtil.requireParamNotNull("language", language); + if (!Files.isRegularFile(file)) { + reporter.error("Not a regular file {}", file); + return false; + } + NioTextFile nioTextFile = new NioTextFile(file, charset, discoverer.getDefaultLanguageVersion(language), getDisplayName(file)); + addFileImpl(nioTextFile); + return true; + } + + /** + * Add a pre-configured text file. The language version will be checked + * to match the contextual default for the language (the file cannot be added + * if it has a different version). + * + * @return True if the file has been added + */ + public boolean addFile(TextFile textFile) { + AssertionUtil.requireParamNotNull("textFile", textFile); + if (checkContextualVersion(textFile)) { + addFileImpl(textFile); + return true; + } + return false; + } + + /** + * Add a text file given its contents and a name. The language version + * will be determined from the name as usual. + * + * @return True if the file has been added + */ + public boolean addSourceFile(String pathId, String sourceContents) { + AssertionUtil.requireParamNotNull("sourceContents", sourceContents); + AssertionUtil.requireParamNotNull("pathId", pathId); + + LanguageVersion version = discoverLanguage(pathId); + if (version != null) { + addFileImpl(new StringTextFile(sourceContents, pathId, pathId, version)); + return true; + } + + return false; + } + + private void addFileImpl(TextFile textFile) { + LOG.trace("Adding file {} (lang: {}) ", textFile.getPathId(), textFile.getLanguageVersion().getTerseName()); + allFilesToProcess.add(textFile); + } + + private LanguageVersion discoverLanguage(String file) { + if (discoverer.getForcedVersion() != null) { + return discoverer.getForcedVersion(); + } + List languages = discoverer.getLanguagesForFile(file); + + if (languages.isEmpty()) { + LOG.trace("File {} matches no known language, ignoring", file); + return null; + } + Language lang = languages.get(0); + if (languages.size() > 1) { + LOG.trace("File {} matches multiple languages ({}), selecting {}", file, languages, lang); + } + return discoverer.getDefaultLanguageVersion(lang); + } + + /** + * Whether the LanguageVersion of the file matches the one set in + * the {@link LanguageVersionDiscoverer}. This is required to ensure + * that all files for a given language have the same language version. + */ + private boolean checkContextualVersion(TextFile textFile) { + LanguageVersion fileVersion = textFile.getLanguageVersion(); + Language language = fileVersion.getLanguage(); + LanguageVersion contextVersion = discoverer.getDefaultLanguageVersion(language); + if (!fileVersion.equals(contextVersion)) { + reporter.error( + "Cannot add file {}: version ''{}'' does not match ''{}''", + textFile.getPathId(), + fileVersion, + contextVersion + ); + return false; + } + return true; + } + + private String getDisplayName(Path file) { + return getDisplayName(file, relativizeRoots); + } + + /** + * Return the textfile's display name. + * test only + */ + static String getDisplayName(Path file, List relativizeRoots) { + String fileName = file.toString(); + for (String root : relativizeRoots) { + if (file.startsWith(root)) { + if (fileName.startsWith(File.separator, root.length())) { + // remove following '/' + return fileName.substring(root.length() + 1); + } + return fileName.substring(root.length()); + } + } + return fileName; + } + + + /** + * Add a directory recursively using {@link #addFile(Path)} on + * all regular files. + * + * @param dir Directory path + * + * @return True if the directory has been added + */ + public boolean addDirectory(Path dir) throws IOException { + if (!Files.isDirectory(dir)) { + reporter.error("Not a directory {}", dir); + return false; + } + Files.walkFileTree(dir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (attrs.isRegularFile()) { + FileCollector.this.addFile(file); + } + return super.visitFile(file, attrs); + } + }); + return true; + } + + + /** + * Add a file or directory recursively. Language is determined automatically + * from the extension/file patterns. + * + * @return True if the file or directory has been added + */ + public boolean addFileOrDirectory(Path file) throws IOException { + if (Files.isDirectory(file)) { + return addDirectory(file); + } else if (Files.isRegularFile(file)) { + return addFile(file); + } else { + reporter.error("Not a file or directory {}", file); + return false; + } + } + + /** + * Opens a zip file and returns a FileSystem for its contents, so + * it can be explored with the {@link Path} API. You can then call + * {@link #addFile(Path)} and such. The zip file is registered as + * a resource to close at the end of analysis. + */ + public FileSystem addZipFile(Path zipFile) { + if (!Files.isRegularFile(zipFile)) { + throw new IllegalArgumentException("Not a regular file: " + zipFile); + } + URI zipUri = URI.create("zip:" + zipFile.toUri()); + try { + FileSystem fs = FileSystems.getFileSystem(zipUri); + resourcesToClose.add(fs); + return fs; + } catch (FileSystemNotFoundException | ProviderNotFoundException e) { + reporter.errorEx("Cannot open zip file " + zipFile, e); + return null; + } + } + + // configuration + + /** + * Sets the charset to use for subsequent calls to {@link #addFile(Path)} + * and other overloads using a {@link Path}. + * + * @param charset A charset + */ + public void setCharset(Charset charset) { + this.charset = Objects.requireNonNull(charset); + } + + /** + * Add a prefix that is used to relativize file paths as their display name. + * For instance, when adding a file {@code /tmp/src/main/java/org/foo.java}, + * and relativizing with {@code /tmp/src/}, the registered {@link TextFile} + * will have a path id of {@code /tmp/src/main/java/org/foo.java}, and a + * display name of {@code main/java/org/foo.java}. + * + * This only matters for files added from a {@link Path} object. + * + * @param prefix Prefix to relativize (if a directory, include a trailing slash) + */ + public void relativizeWith(String prefix) { + this.relativizeRoots.add(Objects.requireNonNull(prefix)); + } + + // filtering + + /** + * Remove all files collected by the given collector from this one. + */ + public void exclude(FileCollector excludeCollector) { + Set toExclude = new HashSet<>(excludeCollector.allFilesToProcess); + for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) { + TextFile file = iterator.next(); + if (toExclude.contains(file)) { + LOG.trace("Excluding file {}", file.getPathId()); + iterator.remove(); + } + } + } + + /** + * Exclude all collected files whose language is not part of the given + * collection. + */ + public void filterLanguages(Set languages) { + for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) { + TextFile file = iterator.next(); + Language lang = file.getLanguageVersion().getLanguage(); + if (!languages.contains(lang)) { + LOG.trace("Filtering out {}, no rules for language {}", file.getPathId(), lang); + iterator.remove(); + } + } + } + + @Override + public String toString() { + return "FileCollector{filesToProcess=" + allFilesToProcess + '}'; + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java new file mode 100644 index 0000000000..705ef530a7 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java @@ -0,0 +1,101 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import org.apache.commons.io.IOUtils; + +import net.sourceforge.pmd.annotation.Experimental; +import net.sourceforge.pmd.internal.util.AssertionUtil; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.util.datasource.DataSource; +import net.sourceforge.pmd.util.datasource.FileDataSource; + +/** + * A {@link TextFile} backed by a file in some {@link FileSystem}. + */ +@Experimental +class NioTextFile implements TextFile { + + private final Path path; + private final Charset charset; + private final LanguageVersion languageVersion; + private final String displayName; + private final String pathId; + + NioTextFile(Path path, Charset charset, LanguageVersion languageVersion, String displayName) { + AssertionUtil.requireParamNotNull("path", path); + AssertionUtil.requireParamNotNull("charset", charset); + AssertionUtil.requireParamNotNull("language version", languageVersion); + + this.displayName = displayName; + this.path = path; + this.charset = charset; + this.languageVersion = languageVersion; + this.pathId = path.toAbsolutePath().toString(); + } + + @Override + public LanguageVersion getLanguageVersion() { + return languageVersion; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getPathId() { + return pathId; + } + + + @Override + public String readContents() throws IOException { + + if (!Files.isRegularFile(path)) { + throw new IOException("Not a regular file: " + path); + } + + try (BufferedReader br = Files.newBufferedReader(path, charset)) { + return IOUtils.toString(br); + } + } + + @Override + public DataSource toDataSourceCompat() { + return new FileDataSource(path.toFile()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NioTextFile that = (NioTextFile) o; + return Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(pathId); + } + + @Override + public String toString() { + return getPathId(); + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java new file mode 100644 index 0000000000..598823e555 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java @@ -0,0 +1,94 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import java.io.StringReader; +import java.util.Objects; + +import net.sourceforge.pmd.annotation.Experimental; +import net.sourceforge.pmd.internal.util.AssertionUtil; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.util.datasource.DataSource; +import net.sourceforge.pmd.util.datasource.ReaderDataSource; + +/** + * Read-only view on a string. + * + * @author ClΓ©ment Fournier + */ +@Experimental +class StringTextFile implements TextFile { + + private final String content; + private final String pathId; + private final String displayName; + private final LanguageVersion languageVersion; + + StringTextFile(String content, + String pathId, + String displayName, + LanguageVersion languageVersion) { + AssertionUtil.requireParamNotNull("source text", content); + AssertionUtil.requireParamNotNull("file name", displayName); + AssertionUtil.requireParamNotNull("file ID", pathId); + AssertionUtil.requireParamNotNull("language version", languageVersion); + + this.languageVersion = languageVersion; + this.content = content; + this.pathId = pathId; + this.displayName = displayName; + } + + + @Override + public LanguageVersion getLanguageVersion() { + return languageVersion; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getPathId() { + return pathId; + } + + @Override + public String readContents() { + return content; + } + + @Override + public DataSource toDataSourceCompat() { + return new ReaderDataSource( + new StringReader(content), + pathId + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StringTextFile that = (StringTextFile) o; + return Objects.equals(pathId, that.pathId); + } + + @Override + public int hashCode() { + return Objects.hash(pathId); + } + + @Override + public String toString() { + return getPathId(); + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java new file mode 100644 index 0000000000..34d232d38a --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java @@ -0,0 +1,98 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import java.io.File; +import java.io.IOException; + +import net.sourceforge.pmd.PmdAnalysis; +import net.sourceforge.pmd.annotation.Experimental; +import net.sourceforge.pmd.cpd.SourceCode; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.util.datasource.DataSource; + +/** + * Represents some location containing character data. Despite the name, + * it's not necessarily backed by a file in the file-system: it may be + * eg an in-memory buffer, or a zip entry, ie it's an abstraction. Text + * files are the input which PMD and CPD process. + * + *

    Text files must provide read access, and may provide write access. + * This interface only provides block IO operations, while {@link TextDocument} adds logic + * about incremental edition (eg replacing a single region of text). + * + *

    This interface is meant to replace {@link DataSource} and {@link SourceCode.CodeLoader}. + * "DataSource" is not an appropriate name for a file which can be written + * to, also, the "data" it provides is text, not bytes. + * + *

    Experimental

    + * This interface will change in PMD 7 to support read/write operations + * and other things. You don't need to use it in PMD 6, as {@link FileCollector} + * decouples you from this. A file collector is available through {@link PmdAnalysis#files()}. + */ +@Experimental +public interface TextFile { + + /** + * The name used for a file that has no name. This is mostly only + * relevant for unit tests. + */ + String UNKNOWN_FILENAME = "(unknown file)"; + + + /** + * Returns the language version which should be used to process this + * file. This is a property of the file, which allows sources for + * several different language versions to be processed in the same + * PMD run. It also makes it so, that the file extension is not interpreted + * to find out the language version after the initial file collection + * phase. + * + * @return A language version + */ + LanguageVersion getLanguageVersion(); + + + /** + * Returns an identifier for the path of this file. This should not + * be interpreted as a {@link File}, it may not be a file on this + * filesystem. The only requirement for this method, is that two + * distinct text files should have distinct path IDs, and that from + * one analysis to the next, the path ID of logically identical files + * be the same. + * + *

    Basically this may be implemented as a URL, or a file path. It + * is used to index violation caches. + */ + String getPathId(); + + + /** + * Returns a display name for the file. This name is used for + * reporting and should not be interpreted. It may be relative + * to a directory, may use platform-specific path separators, + * may not be normalized. Use {@link #getPathId()} when you + * want an identifier. + */ + String getDisplayName(); + + + /** + * Reads the contents of the underlying character source. + * + * @return The most up-to-date content + * + * @throws IOException If this instance is closed + * @throws IOException If reading causes an IOException + */ + String readContents() throws IOException; + + /** + * Compatibility with {@link DataSource} (pmd internals still use DataSource in PMD 6). + */ + @Deprecated + DataSource toDataSourceCompat(); + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java new file mode 100644 index 0000000000..0544cc175c --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java @@ -0,0 +1,63 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document.internal; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; + +/** + * Discovers the languages applicable to a file. + */ +public class LanguageDiscoverer { + + + private final Language forcedLanguage; + + /** + * Build a new instance. + * + * @param forcedLanguage If non-null, all files will be assigned this language. + */ + public LanguageDiscoverer(Language forcedLanguage) { + this.forcedLanguage = forcedLanguage; + } + + /** + * Get the Languages of a given source file. + * + * @param sourceFile The file. + * + * @return The Languages for the source file, may be empty. + */ + public List getLanguagesForFile(Path sourceFile) { + return getLanguagesForFile(sourceFile.getFileName().toString()); + } + + /** + * Get the Languages of a given source file. + * + * @param fileName The file name. + * + * @return The Languages for the source file, may be empty. + */ + public List getLanguagesForFile(String fileName) { + if (forcedLanguage != null) { + return Collections.singletonList(forcedLanguage); + } + String extension = getExtension(fileName); + return LanguageRegistry.findByExtension(extension); + } + + // Get the extensions from a file + private String getExtension(String fileName) { + return StringUtils.substringAfterLast(fileName, "."); + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java index 29382142a4..87df7b0eb6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java @@ -82,7 +82,6 @@ abstract class PmdRunnable implements Runnable { if (e instanceof Error && !SystemProps.isErrorRecoveryMode()) { // NOPMD: throw e; } - configuration.getAnalysisCache().analysisFailed(file); // The listener handles logging if needed, // it may also rethrow the error, as a FileAnalysisException (which we let through below) diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java index 05385eff45..226f2737e7 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java @@ -15,6 +15,7 @@ import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.sourceforge.pmd.internal.util.AssertionUtil; import net.sourceforge.pmd.properties.PropertyDescriptor; /** @@ -61,6 +62,7 @@ public final class RendererFactory { * @return A Renderer instance. */ public static Renderer createRenderer(String reportFormat, Properties properties) { + AssertionUtil.requireParamNotNull("reportFormat", reportFormat); Class rendererClass = getRendererClass(reportFormat); Constructor constructor = getRendererConstructor(rendererClass); @@ -101,6 +103,7 @@ public final class RendererFactory { @SuppressWarnings("unchecked") private static Class getRendererClass(String reportFormat) { + AssertionUtil.requireParamNotNull("reportFormat", reportFormat); Class rendererClass = REPORT_FORMAT_TO_RENDERER.get(reportFormat); // Look up a custom renderer class diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java index ca71b7a608..c2a8f7d586 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java @@ -85,17 +85,7 @@ public interface GlobalAnalysisListener extends AutoCloseable { * A listener that does nothing. */ static GlobalAnalysisListener noop() { - return new GlobalAnalysisListener() { - @Override - public FileAnalysisListener startFileAnalysis(DataSource file) { - return FileAnalysisListener.noop(); - } - - @Override - public void close() { - // do nothing - } - }; + return NoopAnalysisListener.INSTANCE; } /** @@ -111,10 +101,11 @@ public interface GlobalAnalysisListener extends AutoCloseable { */ static GlobalAnalysisListener tee(Collection listeners) { AssertionUtil.requireParamNotNull("Listeners", listeners); - AssertionUtil.requireNotEmpty("Listeners", listeners); AssertionUtil.requireContainsNoNullValue("Listeners", listeners); - if (listeners.size() == 1) { + if (listeners.isEmpty()) { + return noop(); + } else if (listeners.size() == 1) { return listeners.iterator().next(); } @@ -150,6 +141,7 @@ public interface GlobalAnalysisListener extends AutoCloseable { List myList = listeners.stream() .flatMap(l -> l instanceof TeeListener ? ((TeeListener) l).myList.stream() : Stream.of(l)) + .filter(l -> !(l instanceof NoopAnalysisListener)) .collect(CollectionUtil.toUnmodifiableList()); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java new file mode 100644 index 0000000000..fcd5da121c --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java @@ -0,0 +1,29 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.reporting; + +import net.sourceforge.pmd.util.datasource.DataSource; + +/** + * @author Clément Fournier + */ +final class NoopAnalysisListener implements GlobalAnalysisListener { + + static final NoopAnalysisListener INSTANCE = new NoopAnalysisListener(); + + private NoopAnalysisListener() { + + } + + @Override + public FileAnalysisListener startFileAnalysis(DataSource file) { + return FileAnalysisListener.noop(); + } + + @Override + public void close() { + // do nothing + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java new file mode 100644 index 0000000000..eef7cd7dc5 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java @@ -0,0 +1,38 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.reporting; + +/** + * Summarized info about a report. + * + * @author Clément Fournier + */ +public final class ReportStats { + + private final int numErrors; + private final int numViolations; + + ReportStats(int numErrors, int numViolations) { + this.numErrors = numErrors; + this.numViolations = numViolations; + } + + public static ReportStats empty() { + return new ReportStats(0, 0); + } + + public int getNumErrors() { + return numErrors; + } + + public int getNumViolations() { + return numViolations; + } + + @Override + public String toString() { + return "ReportStats{numErrors=" + numErrors + ", numViolations=" + numViolations + '}'; + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java new file mode 100644 index 0000000000..baabb2d12b --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java @@ -0,0 +1,63 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.reporting; + +import java.util.concurrent.atomic.AtomicInteger; + +import net.sourceforge.pmd.Report.ProcessingError; +import net.sourceforge.pmd.RuleViolation; +import net.sourceforge.pmd.util.BaseResultProducingCloseable; +import net.sourceforge.pmd.util.datasource.DataSource; + +/** + * Collects summarized info about a PMD run. + * + * @author Clément Fournier + */ +public final class ReportStatsListener extends BaseResultProducingCloseable implements GlobalAnalysisListener { + + private final AtomicInteger numErrors = new AtomicInteger(0); + private final AtomicInteger numViolations = new AtomicInteger(0); + + @Override + public FileAnalysisListener startFileAnalysis(DataSource file) { + return new FileAnalysisListener() { + // this object does not need thread-safety so we avoid using atomics, + // except during the merge. + private int numErrors = 0; + private int numViolations = 0; + + @Override + public void onRuleViolation(RuleViolation violation) { + numViolations++; + } + + @Override + public void onError(ProcessingError error) { + numErrors++; + } + + @Override + public void close() { + if (numErrors > 0) { + ReportStatsListener.this.numErrors.addAndGet(this.numErrors); + } + if (numViolations > 0) { + ReportStatsListener.this.numViolations.addAndGet(this.numViolations); + } + } + }; + } + + @Override + protected ReportStats getResultImpl() { + return new ReportStats( + numErrors.get(), + numViolations.get() + ); + } + + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java index 47313a12f7..beabbc6080 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java @@ -75,7 +75,7 @@ public class RuleBuilder { Language lang = LanguageRegistry.findLanguageByTerseName(languageName); if (lang == null) { throw new IllegalArgumentException( - "Unknown Language '" + languageName + "' for rule" + name + ", supported Languages are " + "Unknown Language '" + languageName + "' for rule " + name + ", supported Languages are " + LanguageRegistry.getLanguages().stream().map(Language::getTerseName).collect(Collectors.joining(", ")) ); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java index 6e31ee3986..0266574901 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.internal.util.AssertionUtil; /** * Create a ClassLoader which loads classes using a CLASSPATH like String. If @@ -57,17 +58,19 @@ public class ClasspathClassLoader extends URLClassLoader { return urlList.toArray(new URL[0]); } - private static URL[] initURLs(String classpath) throws IOException { - if (classpath == null) { - throw new IllegalArgumentException("classpath argument cannot be null"); - } + private static URL[] initURLs(String classpath) { + AssertionUtil.requireParamNotNull("classpath", classpath); final List urls = new ArrayList<>(); - if (classpath.startsWith("file:")) { - // Treat as file URL - addFileURLs(urls, new URL(classpath)); - } else { - // Treat as classpath - addClasspathURLs(urls, classpath); + try { + if (classpath.startsWith("file:")) { + // Treat as file URL + addFileURLs(urls, new URL(classpath)); + } else { + // Treat as classpath + addClasspathURLs(urls, classpath); + } + } catch (IOException e) { + throw new IllegalArgumentException("Cannot prepend classpath " + classpath + "\n" + e.getMessage(), e); } return urls.toArray(new URL[0]); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java index 28f1aa886c..38484692ed 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java @@ -99,7 +99,7 @@ public final class FileUtil { } private static List collect(List dataSources, String fileLocation, - FilenameFilter filenameFilter) { + FilenameFilter filenameFilter) { File file = new File(fileLocation); if (!file.exists()) { throw new RuntimeException("File " + file.getName() + " doesn't exist"); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java index d25bf1027a..cde70a9b78 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java @@ -4,6 +4,7 @@ package net.sourceforge.pmd.util; +import java.text.MessageFormat; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -513,6 +514,13 @@ public final class StringUtil { return retval.toString(); } + /** + * Escape the string so that it appears literally when interpreted + * by a {@link MessageFormat}. + */ + public static String quoteMessageFormat(String str) { + return str.replaceAll("'", "''"); + } public enum CaseConvention { /** SCREAMING_SNAKE_CASE. */ diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java new file mode 100644 index 0000000000..689e4f41c4 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java @@ -0,0 +1,69 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.log; + +import java.text.MessageFormat; + +import org.slf4j.event.Level; + +import net.sourceforge.pmd.annotation.InternalApi; + +/** + * Façade to report user-facing messages (info, warning and error). + * Note: messages are formatted using {@link MessageFormat}. + * + *

    Internal API: this is a transitional API that will be significantly + * changed in PMD 7, with the transition to SLF4J. See https://github.com/pmd/pmd/issues/3816 + * + * TODO rename to PmdReporter + * + * @author ClΓ©ment Fournier + */ +@InternalApi +public interface MessageReporter { + + boolean isLoggable(Level level); + + void log(Level level, String message, Object... formatArgs); + + void logEx(Level level, String message, Object[] formatArgs, Throwable error); + + default void info(String message, Object... formatArgs) { + log(Level.INFO, message, formatArgs); + } + + default void warn(String message, Object... formatArgs) { + log(Level.WARN, message, formatArgs); + } + + default void warnEx(String message, Throwable error) { + logEx(Level.WARN, message, new Object[0], error); + } + + default void warnEx(String message, Object[] formatArgs, Throwable error) { + logEx(Level.WARN, message, formatArgs, error); + } + + default void error(String message, Object... formatArgs) { + log(Level.ERROR, message, formatArgs); + } + + default void errorEx(String message, Throwable error) { + logEx(Level.ERROR, message, new Object[0], error); + } + + default void errorEx(String message, Object[] formatArgs, Throwable error) { + logEx(Level.ERROR, message, formatArgs, error); + } + + /** + * Returns the number of errors reported on this instance. + * Any call to {@link #log(Level, String, Object...)} or + * {@link #logEx(Level, String, Object[], Throwable)} with a level + * of {@link Level#ERROR} should increment this number. + */ + int numErrors(); + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java new file mode 100644 index 0000000000..80270d9163 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java @@ -0,0 +1,41 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.log.internal; + +import org.slf4j.event.Level; + +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * Turns errors into warnings reported on another logger. + * + * @author ClΓ©ment Fournier + */ +@InternalApi +public final class ErrorsAsWarningsReporter extends MessageReporterBase { + + private final MessageReporter backend; + + public ErrorsAsWarningsReporter(MessageReporter backend) { + this.backend = backend; + } + + @Override + protected boolean isLoggableImpl(Level level) { + if (level == Level.ERROR) { + level = Level.WARN; + } + return super.isLoggableImpl(level); + } + + @Override + protected void logImpl(Level level, String message, Object[] formatArgs) { + if (level == Level.ERROR) { + level = Level.WARN; + } + backend.log(level, message, formatArgs); + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java new file mode 100644 index 0000000000..64f045174d --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java @@ -0,0 +1,80 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.log.internal; + +import java.text.MessageFormat; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.event.Level; + +import net.sourceforge.pmd.util.StringUtil; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * Base implementation. + * + * @author ClΓ©ment Fournier + */ +abstract class MessageReporterBase implements MessageReporter { + + private int numErrors; + private Level minLevel = Level.TRACE; + + /** + * null level means off. + */ + public final void setLevel(Level minLevel) { + this.minLevel = minLevel; + } + + @Override + public final boolean isLoggable(Level level) { + return minLevel != null + && minLevel.compareTo(level) >= 0 + && isLoggableImpl(level); + } + + protected boolean isLoggableImpl(Level level) { + return true; + } + + @Override + public void logEx(Level level, String message, Object[] formatArgs, Throwable error) { + if (isLoggable(level)) { + message = MessageFormat.format(message, formatArgs); + String errorMessage = error.getMessage(); + if (errorMessage == null) { + errorMessage = error.getClass().getSimpleName(); + } + errorMessage = StringUtil.quoteMessageFormat(errorMessage); + log(level, message + ": " + errorMessage); + if (isLoggable(Level.DEBUG)) { + String stackTrace = StringUtil.quoteMessageFormat(ExceptionUtils.getStackTrace(error)); + log(Level.DEBUG, stackTrace); + } + } + } + + @Override + public final void log(Level level, String message, Object... formatArgs) { + if (level == Level.ERROR) { + this.numErrors++; + } + if (isLoggable(level)) { + logImpl(level, message, formatArgs); + } + } + + /** + * Perform logging assuming {@link #isLoggable(Level)} is true. + */ + protected abstract void logImpl(Level level, String message, Object[] formatArgs); + + + @Override + public int numErrors() { + return numErrors; + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java new file mode 100644 index 0000000000..98eb4a72a3 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java @@ -0,0 +1,31 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.log.internal; + +import org.slf4j.event.Level; + +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * A logger that ignores all messages. + * + * @author ClΓ©ment Fournier + */ +@InternalApi +public final class NoopReporter extends MessageReporterBase implements MessageReporter { + + // note: not singleton because PmdLogger accumulates error count. + + @Override + protected boolean isLoggableImpl(Level level) { + return false; + } + + @Override + protected void logImpl(Level level, String message, Object[] formatArgs) { + // noop + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java new file mode 100644 index 0000000000..672e40df98 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java @@ -0,0 +1,36 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.log.internal; + +import org.slf4j.Logger; +import org.slf4j.event.Level; + +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * A {@link Logger} (java.util) based logger impl. + * + * @author ClΓ©ment Fournier + */ +@InternalApi +public class SimpleMessageReporter extends MessageReporterBase implements MessageReporter { + + private final Logger backend; + + public SimpleMessageReporter(Logger backend) { + this.backend = backend; + } + + @Override + protected boolean isLoggableImpl(Level level) { + return backend.isEnabledForLevel(level); + } + + @Override + protected void logImpl(Level level, String message, Object[] formatArgs) { + backend.atLevel(level).log(message, formatArgs); + } +} diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java new file mode 100644 index 0000000000..5c437fc1a9 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java @@ -0,0 +1,75 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; + +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import net.sourceforge.pmd.renderers.Renderer; + +/** + * @author ClΓ©ment Fournier + */ +public class PmdAnalysisTest { + + @Test + public void testPmdAnalysisWithEmptyConfig() { + PMDConfiguration config = new PMDConfiguration(); + try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + assertThat(pmd.files().getCollectedFiles(), empty()); + assertThat(pmd.rulesets(), empty()); + assertThat(pmd.renderers(), empty()); + } + } + + @Test + public void testRendererInteractions() throws IOException { + PMDConfiguration config = new PMDConfiguration(); + config.setInputPaths("sample-source/dummy"); + Renderer renderer = spy(Renderer.class); + try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + pmd.addRenderer(renderer); + verify(renderer, never()).start(); + pmd.performAnalysis(); + } + + verify(renderer, times(1)).renderFileReport(ArgumentMatchers.any()); + verify(renderer, times(1)).start(); + verify(renderer, times(1)).end(); + verify(renderer, times(1)).flush(); + } + + @Test + public void testRulesetLoading() { + PMDConfiguration config = new PMDConfiguration(); + config.addRuleSet("rulesets/dummy/basic.xml"); + try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + assertThat(pmd.rulesets(), hasSize(1)); + } + } + + @Test + public void testRulesetWhenSomeoneHasAnError() { + PMDConfiguration config = new PMDConfiguration(); + config.addRuleSet("rulesets/dummy/basic.xml"); + config.addRuleSet("rulesets/xxxe/notaruleset.xml"); + try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + assertThat(pmd.rulesets(), hasSize(1)); // no failure + assertThat(pmd.getReporter().numErrors(), equalTo(1)); + } + } + +} diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java similarity index 84% rename from pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java rename to pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java index d765920004..99c321530f 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java @@ -4,9 +4,13 @@ package net.sourceforge.pmd; +import static net.sourceforge.pmd.util.CollectionUtil.listOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -16,6 +20,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Properties; import org.junit.Assert; @@ -29,7 +34,7 @@ import net.sourceforge.pmd.renderers.CSVRenderer; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.util.ClasspathClassLoader; -public class ConfigurationTest { +public class PmdConfigurationTest { @Rule public TemporaryFolder folder = new TemporaryFolder(); @@ -37,7 +42,7 @@ public class ConfigurationTest { @Test public void testSuppressMarker() { PMDConfiguration configuration = new PMDConfiguration(); - assertEquals("Default suppress marker", PMD.SUPPRESS_MARKER, configuration.getSuppressMarker()); + assertEquals("Default suppress marker", PMDConfiguration.DEFAULT_SUPPRESS_MARKER, configuration.getSuppressMarker()); configuration.setSuppressMarker("CUSTOM_MARKER"); assertEquals("Changed suppress marker", "CUSTOM_MARKER", configuration.getSuppressMarker()); } @@ -51,10 +56,10 @@ public class ConfigurationTest { } @Test - public void testClassLoader() throws IOException { + public void testClassLoader() { PMDConfiguration configuration = new PMDConfiguration(); assertEquals("Default ClassLoader", PMDConfiguration.class.getClassLoader(), configuration.getClassLoader()); - configuration.prependClasspath("some.jar"); + configuration.prependAuxClasspath("some.jar"); assertEquals("Prepended ClassLoader class", ClasspathClassLoader.class, configuration.getClassLoader().getClass()); URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs(); @@ -68,31 +73,31 @@ public class ConfigurationTest { } @Test - public void auxClasspathWithRelativeFileEmpty() throws IOException { + public void auxClasspathWithRelativeFileEmpty() { String relativeFilePath = "src/test/resources/net/sourceforge/pmd/cli/auxclasspath-empty.cp"; PMDConfiguration configuration = new PMDConfiguration(); - configuration.prependClasspath("file:" + relativeFilePath); + configuration.prependAuxClasspath("file:" + relativeFilePath); URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs(); Assert.assertEquals(0, urls.length); } @Test - public void auxClasspathWithRelativeFileEmpty2() throws IOException { + public void auxClasspathWithRelativeFileEmpty2() { String relativeFilePath = "./src/test/resources/net/sourceforge/pmd/cli/auxclasspath-empty.cp"; PMDConfiguration configuration = new PMDConfiguration(); - configuration.prependClasspath("file:" + relativeFilePath); + configuration.prependAuxClasspath("file:" + relativeFilePath); URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs(); Assert.assertEquals(0, urls.length); } @Test - public void auxClasspathWithRelativeFile() throws IOException, URISyntaxException { + public void auxClasspathWithRelativeFile() throws URISyntaxException { final String FILE_SCHEME = "file"; String currentWorkingDirectory = new File("").getAbsoluteFile().toURI().getPath(); String relativeFilePath = "src/test/resources/net/sourceforge/pmd/cli/auxclasspath.cp"; PMDConfiguration configuration = new PMDConfiguration(); - configuration.prependClasspath("file:" + relativeFilePath); + configuration.prependAuxClasspath("file:" + relativeFilePath); URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs(); URI[] uris = new URI[urls.length]; for (int i = 0; i < urls.length; i++) { @@ -111,6 +116,30 @@ public class ConfigurationTest { Assert.assertArrayEquals(expectedUris, uris); } + @Test + public void testRuleSetsLegacy() { + PMDConfiguration configuration = new PMDConfiguration(); + assertNull("Default RuleSets", configuration.getRuleSets()); + configuration.setRuleSets("/rulesets/basic.xml"); + assertEquals("Changed RuleSets", "/rulesets/basic.xml", configuration.getRuleSets()); + configuration.setRuleSets((String) null); + assertNull(configuration.getRuleSets()); + } + + @Test + public void testRuleSets() { + PMDConfiguration configuration = new PMDConfiguration(); + assertThat(configuration.getRuleSetPaths(), empty()); + configuration.setRuleSets(listOf("/rulesets/basic.xml")); + assertEquals(listOf("/rulesets/basic.xml"), configuration.getRuleSetPaths()); + configuration.addRuleSet("foo.xml"); + assertEquals(listOf("/rulesets/basic.xml", "foo.xml"), configuration.getRuleSetPaths()); + configuration.setRuleSets(Collections.emptyList()); + assertThat(configuration.getRuleSetPaths(), empty()); + // should be addable even though we set it to an unmodifiable empty list + configuration.addRuleSet("foo.xml"); + assertEquals(listOf("foo.xml"), configuration.getRuleSetPaths()); + } @Test public void testMinimumPriority() { @@ -232,7 +261,7 @@ public class ConfigurationTest { } @Test - public void testAnalysisCacheLocation() throws IOException { + public void testAnalysisCacheLocation() { final PMDConfiguration configuration = new PMDConfiguration(); configuration.setAnalysisCacheLocation(null); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java index c7bfe16b0c..81acee181d 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java @@ -91,7 +91,7 @@ public class PMDTaskTest { try (InputStream in = new FileInputStream("target/pmd-ant-test.txt")) { String actual = IOUtils.toString(in, StandardCharsets.UTF_8); // remove any trailing newline - actual = actual.replaceAll("\n|\r", ""); + actual = actual.trim(); Assert.assertEquals("sample.dummy:1:\tSampleXPathRule:\tTest Rule 2", actual); } } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java index 78bbb13f4f..a81cbb55b5 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java @@ -82,21 +82,21 @@ public class FileAnalysisCacheTest { } @Test - public void testStoreCreatesFile() { + public void testStoreCreatesFile() throws Exception { final FileAnalysisCache cache = new FileAnalysisCache(unexistingCacheFile); cache.persist(); assertTrue("Cache file doesn't exist after store", unexistingCacheFile.exists()); } @Test - public void testStoreOnUnwritableFileShouldntThrow() { + public void testStoreOnUnwritableFileShouldntThrow() throws IOException { emptyCacheFile.setWritable(false); final FileAnalysisCache cache = new FileAnalysisCache(emptyCacheFile); cache.persist(); } @Test - public void testStorePersistsFilesWithViolations() { + public void testStorePersistsFilesWithViolations() throws IOException { final FileAnalysisCache cache = new FileAnalysisCache(newCacheFile); cache.checkValidity(mock(RuleSets.class), mock(ClassLoader.class)); cache.isUpToDate(sourceFile); @@ -107,7 +107,9 @@ public class FileAnalysisCacheTest { when(rule.getLanguage()).thenReturn(mock(Language.class)); when(rv.getRule()).thenReturn(rule); - cache.startFileAnalysis(mock(DataSource.class)).onRuleViolation(rv); + DataSource ds = mock(DataSource.class); + when(ds.getNiceFileName(false, "")).thenReturn(sourceFile.getPath()); + cache.startFileAnalysis(ds).onRuleViolation(rv); cache.persist(); final FileAnalysisCache reloadedCache = new FileAnalysisCache(newCacheFile); @@ -120,7 +122,7 @@ public class FileAnalysisCacheTest { } @Test - public void testCacheValidityWithNoChanges() { + public void testCacheValidityWithNoChanges() throws IOException { final RuleSets rs = mock(RuleSets.class); final ClassLoader cl = mock(ClassLoader.class); @@ -150,7 +152,7 @@ public class FileAnalysisCacheTest { } @Test - public void testRulesetChangeInvalidatesCache() { + public void testRulesetChangeInvalidatesCache() throws IOException { final RuleSets rs = mock(RuleSets.class); final ClassLoader cl = mock(ClassLoader.class); @@ -367,7 +369,7 @@ public class FileAnalysisCacheTest { } private void setupCacheWithFiles(final File cacheFile, final RuleSets ruleSets, - final ClassLoader classLoader, final File... files) { + final ClassLoader classLoader, final File... files) throws IOException { // Setup a cache file with an entry for an empty Source.java with no violations final FileAnalysisCache cache = new FileAnalysisCache(cacheFile); cache.checkValidity(ruleSets, classLoader); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java index ed7078f2af..47106eb626 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.cli; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsStringIgnoringCase; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -19,6 +20,7 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import org.apache.commons.io.IOUtils; import org.hamcrest.Matcher; import org.junit.AfterClass; import org.junit.Before; @@ -124,6 +126,19 @@ public class CoreCliTest { assertTrue("Report file should have been created", Files.exists(reportFile)); } + @Test + public void testFileCollectionWithUnknownFiles() throws IOException { + Path reportFile = tempRoot().resolve("out/reportFile.txt"); + Files.createFile(srcDir.resolve("foo.not_analysable")); + assertFalse("Report file should not exist", Files.exists(reportFile)); + + runPmdSuccessfully("--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET, "--report-file", reportFile, "--debug"); + + assertTrue("Report file should have been created", Files.exists(reportFile)); + String reportText = IOUtils.toString(Files.newBufferedReader(reportFile, StandardCharsets.UTF_8)); + assertThat(reportText, not(containsStringIgnoringCase("error"))); + } + @Test public void testNonExistentReportFileDeprecatedOptions() { Path reportFile = tempRoot().resolve("out/reportFile.txt"); @@ -180,13 +195,13 @@ public class CoreCliTest { @Test public void debugLogging() { runPmdSuccessfully("--debug", "--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET); - errStreamCaptor.getLog().contains("[main] DEBUG net.sourceforge.pmd.PMD - Log level is at DEBUG"); + assertThat(errStreamCaptor.getLog(), containsString("[main] INFO net.sourceforge.pmd.PMD - Log level is at TRACE")); } @Test public void defaultLogging() { runPmdSuccessfully("--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET); - errStreamCaptor.getLog().contains("[main] INFO net.sourceforge.pmd.PMD - Log level is at INFO"); + assertThat(errStreamCaptor.getLog(), containsString("[main] INFO net.sourceforge.pmd.PMD - Log level is at INFO")); } @@ -240,8 +255,8 @@ public class CoreCliTest { } private static void runPmd(int expectedExitCode, Object[] args) { - int actualExitCode = PMD.run(argsToString(args)); - assertEquals("Exit code", expectedExitCode, actualExitCode); + StatusCode actualExitCode = PMD.runPmd(argsToString(args)); + assertEquals("Exit code", expectedExitCode, actualExitCode.toInt()); } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java index d93dbd738b..e0e5d2d8e6 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java @@ -4,93 +4,85 @@ package net.sourceforge.pmd.cli; -import java.io.IOException; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.hasSize; -import org.junit.Assert; +import java.io.IOException; +import java.util.List; + +import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Test; -import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; -import net.sourceforge.pmd.lang.DummyLanguageModule; -import net.sourceforge.pmd.lang.Language; -import net.sourceforge.pmd.util.datasource.DataSource; +import net.sourceforge.pmd.internal.util.FileCollectionUtil; +import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; +import net.sourceforge.pmd.lang.document.FileCollector; +import net.sourceforge.pmd.lang.document.TextFile; +import net.sourceforge.pmd.util.log.internal.NoopReporter; public class PMDFilelistTest { + + private static @NonNull FileCollector newCollector() { + return FileCollector.newCollector(new LanguageVersionDiscoverer(), new NoopReporter()); + } + @Test public void testGetApplicableFiles() throws IOException { - Set languages = new HashSet<>(); - languages.add(new DummyLanguageModule()); + FileCollector collector = newCollector(); - PMDConfiguration configuration = new PMDConfiguration(); - configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist.txt"); + FileCollectionUtil.collectFileList(collector, "src/test/resources/net/sourceforge/pmd/cli/filelist.txt"); - List applicableFiles = PMD.getApplicableFiles(configuration, languages); - Assert.assertEquals(2, applicableFiles.size()); - Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile.dummy")); - Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("anotherfile.dummy")); + List applicableFiles = collector.getCollectedFiles(); + assertThat(applicableFiles, hasSize(2)); + assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy")); + assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy")); } @Test public void testGetApplicableFilesMultipleLines() throws IOException { - Set languages = new HashSet<>(); - languages.add(new DummyLanguageModule()); + FileCollector collector = newCollector(); - PMDConfiguration configuration = new PMDConfiguration(); - configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist2.txt"); + FileCollectionUtil.collectFileList(collector, "src/test/resources/net/sourceforge/pmd/cli/filelist2.txt"); - List applicableFiles = PMD.getApplicableFiles(configuration, languages); - Assert.assertEquals(3, applicableFiles.size()); - Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile.dummy")); - Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("anotherfile.dummy")); - Assert.assertTrue(applicableFiles.get(2).getNiceFileName(false, "").endsWith("somefile.dummy")); + List applicableFiles = collector.getCollectedFiles(); + assertThat(applicableFiles, hasSize(3)); + assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy")); + assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy")); + assertThat(applicableFiles.get(2).getPathId(), endsWith("somefile.dummy")); } @Test - public void testGetApplicatbleFilesWithIgnores() throws IOException { - Set languages = new HashSet<>(); - languages.add(new DummyLanguageModule()); + public void testGetApplicableFilesWithIgnores() throws IOException { + FileCollector collector = newCollector(); PMDConfiguration configuration = new PMDConfiguration(); configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist3.txt"); configuration.setIgnoreFilePath("src/test/resources/net/sourceforge/pmd/cli/ignorelist.txt"); + FileCollectionUtil.collectFiles(configuration, collector); - List applicableFiles = PMD.getApplicableFiles(configuration, languages); - Assert.assertEquals(2, applicableFiles.size()); - Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile2.dummy")); - Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("somefile4.dummy")); + List applicableFiles = collector.getCollectedFiles(); + assertThat(applicableFiles, hasSize(2)); + assertThat(applicableFiles.get(0).getPathId(), endsWith("somefile2.dummy")); + assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile4.dummy")); } @Test - public void testGetApplicatbleFilesWithDirAndIgnores() throws IOException { - Set languages = new HashSet<>(); - languages.add(new DummyLanguageModule()); + public void testGetApplicableFilesWithDirAndIgnores() throws IOException { PMDConfiguration configuration = new PMDConfiguration(); configuration.setInputPaths("src/test/resources/net/sourceforge/pmd/cli/src"); configuration.setIgnoreFilePath("src/test/resources/net/sourceforge/pmd/cli/ignorelist.txt"); - List applicableFiles = PMD.getApplicableFiles(configuration, languages); - Assert.assertEquals(4, applicableFiles.size()); - Collections.sort(applicableFiles, new Comparator() { - @Override - public int compare(DataSource o1, DataSource o2) { - if (o1 == null && o2 != null) { - return -1; - } else if (o1 != null && o2 == null) { - return 1; - } else { - return o1.getNiceFileName(false, "").compareTo(o2.getNiceFileName(false, "")); - } - } - }); - Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("anotherfile.dummy")); - Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("somefile.dummy")); - Assert.assertTrue(applicableFiles.get(2).getNiceFileName(false, "").endsWith("somefile2.dummy")); - Assert.assertTrue(applicableFiles.get(3).getNiceFileName(false, "").endsWith("somefile4.dummy")); + FileCollector collector = newCollector(); + FileCollectionUtil.collectFiles(configuration, collector); + + List applicableFiles = collector.getCollectedFiles(); + assertThat(applicableFiles, hasSize(4)); + assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy")); + assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy")); + assertThat(applicableFiles.get(2).getPathId(), endsWith("somefile2.dummy")); + assertThat(applicableFiles.get(3).getPathId(), endsWith("somefile4.dummy")); } + } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java new file mode 100644 index 0000000000..191c004db0 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java @@ -0,0 +1,144 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import static net.sourceforge.pmd.util.CollectionUtil.listOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; + +/** + * @author ClΓ©ment Fournier + */ +public class FileCollectorTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testAddFile() throws IOException { + Path root = tempFolder.getRoot().toPath(); + Path foo = newFile(root, "foo.dummy"); + Path bar = newFile(root, "bar.unknown"); + + FileCollector collector = newCollector(); + + assertTrue("should be dummy language", collector.addFile(foo)); + assertFalse("should be unknown language", collector.addFile(bar)); + + assertCollected(collector, listOf("foo.dummy")); + } + + @Test + public void testAddFileForceLanguage() throws IOException { + Path root = tempFolder.getRoot().toPath(); + Path bar = newFile(root, "bar.unknown"); + + Language dummy = LanguageRegistry.findLanguageByTerseName("dummy"); + + FileCollector collector = newCollector(dummy.getDefaultVersion()); + + assertTrue("should be unknown language", collector.addFile(bar, dummy)); + assertCollected(collector, listOf("bar.unknown")); + assertNoErrors(collector); + } + + @Test + public void testAddFileNotExists() { + Path root = tempFolder.getRoot().toPath(); + + FileCollector collector = newCollector(); + + assertFalse(collector.addFile(root.resolve("does_not_exist.dummy"))); + assertEquals(1, collector.getReporter().numErrors()); + } + + @Test + public void testAddFileNotAFile() throws IOException { + Path root = tempFolder.getRoot().toPath(); + Path dir = root.resolve("src"); + Files.createDirectories(dir); + + FileCollector collector = newCollector(); + assertFalse(collector.addFile(dir)); + assertEquals(1, collector.getReporter().numErrors()); + } + + @Test + public void testAddDirectory() throws IOException { + Path root = tempFolder.getRoot().toPath(); + newFile(root, "src/foo.dummy"); + newFile(root, "src/bar.unknown"); + newFile(root, "src/x/bar.dummy"); + + FileCollector collector = newCollector(); + + collector.addDirectory(root.resolve("src")); + + assertCollected(collector, listOf("src/foo.dummy", "src/x/bar.dummy")); + } + + @Test + public void testRelativize() throws IOException { + String displayName = FileCollector.getDisplayName(Paths.get("a", "b", "c"), listOf(Paths.get("a").toString())); + assertEquals(displayName, Paths.get("b", "c").toString()); + } + + private Path newFile(Path root, String path) throws IOException { + Path resolved = root.resolve(path); + Files.createDirectories(resolved.getParent()); + Files.createFile(resolved); + return resolved; + } + + private void assertCollected(FileCollector collector, List relPaths) { + Map actual = new LinkedHashMap<>(); + for (TextFile file : collector.getCollectedFiles()) { + actual.put(file.getDisplayName(), file.getLanguageVersion().getTerseName()); + } + + relPaths = new ArrayList<>(relPaths); + for (int i = 0; i < relPaths.size(); i++) { + // normalize, we want display names to be platform-specific + relPaths.set(i, relPaths.get(i).replace('/', File.separatorChar)); + } + + assertEquals(relPaths, new ArrayList<>(actual.keySet())); + } + + private void assertNoErrors(FileCollector collector) { + assertEquals("No errors expected", 0, collector.getReporter().numErrors()); + } + + private FileCollector newCollector() { + return newCollector(null); + } + + private FileCollector newCollector(LanguageVersion forcedVersion) { + LanguageVersionDiscoverer discoverer = new LanguageVersionDiscoverer(forcedVersion); + FileCollector collector = FileCollector.newCollector(discoverer, new TestMessageReporter()); + collector.relativizeWith(tempFolder.getRoot().getAbsolutePath()); + return collector; + } +} diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java new file mode 100644 index 0000000000..8689530e30 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java @@ -0,0 +1,23 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.document; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter; + +/** + * @author ClΓ©ment Fournier + */ +public class TestMessageReporter extends SimpleMessageReporter { + + private static final Logger LOG = LoggerFactory.getLogger(TestMessageReporter.class.getName()); + + public TestMessageReporter() { + super(LOG); + setLevel(null); + } +} diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java index 6d749e5775..d10f676ca5 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java @@ -4,7 +4,6 @@ package net.sourceforge.pmd.processor; -import static net.sourceforge.pmd.util.CollectionUtil.listOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -13,15 +12,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.List; - import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Test; import org.mockito.Mockito; import net.sourceforge.pmd.FooRule; -import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; +import net.sourceforge.pmd.PmdAnalysis; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; @@ -31,20 +28,11 @@ import net.sourceforge.pmd.lang.ast.FileAnalysisException; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.reporting.GlobalAnalysisListener; import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener; -import net.sourceforge.pmd.util.datasource.DataSource; public class GlobalListenerTest { static final int NUM_DATA_SOURCES = 3; - static List mockDataSources() { - return listOf( - DataSource.forString("abc", "fname1.dummy"), - DataSource.forString("abcd", "fname2.dummy"), - DataSource.forString("abcd", "fname21.dummy") - ); - } - @Test public void testViolationCounter() throws Exception { @@ -89,7 +77,7 @@ public class GlobalListenerTest { runPmd(config, GlobalAnalysisListener.noop(), rule); verify(mockCache).checkValidity(any(), any()); - verify(mockCache).persist(); + verify(mockCache, times(1)).persist(); verify(mockCache, times(NUM_DATA_SOURCES)).isUpToDate(any()); } @@ -105,7 +93,7 @@ public class GlobalListenerTest { // cache methods are called regardless verify(mockCache).checkValidity(any(), any()); - verify(mockCache).persist(); + verify(mockCache, times(1)).persist(); verify(mockCache, times(NUM_DATA_SOURCES)).isUpToDate(any()); } @@ -127,7 +115,7 @@ public class GlobalListenerTest { // cache methods are called regardless verify(mockCache).checkValidity(any(), any()); - verify(mockCache).persist(); + verify(mockCache, times(1)).persist(); verify(mockCache, times(1)).isUpToDate(any()); } @@ -140,16 +128,14 @@ public class GlobalListenerTest { return config; } - private void runPmd(PMDConfiguration config, GlobalAnalysisListener listener, Rule rule) throws Exception { - try { - PMD.processFiles( - config, - listOf(RuleSet.forSingleRule(rule)), - mockDataSources(), - listener - ); - } finally { - listener.close(); + private void runPmd(PMDConfiguration config, GlobalAnalysisListener listener, Rule rule) { + try (PmdAnalysis pmd = PmdAnalysis.create(config)) { + pmd.addRuleSet(RuleSet.forSingleRule(rule)); + pmd.files().addSourceFile("fname1.dummy", "abc"); + pmd.files().addSourceFile("fname2.dummy", "abcd"); + pmd.files().addSourceFile("fname21.dummy", "abcd"); + pmd.addListener(listener); + pmd.performAnalysis(); } } @@ -159,7 +145,7 @@ public class GlobalListenerTest { @Override public void apply(Node node, RuleContext ctx) { if (node.getAstInfo().getFileName().contains("1")) { - addViolation(ctx, node); + ctx.addViolation(node); } } } diff --git a/pmd-core/src/test/resources/sample-source/dummy/foo.dummy b/pmd-core/src/test/resources/sample-source/dummy/foo.dummy new file mode 100644 index 0000000000..4b1d189a0f --- /dev/null +++ b/pmd-core/src/test/resources/sample-source/dummy/foo.dummy @@ -0,0 +1 @@ +A dummy file. diff --git a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java index 51b6b34992..5def16ed3b 100644 --- a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java +++ b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java @@ -108,7 +108,7 @@ public class BinaryDistributionIT extends AbstractBinaryDistributionTest { result = PMDExecutor.runPMD(tempDir, "-d", srcDir, "-R", "src/test/resources/rulesets/sample-ruleset.xml", "-r", folder.newFile().toString(), "--debug"); result.assertExecutionResult(4); - result.assertErrorOutputContains("[main] DEBUG net.sourceforge.pmd.PMD - Log level is at DEBUG"); + result.assertErrorOutputContains("[main] INFO net.sourceforge.pmd.PMD - Log level is at TRACE"); } @Test diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java index 892f1b82ae..016f208fd6 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java @@ -4,20 +4,18 @@ package net.sourceforge.pmd.cli; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.util.regex.Pattern; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import org.junit.AfterClass; -import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.rules.TestRule; +import net.sourceforge.pmd.PMD.StatusCode; import net.sourceforge.pmd.internal.Slf4jSimpleConfiguration; -import net.sourceforge.pmd.util.FileUtil; /** * @author Romain Pelisse <belaran@gmail.com> @@ -35,64 +33,65 @@ public class CLITest extends BaseCLITest { Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(null); } + @Before + public void setupLogging() { + Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(null); + } + + @Test public void minimalArgs() { - String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/bestpractices.xml,category/java/design.xml", }; - runTest(args, "minimalArgs"); + runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/bestpractices.xml,category/java/design.xml"); } @Test public void minimumPriority() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-min", "1", }; - runTest(args, "minimumPriority"); + runTest(args); } @Test public void usingDebug() { - String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-debug", }; - runTest(args, "minimalArgsWithDebug"); + runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-debug"); } @Test public void usingDebugLongOption() { - String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "--debug", }; - runTest(args, "minimalArgsWithDebug"); + runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "--debug"); } @Test public void changeJavaVersion() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-version", "1.5", "-language", - "java", "-debug", }; - String resultFilename = runTest(args, "chgJavaVersion"); - assertTrue("Invalid Java version", - FileUtil.findPatternInFile(new File(resultFilename), "Using Java version: Java 1.5")); + "java", "--debug", }; + String log = runTest(args); + assertThat(log, containsPattern("Adding file .*\\.java \\(lang: java 1\\.5\\)")); } @Test public void exitStatusNoViolations() { - String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", }; - runTest(args, "exitStatusNoViolations"); + runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml"); } @Test public void exitStatusWithViolations() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", }; - String resultFilename = runTest(args, "exitStatusWithViolations", 4); - assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if")); + String log = runTest(StatusCode.VIOLATIONS_FOUND, args); + assertThat(log, containsString("Avoid empty if")); } @Test public void exitStatusWithViolationsAndWithoutFailOnViolations() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", "-failOnViolation", "false", }; - String resultFilename = runTest(args, "exitStatusWithViolationsAndWithoutFailOnViolations", 0); - assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if")); + String log = runTest(StatusCode.OK, args); + assertThat(log, containsString("Avoid empty if")); } @Test public void exitStatusWithViolationsAndWithoutFailOnViolationsLongOption() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", "--fail-on-violation", "false", }; - String resultFilename = runTest(args, "exitStatusWithViolationsAndWithoutFailOnViolations", 0); - assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if")); + String log = runTest(StatusCode.OK, args); + assertThat(log, containsString("Avoid empty if")); } /** @@ -101,12 +100,9 @@ public class CLITest extends BaseCLITest { @Test public void testWrongRuleset() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml", }; - String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; - createTestOutputFile(filename); - runPMDWith(args); - Assert.assertEquals(1, getStatusCode()); - assertTrue(FileUtil.findPatternInFile(new File(filename), - "Can't find resource 'category/java/designn.xml' for rule 'null'." + " Make sure the resource is a valid file")); + String log = runTest(StatusCode.ERROR, args); + assertThat(log, containsString("Can't find resource 'category/java/designn.xml' for rule 'null'." + + " Make sure the resource is a valid file")); } /** @@ -115,12 +111,9 @@ public class CLITest extends BaseCLITest { @Test public void testWrongRulesetWithRulename() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml/UseCollectionIsEmpty", }; - String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; - createTestOutputFile(filename); - runPMDWith(args); - Assert.assertEquals(1, getStatusCode()); - assertTrue(FileUtil.findPatternInFile(new File(filename), - "Can't find resource 'category/java/designn.xml' for rule " + "'UseCollectionIsEmpty'.")); + String log = runTest(StatusCode.ERROR, args); + assertThat(log, containsString("Can't find resource 'category/java/designn.xml' for rule " + + "'UseCollectionIsEmpty'.")); } /** @@ -129,11 +122,8 @@ public class CLITest extends BaseCLITest { @Test public void testWrongRulename() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml/ThisRuleDoesNotExist", }; - String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; - createTestOutputFile(filename); - runPMDWith(args); - Assert.assertEquals(1, getStatusCode()); - assertTrue(FileUtil.findPatternInFile(new File(filename), Pattern - .quote("No rules found. Maybe you misspelled a rule name?" + " (category/java/design.xml/ThisRuleDoesNotExist)"))); + String log = runTest(StatusCode.OK, args); + assertThat(log, containsString("No rules found. Maybe you misspelled a rule name?" + + " (category/java/design.xml/ThisRuleDoesNotExist)")); } } diff --git a/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java b/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java index a051f56a6f..718995de62 100644 --- a/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java +++ b/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java @@ -4,14 +4,10 @@ package net.sourceforge.pmd.cli; -import static org.junit.Assert.assertTrue; - -import java.io.File; +import static org.hamcrest.MatcherAssert.assertThat; import org.junit.Test; -import net.sourceforge.pmd.util.FileUtil; - /** * @author Romain Pelisse <belaran@gmail.com> * @@ -20,9 +16,8 @@ public class CLITest extends BaseCLITest { @Test public void useEcmaScript() { String[] args = { "-d", SOURCE_FOLDER, "-f", "xml", "-R", "ecmascript-basic", "-l", - "ecmascript", "-debug", }; - String resultFilename = runTest(args, "useEcmaScript"); - assertTrue("Invalid JavaScript version", - FileUtil.findPatternInFile(new File(resultFilename), "Using Ecmascript version: Ecmascript ES6")); + "ecmascript", "--debug", }; + String log = runTest(args); + assertThat(log, containsPattern("Adding file .*\\.js \\(lang: ecmascript ES6\\)")); } } diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt index bedcc9adcf..268bcd9745 100644 --- a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt +++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt @@ -225,5 +225,12 @@ abstract class BaseParsingHelper, T : RootNode } fun executeRuleOnResource(rule: Rule, resourcePath: String): Report = - executeRule(rule, readResource(resourcePath)) + executeRule(rule, code = readResource(resourcePath)) + + fun executeRuleOnFile(rule: Rule, path: Path): Report = + executeRule( + rule, + code = Files.newBufferedReader(path).readText(), + fileName = path.toString() + ) } diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java b/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java index 1c8645fc78..54e9739159 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java @@ -4,20 +4,28 @@ package net.sourceforge.pmd.cli; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; +import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.PMD.StatusCode; +import net.sourceforge.pmd.internal.util.AssertionUtil; /** * @author Romain Pelisse <belaran@gmail.com> @@ -72,25 +80,79 @@ public abstract class BaseCLITest { } } + /** + * @deprecated Use {@link #runTest(String...)}, note that + * it returns the log while this returns the name of a file containing the log. + */ + @Deprecated protected String runTest(String[] args, String testname) { return runTest(args, testname, 0); } + /** + * @deprecated Use {@link #runTest(StatusCode, String...)}, note that + * it returns the log while this returns the name of a file containing the log. + */ + @Deprecated protected String runTest(String[] args, String testname, int expectedExitCode) { String filename = TEST_OUPUT_DIRECTORY + testname + ".txt"; long start = System.currentTimeMillis(); createTestOutputFile(filename); System.out.println("Start running test " + testname); - runPMDWith(args); - checkStatusCode(expectedExitCode); + StatusCode statusCode = PMD.runPmd(args); + assertEquals(expectedExitCode, statusCode.toInt()); System.out.println("Test finished successfully after " + (System.currentTimeMillis() - start) + "ms."); return filename; } + /** + * Returns the log output. + */ + protected String runTest(String... args) { + return runTest(StatusCode.OK, args); + } + + /** + * Returns the log output. + * + * @deprecated Use {@link #runTest(StatusCode, String...)} + */ + @Deprecated + protected String runTest(int expectedExitCode, String... args) { + switch (expectedExitCode) { + case 0: + return runTest(StatusCode.OK, args); + case 1: + return runTest(StatusCode.ERROR, args); + case 4: + return runTest(StatusCode.VIOLATIONS_FOUND, args); + default: + throw AssertionUtil.shouldNotReachHere("unknown status code " + expectedExitCode); + } + } + + protected String runTest(StatusCode expectedExitCode, String... args) { + ByteArrayOutputStream console = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(console); + System.setOut(out); + System.setErr(out); + StatusCode statusCode = PMD.runPmd(args); + assertEquals(expectedExitCode, statusCode); + return console.toString(); + } + + /** + * @deprecated Use {@link #runTest(StatusCode, String...)} + */ + @Deprecated protected void runPMDWith(String[] args) { PMD.main(args); } + /** + * @deprecated Use {@link #runTest(StatusCode, String...)} instead of checking the return code manually + */ + @Deprecated protected void checkStatusCode(int expectedExitCode) { int statusCode = getStatusCode(); if (statusCode != expectedExitCode) { @@ -98,7 +160,28 @@ public abstract class BaseCLITest { } } + /** + * @deprecated Use {@link #runTest(StatusCode, String...)} instead + * of checking the return code manually + */ + @Deprecated protected int getStatusCode() { return Integer.parseInt(System.getProperty(PMDCommandLineInterface.STATUS_CODE_PROPERTY)); } + + public static Matcher containsPattern(final String regex) { + return new BaseMatcher() { + final Pattern pattern = Pattern.compile(regex); + + @Override + public void describeTo(Description description) { + description.appendText("a string containing the pattern '" + this.pattern + "'"); + } + + @Override + public boolean matches(Object o) { + return o instanceof String && pattern.matcher((String) o).find(); + } + }; + } } diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java index d9ceed4b85..51d4de0543 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java @@ -95,11 +95,7 @@ public abstract class RuleTst { private ClassLoader makeClassPathClassLoader() { final ClassLoader classpathClassLoader; PMDConfiguration config = new PMDConfiguration(); - try { - config.prependClasspath("."); - } catch (IOException ignored) { - - } + config.prependAuxClasspath("."); classpathClassLoader = config.getClassLoader(); return classpathClassLoader; } diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java index 3cd0019f9b..1f76408722 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java @@ -4,28 +4,20 @@ package net.sourceforge.pmd.lang.vf.rule.security; -import static net.sourceforge.pmd.util.CollectionUtil.listOf; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import java.util.List; import org.junit.Test; -import net.sourceforge.pmd.PMD; -import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; import net.sourceforge.pmd.Rule; -import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.vf.VFTestUtils; import net.sourceforge.pmd.lang.vf.ast.VfParsingHelper; import net.sourceforge.pmd.testframework.PmdRuleTst; -import net.sourceforge.pmd.util.datasource.FileDataSource; public class VfUnescapeElTest extends PmdRuleTst { public static final String EXPECTED_RULE_MESSAGE = "Avoid unescaped user controlled content in EL"; @@ -34,7 +26,7 @@ public class VfUnescapeElTest extends PmdRuleTst { * Verify that CustomFields stored in sfdx project format are correctly parsed */ @Test - public void testSfdxCustomFields() throws Exception { + public void testSfdxCustomFields() { Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) .resolve("StandardAccount.page"); @@ -47,7 +39,7 @@ public class VfUnescapeElTest extends PmdRuleTst { RuleViolation ruleViolation = ruleViolations.get(i); assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription()); int expectedLineNumber = firstLineWithErrors + i; - if ((ruleViolations.size() + firstLineWithErrors - 1) == expectedLineNumber) { + if (ruleViolations.size() + firstLineWithErrors - 1 == expectedLineNumber) { // The last line has two errors on the same page expectedLineNumber = expectedLineNumber - 1; } @@ -59,7 +51,7 @@ public class VfUnescapeElTest extends PmdRuleTst { * Verify that CustomFields stored in mdapi format are correctly parsed */ @Test - public void testMdapiCustomFields() throws Exception { + public void testMdapiCustomFields() { Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.MDAPI, VFTestUtils.MetadataType.Vf).resolve("StandardAccount.page"); Report report = runRule(vfPagePath); @@ -77,7 +69,7 @@ public class VfUnescapeElTest extends PmdRuleTst { * Tests a page with a single Apex controller */ @Test - public void testApexController() throws Exception { + public void testApexController() { Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf).resolve("ApexController.page"); Report report = runRule(vfPagePath); @@ -96,7 +88,7 @@ public class VfUnescapeElTest extends PmdRuleTst { * Tests a page with a standard controller and two Apex extensions */ @Test - public void testExtensions() throws Exception { + public void testExtensions() { Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) .resolve(Paths.get("StandardAccountWithExtensions.page")); @@ -114,32 +106,8 @@ public class VfUnescapeElTest extends PmdRuleTst { /** * Runs a rule against a Visualforce page on the file system. */ - private Report runRule(Path vfPagePath) throws Exception { - Node node = VfParsingHelper.DEFAULT.parseFile(vfPagePath); - assertNotNull(node); - - PMDConfiguration config = new PMDConfiguration(); - config.setIgnoreIncrementalAnalysis(true); - // simple class loader, that doesn't delegate to parent. - // this allows us in the tests to simulate PMD run without - // auxclasspath, not even the classes from the test dependencies - // will be found. - config.setClassLoader(new ClassLoader() { - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith("java.") || name.startsWith("javax.")) { - return super.loadClass(name, resolve); - } - throw new ClassNotFoundException(name); - } - }); + private Report runRule(Path vfPagePath) { Rule rule = findRule("category/vf/security.xml", "VfUnescapeEl"); - - return PMD.processFiles( - config, - listOf(RuleSet.forSingleRule(rule)), - listOf(new FileDataSource(vfPagePath.toAbsolutePath().toFile())), - Collections.emptyList() - ); + return VfParsingHelper.DEFAULT.executeRuleOnFile(rule, vfPagePath); } } diff --git a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java index ee5722798b..14e717b237 100644 --- a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java +++ b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java @@ -4,18 +4,17 @@ package net.sourceforge.pmd.lang.xml; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; +import static net.sourceforge.pmd.util.CollectionUtil.listOf; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Test; +import net.sourceforge.pmd.PMD.StatusCode; import net.sourceforge.pmd.cli.BaseCLITest; public class XmlCliTest extends BaseCLITest { @@ -23,44 +22,38 @@ public class XmlCliTest extends BaseCLITest { private static final String RULE_MESSAGE = "A tags are not allowed"; private String[] createArgs(String directory, String... args) { - List arguments = new ArrayList<>(); - arguments.add("-f"); - arguments.add("text"); - arguments.add("-no-cache"); - arguments.add("-R"); - arguments.add(BASE_DIR + "/ruleset.xml"); - arguments.add("-d"); - arguments.add(BASE_DIR + directory); + List arguments = new ArrayList<>(listOf( + "-f", + "text", + "-no-cache", + "-R", + BASE_DIR + "/ruleset.xml", + "-d", + BASE_DIR + directory + )); arguments.addAll(Arrays.asList(args)); return arguments.toArray(new String[0]); } @Test public void analyzeSingleXmlWithoutForceLanguage() { - String resultFilename = runTest(createArgs("/src/file1.ext"), "analyzeSingleXmlWithoutForceLanguage", 0); - assertRuleMessage(0, resultFilename); + String log = runTest(StatusCode.OK, createArgs("/src/file1.ext")); + assertRuleMessage(0, log); } @Test public void analyzeSingleXmlWithForceLanguage() { - String resultFilename = runTest(createArgs("/src/file1.ext", "-force-language", "xml"), - "analyzeSingleXmlWithForceLanguage", 4); - assertRuleMessage(1, resultFilename); + String log = runTest(StatusCode.VIOLATIONS_FOUND, createArgs("/src/file1.ext", "-force-language", "xml")); + assertRuleMessage(1, log); } @Test public void analyzeDirectoryWithForceLanguage() { - String resultFilename = runTest(createArgs("/src/", "-force-language", "xml"), - "analyzeDirectoryWithForceLanguage", 4); - assertRuleMessage(3, resultFilename); + String log = runTest(StatusCode.VIOLATIONS_FOUND, createArgs("/src/", "-force-language", "xml")); + assertRuleMessage(3, log); } - private void assertRuleMessage(int expectedCount, String resultFilename) { - try { - String result = FileUtils.readFileToString(new File(resultFilename), StandardCharsets.UTF_8); - Assert.assertEquals(expectedCount, StringUtils.countMatches(result, RULE_MESSAGE)); - } catch (IOException e) { - throw new AssertionError(e); - } + private void assertRuleMessage(int expectedCount, String log) { + Assert.assertEquals(expectedCount, StringUtils.countMatches(log, RULE_MESSAGE)); } }