diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTCatchStatementTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTCatchStatementTest.kt new file mode 100644 index 0000000000..7274942ca7 --- /dev/null +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTCatchStatementTest.kt @@ -0,0 +1,64 @@ +package net.sourceforge.pmd.lang.java.ast + +import io.kotlintest.matchers.collections.shouldContainExactly +import io.kotlintest.should +import io.kotlintest.shouldBe +import io.kotlintest.specs.FunSpec +import net.sourceforge.pmd.lang.java.ast.JavaVersion.Companion.Latest +import net.sourceforge.pmd.lang.java.ast.JavaVersion.J1_5 +import net.sourceforge.pmd.lang.java.ast.JavaVersion.J1_7 +import java.io.IOException + + +class ASTCatchStatementTest : FunSpec({ + + + parserTest("Test single type", javaVersions = J1_5..Latest) { + + importedTypes += IOException::class.java + + "try { } catch (IOException ioe) { }" should matchStmt { + child { } + child { + it.isMulticatchStatement shouldBe false + + unspecifiedChildren(2) + } + } + + } + + parserTest("Test multicatch", javaVersions = J1_7..Latest) { + + importedTypes += IOException::class.java + + "try { } catch (IOException | AssertionError e) { }" should matchStmt { + child { } + child { + it.isMulticatchStatement shouldBe true + + val catchNode = it + + child { + val ioe = child(ignoreChildren = true) { + it.type shouldBe IOException::class.java + } + + val aerr = child(ignoreChildren = true) { + it.type shouldBe java.lang.AssertionError::class.java + } + + catchNode.caughtExceptionTypeNodes.shouldContainExactly(ioe, aerr) + catchNode.caughtExceptionTypes.shouldContainExactly(catchNode.caughtExceptionTypeNodes.map { it.type }) + + child { } + } + + child { } + } + } + + } + + +}) \ No newline at end of file diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/Java11Test.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/Java11Test.kt index a323aae993..193cfc4637 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/Java11Test.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/Java11Test.kt @@ -9,7 +9,7 @@ import net.sourceforge.pmd.lang.java.ast.JavaVersion.Companion.Latest class Java11Test : FunSpec({ - parserTest("Test lambda parameter with var keyword", javaVersionRange = J1_8..J10) { + parserTest("Test lambda parameter with var keyword", javaVersions = J1_8..J10) { "(var x) -> String.valueOf(x)" should matchExpr { child { @@ -75,7 +75,7 @@ class Java11Test : FunSpec({ } } - parserTest("Test lambda parameter with var keyword", javaVersionRange = J11..Latest) { + parserTest("Test lambda parameter with var keyword", javaVersions = J11..Latest) { "(var x) -> String.valueOf(x)" should matchExpr { child { diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinParsingUtils.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinParsingUtils.kt index 76862d91f5..436fbc0860 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinParsingUtils.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinParsingUtils.kt @@ -10,148 +10,17 @@ import net.sourceforge.pmd.lang.java.ParserTstUtil /** - * Finds the first descendant of type [N] of [this] node which is - * accessible in a straight line. The descendant must be accessible - * from the [this] on a path where each node has a single child. - * - * If one node has another child, the search is aborted and the method - * returns null. + * Represents the different Java language versions. */ -fun Node.findFirstNodeOnStraightLine(klass: Class): N? { - return when { - klass.isInstance(this) -> { - @SuppressWarnings("UNCHECKED_CAST") - val n = this as N - n - } - else -> if (this.jjtGetNumChildren() == 1) jjtGetChild(0).findFirstNodeOnStraightLine(klass) else null - } -} - - -/** - * Parse the string in an expression context, and finds the first descendant of type [N]. - * The descendant is searched for by [findFirstNodeOnStraightLine], to prevent accidental - * mis-selection of a node. In such a case, a [NoSuchElementException] is thrown, and you - * should fix your test case. - * - * @param expr The expression to parse - * @param javaVersion The java parser version to use - * @param N type of node to find - * - * @return The first descendant of type [N] found in the parsed expression - * - * @throws NoSuchElementException If no node of type [N] is found by [findFirstNodeOnStraightLine] - * @throws ParseException If the expression is no valid expression for the given language version - */ -inline fun parseExpression(expr: String, javaVersion: JavaVersion = JavaVersion.Latest): N = - parseAstExpression(expr, javaVersion).findFirstNodeOnStraightLine(N::class.java) - ?: throw NoSuchElementException("No node of type ${N::class.java.simpleName} in the given expression:\n\t$expr") - - -/** - * Parse the string in an expression context. The parsed expression is the child of the - * returned [ASTExpression]. Sometimes there may be many layers between the returned node - * and the node of interest, e.g. when the expression is contained in a PrimaryExpression/PrimaryPrefix - * construct, in which case [parseExpression] saves some keystrokes. - * - * @param expr The expression to parse - * @param javaVersion The java parser version to use - * - * @return An [ASTExpression] whose child is the given expression - * - * @throws ParseException If the argument is no valid expression for the given language version - */ -fun parseAstExpression(expr: String, javaVersion: JavaVersion = JavaVersion.Latest): ASTExpression { - - val source = """ - class Foo { - { - Object o = $expr; - } - } - """.trimIndent() - - val acu = ParserTstUtil.parseAndTypeResolveJava(javaVersion.pmdName, source) - - return acu.getFirstDescendantOfType(ASTVariableInitializer::class.java).jjtGetChild(0) as ASTExpression -} - - -/** - * Parse the string in a statement context, and finds the first descendant of type [N]. - * The descendant is searched for by [findFirstNodeOnStraightLine], to prevent accidental - * mis-selection of a node. In such a case, a [NoSuchElementException] is thrown, and you - * should fix your test case. - * - * @param stmt The statement to parse - * @param javaVersion The java parser version to use - * @param N The type of node to find - * - * @return The first descendant of type [N] found in the parsed expression - * - * @throws NoSuchElementException If no node of type [N] is found by [findFirstNodeOnStraightLine] - * @throws ParseException If the argument is no valid statement for the given language version. - * Don't forget the semicolon! - */ -inline fun parseStatement(stmt: String, javaVersion: JavaVersion = JavaVersion.Latest): N = - parseAstStatement(stmt, javaVersion).findFirstNodeOnStraightLine(N::class.java) - ?: throw NoSuchElementException("No node of type ${N::class.java.simpleName} in the given statement:\n\t$stmt") - -/** - * Parse the string in a statement context. The parsed statement is the child of the - * returned [ASTBlockStatement]. Note that [parseStatement] can save you some keystrokes - * because it finds a descendant of the wanted type. - * - * @param statement The statement to parse - * @param javaVersion The java parser version to use - * - * @return An [ASTBlockStatement] whose child is the given statement - * - * @throws ParseException If the argument is no valid statement for the given language version. - * Don't forget the semicolon! - */ -fun parseAstStatement(statement: String, javaVersion: JavaVersion = JavaVersion.Latest): ASTBlockStatement { - - // place the param in a statement parsing context - val source = """ - class Foo { - { - $statement - } - } - """.trimIndent() - - val root = ParserTstUtil.parseAndTypeResolveJava(javaVersion.pmdName, source) - - return root.getFirstDescendantOfType(ASTBlockStatement::class.java) -} - - -inline fun matchExpr(ignoreChildren: Boolean = false, - javaVersion: JavaVersion = JavaVersion.Latest, - noinline nodeSpec: NWrapper.() -> Unit): Matcher = object : Matcher { - - override fun test(value: String): Result = - matchNode(ignoreChildren, nodeSpec).test(parseExpression(value, javaVersion)) - -} - -inline fun matchStmt(ignoreChildren: Boolean = false, - javaVersion: JavaVersion = JavaVersion.Latest, - noinline nodeSpec: NWrapper.() -> Unit): Matcher = object : Matcher { - - override fun test(value: String): Result = - matchNode(ignoreChildren, nodeSpec).test(parseStatement(value, javaVersion)) - -} - - enum class JavaVersion : Comparable { J1_3, J1_4, J1_5, J1_6, J1_7, J1_8, J9, J10, J11; val pmdName: String = name.removePrefix("J").replace('_', '.') + /** + * Overloads the range operator, e.g. (`J9..J11`). + * If both operands are the same, a singleton is returned. + */ operator fun rangeTo(last: JavaVersion): List = when { last == this -> listOf(this) @@ -165,46 +34,186 @@ enum class JavaVersion : Comparable { } - - - - +/** + * Specify several tests at once for different java versions. + * One test will be generated per version in [javaVersions]. + * + * @param name Name of the test. Will be postfixed by the specific + * java version used to run it + * @param javaVersions Language versions for which to generate tests + * @param assertions Assertions and further configuration + * to perform with the parsing context + */ fun AbstractFunSpec.parserTest(name: String, - javaVersionRange: List = listOf(JavaVersion.Latest), + javaVersions: List, assertions: ParsingCtx.() -> Unit) { - javaVersionRange.forEach { - + javaVersions.forEach { test("$name (Java ${it.pmdName})") { - - val ctx = ParsingCtx(it) - - ctx.assertions() + ParsingCtx(it).assertions() } - } +} +/** + * Specify a new test for a single java version. + * + * @param name Name of the test. Will be postfixed by the [javaVersion] + * @param javaVersion Language version to use when parsing + * @param assertions Assertions and further configuration + * to perform with the parsing context + */ +fun AbstractFunSpec.parserTest(name: String, + javaVersion: JavaVersion = JavaVersion.Latest, + assertions: ParsingCtx.() -> Unit) { + parserTest(name, listOf(javaVersion), assertions) } -data class ParsingCtx constructor(val javaVersion: JavaVersion) { +data class ParsingCtx(val javaVersion: JavaVersion = JavaVersion.Latest, + val importedTypes: MutableList> = mutableListOf(), + val otherImports: MutableList = mutableListOf()) { + private val imports: List + get() { + val types = importedTypes.mapNotNull { it.canonicalName }.map { "import $it;" } + return types + otherImports.map { "import $it;" } + } inline fun matchExpr(ignoreChildren: Boolean = false, noinline nodeSpec: NWrapper.() -> Unit): Matcher = - matchExpr(ignoreChildren, javaVersion, nodeSpec) + object : Matcher { + override fun test(value: String): Result = + matchNode(ignoreChildren, nodeSpec).test(parseExpression(value)) + + } inline fun matchStmt(ignoreChildren: Boolean = false, - noinline nodeSpec: NWrapper.() -> Unit): Matcher = - matchStmt(ignoreChildren, javaVersion, nodeSpec) + noinline nodeSpec: NWrapper.() -> Unit) = + object : Matcher { + + override fun test(value: String): Result = + matchNode(ignoreChildren, nodeSpec).test(parseStatement(value)) + + } + + + /** + * Parse the string in an expression context. The parsed expression is the child of the + * returned [ASTExpression]. Sometimes there may be many layers between the returned node + * and the node of interest, e.g. when the expression is contained in a PrimaryExpression/PrimaryPrefix + * construct, in which case [parseExpression] saves some keystrokes. + * + * @param expr The expression to parse + * + * @return An [ASTExpression] whose child is the given expression + * + * @throws ParseException If the argument is no valid expression for the given language version + */ + fun parseAstExpression(expr: String): ASTExpression { + + val source = """ + ${imports.joinToString(separator = "\n")} + class Foo { + { + Object o = $expr; + } + } + """.trimIndent() + + val acu = ParserTstUtil.parseAndTypeResolveJava(javaVersion.pmdName, source) + + return acu.getFirstDescendantOfType(ASTVariableInitializer::class.java).jjtGetChild(0) as ASTExpression + } + + + /** + * Parse the string in a statement context. The parsed statement is the child of the + * returned [ASTBlockStatement]. Note that [parseStatement] can save you some keystrokes + * because it finds a descendant of the wanted type. + * + * @param statement The statement to parse + * + * @return An [ASTBlockStatement] whose child is the given statement + * + * @throws ParseException If the argument is no valid statement for the given language version. + * Don't forget the semicolon! + */ + fun parseAstStatement(statement: String): ASTBlockStatement { + + // place the param in a statement parsing context + val source = """ + ${imports.joinToString(separator = "\n")} + class Foo { + { + $statement + } + } + """.trimIndent() + + val root = ParserTstUtil.parseAndTypeResolveJava(javaVersion.pmdName, source) + + return root.getFirstDescendantOfType(ASTBlockStatement::class.java) + } + + + /** + * Parse the string in an expression context, and finds the first descendant of type [N]. + * The descendant is searched for by [findFirstNodeOnStraightLine], to prevent accidental + * mis-selection of a node. In such a case, a [NoSuchElementException] is thrown, and you + * should fix your test case. + * + * @param expr The expression to parse + * @param N type of node to find + * + * @return The first descendant of type [N] found in the parsed expression + * + * @throws NoSuchElementException If no node of type [N] is found by [findFirstNodeOnStraightLine] + * @throws ParseException If the expression is no valid expression for the given language version + */ + inline fun parseExpression(expr: String): N = + parseAstExpression(expr).findFirstNodeOnStraightLine(N::class.java) + ?: throw NoSuchElementException("No node of type ${N::class.java.simpleName} in the given expression:\n\t$expr") + + + /** + * Parse the string in a statement context, and finds the first descendant of type [N]. + * The descendant is searched for by [findFirstNodeOnStraightLine], to prevent accidental + * mis-selection of a node. In such a case, a [NoSuchElementException] is thrown, and you + * should fix your test case. + * + * @param stmt The statement to parse + * @param N The type of node to find + * + * @return The first descendant of type [N] found in the parsed expression + * + * @throws NoSuchElementException If no node of type [N] is found by [findFirstNodeOnStraightLine] + * @throws ParseException If the argument is no valid statement for the given language version. + * Don't forget the semicolon! + */ + inline fun parseStatement(stmt: String): N = + parseAstStatement(stmt).findFirstNodeOnStraightLine(N::class.java) + ?: throw NoSuchElementException("No node of type ${N::class.java.simpleName} in the given statement:\n\t$stmt") + + + /** + * Finds the first descendant of type [N] of [this] node which is + * accessible in a straight line. The descendant must be accessible + * from the [this] on a path where each node has a single child. + * + * If one node has another child, the search is aborted and the method + * returns null. + */ + fun Node.findFirstNodeOnStraightLine(klass: Class): N? { + return when { + klass.isInstance(this) -> { + @SuppressWarnings("UNCHECKED_CAST") + val n = this as N + n + } + else -> if (this.jjtGetNumChildren() == 1) jjtGetChild(0).findFirstNodeOnStraightLine(klass) else null + } + } } - - - - - - - -// also need e.g. parseDeclaration diff --git a/pmd-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/AstMatcherDsl.kt b/pmd-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/AstMatcherDsl.kt index 7e2044393f..64528e9bc0 100644 --- a/pmd-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/AstMatcherDsl.kt +++ b/pmd-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/AstMatcherDsl.kt @@ -69,19 +69,25 @@ class NWrapper private constructor(val it: N, * calls to this method at the same tree level will test the next * children. * - * @param ignoreChildren If true, calls to [child] in the [nodeSpec] are ignored. - * The number of children of the child is not asserted either. + * @param ignoreChildren If true, the number of children of the child is not asserted. + * Calls to [child] in the [nodeSpec] throw an exception. * @param nodeSpec Sequence of assertions to carry out on the child node * * @param M Expected type of the child + * + * @throws AssertionError If the child is not of type [M], or fails the assertions of the [nodeSpec] + * @return The child, if it passes all assertions, otherwise throws an exception */ - inline fun child(ignoreChildren: Boolean = false, noinline nodeSpec: NWrapper.() -> Unit) = + inline fun child(ignoreChildren: Boolean = false, noinline nodeSpec: NWrapper.() -> Unit): M = childImpl(ignoreChildren, M::class.java, nodeSpec) @PublishedApi - internal fun childImpl(ignoreChildren: Boolean, childType: Class, nodeSpec: NWrapper.() -> Unit) { - if (!childMatchersAreIgnored) executeWrapper(childType, shiftChild(), matcherPath, ignoreChildren, nodeSpec) + internal fun childImpl(ignoreChildren: Boolean, childType: Class, nodeSpec: NWrapper.() -> Unit): M { + if (!childMatchersAreIgnored) + return executeWrapper(childType, shiftChild(), matcherPath, ignoreChildren, nodeSpec) + else + throw IllegalStateException(formatErrorMessage(matcherPath, "Calling child when ignoreChildren=true is forbidden")) } @@ -121,13 +127,14 @@ class NWrapper private constructor(val it: N, * @param spec Assertions to carry out on [toWrap] * * @throws AssertionError If some assertions fail + * @return [toWrap], if it passes all assertions, otherwise throws an exception */ @PublishedApi internal fun executeWrapper(childType: Class, toWrap: Node, matcherPath: List>, ignoreChildrenMatchers: Boolean, - spec: NWrapper.() -> Unit) { + spec: NWrapper.() -> Unit): M { val nodeNameForMsg = when { matcherPath.isEmpty() -> "node" @@ -139,9 +146,10 @@ class NWrapper private constructor(val it: N, } val childPath = matcherPath + childType - @Suppress("UNCHECKED_CAST") - val wrapper = NWrapper(toWrap as M, childPath, ignoreChildrenMatchers) + val m = toWrap as M + + val wrapper = NWrapper(m, childPath, ignoreChildrenMatchers) try { wrapper.spec() @@ -156,6 +164,7 @@ class NWrapper private constructor(val it: N, assertFalse(formatErrorMessage(matcherPath + childType, "Wrong number of children, expected ${wrapper.nextChildMatcherIdx}, actual ${wrapper.it.numChildren}")) { !ignoreChildrenMatchers && wrapper.nextChildMatcherIdx != wrapper.it.numChildren } + return m } } }