diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml index 2520f68f0..08e166fb9 100644 --- a/.github/workflows/php-test.yml +++ b/.github/workflows/php-test.yml @@ -112,7 +112,7 @@ jobs: if: ${{ matrix.coverage }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: - file: build/logs/*.xml + files: build/logs/*.xml flags: unit token: ${{ secrets.CODECOV_TOKEN }} @@ -158,6 +158,6 @@ jobs: if: ${{ matrix.coverage }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: - file: build/logs/*.xml + files: build/logs/*.xml flags: phpcs-sniffs token: ${{ secrets.CODECOV_TOKEN }} diff --git a/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php b/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php index 62be77f8e..5885de4f5 100644 --- a/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php @@ -400,8 +400,9 @@ private function is_ast_valid_for_direct_access( array $ast ) { Stmt\Enum_::class, ); - $has_assignments = false; - $has_returns = false; + $has_assignments = false; + $has_returns = false; + $has_structural_declaration = $this->has_structural_declaration( $ast ); foreach ( $ast as $node ) { $node_class = get_class( $node ); @@ -428,6 +429,10 @@ private function is_ast_valid_for_direct_access( array $ast ) { } if ( $node instanceof Stmt\Expression ) { + if ( $node->expr instanceof Expr\Include_ && $has_structural_declaration ) { + continue; + } + if ( $this->is_safe_expression( $node->expr ) ) { continue; } @@ -455,6 +460,33 @@ private function is_ast_valid_for_direct_access( array $ast ) { return true; } + /** + * Checks whether the AST contains structural declarations. + * + * @since n.e.x.t + * + * @param array $ast The parsed AST nodes. + * @return bool True if the AST contains class/interface/trait/enum declarations, false otherwise. + */ + private function has_structural_declaration( array $ast ) { + foreach ( $ast as $node ) { + if ( + $node instanceof Stmt\Class_ || + $node instanceof Stmt\Interface_ || + $node instanceof Stmt\Trait_ || + $node instanceof Stmt\Enum_ + ) { + return true; + } + + if ( $node instanceof Stmt\Namespace_ && ! empty( $node->stmts ) ) { + return $this->has_structural_declaration( $node->stmts ); + } + } + + return false; + } + /** * Checks if an expression is an asset file assignment (variable = array/string). * @@ -895,7 +927,7 @@ private function has_procedural_code( $contents ) { private function has_only_safe_function_calls( $contents ) { $safe_if_count = preg_match_all( '/if\s*\([^)]*(?:class_exists|function_exists|interface_exists|trait_exists|defined)\s*\(/i', $contents ); $return_count = preg_match_all( '/return\s*;/', $contents ); - $all_function_calls = preg_match_all( '/\b(?!class_exists|function_exists|interface_exists|trait_exists|defined|return|if|else|elseif|isset|empty|unset|array|list|echo|print)\w+\s*\(/i', $contents ); + $all_function_calls = preg_match_all( '/\b(?!class_exists|function_exists|interface_exists|trait_exists|defined|return|if|else|elseif|isset|empty|unset|array|list|echo|print|require|require_once|include|include_once)\w+\s*\(/i', $contents ); return $safe_if_count > 0 && $return_count >= $safe_if_count && 0 === $all_function_calls; } diff --git a/tests/phpunit/testdata/plugins/test-plugin-direct-file-access-without-errors/includes/require-once-class-only.php b/tests/phpunit/testdata/plugins/test-plugin-direct-file-access-without-errors/includes/require-once-class-only.php new file mode 100644 index 000000000..b27b5dfaa --- /dev/null +++ b/tests/phpunit/testdata/plugins/test-plugin-direct-file-access-without-errors/includes/require-once-class-only.php @@ -0,0 +1,18 @@ +assertArrayNotHasKey( 'includes/class-only-file.php', $errors ); $this->assertArrayNotHasKey( 'includes/namespace-class-only.php', $errors ); + $this->assertArrayNotHasKey( 'includes/require-once-class-only.php', $errors ); } /**