initial
This commit is contained in:
+322
@@ -0,0 +1,322 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user