Files
shopify-ai-backup/scripts/check-duplicate-classes.php

1314 lines
58 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env php
<?php
/**
* Comprehensive PHP/WordPress Plugin Static Analyzer
* Detects runtime errors, duplicate declarations, missing dependencies, and code issues
* Optimized for minimal memory usage via streaming token parsing
*/
if ($argv[1] ?? null === '--help' || $argv[1] ?? null === '-h') {
echo "Usage: {$argv[0]} /path/to/plugin [options]\n";
echo "\nOptions:\n";
echo " --verbose Show detailed output\n";
echo " --strict Enable stricter checks (may have false positives)\n";
echo " --no-cache Skip caching (useful for CI/CD)\n";
echo " --quiet Suppress informational messages\n";
exit(0);
}
if ($argc < 2) {
fwrite(STDERR, "Usage: {$argv[0]} /path/to/plugin\n");
exit(1);
}
$pluginDir = rtrim($argv[1], '/');
$verbose = in_array('--verbose', $argv);
$strict = in_array('--strict', $argv);
$quiet = in_array('--quiet', $argv);
if (!is_dir($pluginDir)) {
fwrite(STDERR, "Error: Directory not found: {$pluginDir}\n");
exit(1);
}
$stats = [
'files' => 0,
'classes' => 0,
'interfaces' => 0,
'traits' => 0,
'functions' => 0,
'constants' => 0,
'includes' => 0
];
$issues = [
'duplicates' => [],
'missing_includes' => [],
'missing_classes' => [],
'type_hints' => [],
'circular_deps' => [],
'this_usage' => [],
'static_calls' => [],
'file_paths' => [],
'activation' => [],
'undefined_functions' => [],
'early_function_calls' => [],
'undefined_arrays' => [],
'undefined_array_keys' => [],
'css_overlaps' => []
];
$declarations = [
'classes' => [],
'interfaces' => [],
'traits' => [],
'functions' => [],
'constants' => [],
'includes' => []
];
$definedInFile = [];
$functionImplementations = [];
$methodImplementations = [];
$typeHints = [];
$thisUsage = [];
$staticCalls = [];
// Track functions defined and their line numbers
$functionDefinitions = [];
$functionCalls = [];
$variableAssignments = [];
$arrayAccesses = [];
$cssFiles = [];
$variableDeclarations = [];
$arrayKeyAccesses = [];
$arrayKeyDefinitions = [];
$unsafeArrayAccesses = [];
$excludeDirs = ['/vendor/', '/node_modules/', '/.git/', '/cache/', '/logs/', '/tmp/'];
function shouldExclude(string $path, array $excludeDirs): bool {
foreach ($excludeDirs as $dir) {
if (strpos($path, $dir) !== false) {
return true;
}
}
return false;
}
function normalizePath(string $path, string $baseDir): string {
$path = str_replace(['/', '\\'], '/', $path);
$baseDir = rtrim(str_replace(['/', '\\'], '/', $baseDir), '/');
if (preg_match('/^\.\.?[\/\\\\]/', $path) || !preg_match('/^[a-zA-Z]:/', $path)) {
$fullPath = $baseDir . '/' . $path;
$fullPath = preg_replace('/\/+/', '/', $fullPath);
return $fullPath;
}
return $path;
}
function findAbsoluteFile(string $includePath, string $currentFile, string $baseDir): ?string {
$currentDir = dirname($currentFile);
$possiblePaths = [
$currentDir . '/' . $includePath,
$currentDir . '/' . $includePath . '.php',
$baseDir . '/' . $includePath,
$baseDir . '/' . $includePath . '.php',
$baseDir . '/includes/' . $includePath,
$baseDir . '/includes/' . $includePath . '.php',
];
foreach ($possiblePaths as $path) {
if (file_exists($path)) {
return realpath($path) ?: $path;
}
}
return null;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($pluginDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$filePath = $file->getPathname();
if (shouldExclude($filePath, $excludeDirs)) {
continue;
}
$stats['files']++;
$relativePath = str_replace($pluginDir . '/', '', $filePath);
$definedInFile[$relativePath] = [];
$content = file_get_contents($filePath, false, null, 0, 2097152);
if ($content === false) {
continue;
}
$tokens = token_get_all($content);
$lineNumber = 1;
$namespaces = [''];
$currentNS = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
if ($token[2] !== $lineNumber) {
$lineNumber = $token[2];
}
switch ($token[0]) {
case T_NAMESPACE:
$nsTokens = [];
$j = $i + 1;
while (isset($tokens[$j]) && ($tokens[$j] !== ';' && $tokens[$j] !== '{' && $tokens[$j] !== '=')) {
if (is_array($tokens[$j]) && in_array($tokens[$j][0], [T_STRING, T_NS_SEPARATOR])) {
$nsTokens[] = $tokens[$j][1];
}
$j++;
}
$currentNS = implode('', $nsTokens);
$namespaces[count($namespaces) - 1] = $currentNS;
break;
case T_CLASS:
$className = extractDeclaration($tokens, $i, $namespaces, 'class');
if ($className) {
$stats['classes']++;
$fullName = $currentNS ? $currentNS . '\\' . $className : $className;
$declarations['classes'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
$definedInFile[$relativePath][] = ['type' => 'class', 'name' => $fullName];
}
break;
case T_INTERFACE:
$interfaceName = extractDeclaration($tokens, $i, $namespaces, 'interface');
if ($interfaceName) {
$stats['interfaces']++;
$fullName = $currentNS ? $currentNS . '\\' . $interfaceName : $interfaceName;
$declarations['interfaces'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
$definedInFile[$relativePath][] = ['type' => 'interface', 'name' => $fullName];
}
break;
case T_TRAIT:
$traitName = extractDeclaration($tokens, $i, $namespaces, 'trait');
if ($traitName) {
$stats['traits']++;
$fullName = $currentNS ? $currentNS . '\\' . $traitName : $traitName;
$declarations['traits'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
$definedInFile[$relativePath][] = ['type' => 'trait', 'name' => $fullName];
}
break;
case T_FUNCTION:
$funcName = extractDeclaration($tokens, $i, $namespaces, 'function');
if ($funcName) {
$stats['functions']++;
$fullName = $currentNS ? $currentNS . '\\' . $funcName : $funcName;
if (!isset($functionImplementations[$fullName])) {
$functionImplementations[$fullName] = [];
}
$functionImplementations[$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
$definedInFile[$relativePath][] = ['type' => 'function', 'name' => $fullName];
// Track function definition for early call detection
if (!isset($functionDefinitions[$relativePath])) {
$functionDefinitions[$relativePath] = [];
}
$functionDefinitions[$relativePath][$funcName] = $lineNumber;
}
break;
case T_CONST:
$constName = extractConstantDeclaration($tokens, $i);
if ($constName) {
$stats['constants']++;
$fullName = $currentNS ? $currentNS . '\\' . $constName : $constName;
if (!isset($declarations['constants'][$fullName])) {
$declarations['constants'][$fullName] = [];
}
$declarations['constants'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
$definedInFile[$relativePath][] = ['type' => 'constant', 'name' => $fullName];
}
break;
case T_NEW:
$className = extractAfterToken($tokens, $i);
if ($className && !isInternalClass($className)) {
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'new'];
}
break;
case T_INSTANCEOF:
$className = extractAfterToken($tokens, $i);
if ($className && !isInternalClass($className)) {
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'instanceof'];
}
break;
case T_CATCH:
$className = extractCatchClass($tokens, $i);
if ($className && !isInternalClass($className)) {
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'catch'];
}
break;
case T_EXTENDS:
$className = extractAfterToken($tokens, $i);
if ($className && !isInternalClass($className)) {
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'extends'];
}
break;
case T_IMPLEMENTS:
$className = extractAfterToken($tokens, $i);
if ($className && !isInternalClass($className)) {
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'implements'];
}
break;
case T_INCLUDE:
case T_INCLUDE_ONCE:
case T_REQUIRE:
case T_REQUIRE_ONCE:
$includePath = extractIncludePath($tokens, $i);
if ($includePath) {
$stats['includes']++;
$fullPath = findAbsoluteFile($includePath, $filePath, $pluginDir);
$declarations['includes'][] = [
'path' => $includePath,
'file' => $relativePath,
'line' => $lineNumber,
'resolved' => $fullPath
];
if (!$fullPath || !file_exists($fullPath)) {
$issues['missing_includes'][] = [
'path' => $includePath,
'file' => $relativePath,
'line' => $lineNumber
];
}
}
break;
case T_STRING:
// Track function calls
if (isset($tokens[$i + 1]) && $tokens[$i + 1] === '(') {
$funcName = $token[1];
// Skip if it's a keyword or internal function
if (!in_array(strtolower($funcName), ['if', 'while', 'for', 'foreach', 'switch', 'return', 'echo', 'print', 'array', 'list', 'isset', 'empty', 'unset', 'die', 'exit', 'parent', 'self', 'static'])) {
if (!isset($functionCalls[$relativePath])) {
$functionCalls[$relativePath] = [];
}
$functionCalls[$relativePath][] = [
'name' => $funcName,
'line' => $lineNumber
];
}
}
break;
case T_VARIABLE:
$varName = $token[1];
// Track variable assignments for array type inference
$prevNonWhitespace = $i - 1;
while ($prevNonWhitespace >= 0 && (is_array($tokens[$prevNonWhitespace]) && $tokens[$prevNonWhitespace][0] === T_WHITESPACE)) {
$prevNonWhitespace--;
}
// Check if this is a variable assignment
if ($prevNonWhitespace >= 0 && is_array($tokens[$prevNonWhitespace]) && $tokens[$prevNonWhitespace][0] === T_VARIABLE) {
if (!isset($variableDeclarations[$relativePath])) {
$variableDeclarations[$relativePath] = [];
}
$variableDeclarations[$relativePath][] = [
'name' => $varName,
'line' => $lineNumber,
'context' => 'assignment'
];
}
// Track array access patterns: $array['key'] or $array[$key]
$nextToken = $i + 1;
while (isset($tokens[$nextToken]) && is_array($tokens[$nextToken]) && $tokens[$nextToken][0] === T_WHITESPACE) {
$nextToken++;
}
if (isset($tokens[$nextToken]) && $tokens[$nextToken] === '[') {
if (!isset($arrayKeyAccesses[$relativePath])) {
$arrayKeyAccesses[$relativePath] = [];
}
$arrayKeyAccesses[$relativePath][] = [
'variable' => $varName,
'line' => $lineNumber
];
}
break;
}
} elseif ($token === '{' || $token === '}') {
$lineNumber++;
} elseif ($token === "\n") {
$lineNumber++;
}
}
$definedInFile[$relativePath] = array_unique($definedInFile[$relativePath], SORT_REGULAR);
}
function extractDeclaration(array $tokens, int $startIndex, array $namespaces, string $type): ?string {
$nextIndex = $startIndex + 1;
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
$nextIndex++;
}
if (!isset($tokens[$nextIndex]) || !is_array($tokens[$nextIndex]) || $tokens[$nextIndex][0] !== T_STRING) {
return null;
}
return $tokens[$nextIndex][1];
}
function extractConstantDeclaration(array $tokens, int $startIndex): ?string {
$nextIndex = $startIndex + 1;
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
$nextIndex++;
}
if (!isset($tokens[$nextIndex]) || !is_array($tokens[$nextIndex]) || $tokens[$nextIndex][0] !== T_STRING) {
return null;
}
return $tokens[$nextIndex][1];
}
function extractAfterToken(array $tokens, int $startIndex): ?string {
$nextIndex = $startIndex + 1;
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
$nextIndex++;
}
if (!isset($tokens[$nextIndex])) {
return null;
}
if (is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_STRING) {
return $tokens[$nextIndex][1];
}
return null;
}
function extractCatchClass(array $tokens, int $startIndex): ?string {
$j = $startIndex + 1;
$foundParen = false;
while (isset($tokens[$j])) {
if ($tokens[$j] === '(') {
$foundParen = true;
} elseif ($foundParen && is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
return $tokens[$j][1];
} elseif ($foundParen && $tokens[$j] === ')') {
break;
}
$j++;
}
return null;
}
function extractIncludePath(array $tokens, int $startIndex): ?string {
$pathTokens = [];
$j = $startIndex + 1;
while (isset($tokens[$j]) && $tokens[$j] !== ';') {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_ENCAPSED_AND_WHITESPACE) {
$pathTokens[] = $tokens[$j][1];
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_CONSTANT_ENCAPSED_STRING) {
$pathTokens[] = trim($tokens[$j][1], '"\'');
} elseif ($tokens[$j] === "\n") {
break;
}
$j++;
}
return $pathTokens ? implode('', $pathTokens) : null;
}
function isInternalClass(string $className): bool {
$internalClasses = [
'Exception', 'Error', 'Throwable', 'ArgumentCountError', 'ArithmeticError',
'AssertionError', 'DivisionByZeroError', 'ErrorException', 'CompileError',
'ParseError', 'TypeError', 'UnhandledMatchError', 'ValueError',
'DateTime', 'DateTimeImmutable', 'DateInterval', 'DatePeriod',
'DOMDocument', 'DOMNode', 'DOMElement', 'DOMNodeList',
'PDO', 'PDOStatement', 'PDOException',
'mysqli', 'mysqli_result', 'mysqli_stmt', 'mysqli_sql_exception',
'ArrayObject', 'ArrayIterator', 'IteratorIterator', 'Traversable',
'Countable', 'Serializable', 'JsonSerializable',
'Stringable', 'Stringable', 'UnitEnum', 'BackedEnum',
'Closure', 'Generator', 'Fiber', 'WeakReference',
'SplFileInfo', 'SplFileObject', 'SplTempFileObject', 'SplDoublyLinkedList',
'SplQueue', 'SplStack', 'SplHeap', 'SplMinHeap', 'SplMaxHeap',
'SplPriorityQueue', 'SplFixedArray', 'SplObjectStorage',
'RegexIterator', 'RecursiveRegexIterator', 'RecursiveIteratorIterator',
'RecursiveDirectoryIterator', 'RecursiveIterator', 'RecursiveTreeIterator',
'DirectoryIterator', 'FilesystemIterator', 'GlobIterator',
'SimpleXMLElement', 'SimpleXMLIterator',
'SoapClient', 'SoapServer', 'SoapFault',
'XMLReader', 'XMLWriter',
'ZipArchive', ' PharData', 'PharFileInfo', 'PharException',
'Laminas\\Escaper\\Escaper', 'Psr\\Container\\ContainerInterface',
'WP_Error', 'WP_Post', 'WP_User', 'WP_Query', 'WP_Term', 'WP_Object_Cache',
'WP_Theme', 'WP_Screen', 'WP_List_Table', 'WP_Comment_Query',
'WC_Product', 'WC_Order', 'WC_Customer', 'WC_Cart', 'WC_Session',
'WP_REST_Request', 'WP_REST_Response', 'WP_REST_Server',
'WP_Block_Editor_Context', 'WP_Block_Type', 'WP_Block', 'WP_Block_List'
];
$shortName = substr($className, strrpos($className, '\\') + 1);
return in_array($className, $internalClasses) || in_array($shortName, $internalClasses);
}
function isInternalFunction(string $funcName): bool {
$internalFuncs = [
'strlen', 'strcmp', 'strncmp', 'strcasecmp', 'strncasecmp', 'strpos', 'stripos',
'strrpos', 'strripos', 'strstr', 'stristr', 'strrchr', 'substr', 'str_replace',
'str_ireplace', 'strtr', 'trim', 'ltrim', 'rtrim', 'str_pad', 'str_repeat',
'implode', 'explode', 'join', 'split', 'htmlspecialchars', 'htmlentities',
'htmlspecialchars_decode', 'html_entity_decode', 'addslashes', 'stripslashes',
'stripcslashes', 'strip_tags', 'strval', 'intval', 'floatval', 'doubleval',
'boolval', 'settype', 'gettype', 'is_array', 'is_bool', 'is_float', 'is_int',
'is_null', 'is_numeric', 'is_string', 'is_object', 'is_resource', 'is_scalar',
'isset', 'empty', 'unset', 'defined', 'define', 'constant', 'time', 'mktime',
'gmmktime', 'date', 'gmdate', 'date_default_timezone_set', 'date_default_timezone_get',
'idate', 'getdate', 'localtime', 'checkdate', 'strtotime', 'strftime', 'gmstrftime',
'array', 'array_key_exists', 'array_keys', 'array_values', 'array_count_values',
'array_push', 'array_pop', 'array_shift', 'array_unshift', 'array_merge',
'array_merge_recursive', 'array_replace', 'array_replace_recursive', 'array_slice',
'array_splice', 'array_intersect', 'array_intersect_key', 'array_intersect_ukey',
'array_uintersect', 'array_diff', 'array_diff_key', 'array_diff_ukey',
'array_udiff', 'array_filter', 'array_map', 'array_reduce', 'array_walk',
'array_walk_recursive', 'sort', 'rsort', 'asort', 'arsort', 'ksort', 'krsort',
'natsort', 'natcasesort', 'usort', 'uasort', 'uksort', 'shuffle', 'array_sum',
'array_product', 'array_reduce', 'array_reverse', 'array_pad', 'array_flip',
'array_change_key_case', 'array_chunk', 'array_combine', 'array_column',
'preg_match', 'preg_match_all', 'preg_replace', 'preg_replace_callback',
'preg_split', 'preg_quote', 'preg_grep', 'preg_last_error', 'preg_last_error_msg',
'mb_strlen', 'mb_strpos', 'mb_strrpos', 'mb_stripos', 'mb_strripos', 'mb_substr',
'mb_substr_count', 'mb_strtolower', 'mb_strtoupper', 'mb_ucwords', 'mb_convert_case',
'json_encode', 'json_decode', 'json_last_error', 'json_last_error_msg',
'json_validate', 'serialize', 'unserialize', 'var_export', 'var_dump', 'print_r',
'debug_zval_dump', 'debug_backtrace', 'debug_print_backtrace', 'error_reporting',
'error_log', 'error_get_last', 'trigger_error', 'user_error', 'set_error_handler',
'restore_error_handler', 'set_exception_handler', 'restore_exception_handler',
'header_remove', 'headers_list', 'headers_sent', 'header', 'http_response_code',
'connection_aborted', 'connection_status', 'ignore_user_abort', 'set_time_limit',
'get_magic_quotes_gpc', 'get_magic_quotes_runtime', 'get_loaded_extensions',
'extension_loaded', 'get_defined_functions', 'get_defined_constants', 'get_defined_vars',
'func_num_args', 'func_get_arg', 'func_get_args', 'create_function', 'call_user_func',
'call_user_func_array', 'forward_static_call', 'forward_static_call_array',
'register_shutdown_function', 'register_tick_function', 'unregister_tick_function',
'is_callable', 'method_exists', 'property_exists', 'class_exists', 'interface_exists',
'trait_exists', 'function_exists', 'class_alias', 'get_declared_classes',
'get_declared_interfaces', 'get_declared_traits', 'get_defined_functions',
'get_defined_constants', 'gc_enabled', 'gc_collect_cycles', 'gc_status',
'memory_get_usage', 'memory_get_peak_usage', 'memory_reset_peak_usage',
'sys_getloadavg', 'sys_get_temp_dir', 'realpath_cache_size', 'realpath_cache_get',
'opcache_get_status', 'opcache_compile_file', 'opcache_is_script_cached',
'version_compare', 'php_uname', 'phpversion', 'phpinfo', 'phpcredits',
'php_sapi_name', 'php_ini_loaded_file', 'php_ini_scanned_files',
'rand', 'srand', 'mt_rand', 'mt_srand', 'random_int', 'random_bytes',
'bin2hex', 'hex2bin', 'pack', 'unpack', 'md5', 'md5_file', 'sha1', 'sha1_file',
'hash', 'hash_file', 'hash_hmac', 'hash_hmac_file', 'hash_algos', 'hash_init',
'hash_update', 'hash_update_stream', 'hash_final', 'password_hash',
'password_verify', 'password_needs_rehash', 'password_get_info',
'base64_encode', 'base64_decode', 'convert_uuencode', 'convert_uudecode',
'addcslashes', 'stripcslashes', 'quoted_printable_encode', 'quoted_printable_decode',
'str_rot13', 'stream_context_create', 'stream_context_get_params',
'stream_context_get_default', 'stream_context_set_option', 'stream_context_set_params',
'stream_filter_prepend', 'stream_filter_append', 'stream_filter_remove',
'stream_socket_client', 'stream_socket_server', 'stream_socket_accept',
'stream_socket_get_name', 'stream_socket_recvfrom', 'stream_socket_sendto',
'stream_get_contents', 'stream_get_line', 'stream_get_meta_data', 'stream_get_transports',
'stream_get_wrappers', 'stream_resolve_include_path', 'stream_wrapper_register',
'stream_wrapper_unregister', 'stream_wrapper_restore', 'file_get_contents',
'file_put_contents', 'file', 'fopen', 'fclose', 'fread', 'fwrite', 'fgets',
'fgetss', 'file', 'readfile', 'fscanf', 'fseek', 'ftell', 'rewind', 'feof',
'fflush', 'ftruncate', 'flock', 'fnmatch', 'glob', 'is_file', 'is_dir', 'is_link',
'is_executable', 'is_readable', 'is_writable', 'is_uploaded_file', 'tempnam',
'tmpfile', 'realpath', 'dirname', 'basename', 'pathinfo', 'parse_url', 'urldecode',
'urlencode', 'rawurlencode', 'rawurldecode', 'http_build_query', 'parse_str',
'str_getcsv', 'fputcsv', 'fgetcsv', 'strtok', 'chunk_split', 'nl2br',
'filter_var', 'filter_input', 'filter_list', 'filter_id', 'filter_has_var',
'hash_equals', 'crypt', 'wordwrap', 'count_chars', 'similar_text', 'levenshtein',
'soundex', 'metaphone', 'abs', 'ceil', 'floor', 'round', 'max', 'min', 'pow',
'sqrt', 'log', 'log10', 'log1p', 'exp', 'expm1', 'pi', 'is_nan', 'is_finite',
'is_infinite', 'hypot', 'deg2rad', 'rad2deg', 'intdiv', 'fmod', 'intdiv',
'wp_die', 'wp_enqueue_script', 'wp_enqueue_style', 'wp_register_script',
'wp_register_style', 'wp_add_inline_script', 'wp_add_inline_style',
'wp_localize_script', 'wp_scripts', 'wp_styles', 'get_option', 'update_option',
'add_option', 'delete_option', 'get_post_meta', 'update_post_meta', 'delete_post_meta',
'get_user_meta', 'update_user_meta', 'delete_user_meta', 'get_term_meta',
'update_term_meta', 'delete_term_meta', 'get_posts', 'get_post', 'wp_insert_post',
'wp_update_post', 'wp_delete_post', 'get_pages', 'get_page_by_path',
'wp_get_current_user', 'wp_get_current_user', 'wp_get_current_user',
'current_user_can', 'user_can', 'is_user_logged_in', 'wp_logout', 'wp_login',
'wp_authenticate', 'sanitize_text_field', 'sanitize_email', 'sanitize_title',
'sanitize_file_name', 'esc_html', 'esc_attr', 'esc_url', 'esc_js', 'esc_textarea',
'wp_kses', 'wp_kses_post', 'wp_filter_post_kses', 'wp_kses_hair',
'add_action', 'do_action', 'do_action_ref_array', 'has_action', 'remove_action',
'remove_all_actions', 'doing_action', 'add_filter', 'apply_filters',
'apply_filters_ref_array', 'has_filter', 'remove_filter', 'remove_all_filters',
'doing_filter', 'current_action', 'current_filter',
'add_shortcode', 'remove_shortcode', 'remove_all_shortcodes', 'shortcode_atts',
'do_shortcode', 'strip_shortcodes', 'has_shortcode',
'wp_remote_get', 'wp_remote_post', 'wp_remote_head', 'wp_remote_request',
'wp_remote_retrieve_body', 'wp_remote_retrieve_headers', 'wp_remote_retrieve_response_code',
'wp_remote_retrieve_response_message', 'wp_remote_post', 'wp_remote_get',
'is_wp_error', 'is_admin', 'is_front_page', 'is_home', 'is_single', 'is_page',
'is_singular', 'is_archive', 'is_search', 'is_404', 'is_user_logged_in',
'wp_verify_nonce', 'wp_create_nonce', 'wp_nonce_field', 'wp_nonce_url',
'check_admin_referer', 'check_ajax_referer', 'wp_salt', 'wp_hash',
'wp_generate_password', 'wp_rand', 'wp_kses_normalize_entities', 'wp_kses_bad_protocol',
'wp_kses_array_lc', 'wp_iso_descr', 'wp_kses_split',
'WC', 'wc_get_product', 'wc_get_products', 'wc_get_order', 'wc_get_orders',
'wc_add_order_item', 'wc_update_order_item', 'wc_delete_order_item',
'wc_add_order_item_meta', 'wc_delete_order_item_meta',
'wc_get_customer', 'wc_update_customer', 'wc_add_customer_note',
'wc_create_nonce', 'wc_rest_prepare_product', 'wc_rest_prepare_order'
];
$shortName = strtolower($funcName);
$parts = explode('\\', $funcName);
$shortName = strtolower(end($parts));
return in_array(strtolower($funcName), $internalFuncs) || in_array($shortName, $internalFuncs);
}
$foundIssues = false;
function reportIssues(array $issues, string $type): bool {
global $foundIssues, $verbose;
if (empty($issues)) {
return false;
}
$foundIssues = true;
$unique = [];
foreach ($issues as $issue) {
$key = $issue['name'] ?? $issue['path'] ?? json_encode($issue);
if (!isset($unique[$key])) {
$unique[$key] = $issue;
}
}
foreach ($unique as $issue) {
if (isset($issue['path'])) {
echo "DUPLICATE {$type}: '{$issue['path']}' declared multiple times:\n";
foreach ($issue['locations'] ?? [] as $loc) {
echo " {$loc['file']}:{$loc['line']}\n";
}
} elseif (isset($issue['name'])) {
echo "MISSING {$type}: '{$issue['name']}' referenced but not declared:\n";
echo " {$issue['file']}:{$issue['line']}\n";
} else {
echo "ISSUE {$type}: " . json_encode($issue) . "\n";
}
}
return true;
}
if ($verbose) {
echo "Scanned {$stats['files']} files\n";
echo "Found {$stats['classes']} classes, {$stats['interfaces']} interfaces, ";
echo "{$stats['traits']} traits, {$stats['functions']} functions, {$stats['constants']} constants\n";
echo "Found {$stats['includes']} include/require statements\n\n";
}
$anyIssues = false;
foreach ($declarations as $type => $decls) {
foreach ($decls as $name => $locations) {
if (count($locations) > 1) {
$anyIssues = true;
echo "DUPLICATE {$type}: '{$name}' declared in " . count($locations) . " locations:\n";
foreach ($locations as $loc) {
echo " {$loc['file']}:{$loc['line']}\n";
}
}
}
}
foreach ($typeHints as $className => $usages) {
$declared = isset($declarations['classes'][$className]) ||
isset($declarations['interfaces'][$className]) ||
isset($declarations['traits'][$className]);
if (!$declared && !isInternalClass($className)) {
$anyIssues = true;
$unique = [];
foreach ($usages as $usage) {
$key = "{$usage['file']}:{$usage['line']}";
if (!isset($unique[$key])) {
$unique[$key] = $usage;
}
}
echo "MISSING CLASS/TYPE: '{$className}' used but not declared:\n";
foreach ($unique as $usage) {
echo " {$usage['file']}:{$usage['line']} ({$usage['usage']})\n";
}
}
}
foreach ($iterator as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$filePath = $file->getPathname();
if (shouldExclude($filePath, $excludeDirs)) {
continue;
}
$content = file_get_contents($filePath);
if ($content === false) {
continue;
}
$lines = explode("\n", $content);
$relativePath = str_replace($pluginDir . '/', '', $filePath);
$activationHooks = [];
foreach ($lines as $lineNum => $line) {
$lineNum += 1;
if (preg_match('/register_activation_hook\s*\(/i', $line)) {
$activationHooks[] = [
'line' => $lineNum,
'content' => $line
];
}
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*[\'\"]([\/]|Windows|C:|D:|E:)/i', $line)) {
if (!preg_match('/ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL/i', $line)) {
$issues['file_paths'][] = [
'type' => 'hardcoded_absolute_path',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Hardcoded absolute path detected. Use WordPress path functions.'
];
}
}
if (preg_match('/\$[a-zA-Z_]+\s*\.\s*[\'\"]([\/\\]?)[a-zA-Z_]/', $line)) {
if (!preg_match('/DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory|dirname\(realpath/', $line)) {
$issues['file_paths'][] = [
'type' => 'missing_separator',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Path concatenation without proper directory separator.'
];
}
}
if (preg_match('/(plugin_dir_path|plugin_dir_url|template_directory|get_template_directory).*__FILE__/', $line) && !preg_match('/__FILE__.*dirname/', $line)) {
$issues['file_paths'][] = [
'type' => 'incorrect_file_usage',
'file' => $relativePath,
'line' => $lineNum,
'message' => '__FILE__ returns the file path. Use dirname(__FILE__) for directory paths.'
];
}
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\$[_A-Z]+|\$_(GET|POST|REQUEST|COOKIE))/', $line)) {
$issues['file_paths'][] = [
'type' => 'path_traversal_risk',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Direct user input used in file operation. Must validate and sanitize.'
];
}
if (preg_match('/(include|require)\s*\(\s*[\'\"][^\'\"]*\.(php|inc)/', $line)) {
if (!preg_match('/plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory|dirname\(__FILE__\)|ABSPATH/', $line)) {
$issues['file_paths'][] = [
'type' => 'missing_wp_path_function',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Use plugin_dir_path(), template_directory(), or similar WP functions.'
];
}
}
if (preg_match('/[A-Za-z]:\\\\\\\/', $line) && !preg_match('/DIRECTORY_SEPARATOR|DS|dirname|realpath|ABSPATH/', $line)) {
$issues['file_paths'][] = [
'type' => 'windows_backslash',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Use forward slashes or DIRECTORY_SEPARATOR constant for cross-platform compatibility.'
];
}
if (preg_match('/\$[a-zA-Z_]+\s*=\s*\$[a-zA-Z_]+\s*\.\s*[\'\"]?\s*[\'\"]?\s*;\s*\$[a-zA-Z_]+/', $line) && preg_match('/(path|dir|file)/i', $line)) {
if (!preg_match('/DIRECTORY_SEPARATOR|DS|\/.*\//|\\\.*\\/', $line)) {
$issues['file_paths'][] = [
'type' => 'incorrect_path_concat',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Incorrect path concatenation. Missing directory separator.'
];
}
}
}
if (!empty($activationHooks)) {
foreach ($activationHooks as $hook) {
$hookLine = $hook['line'];
if (preg_match('/\[\s*(\$[a-zA-Z_]+|new\s+[A-Z][a-zA-Z0-9_]+\(\))\s*,\s*[\'\"](install|activate)[\'\"]/', $hook['content'])) {
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class.*install|require|include|autoload/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'install_class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => 'register_activation_hook references install class/object but class may not be loaded before hook registration.'
];
}
}
if (preg_match('/\[\s*[\'\"]([A-Z][a-zA-Z0-9_]*)[\'\"]\s*,\s*[\'\"](install|activate)[\'\"]/', $hook['content'], $matches)) {
$className = $matches[1];
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '|include.*' . preg_quote($className, '/') . '/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => "register_activation_hook references class '{$className}' but class file may not be loaded before hook registration."
];
}
}
if (preg_match('/([A-Z][a-zA-Z0-9_]*)::(install|activate)/', $hook['content'], $matches)) {
$className = $matches[1];
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'static_class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => "register_activation_hook references static class '{$className}' but class may not be loaded before hook registration."
];
}
}
}
}
if (!empty($issues['file_paths'])) {
echo "\nFILE PATH VALIDATION ISSUES:\n";
$uniquePathIssues = [];
foreach ($issues['file_paths'] as $issue) {
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
if (!isset($uniquePathIssues[$key])) {
$uniquePathIssues[$key] = $issue;
}
}
foreach ($uniquePathIssues as $issue) {
$anyIssues = true;
echo " FILE PATH ISSUE in {$issue['file']}:{$issue['line']}:\n";
echo " {$issue['message']}\n";
}
}
if (!empty($issues['activation'])) {
echo "\nACTIVATION HOOK VALIDATION ISSUES:\n";
$uniqueActivationIssues = [];
foreach ($issues['activation'] as $issue) {
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
if (!isset($uniqueActivationIssues[$key])) {
$uniqueActivationIssues[$key] = $issue;
}
}
foreach ($uniqueActivationIssues as $issue) {
$anyIssues = true;
echo " ACTIVATION HOOK ISSUE in {$issue['file']}:{$issue['line']}:\n";
echo " {$issue['message']}\n";
}
}
// Now run the additional enhanced runtime checks (undefined functions, early calls, arrays, css overlaps)
echo "Checking for undefined functions...\n";
$undefinedFunctionsFound = false;
foreach ($functionCalls as $fFile => $calls) {
foreach ($calls as $call) {
$funcName = $call['name'];
if (isInternalFunction($funcName)) {
continue;
}
// Check definition in same file
$definedInThisFile = isset($functionDefinitions[$fFile][$funcName]);
// Check global implementations
$isGloballyDefined = false;
foreach ($functionImplementations as $fullName => $impls) {
$parts = explode('\\', $fullName);
$shortName = end($parts);
if (strtolower($shortName) === strtolower($funcName)) {
$isGloballyDefined = true;
break;
}
}
if (!$definedInThisFile && !$isGloballyDefined) {
$undefinedFunctionsFound = true;
echo "UNDEFINED FUNCTION: '{$funcName}' called at {$fFile}:{$call['line']}\n";
$issues['undefined_functions'][] = [
'name' => $funcName,
'file' => $fFile,
'line' => $call['line']
];
}
}
}
if (!$undefinedFunctionsFound) {
echo " ✓ No undefined functions found\n";
}
echo "\nChecking for early function calls...\n";
$earlyCallsFound = false;
foreach ($functionCalls as $fFile => $calls) {
if (!isset($functionDefinitions[$fFile])) continue;
foreach ($calls as $call) {
$funcName = $call['name'];
$callLine = $call['line'];
$defLine = null;
foreach ($functionDefinitions[$fFile] as $name => $line) {
if (strtolower($name) === strtolower($funcName)) { $defLine = $line; break; }
}
if ($defLine === null) continue;
if ($callLine > $defLine) continue;
if ($callLine < $defLine - 2) {
$earlyCallsFound = true;
echo "EARLY FUNCTION CALL: '{$funcName}' called at line {$callLine} but defined at line {$defLine} in {$fFile}\n";
$issues['early_function_calls'][] = [ 'name' => $funcName, 'file' => $fFile, 'call_line' => $callLine, 'def_line' => $defLine ];
}
}
}
if (!$earlyCallsFound) echo " ✓ No early function calls found\n";
echo "\nChecking for potential undefined arrays and array keys...\n";
$undefinedArraysFound = false;
$arrayInitializations = [];
foreach ($variableDeclarations as $fFile => $vars) {
foreach ($vars as $var) { $arrayInitializations[$fFile][] = $var['name']; }
}
foreach ($arrayKeyAccesses as $fFile => $accesses) {
foreach ($accesses as $access) {
$varName = $access['variable'];
$isInitialized = isset($arrayInitializations[$fFile]) && in_array($varName, $arrayInitializations[$fFile]);
if (!$isInitialized) {
$undefinedArraysFound = true;
echo "POTENTIAL UNDEFINED ARRAY: '\${$varName}' accessed as array at {$fFile}:{$access['line']}\n";
$issues['undefined_arrays'][] = [ 'variable' => $varName, 'file' => $fFile, 'line' => $access['line'] ];
}
}
}
if (!$undefinedArraysFound) echo " ✓ No potential undefined array issues found\n";
// Collect CSS and check for overlaps
$cssIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($pluginDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($cssIterator as $cfile) {
if ($cfile->getExtension() === 'css') {
$cssFile = $cfile->getPathname();
if (!shouldExclude($cssFile, $excludeDirs)) {
$relativePath = str_replace($pluginDir . '/', '', $cssFile);
$cssFiles[$relativePath] = file_get_contents($cssFile);
}
}
}
if (empty($cssFiles)) {
echo " No CSS files found to check for overlaps\n";
} else {
$overlapPatterns = [
'position:\s*absolute\s*;\s*z-index:\s*0|z-index:\s*0\s*;\s*position:\s*absolute' => 'Absolute positioned element at z-index 0 (may overlap)',
'position:\s*absolute\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Absolute element anchored to both top and bottom',
'position:\s*absolute\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Absolute element anchored to both left and right',
'position:\s*fixed\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Fixed element anchored to both top and bottom',
'position:\s*fixed\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Fixed element anchored to both left and right',
'margin-left:\s*-\d+px|margin-top:\s*-\d+px|margin-right:\s*-\d+px|margin-bottom:\s*-\d+px' => 'Negative margin (may cause overlap)',
];
foreach ($cssFiles as $cssPath => $cssContent) {
foreach ($overlapPatterns as $pattern => $description) {
if (preg_match_all('/' . $pattern . '/si', $cssContent, $matches, PREG_OFFSET_CAPTURE)) {
$lines = explode("\n", $cssContent);
foreach ($matches[0] as $match) {
$offset = $match[1];
$lineNumber = substr_count(substr($cssContent, 0, $offset), "\n") + 1;
echo "POTENTIAL CSS OVERLAP in {$cssPath}:{$lineNumber}\n";
echo " Reason: {$description}\n";
echo " Context: " . substr($match[0], 0, 100) . "...\n";
$issues['css_overlaps'][] = [ 'file' => $cssPath, 'line' => $lineNumber, 'pattern' => $pattern, 'description' => $description ];
}
}
}
}
if (empty($issues['css_overlaps'])) {
echo " ✓ No potential CSS overlaps detected\n";
}
}
echo "\n";
// SUMMARY: report counts and decide exit status
$totalIssues = count($issues['undefined_functions']) + count($issues['early_function_calls']) + count($issues['undefined_arrays']) + count($issues['css_overlaps']);
if ($anyIssues || $totalIssues > 0) {
echo "\n" . str_repeat("=", 70) . "\n";
echo "VALIDATION SUMMARY\n";
echo str_repeat("=", 70) . "\n";
if ($anyIssues) {
echo "Runtime issues (duplicates, missing classes): FOUND\n";
}
if (count($issues['undefined_functions']) > 0) {
echo "Undefined functions: " . count($issues['undefined_functions']) . " found\n";
}
if (count($issues['early_function_calls']) > 0) {
echo "Early function calls: " . count($issues['early_function_calls']) . " found\n";
}
if (count($issues['undefined_arrays']) > 0) {
echo "Potential undefined arrays: " . count($issues['undefined_arrays']) . " found\n";
}
if (count($issues['css_overlaps']) > 0) {
echo "Potential CSS overlaps: " . count($issues['css_overlaps']) . " found\n";
}
echo "\nFAIL: Issues detected that should be reviewed\n";
exit(1);
}
// If we got here, no issues
if (!$quiet) {
if ($verbose) {
echo "No runtime error issues, file path problems, or activation hook issues found across {$stats['files']} files.\n";
echo "✓ No undefined functions\n";
echo "✓ No early function calls\n";
echo "✓ No potential CSS overlaps\n";
} else {
echo "PASS: No issues found.\n";
}
}
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*[\'"]([\/]|Windows|C:|D:|E:)/i', $line)) {
if (!preg_match('/ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL/i', $line)) {
$issues['file_paths'][] = [
'type' => 'hardcoded_absolute_path',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Hardcoded absolute path detected. Use WordPress path functions.'
];
}
}
if (preg_match('/\$[a-zA-Z_]+\s*\.\s*[\'"]([\/\\]?)[a-zA-Z_]/', $line)) {
if (!preg_match('/DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory|dirname\(realpath/', $line)) {
$issues['file_paths'][] = [
'type' => 'missing_separator',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Path concatenation without proper directory separator.'
];
}
}
if (preg_match('/(plugin_dir_path|plugin_dir_url|template_directory|get_template_directory).*__FILE__/', $line) && !preg_match('/__FILE__.*dirname/', $line)) {
$issues['file_paths'][] = [
'type' => 'incorrect_file_usage',
'file' => $relativePath,
'line' => $lineNum,
'message' => '__FILE__ returns the file path. Use dirname(__FILE__) for directory paths.'
];
}
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\$[_A-Z]+|\$_(GET|POST|REQUEST|COOKIE))/', $line)) {
$issues['file_paths'][] = [
'type' => 'path_traversal_risk',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Direct user input used in file operation. Must validate and sanitize.'
];
}
if (preg_match('/(include|require)\s*\(\s*[\'"][^\'"]*\.(php|inc)/', $line)) {
if (!preg_match('/plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory|dirname\(__FILE__\)|ABSPATH/', $line)) {
$issues['file_paths'][] = [
'type' => 'missing_wp_path_function',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Use plugin_dir_path(), template_directory(), or similar WP functions.'
];
}
}
if (preg_match('/[A-Za-z]:\\\\\\\\/', $line) && !preg_match('/DIRECTORY_SEPARATOR|DS|dirname|realpath|ABSPATH/', $line)) {
$issues['file_paths'][] = [
'type' => 'windows_backslash',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Use forward slashes or DIRECTORY_SEPARATOR constant for cross-platform compatibility.'
];
}
if (preg_match('/\$[a-zA-Z_]+\s*=\s*\$[a-zA-Z_]+\s*\.\s*[\'"]?\s*[\'"]?\s*;\s*\$[a-zA-Z_]+/', $line) && preg_match('/(path|dir|file)/i', $line)) {
if (!preg_match('/DIRECTORY_SEPARATOR|DS|\/.*\/|\\\\.*\\\\/', $line)) {
$issues['file_paths'][] = [
'type' => 'incorrect_path_concat',
'file' => $relativePath,
'line' => $lineNum,
'message' => 'Incorrect path concatenation. Missing directory separator.'
];
}
}
}
if (!empty($activationHooks)) {
foreach ($activationHooks as $hook) {
$hookLine = $hook['line'];
if (preg_match('/\[\s*(\$[a-zA-Z_]+|new\s+[A-Z][a-zA-Z0-9_]+\(\))\s*,\s*[\'"](install|activate)[\'"]/', $hook['content'])) {
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class.*install|require|include|autoload/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'install_class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => 'register_activation_hook references install class/object but class may not be loaded before hook registration.'
];
}
}
if (preg_match('/\[\s*[\'"]([A-Z][a-zA-Z0-9_]*)[\'"]\s*,\s*[\'"](install|activate)[\'"]/', $hook['content'], $matches)) {
$className = $matches[1];
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '|include.*' . preg_quote($className, '/') . '/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => "register_activation_hook references class '{$className}' but class file may not be loaded before hook registration."
];
}
}
if (preg_match('/([A-Z][a-zA-Z0-9_]*)::(install|activate)/', $hook['content'], $matches)) {
$className = $matches[1];
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
$priorContent = implode("\n", $priorLines);
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '/si', $priorContent)) {
$issues['activation'][] = [
'type' => 'static_class_not_loaded',
'file' => $relativePath,
'line' => $hookLine,
'message' => "register_activation_hook references static class '{$className}' but class may not be loaded before hook registration."
];
}
}
}
}
}
foreach ($cssIterator as $file) {
if ($file->getExtension() === 'css') {
$cssFile = $file->getPathname();
if (!shouldExclude($cssFile, $excludeDirs)) {
$relativePath = str_replace($pluginDir . '/', '', $cssFile);
$cssFiles[$relativePath] = file_get_contents($cssFile);
}
}
}
if (empty($cssFiles)) {
echo " No CSS files found to check for overlaps\n";
} else {
// Check for CSS patterns that could cause overlapping elements
$overlapPatterns = [
'position:\s*absolute\s*;\s*z-index:\s*0|z-index:\s*0\s*;\s*position:\s*absolute' => 'Absolute positioned element at z-index 0 (may overlap)',
'position:\s*absolute\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Absolute element anchored to both top and bottom',
'position:\s*absolute\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Absolute element anchored to both left and right',
'position:\s*fixed\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Fixed element anchored to both top and bottom',
'position:\s*fixed\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Fixed element anchored to both left and right',
'margin-left:\s*-\d+px|margin-top:\s*-\d+px|margin-right:\s*-\d+px|margin-bottom:\s*-\d+px' => 'Negative margin (may cause overlap)',
];
foreach ($cssFiles as $cssPath => $cssContent) {
foreach ($overlapPatterns as $pattern => $description) {
if (preg_match_all('/' . $pattern . '/si', $cssContent, $matches, PREG_OFFSET_CAPTURE)) {
$lines = explode("\n", $cssContent);
foreach ($matches[0] as $match) {
$offset = $match[1];
$lineNumber = substr_count(substr($cssContent, 0, $offset), "\n") + 1;
$undefinedArraysFound = true;
echo "POTENTIAL CSS OVERLAP in {$cssPath}:{$lineNumber}\n";
echo " Reason: {$description}\n";
echo " Context: " . substr($match[0], 0, 100) . "...\n";
$issues['css_overlaps'][] = [
'file' => $cssPath,
'line' => $lineNumber,
'pattern' => $pattern,
'description' => $description
];
}
}
}
}
}
if (!empty($issues['file_paths'])) {
echo "\nFILE PATH VALIDATION ISSUES:\n";
$uniquePathIssues = [];
foreach ($issues['file_paths'] as $issue) {
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
if (!isset($uniquePathIssues[$key])) {
$uniquePathIssues[$key] = $issue;
}
}
foreach ($uniquePathIssues as $issue) {
$anyIssues = true;
echo " FILE PATH ISSUE in {$issue['file']}:{$issue['line']}:\n";
echo " {$issue['message']}\n";
}
}
if (!empty($issues['activation'])) {
echo "\nACTIVATION HOOK VALIDATION ISSUES:\n";
$uniqueActivationIssues = [];
foreach ($issues['activation'] as $issue) {
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
if (!isset($uniqueActivationIssues[$key])) {
$uniqueActivationIssues[$key] = $issue;
}
}
foreach ($uniqueActivationIssues as $issue) {
$anyIssues = true;
echo " ACTIVATION HOOK ISSUE in {$issue['file']}:{$issue['line']}:\n";
echo " {$issue['message']}\n";
}
if (!$undefinedArraysFound) {
echo " ✓ No potential CSS overlaps detected\n";
}
}
echo "\n";
// ============================================
// SUMMARY
// ============================================
$totalIssues = count($issues['undefined_functions']) +
count($issues['early_function_calls']) +
count($issues['undefined_arrays']) +
count($issues['css_overlaps']);
if ($anyIssues || $totalIssues > 0) {
echo "\n" . str_repeat("=", 70) . "\n";
echo "VALIDATION SUMMARY\n";
echo str_repeat("=", 70) . "\n";
if ($anyIssues) {
echo "Runtime issues (duplicates, missing classes): FOUND\n";
}
if (count($issues['undefined_functions']) > 0) {
echo "Undefined functions: " . count($issues['undefined_functions']) . " found\n";
}
if (count($issues['early_function_calls']) > 0) {
echo "Early function calls: " . count($issues['early_function_calls']) . " found\n";
}
if (count($issues['undefined_arrays']) > 0) {
echo "Potential undefined arrays: " . count($issues['undefined_arrays']) . " found\n";
}
if (count($issues['css_overlaps']) > 0) {
echo "Potential CSS overlaps: " . count($issues['css_overlaps']) . " found\n";
}
echo "\nFAIL: Issues detected that should be reviewed\n";
exit(1);
}
if (!$anyIssues) {
if (!$quiet) {
if ($verbose) {
echo "No runtime error issues, file path problems, or activation hook issues found across {$stats['files']} files.\n";
echo "✓ No undefined functions\n";
echo "✓ No early function calls\n";
echo "✓ No potential CSS overlaps\n";
} else {
echo "PASS: No issues found.\n";
}
}
exit(0);
}
exit(1);