optMostMatchedCanonicalClassPrefix =
+ s.currentClassDeclarationCanonicalPrefixes.stream()
+ .filter(canonicalName::startsWith)
+ .findFirst();
+ if(optMostMatchedCanonicalClassPrefix.isPresent())
+ {
+ s.append(canonicalName.substring(optMostMatchedCanonicalClassPrefix.orElseThrow().length()));
+ return;
+ }
+ }
+ }
+
+ final String packageName = t.getSymbol().getPackageName();
+ s.append(canonicalName.startsWith(packageName) && canonicalName.length() > packageName.length()
+ ? canonicalName.substring(packageName.length() + 1)
+ : canonicalName);
+ }
+ }
+}
diff --git a/rules/src/main/java/software/xdev/pmd/rule/prettyprint/PrettyPrintVisitor.java b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/PrettyPrintVisitor.java
new file mode 100644
index 0000000..6e77132
--- /dev/null
+++ b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/PrettyPrintVisitor.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright © 2026 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package software.xdev.pmd.rule.prettyprint;
+
+import static net.sourceforge.pmd.util.OptionalBool.NO;
+import static net.sourceforge.pmd.util.OptionalBool.YES;
+
+import java.util.Arrays;
+import java.util.List;
+
+import net.sourceforge.pmd.lang.java.symbols.JTypeParameterSymbol;
+import net.sourceforge.pmd.lang.java.types.JArrayType;
+import net.sourceforge.pmd.lang.java.types.JClassType;
+import net.sourceforge.pmd.lang.java.types.JIntersectionType;
+import net.sourceforge.pmd.lang.java.types.JMethodSig;
+import net.sourceforge.pmd.lang.java.types.JPrimitiveType;
+import net.sourceforge.pmd.lang.java.types.JTypeMirror;
+import net.sourceforge.pmd.lang.java.types.JTypeVar;
+import net.sourceforge.pmd.lang.java.types.JTypeVisitor;
+import net.sourceforge.pmd.lang.java.types.JWildcardType;
+import net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar;
+import net.sourceforge.pmd.util.OptionalBool;
+
+
+@SuppressWarnings("all") // Upstream code from https://github.com/pmd/pmd/pull/6904
+public class PrettyPrintVisitor implements JTypeVisitor
+{
+ @Override
+ public Void visit(final JTypeMirror t, final P sb)
+ {
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+ sb.append(t.toString());
+ return null;
+ }
+
+ @Override
+ public Void visitClass(final JClassType t, final P sb)
+ {
+ final JClassType enclosing = t.getEnclosingType();
+ final boolean isAnon = t.getSymbol().isAnonymousClass();
+
+ if(enclosing != null && !isAnon)
+ {
+ this.visitClass(enclosing, sb);
+ sb.append('#');
+ }
+ else if(t.hasErasedSuperTypes() && !t.isRaw())
+ {
+ sb.append("(erased) ");
+ }
+
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+
+ if(t.getSymbol().isUnresolved())
+ {
+ sb.append('*'); // a small marker to spot them
+ }
+
+ this.appendClassName(t, sb, enclosing, isAnon);
+
+ final List targs = t.getTypeArgs();
+ if(t.isRaw() || targs.isEmpty())
+ {
+ return null;
+ }
+
+ if(t.isGenericTypeDeclaration() && sb.printTypeVarBounds != NO)
+ {
+ sb.printTypeVarBounds = YES;
+ }
+ this.join(sb, targs, ", ", "<", ">");
+ return null;
+ }
+
+ protected void appendClassName(final JClassType t, final P sb, final JClassType enclosing, final boolean isAnon)
+ {
+ if(enclosing != null && !isAnon || !sb.qualifyNames)
+ {
+ sb.append(t.getSymbol().getSimpleName());
+ }
+ else
+ {
+ sb.append(t.getSymbol().getBinaryName());
+ }
+ }
+
+ @Override
+ public Void visitWildcard(final JWildcardType t, final P sb)
+ {
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+ sb.append("?");
+ if(t.isUnbounded())
+ {
+ return null;
+ }
+
+ sb.append(t.isUpperBound() ? " extends " : " super ");
+
+ t.getBound().acceptVisitor(this, sb);
+ return null;
+ }
+
+ @Override
+ public Void visitPrimitive(final JPrimitiveType t, final P sb)
+ {
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+ sb.append(t.getSimpleName());
+ return null;
+ }
+
+ @Override
+ public Void visitTypeVar(final JTypeVar t, final P sb)
+ {
+ if(!t.isCaptured() && sb.qualifyTvars)
+ {
+ final JTypeParameterSymbol sym = t.getSymbol();
+ if(sym != null)
+ {
+ sb.append(sym.getDeclaringSymbol().getSimpleName());
+ sb.append('#');
+ }
+ }
+
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+ sb.append(t.getName());
+
+ if(sb.printTypeVarBounds == YES)
+ {
+ sb.printTypeVarBounds = NO;
+ if(!t.getUpperBound().isTop())
+ {
+ sb.append(" extends ");
+ t.getUpperBound().acceptVisitor(this, sb);
+ }
+ if(!t.getLowerBound().isBottom())
+ {
+ sb.append(" super ");
+ t.getLowerBound().acceptVisitor(this, sb);
+ }
+ sb.printTypeVarBounds = YES;
+ }
+ return null;
+ }
+
+ /**
+ * Formats {@link Arrays#asList(Object[])} as {@code asList(T...) -> List}
+ */
+ @Override
+ public Void visitMethodType(final JMethodSig t, final P sb)
+ {
+ if(sb.printMethodHeader)
+ {
+ t.getDeclaringType().acceptVisitor(this, sb);
+ sb.append(".");
+
+ if(t.isGeneric())
+ {
+ final OptionalBool printBounds = sb.printTypeVarBounds;
+ if(printBounds != NO)
+ {
+ sb.printTypeVarBounds = YES;
+ }
+ this.join(sb, t.getTypeParameters(), ", ", "<", "> ", false);
+ sb.printTypeVarBounds = printBounds;
+ }
+ }
+
+ sb.append(t.getName());
+
+ this.join(sb, t.getFormalParameters(), ", ", "(", ")", t.isVarargs());
+
+ if(sb.printMethodReturnType)
+ {
+ sb.append(" -> ");
+ t.getReturnType().acceptVisitor(this, sb);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitIntersection(final JIntersectionType t, final P sb)
+ {
+ return this.join(sb, t.getComponents(), " & ", "", "");
+ }
+
+ @Override
+ public Void visitArray(final JArrayType t, final P sb)
+ {
+ final JTypeMirror component = t.getComponentType();
+ if(component instanceof JIntersectionType)
+ {
+ sb.append("(");
+ }
+
+ final boolean isVarargs = sb.isVarargs;
+ sb.isVarargs = false;
+ component.acceptVisitor(this, sb);
+
+ if(component instanceof JIntersectionType)
+ {
+ sb.append(")");
+ }
+ sb.printTypeAnnotations(t.getTypeAnnotations());
+ sb.append(isVarargs ? "..." : "[]");
+ return null;
+ }
+
+ @Override
+ public Void visitNullType(final JTypeMirror t, final P sb)
+ {
+ sb.append("null");
+ return null;
+ }
+
+ @Override
+ public Void visitInferenceVar(final InferenceVar t, final P sb)
+ {
+ sb.append(t.getName());
+ return null;
+ }
+
+ protected Void join(
+ final P sb,
+ final List extends JTypeMirror> ts,
+ final String delim,
+ final String prefix,
+ final String suffix)
+ {
+ return this.join(sb, ts, delim, prefix, suffix, false);
+ }
+
+ protected Void join(
+ final P sb,
+ final List extends JTypeMirror> types,
+ final String delim,
+ final String prefix,
+ final String suffix,
+ final boolean isVarargs)
+ {
+ sb.isVarargs = false;
+ sb.append(prefix);
+ if(!types.isEmpty())
+ {
+ for(int i = 0; i < types.size() - 1; i++)
+ {
+ types.get(i).acceptVisitor(this, sb);
+ sb.append(delim);
+ }
+ if(isVarargs)
+ {
+ sb.isVarargs = true;
+ }
+ types.get(types.size() - 1).acceptVisitor(this, sb);
+ }
+ sb.append(suffix);
+ return null;
+ }
+}
diff --git a/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrint.java b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrint.java
new file mode 100644
index 0000000..dac8f59
--- /dev/null
+++ b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrint.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2026 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package software.xdev.pmd.rule.prettyprint;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import net.sourceforge.pmd.lang.java.types.JTypeVisitable;
+
+
+/**
+ * Pretty-printing methods to display types. The current API is only offered for debugging, not for displaying types to
+ * users.
+ */
+@SuppressWarnings("all") // Upstream code from https://github.com/pmd/pmd/pull/6904
+public final class TypePrettyPrint
+{
+
+ private TypePrettyPrint()
+ {
+ }
+
+ public static @NonNull String prettyPrint(@NonNull final JTypeVisitable t)
+ {
+ return prettyPrint(t, new TypePrettyPrinter());
+ }
+
+ public static @NonNull String prettyPrintWithSimpleNames(@NonNull final JTypeVisitable t)
+ {
+ return prettyPrint(t, new TypePrettyPrinter().qualifyNames(false));
+ }
+
+ public static String prettyPrint(@NonNull final JTypeVisitable t, final TypePrettyPrinter prettyPrinter)
+ {
+ t.acceptVisitor(DefaultVisitor.INSTANCE, prettyPrinter);
+ return prettyPrinter.consumeResult();
+ }
+
+ static final class DefaultVisitor extends PrettyPrintVisitor
+ {
+ static final DefaultVisitor INSTANCE = new DefaultVisitor();
+ }
+}
diff --git a/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrinter.java b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrinter.java
new file mode 100644
index 0000000..bf6dad2
--- /dev/null
+++ b/rules/src/main/java/software/xdev/pmd/rule/prettyprint/TypePrettyPrinter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright © 2026 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package software.xdev.pmd.rule.prettyprint;
+
+import static net.sourceforge.pmd.util.OptionalBool.UNKNOWN;
+
+import org.pcollections.PSet;
+
+import net.sourceforge.pmd.lang.java.symbols.SymbolicValue;
+import net.sourceforge.pmd.util.OptionalBool;
+
+
+/**
+ * Options to pretty print a type. Cannot be used concurrently.
+ */
+@SuppressWarnings("all") // Upstream code from https://github.com/pmd/pmd/pull/6904
+public class TypePrettyPrinter
+{
+ protected final StringBuilder sb = new StringBuilder();
+
+ protected boolean printMethodHeader = true;
+ protected boolean printMethodReturnType = true;
+ protected OptionalBool printTypeVarBounds = UNKNOWN;
+ protected boolean qualifyTvars = false;
+ protected boolean qualifyNames = true;
+ protected boolean isVarargs = false;
+ protected boolean printTypeAnnotations = true;
+ protected boolean qualifyAnnotations = false;
+
+ /**
+ * Create a new pretty printer with the default configuration.
+ */
+ public TypePrettyPrinter()
+ {
+ // default
+ }
+
+ public StringBuilder append(final char o)
+ {
+ return this.sb.append(o);
+ }
+
+ public StringBuilder append(final String o)
+ {
+ return this.sb.append(o);
+ }
+
+ /**
+ * Print the declaring type of the method and its type parameters. Default: true.
+ */
+ public TypePrettyPrinter printMethodHeader(final boolean printMethodHeader)
+ {
+ this.printMethodHeader = printMethodHeader;
+ return this;
+ }
+
+ /**
+ * Print the return type of methods (as postfix). Default: true.
+ */
+ public TypePrettyPrinter printMethodResult(final boolean printMethodResult)
+ {
+ this.printMethodReturnType = printMethodResult;
+ return this;
+ }
+
+ /**
+ * Print the bounds of type variables. Default: false.
+ */
+ public void printTypeVarBounds(final OptionalBool printTypeVarBounds)
+ {
+ this.printTypeVarBounds = printTypeVarBounds;
+ }
+
+ /**
+ * Qualify type variables with the name of the declaring symbol. Eg {@code Foo#T} for {@code class Foo}}.
+ * Default: false.
+ */
+ public TypePrettyPrinter qualifyTvars(final boolean qualifyTvars)
+ {
+ this.qualifyTvars = qualifyTvars;
+ return this;
+ }
+
+ /**
+ * Whether to print the binary name of a type annotation or just the simple name if false. Default: false.
+ */
+ public TypePrettyPrinter qualifyAnnotations(final boolean qualifyAnnotations)
+ {
+ this.qualifyAnnotations = qualifyAnnotations;
+ return this;
+ }
+
+ /**
+ * Whether to print the type annotations. Default: true.
+ */
+ public TypePrettyPrinter printAnnotations(final boolean printAnnotations)
+ {
+ this.printTypeAnnotations = printAnnotations;
+ return this;
+ }
+
+ /**
+ * Use qualified names for class types instead of simple names. Default: true.
+ */
+ public TypePrettyPrinter qualifyNames(final boolean qualifyNames)
+ {
+ this.qualifyNames = qualifyNames;
+ return this;
+ }
+
+ public String consumeResult()
+ {
+ // The pretty printer might be reused by another call,
+ // delete the buffer.
+ final String result = this.sb.toString();
+ this.sb.setLength(0);
+ return result;
+ }
+
+ protected void printTypeAnnotations(final PSet annots)
+ {
+ if(this.printTypeAnnotations)
+ {
+ for(final SymbolicValue.SymAnnot annot : annots)
+ {
+ final String name = this.qualifyAnnotations ? annot.getBinaryName()
+ : annot.getSimpleName();
+ this.append('@').append(name).append(' ');
+ }
+ }
+ }
+}
diff --git a/template-placeholder-demo/pom.xml b/template-placeholder-demo/pom.xml
deleted file mode 100644
index 06fc318..0000000
--- a/template-placeholder-demo/pom.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
- 4.0.0
-
-
- software.xdev
- template-placeholder-root
- 1.0.0-SNAPSHOT
-
-
- template-placeholder-demo
- 1.0.0-SNAPSHOT
- jar
-
-
- XDEV Software
- https://xdev.software
-
-
-
- 17
- ${javaVersion}
-
- UTF-8
- UTF-8
-
- software.xdev.Application
-
-
-
-
- software.xdev
- template-placeholder
- ${project.version}
-
-
-
-
- ${project.artifactId}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.15.0
-
- ${maven.compiler.release}
-
- -proc:none
-
-
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
- 3.8.0
-
-
-
- ${mainClass}
-
-
- true
-
-
-
- jar-with-dependencies
-
- false
-
-
-
- make-assembly
- package
-
- single
-
-
-
-
-
-
-