Repository: phpDocumentor/phpDocumentor (language: php)

Issue:
Excluded classes and empty namespaces are indexed
After #3740 empty namespaces aren't rendered. But they are still indexed by the search feature, leading to 404 errors.
Excluded `@internal` classes are also indexed.

## Expected behavior

Internal classes and empty namespaces shouldn't appear in the search index.

## Actual behavior

Empty namespaces or internal classes appear in the search index.

## Steps to reproduce the problem

1. In an empty temporary directory, create the two following files:
  `Internal/MyInternalInterface.php`
  ```php
  <?php
  namespace App\Internal;
  /** @internal */
  interface MyInternalInterface {}
  ```
  `Public/MyPublicInterface.php`
  ```php
  <?php
  namespace App\Public;
  interface MyPublicInterface {}
  ```
3. Generate documentation from this folder using 3.5.3 (latest at the time of writing)
  ```bash
  curl -LO "https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.5.3/phpDocumentor.phar";
  php phpDocumentor.phar;
  ```
4. Move to doc build and launch a local server, for example on port 8080:
  ```bash
  cd .phpdoc/build;
  php -S 127.0.0.1:8080;
  ``` 
5. Open the doc index in a browser. For previous example, `http://127.0.0.1:8080/`.
6. In the top right corner search input, search for "internal".
  Two entries are found: `\App\Internal` and `\App\Public\MyPublicInterface`.
7. Click on the first search result, the `\App\Internal` namespace, the page isn't found.

## Your environment

* Version used: 3.5.3
* phpDocumentor.phar downloaded and used as-is
* PHP 8.3.6 (cli) (built: Apr 10 2024 14:21:20) (NTS)
* macOS Sonoma 14.6.1 (23G93)



Discussion / hints:

Fix patch:
diff --git a/src/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaces.php b/src/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaces.php
index 594414912..f5bbeab75 100644
--- a/src/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaces.php
+++ b/src/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaces.php
@@ -7,6 +7,7 @@
 use phpDocumentor\Compiler\ApiDocumentation\ApiDocumentationPass;
 use phpDocumentor\Descriptor\ApiSetDescriptor;
 use phpDocumentor\Descriptor\Collection;
+use phpDocumentor\Descriptor\Interfaces\ElementInterface;
 use phpDocumentor\Descriptor\Interfaces\NamespaceInterface;
 use phpDocumentor\Pipeline\Attribute\Stage;
 
@@ -21,9 +22,10 @@ protected function process(ApiSetDescriptor $subject): ApiSetDescriptor
     {
         /** @var Collection<NamespaceInterface> $index */
         $index = $subject->getIndex('namespaces');
+        $elementsIndex = $subject->getIndex('elements');
         $namespace = $subject->getNamespace();
 
-        $this->checkForEmptyChildren($namespace, $namespace->getChildren(), $index);
+        $this->checkForEmptyChildren($namespace, $namespace->getChildren(), $index, $elementsIndex);
 
         return $subject;
     }
@@ -31,14 +33,16 @@ protected function process(ApiSetDescriptor $subject): ApiSetDescriptor
     /**
      * @param Collection<NamespaceInterface> $namespaces
      * @param Collection<NamespaceInterface> $index
+     * @param Collection<ElementInterface> $elementsIndex
      */
     private function checkForEmptyChildren(
         NamespaceInterface $namespace,
         Collection $namespaces,
         Collection $index,
+        Collection $elementsIndex,
     ): void {
         foreach ($namespace->getChildren() as $childNamespace) {
-            $this->checkForEmptyChildren($childNamespace, $namespace->getChildren(), $index);
+            $this->checkForEmptyChildren($childNamespace, $namespace->getChildren(), $index, $elementsIndex);
         }
 
         if (! $namespace->isEmpty()) {
@@ -47,5 +51,6 @@ private function checkForEmptyChildren(
 
         $index->offsetUnset((string) $namespace->getFullyQualifiedStructuralElementName());
         $namespaces->offsetUnset($namespace->getName());
+        $elementsIndex->offsetUnset('~' . $namespace->getFullyQualifiedStructuralElementName());
     }
 }


Test patch:
diff --git a/tests/unit/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaceTest.php b/tests/unit/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaceTest.php
index cc3341c6d..4b1653805 100644
--- a/tests/unit/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaceTest.php
+++ b/tests/unit/phpDocumentor/Compiler/ApiDocumentation/Pass/FilterEmptyNamespaceTest.php
@@ -27,10 +27,28 @@ public function testEmptyNamespaceIsRemovedFromTree(): void
 
         $apiSet = self::faker()->apiSetDescriptor();
         $apiSet->setNamespace($parentNamespace);
+        $apiSet->getIndex('namespaces')->set(
+            (string) $parentNamespace->getFullyQualifiedStructuralElementName(),
+            $parentNamespace,
+        );
+        $apiSet->getIndex('elements')->set(
+            '~' . $parentNamespace->getFullyQualifiedStructuralElementName(),
+            $parentNamespace,
+        );
+        $apiSet->getIndex('namespaces')->set(
+            (string) $childNamespace->getFullyQualifiedStructuralElementName(),
+            $childNamespace,
+        );
+        $apiSet->getIndex('elements')->set(
+            '~' . $childNamespace->getFullyQualifiedStructuralElementName(),
+            $childNamespace,
+        );
 
         $fixture = new FilterEmptyNamespaces();
         $fixture($apiSet);
 
         $this->assertCount(0, $apiSet->getNamespace()->getChildren());
+        $this->assertCount(1, $apiSet->getIndex('namespaces')->getAll());
+        $this->assertCount(1, $apiSet->getIndex('elements')->getAll());
     }
 }