Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #7123: Rules about get_subscribed_events #7124

Merged
merged 6 commits into from
Nov 28, 2024
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
],
"run-stan": "vendor/bin/phpstan analyze --memory-limit=2G --no-progress",
"run-stan-reset-baseline": "vendor/bin/phpstan analyze --memory-limit=2G --no-progress --generate-baseline",
"run-stan-test": "vendor/bin/phpstan analyze inc/Engine/Common/ExtractCSS/Subscriber.php --memory-limit=2G --no-progress --debug",
"install-codestandards": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run",
"phpcs": "phpcs --basepath=.",
"phpcs-changed": "./bin/phpcs-changed.sh",
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ parameters:
- %currentWorkingDirectory%/inc/3rd-party/
rules:
- WP_Rocket\Tests\phpstan\Rules\DiscourageApplyFilters
- WP_Rocket\Tests\phpstan\Rules\EnsureCallbackMethodsExistsInSubscribedEvents
153 changes: 153 additions & 0 deletions tests/phpstan/Rules/EnsureCallbackMethodsExistsInSubscribedEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

namespace WP_Rocket\Tests\phpstan\Rules;

use PhpParser\Node;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

class EnsureCallbackMethodsExistsInSubscribedEvents implements Rule {

/**
* Returns the type of the node that this rule is interested in.
*
* @return string The class name of the node type.
*/
public function getNodeType(): string {
return Return_::class;
}

/**
* Processes a node to ensure that callback methods exist in subscribed events.
*
* @param Node $node The node to process.
* @param Scope $scope The scope in which the node is being processed.
* @return array An array of errors found during the processing of the node.
*/
public function processNode( Node $node, Scope $scope ): array {
// Bail out early if the node is not a return statement with an expression.
if ( ! ( $node instanceof Return_ && $node->expr ) ) {
return [];
}

$function_name = $scope->getFunctionName();
// Bail out early if the method is not `get_subscribed_events`.
if ( 'get_subscribed_events' !== $function_name ) {
return [];
}

// Check if the return expression is an array.
if ( $node->expr instanceof Node\Expr\Array_ ) {
return $this->analyzeArray( $node->expr, $scope );
}

return [];
}

/**
* Analyzes the array structure returned by `get_subscribed_events`.
*
* @param Node\Expr\Array_ $array_expr The array expression node to analyze.
* @param Scope $scope The scope in which the array expression is being analyzed.
* @return array An array of errors found during the analysis of the array structure.
*/
private function analyzeArray( Node\Expr\Array_ $array_expr, Scope $scope ): array {
$errors = [];

foreach ( $array_expr->items as $item ) {
// Skip invalid array items.
if ( ! $item instanceof ArrayItem ) { // @phpstan-ignore-line PHPStan mess up with the type, and report a false positive error.
continue;
}

$method_value = $item->value; // @phpstan-ignore-line Because of the above issue, we need to ignore the error here as it thinks it's unreachable.

// Analyze the method value.
$errors = array_merge( $errors, $this->analyzeMethodValue( $method_value, $scope ) );
}

return $errors;
}

/**
* Analyzes the method value from the array structure.
*
* @param Node $method_value The method value node to analyze.
* @param Scope $scope The scope in which the method value is being analyzed.
* @return array An array of errors found during the analysis of the method value.
* @phpstan-ignore-next-line While running phpstan, it can't detect it's being used while in real it is.
*/
private function analyzeMethodValue( Node $method_value, Scope $scope ): array {
$errors = [];

if ( $method_value instanceof Node\Scalar\String_ ) {
// Simple structure: array('hook_name' => 'method_name').
return $this->checkIfMethodExistsInClass( $method_value->value, $scope, $method_value );
}

if ( $method_value instanceof Node\Expr\Array_ ) {
// More complex structures: array or nested array.
foreach ( $method_value->items as $sub_item ) {
if ( ! ( $sub_item instanceof ArrayItem ) ) { // @phpstan-ignore-line
continue;
}

// @phpstan-ignore-next-line While running phpstan, it can't detect it's being used while in real it is.
if ( $sub_item->value instanceof Node\Scalar\String_ ) {
// Handle string callback in nested array.
$errors = array_merge( $errors, $this->checkIfMethodExistsInClass( $sub_item->value->value, $scope, $sub_item->value ) );
} elseif ( $sub_item->value instanceof Node\Expr\Array_ ) {
// Recursively analyze nested arrays.
$errors = array_merge( $errors, $this->analyzeMethodValue( $sub_item->value, $scope ) );
}
}
}

return $errors;
}

/**
* Checks if a method exists in the class, its parent class, or its interfaces.
*
* @param string $method_name The name of the method to check.
* @param Scope $scope The scope in which the method is being checked.
* @param Node $node The node representing the method call.
* @return array An array of errors if the method does not exist, otherwise an empty array.
*/
public function checkIfMethodExistsInClass( string $method_name, Scope $scope, Node $node ): array {
$class_reflection = $scope->getClassReflection();

// Bail out early if the class reflection or method is found.
if ( $class_reflection && $class_reflection->hasMethod( $method_name ) ) {
return [];
}

$parent_class = $class_reflection ? $class_reflection->getParentClass() : null;
if ( $parent_class && $parent_class->hasMethod( $method_name ) ) {
return [];
}

foreach ( $class_reflection->getInterfaces() as $interface ) {
if ( $interface->hasMethod( $method_name ) ) {
return [];
}
}

// If the method doesn't exist, return an error.
$error_message = sprintf(
"The callback function '%s' declared within 'get_subscribed_events' does not exist in the class '%s'.",
$method_name,
$class_reflection ? $class_reflection->getName() : 'unknown'
);

return [
RuleErrorBuilder::message( $error_message )
->line( $node->getLine() ) // Add the line number.
Miraeld marked this conversation as resolved.
Show resolved Hide resolved
->identifier( 'callbackMethodNotFound' )
->build(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace WP_Rocket\Tests\phpstan\tests\Rules;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use WP_Rocket\Tests\phpstan\Rules\EnsureCallbackMethodsExistsInSubscribedEvents;

class EnsureCallbackMethodsExistsInSubscribedEventsTest extends RuleTestCase {

protected function getRule(): Rule {
return new EnsureCallbackMethodsExistsInSubscribedEvents();
}

public function testValidSubscriberShouldNotHaveErrors() {
$this->analyse([__DIR__ . '/../data/EnsureCallbackMethodsExistsInSubscribedEventsTest/valid.php'], [
]);
}

public function testMethodNotExistingShouldHaveErrors() {
$this->analyse([__DIR__ . '/../data/EnsureCallbackMethodsExistsInSubscribedEventsTest/not-existing.php'], [
[
"The callback function 'return_falses' declared within 'get_subscribed_events' does not exist in the class 'WP_Rocket\Engine\Admin\ActionSchedulerSubscriber'.",
19
],
[
"The callback function 'hide_pastdue_status_filterss' declared within 'get_subscribed_events' does not exist in the class 'WP_Rocket\Engine\Admin\ActionSchedulerSubscriber'.",
21
],
[
"The callback function 'hide_pastdue_status_filterss' declared within 'get_subscribed_events' does not exist in the class 'WP_Rocket\Engine\Admin\ActionSchedulerSubscriber'.",
22
]
]);
}

public function testComplexSyntaxNotExistingShouldHaveErrors() {
$this->analyse([__DIR__ . '/../data/EnsureCallbackMethodsExistsInSubscribedEventsTest/complex-syntax.php'], [
[
"The callback function 'exclude_inline_from_rucsss' declared within 'get_subscribed_events' does not exist in the class 'WP_Rocket\ThirdParty\Plugins\InlineRelatedPosts'.",
23
]
]);
}
}
3 changes: 3 additions & 0 deletions tests/phpstan/tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

require_once __DIR__ . '/../../../vendor/autoload.php';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace WP_Rocket\ThirdParty\Plugins;

use WP_Rocket\Event_Management\Subscriber_Interface;

/**
* Subscriber for compatibility with Inline Related Posts.
*/
class InlineRelatedPosts implements Subscriber_Interface {


/**
* Subscriber for Inline Related Posts.
*
* @return array
*/
public static function get_subscribed_events() {
if ( ! defined( 'IRP_PLUGIN_SLUG' ) ) {
return [];
}

return [ 'rocket_rucss_inline_content_exclusions' => 'exclude_inline_from_rucsss' ];
}

/**
* Exclude inline style from RUCSS.
*
* @param array $excluded excluded css.
* @return array
*/
public function exclude_inline_from_rucss( $excluded ) {
$excluded[] = '.centered-text-area';
$excluded[] = '.ctaText';

return $excluded;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace WP_Rocket\Engine\Admin;

use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\ThirdParty\ReturnTypesTrait;

class ActionSchedulerSubscriber implements Subscriber_Interface {

use ReturnTypesTrait;

/**
* Return an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
return [
'action_scheduler_check_pastdue_actions' => 'return_falses',
'action_scheduler_extra_action_counts' => [
['hide_pastdue_status_filterss'],
['hide_pastdue_status_filterss', 10, 3],
],
];
}

/**
* Hide past-due from status filter in Action Scheduler tools page.
*
* @param array $extra_actions Array with format action_count_identifier => action count.
*
* @return array
*/
public function hide_pastdue_status_filter( array $extra_actions ) {
if ( ! isset( $extra_actions['past-due'] ) ) {
return $extra_actions;
}

unset( $extra_actions['past-due'] );
return $extra_actions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace WP_Rocket\Engine\Admin;

use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\ThirdParty\ReturnTypesTrait;

class ActionSchedulerSubscriber implements Subscriber_Interface {

use ReturnTypesTrait;

/**
* Return an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
return [
'action_scheduler_check_pastdue_actions' => 'return_false',
'action_scheduler_extra_action_counts' => [
['hide_pastdue_status_filter'],
['hide_pastdue_status_filter', 10, 3],
],
];
}

/**
* Hide past-due from status filter in Action Scheduler tools page.
*
* @param array $extra_actions Array with format action_count_identifier => action count.
*
* @return array
*/
public function hide_pastdue_status_filter( array $extra_actions ) {
if ( ! isset( $extra_actions['past-due'] ) ) {
return $extra_actions;
}

unset( $extra_actions['past-due'] );
return $extra_actions;
}
}
27 changes: 27 additions & 0 deletions tests/phpstan/tests/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="bootstrap.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>Rules</directory>
</testsuite>
</testsuites>

<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">Rules</directory>
</include>
</coverage>
</phpunit>
Loading