Skip to content

Fix false positive when deprecation helper is inside functioncall#989

Open
bbrala wants to merge 2 commits into
mglaman:mainfrom
bbrala:fix/deprecation-helper-inside-functioncall
Open

Fix false positive when deprecation helper is inside functioncall#989
bbrala wants to merge 2 commits into
mglaman:mainfrom
bbrala:fix/deprecation-helper-inside-functioncall

Conversation

@bbrala
Copy link
Copy Markdown
Contributor

@bbrala bbrala commented May 15, 2026

I'm doing a run of improvements for Druapl Rector. That will now use DeprecationHelper also for deprecations that are called in a rector. When running against contrib i noticed a lot of deprecations where still reported. After some investigation with Claude we noticed the DeprecationHelperScope does not account for the callstack it might be in. See the report below:

Summary

DeprecationHelperScope::isScopeDeprecated() only inspects $callStack[0] from
Scope::getFunctionCallStackWithParameters(). When DeprecationHelper::backwardsCompatibleCall()
is itself passed as an argument to an outer function, PHPStan pushes the outer function onto the
stack first — so index 0 is the outer call, not backwardsCompatibleCall. The deprecated call
inside deprecatedCallable is never suppressed, producing a false-positive deprecation error.

Failing patterns

// BC wrapper as argument to trim() — NOT suppressed (false positive)
$font_title = trim(DeprecationHelper::backwardsCompatibleCall(
    \Drupal::VERSION, '11.3.0',
    fn() => \Drupal::service(ThemeSettingsProvider::class)->getSetting('font_family_title'),
    fn() => theme_get_setting('font_family_title')  // ← still flagged
));

// BC wrapper as element of array passed to array_filter() — NOT suppressed
$font_urls = array_filter([
    DeprecationHelper::backwardsCompatibleCall(
        \Drupal::VERSION, '11.3.0',
        fn() => \Drupal::service(ThemeSettingsProvider::class)->getSetting('font_url_title'),
        fn() => theme_get_setting('font_url_title')  // ← still flagged
    ),
], 'trim');

Working pattern (for contrast)

// Direct assignment — correctly suppressed
$logo = DeprecationHelper::backwardsCompatibleCall(
    \Drupal::VERSION, '11.3.0',
    fn() => \Drupal::service(ThemeSettingsProvider::class)->getSetting('logo.url'),
    fn() => theme_get_setting('logo.url')  // ← correctly suppressed
);

Root cause

getFunctionCallStackWithParameters() returns an array where [0] is the outermost active
function-argument context and the last element is the innermost. Arrow functions inherit the
parent scope's stack unchanged, so all outer entries are visible when the body is analysed.

For trim(backwardsCompatibleCall(..., fn() => deprecated_fn())), the stack inside the arrow
function body is:

[0] => [trim_reflection,                  trim_first_param       ]  ← only this is checked
[1] => [backwardsCompatibleCall_reflection, deprecatedCallable_param]  ← never reached

Current code:

[$function, $parameter] = $callStack[0];  // always the outer call, not backwardsCompatibleCall

Fix

Iterate the full stack instead of only checking index 0:

foreach ($scope->getFunctionCallStackWithParameters() as [$function, $parameter]) {
    if (!$function instanceof MethodReflection) {
        continue;
    }
    if ($function->getName() !== 'backwardsCompatibleCall'
        || $function->getDeclaringClass()->getName() !== DeprecationHelper::class
    ) {
        continue;
    }
    if ($parameter !== null && $parameter->getName() === 'deprecatedCallable') {
        return true;
    }
}
return false;

This correctly handles any nesting depth, e.g. trim(strrev(strtolower(backwardsCompatibleCall(...)))).

Real-world proof

The project_analysis pipeline runs
drupal-rector followed by upgrade_status/PHPStan on ~1,800 Drupal contrib modules. After rector
correctly wraps all theme_get_setting() calls in the alice theme, 5 false-positive errors
remain in the post-rector report — all on nested patterns:

  • Post-rector XML for alice
    — errors at lines 107, 114, 115, 135, 136 are all trim(backwardsCompatibleCall(...)) or
    array_filter([backwardsCompatibleCall(...)], 'trim') patterns
  • Rector patch for alice
    — confirms rector wrapped all 19 calls correctly; the remaining errors are PHPStan false positives,
    not rector misses

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.

1 participant