Skip to content

Secrets API: encrypted, versioned storage for credentials - #13759

Closed
ericmann wants to merge 14 commits into
WordPress:trunkfrom
ericmann:secrets-api-7.2
Closed

ericmann wants to merge 14 commits into
WordPress:trunkfrom
ericmann:secrets-api-7.2

Conversation

@ericmann

@ericmann ericmann commented Sep 25, 2026 •

Copy link
Copy Markdown

Adds the Secrets API for 7.2: encrypted, versioned storage for credentials, with a secrets.php drop-in for hosts. The ticket has the full description and the list of additions beyond the proposal. The documentation is at secretsapi.dev.

What is in the patch

  • The API. src/wp-includes/secrets.php, 13 class and interface files in src/wp-includes/secrets/, and the Site Health tests in src/wp-admin/includes/secrets-site-health.php. These are the feature plugin's src/ at v0.3.0, byte for byte.
  • Loading and capabilities.
    • wp-settings.php loads the API, then loads wp-content/secrets.php through a new wp_load_secrets_dropin() in load.php, just before must-use plugins.
    • A broken drop-in fails closed with WP_Error and never falls back to the default provider.
    • class-wp-site-health.php loads the secrets Site Health tests.
    • populate_roles_720() and upgrade_720() grant manage_secrets to administrators.
    • manage_network_secrets goes with the network capabilities in map_meta_cap().
    • $wp_db_version is a placeholder, 61900, which needs the real revision on commit.
  • Tests. 380 tests in tests/phpunit/tests/secrets/, with helpers in tests/phpunit/includes/secrets/, plus the capability inventory.
    • The helpers include provider and keyring conformance suites for hosts that write drop-ins.
    • The test bootstrap defines a fixed WP_SECRETS_KEY when wp-tests-config.php has the placeholder salts, which the default keyring refuses.
    • Four tests that exercise the salt-based fallback skip in that case.
  • PHPStan config. tests/phpstan/base.neon adds LOGGED_IN_KEY and LOGGED_IN_SALT to dynamicConstantNames, next to AUTH_SALT. Without it PHPStan reads the placeholder from wp-config-sample.php and treats the placeholder checks as always true.

Changes from review

The patch was opened at plugin v0.2.0. Since then:

  • The class and interface files moved into src/wp-includes/secrets/, following abilities-api/ and ai-client/, per comment 5 on the ticket. The functions file stays at src/wp-includes/secrets.php.
  • The Secrets API files and their tests are clean at PHPStan level 10. Tests that pass a wrong type on purpose carry // @phpstan-ignore <identifier> (reason).
  • Parameters and return values carry native PHP 7.4 types. A return that needs a union type, such as WP_Secret|null|WP_Error, stays in the docblock. A plaintext value stays untyped and checked, because string $value would let PHP coerce false into an empty secret. See ADR 0013.
  • Static types are narrowed where that holds: versions and slots, scopes, the protection boundary, Site Health statuses, and counts. A secret's name stays string.
  • wp_secret_changed passes bool $network as a seventh argument. The provider still fires the hook, and the provider conformance suite now tests that it does. See ADR 0012.
  • Translation calls leave the text domain off.
  • Comments use an en dash where they used --.

Testing

  • npm run test:php -- --group secrets passes on single site and multisite.
  • tests/phpunit/tests/user/capabilities.php passes.
  • vendor/bin/phpstan analyse passes with core's configuration, and the Secrets API files and tests pass at level 10.

