diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..8b51ad81f07033854f0d5b9fb36b8d8da032cf94
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,75 @@
+# Contributing to `sebastian/recursion-context`
+
+## Welcome!
+
+We look forward to your contributions! Here are some examples how you can contribute:
+
+* [Report a bug](https://github.com/sebastianbergmann/recursion-context/issues/new)
+* [Send a pull request to fix a bug](https://github.com/sebastianbergmann/recursion-context/pulls)
+
+Please do not send pull requests that expand the scope of this project (see below).
+
+
+## Any contributions you make will be under the BSD-3-Clause License
+
+When you submit code changes, your submissions are understood to be under the same [BSD-3-Clause License](https://github.com/sebastianbergmann/recursion-context/blob/main/LICENSE) that covers the project. By contributing to this project, you agree that your contributions will be licensed under its BSD-3-Clause License.
+
+
+## Write bug reports with detail, background, and sample code
+
+[This is an example](https://github.com/sebastianbergmann/phpunit/issues/4376) of a bug report I wrote, and I think it's not too bad.
+
+In your bug report, please provide the following:
+
+* A quick summary and/or background
+* Steps to reproduce
+    * Be specific!
+    * Give sample code if you can.
+* What you expected would happen
+* What actually happens
+* Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
+
+Please post code and output as text ([using proper markup](https://guides.github.com/features/mastering-markdown/)). Do not post screenshots of code or output.
+
+
+## Workflow for Pull Requests
+
+1. Fork the repository.
+2. Create your branch from the oldest branch that is affected by the bug you plan to fix.
+3. Implement your change and add tests for it.
+4. Ensure the test suite passes.
+5. Ensure the code complies with our coding guidelines (see below).
+6. Send that pull request!
+
+Please make sure you have [set up your username and email address](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup) for use with Git. Strings such as `silly nick name <root@localhost>` look really stupid in the commit history of a project.
+
+We encourage you to [sign your Git commits with your GPG key](https://docs.github.com/en/github/authenticating-to-github/signing-commits).
+
+
+## Development
+
+This project uses [PHPUnit](https://phpunit.de/) for testing:
+
+```shell
+./vendor/bin/phpunit
+```
+
+This project uses [PHPStan](https://phpstan.org/) for static analysis:
+
+```shell
+./tools/phpstan
+```
+
+This project uses [PHP-CS-Fixer](https://cs.symfony.com/) to enforce coding guidelines:
+
+```shell
+./tools/php-cs-fixer fix
+```
+
+The commands shown above require an autoloader script at `vendor/autoload.php`. This can be generated like so:
+
+```shell
+./tools/composer dump-autoload
+```
+
+Please understand that we will not accept a pull request when its changes violate this project's coding guidelines or break the test suite.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6a608e898134fcf3c75c08a61404edd959ef8a3b..4eb5a7dcd80fa09a22d4b541d854d2b99e58eeb1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ on:
 name: "CI"
 
 env:
-  COMPOSER_ROOT_VERSION: "6.0-dev"
+  COMPOSER_ROOT_VERSION: "7.0.x-dev"
 
 permissions:
   contents: read
@@ -61,9 +61,9 @@ jobs:
       fail-fast: false
       matrix:
         php-version:
-          - "8.2"
           - "8.3"
           - "8.4"
+          - "8.5"
 
     steps:
       - name: "Checkout"
@@ -73,15 +73,21 @@ jobs:
         uses: "shivammathur/setup-php@v2"
         with:
           php-version: "${{ matrix.php-version }}"
-          coverage: "pcov"
+          coverage: "xdebug"
 
       - name: "Install dependencies with Composer"
         run: "./tools/composer update --no-ansi --no-interaction --no-progress"
 
       - name: "Run tests with PHPUnit"
-        run: "vendor/bin/phpunit --coverage-clover=coverage.xml"
+        run: "vendor/bin/phpunit --log-junit junit.xml --coverage-clover=coverage.xml"
 
-      - name: "Send code coverage report to Codecov.io"
-        env:
-          CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
-        run: "bash <(curl -s https://codecov.io/bash) || true"
+      - name: Upload test results to Codecov.io
+        if: ${{ !cancelled() }}
+        uses: codecov/test-results-action@v1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+
+      - name: Upload code coverage data to Codecov.io
+        uses: codecov/codecov-action@v4
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.phive/phars.xml b/.phive/phars.xml
index 4817da71b394359dd9a4e623de1ab849dab53d4c..e52e2ca21d6d97895edc3e9af711db6c5ec31b5d 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.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"/>
+  <phar name="php-cs-fixer" version="^3.64" installed="3.68.0" location="./tools/php-cs-fixer" copy="true"/>
+  <phar name="composer" version="^2.8" installed="2.8.4" location="./tools/composer" copy="true"/>
+  <phar name="phpstan" version="^2.0" installed="2.1.1" location="./tools/phpstan" copy="true"/>
 </phive>
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 7d14230ebdf2aa5fcb86a467e62c2db119f0643c..74ac4bd7c5e77d5c652fbdf47983a70220933823 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -99,7 +99,7 @@ $config->setFinder($finder)
         'explicit_string_variable' => true,
         'fopen_flag_order' => true,
         'full_opening_tag' => true,
-        'fully_qualified_strict_types' => true,
+        'fully_qualified_strict_types' => ['import_symbols' => true],
         'function_declaration' => true,
         'function_to_constant' => true,
         'get_class_to_class_keyword' => true,
@@ -135,7 +135,7 @@ $config->setFinder($finder)
         'modernize_types_casting' => true,
         'multiline_comment_opening_closing' => true,
         'multiline_whitespace_before_semicolons' => true,
-        'native_constant_invocation' => false,
+        'native_constant_invocation' => true,
         'native_function_casing' => false,
         'native_function_invocation' => [
             'include' => [
@@ -158,7 +158,23 @@ $config->setFinder($finder)
         'no_empty_comment' => true,
         'no_empty_phpdoc' => true,
         'no_empty_statement' => true,
-        'no_extra_blank_lines' => true,
+        'no_extra_blank_lines' => [
+            'tokens' => [
+                'attribute',
+                'break',
+                'case',
+                'continue',
+                'curly_brace_block',
+                'default',
+                'extra',
+                'parenthesis_brace_block',
+                'return',
+                'square_brace_block',
+                'switch',
+                'throw',
+                'use',
+            ],
+        ],
         'no_homoglyph_names' => true,
         'no_leading_import_slash' => true,
         'no_leading_namespace_whitespace' => true,
@@ -197,6 +213,7 @@ $config->setFinder($finder)
         'no_whitespace_in_blank_line' => true,
         'non_printable_character' => true,
         'normalize_index_brace' => true,
+        'nullable_type_declaration_for_default_null_value' => true,
         'object_operator_without_whitespace' => true,
         'octal_notation' => true,
         'operator_linebreak' => [
@@ -340,6 +357,8 @@ $config->setFinder($finder)
         'whitespace_after_comma_in_array' => true,
     ]);
 
-$config->setCacheFile(__DIR__ . '/.php-cs-fixer.cache/' . sha1(@trim((string) @shell_exec('git rev-parse --abbrev-ref HEAD'))));
+$config->setCacheFile(__DIR__ . '/.php-cs-fixer.cache/' . json_decode((string) @file_get_contents('composer.json'), true)["extra"]["branch-alias"]["dev-main"] ?? 'unknown');
+
+$config->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());
 
 return $config;
diff --git a/ChangeLog.md b/ChangeLog.md
index 0fc1698236bbf9b6df3eec771e192a7616771aa3..53b640a944265822de81b8c515ea98ccd9e9f978 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,12 @@
 
 All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
 
+## [7.0.0] - 2025-02-07
+
+### Removed
+
+* This component is no longer supported on PHP 8.2
+
 ## [6.0.2] - 2024-07-03
 
 ### Changed
@@ -57,6 +63,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
 
 * Tests etc. are now ignored for archive exports
 
+[7.0.0]: https://github.com/sebastianbergmann/recursion-context/compare/6.0...7.0.0
 [6.0.2]: https://github.com/sebastianbergmann/recursion-context/compare/6.0.1...6.0.2
 [6.0.1]: https://github.com/sebastianbergmann/recursion-context/compare/6.0.0...6.0.1
 [6.0.0]: https://github.com/sebastianbergmann/recursion-context/compare/5.0...6.0.0
diff --git a/LICENSE b/LICENSE
index 5b4705a48d71526773da1aed0ce84464e4c94880..c5268a9168f8acfd38250813f0565f9792227c83 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2002-2024, Sebastian Bergmann
+Copyright (c) 2002-2025, Sebastian Bergmann
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/README.md b/README.md
index 8137f1aea1a16746749a8e2de0dfe77205f59027..f1d87b6142f7a731714a76c258e053cc070734b7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Latest Stable Version](https://poser.pugx.org/sebastian/recursion-context/v/stable.png)](https://packagist.org/packages/sebastian/recursion-context)
+[![Latest Stable Version](https://poser.pugx.org/sebastian/recursion-context/v)](https://packagist.org/packages/sebastian/recursion-context)
 [![CI Status](https://github.com/sebastianbergmann/recursion-context/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/recursion-context/actions)
 [![codecov](https://codecov.io/gh/sebastianbergmann/recursion-context/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/recursion-context)
 
diff --git a/composer.json b/composer.json
index 94e5be7cd4acf52bf7b64c40cdf4c0051f37d663..f37a207b384557ab327a8667781a9fb1cd725399 100644
--- a/composer.json
+++ b/composer.json
@@ -24,16 +24,16 @@
     "prefer-stable": true,
     "config": {
         "platform": {
-            "php": "8.2.0"
+            "php": "8.3.0"
         },
         "optimize-autoloader": true,
         "sort-packages": true
     },
     "require": {
-        "php": ">=8.2"
+        "php": ">=8.3"
     },
     "require-dev": {
-        "phpunit/phpunit": "^11.0"
+        "phpunit/phpunit": "^12.0"
     },
     "autoload": {
         "classmap": [
@@ -42,7 +42,7 @@
     },
     "extra": {
         "branch-alias": {
-            "dev-main": "6.0-dev"
+            "dev-main": "7.0-dev"
         }
     }
 }
diff --git a/phpstan.neon b/phpstan.neon
index 08566d07a495531f34abd8522c67f0c39eb68d96..e9a9e7e13a8945414577c267f4cbc7ccf7b81a10 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,5 @@
 parameters:
-    level: 9
+    level: 10
     paths:
         - src
         - tests
diff --git a/phpunit.xml b/phpunit.xml
index 911e767835b1224b2c7100d1e25eb41adfa89c9a..acd34e2f4ce35bebc35f9a0db2cb4146879549cc 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,12 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
-         bootstrap="vendor/autoload.php"
          cacheDirectory=".phpunit.cache"
          executionOrder="depends,defects"
          requireCoverageMetadata="true"
          beStrictAboutCoverageMetadata="true"
          beStrictAboutOutputDuringTests="true"
+         displayDetailsOnPhpunitDeprecations="true"
+         failOnPhpunitDeprecation="true"
          failOnRisky="true"
          failOnWarning="true"
          colors="true">
@@ -16,7 +17,7 @@
         </testsuite>
     </testsuites>
 
-    <source>
+    <source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
         <include>
             <directory>src</directory>
         </include>
diff --git a/src/Context.php b/src/Context.php
index 4e6160dd9850b75c7c4e736a35d51b2d9e11ff15..782e41d0c5d430faa357fa5bb4036c2ff3aac1af 100644
--- a/src/Context.php
+++ b/src/Context.php
@@ -16,6 +16,7 @@ use function array_pop;
 use function array_slice;
 use function count;
 use function is_array;
+use function is_int;
 use function random_int;
 use function spl_object_id;
 use SplObjectStorage;
@@ -43,6 +44,7 @@ final class Context
     public function __destruct()
     {
         foreach ($this->arrays as &$array) {
+            /* @phpstan-ignore function.alreadyNarrowedType */
             if (is_array($array)) {
                 array_pop($array);
                 array_pop($array);
@@ -57,9 +59,10 @@ final class Context
      *
      * @param-out T $value
      */
-    public function add(array|object &$value): false|int|string
+    public function add(array|object &$value): int
     {
         if (is_array($value)) {
+            /* @phpstan-ignore paramOut.type */
             return $this->addArray($value);
         }
 
@@ -73,7 +76,7 @@ final class Context
      *
      * @param-out T $value
      */
-    public function contains(array|object &$value): false|int|string
+    public function contains(array|object &$value): false|int
     {
         if (is_array($value)) {
             return $this->containsArray($value);
@@ -140,7 +143,13 @@ final class Context
     {
         $end = array_slice($array, -2);
 
-        return isset($end[1]) && $end[1] === $this->objects ? $end[0] : false;
+        if (isset($end[1]) &&
+            $end[1] === $this->objects &&
+            is_int($end[0])) {
+            return $end[0];
+        }
+
+        return false;
     }
 
     private function containsObject(object $value): false|int