Skip to content

Commit f31cc46

Browse files
committed
Merge pull request #3417 from aidan-harding:language-detection
[core] Support forcing a specific language from the command-line #3417
2 parents 7be50ea + a33b465 commit f31cc46

File tree

14 files changed

+262
-6
lines changed

14 files changed

+262
-6
lines changed

docs/pages/pmd/userdocs/cli_reference.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ The tool comes with a rather extensive help text, simply running with `-help`!
7979
description="Path to file containing a comma delimited list of files to analyze.
8080
If this is given, then you don't need to provide `-dir`."
8181
%}
82+
{% include custom/cli_option_row.html options="-force-language"
83+
option_arg="lang"
84+
description="Force a language to be used for all input files, irrespective of
85+
filenames. When using this option, the automatic language selection
86+
by extension is disabled and all files are tried to be parsed with
87+
the given language `<lang>`. Parsing errors are ignored and unparsable files
88+
are skipped.
89+
90+
<p>This option allows to use the xml language for files, that don't
91+
use xml as extension. See [example](#analyze-other-xml-formats) below.</p>"
92+
%}
8293
{% include custom/cli_option_row.html options="-ignorelist"
8394
option_arg="filepath"
8495
description="Path to file containing a comma delimited list of files to ignore.
@@ -202,3 +213,26 @@ Example:
202213
PMD comes with many different renderers.
203214
All formats are described at [PMD Report formats](pmd_userdocs_report_formats.html)
204215

216+
## Examples
217+
218+
### Analyze other xml formats
219+
220+
If your xml language doesn't use `xml` as file extension, you can still use PMD with `-force-language`:
221+
222+
```
223+
$ ./run.sh pmd -d /home/me/src/xml-file.ext -f text -R ruleset.xml -force-language xml
224+
```
225+
226+
You can also specify a directory instead of a single file. Then all files are analyzed. In that case,
227+
parse errors are suppressed in order to reduce irrelevant noise:
228+
229+
```
230+
$ ./run.sh pmd -d /home/me/src/ -f text -R ruleset.xml -force-language xml
231+
```
232+
233+
Alternatively, you can create a filelist to only analyze files with a given extension:
234+
235+
```
236+
$ find /home/me/src -name "*.ext" > /home/me/src/filelist.txt
237+
$ ./run.sh pmd -filelist /home/me/src/filelist.txt -f text -R ruleset.xml -force-language xml
238+
```

docs/pages/release_notes.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ This release ships with 3 new Java rules.
110110
* [#3329](https://github.com/pmd/pmd/issues/3329): \[apex] ApexCRUDViolation doesn't report SOQL for loops
111111
* core
112112
* [#1603](https://github.com/pmd/pmd/issues/1603): \[core] Language version comparison
113+
* [#2133](https://github.com/pmd/pmd/issues/2133): \[xml] Allow to check Salesforce XML Metadata using XPath rules
113114
* [#3377](https://github.com/pmd/pmd/issues/3377): \[core] NPE when specifying report file in current directory in PMD CLI
114115
* [#3387](https://github.com/pmd/pmd/issues/3387): \[core] CPD should avoid unnecessary copies when running with --skip-lexical-errors
115116
* java-bestpractices
@@ -123,6 +124,16 @@ This release ships with 3 new Java rules.
123124

124125
### API Changes
125126

127+
#### PMD CLI
128+
129+
* PMD has a new CLI option `-force-language`. With that a language can be forced to be used for all input files,
130+
irrespective of filenames. When using this option, the automatic language selection by extension is disabled
131+
and all files are tried to be parsed with the given language. Parsing errors are ignored and unparsable files
132+
are skipped.
133+
134+
This option allows to use the xml language for files, that don't use xml as extension.
135+
See also the examples on [PMD CLI reference](pmd_userdocs_cli_reference.html#analyze-other-xml-formats).
136+
126137
#### Experimental APIs
127138

128139
* The AST types and APIs around Sealed Classes are not experimental anymore:
@@ -145,6 +156,7 @@ You can identify them with the `@InternalApi` annotation. You'll also get a depr
145156
* [#3373](https://github.com/pmd/pmd/pull/3373): \[apex] Add ApexCRUDViolation support for database class, inline no-arg object construction DML and inline list initialization DML - [Jonathan Wiesel](https://github.com/jonathanwiesel)
146157
* [#3385](https://github.com/pmd/pmd/pull/3385): \[core] CPD: Optimize --skip-lexical-errors option - [Woongsik Choi](https://github.com/woongsikchoi)
147158
* [#3388](https://github.com/pmd/pmd/pull/3388): \[doc] Add Code Inspector in the list of tools - [Julien Delange](https://github.com/juli1)
159+
* [#3417](https://github.com/pmd/pmd/pull/3417): \[core] Support forcing a specific language from the command-line - [Aidan Harding](https://github.com/aidan-harding)
148160

149161
{% endtocmaker %}
150162

pmd-core/src/main/java/net/sourceforge/pmd/PMD.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package net.sourceforge.pmd;
66

77
import java.io.File;
8+
import java.io.FilenameFilter;
89
import java.io.IOException;
910
import java.io.OutputStreamWriter;
1011
import java.io.Writer;
@@ -417,7 +418,7 @@ public static List<DataSource> getApplicableFiles(PMDConfiguration configuration
417418

418419
private static List<DataSource> internalGetApplicableFiles(PMDConfiguration configuration,
419420
Set<Language> languages) {
420-
LanguageFilenameFilter fileSelector = new LanguageFilenameFilter(languages);
421+
FilenameFilter fileSelector = configuration.isForceLanguageVersion() ? new AcceptAllFilenames() : new LanguageFilenameFilter(languages);
421422
List<DataSource> files = new ArrayList<>();
422423

423424
if (null != configuration.getInputPaths()) {
@@ -636,4 +637,11 @@ public int toInt() {
636637
}
637638

638639
}
640+
641+
private static class AcceptAllFilenames implements FilenameFilter {
642+
@Override
643+
public boolean accept(File dir, String name) {
644+
return true;
645+
}
646+
}
639647
}

pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public class PMDConfiguration extends AbstractConfiguration {
8888
private int threads = Runtime.getRuntime().availableProcessors();
8989
private ClassLoader classLoader = getClass().getClassLoader();
9090
private LanguageVersionDiscoverer languageVersionDiscoverer = new LanguageVersionDiscoverer();
91+
private LanguageVersion forceLanguageVersion;
9192

9293
// Rule and source file options
9394
private String ruleSets;
@@ -210,6 +211,35 @@ public LanguageVersionDiscoverer getLanguageVersionDiscoverer() {
210211
return languageVersionDiscoverer;
211212
}
212213

214+
/**
215+
* Get the LanguageVersion specified by the force-language parameter. This overrides detection based on file
216+
* extensions
217+
*
218+
* @return The LanguageVersion.
219+
*/
220+
public LanguageVersion getForceLanguageVersion() {
221+
return forceLanguageVersion;
222+
}
223+
224+
/**
225+
* Is the force-language parameter set to anything?
226+
*
227+
* @return true if ${@link #getForceLanguageVersion()} is not null
228+
*/
229+
public boolean isForceLanguageVersion() {
230+
return forceLanguageVersion != null;
231+
}
232+
233+
/**
234+
* Set the LanguageVersion specified by the force-language parameter. This overrides detection based on file
235+
* extensions
236+
*
237+
* @param forceLanguageVersion the language version
238+
*/
239+
public void setForceLanguageVersion(LanguageVersion forceLanguageVersion) {
240+
this.forceLanguageVersion = forceLanguageVersion;
241+
}
242+
213243
/**
214244
* Set the given LanguageVersion as the current default for it's Language.
215245
*

pmd-core/src/main/java/net/sourceforge/pmd/SourceCodeProcessor.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.io.Reader;
1111
import java.util.Collections;
1212
import java.util.List;
13+
import java.util.logging.Level;
14+
import java.util.logging.Logger;
1315

1416
import net.sourceforge.pmd.annotation.InternalApi;
1517
import net.sourceforge.pmd.benchmark.TimeTracker;
@@ -31,6 +33,8 @@
3133
@InternalApi
3234
public class SourceCodeProcessor {
3335

36+
private static final Logger LOG = Logger.getLogger(SourceCodeProcessor.class.getName());
37+
3438
private final PMDConfiguration configuration;
3539

3640
public SourceCodeProcessor(PMDConfiguration configuration) {
@@ -113,7 +117,11 @@ private void processSourceCodeWithoutCache(final Reader sourceCode, final RuleSe
113117
processSource(sourceCode, ruleSets, ctx);
114118
} catch (ParseException pe) {
115119
configuration.getAnalysisCache().analysisFailed(ctx.getSourceCodeFile());
116-
throw new PMDException("Error while parsing " + ctx.getSourceCodeFile(), pe);
120+
if (configuration.isForceLanguageVersion()) {
121+
LOG.log(Level.FINE, "Error while parsing " + ctx.getSourceCodeFile(), pe);
122+
} else {
123+
throw new PMDException("Error while parsing " + ctx.getSourceCodeFile(), pe);
124+
}
117125
} catch (Exception e) {
118126
configuration.getAnalysisCache().analysisFailed(ctx.getSourceCodeFile());
119127
throw new PMDException("Error while processing " + ctx.getSourceCodeFile(), e);
@@ -201,9 +209,18 @@ private void processSource(Reader sourceCode, RuleSets ruleSets, RuleContext ctx
201209
}
202210

203211
private void determineLanguage(RuleContext ctx) {
204-
// If LanguageVersion of the source file is not known, make a
205-
// determination
206-
if (ctx.getLanguageVersion() == null) {
212+
if (ctx.getLanguageVersion() != null) {
213+
// we already have a language
214+
return;
215+
}
216+
217+
// If LanguageVersion of the source file is not known, make a determination
218+
LanguageVersion forceLanguage = configuration.getForceLanguageVersion();
219+
if (forceLanguage != null) {
220+
// use force language if given
221+
ctx.setLanguageVersion(forceLanguage);
222+
} else {
223+
// otherwise determine by file extension
207224
LanguageVersion languageVersion = configuration.getLanguageVersionOfFile(ctx.getSourceCodeFilename());
208225
ctx.setLanguageVersion(languageVersion);
209226
}

pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ public class PMDParameters {
101101
@Parameter(names = { "-language", "-l" }, description = "Specify a language PMD should use.")
102102
private String language = null;
103103

104+
@Parameter(names = "-force-language", description = "Force a language to be used for all input files, irrespective of filenames.")
105+
private String forceLanguage = null;
106+
104107
@Parameter(names = "-auxclasspath",
105108
description = "Specifies the classpath for libraries used by the source code. "
106109
+ "This is used by the type resolution. The platform specific path delimiter "
@@ -214,11 +217,18 @@ public PMDConfiguration toConfiguration() {
214217
configuration.setAnalysisCacheLocation(this.cacheLocation);
215218
configuration.setIgnoreIncrementalAnalysis(this.isIgnoreIncrementalAnalysis());
216219

220+
LanguageVersion forceLangVersion = LanguageRegistry
221+
.findLanguageVersionByTerseName(this.getForceLanguage());
222+
if (forceLangVersion != null) {
223+
configuration.setForceLanguageVersion(forceLangVersion);
224+
}
225+
217226
LanguageVersion languageVersion = LanguageRegistry
218227
.findLanguageVersionByTerseName(this.getLanguage() + ' ' + this.getVersion());
219228
if (languageVersion != null) {
220229
configuration.getLanguageVersionDiscoverer().setDefaultLanguageVersion(languageVersion);
221230
}
231+
222232
try {
223233
configuration.prependClasspath(this.getAuxclasspath());
224234
} catch (IOException e) {
@@ -305,6 +315,10 @@ public String getLanguage() {
305315
return language != null ? language : LanguageRegistry.getDefaultLanguage().getTerseName();
306316
}
307317

318+
public String getForceLanguage() {
319+
return forceLanguage != null ? forceLanguage : "";
320+
}
321+
308322
public String getAuxclasspath() {
309323
return auxclasspath;
310324
}

pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public static boolean findPatternInFile(final File file, final String pattern) {
174174
/**
175175
* Reads the file, which contains the filelist. This is used for the
176176
* command line arguments --filelist/-filelist for both PMD and CPD.
177-
* The separator in the filelist is a command and/or newlines.
177+
* The separator in the filelist is a comma and/or newlines.
178178
*
179179
* @param filelist the file which contains the list of path names
180180
* @return a comma-separated list of file paths
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3+
*/
4+
5+
package net.sourceforge.pmd.lang;
6+
7+
import org.junit.Assert;
8+
import org.junit.Test;
9+
10+
import net.sourceforge.pmd.cli.PMDCommandLineInterface;
11+
import net.sourceforge.pmd.cli.PMDParameters;
12+
13+
public class LanguageParameterTest {
14+
15+
/** Test that language parameters from the CLI are correctly passed through to the PMDConfiguration. Although this is a
16+
* CLI test, it resides here to take advantage of {@link net.sourceforge.pmd.lang.DummyLanguageModule}
17+
*/
18+
@Test
19+
public void testLanguageFromCliToConfiguration() {
20+
PMDParameters params = new PMDParameters();
21+
String[] args = { "-d", "source_folder", "-f", "ideaj", "-P", "sourcePath=/home/user/source/", "-R", "java-empty", "-force-language", "dummy"};
22+
PMDCommandLineInterface.extractParameters(params, args, "PMD");
23+
24+
Assert.assertEquals(new DummyLanguageModule().getDefaultVersion().getName(), params.toConfiguration().getForceLanguageVersion().getName());
25+
}
26+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3+
*/
4+
5+
package net.sourceforge.pmd.lang.xml;
6+
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.ArrayList;
11+
import java.util.Arrays;
12+
import java.util.List;
13+
14+
import org.apache.commons.io.FileUtils;
15+
import org.apache.commons.lang3.StringUtils;
16+
import org.junit.Assert;
17+
import org.junit.Test;
18+
19+
import net.sourceforge.pmd.cli.BaseCLITest;
20+
21+
public class XmlCliTest extends BaseCLITest {
22+
private static final String BASE_DIR = "src/test/resources/net/sourceforge/pmd/lang/xml/cli-tests/sampleproject";
23+
private static final String RULE_MESSAGE = "A tags are not allowed";
24+
25+
private String[] createArgs(String directory, String ... args) {
26+
List<String> arguments = new ArrayList<>();
27+
arguments.add("-f");
28+
arguments.add("text");
29+
arguments.add("-no-cache");
30+
arguments.add("-R");
31+
arguments.add(BASE_DIR + "/ruleset.xml");
32+
arguments.add("-d");
33+
arguments.add(BASE_DIR + directory);
34+
arguments.addAll(Arrays.asList(args));
35+
return arguments.toArray(new String[0]);
36+
}
37+
38+
@Test
39+
public void analyzeSingleXmlWithoutForceLanguage() {
40+
String resultFilename = runTest(createArgs("/src/file1.ext"), "analyzeSingleXmlWithoutForceLanguage", 0);
41+
assertRuleMessage(0, resultFilename);
42+
}
43+
44+
@Test
45+
public void analyzeSingleXmlWithForceLanguage() {
46+
String resultFilename = runTest(createArgs("/src/file1.ext", "-force-language", "xml"),
47+
"analyzeSingleXmlWithForceLanguage", 4);
48+
assertRuleMessage(1, resultFilename);
49+
}
50+
51+
@Test
52+
public void analyzeDirectoryWithForceLanguage() {
53+
String resultFilename = runTest(createArgs("/src/", "-force-language", "xml"),
54+
"analyzeDirectoryWithForceLanguage", 4);
55+
assertRuleMessage(3, resultFilename);
56+
}
57+
58+
private void assertRuleMessage(int expectedCount, String resultFilename) {
59+
try {
60+
String result = FileUtils.readFileToString(new File(resultFilename), StandardCharsets.UTF_8);
61+
Assert.assertEquals(expectedCount, StringUtils.countMatches(result, RULE_MESSAGE));
62+
} catch (IOException e) {
63+
throw new AssertionError(e);
64+
}
65+
}
66+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0"?>
2+
3+
<ruleset name="sample"
4+
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
5+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
6+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
7+
8+
<description>
9+
Sample
10+
</description>
11+
12+
<rule name="A"
13+
language="xml"
14+
message="A tags are not allowed"
15+
class="net.sourceforge.pmd.lang.rule.XPathRule">
16+
<description>
17+
A tags are not allowed
18+
</description>
19+
<priority>3</priority>
20+
<properties>
21+
<property name="version" value="2.0"/>
22+
<property name="xpath">
23+
<value>
24+
<![CDATA[
25+
//a
26+
]]>
27+
</value>
28+
</property>
29+
</properties>
30+
</rule>
31+
</ruleset>

0 commit comments

Comments
 (0)