Fix plugin subclassing in Log4j Docgen (#117, #120)

diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/internal/TypeLookup.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/internal/TypeLookup.java
index d32bd00..1a592ec 100644
--- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/internal/TypeLookup.java
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/internal/TypeLookup.java
@@ -23,6 +23,8 @@
 import org.apache.logging.log4j.docgen.AbstractType;
 import org.apache.logging.log4j.docgen.PluginSet;
 import org.apache.logging.log4j.docgen.PluginType;
+import org.apache.logging.log4j.docgen.ScalarType;
+import org.apache.logging.log4j.docgen.Type;
 import org.jspecify.annotations.Nullable;
 
 public final class TypeLookup extends TreeMap<String, ArtifactSourcedType> {
@@ -42,21 +44,109 @@
 
     private void mergeDescriptors(Iterable<? extends PluginSet> pluginSets) {
         pluginSets.forEach(pluginSet -> {
-            pluginSet.getScalars().forEach(scalar -> {
-                final ArtifactSourcedType sourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, scalar);
-                putIfAbsent(scalar.getClassName(), sourcedType);
-            });
-            pluginSet.getAbstractTypes().forEach(abstractType -> {
-                final ArtifactSourcedType sourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, abstractType);
-                putIfAbsent(abstractType.getClassName(), sourcedType);
-            });
-            pluginSet.getPlugins().forEach(pluginType -> {
-                final ArtifactSourcedType sourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, pluginType);
-                putIfAbsent(pluginType.getClassName(), sourcedType);
-            });
+            mergeScalarTypes(pluginSet);
+            mergeAbstractTypes(pluginSet);
+            mergePluginTypes(pluginSet);
         });
     }
 
+    private void mergeScalarTypes(PluginSet pluginSet) {
+        pluginSet.getScalars().forEach(newType -> {
+            final String className = newType.getClassName();
+            final ArtifactSourcedType newSourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, newType);
+            merge(className, newSourcedType, TypeLookup::mergeScalarType);
+        });
+    }
+
+    private static ArtifactSourcedType mergeScalarType(
+            final ArtifactSourcedType oldSourcedType, final ArtifactSourcedType newSourcedType) {
+        // If the entry already exists and is of expected type, we should ideally extend it.
+        // Since Modello doesn't generate `hashCode()`, `equals()`, etc. it is difficult to compare instances.
+        // Hence, we will be lazy, and just assume they are the same.
+        if (oldSourcedType.type instanceof ScalarType) {
+            return oldSourcedType;
+        }
+
+        // If the entry already exists, but with an unexpected type, fail
+        else {
+            throw conflictingTypeFailure(oldSourcedType.type, newSourcedType.type);
+        }
+    }
+
+    private static RuntimeException conflictingTypeFailure(final Type oldType, final Type newType) {
+        final String message = String.format(
+                "`%s` class occurs multiple times with conflicting types: `%s` and `%s`",
+                oldType.getClassName(),
+                oldType.getClass().getSimpleName(),
+                newType.getClass().getSimpleName());
+        return new IllegalArgumentException(message);
+    }
+
+    private void mergeAbstractTypes(PluginSet pluginSet) {
+        pluginSet.getAbstractTypes().forEach(newType -> {
+            final String className = newType.getClassName();
+            final ArtifactSourcedType newSourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, newType);
+            merge(className, newSourcedType, TypeLookup::mergeAbstractType);
+        });
+    }
+
+    private static ArtifactSourcedType mergeAbstractType(
+            final ArtifactSourcedType oldSourcedType, final ArtifactSourcedType newSourcedType) {
+
+        // If the entry already exists and is of expected type, extend it
+        if (oldSourcedType.type instanceof AbstractType) {
+            final AbstractType oldType = (AbstractType) oldSourcedType.type;
+            final AbstractType newType = (AbstractType) newSourcedType.type;
+            newType.getImplementations().forEach(oldType::addImplementation);
+            return oldSourcedType;
+        }
+
+        // If the entry already exists, but with an unexpected type, fail
+        else {
+            throw conflictingTypeFailure(oldSourcedType.type, newSourcedType.type);
+        }
+    }
+
+    private void mergePluginTypes(PluginSet pluginSet) {
+        pluginSet.getPlugins().forEach(newType -> {
+            final String className = newType.getClassName();
+            final ArtifactSourcedType newSourcedType = ArtifactSourcedType.ofPluginSet(pluginSet, newType);
+            merge(className, newSourcedType, TypeLookup::mergePluginType);
+        });
+    }
+
+    private static ArtifactSourcedType mergePluginType(
+            final ArtifactSourcedType oldSourcedType, final ArtifactSourcedType newSourcedType) {
+
+        // If the entry already exists, but is of `AbstractType`, promote it to `PluginType`.
+        //
+        // The most prominent example to this is `LoggerConfig`, which is a plugin.
+        // Assume `AsyncLoggerConfig` (extending from `LoggerConfig`) is encountered first.
+        // This results in `LoggerConfig` getting registered as an `AbstractType`.
+        // When the actual `LoggerConfig` definition is encountered, the type needs to be promoted to `PluginType`.
+        // Otherwise, `LoggerConfig` plugin definition will get skipped.
+        if (oldSourcedType.type instanceof AbstractType && !(oldSourcedType.type instanceof PluginType)) {
+            final PluginType newType = (PluginType) newSourcedType.type;
+            // Preserve old implementations
+            final AbstractType oldType = (AbstractType) oldSourcedType.type;
+            oldType.getImplementations().forEach(newType::addImplementation);
+            return newSourcedType;
+        }
+
+        // If the entry already exists and is of expected type, extend it
+        else if (oldSourcedType.type instanceof PluginType) {
+            final PluginType oldType = (PluginType) oldSourcedType.type;
+            final PluginType newType = (PluginType) newSourcedType.type;
+            newType.getImplementations().forEach(oldType::addImplementation);
+            return oldSourcedType;
+        }
+
+        // If the entry already exists, but with an unexpected type, fail
+        else {
+            throw conflictingTypeFailure(oldSourcedType.type, newSourcedType.type);
+        }
+    }
+
     private void populateTypeHierarchy(Iterable<? extends PluginSet> pluginSets) {
         pluginSets.forEach(pluginSet -> {
             final Set<PluginType> pluginTypes = pluginSet.getPlugins();
diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DescriptorGenerator.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DescriptorGenerator.java
index eb9b82a..cd82d7b 100644
--- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DescriptorGenerator.java
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DescriptorGenerator.java
@@ -135,11 +135,11 @@
      */
     private static final String IMPOSSIBLE_REGEX = "(?!.*)";
 
-    // Abstract types to process
-    private final Collection<TypeElement> abstractTypesToDocument = new HashSet<>();
+    private final Set<TypeElement> pluginTypesToDocument = new HashSet<>();
 
-    // Scalar types to process
-    private final Collection<TypeElement> scalarTypesToDocument = new HashSet<>();
+    private final Set<TypeElement> abstractTypesToDocument = new HashSet<>();
+
+    private final Set<TypeElement> scalarTypesToDocument = new HashSet<>();
 
     private Predicate<String> classNameFilter;
 
@@ -253,7 +253,8 @@
     @Override
     public boolean process(final Set<? extends TypeElement> unused, final RoundEnvironment roundEnv) {
         // First step: document plugins
-        roundEnv.getElementsAnnotatedWithAny(annotations.getPluginAnnotations()).forEach(this::addPluginDocumentation);
+        populatePluginTypesToDocument(roundEnv);
+        pluginTypesToDocument.forEach(this::addPluginDocumentation);
         // Second step: document abstract types
         abstractTypesToDocument.forEach(this::addAbstractTypeDocumentation);
         // Second step: document scalars
@@ -265,28 +266,39 @@
         return false;
     }
 
-    private void addPluginDocumentation(final Element element) {
-        try {
+    private void populatePluginTypesToDocument(final RoundEnvironment roundEnv) {
+        roundEnv.getElementsAnnotatedWithAny(annotations.getPluginAnnotations()).forEach(element -> {
             if (element instanceof TypeElement) {
-                final PluginType pluginType = new PluginType();
-                pluginType.setName(annotations.getPluginSpecifiedName(element).orElseGet(() -> element.getSimpleName()
-                        .toString()));
-                pluginType.setNamespace(
-                        annotations.getPluginSpecifiedNamespace(element).orElse("Core"));
-                populatePlugin((TypeElement) element, pluginType);
-                pluginSet.addPlugin(pluginType);
+                pluginTypesToDocument.add((TypeElement) element);
             } else {
                 messager.printMessage(
                         Diagnostic.Kind.WARNING, "Found @Plugin annotation on unexpected element.", element);
             }
+        });
+    }
+
+    private void addPluginDocumentation(final TypeElement element) {
+        try {
+            final PluginType pluginType = new PluginType();
+            pluginType.setName(annotations.getPluginSpecifiedName(element).orElseGet(() -> element.getSimpleName()
+                    .toString()));
+            pluginType.setNamespace(
+                    annotations.getPluginSpecifiedNamespace(element).orElse("Core"));
+            populatePlugin(element, pluginType);
+            pluginSet.addPlugin(pluginType);
         } catch (final Exception error) {
             final String message = String.format("failed to process element `%s`", element);
             throw new RuntimeException(message, error);
         }
     }
 
+    @SuppressWarnings("SuspiciousMethodCalls")
     private void addAbstractTypeDocumentation(final QualifiedNameable element) {
         try {
+            // Short-circuit if the type is already documented as a plugin
+            if (pluginTypesToDocument.contains(element)) {
+                return;
+            }
             final AbstractType abstractType = new AbstractType();
             final ElementImports imports = importsFactory.ofElement(element);
             final String qualifiedClassName = getClassName(element.asType());
diff --git a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/expected-plugins.xml b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/expected-plugins.xml
index 3cdd37b..39f6806 100644
--- a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/expected-plugins.xml
+++ b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/expected-plugins.xml
@@ -123,6 +123,19 @@
 * apiref:example.Appender[],
 * apiref:example.BaseAppender[]</description>
         </plugin>
+        <plugin name="MyAppenderSubclassingAppender" namespace="namespace" className="example.MyAppenderSubclassingAppender">
+            <supertypes>
+                <supertype>example.AbstractAppender</supertype>
+                <supertype>example.Appender</supertype>
+                <supertype>example.BaseAppender</supertype>
+                <supertype>example.MyAppender</supertype>
+                <supertype>java.lang.Object</supertype>
+            </supertypes>
+            <attributes>
+                <attribute name="awesomenessEnabled" type="boolean"/>
+            </attributes>
+            <description>Example plugin to demonstrate the case where a plugin subclasses another plugin.</description>
+        </plugin>
         <plugin name="MyLayout" className="example.MyOldLayout">
             <supertypes>
                 <supertype>example.Layout</supertype>
diff --git a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppender.java b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppender.java
index 493ff00..37a47ef 100644
--- a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppender.java
+++ b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppender.java
@@ -45,7 +45,7 @@
  * </ul>
  */
 @Plugin(name = "MyAppender", category = "namespace")
-public final class MyAppender extends AbstractAppender implements Appender {
+public class MyAppender extends AbstractAppender implements Appender {
 
     /**
      * Parent builder with some private fields that are not returned by
diff --git a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppenderSubclassingAppender.java b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppenderSubclassingAppender.java
new file mode 100644
index 0000000..8bc13a6
--- /dev/null
+++ b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j2/MyAppenderSubclassingAppender.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package example;
+
+import java.util.List;
+import java.util.Set;
+import javax.lang.model.element.TypeElement;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * Example plugin to demonstrate the case where a plugin subclasses another plugin.
+ *
+ * @see <a href="https://github.com/apache/logging-log4j-tools/issues/117">apache/logging-log4j-tools#117</a>
+ */
+@Plugin(name = "MyAppenderSubclassingAppender", category = "namespace")
+public final class MyAppenderSubclassingAppender extends MyAppender {
+
+    /**
+     * The canonical constructor.
+     */
+    @PluginFactory
+    public static MyAppenderSubclassingAppender newLayout(
+            final @PluginAttribute(value = "awesomenessEnabled", defaultBoolean = true) boolean awesomenessEnabled) {
+        return null;
+    }
+}
diff --git a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppender.java b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppender.java
index 1318ed2..3024620 100644
--- a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppender.java
+++ b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppender.java
@@ -48,7 +48,7 @@
  */
 @Plugin
 @Namespace("namespace")
-public final class MyAppender extends AbstractAppender implements Appender {
+public class MyAppender extends AbstractAppender implements Appender {
 
     /**
      * Parent builder with some private fields that are not returned by
diff --git a/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppenderSubclassingAppender.java b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppenderSubclassingAppender.java
new file mode 100644
index 0000000..9a7b5a5
--- /dev/null
+++ b/log4j-docgen/src/test/resources/DescriptorGeneratorTest/java-of-log4j3/MyAppenderSubclassingAppender.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package example;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.logging.log4j.plugins.Factory;
+import org.apache.logging.log4j.plugins.Namespace;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+
+/**
+ * Example plugin to demonstrate the case where a plugin subclasses another plugin.
+ *
+ * @see <a href="https://github.com/apache/logging-log4j-tools/issues/117">apache/logging-log4j-tools#117</a>
+ */
+@Plugin
+@Namespace("namespace")
+public final class MyAppenderSubclassingAppender extends MyAppender {
+
+    /**
+     * The canonical constructor.
+     */
+    @Factory
+    public static MyAppenderSubclassingAppender newLayout(
+            final @PluginAttribute(value = "awesomenessEnabled", defaultBoolean = true) boolean awesomenessEnabled) {
+        return null;
+    }
+}
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.appender.SocketAppender.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.appender.SocketAppender.adoc
index 5268f96..a68b7c9 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.appender.SocketAppender.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.appender.SocketAppender.adoc
@@ -16,7 +16,7 @@
 ////
 
 [#org_apache_logging_log4j_core_appender_SocketAppender]
-= `org.apache.logging.log4j.core.appender.SocketAppender`
+= Socket
 
 Class:: `org.apache.logging.log4j.core.appender.SocketAppender`
 Provider:: `org.apache.logging.log4j:log4j-core`
@@ -26,6 +26,131 @@
 
 Supports both TCP and UDP.
 
+[#org_apache_logging_log4j_core_appender_SocketAppender-XML-snippet]
+== XML snippet
+[source, xml]
+----
+<Socket advertise=""
+        bufferedIo=""
+        bufferSize=""
+        connectTimeoutMillis=""
+        host=""
+        ignoreExceptions=""
+        immediateFail=""
+        immediateFlush=""
+        name=""
+        port=""
+        protocol=""
+        reconnectDelayMillis="">
+    <a-Filter-implementation/>
+    <a-Layout-implementation/>
+    <property/><!-- multiple occurrences allowed -->
+    <SocketOptions/>
+    <Ssl/>
+</Socket>
+----
+
+[#org_apache_logging_log4j_core_appender_SocketAppender-attributes]
+== Attributes
+
+Optional attributes are denoted by `?`-suffixed types.
+
+[cols="1m,1m,1m,5"]
+|===
+|Name|Type|Default|Description
+
+|advertise
+|boolean?
+|
+a|
+
+|bufferedIo
+|boolean?
+|
+a|
+
+|bufferSize
+|int?
+|
+a|
+
+|connectTimeoutMillis
+|int?
+|
+a|
+
+|host
+|String?
+|
+a|
+
+|ignoreExceptions
+|boolean?
+|
+a|
+
+|immediateFail
+|boolean?
+|
+a|
+
+|immediateFlush
+|boolean?
+|
+a|
+
+|name
+|String
+|
+a|
+
+|port
+|int?
+|
+a|
+
+|protocol
+|xref:../log4j-core/org.apache.logging.log4j.core.net.Protocol.adoc[Protocol]?
+|
+a|
+
+|reconnectDelayMillis
+|int?
+|
+a|
+
+|===
+
+[#org_apache_logging_log4j_core_appender_SocketAppender-components]
+== Nested components
+
+Optional components are denoted by `?`-suffixed types.
+
+[cols="1m,1m,5"]
+|===
+|Tag|Type|Description
+
+|property
+|xref:../log4j-core/org.apache.logging.log4j.core.config.Property.adoc[Property]?
+a|
+
+|
+|xref:../log4j-core/org.apache.logging.log4j.core.Filter.adoc[Filter]?
+a|
+
+|
+|xref:../log4j-core/org.apache.logging.log4j.core.Layout.adoc[Layout]?
+a|
+
+|SocketOptions
+|xref:../log4j-core/org.apache.logging.log4j.core.net.SocketOptions.adoc[SocketOptions]?
+a|
+
+|Ssl
+|xref:../log4j-core/org.apache.logging.log4j.core.net.ssl.SslConfiguration.adoc[SslConfiguration]?
+a|
+
+|===
 
 [#org_apache_logging_log4j_core_appender_SocketAppender-implementations]
 == Known implementations
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggerConfig.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggerConfig.adoc
index b47395e..4fb693e 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggerConfig.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggerConfig.adoc
@@ -16,7 +16,7 @@
 ////
 
 [#org_apache_logging_log4j_core_config_LoggerConfig]
-= `org.apache.logging.log4j.core.config.LoggerConfig`
+= logger
 
 Class:: `org.apache.logging.log4j.core.config.LoggerConfig`
 Provider:: `org.apache.logging.log4j:log4j-core`
@@ -24,6 +24,79 @@
 
 Logger object that is created via configuration.
 
+[#org_apache_logging_log4j_core_config_LoggerConfig-XML-snippet]
+== XML snippet
+[source, xml]
+----
+<logger additivity=""
+        includeLocation=""
+        level=""
+        levelAndRefs=""
+        name="">
+    <a-Filter-implementation/>
+    <AppenderRef/><!-- multiple occurrences allowed -->
+    <property/><!-- multiple occurrences allowed -->
+</logger>
+----
+
+[#org_apache_logging_log4j_core_config_LoggerConfig-attributes]
+== Attributes
+
+Optional attributes are denoted by `?`-suffixed types.
+
+[cols="1m,1m,1m,5"]
+|===
+|Name|Type|Default|Description
+
+|additivity
+|Boolean?
+|
+a|
+
+|includeLocation
+|String?
+|
+a|
+
+|level
+|xref:../log4j-core/org.apache.logging.log4j.Level.adoc[Level]?
+|
+a|
+
+|levelAndRefs
+|String?
+|
+a|
+
+|name
+|String
+|
+a|
+
+|===
+
+[#org_apache_logging_log4j_core_config_LoggerConfig-components]
+== Nested components
+
+Optional components are denoted by `?`-suffixed types.
+
+[cols="1m,1m,5"]
+|===
+|Tag|Type|Description
+
+|AppenderRef
+|xref:../log4j-core/org.apache.logging.log4j.core.config.AppenderRef.adoc[AppenderRef]?
+a|
+
+|property
+|xref:../log4j-core/org.apache.logging.log4j.core.config.Property.adoc[Property]?
+a|
+
+|
+|xref:../log4j-core/org.apache.logging.log4j.core.Filter.adoc[Filter]?
+a|
+
+|===
 
 [#org_apache_logging_log4j_core_config_LoggerConfig-implementations]
 == Known implementations
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggersPlugin.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggersPlugin.adoc
index 6ad8a91..23d2fbc 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggersPlugin.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.config.LoggersPlugin.adoc
@@ -40,7 +40,7 @@
 |===
 |Tag|Type|Description
 
-|
+|logger
 |xref:../log4j-core/org.apache.logging.log4j.core.config.LoggerConfig.adoc[LoggerConfig]?
 a|An array of Loggers.
 
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.filter.MapFilter.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.filter.MapFilter.adoc
index 7f1aaa4..a0ae030 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.filter.MapFilter.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.filter.MapFilter.adoc
@@ -16,7 +16,7 @@
 ////
 
 [#org_apache_logging_log4j_core_filter_MapFilter]
-= `org.apache.logging.log4j.core.filter.MapFilter`
+= MapFilter
 
 Class:: `org.apache.logging.log4j.core.filter.MapFilter`
 Provider:: `org.apache.logging.log4j:log4j-core`
@@ -24,6 +24,57 @@
 
 A Filter that operates on a Map.
 
+[#org_apache_logging_log4j_core_filter_MapFilter-XML-snippet]
+== XML snippet
+[source, xml]
+----
+<MapFilter onMatch=""
+           onMismatch=""
+           operator="">
+    <KeyValuePair/><!-- multiple occurrences allowed -->
+</MapFilter>
+----
+
+[#org_apache_logging_log4j_core_filter_MapFilter-attributes]
+== Attributes
+
+Optional attributes are denoted by `?`-suffixed types.
+
+[cols="1m,1m,1m,5"]
+|===
+|Name|Type|Default|Description
+
+|onMatch
+|xref:../log4j-core/org.apache.logging.log4j.core.Filter.Result.adoc[Result]?
+|
+a|
+
+|onMismatch
+|xref:../log4j-core/org.apache.logging.log4j.core.Filter.Result.adoc[Result]?
+|
+a|
+
+|operator
+|String?
+|
+a|
+
+|===
+
+[#org_apache_logging_log4j_core_filter_MapFilter-components]
+== Nested components
+
+Optional components are denoted by `?`-suffixed types.
+
+[cols="1m,1m,5"]
+|===
+|Tag|Type|Description
+
+|KeyValuePair
+|xref:../log4j-core/org.apache.logging.log4j.core.util.KeyValuePair.adoc[KeyValuePair]?
+a|
+
+|===
 
 [#org_apache_logging_log4j_core_filter_MapFilter-implementations]
 == Known implementations
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.lookup.MapLookup.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.lookup.MapLookup.adoc
index 70511a5..ab1ebb4 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.lookup.MapLookup.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.lookup.MapLookup.adoc
@@ -16,7 +16,7 @@
 ////
 
 [#org_apache_logging_log4j_core_lookup_MapLookup]
-= `org.apache.logging.log4j.core.lookup.MapLookup`
+= map
 
 Class:: `org.apache.logging.log4j.core.lookup.MapLookup`
 Provider:: `org.apache.logging.log4j:log4j-core`
@@ -24,6 +24,12 @@
 
 A map-based lookup.
 
+[#org_apache_logging_log4j_core_lookup_MapLookup-XML-snippet]
+== XML snippet
+[source, xml]
+----
+<map/>
+----
 
 [#org_apache_logging_log4j_core_lookup_MapLookup-implementations]
 == Known implementations
diff --git a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.pattern.ThrowablePatternConverter.adoc b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.pattern.ThrowablePatternConverter.adoc
index b67917e..7bee906 100644
--- a/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.pattern.ThrowablePatternConverter.adoc
+++ b/log4j-docgen/src/test/resources/DocumentationGeneratorTest/complex/docs/log4j-core/org.apache.logging.log4j.core.pattern.ThrowablePatternConverter.adoc
@@ -16,7 +16,7 @@
 ////
 
 [#org_apache_logging_log4j_core_pattern_ThrowablePatternConverter]
-= `org.apache.logging.log4j.core.pattern.ThrowablePatternConverter`
+= ThrowablePatternConverter
 
 Class:: `org.apache.logging.log4j.core.pattern.ThrowablePatternConverter`
 Provider:: `org.apache.logging.log4j:log4j-core`
@@ -24,6 +24,12 @@
 
 Outputs the Throwable portion of the LoggingEvent as a full stack trace unless this converter's option is 'short', where it just outputs the first line of the trace, or if the number of lines to print is explicitly specified.
 
+[#org_apache_logging_log4j_core_pattern_ThrowablePatternConverter-XML-snippet]
+== XML snippet
+[source, xml]
+----
+<ThrowablePatternConverter/>
+----
 
 [#org_apache_logging_log4j_core_pattern_ThrowablePatternConverter-implementations]
 == Known implementations
diff --git a/src/changelog/.0.x.x/fix-docgen-plugin-subclass.xml b/src/changelog/.0.x.x/fix-docgen-plugin-subclass.xml
new file mode 100644
index 0000000..5de37e2
--- /dev/null
+++ b/src/changelog/.0.x.x/fix-docgen-plugin-subclass.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="120" link="https://github.com/apache/logging-log4j-tools/pull/120"/>
+  <description format="asciidoc">Fix handling of subclassed plugins in Log4j Docgen</description>
+</entry>