<?php
/**
* File Manager Advanced - PHP Debug Analyzer
*
* This class provides PHP code analysis using nikic/php-parser
* It's designed to be conflict-free with other file managers
*
* @package FileManagerAdvanced
* @since 1.0.0
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Load composer autoloader
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require_once __DIR__ . '/../vendor/autoload.php';
}
class FMA_PhpDebugAnalyzer {
/**
* Analyze PHP code and return debug information
*
* @param string $php_code The PHP code to analyze
* @param string $filename The filename for context
* @return array Debug analysis result
*/
public static function analyze($php_code, $filename = 'temp.php') {
$result = array(
'valid' => true,
'errors' => array(),
'debug_info' => array(),
'message' => ''
);
// Check if nikic/php-parser is available
if (!class_exists('PhpParser\ParserFactory')) {
$result['valid'] = false;
$result['message'] = 'PHP Parser library not available';
$result['errors'][] = array(
'line' => 0,
'message' => 'nikic/php-parser library not found',
'type' => 'error'
);
return $result;
}
try {
$parser = (new \PhpParser\ParserFactory())->createForNewestSupportedVersion();
$ast = $parser->parse($php_code);
if ($ast === null) {
$result['valid'] = false;
$result['message'] = 'PHP syntax error detected';
$result['errors'][] = array(
'line' => 0,
'message' => 'Invalid PHP syntax',
'type' => 'error'
);
return $result;
}
// Analyze the AST for debug information
$debug_info = self::extract_debug_info($ast, $php_code);
$result['debug_info'] = $debug_info;
$result['message'] = 'PHP code analyzed successfully';
} catch (\PhpParser\Error $e) {
$result['valid'] = false;
$result['message'] = 'PHP parse error detected';
// Get more accurate line number using fallback method
$line_number = self::get_accurate_line_number($e, $php_code);
$error_message = $e->getMessage();
$result['errors'][] = array(
'line' => $line_number,
'message' => $error_message,
'type' => 'error'
);
} catch (Exception $e) {
$result['valid'] = false;
$result['message'] = 'Error analyzing PHP code';
$result['errors'][] = array(
'line' => 0,
'message' => $e->getMessage(),
'type' => 'error'
);
}
return $result;
}
/**
* Extract debug information from PHP AST
*
* @param array $ast The parsed AST
* @param string $php_code The original PHP code
* @return array Debug information
*/
private static function extract_debug_info($ast, $php_code) {
$debug_info = array(
'functions' => array(),
'classes' => array(),
'variables' => array(),
'includes' => array(),
'debug_statements' => array(),
'complexity_score' => 0,
'line_count' => 0,
'statistics' => array(),
'suggestions' => array()
);
$visitor = new class($debug_info, $php_code) extends \PhpParser\NodeVisitorAbstract {
private $debug_info;
private $php_code;
private $line_count = 0;
public function __construct(&$debug_info, $php_code) {
$this->debug_info = &$debug_info;
$this->php_code = $php_code;
}
public function enterNode(\PhpParser\Node $node) {
// Count lines
if (isset($node->getAttributes()['startLine'])) {
$this->line_count = max($this->line_count, $node->getAttributes()['startLine']);
}
// Extract functions
if ($node instanceof \PhpParser\Node\Stmt\Function_) {
$this->debug_info['functions'][] = array(
'name' => $node->name->toString(),
'line' => $node->getLine(),
'params' => count($node->params),
'is_public' => true,
'visibility' => 'public'
);
}
// Extract class methods
if ($node instanceof \PhpParser\Node\Stmt\ClassMethod) {
$visibility = 'public';
if ($node->isPrivate()) $visibility = 'private';
if ($node->isProtected()) $visibility = 'protected';
$this->debug_info['functions'][] = array(
'name' => $node->name->toString(),
'line' => $node->getLine(),
'params' => count($node->params),
'is_public' => $visibility === 'public',
'visibility' => $visibility,
'is_method' => true
);
}
// Extract classes
if ($node instanceof \PhpParser\Node\Stmt\Class_) {
$this->debug_info['classes'][] = array(
'name' => $node->name->toString(),
'line' => $node->getLine(),
'methods' => count(array_filter($node->stmts, function($stmt) {
return $stmt instanceof \PhpParser\Node\Stmt\ClassMethod;
})),
'properties' => count(array_filter($node->stmts, function($stmt) {
return $stmt instanceof \PhpParser\Node\Stmt\Property;
}))
);
}
// Extract variables
if ($node instanceof \PhpParser\Node\Expr\Variable) {
$var_name = is_string($node->name) ? $node->name : 'dynamic';
if (!in_array($var_name, $this->debug_info['variables'])) {
$this->debug_info['variables'][] = $var_name;
}
}
// Extract includes
if ($node instanceof \PhpParser\Node\Expr\Include_) {
$this->debug_info['includes'][] = array(
'type' => $node->type,
'line' => $node->getLine()
);
}
// Extract debug statements
if ($node instanceof \PhpParser\Node\Expr\FuncCall) {
$func_name = $node->name->toString();
if (in_array($func_name, ['var_dump', 'print_r', 'var_export', 'debug_print_backtrace', 'error_log', 'error_reporting', 'ini_set'])) {
$this->debug_info['debug_statements'][] = array(
'function' => $func_name,
'line' => $node->getLine(),
'type' => 'debug'
);
}
}
// Extract potential issues
if ($node instanceof \PhpParser\Node\Expr\FuncCall) {
$func_name = $node->name->toString();
if (in_array($func_name, ['eval', 'exec', 'system', 'shell_exec', 'passthru'])) {
$this->debug_info['suggestions'][] = array(
'type' => 'warning',
'message' => 'Potentially dangerous function: ' . $func_name . '()',
'line' => $node->getLine(),
'suggestion' => 'Consider using safer alternatives'
);
}
}
// Calculate complexity (simple cyclomatic complexity)
if ($node instanceof \PhpParser\Node\Stmt\If_ ||
$node instanceof \PhpParser\Node\Stmt\For_ ||
$node instanceof \PhpParser\Node\Stmt\Foreach_ ||
$node instanceof \PhpParser\Node\Stmt\While_ ||
$node instanceof \PhpParser\Node\Stmt\Do_ ||
$node instanceof \PhpParser\Node\Stmt\Switch_ ||
$node instanceof \PhpParser\Node\Expr\BinaryOp) {
$this->debug_info['complexity_score']++;
}
}
public function afterTraverse(array $nodes) {
$this->debug_info['line_count'] = $this->line_count;
$this->debug_info['statistics'] = array(
'total_functions' => count($this->debug_info['functions']),
'total_classes' => count($this->debug_info['classes']),
'total_variables' => count($this->debug_info['variables']),
'total_includes' => count($this->debug_info['includes']),
'total_debug_statements' => count($this->debug_info['debug_statements']),
'complexity_score' => $this->debug_info['complexity_score'],
'line_count' => $this->line_count,
'file_size' => strlen($this->php_code)
);
}
};
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor($visitor);
$traverser->traverse($ast);
return $debug_info;
}
/**
* Get accurate line number for parse errors
*
* @param \PhpParser\Error $e The parse error
* @param string $php_code The PHP code
* @return int Accurate line number
*/
private static function get_accurate_line_number($e, $php_code) {
$line_number = $e->getLine();
$error_message = $e->getMessage();
// First try to extract from error message
if (preg_match('/on line (\d+)/', $error_message, $matches)) {
$reported_line = (int)$matches[1];
// For "unexpected '}'" errors, check if the previous line has missing semicolon
if (strpos($error_message, "unexpected '}'") !== false) {
$lines = explode("\n", $php_code);
if (isset($lines[$reported_line - 2])) { // Check line before the reported error
$prev_line = trim($lines[$reported_line - 2]);
// If previous line ends without semicolon and is not a control structure
if (!empty($prev_line) &&
!preg_match('/[;{}]\s*$/', $prev_line) &&
!preg_match('/^(if|for|while|foreach|switch|function|class|interface|trait)\s*\(/', $prev_line) &&
!preg_match('/^(else|elseif|case|default):/', $prev_line)) {
return $reported_line - 1; // Return the actual error line
}
}
}
return $reported_line;
}
// If that fails, try to find the error position in the code
if (preg_match('/at position (\d+)/', $error_message, $matches)) {
$position = (int)$matches[1];
$lines = explode("\n", substr($php_code, 0, $position));
return count($lines);
}
// Fallback: try to find syntax errors manually
$lines = explode("\n", $php_code);
$current_line = 1;
foreach ($lines as $line) {
$trimmed_line = trim($line);
// Skip empty lines and comments
if (empty($trimmed_line) || strpos($trimmed_line, '//') === 0 || strpos($trimmed_line, '/*') === 0) {
$current_line++;
continue;
}
// Check for missing semicolon (most common error)
if (preg_match('/^[^;{}]*[^;{}]\s*$/', $trimmed_line) &&
!preg_match('/^(if|for|while|foreach|switch|function|class|interface|trait)\s*\(/', $trimmed_line) &&
!preg_match('/^(else|elseif|case|default):/', $trimmed_line) &&
!preg_match('/^[{}]\s*$/', $trimmed_line) &&
!preg_match('/^\/\*/', $trimmed_line) &&
!preg_match('/^\*\//', $trimmed_line)) {
// This might be a missing semicolon
if (strpos($error_message, 'unexpected') !== false) {
return $current_line;
}
}
$current_line++;
}
// Return the original line number as fallback
return $line_number;
}
}