diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b9c9392fa689789304797bead085a1ad02fc2570..d56b4bc1d9bb08f650c32d05a24f4408a3caefd8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,8 +31,8 @@ jobs:
       - name: Run PHP-CS-Fixer
         run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose
 
-  type-checker:
-    name: Type Checker
+  static-analysis:
+    name: Static Analysis
 
     runs-on: ubuntu-latest
 
@@ -49,8 +49,8 @@ jobs:
       - name: Install dependencies with Composer
         run: ./tools/composer update --no-interaction --no-ansi --no-progress
 
-      - name: Run Psalm
-        run: ./tools/psalm --config=.psalm/config.xml --no-progress --shepherd --show-info=false --stats
+      - name: Run PHPStan
+        run: ./tools/phpstan analyse --no-progress --error-format=github
 
   tests:
     name: Tests
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..10c6ce0610676c13babf385135c5e3ebec4282ee
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,43 @@
+# https://docs.github.com/en/actions
+
+on:
+  push:
+    tags:
+      - "**"
+
+name: Release
+
+jobs:
+  release:
+    name: Release
+
+    runs-on: ubuntu-latest
+
+    permissions:
+      contents: write
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Install PHP with extensions
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: 8.3
+          coverage: none
+          extensions: none
+          tools: none
+
+      - name: Determine tag
+        run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+
+      - name: Parse ChangeLog
+        run: build/scripts/extract-release-notes.php ${{ env.RELEASE_TAG }} > release-notes.md
+
+      - name: Create release
+        uses: ncipollo/release-action@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          tag: ${{ env.RELEASE_TAG }}
+          name: phpunit/php-file-iterator ${{ env.RELEASE_TAG }}
+          bodyFile: release-notes.md
diff --git a/.phive/phars.xml b/.phive/phars.xml
index e9c9ac614ce23a493db0e191ef7f71bdc5974af7..4817da71b394359dd9a4e623de1ab849dab53d4c 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phive xmlns="https://phar.io/phive">
-  <phar name="php-cs-fixer" version="^3.0" installed="3.41.1" location="./tools/php-cs-fixer" copy="true"/>
-  <phar name="psalm" version="^5.0" installed="5.18.0" location="./tools/psalm" copy="true"/>
-  <phar name="composer" version="^2.0.3" installed="2.6.6" location="./tools/composer" copy="true"/>
+  <phar name="php-cs-fixer" version="^3.59" installed="3.59.3" location="./tools/php-cs-fixer" copy="true"/>
+  <phar name="composer" version="^2.7" installed="2.7.7" location="./tools/composer" copy="true"/>
+  <phar name="phpstan" version="^1.11" installed="1.11.5" location="./tools/phpstan" copy="true"/>
 </phive>
diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml
deleted file mode 100644
index 09f04decaf4b9d0bd54237d72aa92d06d63ce3db..0000000000000000000000000000000000000000
--- a/.psalm/baseline.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="5.6.0@e784128902dfe01d489c4123d69918a9f3c1eac5">
-  <file src="src/ExcludeIterator.php">
-    <MissingTemplateParam>
-      <code>ExcludeIterator</code>
-    </MissingTemplateParam>
-  </file>
-</files>
diff --git a/.psalm/config.xml b/.psalm/config.xml
deleted file mode 100644
index 343cc8e3ed28b3bb8dcaaec70244b56f6daf71a5..0000000000000000000000000000000000000000
--- a/.psalm/config.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0"?>
-<psalm
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
-    resolveFromConfigFile="false"
-    errorBaseline=".psalm/baseline.xml"
-    findUnusedBaselineEntry="true"
-    findUnusedCode="false"
->
-    <projectFiles>
-        <directory name="src" />
-        <ignoreFiles>
-            <directory name="vendor" />
-        </ignoreFiles>
-    </projectFiles>
-</psalm>
diff --git a/ChangeLog.md b/ChangeLog.md
index 959ce0ba7228ea4627bc2b340e8ae8edcdab707a..970f762489ce21a793cc43d106a58cda79c077b4 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,12 @@
 
 All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [5.0.1] - 2024-07-03
+
+### Changed
+
+* This project now uses PHPStan instead of Psalm for static analysis
+
 ## [5.0.0] - 2024-02-02
 
 ### Removed
@@ -156,6 +162,7 @@ No changes
 
 * [#23](https://github.com/sebastianbergmann/php-file-iterator/pull/23): Added support for wildcards (glob) in exclude
 
+[5.0.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/5.0.0...5.0.1
 [5.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.1...5.0.0
 [4.1.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.0.2...4.1.0
 [4.0.2]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.0.1...4.0.2
diff --git a/build/scripts/extract-release-notes.php b/build/scripts/extract-release-notes.php
new file mode 100755
index 0000000000000000000000000000000000000000..553189267ddf74728028ea8f5992eb35ebf2d114
--- /dev/null
+++ b/build/scripts/extract-release-notes.php
@@ -0,0 +1,46 @@
+#!/usr/bin/env php
+<?php declare(strict_types=1);
+if ($argc !== 2) {
+    print $argv[0] . ' <tag>' . PHP_EOL;
+
+    exit(1);
+}
+
+$version = $argv[1];
+
+$file = __DIR__ . '/../../ChangeLog.md';
+
+if (!is_file($file) || !is_readable($file)) {
+    print $file . ' cannot be read' . PHP_EOL;
+
+    exit(1);
+}
+
+$buffer = '';
+$append = false;
+
+foreach (file($file) as $line) {
+    if (str_starts_with($line, '## [' . $version . ']')) {
+        $append = true;
+
+        continue;
+    }
+
+    if ($append && (str_starts_with($line, '## ') || str_starts_with($line, '['))) {
+        break;
+    }
+
+    if ($append) {
+        $buffer .= $line;
+    }
+}
+
+$buffer = trim($buffer);
+
+if ($buffer === '') {
+    print 'Unable to extract release notes' . PHP_EOL;
+
+    exit(1);
+}
+
+print $buffer . PHP_EOL;
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000000000000000000000000000000000000..08566d07a495531f34abd8522c67f0c39eb68d96
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,5 @@
+parameters:
+    level: 9
+    paths:
+        - src
+        - tests
diff --git a/phpunit.xml b/phpunit.xml
index 9f8393da394b824f0fd92b7e216d45ab31695c16..931d837d11cacac17f662ce691c6c79f600aabc6 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -16,9 +16,7 @@
         </testsuite>
     </testsuites>
 
-    <source restrictDeprecations="true"
-            restrictNotices="true"
-            restrictWarnings="true">
+    <source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
         <include>
             <directory>src</directory>
         </include>
diff --git a/src/ExcludeIterator.php b/src/ExcludeIterator.php
index 23c9335e2893dd9c2383e85a2c038a76b72beec2..209ec00f59ca505972acddbf37d462c1d77c853f 100644
--- a/src/ExcludeIterator.php
+++ b/src/ExcludeIterator.php
@@ -21,12 +21,12 @@ use SplFileInfo;
 final class ExcludeIterator extends RecursiveFilterIterator
 {
     /**
-     * @psalm-var list<string>
+     * @var list<string>
      */
     private array $exclude;
 
     /**
-     * @psalm-param list<string> $exclude
+     * @param list<string> $exclude
      */
     public function __construct(RecursiveDirectoryIterator $iterator, array $exclude)
     {
diff --git a/src/Facade.php b/src/Facade.php
index b782bf96a3daa24d3748e98630719b08ccef268e..a0410e842ef6f3adc23c0572462c464514e395bb 100644
--- a/src/Facade.php
+++ b/src/Facade.php
@@ -20,12 +20,12 @@ use SplFileInfo;
 final class Facade
 {
     /**
-     * @psalm-param list<non-empty-string>|non-empty-string $paths
-     * @psalm-param list<non-empty-string>|string $suffixes
-     * @psalm-param list<non-empty-string>|string $prefixes
-     * @psalm-param list<non-empty-string> $exclude
+     * @param list<non-empty-string>|non-empty-string $paths
+     * @param list<non-empty-string>|string           $suffixes
+     * @param list<non-empty-string>|string           $prefixes
+     * @param list<non-empty-string>                  $exclude
      *
-     * @psalm-return list<non-empty-string>
+     * @return list<non-empty-string>
      */
     public function getFilesAsArray(array|string $paths, array|string $suffixes = '', array|string $prefixes = '', array $exclude = []): array
     {
diff --git a/src/Factory.php b/src/Factory.php
index 7a0f0b1263649879745bd3c80e67a233fd100c4f..a2ebce6c184f7f4c2aa9e5e3c614f95ab18f68f3 100644
--- a/src/Factory.php
+++ b/src/Factory.php
@@ -29,10 +29,10 @@ use RecursiveIteratorIterator;
 final class Factory
 {
     /**
-     * @psalm-param list<non-empty-string>|non-empty-string $paths
-     * @psalm-param list<non-empty-string>|string $suffixes
-     * @psalm-param list<non-empty-string>|string $prefixes
-     * @psalm-param list<non-empty-string> $exclude
+     * @param list<non-empty-string>|non-empty-string $paths
+     * @param list<non-empty-string>|string           $suffixes
+     * @param list<non-empty-string>|string           $prefixes
+     * @param list<non-empty-string>                  $exclude
      */
     public function getFileIterator(array|string $paths, array|string $suffixes = '', array|string $prefixes = '', array $exclude = []): AppendIterator
     {
@@ -83,9 +83,9 @@ final class Factory
     }
 
     /**
-     * @psalm-param list<non-empty-string> $paths
+     * @param list<non-empty-string> $paths
      *
-     * @psalm-return list<non-empty-string>
+     * @return list<non-empty-string>
      */
     private function resolveWildcards(array $paths): array
     {
diff --git a/src/Iterator.php b/src/Iterator.php
index 52dd22d7794da69dac2f4efa627eec252ee3b4ec..405f25de2005fcaaab1dca79f55ee19a6ddd3c4c 100644
--- a/src/Iterator.php
+++ b/src/Iterator.php
@@ -15,12 +15,11 @@ use function realpath;
 use function str_ends_with;
 use function str_replace;
 use function str_starts_with;
-use AppendIterator;
 use FilterIterator;
 use SplFileInfo;
 
 /**
- * @template-extends FilterIterator<int, string, AppendIterator>
+ * @template-extends FilterIterator<int, SplFileInfo, \Iterator>
  *
  * @internal This class is not covered by the backward compatibility promise for phpunit/php-file-iterator
  */
@@ -31,18 +30,18 @@ final class Iterator extends FilterIterator
     private false|string $basePath;
 
     /**
-     * @psalm-var list<string>
+     * @var list<string>
      */
     private array $suffixes;
 
     /**
-     * @psalm-var list<string>
+     * @var list<string>
      */
     private array $prefixes;
 
     /**
-     * @psalm-param list<string> $suffixes
-     * @psalm-param list<string> $prefixes
+     * @param list<string> $suffixes
+     * @param list<string> $prefixes
      */
     public function __construct(string $basePath, \Iterator $iterator, array $suffixes = [], array $prefixes = [])
     {
@@ -94,7 +93,7 @@ final class Iterator extends FilterIterator
     }
 
     /**
-     * @psalm-param list<string> $subStrings
+     * @param list<string> $subStrings
      */
     private function acceptSubString(string $filename, array $subStrings, int $type): bool
     {
diff --git a/tests/unit/FacadeTest.php b/tests/unit/FacadeTest.php
index 8d85e5ea71764599c4ab10fb6175e6c2f224c71d..3200790702ef77a884fcde09daa994fd456b51ea 100644
--- a/tests/unit/FacadeTest.php
+++ b/tests/unit/FacadeTest.php
@@ -24,6 +24,9 @@ use PHPUnit\Framework\TestCase;
 #[Small]
 final class FacadeTest extends TestCase
 {
+    /**
+     * @return non-empty-array<non-empty-string, array{0: list<non-empty-string>, 1: list<non-empty-string>|non-empty-string, 2: list<non-empty-string>|string, 3: list<non-empty-string>|string, 4: list<non-empty-string>}>
+     */
     public static function provider(): array
     {
         $fixtureDirectoryRealpath = self::fixtureDirectoryRealpath();
@@ -136,6 +139,13 @@ final class FacadeTest extends TestCase
         unlink(self::fixtureDirectoryRealpath() . '/a/DoesNotExist.php');
     }
 
+    /**
+     * @param list<non-empty-string>                  $expected
+     * @param list<non-empty-string>|non-empty-string $paths
+     * @param list<non-empty-string>|string           $suffixes
+     * @param list<non-empty-string>|string           $prefixes
+     * @param list<non-empty-string>                  $exclude
+     */
     #[DataProvider('provider')]
     public function testSomething(array $expected, array|string $paths, array|string $suffixes, array|string $prefixes, array $exclude): void
     {