diff --git a/modules/project_browser_devel/project_browser_devel.install b/modules/project_browser_devel/project_browser_devel.install
index b86aaffb0a053f2708eb645211966389c20a9c26..ffc19ab2361e07482a630b2f55629a1493548bda 100644
--- a/modules/project_browser_devel/project_browser_devel.install
+++ b/modules/project_browser_devel/project_browser_devel.install
@@ -10,7 +10,7 @@ use Drupal\project_browser\Plugin\ProjectBrowserSourceManager;
 /**
  * Implements hook_install().
  */
-function project_browser_devel_install() {
+function project_browser_devel_install(): void {
   // Set the new random data generator as plugin and keep the current one.
   $configFactory = \Drupal::configFactory();
   $current_source_plugin = $configFactory->getEditable('project_browser.admin_settings')
@@ -27,7 +27,7 @@ function project_browser_devel_install() {
 /**
  * Implements hook_uninstall().
  */
-function project_browser_devel_uninstall() {
+function project_browser_devel_uninstall(): void {
   // Set the previous plugin.
   $admin_settings = \Drupal::configFactory()->getEditable('project_browser.admin_settings');
   $enabled_sources = $admin_settings->get('enabled_sources');
diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
index 3fb484262afee880487f9dec31de2bd5f4297d56..7b4a9cc65add3fd579482ae48dc33bcc9219a3d7 100644
--- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
+++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
@@ -25,21 +25,21 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *   local_task = {}
  * )
  */
-class RandomDataPlugin extends ProjectBrowserSourceBase {
+final class RandomDataPlugin extends ProjectBrowserSourceBase {
 
   /**
    * Utility to create random data.
    *
    * @var \Drupal\Component\Utility\Random
    */
-  protected $randomGenerator;
+  protected Random $randomGenerator;
 
   /**
    * ProjectBrowser cache bin.
    *
    * @var \Drupal\Core\Cache\CacheBackendInterface
    */
-  protected $cacheBin;
+  protected CacheBackendInterface $cacheBin;
 
   /**
    * Constructs a MockDrupalDotOrg object.
@@ -62,7 +62,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
     return new static(
       $configuration,
       $plugin_id,
@@ -80,7 +80,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
    * @return array
    *   Array of random IDs and names.
    */
-  protected function getRandomIdsAndNames($array_length = 4): array {
+  protected function getRandomIdsAndNames(int $array_length = 4): array {
     $data = [];
     for ($i = 0; $i < $array_length; $i++) {
       $data[] = [
@@ -98,7 +98,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
    * @return int
    *   Random timestamp.
    */
-  protected function getRandomDate() {
+  protected function getRandomDate(): int {
     return rand(strtotime('2 years ago'), strtotime('today'));
   }
 
@@ -163,17 +163,17 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
 
     // Filter by project machine name.
     if (!empty($query['machine_name'])) {
-      $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']);
+      $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']);
     }
 
     // Filter by categories.
     if (!empty($query['categories'])) {
-      $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])));
+      $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))));
     }
 
     // Filter by search text.
     if (!empty($query['search'])) {
-      $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE);
+      $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE);
     }
 
     return $this->createResultsPage($projects);
@@ -181,6 +181,9 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
 
   /**
    * Gets the project data from cache if available, or builds it if not.
+   *
+   * @return \Drupal\project_browser\ProjectBrowser\Project[]
+   *   An array of projects.
    */
   private function getProjectData(): array {
     $stored_projects = $this->cacheBin->get('RandomData:projects');
@@ -199,14 +202,14 @@ class RandomDataPlugin extends ProjectBrowserSourceBase {
       if ($i !== 0) {
         $project_images[] = [
           'file' => [
-            'uri' => str_replace(4, 5, $good_image),
+            'uri' => str_replace('4', '5', $good_image),
             'resource' => 'image',
           ],
           'alt' => $machine_name . ' something',
         ];
         $project_images[] = [
           'file' => [
-            'uri' => str_replace(4, 6, $good_image),
+            'uri' => str_replace('4', '6', $good_image),
             'resource' => 'image',
           ],
           'alt' => $machine_name . ' another thing',
diff --git a/modules/project_browser_source_example/project_browser_source_example.install b/modules/project_browser_source_example/project_browser_source_example.install
index 19a515978151549377bf3046f7e04d474a1d4475..c999f3663bd927c61c2e5990258eaaa712050382 100644
--- a/modules/project_browser_source_example/project_browser_source_example.install
+++ b/modules/project_browser_source_example/project_browser_source_example.install
@@ -10,7 +10,7 @@
 /**
  * Implements hook_install().
  */
-function project_browser_source_example_install() {
+function project_browser_source_example_install(): void {
   $configFactory = \Drupal::configFactory();
   $current_source_plugin = $configFactory->getEditable('project_browser.admin_settings')
     ->get('enabled_sources');
@@ -23,7 +23,7 @@ function project_browser_source_example_install() {
 /**
  * Implements hook_uninstall().
  */
-function project_browser_source_example_uninstall() {
+function project_browser_source_example_uninstall(): void {
   $admin_settings = \Drupal::configFactory()->getEditable('project_browser.admin_settings');
   $enabled_sources = $admin_settings->get('enabled_sources');
   if (($key = array_search('project_browser_source_example', $enabled_sources)) !== FALSE) {
diff --git a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
index c7c429eab83e7126891f89e65665681d1f9a73f5..facbb1891d87a2a2f3ad756820ca528b66488135 100644
--- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
+++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\project_browser_source_example\Plugin\ProjectBrowserSource;
 
-use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
@@ -18,7 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
  *   description = @Translation("Example source plugin for Project Browser."),
  * )
  */
-class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
+final class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
 
   /**
    * Constructor for example plugin.
@@ -31,15 +30,12 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
    *   The plugin implementation definition.
    * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
    *   The request from the browser.
-   * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
-   *   The module extension list.
    */
   public function __construct(
     array $configuration,
     $plugin_id,
     $plugin_definition,
     protected readonly RequestStack $requestStack,
-    protected ModuleExtensionList $moduleExtensionList,
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
   }
@@ -47,13 +43,12 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
     return new static(
       $configuration,
       $plugin_id,
       $plugin_definition,
       $container->get(RequestStack::class),
-      $container->get(ModuleExtensionList::class),
     );
   }
 
@@ -85,7 +80,7 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
         'short_description' => 'Quick summary to show in the cards.',
         'long_description' => 'Extended project information to show in the detail page',
         'author' => 'Jane Doe',
-        'logo' => $request->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg',
+        'logo' => $request?->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg',
         'created_at' => strtotime('1 year ago'),
         'updated_at' => strtotime('1 month ago'),
         'categories' => ['cat_1:Category 1'],
@@ -152,7 +147,6 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
         // Images: Array of images using the same structure as $logo, above.
         images: [],
       );
-      $pb_path = $this->moduleExtensionList->getPath('project_browser');
       $projects[] = new Project(
         logo: $logo,
         // Maybe the source won't have all fields, but we still need to
diff --git a/phpstan.neon b/phpstan.neon
index ea077c431dbf5420f0c8903bed143a42c30743c7..76dde2088bba117a8d58282f01024deb0a8e92a3 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,7 +1,11 @@
 parameters:
-  level: 1
+  level: 8
+  universalObjectCratesClasses:
+    - Drupal\Core\Extension\Extension
   reportUnmatchedIgnoredErrors: true
   excludePaths:
+    # The scripts directory does not contain runtime code.
+    - scripts
     # The node_modules contains some PHP to ignore.
     - sveltejs
     # The recipe form contains a couple of errors that cannot be ignored.
@@ -9,9 +13,60 @@ parameters:
     - src/Form/RecipeForm.php
   # Every ignore should be documented.
   ignoreErrors:
+    # Ignore errors when using `array` as a return type.
     -
-      # @see https://www.drupal.org/docs/develop/development-tools/phpstan/handling-unsafe-usage-of-new-static#s-ignoring-the-issue
-      identifier: new.static
+      identifier: missingType.iterableValue
+      reportUnmatched: false
+
+    ### Core testing suite
+    # Caused by missing return type on \Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession().
+    -
+      message: "#^Call to an undefined method Drupal\\\\Tests\\\\WebAssert\\:\\:assert[a-zA-Z]+\\(\\)\\.$#"
+      paths:
+        - tests/src/FunctionalJavascript
+      reportUnmatched: false
+    # Caused by missing return type on \Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession().
+    -
+      message: "#^Call to an undefined method Drupal\\\\Tests\\\\WebAssert\\:\\:wait[a-zA-Z]+\\(\\)\\.$#"
+      paths:
+        - tests/src/FunctionalJavascript
+      reportUnmatched: false
+    # Caused by \Drupal\KernelTests\KernelTestBase::$container having the wrong type.
+    -
+      message: "#^Property Drupal\\\\KernelTests\\\\KernelTestBase\\:\\:\\$container \\(Drupal\\\\Core\\\\DependencyInjection\\\\ContainerBuilder\\) does not accept Drupal\\\\Component\\\\DependencyInjection\\\\ContainerInterface\\.$#"
+      paths:
+        - tests/src/Kernel/DatabaseTablesTest.php
+      reportUnmatched: false
+    # Caused by \Drupal\Tests\user\Traits\UserCreationTrait::createUser() returning FALSE instead of throwing an exception.
+    -
+      message: "#^Parameter \\#1 \\$account of method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:drupalLogin\\(\\) expects Drupal\\\\Core\\\\Session\\\\AccountInterface, Drupal\\\\user\\\\Entity\\\\User\\|false given\\.$#"
+      paths:
+        - tests/src/Functional
+        - tests/src/FunctionalJavascript
+      reportUnmatched: false
+
+    ### Package Manager
+    # @todo Remove after resolving https://www.drupal.org/i/3501836.
+    # Caused by using self instead of static as a return type in \Drupal\fixture_manipulator\FixtureManipulator.
+    -
+      message: "#^Method Drupal\\\\fixture_manipulator\\\\FixtureManipulator\\:\\:commitChanges\\(\\) invoked with 0 parameters, 1 required\\.$#"
+      paths:
+        - tests/src/Kernel/InstallerTest.php
+        - tests/src/Kernel/CoreNotUpdatedValidatorTest.php
+        - tests/src/Kernel/PackageNotInstalledValidatorTest.php
+      reportUnmatched: false
+    # Caused by missing return type on \Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait::getStageFixtureManipulator().
+    -
+      message: "#^Call to an undefined method object\\:\\:setCorePackageVersion\\(\\)\\.$#"
+      paths:
+        - tests/src/Kernel/CoreNotUpdatedValidatorTest.php
+      reportUnmatched: false
+    # Caused by missing @throws on \Drupal\package_manager\StageBase::apply().
+    -
+      message: "#^Dead catch \\- Drupal\\\\package_manager\\\\Exception\\\\StageEventException is never thrown in the try block\\.$#"
+      paths:
+        - tests/src/Kernel/CoreNotUpdatedValidatorTest.php
+      reportUnmatched: false
 
     # @todo: Remove the following rules when support is dropped for Drupal 10.2, which does not have recipes.
     -
diff --git a/project_browser.install b/project_browser.install
index 3cf1624f53adcea74973739ae87bbdd73155092f..8c5e62d7a085793c0883e5fad9d24f201be4b71f 100644
--- a/project_browser.install
+++ b/project_browser.install
@@ -13,7 +13,7 @@ use Drupal\Core\Recipe\Recipe;
  * Populates the project_browser_projects using a fixture with PHP serialized
  * items.
  */
-function project_browser_install() {
+function project_browser_install(): void {
   if (class_exists(Recipe::class)) {
     $config = \Drupal::configFactory()
       ->getEditable('project_browser.admin_settings');
@@ -26,7 +26,7 @@ function project_browser_install() {
 /**
  * Implements hook_update_last_removed().
  */
-function project_browser_update_last_removed() {
+function project_browser_update_last_removed(): int {
   return 9017;
 }
 
@@ -35,7 +35,7 @@ function project_browser_update_last_removed() {
  *
  * Remove disable_add_new_module setting.
  */
-function project_browser_update_9018() {
+function project_browser_update_9018(): void {
   $config_factory = \Drupal::configFactory();
   $config_factory->getEditable('project_browser.admin_settings')
     ->clear('disable_add_new_module')
@@ -47,7 +47,7 @@ function project_browser_update_9018() {
  *
  * Remove the Drupal.org(Mock API) setting and tables.
  */
-function project_browser_update_9019() {
+function project_browser_update_9019(): void {
   // Remove the mock from the enabled_sources, if present.
   $config = \Drupal::configFactory()->getEditable('project_browser.admin_settings');
   $enabled_sources = $config->get('enabled_sources');
diff --git a/project_browser.module b/project_browser.module
index a823a5531ffb805cf64b7cfa3fd5f1b66d4d1c67..211b4d17c3c9e7e5013aed3bf79cb811b1d359bc 100644
--- a/project_browser.module
+++ b/project_browser.module
@@ -13,10 +13,10 @@ use Drupal\project_browser\Plugin\ProjectBrowserSource\Recipes;
 /**
  * Implements hook_help().
  */
-function project_browser_help($route_name, RouteMatchInterface $route_match) {
+function project_browser_help(string $route_name, RouteMatchInterface $route_match): string {
+  $output = '';
   switch ($route_name) {
     case 'help.page.project_browser':
-      $output = '';
       $output .= '<h3>' . t('About') . '</h3>';
       $output .= '<p>' . t("The Project Browser module allows users to easily search for available Drupal modules from your site. Enhanced filtering is provided so you can find what you need.") . '</p>';
       $output .= '<p>' . t('For more information, see the <a href=":project_browser">online documentation for the Project Browser module</a>.', [':project_browser' => 'https://www.drupal.org/docs/contributed-modules/project-browser']) . '</p>';
@@ -28,14 +28,14 @@ function project_browser_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dd>' . t('Users who have the <em>Administer site configuration</em> permission can select where to search for modules from the <a href=":project_browser_settings">Project Browser settings page</a>. This can include the modules already on your site as well as contributed modules on Drupal.org', [':project_browser_settings' => Url::fromRoute('project_browser.settings')->toString()]) . '</dd>';
       $output .= '</dl>';
 
-      return $output;
   }
+  return $output;
 }
 
 /**
  * Implements hook_theme().
  */
-function project_browser_theme() {
+function project_browser_theme(): array {
   return [
     'project_browser_main_app' => [
       'variables' => [],
diff --git a/src/Activator.php b/src/Activator.php
index 19245c186ca33fb80026afc2e28d7b9789adac28..0e363bf816a19a8c3f9768dfdb88d1c8d5532246 100644
--- a/src/Activator.php
+++ b/src/Activator.php
@@ -69,7 +69,8 @@ final class Activator implements ActivatorInterface {
    */
   public function supports(Project $project): bool {
     try {
-      return $this->getActivatorForProject($project) instanceof ActivatorInterface;
+      $this->getActivatorForProject($project);
+      return TRUE;
     }
     catch (\InvalidArgumentException) {
       return FALSE;
diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php
index db85b666bf19b813e0ca2526e525103af8416c3e..78ba9e795fd78252d52ae562f9aee220154025cf 100644
--- a/src/Controller/InstallerController.php
+++ b/src/Controller/InstallerController.php
@@ -13,16 +13,18 @@ use Drupal\project_browser\ComposerInstaller\Installer;
 use Drupal\project_browser\EnabledSourceHandler;
 use Drupal\project_browser\InstallState;
 use Drupal\project_browser\ProjectBrowser\Project;
+use Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Defines a controller to install projects via UI.
  */
-class InstallerController extends ControllerBase {
+final class InstallerController extends ControllerBase {
 
   /**
    * No require or install in progress for a given module.
@@ -64,7 +66,11 @@ class InstallerController extends ControllerBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
+    // @see \Drupal\project_browser\ProjectBrowserServiceProvider.
+    assert($container->get(Installer::class) instanceof Installer);
+    assert($container->get(InstallState::class) instanceof InstallState);
+
     return new static(
       $container->get(Installer::class),
       $container->get(EnabledSourceHandler::class),
@@ -78,7 +84,7 @@ class InstallerController extends ControllerBase {
   /**
    * Checks if UI install is enabled on the site.
    */
-  public function access() :AccessResult {
+  public function access(): AccessResult {
     $ui_install = $this->config('project_browser.admin_settings')->get('allow_ui_install');
     return AccessResult::allowedIf((bool) $ui_install);
   }
@@ -131,7 +137,7 @@ class InstallerController extends ControllerBase {
   /**
    * Provides a JSON response for a given error.
    *
-   * @param \Exception $e
+   * @param \Throwable $e
    *   The error that occurred.
    * @param string $phase
    *   The phase the error occurred in.
@@ -139,7 +145,7 @@ class InstallerController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   Provides an error message to be displayed by the Project Browser UI.
    */
-  private function errorResponse(\Exception $e, string $phase = ''): JsonResponse {
+  private function errorResponse(\Throwable $e, string $phase = ''): JsonResponse {
     $exception_type_short = (new \ReflectionClass($e))->getShortName();
     $exception_message = $e->getMessage();
     $response_body = ['message' => "$exception_type_short: $exception_message"];
@@ -213,7 +219,7 @@ class InstallerController extends ControllerBase {
       // accessed after. This final check ensures a destroy is not attempted
       // during apply.
       if ($this->installer->isApplying()) {
-        throw new StageException('Another project is being added. Try again in a few minutes.');
+        throw new StageException($this->installer, 'Another project is being added. Try again in a few minutes.');
       }
 
       // Adding the TRUE parameter to destroy is dangerous, but we provide it
@@ -351,6 +357,7 @@ class InstallerController extends ControllerBase {
         if ($source === NULL) {
           return new JsonResponse(['message' => "Cannot download $project->id from any available source"], 500);
         }
+        assert($source instanceof ProjectBrowserTestMock);
         if (!$source->isProjectSafe($project)) {
           return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500);
         }
@@ -439,10 +446,10 @@ class InstallerController extends ControllerBase {
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request.
    *
-   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   * @return \Symfony\Component\HttpFoundation\Response
    *   Status message.
    */
-  public function activate(Request $request): JsonResponse {
+  public function activate(Request $request): Response {
     foreach ($request->toArray() as $project) {
       $project = $this->enabledSourceHandler->getStoredProject($project);
       $this->installState->setState($project, 'activating');
diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php
index b39b59498468583d1ea204b4daa8fb7f1fe2421c..ad6666740318e990e2089e4110753ef8c2a66020 100644
--- a/src/Controller/ProjectBrowserEndpointController.php
+++ b/src/Controller/ProjectBrowserEndpointController.php
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response;
 /**
  * Controller for the proxy layer.
  */
-class ProjectBrowserEndpointController extends ControllerBase {
+final class ProjectBrowserEndpointController extends ControllerBase {
 
   /**
    * Constructor for endpoint controller.
@@ -27,7 +27,7 @@ class ProjectBrowserEndpointController extends ControllerBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get(EnabledSourceHandler::class),
     );
@@ -44,9 +44,10 @@ class ProjectBrowserEndpointController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   Typically a project listing.
    */
-  public function getAllProjects(Request $request) {
+  public function getAllProjects(Request $request): JsonResponse {
     $id = $request->query->get('id');
     if ($id) {
+      assert(is_string($id));
       return new JsonResponse($this->enabledSource->getStoredProject($id));
     }
 
@@ -125,7 +126,7 @@ class ProjectBrowserEndpointController extends ControllerBase {
   /**
    * Returns a list of categories.
    */
-  public function getAllCategories() {
+  public function getAllCategories(): JsonResponse {
     $current_sources = $this->enabledSource->getCurrentSources();
     if (!$current_sources) {
       return new JsonResponse([], Response::HTTP_ACCEPTED);
diff --git a/src/Drush/Commands/ProjectBrowserCommands.php b/src/Drush/Commands/ProjectBrowserCommands.php
index 34e89aa41fa3013c213c89ea7c48c5aba5b41ed5..89d132808382962c068e41a4f3759d8b0b2c179c 100644
--- a/src/Drush/Commands/ProjectBrowserCommands.php
+++ b/src/Drush/Commands/ProjectBrowserCommands.php
@@ -28,7 +28,7 @@ final class ProjectBrowserCommands extends DrushCommands {
   #[Usage(name: 'project-browser:storage-clear', description: 'Clear stored Project Browser data')]
   public function storageClear(): void {
     $this->enabledSourceHandler->clearStorage();
-    $this->logger()->success(dt('Stored data from Project Browser sources have been cleared.'));
+    $this->logger()?->success(dt('Stored data from Project Browser sources have been cleared.'));
   }
 
 }
diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php
index eff2aa0c07e777b290efe2e2d7026648d5de6a8f..757a955cd7a2c6165f9b6d4193454c606cbb5901 100644
--- a/src/Element/ProjectBrowser.php
+++ b/src/Element/ProjectBrowser.php
@@ -51,10 +51,13 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
+    $install_readiness = $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE);
+    assert(is_null($install_readiness) || $install_readiness instanceof InstallReadiness);
+
     return new static(
       $plugin_id,
       $plugin_definition,
-      $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE),
+      $install_readiness,
       $container->get(ModuleHandlerInterface::class),
       $container->get(ConfigFactoryInterface::class),
     );
@@ -123,7 +126,7 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
     ];
     // @todo Fix https://www.drupal.org/node/3494512 to avoid adding
     // hard-coded values. #techdebt
-    if ($source->getPluginId() !== 'recipes' && $package_manager['available']) {
+    if ($source->getPluginId() !== 'recipes' && $package_manager['available'] && is_object($this->installReadiness)) {
       $package_manager = array_merge($package_manager, $this->installReadiness->validatePackageManager());
       $package_manager['status_checked'] = TRUE;
     }
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 30a5a0e490998f7037fe5835a774435e1f5938b3..3429dbd4b629f6723b61ab008c5d3d259709f9eb 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 /**
  * Settings form for Project Browser.
  */
-class SettingsForm extends ConfigFormBase {
+final class SettingsForm extends ConfigFormBase {
 
   public function __construct(
     ConfigFactoryInterface $config_factory,
@@ -32,7 +32,7 @@ class SettingsForm extends ConfigFormBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get(ConfigFactoryInterface::class),
       $container->get(TypedConfigManagerInterface::class),
@@ -109,6 +109,7 @@ class SettingsForm extends ConfigFormBase {
       '#attributes' => [
         'id' => 'project_browser',
       ],
+      '#tabledrag' => [],
     ];
     $options = [
       'enabled' => $this->t('Enabled'),
@@ -117,6 +118,7 @@ class SettingsForm extends ConfigFormBase {
     if (count($source_plugins) > 1) {
       $form['#attached']['library'][] = 'project_browser/tabledrag';
       foreach ($options as $status => $title) {
+        assert(is_array($table['#tabledrag']));
         $table['#tabledrag'][] = [
           'action' => 'match',
           'relationship' => 'sibling',
@@ -135,11 +137,11 @@ class SettingsForm extends ConfigFormBase {
             'class' => ['status-title', 'status-title-' . $status],
             'no_striping' => TRUE,
           ],
-        ];
-        $table['status-' . $status]['title'] = [
-          '#plain_text' => $title,
-          '#wrapper_attributes' => [
-            'colspan' => 4,
+          'title' => [
+            '#plain_text' => $title,
+            '#wrapper_attributes' => [
+              'colspan' => 4,
+            ],
           ],
         ];
 
@@ -204,7 +206,7 @@ class SettingsForm extends ConfigFormBase {
   /**
    * {@inheritdoc}
    */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
+  public function validateForm(array &$form, FormStateInterface $form_state): void {
     $all_plugins = $form_state->getValue('enabled_sources');
     if (!array_key_exists('enabled', array_count_values(array_column($all_plugins, 'status')))) {
       $form_state->setErrorByName('enabled_sources', $this->t('At least one source plugin must be enabled.'));
@@ -214,10 +216,10 @@ class SettingsForm extends ConfigFormBase {
   /**
    * {@inheritdoc}
    */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
+  public function submitForm(array &$form, FormStateInterface $form_state): void {
     $settings = $this->config('project_browser.admin_settings');
     $all_plugins = $form_state->getValue('enabled_sources');
-    $enabled_plugins = array_filter($all_plugins, fn($source) => $source['status'] === 'enabled');
+    $enabled_plugins = array_filter($all_plugins, fn($source): bool => $source['status'] === 'enabled');
     $settings
       ->set('enabled_sources', array_keys($enabled_plugins))
       ->set('allow_ui_install', $form_state->getValue('allow_ui_install'))
diff --git a/src/ModuleActivator.php b/src/ModuleActivator.php
index 6517f7795ad941e7f6ca7bb690b133293e849616..f329fe3ab0a74f00181ad3be4b0e10ff177b813d 100644
--- a/src/ModuleActivator.php
+++ b/src/ModuleActivator.php
@@ -17,16 +17,13 @@ use Symfony\Component\HttpFoundation\Response;
  */
 final class ModuleActivator implements ActivatorInterface {
 
-  use ActivationInstructionsTrait {
-    __construct as traitConstruct;
-  }
+  use ActivationInstructionsTrait;
 
   public function __construct(
     private readonly ModuleInstallerInterface $moduleInstaller,
-    ModuleExtensionList $moduleList,
-    FileUrlGeneratorInterface $fileUrlGenerator,
+    protected readonly ModuleExtensionList $moduleList,
+    protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
   ) {
-    $this->traitConstruct($moduleList, $fileUrlGenerator);
   }
 
   /**
@@ -60,7 +57,7 @@ final class ModuleActivator implements ActivatorInterface {
   /**
    * {@inheritdoc}
    */
-  public function getInstructions(Project $project): string|Url|null {
+  public function getInstructions(Project $project): string|Url {
     if ($this->getStatus($project) === ActivationStatus::Present) {
       return Url::fromRoute('system.modules_list', options: [
         'fragment' => 'module-' . str_replace('_', '-', $project->machineName),
diff --git a/src/Plugin/Derivative/LocalTaskDeriver.php b/src/Plugin/Derivative/LocalTaskDeriver.php
index 11e8b20cec79db4ed494ad66093a6477d2fc04c1..25bfc6e4638a779c63c72e45a0a957f76e99efca 100644
--- a/src/Plugin/Derivative/LocalTaskDeriver.php
+++ b/src/Plugin/Derivative/LocalTaskDeriver.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Drupal\project_browser\Plugin\Derivative;
 
 use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Component\Plugin\PluginBase;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\project_browser\EnabledSourceHandler;
@@ -24,7 +25,7 @@ final class LocalTaskDeriver extends DeriverBase implements ContainerDeriverInte
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, $base_plugin_id) {
+  public static function create(ContainerInterface $container, $base_plugin_id): static {
     return new static(
       $container->get(EnabledSourceHandler::class),
     );
@@ -50,7 +51,7 @@ final class LocalTaskDeriver extends DeriverBase implements ContainerDeriverInte
         $local_task['route_parameters'] = [
           'source' => $source_id,
         ];
-        $derivative_id = str_replace($source::DERIVATIVE_SEPARATOR, '__', $source_id);
+        $derivative_id = str_replace(PluginBase::DERIVATIVE_SEPARATOR, '__', $source_id);
         $this->derivatives[$derivative_id] = $local_task;
       }
     }
diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php
index 12aa0cb490320db4ae4a600511478306c7201a81..f8a48728608221ec90463cb15ff7377ae8780535 100644
--- a/src/Plugin/ProjectBrowserSource/DrupalCore.php
+++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php
@@ -21,7 +21,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
  *   description = @Translation("Modules included in Drupal core"),
  * )
  */
-class DrupalCore extends ProjectBrowserSourceBase {
+final class DrupalCore extends ProjectBrowserSourceBase {
 
   /**
    * All core modules are covered under security policy.
@@ -74,7 +74,7 @@ class DrupalCore extends ProjectBrowserSourceBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
     return new static(
       $configuration,
       $plugin_id,
@@ -92,10 +92,10 @@ class DrupalCore extends ProjectBrowserSourceBase {
    *   The array containing core modules, keyed by module machine name.
    */
   protected function getCoreModules() {
-    $projects = array_filter($this->moduleExtensionList->reset()->getList(), fn(Extension $project) => $project->origin === 'core');
+    $projects = array_filter($this->moduleExtensionList->reset()->getList(), fn(Extension $project): bool => $project->origin === 'core');
     $include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua();
     if (!$include_tests) {
-      $projects = array_filter($projects, fn(Extension $project) => empty($project->info['hidden']) && $project->info['package'] !== 'Testing');
+      $projects = array_filter($projects, fn(Extension $project): bool => empty($project->info['hidden']) && $project->info['package'] !== 'Testing');
     }
     return $projects;
   }
@@ -123,22 +123,22 @@ class DrupalCore extends ProjectBrowserSourceBase {
 
     // Filter by project machine name.
     if (!empty($query['machine_name'])) {
-      $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']);
+      $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']);
     }
 
     // Filter by coverage.
     if (!empty($query['security_advisory_coverage'])) {
-      $projects = array_filter($projects, fn(Project $project) => $project->isCovered);
+      $projects = array_filter($projects, fn(Project $project): bool => $project->isCovered ?? FALSE);
     }
 
     // Filter by categories.
     if (!empty($query['categories'])) {
-      $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])));
+      $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))));
     }
 
     // Filter by search text.
     if (!empty($query['search'])) {
-      $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE);
+      $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE);
     }
 
     // Filter by sorting criterion.
@@ -181,7 +181,7 @@ class DrupalCore extends ProjectBrowserSourceBase {
       $returned_list[] = new Project(
         logo: [
           'file' => [
-            'uri' => $request->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg',
+            'uri' => $request?->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg',
             'resource' => 'image',
           ],
           'alt' => '',
diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
index 8eff2b332aba2a20f4bc14b99d47ce46ccd2a2a9..3b8e26f0dfc65c2bd6c4ebfdc92d4c6ca4e50772 100644
--- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
+++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
@@ -33,7 +33,7 @@ use Symfony\Component\HttpFoundation\Response;
  *   }
  * )
  */
-class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
+final class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
 
   use StringTranslationTrait;
 
@@ -98,7 +98,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
     return new static(
       $configuration,
       $plugin_id,
@@ -159,7 +159,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
           $response_data = Json::decode($response->getBody()->getContents());
 
           $result['data'] = array_merge($result['data'], $response_data['data']);
-          if (!empty($response_data['included'])) {
+          if (!empty($response_data['included']) && !empty($result['included'])) {
             $result['included'] = array_merge($result['included'], $response_data['included']);
           }
           $iterations++;
@@ -296,13 +296,13 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
     }
 
     $api_response = $this->fetchProjects($query);
-    if (!is_array($api_response) || $api_response['code'] !== Response::HTTP_OK) {
+    if ($api_response['code'] !== Response::HTTP_OK) {
       $error_message = $api_response['message'] ?? $this->t('Error querying data.');
       return $this->createResultsPage([], 0, $error_message);
     }
 
     $returned_list = [];
-    if (is_array($api_response) && !empty($api_response['list'])) {
+    if (!empty($api_response['list'])) {
       $related = !empty($api_response['related']) ? $api_response['related'] : NULL;
       $current_drupal_version = $this->getNumericSemverVersion(\Drupal::VERSION);
       $maintained_values = $filter_values['maintained'] ?? [];
@@ -313,7 +313,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
         $uid_info = $project['relationships']['uid']['data'];
 
         $maintenance_status = $project['relationships']['field_maintenance_status']['data'] ?? [];
-        if (!empty($maintenance_status)) {
+        if (is_array($maintenance_status)) {
           $maintenance_status = [
             'id' => $maintenance_status['id'],
             'name' => $related[$maintenance_status['type']][$maintenance_status['id']]['name'],
@@ -328,8 +328,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
           ];
         }
 
-        $module_categories = $project['relationships']['field_module_categories']['data'] ?? [];
-        if (!empty($module_categories)) {
+        $module_categories = $project['relationships']['field_module_categories']['data'] ?? NULL;
+        if (is_array($module_categories)) {
           $categories = [];
           foreach ($module_categories as $module_category) {
             $categories[] = [
@@ -340,8 +340,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
           $module_categories = $categories;
         }
 
-        $project_images = $project['relationships']['field_project_images']['data'] ?? [];
-        if (!empty($project_images)) {
+        $project_images = $project['relationships']['field_project_images']['data'] ?? NULL;
+        if (is_array($project_images)) {
           $images = [];
           foreach ($project_images as $image) {
             $uri = self::DRUPAL_ORG_ENDPOINT . $related[$image['type']][$image['id']]['uri']['url'];
@@ -400,8 +400,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
             'name' => $related[$uid_info['type']][$uid_info['id']]['name'],
           ],
           packageName: $project['attributes']['field_composer_namespace'] ?? 'drupal/' . $machine_name,
-          categories: $module_categories,
-          images: $project_images,
+          categories: $module_categories ?? [],
+          images: $project_images ?? [],
           url: Url::fromUri('https://www.drupal.org/project/' . $machine_name),
         );
         $returned_list[] = $project_object;
@@ -565,7 +565,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
     if ($extra = $version_object->getVersionExtra()) {
       $version = str_replace("-$extra", '', $version);
     }
-    $minor_version = $version_object->getMinorVersion() ?? 0;
+    $minor_version = $version_object->getMinorVersion() ?? '0';
     $patch_version = explode('.', $version)[2] ?? '0';
 
     return (int) (
diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php
index 36602ff31aaf2931a7b0732436794d6dd6a489bc..a658c1ea0ce075ee5f88ede6da1475674f4c33d6 100644
--- a/src/Plugin/ProjectBrowserSource/Recipes.php
+++ b/src/Plugin/ProjectBrowserSource/Recipes.php
@@ -25,7 +25,7 @@ use Symfony\Component\Finder\Finder;
 /**
  * A source plugin that exposes recipes installed locally.
  */
-class Recipes extends ProjectBrowserSourceBase {
+final class Recipes extends ProjectBrowserSourceBase {
 
   public function __construct(
     private readonly FileSystemInterface $fileSystem,
@@ -43,6 +43,7 @@ class Recipes extends ProjectBrowserSourceBase {
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
+    assert(is_string($container->getParameter('app.root')));
     return new static(
       $container->get(FileSystemInterface::class),
       $container->get('cache.project_browser'),
@@ -89,6 +90,7 @@ class Recipes extends ProjectBrowserSourceBase {
         }
         else {
           $package = file_get_contents($path . '/composer.json');
+          assert(is_string($package));
           $package = Json::decode($package);
           $package_name = $package['name'];
 
@@ -125,22 +127,22 @@ class Recipes extends ProjectBrowserSourceBase {
 
     // Filter by project machine name.
     if (!empty($query['machine_name'])) {
-      $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']);
+      $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']);
     }
 
     // Filter by coverage.
     if (!empty($query['security_advisory_coverage'])) {
-      $projects = array_filter($projects, fn(Project $project) => $project->isCovered);
+      $projects = array_filter($projects, fn(Project $project): bool => $project->isCovered ?? FALSE);
     }
 
     // Filter by categories.
     if (!empty($query['categories'])) {
-      $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])));
+      $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))));
     }
 
     // Filter by search text.
     if (!empty($query['search'])) {
-      $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE);
+      $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE);
     }
 
     $total = count($projects);
@@ -181,7 +183,9 @@ class Recipes extends ProjectBrowserSourceBase {
     $contrib_recipe_names = InstalledVersions::getInstalledPackagesByType(Recipe::COMPOSER_PROJECT_TYPE);
     if ($contrib_recipe_names) {
       $path = InstalledVersions::getInstallPath($contrib_recipe_names[0]);
+      assert(is_string($path));
       $path = $this->fileSystem->realpath($path);
+      assert(is_string($path));
 
       $search_in[] = dirname($path);
     }
diff --git a/src/Plugin/ProjectBrowserSourceBase.php b/src/Plugin/ProjectBrowserSourceBase.php
index afe348c5e664ddab2f0ae7b3387783c0a4ba3789..6a481821ec4856158944f518221645173d204a63 100644
--- a/src/Plugin/ProjectBrowserSourceBase.php
+++ b/src/Plugin/ProjectBrowserSourceBase.php
@@ -7,7 +7,6 @@ use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines an abstract base class for a Project Browser source.
@@ -20,17 +19,6 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro
 
   use StringTranslationTrait;
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-    );
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -102,4 +90,13 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition(): array {
+    $definition = parent::getPluginDefinition();
+    assert(is_array($definition));
+    return $definition;
+  }
+
 }
diff --git a/src/Plugin/ProjectBrowserSourceInterface.php b/src/Plugin/ProjectBrowserSourceInterface.php
index 9bb2d2703d5d3735450127b5d60b40f34696d5b1..482e153be2b5618d275a1611b642b8ad9ae9c8a0 100644
--- a/src/Plugin/ProjectBrowserSourceInterface.php
+++ b/src/Plugin/ProjectBrowserSourceInterface.php
@@ -62,4 +62,9 @@ interface ProjectBrowserSourceInterface extends PluginInspectionInterface {
    */
   public function getSortOptions(): array;
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition(): array;
+
 }
diff --git a/src/Plugin/ProjectBrowserSourceManager.php b/src/Plugin/ProjectBrowserSourceManager.php
index 7e913bb29bc0c909cf12fff22ebff02c4fd3b620..e7b10717fd78741b8bc7cfa7afb7c81eaba372d6 100644
--- a/src/Plugin/ProjectBrowserSourceManager.php
+++ b/src/Plugin/ProjectBrowserSourceManager.php
@@ -39,4 +39,16 @@ class ProjectBrowserSourceManager extends DefaultPluginManager {
     $this->setCacheBackend($cache_backend, 'project_browser_source_info_plugins');
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface
+   *   The source plugin.
+   */
+  public function createInstance($plugin_id, array $configuration = []) {
+    $instance = parent::createInstance($plugin_id, $configuration);
+    assert($instance instanceof ProjectBrowserSourceInterface);
+    return $instance;
+  }
+
 }
diff --git a/src/ProjectBrowser/Filter/FilterBase.php b/src/ProjectBrowser/Filter/FilterBase.php
index fa7f1ce47d4e44c788cfc93ad7972978022fc5a6..a84a7ede9ca5397e1369ee463dfb513291411f85 100644
--- a/src/ProjectBrowser/Filter/FilterBase.php
+++ b/src/ProjectBrowser/Filter/FilterBase.php
@@ -22,6 +22,7 @@ abstract class FilterBase implements \JsonSerializable {
       '_type' => match (static::class) {
         BooleanFilter::class => 'boolean',
         MultipleChoiceFilter::class => 'multiple_choice',
+        default => throw new \UnhandledMatchError('Unexpected class ' . static::class),
       },
     ] + get_object_vars($this);
 
diff --git a/src/ProjectBrowserServiceProvider.php b/src/ProjectBrowserServiceProvider.php
index 01f34d2d7f628679ca759bcacd7769dd5be2d3b4..595a6a4d922cf210a188cc7dd3bc03c330476c50 100644
--- a/src/ProjectBrowserServiceProvider.php
+++ b/src/ProjectBrowserServiceProvider.php
@@ -29,7 +29,8 @@ class ProjectBrowserServiceProvider extends ServiceProviderBase {
   /**
    * {@inheritdoc}
    */
-  public function alter(ContainerBuilder $container) {
+  public function alter(ContainerBuilder $container): void {
+    assert(is_array($container->getParameter('container.modules')));
     if (array_key_exists('package_manager', $container->getParameter('container.modules'))) {
       parent::register($container);
 
diff --git a/src/RecipeActivator.php b/src/RecipeActivator.php
index 21ef587d6497952f799eccdcdac13e81b07eb3c9..72df6d4afc7dab83bb16425fd59e9235d0c9d64e 100644
--- a/src/RecipeActivator.php
+++ b/src/RecipeActivator.php
@@ -22,11 +22,9 @@ use Symfony\Component\HttpFoundation\Response;
 /**
  * Applies locally installed recipes.
  */
-class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
+final class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
 
-  use ActivationInstructionsTrait {
-    __construct as traitConstruct;
-  }
+  use ActivationInstructionsTrait;
 
   /**
    * The state key that stores the record of all applied recipes.
@@ -39,11 +37,9 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
     private readonly string $appRoot,
     private readonly StateInterface $state,
     private readonly FileSystemInterface $fileSystem,
-    ModuleExtensionList $moduleList,
-    FileUrlGeneratorInterface $fileUrlGenerator,
-  ) {
-    $this->traitConstruct($moduleList, $fileUrlGenerator);
-  }
+    protected readonly ModuleExtensionList $moduleList,
+    protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
+  ) {}
 
   /**
    * {@inheritdoc}
@@ -97,6 +93,10 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
    */
   public function activate(Project $project): ?Response {
     $path = $this->getPath($project);
+    if (!$path) {
+      return NULL;
+    }
+
     $recipe = Recipe::createFromDirectory($path);
 
     // If the recipe has input, return a response that will instruct the Svelte
@@ -127,7 +127,7 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public function getInstructions(Project $project): string|Url|null {
+  public function getInstructions(Project $project): string {
     $instructions = '<p>' . $this->t('To apply this recipe, run the following command at the command line:') . '</p>';
 
     $command = sprintf(
@@ -174,7 +174,7 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface {
       }
     }
 
-    return $path ? $this->fileSystem->realpath($path) : NULL;
+    return $path ? ($this->fileSystem->realpath($path) ?: NULL) : NULL;
   }
 
 }
diff --git a/src/Routing/ProjectBrowserRoutes.php b/src/Routing/ProjectBrowserRoutes.php
index 0f2361d25f632fa88adeb1039372839c161f2dcf..2ca6b7859f4d6367ef3ff3bce5b52c11b5018221 100644
--- a/src/Routing/ProjectBrowserRoutes.php
+++ b/src/Routing/ProjectBrowserRoutes.php
@@ -16,7 +16,7 @@ use Symfony\Component\Routing\Route;
  * @internal
  *   Routing callbacks are internal.
  */
-class ProjectBrowserRoutes implements ContainerInjectionInterface {
+final class ProjectBrowserRoutes implements ContainerInjectionInterface {
 
   /**
    * Constructor for project browser routes.
@@ -31,7 +31,7 @@ class ProjectBrowserRoutes implements ContainerInjectionInterface {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get(ModuleHandlerInterface::class),
     );
diff --git a/tests/modules/project_browser_test/project_browser_test.install b/tests/modules/project_browser_test/project_browser_test.install
index ee0e5c0713f561d163e6fa46b51c5c40dae28d49..a8600740df05188a0c89cadd2e629e929f346230 100644
--- a/tests/modules/project_browser_test/project_browser_test.install
+++ b/tests/modules/project_browser_test/project_browser_test.install
@@ -5,6 +5,8 @@
  * Contains install and update functions for testing Project Browser.
  */
 
+declare(strict_types=1);
+
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Database\Database;
 
@@ -13,7 +15,7 @@ use Drupal\Core\Database\Database;
  *
  * Database Table storing.
  */
-function project_browser_test_schema() {
+function project_browser_test_schema(): array {
   return [
     'project_browser_projects' => [
       'description' => 'Project browser project',
@@ -120,7 +122,7 @@ function project_browser_test_schema() {
 /**
  * Replace Project Browser data with test data.
  */
-function project_browser_test_install() {
+function project_browser_test_install(): void {
   $connection = Database::getConnection();
   $connection->truncate('project_browser_projects')->execute();
   $connection->truncate('project_browser_categories')->execute();
@@ -128,7 +130,9 @@ function project_browser_test_install() {
   $module_path = \Drupal::service('module_handler')->getModule('project_browser')->getPath();
 
   $category_values = [];
-  $projects = Json::decode(file_get_contents($module_path . '/tests/fixtures/projects_fixture.json'));
+  $contents = file_get_contents($module_path . '/tests/fixtures/projects_fixture.json');
+  assert(is_string($contents));
+  $projects = Json::decode($contents);
   // Insert fixture data to the database.
   $query = $connection->insert('project_browser_projects')->fields([
     'nid',
diff --git a/tests/modules/project_browser_test/project_browser_test.module b/tests/modules/project_browser_test/project_browser_test.module
index faaa7233cca748a5d3f7ae94544f77514fe72065..a4bf231ed0ef8ec75a9dc62284d4b7ee774570a1 100644
--- a/tests/modules/project_browser_test/project_browser_test.module
+++ b/tests/modules/project_browser_test/project_browser_test.module
@@ -5,12 +5,14 @@
  * For use in Project Browser tests.
  */
 
+declare(strict_types=1);
+
 use Drupal\Core\Asset\AttachedAssetsInterface;
 
 /**
  * Implements hook_js_settings_alter().
  */
-function project_browser_test_js_settings_alter(array &$settings, AttachedAssetsInterface $assets) {
+function project_browser_test_js_settings_alter(array &$settings, AttachedAssetsInterface $assets): void {
   // For testing purposes, trick Project Browser into thinking Pinky and The
   // Brain has been downloaded but not installed.
   $settings['project_browser']['modules']['pinky_brain'] = 0;
diff --git a/tests/modules/project_browser_test/src/Controller/TestPageController.php b/tests/modules/project_browser_test/src/Controller/TestPageController.php
index 225cc4dc1bb42597569555b7d69ae3c317beac75..99372e6b151a375fd967479f80706754f9e91dbf 100644
--- a/tests/modules/project_browser_test/src/Controller/TestPageController.php
+++ b/tests/modules/project_browser_test/src/Controller/TestPageController.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test\Controller;
 
 use Drupal\Core\Controller\ControllerBase;
diff --git a/tests/modules/project_browser_test/src/Datetime/TestTime.php b/tests/modules/project_browser_test/src/Datetime/TestTime.php
index 041bf174b59470bef30df86a4d158827e5917211..b4b4100c63f1ae26d66d3b69782a44878d6403d9 100644
--- a/tests/modules/project_browser_test/src/Datetime/TestTime.php
+++ b/tests/modules/project_browser_test/src/Datetime/TestTime.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test\Datetime;
 
 use Drupal\Component\Datetime\TimeInterface;
@@ -20,21 +22,21 @@ class TestTime implements TimeInterface {
   /**
    * {@inheritdoc}
    */
-  public function getRequestMicroTime() {
+  public function getRequestMicroTime(): float {
     return $this->decorated->getRequestMicroTime();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getCurrentTime() {
+  public function getCurrentTime(): int {
     return $this->decorated->getCurrentTime();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getCurrentMicroTime() {
+  public function getCurrentMicroTime(): float {
     return $this->decorated->getCurrentMicroTime();
   }
 
@@ -44,7 +46,9 @@ class TestTime implements TimeInterface {
   public function getRequestTime(): int {
     // @phpstan-ignore-next-line
     if ($faked_date = \Drupal::state()->get('project_browser_test.fake_date_time')) {
-      return \DateTime::createFromFormat('U', $faked_date)->getTimestamp();
+      if ($date_time = \DateTime::createFromFormat('U', $faked_date)) {
+        return $date_time->getTimestamp();
+      }
     }
     return $this->decorated->getRequestTime();
   }
diff --git a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php
index b6beb005a43e1e21fbcaccc504ae81f093d029f6..4e14e69765d40e0518abfc461c9fdf07c2624dfb 100644
--- a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php
+++ b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test;
 
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -109,33 +111,34 @@ class DrupalOrgClientMiddleware {
    * if they were real-time API results, providing controlled and predictable
    * data to validate functionality.
    */
-  public function __invoke() {
+  public function __invoke(): \Closure {
     return function ($handler) {
       return function (RequestInterface $request, array $options) use ($handler) {
         $json_response = '';
         // This endpoint, when accessed in a browser, returns the JSON data
         // which is used to generate the fixtures used in
         // ProjectBrowserUiTestJsonApi test.
-        $actual_api_endpoint = $request->getUri();
+        $actual_api_endpoint = (string) $request->getUri();
         if (strpos($actual_api_endpoint, DrupalDotOrgJsonApi::JSONAPI_ENDPOINT) !== FALSE) {
           $relevant_path = str_replace(DrupalDotOrgJsonApi::JSONAPI_ENDPOINT, '', $actual_api_endpoint);
           // Remove semver query as it is core version dependent.
           // Processed query will act as relevant path to fixtures.
-          $relevant_path = preg_replace('/&filter%5Bcore_semver_minimum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path);
-          $relevant_path = preg_replace('/&filter%5Bcore_semver_maximum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path);
+          $relevant_path = (string) preg_replace('/&filter%5Bcore_semver_minimum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path);
+          $relevant_path = (string) preg_replace('/&filter%5Bcore_semver_maximum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path);
           $path_to_fixture = self::DRUPALORG_JSONAPI_ENDPOINT_TO_FIXTURE_MAP;
           if (isset($path_to_fixture[$relevant_path])) {
             $module_path = $this->moduleHandler->getModule('project_browser')->getPath();
-            $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]);
-            $json_response = new Response(200, [], $data);
-            return new FulfilledPromise($json_response);
+            if ($data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path])) {
+              $json_response = new Response(200, [], $data);
+              return new FulfilledPromise($json_response);
+            }
           }
 
           throw new \Exception('Attempted call to the Drupal.org jsonapi endpoint that is not mocked in middleware: ' . $relevant_path);
         }
         // Other queries to the non-jsonapi endpoints.
-        elseif (strpos($request->getUri(), DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT) !== FALSE) {
-          $relevant_path = str_replace(DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT, '', $request->getUri());
+        elseif (strpos($actual_api_endpoint, DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT) !== FALSE) {
+          $relevant_path = str_replace(DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT, '', $actual_api_endpoint);
           $path_to_fixture = self::DRUPALORG_ENDPOINT_TO_FIXTURE_MAP;
 
           $is_outdated = $this->state->get('project_browser:test_deprecated_api');
@@ -149,9 +152,10 @@ class DrupalOrgClientMiddleware {
 
           if (isset($path_to_fixture[$relevant_path])) {
             $module_path = $this->moduleHandler->getModule('project_browser')->getPath();
-            $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]);
-            $json_response = new Response(200, [], $data);
-            return new FulfilledPromise($json_response);
+            if ($data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path])) {
+              $json_response = new Response(200, [], $data);
+              return new FulfilledPromise($json_response);
+            }
           }
 
           throw new \Exception('Attempted call to the Drupal.org endpoint that is not mocked in middleware: ' . $relevant_path);
diff --git a/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php b/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php
index 3043447d67ec68c98d0cffef5c9cf9c7465788cb..65ac77f652c93dfd8cb5d6b6864858cda00bbacd 100644
--- a/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php
+++ b/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test\Extension;
 
 use Drupal\Core\Extension\ModuleInstallerInterface;
@@ -24,7 +26,7 @@ class TestModuleInstaller implements ModuleInstallerInterface {
    * @param bool $enable_dependencies
    *   True if dependencies should be enabled.
    */
-  public function install(array $module_list, $enable_dependencies = TRUE) {
+  public function install(array $module_list, $enable_dependencies = TRUE): bool {
     if (!empty(array_intersect(['cream_cheese', 'kangaroo'], $module_list))) {
       return TRUE;
     }
@@ -34,21 +36,21 @@ class TestModuleInstaller implements ModuleInstallerInterface {
   /**
    * {@inheritdoc}
    */
-  public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
+  public function uninstall(array $module_list, $uninstall_dependents = TRUE): bool {
     return $this->decorated->uninstall($module_list, $uninstall_dependents);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
+  public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator): void {
     $this->decorated->addUninstallValidator($uninstall_validator);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function validateUninstall(array $module_list) {
+  public function validateUninstall(array $module_list): array {
     return $this->decorated->validateUninstall($module_list);
   }
 
diff --git a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
index c74a5a2995cc0e7062a3704134f0bb71121e9ff3..248149b423a04414dba27787a99009352545e9b5 100644
--- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
+++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
@@ -1,10 +1,11 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test\Plugin\ProjectBrowserSource;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Html;
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\State\StateInterface;
@@ -31,7 +32,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *   }
  * )
  */
-class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
+final class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
 
   /**
    * This is what the Mock understands as "Covered" modules.
@@ -76,8 +77,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    *   The database connection.
    * @param \Drupal\Core\State\StateInterface $state
    *   The session state.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBin
-   *   The back end cache interface.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
    *   The module handler.
    */
@@ -88,7 +87,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
     private readonly LoggerInterface $logger,
     private readonly Connection $database,
     private readonly StateInterface $state,
-    private readonly CacheBackendInterface $cacheBin,
     private readonly ModuleHandlerInterface $moduleHandler,
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
@@ -97,7 +95,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
     return new static(
       $configuration,
       $plugin_id,
@@ -105,7 +103,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
       $container->get('logger.factory')->get('project_browser'),
       $container->get(Connection::class),
       $container->get(StateInterface::class),
-      $container->get('cache.project_browser'),
       $container->get(ModuleHandlerInterface::class),
     );
   }
@@ -119,7 +116,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @return array|array[]
    *   An array with the term id, name and description.
    */
-  protected function getStatuses(int $taxonomy_id) {
+  protected function getStatuses(int $taxonomy_id): array {
     $body = '';
     // Development.
     if ($taxonomy_id === 46) {
@@ -179,7 +176,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @param array $query
    *   Query array to transform.
    */
-  protected function convertSort(array &$query) {
+  protected function convertSort(array &$query): void {
     if (!empty($query['sort'])) {
       $options_available = $this->getSortOptions();
       if (!in_array($query['sort'], array_keys($options_available))) {
@@ -220,7 +217,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @param array $query
    *   Query array to transform.
    */
-  protected function convertMaintenance(array &$query) {
+  protected function convertMaintenance(array &$query): void {
     if (!empty($query['maintenance_status'])) {
       $query['maintenance_status'] = self::MAINTAINED_VALUES;
     }
@@ -235,7 +232,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @param array $query
    *   Query array to transform.
    */
-  protected function convertDevelopment(array &$query) {
+  protected function convertDevelopment(array &$query): void {
     if (!empty($query['development_status'])) {
       $query['development_status'] = self::ACTIVE_VALUES;
     }
@@ -250,7 +247,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @param array $query
    *   Query array to transform.
    */
-  protected function convertSecurity(array &$query) {
+  protected function convertSecurity(array &$query): void {
     if (!empty($query['security_advisory_coverage'])) {
       $query['security_advisory_coverage'] = self::COVERED_VALUES;
     }
@@ -291,7 +288,9 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    */
   protected function getCategoryData(): array {
     $module_path = $this->moduleHandler->getModule('project_browser')->getPath();
-    $category_list = Json::decode(file_get_contents($module_path . '/tests/fixtures/category_list.json')) ?? [];
+    $contents = file_get_contents($module_path . '/tests/fixtures/category_list.json');
+    assert(is_string($contents));
+    $category_list = Json::decode($contents) ?? [];
     $categories = [];
     foreach ($category_list as $category) {
       $categories[$category['tid']] = [
@@ -366,7 +365,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
     $categories = $this->getCategoryData();
 
     $returned_list = [];
-    if ($api_response) {
+    if (is_array($api_response)) {
       foreach ($api_response['list'] as $project_data) {
         $avatar_url = 'https://git.drupalcode.org/project/' . $project_data['field_project_machine_name'] . '/-/avatar';
         $logo = [
@@ -384,7 +383,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
           isCompatible: TRUE,
           isMaintained: in_array($project_data['maintenance_status'], self::MAINTAINED_VALUES),
           isCovered: in_array($project_data['field_security_advisory_coverage'], self::COVERED_VALUES),
-          projectUsageTotal: array_reduce($project_data['project_data']['project_usage'] ?? [], fn($total, $project_usage) => $total + $project_usage) ?: 0,
+          projectUsageTotal: array_reduce($project_data['project_data']['project_usage'] ?? [], fn($total, $project_usage): int => $total + $project_usage) ?: 0,
           machineName: $project_data['field_project_machine_name'],
           body: $this->relativeToAbsoluteUrls($project_data['project_data']['body'], 'https://www.drupal.org'),
           title: $project_data['title'],
@@ -392,7 +391,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
           packageName: 'drupal/' . $project_data['field_project_machine_name'],
           url: Url::fromUri('https://www.drupal.org/project/' . $project_data['field_project_machine_name']),
           // Add name property to each category, so it can be rendered.
-          categories: array_map(fn($category) => $categories[$category['id']] ?? '', $project_data['project_data']['taxonomy_vocabulary_3'] ?? []),
+          categories: array_map(fn($category): array => $categories[$category['id']] ?? [], $project_data['project_data']['taxonomy_vocabulary_3'] ?? []),
           images: $project_data['project_data']['field_project_images'] ?? [],
           warnings: $this->getWarnings($project_data),
           id: $project_data['field_project_machine_name'],
@@ -400,7 +399,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
       }
     }
 
-    return $this->createResultsPage($returned_list, $api_response['total_results'] ?? 0, static::$resultsError);
+    return $this->createResultsPage($returned_list, (int) ($api_response['total_results'] ?? 0), static::$resultsError);
   }
 
   /**
@@ -408,7 +407,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    *
    * Here, we're querying the local database, populated from the fixture.
    */
-  protected function fetchProjects($query) {
+  protected function fetchProjects(array $query): bool|array {
     $query = $this->convertQueryOptions($query);
     try {
       $db_query = $this->database->select('project_browser_projects', 'pbp')
@@ -462,13 +461,13 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
       // projects.
       $total_results = $db_query->countQuery()
         ->execute()
-        ->fetchField();
+        ?->fetchField();
       $offset = $query['page'] ?? 0;
       $limit = $query['limit'] ?? 50;
       $db_query->range($limit * $offset, $limit);
       $result = $db_query
         ->execute()
-        ->fetchAll();
+        ?->fetchAll() ?? [];
       $db_projects = array_map(function ($project_data) {
         $data = (array) $project_data;
         $data['project_data'] = unserialize($project_data->project_data);
@@ -498,7 +497,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @return string[]
    *   An array of warning messages.
    */
-  protected function getWarnings(array $project) {
+  protected function getWarnings(array $project): array {
     // This is based on logic from Drupal.org.
     // @see https://git.drupalcode.org/project/drupalorg/-/blob/e31465608d1380345834/drupalorg_project/drupalorg_project.module
     $warnings = [];
@@ -530,7 +529,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
    * @return array
    *   Body array with relative URLs converted to absolute ones.
    */
-  protected function relativeToAbsoluteUrls(array $body, string $base_url) {
+  protected function relativeToAbsoluteUrls(array $body, string $base_url): array {
     if (empty($body['value'])) {
       $body['value'] = $body['summary'] ?? '';
     }
diff --git a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
index da31f551e695f1a33d59c6fc025a485932436d8a..d70305429cc4e78c08bb7cfc00eacbb2b669b48b 100644
--- a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
+++ b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -14,7 +16,7 @@ class ProjectBrowserTestServiceProvider extends ServiceProviderBase {
   /**
    * {@inheritdoc}
    */
-  public function alter(ContainerBuilder $container) {
+  public function alter(ContainerBuilder $container): void {
     // The InstallReadiness service is defined by ProjectBrowserServiceProvider
     // if Package Manager is installed.
     if ($container->hasDefinition(InstallReadiness::class)) {
diff --git a/tests/modules/project_browser_test/src/TestInstallReadiness.php b/tests/modules/project_browser_test/src/TestInstallReadiness.php
index 8dcacb2da268593501c01a447b0d2455c129b069..bc17893aee871f02adb63d39e16bbd6a7175d31b 100644
--- a/tests/modules/project_browser_test/src/TestInstallReadiness.php
+++ b/tests/modules/project_browser_test/src/TestInstallReadiness.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\project_browser_test;
 
 use Drupal\Core\State\StateInterface;
diff --git a/tests/src/Functional/ClearStorageTest.php b/tests/src/Functional/ClearStorageTest.php
index 47c893922c019cb60590991f0a84ff7be8522151..d0be6f3f68a1136fb86c72673638ff12e936aed3 100644
--- a/tests/src/Functional/ClearStorageTest.php
+++ b/tests/src/Functional/ClearStorageTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Functional;
 
 use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php
index dd18731dbe0870c2c48e2cba4f6e50eca2892f3f..9f9a05ca1f804eaf3d813367c14ea0782d0e5b38 100644
--- a/tests/src/Functional/EnabledSourceHandlerTest.php
+++ b/tests/src/Functional/EnabledSourceHandlerTest.php
@@ -54,6 +54,7 @@ class EnabledSourceHandlerTest extends BrowserTestBase {
     $handler = $this->container->get(EnabledSourceHandler::class);
 
     $projects = $handler->getProjects('project_browser_test_mock');
+    assert(!empty($projects));
     $list = reset($projects)->list;
     $this->assertNotEmpty($list);
     $project = reset($list);
@@ -74,6 +75,7 @@ class EnabledSourceHandlerTest extends BrowserTestBase {
     // and commands set.
     $projects = $this->container->get(EnabledSourceHandler::class)
       ->getProjects('project_browser_test_mock');
+    assert(!empty($projects));
     $list = reset($projects)->list;
     $this->assertNotEmpty($list);
     $project = reset($list);
diff --git a/tests/src/Functional/InstallTest.php b/tests/src/Functional/InstallTest.php
index ff9b82719908b3499348a38f54171e0924e3afb0..c4756360953de949e4abb47f524bd0ad4d6971c4 100644
--- a/tests/src/Functional/InstallTest.php
+++ b/tests/src/Functional/InstallTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Functional;
 
 use Drupal\Tests\BrowserTestBase;
@@ -40,7 +42,7 @@ class InstallTest extends BrowserTestBase {
   /**
    * Reloads services used by this test.
    */
-  protected function reloadServices() {
+  protected function reloadServices(): void {
     $this->rebuildContainer();
     $this->moduleHandler = $this->container->get('module_handler');
   }
@@ -48,15 +50,15 @@ class InstallTest extends BrowserTestBase {
   /**
    * Tests that the module is installable.
    */
-  public function testInstallation() {
+  public function testInstallation(): void {
     $edit = [];
     $edit['modules[project_browser][enable]'] = 'project_browser';
     $this->drupalGet('admin/modules');
     $this->submitForm($edit, 'Install');
     // @todo Convert this to pageTextContains(), and only look for `installed`,
     //   once Drupal 10 support is dropped.
-    $this->assertSession()->pageTextMatches('/Module Project Browser has been (installed|enabled)\./', 'status');
-    $this->assertSession()->statusCodeEquals('200');
+    $this->assertSession()->pageTextMatches('/Module Project Browser has been (installed|enabled)\./');
+    $this->assertSession()->statusCodeEquals(200);
     $this->reloadServices();
     $this->assertTrue($this->moduleHandler->moduleExists('project_browser'));
     $this->assertFalse($this->moduleHandler->moduleExists('package_manager'));
diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php
index bd43f760cf308d79b8d7dda108a02d44a4f5ffef..1bb7a0d50cddd19d4324b8deaa9999bbebf9e5da 100644
--- a/tests/src/Functional/InstallerControllerTest.php
+++ b/tests/src/Functional/InstallerControllerTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Functional;
 
 use Drupal\Component\Serialization\Json;
@@ -162,7 +164,9 @@ class InstallerControllerTest extends BrowserTestBase {
     ]);
     $query->execute();
     $this->initPackageManager();
-    $this->installer = $this->container->get(Installer::class);
+    /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
+    $installer = $this->container->get(Installer::class);
+    $this->installer = $installer;
     $this->drupalLogin($this->drupalCreateUser(['administer modules']));
     $this->config('project_browser.admin_settings')
       ->set('enabled_sources', ['project_browser_test_mock', 'drupal_core'])
@@ -178,7 +182,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::access
    */
-  public function testUiInstallUnavailableIfDisabled() {
+  public function testUiInstallUnavailableIfDisabled(): void {
     $this->config('project_browser.admin_settings')->set('allow_ui_install', FALSE)->save();
     $this->drupalGet('admin/modules/project_browser/install-begin');
     $this->assertSession()->statusCodeEquals(403);
@@ -190,8 +194,8 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::begin
    */
-  public function testInstallSecurityRevokedModule() {
-    $this->assertSame([], $this->container->get(InstallState::class)->toArray());
+  public function testInstallSecurityRevokedModule(): void {
+    $this->assertSame([], $this->getInstallState()->toArray());
     $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
     $this->stageId = Json::decode($contents)['stage_id'];
     $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/security_revoked_module', [
@@ -206,8 +210,8 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::require
    */
-  public function testInstallAlreadyPresentPackage() {
-    $this->assertSame([], $this->container->get(InstallState::class)->toArray());
+  public function testInstallAlreadyPresentPackage(): void {
+    $this->assertSame([], $this->getInstallState()->toArray());
     // Though core is not available as a choice in project browser, it works
     // well for the purposes of this test as it's definitely already added
     // via composer.
@@ -225,8 +229,8 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::begin
    */
-  private function doStart() {
-    $this->assertSame([], $this->container->get(InstallState::class)->toArray());
+  private function doStart(): void {
+    $this->assertSame([], $this->getInstallState()->toArray());
     $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
     $this->stageId = Json::decode($contents)['stage_id'];
     $this->assertSession()->statusCodeEquals(200);
@@ -238,7 +242,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::require
    */
-  private function doRequire() {
+  private function doRequire(): void {
     $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [
       'stage_id' => $this->stageId,
     ]);
@@ -252,7 +256,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::apply
    */
-  private function doApply() {
+  private function doApply(): void {
     $this->drupalGet("/admin/modules/project_browser/install-apply/$this->stageId");
     $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId);
     $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
@@ -264,7 +268,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::postApply
    */
-  private function doPostApply() {
+  private function doPostApply(): void {
     $this->drupalGet("/admin/modules/project_browser/install-post_apply/$this->stageId");
     $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId);
     $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
@@ -276,7 +280,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::destroy
    */
-  private function doDestroy() {
+  private function doDestroy(): void {
     $this->drupalGet("/admin/modules/project_browser/install-destroy/$this->stageId");
     $expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId);
     $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
@@ -286,7 +290,7 @@ class InstallerControllerTest extends BrowserTestBase {
   /**
    * Calls every endpoint needed to do a UI install and confirms they work.
    */
-  public function testUiInstallerEndpoints() {
+  public function testUiInstallerEndpoints(): void {
     $this->doStart();
     $this->doRequire();
     $this->doApply();
@@ -299,7 +303,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::create
    */
-  public function testPreCreateError() {
+  public function testPreCreateError(): void {
     $message = t('This is a PreCreate error.');
     $result = ValidationResult::createError([$message]);
     TestSubscriber::setTestResult([$result], PreCreateEvent::class);
@@ -313,7 +317,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::create
    */
-  public function testPreCreateException() {
+  public function testPreCreateException(): void {
     $error = new \Exception('PreCreate did not go well.');
     TestSubscriber::setException($error, PreCreateEvent::class);
     $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
@@ -326,7 +330,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::create
    */
-  public function testPostCreateException() {
+  public function testPostCreateException(): void {
     $error = new \Exception('PostCreate did not go well.');
     TestSubscriber::setException($error, PostCreateEvent::class);
     $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
@@ -339,7 +343,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::require
    */
-  public function testPreRequireError() {
+  public function testPreRequireError(): void {
     $message = t('This is a PreRequire error.');
     $result = ValidationResult::createError([$message]);
     $this->doStart();
@@ -356,7 +360,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::require
    */
-  public function testPreRequireException() {
+  public function testPreRequireException(): void {
     $error = new \Exception('PreRequire did not go well.');
     TestSubscriber::setException($error, PreRequireEvent::class);
     $this->doStart();
@@ -372,7 +376,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::require
    */
-  public function testPostRequireException() {
+  public function testPostRequireException(): void {
     $error = new \Exception('PostRequire did not go well.');
     TestSubscriber::setException($error, PostRequireEvent::class);
     $this->doStart();
@@ -388,7 +392,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::apply
    */
-  public function testPreApplyError() {
+  public function testPreApplyError(): void {
     $message = t('This is a PreApply error.');
     $result = ValidationResult::createError([$message]);
     TestSubscriber::setTestResult([$result], PreApplyEvent::class);
@@ -404,7 +408,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::apply
    */
-  public function testPreApplyException() {
+  public function testPreApplyException(): void {
     $error = new \Exception('PreApply did not go well.');
     TestSubscriber::setException($error, PreApplyEvent::class);
     $this->doStart();
@@ -419,7 +423,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::apply
    */
-  public function testPostApplyException() {
+  public function testPostApplyException(): void {
     $error = new \Exception('PostApply did not go well.');
     TestSubscriber::setException($error, PostApplyEvent::class);
     $this->doStart();
@@ -435,7 +439,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::unlock
    */
-  public function testInstallUnlockMessage() {
+  public function testInstallUnlockMessage(): void {
     $this->doStart();
     $this->doRequire();
 
@@ -448,10 +452,14 @@ class InstallerControllerTest extends BrowserTestBase {
       $this->assertSame($expected_message, $response['message']);
 
       if ($response['unlock_url']) {
-        $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', parse_url($response['unlock_url'], PHP_URL_PATH));
-        $query = parse_url($response['unlock_url'], PHP_URL_QUERY);
-        parse_str($query, $query);
+        $path_string = parse_url($response['unlock_url'], PHP_URL_PATH);
+        $this->assertIsString($path_string);
+        $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', $path_string);
+        $query_string = parse_url($response['unlock_url'], PHP_URL_QUERY);
+        $this->assertIsString($query_string);
+        parse_str($query_string, $query);
         $this->assertNotEmpty($query['token']);
+        $this->assertIsString($query['destination']);
         $this->assertStringEndsWith('/admin/modules/browse/project_browser_test_mock', $query['destination']);
       }
     };
@@ -466,7 +474,7 @@ class InstallerControllerTest extends BrowserTestBase {
         'status' => 'requiring',
       ],
     ];
-    $this->assertSame($expected, $this->container->get(InstallState::class)->toArray());
+    $this->assertSame($expected, $this->getInstallState()->toArray());
     $this->assertFalse($this->installer->isAvailable());
     $this->assertFalse($this->installer->isApplying());
     TestTime::setFakeTimeByOffset("+800 seconds");
@@ -500,7 +508,7 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::unlock
    */
-  public function testCanBreakLock() {
+  public function testCanBreakLock(): void {
     $this->doStart();
     // Try beginning another install while one is in progress, but not yet in
     // the applying stage.
@@ -515,8 +523,8 @@ class InstallerControllerTest extends BrowserTestBase {
     $json = Json::decode($content);
     $this->assertSame('The process for adding projects is locked, but that lock has expired. Use [+ unlock link] to unlock the process and try to add the project again.', $json['message']);
     $unlock_url = parse_url($json['unlock_url']);
-    parse_str($unlock_url['query'], $unlock_url['query']);
-    $unlock_content = $this->drupalGet($unlock_url['path'], ['query' => $unlock_url['query']]);
+    parse_str($unlock_url['query'] ?? '', $unlock_url['query']);
+    $unlock_content = $this->drupalGet($unlock_url['path'] ?? '', ['query' => $unlock_url['query']]);
     $this->assertSession()->statusCodeEquals(200);
     $this->assertTrue($this->installer->isAvailable());
     $this->assertStringContainsString('Operation complete, you can add a new project again.', $unlock_content);
@@ -529,9 +537,9 @@ class InstallerControllerTest extends BrowserTestBase {
    *
    * @covers ::unlock
    */
-  public function testCanBreakStageWithMissingProjectBrowserLock() {
+  public function testCanBreakStageWithMissingProjectBrowserLock(): void {
     $this->doStart();
-    $this->container->get(InstallState::class)->deleteAll();
+    $this->getInstallState()->deleteAll();
     $content = $this->drupalGet('admin/modules/project_browser/install-begin', [
       'query' => ['source' => 'project_browser_test_mock'],
     ]);
@@ -541,8 +549,8 @@ class InstallerControllerTest extends BrowserTestBase {
     $json = Json::decode($content);
     $this->assertSame('The process for adding projects is locked, but that lock has expired. Use [+ unlock link] to unlock the process and try to add the project again.', $json['message']);
     $unlock_url = parse_url($json['unlock_url']);
-    parse_str($unlock_url['query'], $unlock_url['query']);
-    $unlock_content = $this->drupalGet($unlock_url['path'], ['query' => $unlock_url['query']]);
+    parse_str($unlock_url['query'] ?? '', $unlock_url['query']);
+    $unlock_content = $this->drupalGet($unlock_url['path'] ?? '', ['query' => $unlock_url['query']]);
     $this->assertSession()->statusCodeEquals(200);
     $this->assertTrue($this->installer->isAvailable());
     $this->assertStringContainsString('Operation complete, you can add a new project again.', $unlock_content);
@@ -585,12 +593,12 @@ class InstallerControllerTest extends BrowserTestBase {
    * @param string $status
    *   The install state.
    */
-  protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL) {
+  protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL): void {
     $expect_install[$project_id] = [
       'source' => $source,
       'status' => $status,
     ];
-    $this->assertSame($expect_install, $this->container->get(InstallState::class)->toArray());
+    $this->assertSame($expect_install, $this->getInstallState()->toArray());
     $this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id");
     $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $status), $this->getSession()->getPage()->getContent());
     $this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag');
@@ -624,4 +632,16 @@ class InstallerControllerTest extends BrowserTestBase {
     return $this->makeApiRequest('POST', $post_url, $request_options);
   }
 
+  /**
+   * Gets the install state.
+   *
+   * @return \Drupal\project_browser\InstallState
+   *   The install state.
+   */
+  private function getInstallState(): InstallState {
+    $install_state = $this->container->get(InstallState::class);
+    assert($install_state instanceof InstallState);
+    return $install_state;
+  }
+
 }
diff --git a/tests/src/Functional/ProjectBrowserMenuTabsTest.php b/tests/src/Functional/ProjectBrowserMenuTabsTest.php
index 58336cea91ec84d54a2006f74e2e136177e3172d..01b68e4e7e34cf36aa54e39a0910f3afb26c882e 100644
--- a/tests/src/Functional/ProjectBrowserMenuTabsTest.php
+++ b/tests/src/Functional/ProjectBrowserMenuTabsTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Functional;
 
 use Drupal\Tests\BrowserTestBase;
diff --git a/tests/src/Functional/RoutingTest.php b/tests/src/Functional/RoutingTest.php
index b70a55fadb8298a3aaac963c1eafb5240f948c08..d7ac5dc20f7daa167b81b7892a8dc383486aedb2 100644
--- a/tests/src/Functional/RoutingTest.php
+++ b/tests/src/Functional/RoutingTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Functional;
 
 use Drupal\Core\Url;
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
index 9e84a6ccd4d0af4267e3aa8501f72b1aec53f7d8..d4ea81f0a44cb8eaeded8656680a183614d142d3 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
@@ -55,7 +55,9 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
 
     $this->initPackageManager();
 
-    $this->installState = $this->container->get(InstallState::class);
+    /** @var \Drupal\project_browser\InstallState $install_state */
+    $install_state = $this->container->get(InstallState::class);
+    $this->installState = $install_state;
 
     $this->config('project_browser.admin_settings')
       ->set('enabled_sources', ['project_browser_test_mock'])
@@ -97,7 +99,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    * Browser UI is used, but could happen if the module was added differently,
    * such as via the terminal with Compose or a direct file addition.
    */
-  public function testInstallModuleAlreadyInFilesystem() {
+  public function testInstallModuleAlreadyInFilesystem(): void {
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
@@ -193,7 +195,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->assertNotEmpty($download_button);
     $this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
     $this->drupalGet('/admin/config/development/project_browser');
-    $page->find('css', '#edit-allow-ui-install')->click();
+    $page->find('css', '#edit-allow-ui-install')?->click();
     $assert_session->checkboxNotChecked('edit-allow-ui-install');
     $this->submitForm([], 'Save');
     $this->assertTrue($assert_session->waitForText('The configuration options have been saved.'));
@@ -210,7 +212,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    *
    * @covers ::unlock
    */
-  public function testCanBreakStageWithMissingProjectBrowserLock() {
+  public function testCanBreakStageWithMissingProjectBrowserLock(): void {
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
 
@@ -225,7 +227,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     // the applying stage.
     $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
     $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
-    $cream_cheese_button->click();
+    $cream_cheese_button?->click();
 
     $this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.'));
 
@@ -234,7 +236,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->svelteInitHelper('text', 'Cream cheese on a bagel');
     // Try beginning another install after breaking lock.
     $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
-    $cream_cheese_button->click();
+    $cream_cheese_button?->click();
     $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
     $assert_session->waitForText('Cream cheese on a bagel is Installed');
     $this->assertSame('Cream cheese on a bagel is Installed', $installed_action->getText());
@@ -248,7 +250,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    *
    * @covers ::unlock
    */
-  public function testCanBreakLock() {
+  public function testCanBreakLock(): void {
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
 
@@ -265,14 +267,14 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     // the applying stage.
     $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
     $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
-    $cream_cheese_button->click();
+    $cream_cheese_button?->click();
     $this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.'));
     // Click Unlock Install Stage link.
     $this->clickWithWait('#ui-id-1 > p > a');
     $this->svelteInitHelper('text', 'Cream cheese on a bagel');
     // Try beginning another install after breaking lock.
     $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
-    $cream_cheese_button->click();
+    $cream_cheese_button?->click();
     $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
     $assert_session->waitForText('Cream cheese on a bagel is Installed');
     $this->assertSame('Cream cheese on a bagel is Installed', $installed_action->getText());
@@ -373,7 +375,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->assertTrue($was_selected);
 
     $dancing_queen_button = $page->find('css', '#project-browser .pb-layout__main ul > li:nth-child(3) button');
-    $this->assertFalse($dancing_queen_button->hasAttribute('disabled'));
+    $this->assertFalse($dancing_queen_button?->hasAttribute('disabled'));
 
     $this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
 
@@ -413,7 +415,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
   /**
    * Tests that adding projects to queue is plugin specific.
    */
-  public function testPluginSpecificQueue() {
+  public function testPluginSpecificQueue(): void {
     $assert_session = $this->assertSession();
     $this->container->get('module_installer')->install(['project_browser_devel'], TRUE);
     $this->drupalGet('project-browser/project_browser_test_mock');
@@ -451,10 +453,14 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
     $download_button->click();
     $unlock_url = $assert_session->waitForElementVisible('css', "#unlock-link")->getAttribute('href');
-    $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', parse_url($unlock_url, PHP_URL_PATH));
-    $query = parse_url($unlock_url, PHP_URL_QUERY);
-    parse_str($query, $query);
+    $path_string = parse_url($unlock_url, PHP_URL_PATH);
+    $this->assertIsString($path_string);
+    $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', $path_string);
+    $query_string = parse_url($unlock_url, PHP_URL_QUERY);
+    $this->assertIsString($query_string);
+    parse_str($query_string, $query);
     $this->assertNotEmpty($query['token']);
+    $this->assertIsString($query['destination']);
     $this->assertStringEndsWith('/admin/modules/browse/project_browser_test_mock', $query['destination']);
   }
 
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
index 252acbd106e2f8f460f7f6ea7a3999a7432f0f40..fbbc5ebbadfc979b83b57fa42bd4499ff2a1252d 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
@@ -91,7 +91,7 @@ class ProjectBrowserPluginTest extends WebDriverTestBase {
     $this->svelteInitHelper('css', '.pager__item--next');
     $assert_session->elementsCount('css', '.pager__item--next', 1);
 
-    $page->find('css', 'a[aria-label="Next page"]')->click();
+    $page->find('css', 'a[aria-label="Next page"]')?->click();
     $this->assertNotNull($assert_session->waitForElement('css', '.pager__item--previous'));
     $assert_session->elementsCount('css', '.pager__item--previous', 1);
   }
@@ -163,7 +163,7 @@ class ProjectBrowserPluginTest extends WebDriverTestBase {
 
     $assert_session->waitForElementVisible('css', '.pb-project .pb-project__title');
     $first_project_selector = $page->find('css', '.pb-project .pb-project__title .pb-project__link');
-    $first_project_selector->click();
+    $first_project_selector?->click();
     $this->assertTrue($assert_session->waitForText('sites report using this module'));
   }
 
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
index 2fd9a08f96de5efc8d1413babebe8498cdf88e3d..b15114f2456ea41cf4682789dd339b2e05686587 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
@@ -152,13 +152,13 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->clickWithWait('#104', '10 Results');
 
     // Use blur event to close drop-down so Clear is visible.
-    $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur();
+    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->blur();
 
     $this->pressWithWait('Clear filters', '25 Results');
 
     // Open category drop-down again by pressing space.
-    $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' ');
-    $this->assertSession()->waitForText('Media');
+    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' ');
+    $assert_session->waitForText('Media');
 
     // Click 'Media' checkbox.
     $this->clickWithWait('#67');
@@ -214,12 +214,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     // The first textarea should have the command to require the module.
     $this->assertSame('composer require drupal/helvetica', $command_boxes[0]->getValue());
     // And the second textarea should have the command to install it.
-    $this->assertStringEndsWith('drush install helvetica', $command_boxes[1]->getValue());
+    $value = $command_boxes[1]->getValue();
+    $this->assertIsString($value);
+    $this->assertStringEndsWith('drush install helvetica', $value);
 
     // Tests alt text for copy command image.
     $download_commands = $page->findAll('css', '.command-box img');
     $this->assertCount(2, $download_commands);
     $this->assertEquals('Copy the download command', $download_commands[0]->getAttribute('alt'));
+    $this->assertIsString($download_commands[1]->getAttribute('alt'));
     $this->assertStringStartsWith('Copy the install command', $download_commands[1]->getAttribute('alt'));
   }
 
@@ -531,6 +534,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    * Tests search with strings that need URI encoding.
    */
   public function testSearchForSpecialChar(): void {
+    $assert_session = $this->assertSession();
+
     // Clear filters.
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('text', '10 Results');
@@ -539,7 +544,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     // Fill in the search field.
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('&', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Vitamin&C;$?',
       'Unwritten&:/',
@@ -548,7 +553,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     // Fill in the search field.
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('n&', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Vitamin&C;$?',
       'Unwritten&:/',
@@ -556,28 +561,28 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
 
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('$', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Vitamin&C;$?',
     ]);
 
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('?', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Vitamin&C;$?',
     ]);
 
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('&:', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Unwritten&:/',
     ]);
 
     $this->inputSearchField('', TRUE);
     $this->inputSearchField('$?', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertProjectsVisible([
       'Vitamin&C;$?',
     ]);
@@ -614,7 +619,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $assert_session->waitForButton('Close')?->click();
     $assert_session->elementNotExists('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Helvetica"]');
     // Check that a different module modal can be opened.
-    $assert_session->waitForButton('Octopus')->click();
+    $assert_session->waitForButton('Octopus')?->click();
     $assert_session->waitForElementVisible('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Octopus"]');
     $assert_session->waitForButton('Close')?->click();
     $assert_session->elementNotExists('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Octopus"]');
@@ -628,7 +633,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    */
   public function testPersistence(): void {
     $this->markTestSkipped('Skipped because the persistence layer has been removed for now and needs to be rewritten.');
-
+    // @phpstan-ignore deadCode.unreachable
     $assert_session = $this->assertSession();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('text', 'Clear Filters');
@@ -642,7 +647,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD);
 
     // Open category drop-down.
-    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->click();
+    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')?->click();
 
     // Select the E-commerce filter.
     $assert_session->waitForElementVisible('css', '#104');
@@ -730,7 +735,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    */
   public function testMultiplePlugins(): void {
     $this->markTestSkipped('This test is skipped because it needs to be rewritten now that in-app tabbing and persistence is removed.');
-
+    // @phpstan-ignore deadCode.unreachable
     $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
     // Enable module for extra source plugin.
@@ -768,7 +773,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->assertSame($second_tab_text, $assert_session->buttonExists('random_data')->getText());
 
     // Use blur event to close drop-down so Clear is visible.
-    $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur();
+    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->blur();
     $this->assertSame('2 categories selected', $page->find('css', '.pb-filter__multi-dropdown__label')->getText());
 
     // Click other tab.
@@ -781,7 +786,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->assertEquals($second_tab_text . ' (active tab)', $page->findButton('random_data')->getText());
 
     // Open category drop-down again by pressing space.
-    $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' ');
+    $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' ');
 
     // Apply the second module category filter.
     $second_category_filter_selector = '.pb-filter__multi-dropdown__items > div:nth-child(2) input';
@@ -822,7 +827,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->pressWithWait('project_browser_test_mock');
     // Filter by search text.
     $this->inputSearchField('Number', TRUE);
-    $assert_session->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertTrue($assert_session->waitForText('2 Results'));
     $this->assertProjectsVisible([
       '9 Starts With a Higher Number',
@@ -846,6 +851,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    * Tests the view mode toggle keeps its state.
    */
   public function testToggleViewState(): void {
+    $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
     $viewSwitches = [
       [
@@ -861,12 +867,12 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     foreach ($viewSwitches as $selector) {
       $this->drupalGet('admin/modules/browse/project_browser_test_mock');
       $this->svelteInitHelper('css', $selector['selector']);
-      $this->getSession()->getPage()->pressButton($selector['value']);
+      $page->pressButton($selector['value']);
       $this->svelteInitHelper('text', 'Helvetica');
       $assert_session->waitForButton('Helvetica')?->click();
       $this->svelteInitHelper('text', 'Close');
       $assert_session->waitForButton('Close')?->click();
-      $this->assertSession()->elementExists('css', $selector['selector'] . '.pb-display__button--selected');
+      $assert_session->elementExists('css', $selector['selector'] . '.pb-display__button--selected');
     }
   }
 
@@ -893,7 +899,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->drupalGet('admin/config/development/project_browser');
     $first_plugin = $page->find('css', '#source--project_browser_test_mock');
     $second_plugin = $page->find('css', '#source--random_data');
-    $first_plugin->find('css', '.handle')->dragTo($second_plugin);
+    $this->assertNotNull($second_plugin);
+    $first_plugin?->find('css', '.handle')?->dragTo($second_plugin);
+    $this->assertNotNull($first_plugin);
     $this->assertTableRowWasDragged($first_plugin);
     $this->submitForm([], 'Save');
 
@@ -905,7 +913,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->drupalGet('admin/config/development/project_browser');
     $enabled_row = $page->find('css', '#source--project_browser_test_mock');
     $disabled_region_row = $page->find('css', '.status-title-disabled');
-    $enabled_row->find('css', '.handle')->dragTo($disabled_region_row);
+    $this->assertNotNull($disabled_region_row);
+    $enabled_row?->find('css', '.handle')?->dragTo($disabled_region_row);
+    $this->assertNotNull($enabled_row);
     $this->assertTableRowWasDragged($enabled_row);
     $this->submitForm([], 'Save');
     $assert_session->pageTextContains('The configuration options have been saved.');
@@ -930,6 +940,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    * Tests the visibility of categories in list and grid view.
    */
   public function testCategoriesVisibility(): void {
+    $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
     $view_options = [
       [
@@ -946,7 +957,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     foreach ($view_options as $selector) {
       $this->drupalGet('admin/modules/browse/project_browser_test_mock');
       $this->svelteInitHelper('css', $selector['selector']);
-      $this->getSession()->getPage()->pressButton($selector['value']);
+      $page->pressButton($selector['value']);
       $this->svelteInitHelper('text', 'Helvetica');
       $assert_session->elementsCount('css', '#project-browser .pb-layout__main ul li:nth-child(7) .pb-project-categories ul li', 1);
       $grid_text = $this->getElementText('#project-browser .pb-layout__main ul li:nth-child(7) .pb-project-categories ul li:nth-child(1)');
@@ -1010,13 +1021,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->svelteInitHelper('css', '.pb-project.pb-project--list');
 
     $this->inputSearchField('inline form errors', TRUE);
-    $assert_session->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->svelteInitHelper('text', 'Inline Form Errors');
 
     $install_link = $page->find('css', '.pb-layout__main .pb-actions a');
 
-    $this->assertStringContainsString('admin/modules#module-inline-form-errors', $install_link->getAttribute('href'));
-    $this->drupalGet($install_link->getAttribute('href'));
+    $href = $install_link?->getAttribute('href');
+    $this->assertIsString($href);
+    $this->assertStringContainsString('admin/modules#module-inline-form-errors', $href);
+    $this->drupalGet($href);
     $assert_session->waitForElementVisible('css', "#edit-modules-inline-form-errors-enable");
     $assert_session->assertVisibleInViewport('css', '#edit-modules-inline-form-errors-enable');
   }
@@ -1024,10 +1037,12 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
   /**
    * Confirms UI install can not be enabled without Package Manager installed.
    */
-  public function testUiInstallNeedsPackageManager() {
+  public function testUiInstallNeedsPackageManager(): void {
+    $page = $this->getSession()->getPage();
+
     $this->drupalGet('admin/config/development/project_browser');
-    $ui_install_input = $this->getSession()->getPage()->find('css', '[data-drupal-selector="edit-allow-ui-install"]');
-    $this->assertTrue($ui_install_input->getAttribute('disabled') === 'disabled');
+    $ui_install_input = $page->find('css', '[data-drupal-selector="edit-allow-ui-install"]');
+    $this->assertTrue($ui_install_input?->getAttribute('disabled') === 'disabled');
 
     // @todo Remove try/catch in https://www.drupal.org/i/3349193.
     try {
@@ -1037,14 +1052,14 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       $this->markTestSkipped($e->getMessage());
     }
     $this->drupalGet('admin/config/development/project_browser');
-    $ui_install_input = $this->getSession()->getPage()->find('css', '[data-drupal-selector="edit-allow-ui-install"]');
-    $this->assertFalse($ui_install_input->hasAttribute('disabled'));
+    $ui_install_input = $page->find('css', '[data-drupal-selector="edit-allow-ui-install"]');
+    $this->assertFalse($ui_install_input?->hasAttribute('disabled'));
   }
 
   /**
    * Tests that we can clear search results with one click.
    */
-  public function testClearKeywordSearch() {
+  public function testClearKeywordSearch(): void {
     $assert_session = $this->assertSession();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('css', '.pb-search-results');
@@ -1056,7 +1071,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
 
     // Search for something to change it.
     $this->inputSearchField('abcdefghijklmnop', TRUE);
-    $assert_session->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
     $this->assertTrue($results->waitFor(10, fn (NodeElement $element) => $element->getText() !== $original_text));
 
     // Remove the search text and make sure it auto-updates.
@@ -1072,13 +1087,13 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    */
   public function testSearchClearNoTabIndex(): void {
     $page = $this->getSession()->getPage();
-    $this->assertSession();
+    $assert_session = $this->assertSession();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('css', '.pb-search-results');
 
     // Search and confirm clear button has no focus after tabbing.
     $this->inputSearchField('abcdefghijklmnop', TRUE);
-    $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
 
     $this->getSession()->getDriver()->keyPress($page->getXpath(), '9');
     $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
@@ -1101,7 +1116,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->drupalGet('admin/modules/browse/recipes');
     $this->svelteInitHelper('css', '.pb-projects-list');
     $this->inputSearchField('image', TRUE);
-    $assert_session->waitForElementVisible('css', ".search__search-submit")->click();
+    $assert_session->waitForElementVisible('css', ".search__search-submit")?->click();
 
     // Look for a recipe that ships with core.
     $card = $assert_session->waitForElementVisible('css', '.pb-project:contains("Image media type")');
@@ -1150,6 +1165,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    * Tests that each source plugin has its own dedicated route.
    */
   public function testSourcePluginRoutes(): void {
+    $assert_session = $this->assertSession();
+
     // Enable module for extra source plugin.
     $this->container->get('module_installer')->install(['project_browser_devel'], TRUE);
     $this->rebuildContainer();
@@ -1159,7 +1176,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
 
     foreach (array_keys($current_sources) as $plugin_id) {
       $this->drupalGet("/admin/modules/browse/{$plugin_id}");
-      $this->assertNotNull($this->assertSession()->waitForElementVisible('css', '#project-browser .pb-project.pb-project--list'));
+      $this->assertNotNull($assert_session->waitForElementVisible('css', '#project-browser .pb-project.pb-project--list'));
     }
   }
 
@@ -1168,6 +1185,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    */
   public function testWrenchIcon(): void {
     $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
     $this->getSession()->resizeWindow(1460, 960);
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('text', 'Helvetica');
@@ -1177,7 +1196,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->assertTrue($assert_session->waitForText('The module is actively maintained by the maintainers'));
     // This asserts that status icon is present in detail's modal.
     $this->assertNotNull($assert_session->waitForElementVisible('css', '.pb-detail-modal__sidebar .pb-project__status-icon-btn'));
-    $this->getSession()->getPage()->find('css', '.ui-dialog-titlebar-close')->click();
+    $page->find('css', '.ui-dialog-titlebar-close')?->click();
 
     $this->clickWithWait(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_LAST_CHILD);
     $this->assertEquals('Show all', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_LAST_CHILD));
@@ -1204,21 +1223,21 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
 
     // Locate and click the Grapefruit project link.
     $grapefruit_link = $page->find('xpath', '//button[contains(@class, "pb-project__link") and contains(text(), "Grapefruit")]');
-    $grapefruit_link->click();
+    $grapefruit_link?->click();
 
     // Verify the text for Grapefruit (singular case).
     $this->assertTrue($assert_session->waitForText('site reports using this module'));
 
     // Go back to the project list.
     $close_button = $page->find('xpath', '//button[contains(@class, "ui-dialog-titlebar-close") and contains(text(), "Close")]');
-    $close_button->click();
+    $close_button?->click();
 
     // Expect Octopus to have 235 installs.
     $assert_session->elementExists('xpath', '//span[contains(@class, "pb-project__install-count") and text()="235 installs"]');
 
     // Locate and click the Octopus project link.
     $octopus_link = $page->find('xpath', '//button[contains(@class, "pb-project__link") and contains(text(), "Octopus")]');
-    $octopus_link->click();
+    $octopus_link?->click();
 
     // Verify the text for Octopus (plural case).
     $this->assertTrue($assert_session->waitForText('sites report using this module'));
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
index 7e829566a26f90240df8fd7f58b6e41e92bceb4a..7154f5b6cdd85d31bbe8ea662aa502eedc100f67 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
@@ -418,7 +418,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
     $this->drupalGet('admin/config/development/project_browser');
     $first_plugin = $page->find('css', '#source--drupalorg_jsonapi');
     $second_plugin = $page->find('css', '#source--random_data');
-    $first_plugin->find('css', '.tabledrag-handle')->dragTo($second_plugin);
+    $this->assertNotNull($second_plugin);
+    $first_plugin?->find('css', '.tabledrag-handle')?->dragTo($second_plugin);
+    $this->assertNotNull($first_plugin);
     $this->assertTableRowWasDragged($first_plugin);
     $this->submitForm([], 'Save');
 
@@ -432,7 +434,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
     $this->drupalGet('admin/config/development/project_browser');
     $enabled_row = $page->find('css', '#source--drupalorg_jsonapi');
     $disabled_region_row = $page->find('css', '.status-title-disabled');
-    $enabled_row->find('css', '.handle')->dragTo($disabled_region_row);
+    $this->assertNotNull($disabled_region_row);
+    $enabled_row?->find('css', '.handle')?->dragTo($disabled_region_row);
+    $this->assertNotNull($enabled_row);
     $this->assertTableRowWasDragged($enabled_row);
     $this->submitForm([], 'Save');
     $assert_session->pageTextContains('The configuration options have been saved.');
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
index 12440efeba96f33eb136f9b17220fbf00c63c0e9..b17b3420b452b1736449d5584b4df2a884d3afe0 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
@@ -55,7 +55,7 @@ trait ProjectBrowserUiTestTrait {
       try {
         $this->assertTrue($this->getSession()->evaluateScript($script), 'Ran:' . $script . 'Svelte did not initialize. Markup: ' . $this->getSession()->evaluateScript('document.querySelector("#project-browser").innerHTML'));
       }
-      catch (\Exception $e) {
+      catch (\Exception) {
         $this->getSession()->reload();
         $this->getSession()->wait(10000, $script);
       }
@@ -72,7 +72,7 @@ trait ProjectBrowserUiTestTrait {
    * @param bool $bypass_wait
    *   When TRUE, do not wait for a rerender after entering a search string.
    */
-  protected function inputSearchField(string $value, bool $bypass_wait = FALSE) {
+  protected function inputSearchField(string $value, bool $bypass_wait = FALSE): void {
     $search_field = $this->assertSession()->waitForElementVisible('css', '#pb-text');
     if ($bypass_wait) {
       $search_field->setValue($value);
@@ -94,7 +94,7 @@ trait ProjectBrowserUiTestTrait {
    * @param bool $bypass_wait
    *   When TRUE, do not wait for a rerender after entering a search string.
    */
-  protected function pressWithWait(string $locator, string $wait_for_text = '', bool $bypass_wait = FALSE) {
+  protected function pressWithWait(string $locator, string $wait_for_text = '', bool $bypass_wait = FALSE): void {
     if ($bypass_wait) {
       $this->getSession()->getPage()->pressButton($locator);
     }
@@ -119,7 +119,7 @@ trait ProjectBrowserUiTestTrait {
    * @param bool $bypass_wait
    *   When TRUE, do not wait for a rerender after entering a search string.
    */
-  protected function clickWithWait(string|NodeElement $element, string $wait_for_text = '', bool $bypass_wait = FALSE) {
+  protected function clickWithWait(string|NodeElement $element, string $wait_for_text = '', bool $bypass_wait = FALSE): void {
     if (is_string($element)) {
       $element = $this->assertSession()->elementExists('css', $element);
     }
@@ -151,9 +151,9 @@ trait ProjectBrowserUiTestTrait {
   /**
    * Opens the advanced filter element.
    */
-  protected function openAdvancedFilter() {
+  protected function openAdvancedFilter(): void {
     $filter_icon_selector = $this->getSession()->getPage()->find('css', '.search__filter__toggle');
-    $filter_icon_selector->click();
+    $filter_icon_selector?->click();
     $this->assertSession()->waitForElementVisible('css', '.search__filter__toggle[aria-expanded="true"]');
   }
 
@@ -165,7 +165,7 @@ trait ProjectBrowserUiTestTrait {
    * @param bool $bypass_wait
    *   When TRUE, do not wait for a rerender after entering a search string.
    */
-  protected function sortBy(string $value, bool $bypass_wait = FALSE) {
+  protected function sortBy(string $value, bool $bypass_wait = FALSE): void {
     if ($bypass_wait) {
       $this->getSession()->getPage()->selectFieldOption('pb-sort', $value);
     }
@@ -179,14 +179,14 @@ trait ProjectBrowserUiTestTrait {
   /**
    * Add an attribute to a project card that will vanish after filtering.
    */
-  protected function preFilterWait() {
+  protected function preFilterWait(): void {
     $this->getSession()->executeScript("document.querySelectorAll('.pb-project').forEach((project) => project.setAttribute('data-pre-filter', 'true'))");
   }
 
   /**
    * Confirm the attribute added in preFilterWait() is no longer present.
    */
-  protected function postFilterWait() {
+  protected function postFilterWait(): void {
     $this->assertSession()->assertNoElementAfterWait('css', '[data-pre-filter]');
   }
 
@@ -204,7 +204,7 @@ trait ProjectBrowserUiTestTrait {
    * @param int $timeout
    *   Timeout in milliseconds, defaults to 10000.
    */
-  protected function svelteInitHelper(string $check_type, string $check_value, int $timeout = 10000) {
+  protected function svelteInitHelper(string $check_type, string $check_value, int $timeout = 10000): void {
     if ($check_type === 'css') {
       if (!$this->assertSession()->waitForElement('css', $check_value, $timeout)) {
         $this->getSession()->reload();
@@ -232,7 +232,7 @@ trait ProjectBrowserUiTestTrait {
    * @return string
    *   The trimmed text content of the element.
    */
-  protected function getElementText($selector) {
+  protected function getElementText(string $selector): string {
     return trim($this->getSession()->evaluateScript("document.querySelector('$selector').textContent"));
   }
 
@@ -274,7 +274,7 @@ trait ProjectBrowserUiTestTrait {
    * @param \Behat\Mink\Element\NodeElement|null $container
    *   The container to look within.
    */
-  protected function waitForField(string $locator, ?NodeElement $container = NULL): NodeElement {
+  protected function waitForField(string $locator, ?NodeElement $container = NULL): ?NodeElement {
     $container ??= $this->getSession()->getPage();
     $this->assertTrue(
       $container->waitFor(10, fn ($container) => $container->findField($locator)?->isVisible()),
diff --git a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php
index 73f292b4c0bc89b603f5e904d471cf9405795311..86d2864f1ff282f5035a28c8192a3daa09a6d186 100644
--- a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php
+++ b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php
@@ -38,7 +38,7 @@ class TranslatedSvelteAppTest extends WebDriverTestBase {
    * 90% of this is code borrowed from
    * \Drupal\Tests\locale\Functional\LocaleContentTest.
    */
-  public function testTranslation() {
+  public function testTranslation(): void {
     $admin_user = $this->drupalCreateUser([
       'administer languages',
       'access administration pages',
diff --git a/tests/src/Kernel/CoreExperimentalLabelTest.php b/tests/src/Kernel/CoreExperimentalLabelTest.php
index 61976f33191e19599dd1e63970ceb99eda4ccb7b..17c787316367deb9f0ca5c49fa40c9a0edf40eba 100644
--- a/tests/src/Kernel/CoreExperimentalLabelTest.php
+++ b/tests/src/Kernel/CoreExperimentalLabelTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
@@ -32,10 +34,11 @@ class CoreExperimentalLabelTest extends KernelTestBase {
    * @covers ::getProjectData
    */
   public function testCoreExperimentalLabel(): void {
+    /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $plugin_instance */
     $plugin_instance = $this->container->get(ProjectBrowserSourceManager::class)
       ->createInstance('drupal_core');
     $modules_to_test = ['Experimental Test', 'System'];
-    $filtered_projects = array_filter($plugin_instance->getProjects()->list, fn(Project $value) => in_array($value->title, $modules_to_test));
+    $filtered_projects = array_filter($plugin_instance->getProjects()->list, fn(Project $value): bool => in_array($value->title, $modules_to_test));
     $this->assertCount(2, $filtered_projects);
     foreach ($filtered_projects as $project) {
       if ($project->title === 'System') {
diff --git a/tests/src/Kernel/CoreNotUpdatedValidatorTest.php b/tests/src/Kernel/CoreNotUpdatedValidatorTest.php
index d2ef82e5c7fb66de30be69d739a430f5b7522151..87dc30f6724effbbcd3792649cd5a62e569184ac 100644
--- a/tests/src/Kernel/CoreNotUpdatedValidatorTest.php
+++ b/tests/src/Kernel/CoreNotUpdatedValidatorTest.php
@@ -1,7 +1,10 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Kernel;
 
+use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\package_manager\Exception\StageEventException;
@@ -70,8 +73,9 @@ class CoreNotUpdatedValidatorTest extends PackageManagerKernelTestBase {
    */
   public function testPreApplyException(bool $core_updated, array $expected_results): void {
     if ($core_updated) {
-      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
+      $this->getStageFixtureManipulator()?->setCorePackageVersion('9.8.1');
     }
+    /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
     $installer = $this->container->get(Installer::class);
     $installer->create();
     $installer->require(['org/package-name']);
@@ -81,6 +85,7 @@ class CoreNotUpdatedValidatorTest extends PackageManagerKernelTestBase {
       $this->assertEmpty($expected_results);
     }
     catch (StageEventException $e) {
+      assert($e->event instanceof PreOperationStageEvent);
       $this->assertValidationResultsEqual($expected_results, $e->event->getResults());
     }
   }
diff --git a/tests/src/Kernel/DatabaseTablesTest.php b/tests/src/Kernel/DatabaseTablesTest.php
index 6f6ef2a3d380ba958f74cc6e6c7629995ad88fd0..f77d32ec9ad48a97f8aaa17d001ed6b97489597c 100644
--- a/tests/src/Kernel/DatabaseTablesTest.php
+++ b/tests/src/Kernel/DatabaseTablesTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
@@ -36,7 +38,7 @@ class DatabaseTablesTest extends KernelTestBase {
     project_browser_test_install();
     $this->container = \Drupal::getContainer();
 
-    /** @var \Drupal\Core\Database\Schema $database */
+    /** @var \Drupal\Core\Database\Schema $schema */
     $schema = $this->container->get('database')->schema();
     $this->assertTrue($schema->tableExists('project_browser_projects'));
     $this->assertTrue($schema->tableExists('project_browser_categories'));
@@ -44,9 +46,11 @@ class DatabaseTablesTest extends KernelTestBase {
     // Make sure the fixture files do have data in them.
     /** @var \Drupal\Core\Database\Connection $database */
     $database = $this->container->get('database');
-    $rows = $database->select('project_browser_projects')->countQuery()->execute()->fetchCol();
+    $rows = $database->select('project_browser_projects')->countQuery()->execute()?->fetchCol();
+    $this->assertIsArray($rows);
     $this->assertGreaterThan(1, $rows[0]);
-    $rows = $database->select('project_browser_categories')->countQuery()->execute()->fetchCol();
+    $rows = $database->select('project_browser_categories')->countQuery()->execute()?->fetchCol();
+    $this->assertIsArray($rows);
     $this->assertGreaterThan(1, $rows[0]);
 
     $module_installer->uninstall(['project_browser_test']);
diff --git a/tests/src/Kernel/InstallerTest.php b/tests/src/Kernel/InstallerTest.php
index 9a25957209dcfd00798f1e5109526f06475d0f2e..04cc7ab648637dda92d847f309c95f6cd5291c10 100644
--- a/tests/src/Kernel/InstallerTest.php
+++ b/tests/src/Kernel/InstallerTest.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Kernel;
 
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
@@ -54,7 +56,7 @@ class InstallerTest extends PackageManagerKernelTestBase {
   /**
    * Data provider for testCommitException().
    *
-   * @return \string[][]
+   * @return string[][]
    *   The test cases.
    */
   public static function providerCommitException(): array {
@@ -77,14 +79,15 @@ class InstallerTest extends PackageManagerKernelTestBase {
   /**
    * Tests exception handling during calls to Composer Stager commit.
    *
-   * @param string $thrown_class
+   * @param class-string<\Throwable> $thrown_class
    *   The throwable class that should be thrown by Composer Stager.
-   * @param string|null $expected_class
+   * @param class-string<\Throwable> $expected_class
    *   The expected exception class.
    *
    * @dataProvider providerCommitException
    */
-  public function testCommitException(string $thrown_class, ?string $expected_class = NULL): void {
+  public function testCommitException(string $thrown_class, string $expected_class): void {
+    /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
     $installer = $this->container->get(Installer::class);
     $installer->create();
     $installer->require(['org/package-name']);
@@ -111,7 +114,8 @@ class InstallerTest extends PackageManagerKernelTestBase {
    *
    * @covers ::dispatch
    */
-  public function testInstallException() {
+  public function testInstallException(): void {
+    /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
     $installer = $this->container->get(Installer::class);
     $installer->create();
     $installer->require(['org/package-name']);
diff --git a/tests/src/Kernel/PackageNotInstalledValidatorTest.php b/tests/src/Kernel/PackageNotInstalledValidatorTest.php
index 61000e4ebcb2914c0b5dcac6a72f6f8fd6c09746..eff4ae007d5b47a577f43606c0c76cfce739092e 100644
--- a/tests/src/Kernel/PackageNotInstalledValidatorTest.php
+++ b/tests/src/Kernel/PackageNotInstalledValidatorTest.php
@@ -1,7 +1,10 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Kernel;
 
+use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\package_manager\Exception\StageEventException;
@@ -85,6 +88,7 @@ class PackageNotInstalledValidatorTest extends PackageManagerKernelTestBase {
       // entry for it, so we can 'composer require' it later.
       ->removePackage('drupal/new_module')
       ->commitChanges();
+    /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
     $installer = $this->container->get(Installer::class);
     try {
       $installer->create();
@@ -94,6 +98,7 @@ class PackageNotInstalledValidatorTest extends PackageManagerKernelTestBase {
     }
     catch (StageEventException $e) {
       $this->assertNotNull($expected_result);
+      assert($e->event instanceof PreOperationStageEvent);
       $this->assertValidationResultsEqual([$expected_result], $e->event->getResults());
     }
   }
diff --git a/tests/src/Kernel/RecipeActivatorTest.php b/tests/src/Kernel/RecipeActivatorTest.php
index c832f0d1cc778c1f196a449f577adc21eb45ba10..c25952a2e60d6ca52126c2742718d062d04d5e12 100644
--- a/tests/src/Kernel/RecipeActivatorTest.php
+++ b/tests/src/Kernel/RecipeActivatorTest.php
@@ -56,6 +56,7 @@ class RecipeActivatorTest extends KernelTestBase {
       packageName: 'My Project',
       type: ProjectType::Recipe,
     );
+    /** @var \Drupal\project_browser\ActivatorInterface $activator */
     $activator = $this->container->get(ActivatorInterface::class);
     // As this project is not installed the RecipeActivator::getPath() will
     // return NULL in RecipeActivator::getStatus() and it will return the
diff --git a/tests/src/Kernel/RecipesSourceTest.php b/tests/src/Kernel/RecipesSourceTest.php
index 1ee60153ab91f973f90cd357c251fb9b99742d50..32f993ffd5eec73ef3462fcfbddf7cbeb9101a92 100644
--- a/tests/src/Kernel/RecipesSourceTest.php
+++ b/tests/src/Kernel/RecipesSourceTest.php
@@ -68,8 +68,7 @@ class RecipesSourceTest extends KernelTestBase {
     $this->setSetting('extension_discovery_scan_tests', TRUE);
 
     /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $source */
-    $source = $this->container->get(ProjectBrowserSourceManager::class)
-      ->createInstance('recipes');
+    $source = $this->container->get(ProjectBrowserSourceManager::class)->createInstance('recipes');
 
     // Generate a fake recipe in the temporary directory.
     $generated_recipe_name = uniqid();
@@ -100,10 +99,7 @@ class RecipesSourceTest extends KernelTestBase {
       $expected_recipe_names[] = $core_recipe->getBasename();
     }
 
-    /** @var \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $projects */
-    $projects = $this->container->get(ProjectBrowserSourceManager::class)
-      ->createInstance('recipes')
-      ->getProjects();
+    $projects = $source->getProjects();
     $found_recipes = [];
     foreach ($projects->list as $project) {
       $this->assertNotEmpty($project->title);
@@ -120,12 +116,13 @@ class RecipesSourceTest extends KernelTestBase {
     $this->assertSame($expected_recipe_names, $found_recipe_names);
 
     // Ensure the package names are properly resolved.
-    $this->assertSame('drupal/core', $found_recipes['standard']?->packageName);
-    $this->assertSame('project-browser-test/test-recipe', $found_recipes['test_recipe']?->packageName);
+    $this->assertArrayHasKey('standard', $found_recipes);
+    $this->assertSame('drupal/core', $found_recipes['standard']->packageName);
+    $this->assertArrayHasKey('test_recipe', $found_recipes);
+    $this->assertSame('project-browser-test/test-recipe', $found_recipes['test_recipe']->packageName);
 
     // The core recipes should have descriptions, which should become the body
     // text of the project.
-    $this->assertArrayHasKey('standard', $found_recipes);
     // The need for reflection sucks, but there's no way to introspect the body
     // on the backend.
     $body = (new \ReflectionProperty($found_recipes['standard'], 'body'))
@@ -151,10 +148,9 @@ class RecipesSourceTest extends KernelTestBase {
       ])
       ->save();
 
-    /** @var \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $projects */
-    $projects = $this->container->get(ProjectBrowserSourceManager::class)
-      ->createInstance('recipes')
-      ->getProjects();
+    /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $source */
+    $source = $this->container->get(ProjectBrowserSourceManager::class)->createInstance('recipes');
+    $projects = $source->getProjects();
     $found_recipe_names = array_column($projects->list, 'machineName');
 
     // The `example` recipe (from core) should always be hidden, even if it's in
diff --git a/tests/src/Traits/PackageManagerFixtureUtilityTrait.php b/tests/src/Traits/PackageManagerFixtureUtilityTrait.php
index 7c10dfeef226e71aef553853a548da258e9b8d7f..bfef2179945266671d6cfc7d83b910cd0b6147b2 100644
--- a/tests/src/Traits/PackageManagerFixtureUtilityTrait.php
+++ b/tests/src/Traits/PackageManagerFixtureUtilityTrait.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Drupal\Tests\project_browser\Traits;
 
 use Drupal\package_manager\PathLocator;
+use Drupal\package_manager_bypass\MockPathLocator;
 use Symfony\Component\Filesystem\Filesystem;
 
 /**
@@ -37,8 +38,9 @@ trait PackageManagerFixtureUtilityTrait {
     // unique for each test run. This will enable changing files in the
     // directory and not affect other tests.
     $active_dir = $this->copyFixtureToTempDirectory($fixture_directory);
-    $this->container->get(PathLocator::class)
-      ->setPaths($active_dir, $active_dir . '/vendor', '', NULL);
+    $path_locator = $this->container->get(PathLocator::class);
+    assert($path_locator instanceof MockPathLocator);
+    $path_locator->setPaths($active_dir, $active_dir . '/vendor', '', NULL);
   }
 
   /**
diff --git a/tests/src/Unit/ProjectBrowserTestMockTest.php b/tests/src/Unit/ProjectBrowserTestMockTest.php
index a8076ab1c115f8ab2f8ff09b72508fb4b86cd288..e286066d5d8be1979a445d997bbacfbb343137a8 100644
--- a/tests/src/Unit/ProjectBrowserTestMockTest.php
+++ b/tests/src/Unit/ProjectBrowserTestMockTest.php
@@ -1,8 +1,9 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\Tests\project_browser\Unit;
 
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Tests\UnitTestCase;
@@ -19,7 +20,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase {
   /**
    * The plugin.
    *
-   * @var \Drupal\project_browser\Plugin\ProjectBrowserSource\ProjectBrowserTestMock
+   * @var \Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock
    */
   protected $plugin;
 
@@ -51,13 +52,6 @@ class ProjectBrowserTestMockTest extends UnitTestCase {
    */
   protected ModuleHandlerInterface $moduleHandler;
 
-  /**
-   * ProjectBrowser cache bin.
-   *
-   * @var \Drupal\Core\Cache\CacheBackendInterface
-   */
-  protected $cacheBin;
-
   /**
    * {@inheritdoc}
    */
@@ -66,14 +60,13 @@ class ProjectBrowserTestMockTest extends UnitTestCase {
 
     $this->logger = $this->createMock(LoggerInterface::class);
     $this->database = $this->createMock(Connection::class);
-    $this->cacheBin = $this->createMock(CacheBackendInterface::class);
     $this->state = $this->createMock('\Drupal\Core\State\StateInterface');
     $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
 
     $configuration = [];
     $plugin_id = $this->randomMachineName();
     $plugin_definition = [];
-    $this->plugin = new ProjectBrowserTestMock($configuration, $plugin_id, $plugin_definition, $this->logger, $this->database, $this->state, $this->cacheBin, $this->moduleHandler);
+    $this->plugin = new ProjectBrowserTestMock($configuration, $plugin_id, $plugin_definition, $this->logger, $this->database, $this->state, $this->moduleHandler);
   }
 
   /**
@@ -85,7 +78,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase {
    * @return \ReflectionMethod
    *   The accessible method.
    */
-  protected static function getMethod($name) {
+  protected static function getMethod($name): \ReflectionMethod {
     $class = new \ReflectionClass(ProjectBrowserTestMock::class);
     $method = $class->getMethod($name);
     $method->setAccessible(TRUE);
@@ -95,7 +88,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase {
   /**
    * Tests relative to absolute URL conversion.
    */
-  public function testRelativeToAbsoluteUrl() {
+  public function testRelativeToAbsoluteUrl(): void {
     // Project body with relative URLs.
     $project_data['body'] = ['value' => '<img src="/files/issues/123" alt="Image1" /><img src="/files/issues/321" alt="Image2" />'];
     // Expected Absolute URLs.