Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,26 @@ The `stubs/wordpress-override.php` file provides corrected type definitions for
stubFiles:
- vendor/wpsyntex/polylang-phpstan/stubs/wordpress-override.php
```

## Language switcher typing (Polylang 3.9+)

Polylang 3.9 replaced `PLL_Switcher::the_languages()` with `pll_the_languages()` and the internal `WP_Syntex\Polylang\Switcher\Switcher` class.

This extension provides dynamic return types for `pll_the_languages()`:

| `$args` | Return type |
|---|---|
| `raw => true` | `array<string, mixed>` |
| `echo => false` | `string` |
| default (`echo => true`) | `void` |

Direct usage of `WP_Syntex\Polylang\Switcher\Switcher` is typed from Polylang stubs or from Polylang source when you analyze the plugin itself.

### Deprecated `PLL_Switcher`

`PLL_Switcher` was removed in Polylang 3.9. When this extension is enabled, PHPStan reports:

- `polylang.deprecatedSwitcher` on `new PLL_Switcher()`
- `polylang.deprecatedSwitcherMethod` on `PLL_Switcher::the_languages()`

Migrate to `pll_the_languages()` or to `WP_Syntex\Polylang\Switcher\Switcher`.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"type": "library",
"require": {
"php": "^8.0",
"phpstan/phpstan": "^2.0",
"szepeviktor/phpstan-wordpress": "^2.0"
},
"require-dev": {
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9",
"wpsyntex/polylang-stubs": "dev-master"
},
Expand All @@ -18,6 +20,7 @@
}
},
"scripts": {
"test":"vendor/bin/phpunit -d memory_limit=1G"
"test": "vendor/bin/phpunit -d memory_limit=1G",
"stan": "vendor/bin/phpstan analyse src/GuessTypeFromSwitcherArgs.php src/TheLanguagesFunctionReturnTypeExtension.php --memory-limit=512M"
}
}
10 changes: 6 additions & 4 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ services:
class: WPSyntex\Polylang\PHPStan\LanguageReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: WPSyntex\Polylang\PHPStan\SwitcherClassReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: WPSyntex\Polylang\PHPStan\TheLanguagesFunctionReturnTypeExtension
tags:
Expand All @@ -23,13 +19,19 @@ services:
class: WPSyntex\Polylang\PHPStan\OptionsGetDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: WPSyntex\Polylang\PHPStan\Rules\DeprecatedPllSwitcherRule
tags:
- phpstan.rules.rule
includes:
- ../../szepeviktor/phpstan-wordpress/extension.neon
parameters:
bootstrapFiles:
- bootstrap.php
stubFiles:
- %rootDir%/../../php-stubs/wordpress-stubs/wordpress-stubs.php
scanFiles:
- stubs/pll-switcher-deprecated.php
dynamicConstantNames:
- POLYLANG_VERSION
- PLL_ADMIN
Expand Down
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: 9
paths:
- src/
127 changes: 127 additions & 0 deletions src/GuessTypeFromSwitcherArgs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

namespace WPSyntex\Polylang\PHPStan;

use PhpParser\Node\Arg;
use PHPStan\Analyser\Scope;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VoidType;

/**
* Infers pll_the_languages() return types from switcher arguments.
*
* Mirrors the runtime branches in polylang/src/api.php:
* raw truthy → array, echo false → string, otherwise → void.
*/
trait GuessTypeFromSwitcherArgs {
private function guessPllTheLanguagesReturnType( Arg $arg, Scope $scope ): Type {
$isRaw = $this->guessBooleanArg( $arg, $scope, 'raw', TrinaryLogic::createNo() );

if ( $isRaw->yes() ) {
return $this->getRawReturnType();
}

if ( $isRaw->maybe() ) {
return TypeCombinator::union(
$this->getRawReturnType(),
$this->guessNonRawReturnType( $arg, $scope )
);
}

return $this->guessNonRawReturnType( $arg, $scope );
}

private function guessNonRawReturnType( Arg $arg, Scope $scope ): Type {
// `echo` defaults to true: the markup is printed and nothing is returned.
$isEcho = $this->guessBooleanArg( $arg, $scope, 'echo', TrinaryLogic::createYes() );

if ( $isEcho->yes() ) {
return new VoidType();
}

if ( $isEcho->no() ) {
return new StringType();
}

return TypeCombinator::union( new VoidType(), new StringType() );
}

private function getRawReturnType(): Type {
return new ArrayType( new StringType(), new MixedType() );
}

private function guessBooleanArg( Arg $arg, Scope $scope, string $key, TrinaryLogic $whenAbsent ): TrinaryLogic {
$argsType = $scope->getType( $arg->value );
$keyType = new ConstantStringType( $key );

$hasKey = $argsType->hasOffsetValueType( $keyType );

if ( $hasKey->yes() ) {
return $argsType->getOffsetValueType( $keyType )->toBoolean()->isTrue();
}

if ( $hasKey->no() ) {
return $whenAbsent;
}

return $this->guessBooleanArgFromConstantArrays( $argsType, $key );
}

private function guessBooleanArgFromConstantArrays( Type $argsType, string $key ): TrinaryLogic {
$constantArrays = $argsType->getConstantArrays();

if ( [] === $constantArrays ) {
return TrinaryLogic::createMaybe();
}

$knownResult = null;

foreach ( $constantArrays as $constantArray ) {
$result = $this->guessBooleanArgFromConstantArray( $constantArray, $key );

if ( $result->maybe() ) {
continue;
}

if ( null === $knownResult ) {
$knownResult = $result;
continue;
}

if ( $knownResult->yes() !== $result->yes() ) {
return TrinaryLogic::createMaybe();
}
}

return $knownResult ?? TrinaryLogic::createMaybe();
}

private function guessBooleanArgFromConstantArray( ConstantArrayType $constantArray, string $key ): TrinaryLogic {
foreach ( $constantArray->getKeyTypes() as $index => $argKey ) {
if ( ! $this->isConstantStringKey( $argKey, $key ) ) {
continue;
}

return $constantArray->getValueTypes()[ $index ]->toBoolean()->isTrue();
}

return TrinaryLogic::createMaybe();
}

private function isConstantStringKey( Type $keyType, string $key ): bool {
foreach ( $keyType->getConstantStrings() as $constantString ) {
if ( $constantString->getValue() === $key ) {
return true;
}
}

return false;
}
}
76 changes: 0 additions & 76 deletions src/GuessTypeFromSwitcherAttributes.php

This file was deleted.

93 changes: 93 additions & 0 deletions src/Rules/DeprecatedPllSwitcherRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace WPSyntex\Polylang\PHPStan\Rules;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;

/**
* Reports usage of PLL_Switcher, removed in Polylang 3.9.
*
* @implements Rule<Expr>
*/
class DeprecatedPllSwitcherRule implements Rule {
private const CLASS_NAME = 'PLL_Switcher';

private const CLASS_MESSAGE = 'Class PLL_Switcher was removed in Polylang 3.9. Use pll_the_languages() or WP_Syntex\Polylang\Switcher\Switcher instead.';

private const METHOD_MESSAGE = 'Method PLL_Switcher::the_languages() was removed in Polylang 3.9. Use pll_the_languages() instead.';

public function getNodeType(): string {
return Expr::class;
}

public function processNode( Node $node, Scope $scope ): array {
if ( ! $node instanceof Expr ) {
return [];
}

if ( $node instanceof New_ ) {
return $this->processInstantiation( $node );
}

if ( $node instanceof MethodCall ) {
return $this->processMethodCall( $node, $scope );
}

return [];
}

/**
* @return list<RuleError>
*/
private function processInstantiation( New_ $node ): array {
if ( ! $node->class instanceof Name ) {
return [];
}

if ( self::CLASS_NAME !== $node->class->toString() ) {
return [];
}

return [
RuleErrorBuilder::message( self::CLASS_MESSAGE )
->line( $node->getStartLine() )
->identifier( 'polylang.deprecatedSwitcher' )
->build(),
];
}

/**
* @return list<RuleError>
*/
private function processMethodCall( MethodCall $node, Scope $scope ): array {
if ( ! $node->name instanceof Node\Identifier ) {
return [];
}

if ( 'the_languages' !== $node->name->toString() ) {
return [];
}

$calledOnType = $scope->getType( $node->var );

if ( ! ( new ObjectType( self::CLASS_NAME ) )->isSuperTypeOf( $calledOnType )->yes() ) {
return [];
}

return [
RuleErrorBuilder::message( self::METHOD_MESSAGE )
->line( $node->getStartLine() )
->identifier( 'polylang.deprecatedSwitcherMethod' )
->build(),
];
}
}
Loading
Loading