From b3cf149f675d2a46bb1e5b768a1c82cfd5f9d771 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 3 May 2024 12:22:02 +0200 Subject: [PATCH 01/23] [core] CPD: Include processing errors in XML report --- .../pmd/userdocs/cpd/cpd_report_formats.md | 29 +++++++++++++++ docs/pages/release_notes.md | 6 ++++ .../net/sourceforge/pmd/cpd/CPDReport.java | 14 ++++++-- .../net/sourceforge/pmd/cpd/CpdAnalysis.java | 10 +++--- .../java/net/sourceforge/pmd/cpd/GUI.java | 4 +-- .../net/sourceforge/pmd/cpd/XMLRenderer.java | 10 ++++++ .../sourceforge/pmd/cpd/CpdAnalysisTest.java | 35 +++++++++++++++++++ .../net/sourceforge/pmd/cpd/CpdTestUtils.java | 11 +++--- .../sourceforge/pmd/cpd/XMLRendererTest.java | 29 +++++++++++++++ 9 files changed, 135 insertions(+), 13 deletions(-) diff --git a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md index 7ba812383b..3a3825d7c8 100644 --- a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md +++ b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md @@ -5,6 +5,7 @@ keywords: [formats, renderers] summary: "Overview of the built-in report formats for CPD" permalink: pmd_userdocs_cpd_report_formats.html author: Andreas Dangel +last_updated: May 2024 (7.2.0) --- ## Overview @@ -97,6 +98,8 @@ Starting at line 110 of /home/pmd/source/pmd-core/src/test/java/net/sourceforge/ This format uses XML to output the duplications in a more structured format. The XML format can then further be processed using XSLT transformations. See [section xslt](#xslt) for examples. +Since PMD 7.2.0 any processing errors are also reported (if `--skip-lexical-errors` is used) as additional elements `error`. + Example: ```xml @@ -167,6 +170,32 @@ Example: Assert.assertEquals(2, query.nodeNameToXPaths.size()); Assert.assertEquals("self::node()[(attribute::Test1 = \"false\")][(attribute::Test2 = \"true\")]", query.nodeNameToXPaths.get("dummyNode").get(0).toString());]]> + net.sourceforge.pmd.lang.ast.LexException: Lexical error in file '/home/pmd/source/pmd-cli/src/test/resources/net/sourceforge/pmd/cli/cpd/badandgood/BadFile.java' at line 4, column 14: "\ufffd" (65533), after : "" (in lexical state DEFAULT) + at net.sourceforge.pmd.lang.ast.InternalApiBridge.newLexException(InternalApiBridge.java:25) + at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:2698) + at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:18) + at net.sourceforge.pmd.cpd.impl.BaseTokenFilter.getNextToken(BaseTokenFilter.java:44) + at net.sourceforge.pmd.cpd.impl.CpdLexerBase.tokenize(CpdLexerBase.java:40) + at net.sourceforge.pmd.cpd.CpdLexer.tokenize(CpdLexer.java:29) + at net.sourceforge.pmd.cpd.CpdAnalysis.doTokenize(CpdAnalysis.java:146) + at net.sourceforge.pmd.cpd.CpdAnalysis.performAnalysis(CpdAnalysis.java:173) + at net.sourceforge.pmd.cli.commands.internal.CpdCommand.doExecute(CpdCommand.java:134) + at net.sourceforge.pmd.cli.commands.internal.CpdCommand.doExecute(CpdCommand.java:29) + at net.sourceforge.pmd.cli.internal.PmdRootLogger.executeInLoggingContext(PmdRootLogger.java:55) + at net.sourceforge.pmd.cli.commands.internal.AbstractAnalysisPmdSubcommand.execute(AbstractAnalysisPmdSubcommand.java:111) + at net.sourceforge.pmd.cli.commands.internal.AbstractPmdSubcommand.call(AbstractPmdSubcommand.java:30) + at net.sourceforge.pmd.cli.commands.internal.AbstractPmdSubcommand.call(AbstractPmdSubcommand.java:16) + at picocli.CommandLine.executeUserObject(CommandLine.java:2041) + at picocli.CommandLine.access$1500(CommandLine.java:148) + at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461) + at picocli.CommandLine$RunLast.handle(CommandLine.java:2453) + at picocli.CommandLine$RunLast.handle(CommandLine.java:2415) + at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273) + at picocli.CommandLine$RunLast.execute(CommandLine.java:2417) + at picocli.CommandLine.execute(CommandLine.java:2170) + at net.sourceforge.pmd.cli.PmdCli.main(PmdCli.java:24) + ``` diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 1a3ab6dc48..8b8fbe3c42 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -33,6 +33,12 @@ This is a {{ site.pmd.release_type }} release. ### 🚨 API Changes +#### CPD Report Format XML + +The CPD XML report will now also contain processing errors (if CPD is called with `--skip-lexical-errors`). + +See [Report formats for CPD](pmd_userdocs_cpd_report_formats.html#xml) for an example. + #### Deprecated API * pmd-java diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDReport.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDReport.java index 7278d4923e..50bbf6ccaa 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDReport.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDReport.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import net.sourceforge.pmd.lang.document.Chars; import net.sourceforge.pmd.lang.document.FileId; +import net.sourceforge.pmd.reporting.Report; /** * The result of a CPD analysis. This is rendered by a {@link CPDReportRenderer}. @@ -24,13 +25,16 @@ public class CPDReport { private final SourceManager sourceManager; private final List matches; private final Map numberOfTokensPerFile; + private final List processingErrors; CPDReport(SourceManager sourceManager, List matches, - Map numberOfTokensPerFile) { + Map numberOfTokensPerFile, + List processingErrors) { this.sourceManager = sourceManager; this.matches = Collections.unmodifiableList(matches); this.numberOfTokensPerFile = Collections.unmodifiableMap(new TreeMap<>(numberOfTokensPerFile)); + this.processingErrors = Collections.unmodifiableList(processingErrors); } /** Return the list of duplication matches found by the CPD analysis. */ @@ -39,11 +43,15 @@ public class CPDReport { } /** Return a map containing the number of tokens by processed file. */ - public Map getNumberOfTokensPerFile() { return numberOfTokensPerFile; } + /** Returns the list of occurred processing errors. */ + public List getProcessingErrors() { + return processingErrors; + } + /** * Return the slice of source code where the mark was found. This * returns the entire lines from the start to the end line of the @@ -66,7 +74,7 @@ public class CPDReport { public CPDReport filterMatches(Predicate filter) { List filtered = this.matches.stream().filter(filter).collect(Collectors.toList()); - return new CPDReport(sourceManager, filtered, this.getNumberOfTokensPerFile()); + return new CPDReport(sourceManager, filtered, this.getNumberOfTokensPerFile(), this.getProcessingErrors()); } /** diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CpdAnalysis.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CpdAnalysis.java index ccc2b6c686..457194166a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CpdAnalysis.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CpdAnalysis.java @@ -7,6 +7,7 @@ package net.sourceforge.pmd.cpd; import java.io.IOException; import java.io.Writer; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +31,7 @@ import net.sourceforge.pmd.lang.document.InternalApiBridge; import net.sourceforge.pmd.lang.document.TextDocument; import net.sourceforge.pmd.lang.document.TextFile; import net.sourceforge.pmd.properties.PropertyDescriptor; +import net.sourceforge.pmd.reporting.Report; import net.sourceforge.pmd.util.log.PmdReporter; /** @@ -162,7 +164,7 @@ public final class CpdAnalysis implements AutoCloseable { Map numberOfTokensPerFile = new HashMap<>(); - boolean hasErrors = false; + List processingErrors = new ArrayList<>(); Tokens tokens = new Tokens(); for (TextFile textFile : sourceManager.getTextFiles()) { TextDocument textDocument = sourceManager.get(textFile); @@ -177,11 +179,11 @@ public final class CpdAnalysis implements AutoCloseable { } String message = configuration.isSkipLexicalErrors() ? "Skipping file" : "Error while tokenizing"; reporter.errorEx(message, e); - hasErrors = true; + processingErrors.add(new Report.ProcessingError(e, textFile.getFileId())); savedState.restore(tokens); } } - if (hasErrors && !configuration.isSkipLexicalErrors()) { + if (!processingErrors.isEmpty() && !configuration.isSkipLexicalErrors()) { // will be caught by CPD command throw new IllegalStateException("Errors were detected while lexing source, exiting because --skip-lexical-errors is unset."); } @@ -192,7 +194,7 @@ public final class CpdAnalysis implements AutoCloseable { tokens = null; // NOPMD null it out before rendering LOGGER.debug("Finished: {} duplicates found", matches.size()); - CPDReport cpdReport = new CPDReport(sourceManager, matches, numberOfTokensPerFile); + CPDReport cpdReport = new CPDReport(sourceManager, matches, numberOfTokensPerFile, processingErrors); if (renderer != null) { try (Writer writer = IOUtil.createWriter(Charset.defaultCharset(), null)) { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/GUI.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/GUI.java index 9231711559..91e1bb8fe6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/GUI.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/GUI.java @@ -267,7 +267,7 @@ public class GUI implements CPDListener { } if (!f.canWrite()) { - final CPDReport report = new CPDReport(sourceManager, matches, numberOfTokensPerFile); + final CPDReport report = new CPDReport(sourceManager, matches, numberOfTokensPerFile, Collections.emptyList()); try (PrintWriter pw = new PrintWriter(Files.newOutputStream(f.toPath()))) { renderer.render(report, pw); pw.flush(); @@ -549,7 +549,7 @@ public class GUI implements CPDListener { for (int selectionIndex : selectionIndices) { selections.add((Match) model.getValueAt(selectionIndex, 99)); } - CPDReport toRender = new CPDReport(sourceManager, selections, Collections.emptyMap()); + CPDReport toRender = new CPDReport(sourceManager, selections, Collections.emptyMap(), Collections.emptyList()); String report = new SimpleRenderer(trimLeadingWhitespace).renderToString(toRender); resultsTextArea.setText(report); resultsTextArea.setCaretPosition(0); // move to the top diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java index 5365d1827a..817e56e044 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java @@ -23,6 +23,7 @@ import org.w3c.dom.Element; import net.sourceforge.pmd.lang.document.Chars; import net.sourceforge.pmd.lang.document.FileId; import net.sourceforge.pmd.lang.document.FileLocation; +import net.sourceforge.pmd.reporting.Report; import net.sourceforge.pmd.util.StringUtil; /** @@ -110,6 +111,15 @@ public final class XMLRenderer implements CPDReportRenderer { addCodeSnippet(doc, dupElt, match, report); root.appendChild(dupElt); } + + for (Report.ProcessingError error : report.getProcessingErrors()) { + Element errorElt = doc.createElement("error"); + errorElt.setAttribute("filename", report.getDisplayName(error.getFileId())); + errorElt.setAttribute("msg", error.getMsg()); + errorElt.setTextContent(error.getDetail()); + root.appendChild(errorElt); + } + dumpDocToWriter(doc, writer); writer.flush(); } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdAnalysisTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdAnalysisTest.java index 84c22ffc6c..e5140e9abe 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdAnalysisTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdAnalysisTest.java @@ -4,8 +4,11 @@ package net.sourceforge.pmd.cpd; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.any; @@ -22,6 +25,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +38,7 @@ import net.sourceforge.pmd.lang.ast.LexException; import net.sourceforge.pmd.lang.ast.impl.javacc.MalformedSourceException; import net.sourceforge.pmd.lang.document.FileId; import net.sourceforge.pmd.lang.document.TextFile; +import net.sourceforge.pmd.reporting.Report; import net.sourceforge.pmd.util.log.PmdReporter; /** @@ -212,6 +217,36 @@ class CpdAnalysisTest { verifyNoMoreInteractions(reporter); } + @Test + void reportShouldContainProcessingErrors() throws IOException { + AtomicReference report = new AtomicReference<>(); + PmdReporter reporter = mock(PmdReporter.class); + config.setReporter(reporter); + + config.setSkipLexicalErrors(true); // must be true, otherwise CPD is aborted with first processing error + try (CpdAnalysis cpd = CpdAnalysis.create(config)) { + assertTrue(cpd.files().addSourceFile(FileId.fromPathLikeString("foo.dummy"), DummyLanguageModule.CPD_THROW_LEX_EXCEPTION)); + assertTrue(cpd.files().addSourceFile(FileId.fromPathLikeString("foo2.dummy"), DummyLanguageModule.CPD_THROW_MALFORMED_SOURCE_EXCEPTION)); + cpd.performAnalysis(report::set); + } + + assertNotNull(report.get(), "CPD aborted early without producing a report"); + List processingErrors = report.get().getProcessingErrors(); + assertEquals(2, processingErrors.size()); + + Report.ProcessingError error1 = processingErrors.get(0); + assertEquals("foo.dummy", error1.getFileId().getFileName()); + assertThat(error1.getDetail(), containsString(LexException.class.getSimpleName())); + + Report.ProcessingError error2 = processingErrors.get(1); + assertEquals("foo2.dummy", error2.getFileId().getFileName()); + assertThat(error2.getDetail(), containsString(MalformedSourceException.class.getSimpleName())); + + verify(reporter).errorEx(eq("Skipping file"), any(LexException.class)); + verify(reporter).errorEx(eq("Skipping file"), any(MalformedSourceException.class)); + verifyNoMoreInteractions(reporter); + } + @Test void testSkipLexicalErrors() throws IOException { PmdReporter reporter = mock(PmdReporter.class); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdTestUtils.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdTestUtils.java index e40b4c34c7..1a41f61666 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdTestUtils.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/CpdTestUtils.java @@ -15,6 +15,7 @@ import java.util.Set; import net.sourceforge.pmd.lang.DummyLanguageModule; import net.sourceforge.pmd.lang.document.FileId; import net.sourceforge.pmd.lang.document.TextFile; +import net.sourceforge.pmd.reporting.Report; final class CpdTestUtils { @@ -26,10 +27,10 @@ final class CpdTestUtils { } static CPDReport makeReport(List matches) { - return makeReport(matches, Collections.emptyMap()); + return makeReport(matches, Collections.emptyMap(), Collections.emptyList()); } - static CPDReport makeReport(List matches, Map numTokensPerFile) { + static CPDReport makeReport(List matches, Map numTokensPerFile, List processingErrors) { Set textFiles = new HashSet<>(); for (Match match : matches) { match.iterator().forEachRemaining( @@ -41,7 +42,8 @@ final class CpdTestUtils { return new CPDReport( new SourceManager(new ArrayList<>(textFiles)), matches, - numTokensPerFile + numTokensPerFile, + processingErrors ); } @@ -73,7 +75,8 @@ final class CpdTestUtils { return new CPDReport( new SourceManager(new ArrayList<>(textFiles)), matches, - numTokensPerFile + numTokensPerFile, + Collections.emptyList() ); } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java index eb96c32af9..afec863903 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java @@ -24,7 +24,9 @@ import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import net.sourceforge.pmd.cpd.CpdTestUtils.CpdReportBuilder; +import net.sourceforge.pmd.lang.ast.LexException; import net.sourceforge.pmd.lang.document.FileId; +import net.sourceforge.pmd.reporting.Report; /** * @author Philippe T'Seyen @@ -257,4 +259,31 @@ class XMLRendererTest { assertThat(report, containsString("x=\"]]]]>\";")); assertThat(report, not(containsString("x=\"]]>\";"))); // must be escaped } + + @Test + void reportContainsProcessingError() throws IOException, ParserConfigurationException, SAXException { + FileId fileId = FileId.fromPathLikeString("file1.txt"); + Report.ProcessingError processingError = new Report.ProcessingError( + new LexException(2, 1, fileId, "test exception", new RuntimeException("cause exception")), + fileId); + CPDReportRenderer renderer = new XMLRenderer(); + StringWriter sw = new StringWriter(); + renderer.render(CpdTestUtils.makeReport(Collections.emptyList(), Collections.emptyMap(), Collections.singletonList(processingError)), sw); + String report = sw.toString(); + System.out.println("report = " + report); + + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); + NodeList nodes = doc.getChildNodes(); + Node n = nodes.item(0); + assertEquals("pmd-cpd", n.getNodeName()); + assertEquals(1, doc.getElementsByTagName("error").getLength()); + Node error = doc.getElementsByTagName("error").item(0); + String filename = error.getAttributes().getNamedItem("filename").getNodeValue(); + assertEquals(processingError.getFileId().getAbsolutePath(), filename); + String msg = error.getAttributes().getNamedItem("msg").getNodeValue(); + assertEquals(processingError.getMsg(), msg); + String textContent = error.getTextContent(); + assertEquals(processingError.getDetail(), textContent); + } } From 5742ac62e964c8aa64007446fc58202f74f2430c Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 3 May 2024 12:47:49 +0200 Subject: [PATCH 02/23] Fix compat6 --- .../src/main/java/net/sourceforge/pmd/cpd/RendererHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pmd-compat6/src/main/java/net/sourceforge/pmd/cpd/RendererHelper.java b/pmd-compat6/src/main/java/net/sourceforge/pmd/cpd/RendererHelper.java index f7291e9ae5..c25a80d964 100644 --- a/pmd-compat6/src/main/java/net/sourceforge/pmd/cpd/RendererHelper.java +++ b/pmd-compat6/src/main/java/net/sourceforge/pmd/cpd/RendererHelper.java @@ -39,7 +39,7 @@ final class RendererHelper { } try (SourceManager sourceManager = new SourceManager(textFiles)) { - CPDReport report = new CPDReport(sourceManager, matchesList, Collections.emptyMap()); + CPDReport report = new CPDReport(sourceManager, matchesList, Collections.emptyMap(), Collections.emptyList()); renderer.render(report, writer); } catch (Exception e) { throw new RuntimeException(e); From 3e5b152a2c143d62a34396cd03c3152fc5417c96 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 3 May 2024 12:51:43 +0200 Subject: [PATCH 03/23] [doc] Update release notes (#4992) [skip ci] --- docs/pages/release_notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 8b8fbe3c42..9d5ff12224 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,6 +19,7 @@ This is a {{ site.pmd.release_type }} release. * core * [#4978](https://github.com/pmd/pmd/issues/4978): \[core] Referenced Rulesets do not emit details on validation errors * [#4983](https://github.com/pmd/pmd/pull/4983): \[cpd] Fix CPD crashes about unicode escapes + * [#4992](https://github.com/pmd/pmd/pull/4992): \[core] CPD: Include processing errors in XML report * java * [#4912](https://github.com/pmd/pmd/issues/4912): \[java] Unable to parse some Java9+ resource references * [#4973](https://github.com/pmd/pmd/pull/4973): \[java] Stop parsing Java for CPD From 48512a43f6b637c8846376149f016bfed616142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Fournier?= Date: Wed, 5 Jun 2024 12:38:20 +0200 Subject: [PATCH 04/23] Fix #5047 - type inference with enum --- .../pmd/lang/java/types/TypeOps.java | 2 +- .../types/internal/infer/InferenceVar.java | 2 +- .../types/internal/infer/TypeInferenceTest.kt | 47 ++++++++++++ .../bestpractices/xml/UnusedPrivateMethod.xml | 72 +++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java index 66f237ae61..051ea91c02 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java @@ -218,7 +218,7 @@ public final class TypeOps { @Override public Boolean visitInferenceVar(InferenceVar t, JTypeMirror s) { if (pure) { - return t == s; + return t == s || t.getBounds(BoundKind.EQ).contains(s); } if (s instanceof JPrimitiveType) { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceVar.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceVar.java index cf5a71ea5c..abfbc904c1 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceVar.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceVar.java @@ -79,7 +79,7 @@ public final class InferenceVar implements SubstVar { * Returns the bounds of a certain kind that apply to * this variable. */ - Set getBounds(BoundKind kind) { + public Set getBounds(BoundKind kind) { return boundSet.bounds.getOrDefault(kind, Collections.emptySet()); } diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/TypeInferenceTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/TypeInferenceTest.kt index d877ef1b2c..0808d950cf 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/TypeInferenceTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/TypeInferenceTest.kt @@ -407,4 +407,51 @@ public class BadIntersection { acu.firstMethodCall() shouldHaveType java.util.List::class[t_Animal] } } + parserTest("#5047 inference failed with enum") { + val (acu, spy) = parser.parseWithTypeInferenceSpy( + """ + interface Function { R apply(T t); } + + public class Main { + public enum OptOutStatus { + UNKNOWN_STATUS(3L); + + private final long id; + + OptOutStatus(long id) { + this.id = id; + } + + public long id() { + return this.id; + } + } + + static class Utils { + private Long getValue(OptOutStatus val) { + return getValue(val, OptOutStatus::id); + } + + private > Long getValue(T enumValue, Function fn) { + if (enumValue == null) { + return null; + } + return fn.apply(enumValue); + } + } + } + + """.trimIndent() + ) + + val (_, _, optOutEnum) = acu.declaredTypeSignatures() + val (_, getValue2) = acu.methodDeclarations().filter { it.name == "getValue" }.toList() + + spy.shouldBeOk { + val info = acu.firstMethodCall().overloadSelectionInfo + info::isFailed shouldBe false + info.methodType shouldBeSomeInstantiationOf getValue2.genericSignature + info.methodType.formalParameters[0] shouldBe optOutEnum + } + } }) diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/UnusedPrivateMethod.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/UnusedPrivateMethod.xml index 41367ff29f..5f2871d3dc 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/UnusedPrivateMethod.xml +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/UnusedPrivateMethod.xml @@ -2002,4 +2002,76 @@ class FooTest{ } ]]> + + UnusedPrivateMethod for Generics and Overloads #5047 + 0 + > Long getValue(T enumValue, Function fn) { + if (enumValue == null) { + return null; + } + return fn.apply(enumValue); + } + } + } + ]]> + From 57c26dd49b380911839a65801a55249f3d703259 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 24 May 2024 14:47:22 +0200 Subject: [PATCH 05/23] Update kotest and kotlin - Bumps kotest from 5.5.5 to 5.9.0 - Bumps kotlin from 1.7.20 to 1.9.24 - Bumps dokka from 1.7.20 to 1.9.20 --- pom.xml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f78eeeb404..c3e41f7588 100644 --- a/pom.xml +++ b/pom.xml @@ -92,10 +92,10 @@ ${maven.compiler.test.target} - 1.7.20 - 5.5.5 + 1.9.24 + 5.9.0 5.8.2 - 1.7.20 + 1.9.20 5.0 3.2.5 @@ -335,7 +335,11 @@ alphabetical + ${project.build.testResources[0].directory} + + true + true true From a0e7bd739018843339a62f9ea4eb3c9f43763952 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Thu, 6 Jun 2024 16:35:29 +0200 Subject: [PATCH 06/23] Bump kotest from 5.9.0 to 5.9.1 https://github.com/kotest/kotest/releases/tag/v5.9.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c3e41f7588..393f3b7ee7 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ ${maven.compiler.test.target} 1.9.24 - 5.9.0 + 5.9.1 5.8.2 1.9.20 From 72bf5d07b421bef9a38f83ce179dc92e634f9395 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 7 Jun 2024 11:20:50 +0200 Subject: [PATCH 07/23] [plsql] Support MERGE statement Fixes #1934 --- docs/pages/release_notes.md | 2 + pmd-plsql/etc/grammar/PLSQL.jjt | 107 ++++++++++++++--- .../pmd/lang/plsql/ast/PlsqlTreeDumpTest.java | 5 + .../plsql/ast/MergeStatementIssue1934.pls | 17 +++ .../plsql/ast/MergeStatementIssue1934.txt | 108 ++++++++++++++++++ 5 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index e8d7cdbf90..48fbdf1007 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -15,6 +15,8 @@ This is a {{ site.pmd.release_type }} release. ### 🚀 New and noteworthy ### 🐛 Fixed Issues +* plsql + * [#1934](https://github.com/pmd/pmd/issues/1934): \[plsql] ParseException with MERGE statement in anonymous block ### 🚨 API Changes diff --git a/pmd-plsql/etc/grammar/PLSQL.jjt b/pmd-plsql/etc/grammar/PLSQL.jjt index 5108997fac..f7a49d95c9 100644 --- a/pmd-plsql/etc/grammar/PLSQL.jjt +++ b/pmd-plsql/etc/grammar/PLSQL.jjt @@ -27,6 +27,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** + * Add support for MERGE (INTO) statement + * + * Andreas Dangel 06/2024 + *==================================================================== * Add support for Select statement within OPEN FOR Statements * * Andreas Dangel 09/2021 @@ -293,7 +297,8 @@ ASTInput Input() : | DeleteStatement() [";"] | InsertStatement() [";"] | SelectStatement() [";"] - |(|||||) ReadPastNextOccurrence(";") //Ignore SQL statements in scripts + | MergeStatement() [";"] + |(|||
|) ReadPastNextOccurrence(";") //Ignore SQL statements in scripts ) ("/")* )* @@ -1078,19 +1083,32 @@ void Skip2NextTerminator(String initiator,String terminator) : if(t.getImage().equals(initiator)) count++; while (count > 0 || !t.getImage().equals(terminator)) { - t = getNextToken(); - t = getToken(1); - if(t.getImage().equals(initiator)) count++; - if(t.getImage().equals(terminator)) count--; - if((null != t.specialToken && beginToken.kind != SELECT && beginToken.kind != INSERT && beginToken.kind != UPDATE && beginToken.kind != DELETE - && beginToken.kind != MERGE && beginToken.kind != EXECUTE && beginToken.kind != WITH) || t.kind == EOF) - return; - if (t.specialToken != null && "/".equals(t.getImage())) - return; + t = getNextToken(); + t = getToken(1); + if (t.getImage().equals(initiator)) { + count++; + } + if (t.getImage().equals(terminator)) { + count--; + } + if ((null != t.specialToken + && beginToken.kind != SELECT + && beginToken.kind != INSERT + && beginToken.kind != UPDATE + && beginToken.kind != DELETE + && beginToken.kind != MERGE + && beginToken.kind != EXECUTE + && beginToken.kind != WITH) + || t.kind == EOF) { + return; + } + if (t.specialToken != null && "/".equals(t.getImage())) { + return; + } } } { - { return; } + { return; } } /* @@ -1256,10 +1274,10 @@ ASTSqlStatement SqlStatement(String initiator, String terminator) : |
{jjtThis.setType(ASTSqlStatement.Type.LOCK_TABLE); } |{jjtThis.setType(ASTSqlStatement.Type.MERGE); } |) - Skip2NextTerminator(initiator, terminator) - { - return jjtThis ; - } + Skip2NextTerminator(initiator, terminator) + { + return jjtThis; + } } void AbstractSelectStatement(AbstractSelectStatement node) #void : @@ -2346,6 +2364,7 @@ ASTUnlabelledStatement UnlabelledStatement() : UpdateStatement() ";" | DeleteStatement() ";" | InsertStatement() ";" | + LOOKAHEAD(2) MergeStatement() ";" | LOOKAHEAD(["("]
||) SqlStatement(null,";") [";"] | LOOKAHEAD(3) ContinueStatement() ";" // CONTINUE keyword was added in 11G, so Oracle compilation supports CONTINUE as a variable name | CaseStatement() ";" @@ -2654,6 +2673,55 @@ ASTDeleteStatement DeleteStatement() : { return jjtThis; } } +/** + * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/MERGE.html#GUID-5692CCB7-24D9-4C0E-81A7-A22436DC968F + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/MERGE.html#GUID-5692CCB7-24D9-4C0E-81A7-A22436DC968F + */ +ASTMergeStatement MergeStatement() : +{} +{ + [ LOOKAHEAD(2) SchemaName() "." ] TableName() [ TableAlias() ] + + ( + LOOKAHEAD(3) "(" ValuesClause() ")" + | "(" Subquery() ")" [ TableAlias() ] + | [ LOOKAHEAD(2) SchemaName() "." ] TableName() [ TableAlias() ] + ) + "(" Condition() ")" + [ LOOKAHEAD(MergeUpdateClausePrefix()) MergeUpdateClause() ] + [ LOOKAHEAD(2) MergeInsertClause() ] + [ ErrorLoggingClause() ] + [ ReturningClause() ] + { return jjtThis; } +} + +void MergeUpdateClausePrefix() #void: +{} +{ + KEYWORD("MATCHED") +} + +ASTMergeUpdateClause MergeUpdateClause() : +{} +{ + MergeUpdateClausePrefix() + [ LOOKAHEAD(2) TableName() "." ] Column() "=" ( LOOKAHEAD(2) "(" Subquery() ")" | Expression() | <_DEFAULT> ) + ( "," [ LOOKAHEAD(2) TableName() "." ] Column() "=" ( LOOKAHEAD(2) "(" Subquery() ")" | Expression() | <_DEFAULT> ) )* + [ WhereClause() ] + [ WhereClause() ] + { return jjtThis; } +} + +ASTMergeInsertClause MergeInsertClause() : +{} +{ + KEYWORD("MATCHED") + [ "(" Column() ( "," Column() )* ")" ] + ValuesClause() + [ WhereClause() ] + { return jjtThis; } +} + /** Scope rule: the loop index only exists within the Loop */ ASTForStatement ForStatement() : {} @@ -5426,14 +5494,15 @@ void RESERVED_WORD() #void: {} void KEYWORD(String id) #void: {} { - { - if (!token.getImage().equalsIgnoreCase(id)) { + if (!isKeyword(id)) { String eol = System.getProperty("line.separator", "\n"); - throw new ParseException("Encountered \"" + token.getImage() + "\" " - + "Was expecting: " + id).withLocation(token); + throw new ParseException("Encountered \"" + getToken(1).getImage() + "\" " + + "Was expecting: \"" + id + "\"").withLocation(token); } } + + } ASTKEYWORD_UNRESERVED KEYWORD_UNRESERVED (): {} diff --git a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java index 438184c677..5a49292182 100644 --- a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java +++ b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java @@ -41,4 +41,9 @@ class PlsqlTreeDumpTest extends BaseTreeDumpTest { void parseSelectIntoAssociativeArrayType() { doTest("SelectIntoArray"); } + + @Test + void parseMergeStatement() { + doTest("MergeStatementIssue1934"); + } } diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls new file mode 100644 index 0000000000..555f439f0b --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls @@ -0,0 +1,17 @@ +-- +-- BSD-style license; for more info see http://pmd.sourceforge.net/license.html +-- + +-- See https://github.com/pmd/pmd/issues/1934 + +BEGIN + + MERGE INTO jhs_translations b + USING ( SELECT 'PROM_EDIT_PROM_NR' key1,'Edycja promocji nr' text,123123 lce_id FROM dual ) e + ON (b.key1 = e.key1 and b.lce_id=e.lce_id) + WHEN MATCHED + THEN UPDATE SET b.text = e.text + WHEN NOT MATCHED + THEN INSERT (ID,KEY1, TEXT,LCE_ID) values (JHS_SEQ.NEXTVAL,'PROM_EDIT_PROM_NR','Edycja promocji nr',123123); +END; +/ diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt new file mode 100644 index 0000000000..c473d6143c --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt @@ -0,0 +1,108 @@ ++- Input[@CanonicalImage = null, @ExcludedLinesCount = 0, @ExcludedRangesCount = 0] + +- Global[@CanonicalImage = null] + +- Block[@CanonicalImage = null] + +- Statement[@CanonicalImage = null] + +- UnlabelledStatement[@CanonicalImage = null] + +- MergeStatement[@CanonicalImage = null] + +- TableName[@CanonicalImage = "JHS_TRANSLATIONS", @Image = "jhs_translations"] + | +- ID[@CanonicalImage = "JHS_TRANSLATIONS", @Image = "jhs_translations"] + +- TableAlias[@CanonicalImage = "B", @Image = "b"] + | +- ID[@CanonicalImage = "B", @Image = "b"] + +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | +- SelectList[@CanonicalImage = null] + | | +- SqlExpression[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | | | +- PrimaryPrefix[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | | | +- StringLiteral[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @String = "PROM_EDIT_PROM_NR"] + | | +- ColumnAlias[@CanonicalImage = "KEY1", @Image = "key1"] + | | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- SqlExpression[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | | | +- PrimaryPrefix[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | | | +- StringLiteral[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @String = "Edycja promocji nr"] + | | +- ColumnAlias[@CanonicalImage = "TEXT", @Image = "text"] + | | | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + | | +- SqlExpression[@CanonicalImage = "123123", @Image = "123123"] + | | | +- PrimaryPrefix[@CanonicalImage = "123123", @Image = "123123", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "123123", @Image = "123123"] + | | | +- NumericLiteral[@CanonicalImage = "123123", @Image = "123123"] + | | +- ColumnAlias[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- FromClause[@CanonicalImage = null] + | +- TableReference[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "DUAL", @Image = "dual"] + | +- ID[@CanonicalImage = "DUAL", @Image = "dual"] + +- TableAlias[@CanonicalImage = "E", @Image = "e"] + | +- ID[@CanonicalImage = "E", @Image = "e"] + +- Condition[@CanonicalImage = null] + | +- CompoundCondition[@CanonicalImage = null, @Type = "AND"] + | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "B.KEY1", @Image = "b.key1"] + | | | +- PrimaryPrefix[@CanonicalImage = "B.KEY1", @Image = "b.key1", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "B.KEY1", @Image = "b.key1"] + | | | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | | | +- ID[@CanonicalImage = "B", @Image = "b"] + | | | +- Column[@CanonicalImage = "KEY1", @Image = "key1"] + | | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- SqlExpression[@CanonicalImage = "E.KEY1", @Image = "e.key1"] + | | +- PrimaryPrefix[@CanonicalImage = "E.KEY1", @Image = "e.key1", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "E.KEY1", @Image = "e.key1"] + | | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | | +- ID[@CanonicalImage = "E", @Image = "e"] + | | +- Column[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | +- Condition[@CanonicalImage = null] + | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | +- SqlExpression[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id"] + | | +- PrimaryPrefix[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id"] + | | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | | +- ID[@CanonicalImage = "B", @Image = "b"] + | | +- Column[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- SqlExpression[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id"] + | +- PrimaryPrefix[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id"] + | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | +- ID[@CanonicalImage = "E", @Image = "e"] + | +- Column[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + +- MergeUpdateClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | +- ID[@CanonicalImage = "B", @Image = "b"] + | +- Column[@CanonicalImage = "TEXT", @Image = "text"] + | | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + | +- Expression[@CanonicalImage = "E.TEXT", @Image = "e.text"] + | +- PrimaryPrefix[@CanonicalImage = "E.TEXT", @Image = "e.text", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "E.TEXT", @Image = "e.text"] + | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | +- ID[@CanonicalImage = "E", @Image = "e"] + | +- Column[@CanonicalImage = "TEXT", @Image = "text"] + | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + +- MergeInsertClause[@CanonicalImage = null] + +- Column[@CanonicalImage = "ID", @Image = "ID"] + | +- ID[@CanonicalImage = "ID", @Image = "ID"] + +- Column[@CanonicalImage = "KEY1", @Image = "KEY1"] + | +- ID[@CanonicalImage = "KEY1", @Image = "KEY1"] + +- Column[@CanonicalImage = "TEXT", @Image = "TEXT"] + | +- ID[@CanonicalImage = "TEXT", @Image = "TEXT"] + +- Column[@CanonicalImage = "LCE_ID", @Image = "LCE_ID"] + | +- ID[@CanonicalImage = "LCE_ID", @Image = "LCE_ID"] + +- ValuesClause[@CanonicalImage = null] + +- Expression[@CanonicalImage = "", @Image = ""] + | +- PrimaryPrefix[@CanonicalImage = "", @Image = "", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "", @Image = ""] + | +- ID[@CanonicalImage = "JHS_SEQ", @Image = "JHS_SEQ"] + +- Expression[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | +- PrimaryPrefix[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @SelfModifier = false] + | +- Literal[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | +- StringLiteral[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @String = "PROM_EDIT_PROM_NR"] + +- Expression[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | +- PrimaryPrefix[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @SelfModifier = false] + | +- Literal[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | +- StringLiteral[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @String = "Edycja promocji nr"] + +- Expression[@CanonicalImage = "123123", @Image = "123123"] + +- PrimaryPrefix[@CanonicalImage = "123123", @Image = "123123", @SelfModifier = false] + +- Literal[@CanonicalImage = "123123", @Image = "123123"] + +- NumericLiteral[@CanonicalImage = "123123", @Image = "123123"] From d47ca10029a2b851a10aebddbb652d78c5644fe8 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 7 Jun 2024 12:16:43 +0200 Subject: [PATCH 08/23] [plsql] Support Error Logging in INSERT, UPDATE, DELETE Fixes #2779 --- docs/pages/release_notes.md | 2 + pmd-plsql/etc/grammar/PLSQL.jjt | 21 ++- .../pmd/lang/plsql/ast/PlsqlTreeDumpTest.java | 5 + .../lang/plsql/ast/ErrorLoggingClause2779.pls | 29 ++++ .../lang/plsql/ast/ErrorLoggingClause2779.txt | 149 ++++++++++++++++++ 5 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index e8d7cdbf90..9ede3ce465 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -15,6 +15,8 @@ This is a {{ site.pmd.release_type }} release. ### 🚀 New and noteworthy ### 🐛 Fixed Issues +* plsql + * [#2779](https://github.com/pmd/pmd/issues/2779): \[plsql] Error while parsing statement with (Oracle) DML Error Logging ### 🚨 API Changes diff --git a/pmd-plsql/etc/grammar/PLSQL.jjt b/pmd-plsql/etc/grammar/PLSQL.jjt index 5108997fac..25a1033d2f 100644 --- a/pmd-plsql/etc/grammar/PLSQL.jjt +++ b/pmd-plsql/etc/grammar/PLSQL.jjt @@ -27,6 +27,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** + * Add support Error Logging for INSERT, UPDATE, DELETE + * + * Andreas Dangel 06/2024 + *==================================================================== * Add support for Select statement within OPEN FOR Statements * * Andreas Dangel 09/2021 @@ -2087,7 +2091,7 @@ ASTTableReference TableReference() : { QueryTableExpression() - [ LOOKAHEAD(2) TableAlias() ] + [ LOOKAHEAD(2, {!getToken(1).getImage().equalsIgnoreCase("LOG")}) TableAlias() ] { return jjtThis; } } @@ -2457,7 +2461,7 @@ ASTCursorForLoopStatement CursorForLoopStatement() : } /** - * See https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423 */ ASTInsertStatement InsertStatement() : {} @@ -2470,6 +2474,7 @@ ASTSingleTableInsert SingleTableInsert() : {} { InsertIntoClause() ( ValuesClause() [ ReturningClause() ] | Subquery() ) + [ ErrorLoggingClause() ] { return jjtThis; } } @@ -2622,7 +2627,8 @@ ASTUpdateSetClause UpdateSetClause() : } /** - * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2126358 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2126358 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7__I2122564 */ ASTReturningClause ReturningClause() : {} @@ -2632,7 +2638,9 @@ ASTReturningClause ReturningClause() : } /** - * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__BCEEAAGC + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423__BGBDIGAH + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__BCEEAAGC + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7__CEGCHDJF */ ASTErrorLoggingClause ErrorLoggingClause() : {} @@ -2645,12 +2653,17 @@ ASTErrorLoggingClause ErrorLoggingClause() : } +/** + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7 + */ ASTDeleteStatement DeleteStatement() : {} { [ ] ( TableReference() | "(" TableReference() ")" ) [ WhereClause() ] + [ ReturningClause() ] + [ ErrorLoggingClause() ] { return jjtThis; } } diff --git a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java index 438184c677..45612111c7 100644 --- a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java +++ b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java @@ -41,4 +41,9 @@ class PlsqlTreeDumpTest extends BaseTreeDumpTest { void parseSelectIntoAssociativeArrayType() { doTest("SelectIntoArray"); } + + @Test + void errorLoggingClause() { + doTest("ErrorLoggingClause2779"); + } } diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls new file mode 100644 index 0000000000..580a396391 --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls @@ -0,0 +1,29 @@ +-- +-- BSD-style license; for more info see http://pmd.sourceforge.net/license.html +-- + +create or replace procedure test as +begin + + -- https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423__BCEGDJDJ + INSERT INTO raises + SELECT employee_id, salary*1.1 FROM employees + WHERE commission_pct > .2 + LOG ERRORS INTO errlog ('my_bad') REJECT LIMIT 10; + + -- https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2135485 + UPDATE people_demo1 p SET VALUE(p) = + (SELECT VALUE(q) FROM people_demo2 q + WHERE p.department_id = q.department_id) + WHERE p.department_id = 10 + LOG ERRORS INTO errlog ('my_bad') REJECT LIMIT 10; + + -- https://github.com/pmd/pmd/issues/2779 + delete from test_table talias + log errors into err$_test_table reject limit unlimited; + + -- without a table alias + delete from test_table + log errors into err$_test_table reject limit unlimited; +end; +/ diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt new file mode 100644 index 0000000000..ce2cf4c5e2 --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt @@ -0,0 +1,149 @@ ++- Input[@CanonicalImage = null, @ExcludedLinesCount = 0, @ExcludedRangesCount = 0] + +- Global[@CanonicalImage = null] + +- ProgramUnit[@CanonicalImage = null, @MethodName = "test", @Name = "test", @ObjectName = null] + +- MethodDeclarator[@CanonicalImage = "TEST", @Image = "test", @ParameterCount = 1] + | +- ObjectNameDeclaration[@CanonicalImage = "TEST", @Image = "test"] + | +- ID[@CanonicalImage = "TEST", @Image = "test"] + +- DeclarativeSection[@CanonicalImage = null] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- InsertStatement[@CanonicalImage = null] + | +- SingleTableInsert[@CanonicalImage = null] + | +- InsertIntoClause[@CanonicalImage = null] + | | +- DMLTableExpressionClause[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "RAISES", @Image = "raises"] + | | +- ID[@CanonicalImage = "RAISES", @Image = "raises"] + | +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | | +- SelectList[@CanonicalImage = null] + | | | +- SqlExpression[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- PrimaryPrefix[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id", @SelfModifier = false] + | | | | +- SimpleExpression[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- Column[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- ID[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | +- SqlExpression[@CanonicalImage = "SALARY * 1.1", @Image = "salary * 1.1"] + | | | +- MultiplicativeExpression[@CanonicalImage = "SALARY * 1.1", @Image = "salary * 1.1"] + | | | +- PrimaryPrefix[@CanonicalImage = "SALARY", @Image = "salary", @SelfModifier = false] + | | | | +- SimpleExpression[@CanonicalImage = "SALARY", @Image = "salary"] + | | | | +- Column[@CanonicalImage = "SALARY", @Image = "salary"] + | | | | +- ID[@CanonicalImage = "SALARY", @Image = "salary"] + | | | +- PrimaryPrefix[@CanonicalImage = "1.1", @Image = "1.1", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "1.1", @Image = "1.1"] + | | | +- NumericLiteral[@CanonicalImage = "1.1", @Image = "1.1"] + | | +- FromClause[@CanonicalImage = null] + | | | +- TableReference[@CanonicalImage = null] + | | | +- TableName[@CanonicalImage = "EMPLOYEES", @Image = "employees"] + | | | +- ID[@CanonicalImage = "EMPLOYEES", @Image = "employees"] + | | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = ">"] + | | +- SqlExpression[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- PrimaryPrefix[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- Column[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- ID[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | +- SqlExpression[@CanonicalImage = ".2", @Image = ".2"] + | | +- PrimaryPrefix[@CanonicalImage = ".2", @Image = ".2", @SelfModifier = false] + | | +- Literal[@CanonicalImage = ".2", @Image = ".2"] + | | +- NumericLiteral[@CanonicalImage = ".2", @Image = ".2"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | | +- ID[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | +- SqlExpression[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- PrimaryPrefix[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- StringLiteral[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @String = "my_bad"] + | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- UpdateStatement[@CanonicalImage = null] + | +- DMLTableExpressionClause[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "PEOPLE_DEMO1", @Image = "people_demo1"] + | | +- ID[@CanonicalImage = "PEOPLE_DEMO1", @Image = "people_demo1"] + | +- TableAlias[@CanonicalImage = "P", @Image = "p"] + | | +- ID[@CanonicalImage = "P", @Image = "p"] + | +- UpdateSetClause[@CanonicalImage = null] + | | +- TableAlias[@CanonicalImage = "P", @Image = "p"] + | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | | +- SelectList[@CanonicalImage = null] + | | | +- SqlExpression[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- PrimaryPrefix[@CanonicalImage = "VALUE", @Image = "VALUE", @SelfModifier = false] + | | | +- FunctionCall[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- FunctionName[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | | +- ID[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- Arguments[@ArgumentCount = 1, @CanonicalImage = null] + | | | +- ArgumentList[@CanonicalImage = null] + | | | +- Argument[@CanonicalImage = null] + | | | +- Expression[@CanonicalImage = "Q", @Image = "q"] + | | | +- PrimaryPrefix[@CanonicalImage = "Q", @Image = "q", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "Q", @Image = "q"] + | | | +- Column[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- FromClause[@CanonicalImage = null] + | | | +- TableReference[@CanonicalImage = null] + | | | +- TableName[@CanonicalImage = "PEOPLE_DEMO2", @Image = "people_demo2"] + | | | | +- ID[@CanonicalImage = "PEOPLE_DEMO2", @Image = "people_demo2"] + | | | +- TableAlias[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- PrimaryPrefix[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- TableName[@CanonicalImage = "P", @Image = "p"] + | | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- SqlExpression[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id"] + | | +- PrimaryPrefix[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id"] + | | +- TableName[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- PrimaryPrefix[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- TableName[@CanonicalImage = "P", @Image = "p"] + | | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- SqlExpression[@CanonicalImage = "10", @Image = "10"] + | | +- PrimaryPrefix[@CanonicalImage = "10", @Image = "10", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "10", @Image = "10"] + | | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | | +- ID[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | +- SqlExpression[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- PrimaryPrefix[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- StringLiteral[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @String = "my_bad"] + | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- DeleteStatement[@CanonicalImage = null] + | +- TableReference[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | | | +- ID[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | | +- TableAlias[@CanonicalImage = "TALIAS", @Image = "talias"] + | | +- ID[@CanonicalImage = "TALIAS", @Image = "talias"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + | +- ID[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + +- Statement[@CanonicalImage = null] + +- UnlabelledStatement[@CanonicalImage = null] + +- DeleteStatement[@CanonicalImage = null] + +- TableReference[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | +- ID[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + +- ErrorLoggingClause[@CanonicalImage = null] + +- TableName[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + +- ID[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] From 9d803282d9bccbbc33783f347ea75d7e715ae120 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 7 Jun 2024 19:24:26 +0200 Subject: [PATCH 09/23] [apex] Use case-insensitive lexer for CPD This makes it consistent with how the ApexParser reads the files. And the case-sensitive ANTLR rules (e.g. for string literals) work that way. Fixes #5053 --- docs/pages/release_notes.md | 2 + .../pmd/lang/apex/cpd/ApexCpdLexer.java | 26 ++++++----- .../pmd/lang/apex/cpd/ApexCpdLexerTest.java | 10 +++++ .../apex/cpd/testdata/StringLiterals5053.cls | 12 ++++++ .../apex/cpd/testdata/StringLiterals5053.txt | 43 +++++++++++++++++++ 5 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.cls create mode 100644 pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.txt diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index e8d7cdbf90..da03f67d36 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -15,6 +15,8 @@ This is a {{ site.pmd.release_type }} release. ### 🚀 New and noteworthy ### 🐛 Fixed Issues +* apex + * [#5053](https://github.com/pmd/pmd/issues/5053): \[apex] CPD fails to parse string literals with escaped characters ### 🚨 API Changes diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java index f483fca8f4..367b2097e4 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java @@ -9,37 +9,35 @@ import java.util.Locale; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.Token; import net.sourceforge.pmd.cpd.CpdLexer; import net.sourceforge.pmd.cpd.TokenFactory; +import net.sourceforge.pmd.lang.ast.impl.antlr4.AntlrToken; +import net.sourceforge.pmd.lang.ast.impl.antlr4.AntlrTokenManager; import net.sourceforge.pmd.lang.document.TextDocument; import com.nawforce.apexparser.ApexLexer; +import com.nawforce.apexparser.CaseInsensitiveInputStream; public class ApexCpdLexer implements CpdLexer { @Override public void tokenize(TextDocument document, TokenFactory tokenEntries) throws IOException { CharStream charStream = CharStreams.fromReader(document.newReader()); - ApexLexer lexer = new ApexLexer(charStream); + CaseInsensitiveInputStream caseInsensitiveInputStream = new CaseInsensitiveInputStream(charStream); + ApexLexer lexer = new ApexLexer(caseInsensitiveInputStream); + AntlrTokenManager tokenManager = new AntlrTokenManager(lexer, document); - Token token = lexer.nextToken(); + AntlrToken token = tokenManager.getNextToken(); - while (token.getType() != Token.EOF) { - if (token.getChannel() == ApexLexer.DEFAULT_TOKEN_CHANNEL) { // exclude WHITESPACE_CHANNEL and COMMENT_CHANNEL - String tokenText = token.getText(); + while (!token.isEof()) { + if (token.isDefault()) { // excludes WHITESPACE_CHANNEL and COMMENT_CHANNEL + String tokenText = token.getImage(); // be case-insensitive tokenText = tokenText.toLowerCase(Locale.ROOT); - tokenEntries.recordToken( - tokenText, - token.getLine(), - token.getCharPositionInLine() + 1, - token.getLine(), - token.getCharPositionInLine() + tokenText.length() + 1 - ); + tokenEntries.recordToken(tokenText, token.getReportLocation()); } - token = lexer.nextToken(); + token = tokenManager.getNextToken(); } } } diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexerTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexerTest.java index 67c1da6555..7210cbfed7 100644 --- a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexerTest.java +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexerTest.java @@ -32,4 +32,14 @@ class ApexCpdLexerTest extends CpdTextComparisonTest { void testTabWidth() { doTest("tabWidth"); } + + @Test + void lexExceptionExpected() { + expectLexException("class Foo { String s = \"not a string literal\"; }"); + } + + @Test + void caseInsensitiveStringLiterals() { + doTest("StringLiterals5053"); + } } diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.cls b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.cls new file mode 100644 index 0000000000..a4b7278227 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.cls @@ -0,0 +1,12 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +// See https://github.com/pmd/pmd/issues/5053 + +public with sharing class PMD7CPD { + public static void example(){ + String str = 'alice'; + str = str.replace('alice', 'dan' + '\u00A0' + '"100%"'); + } +} diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.txt b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.txt new file mode 100644 index 0000000000..d6204d7fed --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/cpd/testdata/StringLiterals5053.txt @@ -0,0 +1,43 @@ + [Image] or [Truncated image[ Bcol Ecol +L7 + [public] 1 7 + [with] 8 12 + [sharing] 13 20 + [class] 21 26 + [pmd7cpd] 27 34 + [{] 35 36 +L8 + [public] 5 11 + [static] 12 18 + [void] 19 23 + [example] 24 31 + [(] 31 32 + [)] 32 33 + [{] 33 34 +L9 + [string] 7 13 + [str] 14 17 + [=] 18 19 + ['alice'] 20 27 + [;] 27 28 +L10 + [str] 7 10 + [=] 11 12 + [str] 13 16 + [.] 16 17 + [replace] 17 24 + [(] 24 25 + ['alice'] 25 32 + [,] 32 33 + ['dan - 25 + 26-SNAPSHOT 7.2.0 ${settings.localRepository}/net/java/dev/javacc/javacc/${javacc.version}/javacc-${javacc.version}.jar From d76a38805b01c799657d354daefd00f71536e965 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 21 Jun 2024 19:36:15 +0200 Subject: [PATCH 15/23] [doc] Update to 7.3.0 --- docs/pages/pmd/userdocs/cpd/cpd_report_formats.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md index 3a3825d7c8..ebae638168 100644 --- a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md +++ b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md @@ -5,7 +5,7 @@ keywords: [formats, renderers] summary: "Overview of the built-in report formats for CPD" permalink: pmd_userdocs_cpd_report_formats.html author: Andreas Dangel -last_updated: May 2024 (7.2.0) +last_updated: June 2024 (7.3.0) --- ## Overview @@ -98,7 +98,8 @@ Starting at line 110 of /home/pmd/source/pmd-core/src/test/java/net/sourceforge/ This format uses XML to output the duplications in a more structured format. The XML format can then further be processed using XSLT transformations. See [section xslt](#xslt) for examples. -Since PMD 7.2.0 any processing errors are also reported (if `--skip-lexical-errors` is used) as additional elements `error`. +Since PMD 7.3.0 any recoverable errors are also reported as additional elements `error` to help investigate +any errors occurred during analysis. Example: From aed90ff62e5865a037f892f27392403dc0dafd2e Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 21 Jun 2024 20:57:56 +0200 Subject: [PATCH 16/23] [core] CPD: Add schema for cpd xml report --- .../net/sourceforge/pmd/cpd/XMLRenderer.java | 50 +++++++++------- .../src/main/resources/cpd-report_1_0_0.xsd | 58 ++++++++++++++++++ .../sourceforge/pmd/cpd/XMLRendererTest.java | 59 +++++++++++++++++-- 3 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 pmd-core/src/main/resources/cpd-report_1_0_0.xsd diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java index 817e56e044..8aa20956d7 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java @@ -6,7 +6,10 @@ package net.sourceforge.pmd.cpd; import java.io.IOException; import java.io.Writer; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.Map; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -20,6 +23,7 @@ import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; +import net.sourceforge.pmd.PMDVersion; import net.sourceforge.pmd.lang.document.Chars; import net.sourceforge.pmd.lang.document.FileId; import net.sourceforge.pmd.lang.document.FileLocation; @@ -32,6 +36,8 @@ import net.sourceforge.pmd.util.StringUtil; * */ public final class XMLRenderer implements CPDReportRenderer { + private static final String NAMESPACE_URI = "https://pmd-code.org/ns/cpd-report/1.0.0"; + private static final String NAMESPACE_LOCATION = "https://pmd-code.org/ns/cpd-report_1_0_0.xsd"; private String encoding; @@ -83,7 +89,7 @@ public final class XMLRenderer implements CPDReportRenderer { transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.ENCODING, encoding); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "codefragment"); + transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{" + NAMESPACE_URI + "}codefragment"); transformer.transform(new DOMSource(doc), new StreamResult(writer)); } catch (TransformerException e) { throw new IllegalStateException(e); @@ -94,14 +100,18 @@ public final class XMLRenderer implements CPDReportRenderer { @Override public void render(final CPDReport report, final Writer writer) throws IOException { final Document doc = createDocument(); - final Element root = doc.createElement("pmd-cpd"); + final Element root = doc.createElementNS(NAMESPACE_URI, "pmd-cpd"); + root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION); + + root.setAttributeNS(NAMESPACE_URI, "version", PMDVersion.VERSION); + root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); final Map numberOfTokensPerFile = report.getNumberOfTokensPerFile(); doc.appendChild(root); for (final Map.Entry pair : numberOfTokensPerFile.entrySet()) { - final Element fileElement = doc.createElement("file"); - fileElement.setAttribute("path", report.getDisplayName(pair.getKey())); - fileElement.setAttribute("totalNumberOfTokens", String.valueOf(pair.getValue())); + final Element fileElement = doc.createElementNS(NAMESPACE_URI, "file"); + fileElement.setAttributeNS(NAMESPACE_URI, "path", report.getDisplayName(pair.getKey())); + fileElement.setAttributeNS(NAMESPACE_URI, "totalNumberOfTokens", String.valueOf(pair.getValue())); root.appendChild(fileElement); } @@ -113,9 +123,9 @@ public final class XMLRenderer implements CPDReportRenderer { } for (Report.ProcessingError error : report.getProcessingErrors()) { - Element errorElt = doc.createElement("error"); - errorElt.setAttribute("filename", report.getDisplayName(error.getFileId())); - errorElt.setAttribute("msg", error.getMsg()); + Element errorElt = doc.createElementNS(NAMESPACE_URI, "error"); + errorElt.setAttributeNS(NAMESPACE_URI, "filename", report.getDisplayName(error.getFileId())); + errorElt.setAttributeNS(NAMESPACE_URI, "msg", error.getMsg()); errorElt.setTextContent(error.getDetail()); root.appendChild(errorElt); } @@ -126,17 +136,17 @@ public final class XMLRenderer implements CPDReportRenderer { private void addFilesToDuplicationElement(Document doc, Element duplication, Match match, CPDReport report) { for (Mark mark : match) { - final Element file = doc.createElement("file"); + final Element file = doc.createElementNS(NAMESPACE_URI, "file"); FileLocation loc = mark.getLocation(); - file.setAttribute("line", String.valueOf(loc.getStartLine())); + file.setAttributeNS(NAMESPACE_URI, "line", String.valueOf(loc.getStartLine())); // only remove invalid characters, escaping is done by the DOM impl. String filenameXml10 = StringUtil.removedInvalidXml10Characters(report.getDisplayName(loc.getFileId())); - file.setAttribute("path", filenameXml10); - file.setAttribute("endline", String.valueOf(loc.getEndLine())); - file.setAttribute("column", String.valueOf(loc.getStartColumn())); - file.setAttribute("endcolumn", String.valueOf(loc.getEndColumn())); - file.setAttribute("begintoken", String.valueOf(mark.getBeginTokenIndex())); - file.setAttribute("endtoken", String.valueOf(mark.getEndTokenIndex())); + file.setAttributeNS(NAMESPACE_URI, "path", filenameXml10); + file.setAttributeNS(NAMESPACE_URI, "endline", String.valueOf(loc.getEndLine())); + file.setAttributeNS(NAMESPACE_URI, "column", String.valueOf(loc.getStartColumn())); + file.setAttributeNS(NAMESPACE_URI, "endcolumn", String.valueOf(loc.getEndColumn())); + file.setAttributeNS(NAMESPACE_URI, "begintoken", String.valueOf(mark.getBeginTokenIndex())); + file.setAttributeNS(NAMESPACE_URI, "endtoken", String.valueOf(mark.getEndTokenIndex())); duplication.appendChild(file); } } @@ -146,7 +156,7 @@ public final class XMLRenderer implements CPDReportRenderer { if (codeSnippet != null) { // the code snippet has normalized line endings String platformSpecific = codeSnippet.toString().replace("\n", System.lineSeparator()); - Element codefragment = doc.createElement("codefragment"); + Element codefragment = doc.createElementNS(NAMESPACE_URI, "codefragment"); // only remove invalid characters, escaping is not necessary in CDATA. // if the string contains the end marker of a CDATA section, then the DOM impl will // create two cdata sections automatically. @@ -156,9 +166,9 @@ public final class XMLRenderer implements CPDReportRenderer { } private Element createDuplicationElement(Document doc, Match match) { - Element duplication = doc.createElement("duplication"); - duplication.setAttribute("lines", String.valueOf(match.getLineCount())); - duplication.setAttribute("tokens", String.valueOf(match.getTokenCount())); + Element duplication = doc.createElementNS(NAMESPACE_URI, "duplication"); + duplication.setAttributeNS(NAMESPACE_URI, "lines", String.valueOf(match.getLineCount())); + duplication.setAttributeNS(NAMESPACE_URI, "tokens", String.valueOf(match.getTokenCount())); return duplication; } } diff --git a/pmd-core/src/main/resources/cpd-report_1_0_0.xsd b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd new file mode 100644 index 0000000000..e5d0016b24 --- /dev/null +++ b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java index afec863903..fee4de49c6 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java @@ -11,17 +11,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.StringReader; import java.io.StringWriter; import java.util.Collections; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; import net.sourceforge.pmd.cpd.CpdTestUtils.CpdReportBuilder; import net.sourceforge.pmd.lang.ast.LexException; @@ -44,6 +54,17 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(CpdTestUtils.makeReport(Collections.emptyList()), sw); String report = sw.toString(); + assertReportIsValidSchema(report); + + assertEquals("\n" + + "\n", + report.replaceAll(" {4}timestamp=\".+?\"", " timestamp=\"XXX\"") + .replaceAll(" {4}version=\".+?\"", " version=\"XXX\""), + "namespace is missing"); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); @@ -66,6 +87,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(builder.build(), sw); String report = sw.toString(); + assertReportIsValidSchema(report); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); @@ -115,6 +137,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(builder.build(), sw); String report = sw.toString(); + assertReportIsValidSchema(report); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); @@ -135,6 +158,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(builder.build(), sw); String report = sw.toString(); + assertReportIsValidSchema(report); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); @@ -166,7 +190,7 @@ class XMLRendererTest { } @Test - void testRendererEncodedPath() throws IOException { + void testRendererEncodedPath() throws Exception { CPDReportRenderer renderer = new XMLRenderer(); CpdReportBuilder builder = new CpdReportBuilder(); final String escapeChar = "&"; @@ -177,6 +201,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(builder.build(), sw); String report = sw.toString(); + assertReportIsValidSchema(report); assertThat(report, containsString(escapeChar)); } @@ -196,6 +221,7 @@ class XMLRendererTest { final StringWriter writer = new StringWriter(); renderer.render(report, writer); final String xmlOutput = writer.toString(); + assertReportIsValidSchema(xmlOutput); final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING))); final NodeList files = doc.getElementsByTagName("file"); @@ -221,6 +247,7 @@ class XMLRendererTest { final StringWriter writer = new StringWriter(); renderer.render(report, writer); final String xmlOutput = writer.toString(); + assertReportIsValidSchema(xmlOutput); final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING))); final NodeList files = doc.getElementsByTagName("file"); @@ -236,7 +263,7 @@ class XMLRendererTest { } @Test - void testRendererXMLEscaping() throws IOException { + void testRendererXMLEscaping() throws Exception { String codefragment = "code fragment" + FORM_FEED + "\nline2\nline3\nno & escaping necessary in CDATA\nx=\"]]>\";"; CPDReportRenderer renderer = new XMLRenderer(); @@ -253,6 +280,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(builder.build(), sw); String report = sw.toString(); + assertReportIsValidSchema(report); assertThat(report, not(containsString(FORM_FEED))); assertThat(report, not(containsString(FORM_FEED_ENTITY))); assertThat(report, containsString("no & escaping necessary in CDATA")); @@ -261,7 +289,7 @@ class XMLRendererTest { } @Test - void reportContainsProcessingError() throws IOException, ParserConfigurationException, SAXException { + void reportContainsProcessingError() throws Exception { FileId fileId = FileId.fromPathLikeString("file1.txt"); Report.ProcessingError processingError = new Report.ProcessingError( new LexException(2, 1, fileId, "test exception", new RuntimeException("cause exception")), @@ -270,7 +298,7 @@ class XMLRendererTest { StringWriter sw = new StringWriter(); renderer.render(CpdTestUtils.makeReport(Collections.emptyList(), Collections.emptyMap(), Collections.singletonList(processingError)), sw); String report = sw.toString(); - System.out.println("report = " + report); + assertReportIsValidSchema(report); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); @@ -286,4 +314,27 @@ class XMLRendererTest { String textContent = error.getTextContent(); assertEquals(processingError.getDetail(), textContent); } + + private static void assertReportIsValidSchema(String report) throws SAXException, ParserConfigurationException, IOException { + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = schemaFactory.newSchema(new StreamSource(XMLRenderer.class.getResourceAsStream("/cpd-report_1_0_0.xsd"))); + + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + saxParserFactory.setNamespaceAware(true); + saxParserFactory.setValidating(false); + saxParserFactory.setSchema(schema); + + SAXParser saxParser = saxParserFactory.newSAXParser(); + saxParser.parse(new InputSource(new StringReader(report)), new DefaultHandler() { + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + + @Override + public void warning(SAXParseException e) throws SAXException { + throw e; + } + }); + } } From 29983a91a20d6c927671f83f853e03a9e9d617de Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Sun, 23 Jun 2024 19:00:01 +0200 Subject: [PATCH 17/23] Fix schema types Co-authored-by: Juan Martin Sotuyo Dodero --- pmd-core/src/main/resources/cpd-report_1_0_0.xsd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pmd-core/src/main/resources/cpd-report_1_0_0.xsd b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd index e5d0016b24..e159d04229 100644 --- a/pmd-core/src/main/resources/cpd-report_1_0_0.xsd +++ b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd @@ -19,12 +19,12 @@ - + - + @@ -45,8 +45,8 @@ - - + + From a8ab215010a11bf96394a116f76e7f755695ee60 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Sun, 23 Jun 2024 19:21:49 +0200 Subject: [PATCH 18/23] Clarify namespace, version, pmdVersion --- .../java/net/sourceforge/pmd/cpd/XMLRenderer.java | 8 +++++--- pmd-core/src/main/resources/cpd-report_1_0_0.xsd | 13 +++++++++++-- .../net/sourceforge/pmd/cpd/XMLRendererTest.java | 13 +++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java index 8aa20956d7..4bd4524ed4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java @@ -36,8 +36,9 @@ import net.sourceforge.pmd.util.StringUtil; * */ public final class XMLRenderer implements CPDReportRenderer { - private static final String NAMESPACE_URI = "https://pmd-code.org/ns/cpd-report/1.0.0"; - private static final String NAMESPACE_LOCATION = "https://pmd-code.org/ns/cpd-report_1_0_0.xsd"; + private static final String NAMESPACE_URI = "https://pmd-code.org/schema/cpd-report"; + private static final String NAMESPACE_LOCATION = "https://pmd.github.io/schema/cpd-report_1_0_0.xsd"; + private static final String SCHEMA_VERSION = "1.0.0"; private String encoding; @@ -103,7 +104,8 @@ public final class XMLRenderer implements CPDReportRenderer { final Element root = doc.createElementNS(NAMESPACE_URI, "pmd-cpd"); root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION); - root.setAttributeNS(NAMESPACE_URI, "version", PMDVersion.VERSION); + root.setAttributeNS(NAMESPACE_URI, "version", SCHEMA_VERSION); + root.setAttributeNS(NAMESPACE_URI, "pmdVersion", PMDVersion.VERSION); root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); final Map numberOfTokensPerFile = report.getNumberOfTokensPerFile(); doc.appendChild(root); diff --git a/pmd-core/src/main/resources/cpd-report_1_0_0.xsd b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd index e159d04229..71745eda95 100644 --- a/pmd-core/src/main/resources/cpd-report_1_0_0.xsd +++ b/pmd-core/src/main/resources/cpd-report_1_0_0.xsd @@ -1,10 +1,18 @@ + + + + @@ -13,6 +21,7 @@ + diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java index fee4de49c6..b22dc67307 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLRendererTest.java @@ -57,14 +57,15 @@ class XMLRendererTest { assertReportIsValidSchema(report); assertEquals("\n" - + "\n", - report.replaceAll(" {4}timestamp=\".+?\"", " timestamp=\"XXX\"") - .replaceAll(" {4}version=\".+?\"", " version=\"XXX\""), - "namespace is missing"); + + " version=\"1.0.0\"\n" + + " xsi:schemaLocation=\"https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd\"/>\n", + report.replaceAll("timestamp=\".+?\"", "timestamp=\"XXX\"") + .replaceAll("pmdVersion=\".+?\"", "pmdVersion=\"XXX\""), + "namespace is missing or wrong"); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); From 0340bf0568463b8ee94f7c6761eb6e671e7056b0 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Sun, 23 Jun 2024 19:45:38 +0200 Subject: [PATCH 19/23] [cli] Fix unit test after cpd report change --- .../net/sourceforge/pmd/cli/CpdCliTest.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java b/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java index af1eee72aa..7d13a82a16 100644 --- a/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java +++ b/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java @@ -38,6 +38,14 @@ class CpdCliTest extends BaseCliTest { private static final String SRC_DIR = BASE_RES_PATH + "files/"; private static final Path SRC_PATH = Paths.get(SRC_DIR).toAbsolutePath(); + private static final String CPD_REPORT_HEADER_PATTERN = "<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?>\n" + + "\n"; + private static final Map NUMBER_OF_TOKENS; static { @@ -68,8 +76,11 @@ class CpdCliTest extends BaseCliTest { void testEmptyResultRendering() throws Exception { final String expectedFilesXml = getExpectedFileEntriesXml(NUMBER_OF_TOKENS.keySet()); runCliSuccessfully("--minimum-tokens", "340", "--language", "java", "--dir", SRC_DIR, "--format", "xml") - .verify(result -> result.checkStdOut(equalTo( - "" + "\n" + "\n" + expectedFilesXml + "\n" + .verify(result -> result.checkStdOut(containsPattern(CPD_REPORT_HEADER_PATTERN + + "\\Q" // quote start + + expectedFilesXml + + "\n" + + "\\E" // quote end ))); } @@ -176,8 +187,8 @@ class CpdCliTest extends BaseCliTest { @Test void testNoDuplicatesResultRendering() throws Exception { - String expectedReport = "\n" - + "\n" + String expectedReportPattern = CPD_REPORT_HEADER_PATTERN + + "\\Q" // quote start + " \n" + " \n" + " \n" - + "\n"; + + "\n" + + "\\E"; // quote end runCliSuccessfully("--minimum-tokens", "340", "--language", "java", "--dir", SRC_DIR, "--format", "xml") - .verify(result -> result.checkStdOut(equalTo(expectedReport))); + .verify(result -> result.checkStdOut(containsPattern(expectedReportPattern))); } /** @@ -251,9 +263,7 @@ class CpdCliTest extends BaseCliTest { runCli(OK, "--minimum-tokens", "5", "--language", "ecmascript", "-f", "xml", "-d", BASE_RES_PATH + "tsFiles/") - .checkStdOut(equalTo( - "\n\n" - )); + .checkStdOut(containsPattern(CPD_REPORT_HEADER_PATTERN.substring(0, CPD_REPORT_HEADER_PATTERN.length() - 2) + "/>")); } @Test From 33f9268cf7fde48dcd713750b4699c461e10d838 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Sun, 23 Jun 2024 20:39:41 +0200 Subject: [PATCH 20/23] [core] Provide a backwards compatible XMLOldRenderer as "xmlold" --- .../pmd/userdocs/cpd/cpd_report_formats.md | 7 +- docs/pages/release_notes.md | 19 +++- .../java/net/sourceforge/pmd/ant/CPDTask.java | 7 +- .../net/sourceforge/pmd/cli/CpdCliTest.java | 1 - pmd-core/etc/xslt/cpdhtml-v2.xslt | 2 +- pmd-core/etc/xslt/cpdhtml.xslt | 2 +- .../sourceforge/pmd/cpd/CPDConfiguration.java | 1 + .../sourceforge/pmd/cpd/XMLOldRenderer.java | 34 +++++++ .../net/sourceforge/pmd/cpd/XMLRenderer.java | 87 ++++++++++++------ .../pmd/cpd/XMLOldRendererTest.java | 88 +++++++++++++++++++ 10 files changed, 214 insertions(+), 34 deletions(-) create mode 100644 pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLOldRenderer.java create mode 100644 pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLOldRendererTest.java diff --git a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md index ebae638168..02b760aeb9 100644 --- a/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md +++ b/docs/pages/pmd/userdocs/cpd/cpd_report_formats.md @@ -105,7 +105,12 @@ Example: ```xml - + diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index e953a6f650..b81538f603 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -29,10 +29,27 @@ This is a {{ site.pmd.release_type }} release. #### CPD Report Format XML -The CPD XML report will now also contain processing errors (if CPD is called with `--skip-lexical-errors`). +There are some important changes: + +1. The XML format will now use an XSD schema, that is available at . + This schema defines the valid elements and attributes that one can expect from a CPD report. +2. The root element `pmd-cpd` contains the new attributes `pmdVersion`, `timestamp` and `version`. The latter is + the schema version and is currently "1.0.0". +3. The CPD XML report will now also contain recoverable errors as additional `` elements. See [Report formats for CPD](pmd_userdocs_cpd_report_formats.html#xml) for an example. +The XML format should be compatible as only attributes and elements have been added. However, if you parse +the document with a namespace aware parser, you might encounter some issues like no elements being found. +In case the new format doesn't work for you (e.g. namespaces, unexpected error elements), you can +go back using the old format with the renderer "xmlold" ({%jdoc core::cpd.XMLOldRenderer %}). Note, that +this old renderer is deprecated and only there for compatibility reasons. Whatever tooling is used to +read the XML format should be updated. + +#### Deprecated for removal + +* {%jdoc !!core::cpd.XMLOldRenderer %} (the CPD format "xmlold"). + ### ✨ External Contributions {% endtocmaker %} diff --git a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/CPDTask.java b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/CPDTask.java index 607186cb8a..58bf8967d5 100644 --- a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/CPDTask.java +++ b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/CPDTask.java @@ -29,6 +29,7 @@ import net.sourceforge.pmd.cpd.CPDReportRenderer; import net.sourceforge.pmd.cpd.CSVRenderer; import net.sourceforge.pmd.cpd.CpdAnalysis; import net.sourceforge.pmd.cpd.SimpleRenderer; +import net.sourceforge.pmd.cpd.XMLOldRenderer; import net.sourceforge.pmd.cpd.XMLRenderer; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; @@ -66,6 +67,8 @@ public class CPDTask extends Task { private static final String TEXT_FORMAT = "text"; private static final String XML_FORMAT = "xml"; + @Deprecated + private static final String XMLOLD_FORMAT = "xmlold"; private static final String CSV_FORMAT = "csv"; private String format = TEXT_FORMAT; @@ -177,6 +180,8 @@ public class CPDTask extends Task { return new SimpleRenderer(); } else if (CSV_FORMAT.equals(format)) { return new CSVRenderer(); + } else if (XMLOLD_FORMAT.equals(format)) { + return new XMLOldRenderer(); } return new XMLRenderer(); } @@ -253,7 +258,7 @@ public class CPDTask extends Task { } public static class FormatAttribute extends EnumeratedAttribute { - private static final String[] FORMATS = new String[] { XML_FORMAT, TEXT_FORMAT, CSV_FORMAT }; + private static final String[] FORMATS = new String[] { XML_FORMAT, TEXT_FORMAT, CSV_FORMAT, XMLOLD_FORMAT }; @Override public String[] getValues() { diff --git a/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java b/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java index 7d13a82a16..b8388af890 100644 --- a/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java +++ b/pmd-cli/src/test/java/net/sourceforge/pmd/cli/CpdCliTest.java @@ -10,7 +10,6 @@ import static net.sourceforge.pmd.util.CollectionUtil.listOf; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import java.nio.charset.StandardCharsets; diff --git a/pmd-core/etc/xslt/cpdhtml-v2.xslt b/pmd-core/etc/xslt/cpdhtml-v2.xslt index c1862d9cba..0856de0739 100644 --- a/pmd-core/etc/xslt/cpdhtml-v2.xslt +++ b/pmd-core/etc/xslt/cpdhtml-v2.xslt @@ -1,5 +1,5 @@ - + diff --git a/pmd-core/etc/xslt/cpdhtml.xslt b/pmd-core/etc/xslt/cpdhtml.xslt index ef6e9ac94f..18e2f7b5a0 100644 --- a/pmd-core/etc/xslt/cpdhtml.xslt +++ b/pmd-core/etc/xslt/cpdhtml.xslt @@ -1,7 +1,7 @@ - + 30 diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDConfiguration.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDConfiguration.java index d97cc8068a..f29b64b8d6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDConfiguration.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/CPDConfiguration.java @@ -39,6 +39,7 @@ public class CPDConfiguration extends AbstractConfiguration { static { RENDERERS.put(DEFAULT_RENDERER, SimpleRenderer.class); RENDERERS.put("xml", XMLRenderer.class); + RENDERERS.put("xmlold", XMLOldRenderer.class); RENDERERS.put("csv", CSVRenderer.class); RENDERERS.put("csv_with_linecount_per_file", CSVWithLinecountPerFileRenderer.class); RENDERERS.put("vs", VSRenderer.class); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLOldRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLOldRenderer.java new file mode 100644 index 0000000000..d5867e1399 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLOldRenderer.java @@ -0,0 +1,34 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.cpd; + +import java.io.IOException; +import java.io.Writer; + +/** + * Provides backwards compatible XML renderer, which doesn't use namespaces, schema and + * doesn't output error information. + * + *

This renderer is available as "xmlold". + * + * @deprecated Update your tools to use the standard XML renderer "xml" again. + */ +@Deprecated +public class XMLOldRenderer implements CPDReportRenderer { + private final XMLRenderer xmlRenderer; + + public XMLOldRenderer() { + this(null); + } + + public XMLOldRenderer(String encoding) { + this.xmlRenderer = new XMLRenderer(encoding, false); + } + + @Override + public void render(CPDReport report, Writer writer) throws IOException { + xmlRenderer.render(report, writer); + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java index 4bd4524ed4..8e9b8fe5b6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/XMLRenderer.java @@ -42,11 +42,13 @@ public final class XMLRenderer implements CPDReportRenderer { private String encoding; + private final boolean newFormat; + /** * Creates a XML Renderer with the default (platform dependent) encoding. */ public XMLRenderer() { - this(null); + this(null, true); } /** @@ -57,7 +59,12 @@ public final class XMLRenderer implements CPDReportRenderer { * dependent) encoding is used. */ public XMLRenderer(String encoding) { + this(encoding, true); + } + + XMLRenderer(String encoding, boolean newFormat) { setEncoding(encoding); + this.newFormat = newFormat; } public void setEncoding(String encoding) { @@ -90,7 +97,11 @@ public final class XMLRenderer implements CPDReportRenderer { transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.ENCODING, encoding); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{" + NAMESPACE_URI + "}codefragment"); + if (newFormat) { + transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{" + NAMESPACE_URI + "}codefragment"); + } else { + transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "codefragment"); + } transformer.transform(new DOMSource(doc), new StreamResult(writer)); } catch (TransformerException e) { throw new IllegalStateException(e); @@ -101,19 +112,22 @@ public final class XMLRenderer implements CPDReportRenderer { @Override public void render(final CPDReport report, final Writer writer) throws IOException { final Document doc = createDocument(); - final Element root = doc.createElementNS(NAMESPACE_URI, "pmd-cpd"); - root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION); + final Element root = createElement(doc, "pmd-cpd"); + + if (newFormat) { + root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION); + root.setAttributeNS(NAMESPACE_URI, "version", SCHEMA_VERSION); + root.setAttributeNS(NAMESPACE_URI, "pmdVersion", PMDVersion.VERSION); + root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } - root.setAttributeNS(NAMESPACE_URI, "version", SCHEMA_VERSION); - root.setAttributeNS(NAMESPACE_URI, "pmdVersion", PMDVersion.VERSION); - root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); final Map numberOfTokensPerFile = report.getNumberOfTokensPerFile(); doc.appendChild(root); for (final Map.Entry pair : numberOfTokensPerFile.entrySet()) { - final Element fileElement = doc.createElementNS(NAMESPACE_URI, "file"); - fileElement.setAttributeNS(NAMESPACE_URI, "path", report.getDisplayName(pair.getKey())); - fileElement.setAttributeNS(NAMESPACE_URI, "totalNumberOfTokens", String.valueOf(pair.getValue())); + final Element fileElement = createElement(doc, "file"); + setAttribute(fileElement, "path", report.getDisplayName(pair.getKey())); + setAttribute(fileElement, "totalNumberOfTokens", String.valueOf(pair.getValue())); root.appendChild(fileElement); } @@ -124,12 +138,14 @@ public final class XMLRenderer implements CPDReportRenderer { root.appendChild(dupElt); } - for (Report.ProcessingError error : report.getProcessingErrors()) { - Element errorElt = doc.createElementNS(NAMESPACE_URI, "error"); - errorElt.setAttributeNS(NAMESPACE_URI, "filename", report.getDisplayName(error.getFileId())); - errorElt.setAttributeNS(NAMESPACE_URI, "msg", error.getMsg()); - errorElt.setTextContent(error.getDetail()); - root.appendChild(errorElt); + if (newFormat) { + for (Report.ProcessingError error : report.getProcessingErrors()) { + Element errorElt = doc.createElementNS(NAMESPACE_URI, "error"); + errorElt.setAttributeNS(NAMESPACE_URI, "filename", report.getDisplayName(error.getFileId())); + errorElt.setAttributeNS(NAMESPACE_URI, "msg", error.getMsg()); + errorElt.setTextContent(error.getDetail()); + root.appendChild(errorElt); + } } dumpDocToWriter(doc, writer); @@ -138,17 +154,17 @@ public final class XMLRenderer implements CPDReportRenderer { private void addFilesToDuplicationElement(Document doc, Element duplication, Match match, CPDReport report) { for (Mark mark : match) { - final Element file = doc.createElementNS(NAMESPACE_URI, "file"); + final Element file = createElement(doc, "file"); FileLocation loc = mark.getLocation(); - file.setAttributeNS(NAMESPACE_URI, "line", String.valueOf(loc.getStartLine())); + setAttribute(file, "line", String.valueOf(loc.getStartLine())); // only remove invalid characters, escaping is done by the DOM impl. String filenameXml10 = StringUtil.removedInvalidXml10Characters(report.getDisplayName(loc.getFileId())); - file.setAttributeNS(NAMESPACE_URI, "path", filenameXml10); - file.setAttributeNS(NAMESPACE_URI, "endline", String.valueOf(loc.getEndLine())); - file.setAttributeNS(NAMESPACE_URI, "column", String.valueOf(loc.getStartColumn())); - file.setAttributeNS(NAMESPACE_URI, "endcolumn", String.valueOf(loc.getEndColumn())); - file.setAttributeNS(NAMESPACE_URI, "begintoken", String.valueOf(mark.getBeginTokenIndex())); - file.setAttributeNS(NAMESPACE_URI, "endtoken", String.valueOf(mark.getEndTokenIndex())); + setAttribute(file, "path", filenameXml10); + setAttribute(file, "endline", String.valueOf(loc.getEndLine())); + setAttribute(file, "column", String.valueOf(loc.getStartColumn())); + setAttribute(file, "endcolumn", String.valueOf(loc.getEndColumn())); + setAttribute(file, "begintoken", String.valueOf(mark.getBeginTokenIndex())); + setAttribute(file, "endtoken", String.valueOf(mark.getEndTokenIndex())); duplication.appendChild(file); } } @@ -158,7 +174,7 @@ public final class XMLRenderer implements CPDReportRenderer { if (codeSnippet != null) { // the code snippet has normalized line endings String platformSpecific = codeSnippet.toString().replace("\n", System.lineSeparator()); - Element codefragment = doc.createElementNS(NAMESPACE_URI, "codefragment"); + Element codefragment = createElement(doc, "codefragment"); // only remove invalid characters, escaping is not necessary in CDATA. // if the string contains the end marker of a CDATA section, then the DOM impl will // create two cdata sections automatically. @@ -168,9 +184,24 @@ public final class XMLRenderer implements CPDReportRenderer { } private Element createDuplicationElement(Document doc, Match match) { - Element duplication = doc.createElementNS(NAMESPACE_URI, "duplication"); - duplication.setAttributeNS(NAMESPACE_URI, "lines", String.valueOf(match.getLineCount())); - duplication.setAttributeNS(NAMESPACE_URI, "tokens", String.valueOf(match.getTokenCount())); + Element duplication = createElement(doc, "duplication"); + setAttribute(duplication, "lines", String.valueOf(match.getLineCount())); + setAttribute(duplication, "tokens", String.valueOf(match.getTokenCount())); return duplication; } + + private Element createElement(Document doc, String name) { + if (newFormat) { + return doc.createElementNS(NAMESPACE_URI, name); + } + return doc.createElement(name); + } + + private void setAttribute(Element element, String name, String value) { + if (newFormat) { + element.setAttributeNS(NAMESPACE_URI, name, value); + } else { + element.setAttribute(name, value); + } + } } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLOldRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLOldRendererTest.java new file mode 100644 index 0000000000..3c0c74b340 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/XMLOldRendererTest.java @@ -0,0 +1,88 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.cpd; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import net.sourceforge.pmd.lang.document.FileId; + +class XMLOldRendererTest { + private static final String ENCODING = (String) System.getProperties().get("file.encoding"); + + @Test + void testWithNoDuplication() throws IOException, ParserConfigurationException, SAXException { + CPDReportRenderer renderer = new XMLOldRenderer(); + StringWriter sw = new StringWriter(); + renderer.render(CpdTestUtils.makeReport(Collections.emptyList()), sw); + String report = sw.toString(); + + assertEquals("\n\n", + report, + "no namespace expected"); + + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); + NodeList nodes = doc.getChildNodes(); + Node n = nodes.item(0); + assertEquals("pmd-cpd", n.getNodeName()); + assertEquals(0, doc.getElementsByTagName("duplication").getLength()); + } + + @Test + void testWithOneDuplication() throws Exception { + CPDReportRenderer renderer = new XMLOldRenderer(); + CpdTestUtils.CpdReportBuilder builder = new CpdTestUtils.CpdReportBuilder(); + int lineCount = 6; + FileId foo1 = CpdTestUtils.FOO_FILE_ID; + Mark mark1 = builder.createMark("public", foo1, 1, lineCount); + Mark mark2 = builder.createMark("stuff", foo1, 73, lineCount); + builder.addMatch(new Match(75, mark1, mark2)); + + StringWriter sw = new StringWriter(); + renderer.render(builder.build(), sw); + String report = sw.toString(); + + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(new ByteArrayInputStream(report.getBytes(ENCODING))); + NodeList dupes = doc.getElementsByTagName("duplication"); + assertEquals(1, dupes.getLength()); + Node file = dupes.item(0).getFirstChild(); + while (file != null && file.getNodeType() != Node.ELEMENT_NODE) { + file = file.getNextSibling(); + } + if (file != null) { + assertEquals("1", file.getAttributes().getNamedItem("line").getNodeValue()); + assertEquals(foo1.getAbsolutePath(), file.getAttributes().getNamedItem("path").getNodeValue()); + assertEquals("6", file.getAttributes().getNamedItem("endline").getNodeValue()); + assertEquals("1", file.getAttributes().getNamedItem("column").getNodeValue()); + assertEquals("1", file.getAttributes().getNamedItem("endcolumn").getNodeValue()); + file = file.getNextSibling(); + while (file != null && file.getNodeType() != Node.ELEMENT_NODE) { + file = file.getNextSibling(); + } + } + if (file != null) { + assertEquals("73", file.getAttributes().getNamedItem("line").getNodeValue()); + assertEquals("78", file.getAttributes().getNamedItem("endline").getNodeValue()); + assertEquals("1", file.getAttributes().getNamedItem("column").getNodeValue()); + assertEquals("1", file.getAttributes().getNamedItem("endcolumn").getNodeValue()); + } + assertEquals(1, doc.getElementsByTagName("codefragment").getLength()); + assertEquals(CpdTestUtils.generateDummyContent(lineCount), doc.getElementsByTagName("codefragment").item(0).getTextContent()); + } +} From 40bd882f3e721bc7677371dc92aae7bf9ffdd6a6 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Sun, 23 Jun 2024 21:03:12 +0200 Subject: [PATCH 21/23] Fix cpd xslt sample templates --- pmd-core/etc/xslt/cpdhtml-v2.xslt | 20 ++++++++++--------- pmd-core/etc/xslt/cpdhtml.xslt | 20 ++++++++++--------- .../sourceforge/pmd/cpd/SampleCpdReport.xml | 11 +++++++--- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/pmd-core/etc/xslt/cpdhtml-v2.xslt b/pmd-core/etc/xslt/cpdhtml-v2.xslt index 0856de0739..614d0f793f 100644 --- a/pmd-core/etc/xslt/cpdhtml-v2.xslt +++ b/pmd-core/etc/xslt/cpdhtml-v2.xslt @@ -1,5 +1,7 @@ - + @@ -68,10 +70,10 @@

- - - - + + + +
Approximate number of bytes
@@ -91,7 +93,7 @@ - + @@ -133,7 +135,7 @@ - + @@ -141,7 +143,7 @@ - + @@ -152,7 +154,7 @@
columnendcolumnlineendlinepath
-
+
diff --git a/pmd-core/etc/xslt/cpdhtml.xslt b/pmd-core/etc/xslt/cpdhtml.xslt index 18e2f7b5a0..97265d97c0 100644 --- a/pmd-core/etc/xslt/cpdhtml.xslt +++ b/pmd-core/etc/xslt/cpdhtml.xslt @@ -1,11 +1,13 @@ - + 30 - + @@ -46,10 +48,10 @@ Approx # bytes - - - - + + + +

@@ -58,13 +60,13 @@

- +
IDFilesLines
- +
../src/.html# line
@@ -82,7 +84,7 @@
diff --git a/pmd-core/src/test/resources/net/sourceforge/pmd/cpd/SampleCpdReport.xml b/pmd-core/src/test/resources/net/sourceforge/pmd/cpd/SampleCpdReport.xml index e0e6b0c465..ce6169ae59 100644 --- a/pmd-core/src/test/resources/net/sourceforge/pmd/cpd/SampleCpdReport.xml +++ b/pmd-core/src/test/resources/net/sourceforge/pmd/cpd/SampleCpdReport.xml @@ -1,8 +1,13 @@ - + - - + + From b5f6d4e68a3ea79f0e5ab4b84b69eeb77fe5be0a Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Thu, 27 Jun 2024 08:59:32 +0200 Subject: [PATCH 22/23] [core] Update report schema location to https://pmd.github.io/schema/report_2_0_0.xsd --- docs/pages/pmd/userdocs/pmd_report_formats.md | 2 +- .../net/sourceforge/pmd/ant/xml/expected-pmd-ant-xml.xml | 2 +- .../main/java/net/sourceforge/pmd/renderers/XMLRenderer.java | 2 +- .../java/net/sourceforge/pmd/renderers/XMLRendererTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pages/pmd/userdocs/pmd_report_formats.md b/docs/pages/pmd/userdocs/pmd_report_formats.md index e5c32a25be..44c0512478 100644 --- a/docs/pages/pmd/userdocs/pmd_report_formats.md +++ b/docs/pages/pmd/userdocs/pmd_report_formats.md @@ -282,7 +282,7 @@ Example: diff --git a/pmd-ant/src/test/resources/net/sourceforge/pmd/ant/xml/expected-pmd-ant-xml.xml b/pmd-ant/src/test/resources/net/sourceforge/pmd/ant/xml/expected-pmd-ant-xml.xml index 6045610122..025ce8d6d5 100644 --- a/pmd-ant/src/test/resources/net/sourceforge/pmd/ant/xml/expected-pmd-ant-xml.xml +++ b/pmd-ant/src/test/resources/net/sourceforge/pmd/ant/xml/expected-pmd-ant-xml.xml @@ -1,5 +1,5 @@ - + Test Rule 2 diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java index bddc59b842..b18c2ff0d0 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java @@ -43,7 +43,7 @@ public class XMLRenderer extends AbstractIncrementingRenderer { PropertyFactory.stringProperty("encoding").desc("XML encoding format").defaultValue("UTF-8").build(); private static final String PMD_REPORT_NS_URI = "http://pmd.sourceforge.net/report/2.0.0"; - private static final String PMD_REPORT_NS_LOCATION = "http://pmd.sourceforge.net/report_2_0_0.xsd"; + private static final String PMD_REPORT_NS_LOCATION = "https://pmd.github.io/schema/report_2_0_0.xsd"; private static final String XSI_NS_PREFIX = "xsi"; private XMLStreamWriter xmlWriter; diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java index a74589fbdf..d322d3885b 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java @@ -132,7 +132,7 @@ class XMLRendererTest extends AbstractRendererTest { return "" + EOL + "" + EOL; } From 55385a0255785f5d76b3d08ff3e728e9e6d6a46f Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Thu, 27 Jun 2024 09:00:11 +0200 Subject: [PATCH 23/23] Bump build-tools from 26-SNAPSHOT to 26 --- .github/workflows/build.yml | 2 +- .github/workflows/git-repo-sync.yml | 2 +- .github/workflows/troubleshooting.yml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b19e8cc950..c27288ee6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: run: | echo "LANG=en_US.UTF-8" >> $GITHUB_ENV echo "MAVEN_OPTS=-Daether.connector.http.connectionMaxTtl=180 -DautoReleaseAfterClose=true -DstagingProgressTimeoutMinutes=30" >> $GITHUB_ENV - echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/master/scripts" >> $GITHUB_ENV + echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/26/scripts" >> $GITHUB_ENV - name: Check Environment shell: bash run: | diff --git a/.github/workflows/git-repo-sync.yml b/.github/workflows/git-repo-sync.yml index c564eeea92..e4413efb3f 100644 --- a/.github/workflows/git-repo-sync.yml +++ b/.github/workflows/git-repo-sync.yml @@ -24,7 +24,7 @@ jobs: shell: bash run: | echo "LANG=en_US.UTF-8" >> $GITHUB_ENV - echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/master/scripts" >> $GITHUB_ENV + echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/26/scripts" >> $GITHUB_ENV - name: Sync run: .ci/git-repo-sync.sh shell: bash diff --git a/.github/workflows/troubleshooting.yml b/.github/workflows/troubleshooting.yml index 58b1c56fcc..737c1a7ea8 100644 --- a/.github/workflows/troubleshooting.yml +++ b/.github/workflows/troubleshooting.yml @@ -36,7 +36,7 @@ jobs: run: | echo "LANG=en_US.UTF-8" >> $GITHUB_ENV echo "MAVEN_OPTS=-Daether.connector.http.connectionMaxTtl=180 -DstagingProgressTimeoutMinutes=30" >> $GITHUB_ENV - echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/master/scripts" >> $GITHUB_ENV + echo "PMD_CI_SCRIPTS_URL=https://raw.githubusercontent.com/pmd/build-tools/26/scripts" >> $GITHUB_ENV - name: Check Environment shell: bash run: | diff --git a/pom.xml b/pom.xml index 723c606af7..9e4f3c750a 100644 --- a/pom.xml +++ b/pom.xml @@ -115,7 +115,7 @@ -Xmx512m -Dfile.encoding=${project.build.sourceEncoding} ${extraArgLine} - 26-SNAPSHOT + 26 7.2.0 ${settings.localRepository}/net/java/dev/javacc/javacc/${javacc.version}/javacc-${javacc.version}.jar