#!/usr/bin/env php 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);