Trac ticket: https://core.trac.wordpress.org/ticket/66187

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5, Claude Sonnet 5, Claude Fable 5.1
Used for: Most of the code and tests, in the feature plugin and in this port: the API, the core wiring, the test port, and review passes. I directed the design, and the decisions are recorded as ADRs at secretsapi.dev.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Copies src/ from the Secrets API feature plugin at v0.2.0
(https://github.com/ericmann/secrets-api), unchanged: the public
functions, WP_Secret and WP_Secret_Version, the provider, store, and
keyring interfaces with their shipped and fail-closed implementations,
the cipher and key manager, and the Site Health tests.

Nothing loads these files yet; the next commit wires them in.
- wp-settings.php loads the API next to application passwords, and
  loads the secrets.php drop-in just before must-use plugins, once every
  interface a drop-in implements exists.
- wp_load_secrets_dropin() in load.php marks the drop-in broken when it
  throws or sets a provider, store, or keyring global of the wrong
  type, so the API fails closed instead of falling back to the default.
- class-wp-site-health.php loads the Secrets API's Site Health tests,
  so every path that loads WP_Site_Health gets them.
- populate_roles_720() grants manage_secrets to administrators, and
  upgrade_720() does the same for existing sites (db version 61900;
  the committer should set the real revision).
- map_meta_cap() lists manage_network_secrets with the other network
  capabilities, which only super admins hold.
Ports the feature plugin's tests for everything under src/ into
tests/phpunit/tests/secrets/, with its shared helpers (assertions
trait, mock store and keyring, provider and keyring conformance
suites) in tests/phpunit/includes/secrets/. The plugin-only tests
(prototype upgrade path, WP-CLI, architecture, no-op gate) stay behind.

wp-tests-config-sample.php ships placeholder salts, which the config
keyring refuses by design, so the test bootstrap defines a fixed
WP_SECRETS_KEY in the main PHPUnit process. Process-isolated tests set
or unset the key themselves; the four that exercise the LOGGED_IN
salt fallback skip when the salts are placeholders.

Also registers manage_secrets and manage_network_secrets in the
capability inventory test, and adds tests for populate_roles_720(),
the upgrade_720() gate, and manage_network_secrets on multisite.
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props ericmann, whyisjake, westonruter, mindctrl, audrasjb.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

PHP 8.5 deprecates ReflectionMethod::setAccessible() and
ReflectionProperty::setAccessible(), which have had no effect since
8.1, so the four calls in the Secrets API tests errored on the PHP 8.5
jobs. They now use the same PHP_VERSION_ID < 80100 guard as the rest
of the suite.
Follows the layout abilities-api/ and ai-client/ use: the classes and
interfaces sit in their own folder, and the functions file stays at
wp-includes/secrets.php. The 13 files are renamed without edits, and
wp-settings.php requires them from the new paths in the same order.

See #66187.
@westonruter

Copy link
Copy Markdown
Member

I ran PHPStan rule level 10 on the changes in this PR and there were quite a few opportunities for improvement. For the changes in src:


 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/schema.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  977    Function populate_roles_720() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/secrets-site-health.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  23     Function wp_secrets_site_health_tests() has parameter $tests with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  23     Function wp_secrets_site_health_tests() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  24     Cannot access offset 'secrets_api_key_source' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  29     Cannot access offset 'secrets_api_undecryptable' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  34     Cannot access offset 'secrets_api_needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  56     Function wp_secrets_site_health_result() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  82     Function wp_secrets_site_health_test_key_source() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  139    Function wp_secrets_site_health_test_undecryptable() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  160    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  160    Parameter #1 $text of function esc_html expects float|int|string, mixed given.
         🪪  argument.type
  164    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  164    Parameter #1 $text of function esc_html expects float|int|string, mixed given.
         🪪  argument.type
  191    Function wp_secrets_site_health_find_undecryptable() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  201    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  201    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  201    Parameter #1 $name of function wp_get_network_secret expects string, mixed given.
         🪪  argument.type
  201    Parameter #1 $name of function wp_get_secret expects string, mixed given.
         🪪  argument.type
  205    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  206    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  221    Function wp_secrets_site_health_test_needs_rotation() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  278    Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  298    Function wp_secrets_site_health_debug_info() has parameter $info with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  298    Function wp_secrets_site_health_debug_info() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/upgrade.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  2517   Function upgrade_720() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/load.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  848    Function wp_load_secrets_dropin() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  169    Function wp_secrets_memzero() has no return type specified.
         🪪  missingType.return
  325    Function _wp_secrets_get_store() should return WP_Secrets_Store but returns mixed.
         🪪  return.type
  361    Function _wp_secrets_get_key_manager() should return WP_Secrets_Key_Manager but returns mixed.
         🪪  return.type
  549    Function wp_list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  622    Function wp_list_network_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  655    Function _wp_secrets_get_provider() should return WP_Secrets_Provider but returns mixed.
         🪪  return.type
  809    Function _wp_secrets_list() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  239    Method WP_Secret::__debugInfo() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  308    Method WP_Secret::__unserialize() has parameter $data with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-broken-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  51     Method WP_Secrets_Broken_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-cipher.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  69     Method WP_Secrets_Cipher::encrypt_value() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  161    Parameter #1 $ciphertext of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  163    Parameter #3 $nonce of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  175    Parameter #1 $ciphertext of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  177    Parameter #3 $nonce of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  178    Parameter #4 $key of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, string|true given.
         🪪  argument.type
  181    Parameter #1 $value of function wp_secrets_memzero expects string, string|true given.
         🪪  argument.type
  190    Method WP_Secrets_Cipher::decrypt_value() should return string|WP_Error but returns string|true.
         🪪  return.type
  356    Method WP_Secrets_Cipher::decode_record_fields() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-config-key-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  171    Method WP_Secrets_Config_Key_Provider::unwrap() should return string|WP_Error but returns string|true.
         🪪  return.type
  183    Parameter #1 $value of method WP_Secrets_Config_Key_Provider::is_canonical_base64_32() expects string, mixed given.
         🪪  argument.type
  216    Method WP_Secrets_Config_Key_Provider::get_site_key() should return string|WP_Error but returns string|false.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-key-manager.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  217    Method WP_Secrets_Key_Manager::get_root_key() should return string|WP_Error but returns string|null.
         🪪  return.type
  264    Parameter #1 $key_material of method WP_Secrets_Keyring::wrap() expects string, string|null given.
         🪪  argument.type
  267    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  281    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  292    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  315    Method WP_Secrets_Key_Manager::get_wrapped_root_key() should return string|false but returns mixed.
         🪪  return.type
  330    Method WP_Secrets_Key_Manager::get_wrapped_root_key() should return string|false but returns mixed.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  174    Method WP_Secrets_Libsodium_Provider::read_prior_record() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  198    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  230    Method WP_Secrets_Libsodium_Provider::demote_slot() has parameter $current_slot with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  230    Method WP_Secrets_Libsodium_Provider::demote_slot() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  509    do_action() call for hook "wp_secret_changed" references documentation in "src/wp-includes/secrets.php", but no such file exists in the tree being
         analyzed.
         🪪  wordpress.hookDocReferenceFileMissing
  565    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  578    do_action() call for hook "wp_secret_changed" references documentation in "src/wp-includes/secrets.php", but no such file exists in the tree being
         analyzed.
         🪪  wordpress.hookDocReferenceFileMissing
  620    Method WP_Secrets_Libsodium_Provider::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  633    Parameter #1 $haystack of function strpos expects string, mixed given.
         🪪  argument.type
  637    Parameter #1 $name of method WP_Secrets_Store::get() expects string, mixed given.
         🪪  argument.type
  643    Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  645    Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-option-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  52     Method WP_Secrets_Option_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  81     Method WP_Secrets_Option_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  160    Method WP_Secrets_Option_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  164    Binary operation "." between mixed and '%' results in an error.
         🪪  binaryOp.invalid
  164    Cannot call method esc_like() on mixed.
         🪪  method.nonObject
  167    Cannot call method get_col() on mixed.
         🪪  method.nonObject
  168    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  169    Cannot access property $sitemeta on mixed.
         🪪  property.nonObject
  169    Part $wpdb->sitemeta (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  175    Cannot call method get_col() on mixed.
         🪪  method.nonObject
  176    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  177    Cannot access property $options on mixed.
         🪪  property.nonObject
  177    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  193    Parameter #1 $string of function substr expects string, mixed given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/interface-wp-secrets-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  173    Method WP_Secrets_Provider::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/interface-wp-secrets-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  38     Method WP_Secrets_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  52     Method WP_Secrets_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  76     Method WP_Secrets_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 [ERROR] Found 85 errors on changed lines (base: trunk) 
Errors in tests

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/bootstrap.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  42     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-mock-keyring.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  12     Property Mock_Keyring::$fail_wrap has no type specified.
         🪪  missingType.property
  13     Property Mock_Keyring::$fail_unwrap has no type specified.
         🪪  missingType.property
  14     Property Mock_Keyring::$wrap_calls has no type specified.
         🪪  missingType.property
  15     Property Mock_Keyring::$unwrap_calls has no type specified.
         🪪  missingType.property
  18     Cannot use ++ on mixed.
         🪪  preInc.type
  31     Cannot use ++ on mixed.
         🪪  preInc.type
  88     Method Mock_Keyring::wrap_call_count() should return int but returns mixed.
         🪪  return.type
  95     Method Mock_Keyring::unwrap_call_count() should return int but returns mixed.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-mock-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  10     Property Mock_Store::$records has no type specified.
         🪪  missingType.property
  11     Property Mock_Store::$fail has no type specified.
         🪪  missingType.property
  17     Property Mock_Store::$received_records has no type specified.
         🪪  missingType.property
  19     Method Mock_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  20     Cannot access offset 'get' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  26     Method Mock_Store::get() should return array|WP_Error|null but returns mixed.
         🪪  return.type
  26     Parameter #1 $key of function array_key_exists expects int|string, mixed given.
         🪪  argument.type
  26     Parameter #2 $search of function array_key_exists expects array, mixed given.
         🪪  argument.type
  26     Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  29     Method Mock_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  30     Cannot access an offset on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  32     Cannot access offset 'set' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  36     Cannot access offset mixed on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  42     Cannot access offset 'delete' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  46     Cannot access offset mixed on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  51     Method Mock_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  52     Cannot access offset 'list' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  59     Parameter #1 $input of function array_keys expects array, mixed given.
         🪪  argument.type
  60     Parameter #1 $haystack of function strpos expects string, int|string given.
         🪪  argument.type
  61     Parameter #1 $string of function substr expects string, int|string given.
         🪪  argument.type
  75     Cannot access offset string on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  87     Method Mock_Store::get_received_records() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  88     Method Mock_Store::get_received_records() should return array but returns mixed.
         🪪  return.type
  91     Method Mock_Store::key() has no return type specified.
         🪪  missingType.return
  91     Method Mock_Store::key() has parameter $name with no type specified.
         🪪  missingType.parameter
  91     Method Mock_Store::key() has parameter $network with no type specified.
         🪪  missingType.parameter
  92     Binary operation "." between 'network:'|'site:' and mixed results in an error.
         🪪  binaryOp.invalid
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-wp-secrets-keyring-conformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  29     Method WP_Secrets_Keyring_Conformance::test_wrap_returns_a_non_empty_string_that_unwraps_to_the_same_bytes() has no return type specified.
         🪪  missingType.return
  49     Method WP_Secrets_Keyring_Conformance::test_two_wraps_of_the_same_bytes_return_different_strings() has no return type specified.
         🪪  missingType.return
  65     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_garbage_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  77     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_a_truncated_value_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  81     Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  81     Parameter #1 $string of function substr expects string, string|WP_Error given.
         🪪  argument.type
  91     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_a_value_with_one_flipped_byte_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  95     Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  97     Cannot access offset int on string|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  97     Cannot access offset int on string|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  97     Parameter #1 $character of function ord expects string, mixed given.
         🪪  argument.type
  99     Parameter #1 $wrapped of method WP_Secrets_Keyring::unwrap() expects string, (ArrayAccess&WP_Error)|string given.
         🪪  argument.type
  106    Method WP_Secrets_Keyring_Conformance::test_get_key_source_returns_a_non_empty_string() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-wp-secrets-provider-conformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  53     Method WP_Secrets_Provider_Conformance::require_writable() has no return type specified.
         🪪  missingType.return
  61     Method WP_Secrets_Provider_Conformance::test_reports_a_non_empty_label() has no return type specified.
         🪪  missingType.return
  68     Method WP_Secrets_Provider_Conformance::test_reports_a_known_protection_boundary() has no return type specified.
         🪪  missingType.return
  76     Method WP_Secrets_Provider_Conformance::test_is_writable_returns_a_bool() has no return type specified.
         🪪  missingType.return
  84     Method WP_Secrets_Provider_Conformance::test_a_read_only_provider_actually_refuses_writes() has no return type specified.
         🪪  missingType.return
  109    Method WP_Secrets_Provider_Conformance::test_a_name_that_was_never_set_is_null() has no return type specified.
         🪪  missingType.return
  120    Method WP_Secrets_Provider_Conformance::test_previous_on_a_provider_or_secret_without_history_is_null_not_error() has no return type specified.
         🪪  missingType.return
  133    Method WP_Secrets_Provider_Conformance::test_deleting_something_absent_is_success() has no return type specified.
         🪪  missingType.return
  143    Method WP_Secrets_Provider_Conformance::test_retiring_with_no_previous_version_is_success() has no return type specified.
         🪪  missingType.return
  155    Method WP_Secrets_Provider_Conformance::test_a_stored_value_comes_back_intact() has no return type specified.
         🪪  missingType.return
  174    Method WP_Secrets_Provider_Conformance::test_a_fingerprint_is_stable_for_the_same_value() has no return type specified.
         🪪  missingType.return
  181    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  182    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  189    Method WP_Secrets_Provider_Conformance::test_a_deleted_secret_reads_as_absent() has no return type specified.
         🪪  missingType.return
  202    Method WP_Secrets_Provider_Conformance::test_listing_returns_an_array_or_an_error_and_never_a_value() has no return type specified.
         🪪  missingType.return
  214    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  219    Method WP_Secrets_Provider_Conformance::test_listing_includes_a_stored_secret() has no return type specified.
         🪪  missingType.return
  226    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
  236    Method WP_Secrets_Provider_Conformance::test_listing_respects_a_name_prefix() has no return type specified.
         🪪  missingType.return
  243    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/brokenDropinFallbacks.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  12     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Store' and WP_Secrets_Broken_Store will always evaluate to true.
         🪪  method.alreadyNarrowedType
  42     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Keyring' and WP_Secrets_Broken_Keyring will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/capabilities.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  30     Call to method PHPUnit\Framework\Assert::assertSame() with 'manage_secrets' and 'manage_secrets' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  31     Call to method PHPUnit\Framework\Assert::assertSame() with 'manage_network…' and 'manage_network…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  35     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  40     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  45     Cannot call method remove_cap() on WP_Role|null.
         🪪  method.nonObject
  49     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  64     Cannot call method remove_cap() on WP_Role|null.
         🪪  method.nonObject
  70     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  73     Method Tests_Secrets_Capabilities::data_upgrade_720_gate() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/errorCodes.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  11     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_decryption…' and 'secret_decryption…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  12     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_key…' and 'secret_key…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  13     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_store…' and 'secret_store…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  14     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_invalid_name' and 'secret_invalid_name' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  15     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_invalid_value' and 'secret_invalid_value' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  16     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_crypto…' and 'secret_crypto…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  17     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_record…' and 'secret_record…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/extensionPoints.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  38     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  49     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  61     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  110    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/networkScope.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  28     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  29     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  45     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  68     Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  22     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::set() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  29     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::delete() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  37     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::list_secrets() never returns WP_Error so it can be removed from the return type.
         🪪  return.unusedType
  37     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  150    Call to method PHPUnit\Framework\Assert::assertNotInstanceOf() with 'WP_Secrets…' and WP_Secrets_Broken_Provider will always evaluate to true.
         🪪  method.alreadyNarrowedType
  214    Call to method PHPUnit\Framework\Assert::assertNotNull() with WP_Error will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/readonlyProviderConformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  20     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::set() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  27     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::delete() never returns true so it can be removed from the return
         type.
         🪪  return.unusedType
  31     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::retire_previous() never returns true so it can be removed from the
         return type.
         🪪  return.unusedType
  35     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::list_secrets() never returns WP_Error so it can be removed from the
         return type.
         🪪  return.unusedType
  35     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::list_secrets() return type has no value type specified in iterable
         type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/siteHealth.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  13     Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  17     Argument of an invalid type array|ArrayAccess supplied for foreach, only iterables are supported.
         🪪  foreach.nonIterable
  18     Cannot access offset 'test' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  18     Cannot access offset 'test' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  18     Parameter #1 $function_name of function function_exists expects string, mixed given.
         🪪  argument.type
  18     Part $test['test'] (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  25     Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  29     Result of || is always true.
         🪪  booleanOr.alwaysTrue
  29     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  29     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  33     Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  49     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  92     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  95     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  95     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  101    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  102    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  109    Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  109    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  114    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, mixed given.
         🪪  argument.type
  132    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  141    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  143    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  150    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  151    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  152    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  153    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'dropin_active' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  169    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  175    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  175    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayNotHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  191    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  191    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayNotHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  196    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  196    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/threeStateContract.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  103    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  103    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  103    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  107    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  152    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  152    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  169    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  172    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  172    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  175    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/versionSlots.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  45     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  49     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  54     Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  54     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  58     Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  58     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  69     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  69     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  74     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  74     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  86     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  86     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  90     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  90     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  94     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  113    Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  113    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  117    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  129    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  151    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  166    Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpDeleteSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  29     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  43     Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpImportOptionAsSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  13     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  30     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  30     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  38     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  38     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  81     Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
  98     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  99     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpListSecrets.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  18     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  21     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  21     Parameter #1 $input of function array_keys expects array, mixed given.
         🪪  argument.type
  23     Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  23     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  35     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  41     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  44     Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  44     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  50     Cannot access offset 'has_previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  50     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  54     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  61     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  61     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  64     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  64     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  71     Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
  80     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  93     Parameter #1 $namespace of function wp_list_secrets expects string, array<int, string> given.
         🪪  argument.type
  108    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  109    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  109    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  110    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  110    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  111    Cannot access offset 'has_previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  111    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  118    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  21     Method Tests_Secrets_WPSecret::make_secret() has no return type specified.
         🪪  missingType.return
  38     Cannot call method reveal() on mixed.
         🪪  method.nonObject
  44     Cannot call method fingerprint() on mixed.
         🪪  method.nonObject
  50     Cannot call method get_name() on mixed.
         🪪  method.nonObject
  56     Parameter #2 $value of class WP_Secret constructor expects string, int given.
         🪪  argument.type
  76     Method Tests_Secrets_WPSecret::test_masking_surfaces_never_contain_the_plaintext() has parameter $callback with no type specified.
         🪪  missingType.parameter
  79     Trying to invoke mixed but it's not a callable.
         🪪  callable.nonCallable
  81     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, mixed given.
         🪪  argument.type
  84     Method Tests_Secrets_WPSecret::data_masking_surfaces() has no return type specified.
         🪪  missingType.return
  88     Cannot cast mixed to string.
         🪪  cast.string
  93     Part $secret (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  122    Parameter #1 $message of function error_log expects string, mixed given.
         🪪  argument.type
  135    Cannot cast mixed to string.
         🪪  cast.string
  142    Parameter #1 $json of function json_decode expects string, string|false given.
         🪪  argument.type
  168    Cannot clone non-object variable $secret of type mixed.
         🪪  clone.nonObject
  168    Expression "clone $secret" on a separate line does not do anything.
         🪪  expr.resultUnused
  178    Method Tests_Secrets_WPSecret::test_serialization_magic_methods_throw_directly() has parameter $args with no type specified.
         🪪  missingType.parameter
  178    Method Tests_Secrets_WPSecret::test_serialization_magic_methods_throw_directly() has parameter $method with no type specified.
         🪪  missingType.parameter
  180    Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  180    Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  187    Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  187    Parameter #2 $args of method ReflectionMethod::invokeArgs() expects array, mixed given.
         🪪  argument.type
  190    Method Tests_Secrets_WPSecret::data_refusing_magic_methods() has no return type specified.
         🪪  missingType.return
  201    Parameter #1 $obj of function spl_object_id expects object, mixed given.
         🪪  argument.type
  208    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  318    Parameter #3 $reason of static method WP_Secret::withheld() expects WP_Error, string given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsCipher.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has no return type specified.
         🪪  missingType.return
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $args with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $instance with no type specified.
         🪪  missingType.parameter
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $method with no type specified.
         🪪  missingType.parameter
  12     Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  12     Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  17     Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  20     Method Tests_Secrets_WPSecretsCipher::master_key() has no return type specified.
         🪪  missingType.return
  20     Method Tests_Secrets_WPSecretsCipher::master_key() has parameter $byte with no type specified.
         🪪  missingType.parameter
  21     Parameter #1 $ascii of function chr expects int, mixed given.
         🪪  argument.type
  26     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  30     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  37     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  39     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  46     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  48     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  56     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  56     Parameter #6 $plaintext of method WP_Secrets_Cipher::encrypt_value() expects string, array given.
         🪪  argument.type
  65     Method Tests_Secrets_WPSecretsCipher::test_encrypt_rejects_a_bad_master_key() has parameter $bad_key with no type specified.
         🪪  missingType.parameter
  68     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  74     Method Tests_Secrets_WPSecretsCipher::data_bad_master_keys() has no return type specified.
         🪪  missingType.return
  86     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  97     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  108    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  119    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $name with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $scope with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $site_id with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $slot with no type specified.
         🪪  missingType.parameter
  133    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #2 $scope of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #3 $site_id of method WP_Secrets_Cipher::decrypt_value() expects int, mixed given.
         🪪  argument.type
  135    Parameter #4 $name of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #5 $slot of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  141    Method Tests_Secrets_WPSecretsCipher::data_mismatched_context() has no return type specified.
         🪪  missingType.return
  152    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  154    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  163    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_a_field_is_tampered() has parameter $field with no type specified.
         🪪  missingType.parameter
  165    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  167    Cannot access offset mixed on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  167    Parameter #1 $str of function base64_decode expects string, mixed given.
         🪪  argument.type
  167    Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  168    Parameter #1 $string of function strlen expects string, string|false given.
         🪪  argument.type
  169    Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  169    Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  170    Cannot access offset mixed on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  170    Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  172    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  177    Method Tests_Secrets_WPSecretsCipher::data_tamperable_fields() has no return type specified.
         🪪  missingType.return
  189    Method Tests_Secrets_WPSecretsCipher::test_decrypt_rejects_a_malformed_record() has parameter $record with no type specified.
         🪪  missingType.parameter
  192    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  198    Method Tests_Secrets_WPSecretsCipher::data_malformed_records() has no return type specified.
         🪪  missingType.return
  230    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  231    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  233    Cannot access offset 'dk' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  233    Cannot access offset 'dk' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  234    Cannot access offset 'ct' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  234    Cannot access offset 'ct' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  242    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  243    Cannot access offset 'nonce' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  244    Cannot access offset 'dk_nonce' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  247    Parameter #1 $array of function array_unique expects an array of values castable to string, list given.
         🪪  argument.type
  254    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  255    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  263    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  264    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  272    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  273    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  289    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  289    Parameter #2 $plaintext of method WP_Secrets_Cipher::fingerprint() expects string, array given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsConfigKeyProvider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has no return type specified.
         🪪  missingType.return
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $args with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $instance with no type specified.
         🪪  missingType.parameter
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $method with no type specified.
         🪪  missingType.parameter
  17     Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  17     Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  22     Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  26     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Keyring' and WP_Secrets_Config_Key_Provider will always evaluate to true.
         🪪  method.alreadyNarrowedType
  43     Parameter #1 $key_material of method WP_Secrets_Config_Key_Provider::wrap() expects string, int given.
         🪪  argument.type
  68     Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, array given.
         🪪  argument.type
  96     Parameter #1 $str of function base64_decode expects string, string|WP_Error given.
         🪪  argument.type
  98     Parameter #1 $string of function strlen expects string, string|false given.
         🪪  argument.type
  99     Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  99     Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  108    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  108    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  108    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  112    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  120    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_is_canonical_base64_32() has parameter $expected with no type specified.
         🪪  missingType.parameter
  120    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_is_canonical_base64_32() has parameter $value with no type specified.
         🪪  missingType.parameter
  126    Method Tests_Secrets_WPSecretsConfigKeyProvider::data_canonical_base64_32() has no return type specified.
         🪪  missingType.return
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $expected with no type specified.
         🪪  missingType.parameter
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $key with no type specified.
         🪪  missingType.parameter
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $salt with no type specified.
         🪪  missingType.parameter
  147    Method Tests_Secrets_WPSecretsConfigKeyProvider::data_usable_salt_values() has no return type specified.
         🪪  missingType.return
  188    Parameter #1 $data of function bin2hex expects string, mixed given.
         🪪  argument.type
  271    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  271    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  271    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  275    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  312    Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, string|WP_Error given.
         🪪  argument.type
  313    Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, string|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsKeyManager.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  52     Parameter #4 $key of function sodium_crypto_kdf_derive_from_key expects string, string|WP_Error given.
         🪪  argument.type
  107    Method Tests_Secrets_WPSecretsKeyManager::test_get_master_key_reports_an_invalid_site_id_as_a_wp_error() has parameter $site_id with no type specified.
         🪪  missingType.parameter
  112    Parameter #2 $site_id of method WP_Secrets_Key_Manager::get_master_key() expects int|null, mixed given.
         🪪  argument.type
  118    Method Tests_Secrets_WPSecretsKeyManager::data_invalid_site_ids() has no return type specified.
         🪪  missingType.return
  196    Parameter #1 $known_string of function hash_equals expects string, string|WP_Error given.
         🪪  argument.type
  196    Parameter #2 $user_string of function hash_equals expects string, string|WP_Error given.
         🪪  argument.type
  221    Cannot call method reveal() on WP_Secret|null.
         🪪  method.nonObject
  271    Method WP_Secrets_Keyring@anonymous/tests/secrets/wpSecretsKeyManager.php:270::wrap() never returns WP_Error so it can be removed from the return type.
         🪪  return.unusedType
  274    Method WP_Secrets_Keyring@anonymous/tests/secrets/wpSecretsKeyManager.php:270::unwrap() never returns string so it can be removed from the return type.
         🪪  return.unusedType
  332    Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  333    Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  405    Parameter #1 $expected of method PHPUnit\Framework\Assert::assertSame() contains unresolvable type.
         🪪  argument.unresolvableType
  428    Parameter #1 $value of function wp_secrets_memzero expects string, string|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsMemzero.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  19     Parameter #1 $value of function wp_secrets_memzero expects string, int given.
         🪪  argument.type
  41     Call to method PHPUnit\Framework\Assert::assertSame() with 'shared plaintext' and 'shared plaintext' will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsOptionStore.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  9      Method Tests_Secrets_WPSecretsOptionStore::sample_record() has no return type specified.
         🪪  missingType.return
  24     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Store' and WP_Secrets_Option_Store will always evaluate to true.
         🪪  method.alreadyNarrowedType
  37     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  45     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  52     Parameter #1 $arr1 of function array_merge expects array, mixed given.
         🪪  argument.type
  54     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  64     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, string given.
         🪪  argument.type
  83     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  99     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  101    Cannot call method get_var() on mixed.
         🪪  method.nonObject
  102    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  103    Cannot access property $options on mixed.
         🪪  property.nonObject
  103    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  113    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  114    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  118    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  126    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  128    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertNotContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  133    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  134    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  136    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  138    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  153    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  157    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsValidateName.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  10     Method Tests_Secrets_WpSecretsValidateName::test_accepts_valid_names() has parameter $name with no type specified.
         🪪  missingType.parameter
  11     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  14     Method Tests_Secrets_WpSecretsValidateName::data_valid_names() has no return type specified.
         🪪  missingType.return
  30     Method Tests_Secrets_WpSecretsValidateName::test_rejects_invalid_names() has parameter $name with no type specified.
         🪪  missingType.parameter
  31     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  45     Method Tests_Secrets_WpSecretsValidateName::test_accepts_an_unnamespaced_name_but_reports_it() has parameter $name with no type specified.
         🪪  missingType.parameter
  48     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  51     Method Tests_Secrets_WpSecretsValidateName::data_unnamespaced_names() has no return type specified.
         🪪  missingType.return
  72     Method Tests_Secrets_WpSecretsValidateName::data_invalid_names() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSetSecretAndWpGetSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  30     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  31     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  71     Parameter #2 $value of function wp_set_secret expects string, array<int, string> given.
         🪪  argument.type
  79     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  90     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  105    Cannot access property $options on mixed.
         🪪  property.nonObject
  105    Cannot call method get_results() on mixed.
         🪪  method.nonObject
  105    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  120    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  154    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  168    Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
  207    Argument of an invalid type array<int<0, max>|string, mixed>|null supplied for foreach, only iterables are supported.
         🪪  foreach.nonIterable
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 [ERROR] Found 400 errors on changed lines (base: trunk) 

If you want I can start working on pushing up fixes for these.

Comment thread src/wp-includes/secrets/class-wp-secret.php
Comment thread src/wp-includes/secrets/class-wp-secrets-broken-store.php Outdated
Comment thread src/wp-includes/secrets/class-wp-secrets-libsodium-provider.php Outdated
Comment thread src/wp-includes/secrets/class-wp-secrets-libsodium-provider.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
@ericmann

ericmann commented Oct 7, 2026

Copy link
Copy Markdown
Author

I ran PHPStan rule level 10 on the changes in this PR and there were quite a few opportunities for improvement. For the changes in src:

[...]

If you want I can start working on pushing up fixes for these.

I appreciate the callouts sir! I need to get this resolved on the feature plugin as well to keep them in sync, so I'll start working on resolving this all ASAP.

…n PHPStan

PHPStan reads both constants from wp-config-sample.php, where they hold
the placeholder phrase, and so treats any comparison against that phrase
as always true. Their values differ on every install, like AUTH_SALT,
which is already listed.

The Secrets API refuses to derive a key from the placeholder salts, and
its tests and the test bootstrap check for them.

See #66187.
Addresses the PHPStan level 10 report on the pull request: 85 findings
in src/ and 400 in the tests.

In src/, most fixes are docblocks that say what an array holds, with
@phpstan-return shapes where a list entry or a record slot has fixed
keys. The code changes:

- Decrypt results are checked with is_string() rather than against
  false, since sodium_compat's signatures allow more than string|false.
- The cached root key is only served once it is known to be set.
- The config keyring decodes a canonical base64 key once, instead of
  validating it and then decoding it again.
- Demoting a slot keeps `created` only when it is an integer and stores
  `needs_rotation` as a boolean.
- Listing skips a stored option name that is not a string.
- The request-cached store, key manager, and provider are narrowed with
  instanceof rather than compared against null.
- wp_secrets_memzero(), wp_load_secrets_dropin(), populate_roles_720(),
  and upgrade_720() declare a void return.
- The two "documented in" references for wp_secret_changed name the file
  that documents the hook.

Docblocks no longer put a blank line between @PARAM and @return.

In the tests, values are narrowed with assertions before use, helpers
and data providers are typed, and test methods declare a void return. A
test that passes a wrong type on purpose, or pins a constant or an
interface, carries an `@phpstan-ignore <identifier> (reason)` comment.
The error-code test is now one case per constant.

This is the feature plugin's src/ and tests at 0.2.2.

See #66187.
Comment thread tests/phpunit/includes/secrets/class-mock-keyring.php
Comment thread src/wp-includes/secrets/interface-wp-secrets-store.php
Comment thread src/wp-includes/secrets/interface-wp-secrets-store.php Outdated
Core does not pass 'default' to __() and its relatives for its own
strings. The 101 calls in the Secrets API now leave the argument off, as
the rest of core does. The strings are unchanged.

See #66187.
…r firing it

The provider fires wp_secret_changed, because only the provider has the
prior fingerprint without another read. A drop-in provider that left the
call out would silence a site's audit log, and nothing checked for that.

The hook stays with the provider, and the requirement is now enforced:

- WP_Secrets_Provider states it on delete() and retire_previous() as
  well as set(): fire once the change has succeeded, and nothing
  otherwise.
- WP_Secrets_Provider_Conformance checks a write, an overridden action,
  a delete, a retired version, a network-scope change, and a refused
  write. It asks for every argument, so a provider that passes too few
  fails on the count.

wp_secret_changed also gains a seventh argument, `bool $network`. A
network secret and a site secret can share a name, and a listener could
not tell which one had changed. A site-scope change belongs to the
current site.

This is the feature plugin's src/ and tests at 0.2.3.

See #66187.

@audrasjb audrasjb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, some change requests :)

Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-admin/includes/secrets-site-health.php Outdated
Comment thread src/wp-includes/secrets/class-wp-secret.php Outdated
… hold

Parameters and return values carry PHP 7.4 type declarations: the public
functions, the three interfaces and their implementations, and the
internal classes. A return that needs a union type, such as
WP_Secret|null|WP_Error, stays in the docblock.

The runtime checks the declarations replace are removed, with the tests
that exercised them. A plaintext value stays untyped and checked: as
`string`, PHP would coerce false to an empty secret and report success.
The two Site Health filter callbacks stay untyped, since they receive
whatever an earlier callback returned.

Static types are narrowed where that holds: versions and slots, scopes,
the protection boundary, Site Health statuses, and counts. A secret's
name stays `string`, because the functions exist to validate one.

This is the feature plugin's src/ and tests at 0.3.0.

See #66187.
Comments used `--` as a dash. They now use an en dash, and the two
messages that had one are reworded. Ten docblock continuation lines were
indented one column too far.

See #66187.
Trunk moved to PHPStan 2.3.0, which no longer reports
method.impossibleType on the assertion in the memzero test. With
reportUnmatchedIgnoredErrors on, the leftover ignore was itself an error
at level 10.

See #66187.
@whyisjake
whyisjake dismissed audrasjb’s stale review October 13, 2026 18:57

Changes have been addressed.

@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 64337
GitHub commit: 5744bc5

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants