Skip to main content
Glama
php_analyzer.php23.3 kB
#!/usr/bin/env php <?php /** * PHP AST Analyzer Helper * Parses PHP files using PHP-Parser library * * Requires: nikic/php-parser * Install: composer require nikic/php-parser */ // Simple standalone parser without dependencies // For production, use nikic/php-parser function analyzePhpFile($filePath) { if (!file_exists($filePath)) { return [ 'error' => 'File not found: ' . $filePath, 'path' => $filePath ]; } $code = file_get_contents($filePath); // Simple regex-based analysis (fallback when php-parser not available) $analysis = [ 'path' => $filePath, 'classes' => extractClasses($code, $filePath), 'functions' => extractFunctions($code, $filePath), 'interfaces' => extractInterfaces($code, $filePath), 'enums' => extractEnums($code, $filePath), 'traits' => extractTraits($code, $filePath), 'typeAliases' => [], 'imports' => extractImports($code), 'exports' => [], 'documentation' => [ 'hasDocumentation' => false, 'documentedSymbols' => 0, 'totalSymbols' => 0, 'coverage' => 0 ] ]; // Calculate documentation coverage $totalSymbols = 0; $documentedSymbols = 0; // Count classes and their members foreach ($analysis['classes'] as $class) { $totalSymbols++; if (!empty($class['documentation'])) { $documentedSymbols++; } // Count properties foreach ($class['properties'] as $prop) { $totalSymbols++; if (!empty($prop['documentation'])) { $documentedSymbols++; } } // Count methods foreach ($class['methods'] as $method) { $totalSymbols++; if (!empty($method['documentation'])) { $documentedSymbols++; } } // Count constructors foreach ($class['constructors'] as $constructor) { $totalSymbols++; if (!empty($constructor['documentation'])) { $documentedSymbols++; } } } // Count top-level functions foreach ($analysis['functions'] as $func) { $totalSymbols++; if (!empty($func['documentation'])) { $documentedSymbols++; } } // Count interfaces foreach ($analysis['interfaces'] as $interface) { $totalSymbols++; if (!empty($interface['documentation'])) { $documentedSymbols++; } } // Count enums and their cases foreach ($analysis['enums'] as $enum) { $totalSymbols++; if (!empty($enum['documentation'])) { $documentedSymbols++; } foreach ($enum['cases'] as $case) { $totalSymbols++; if (!empty($case['documentation'])) { $documentedSymbols++; } } } // Count traits and their methods foreach ($analysis['traits'] as $trait) { $totalSymbols++; if (!empty($trait['documentation'])) { $documentedSymbols++; } foreach ($trait['methods'] as $method) { $totalSymbols++; if (!empty($method['documentation'])) { $documentedSymbols++; } } } $coverage = $totalSymbols > 0 ? ($documentedSymbols / $totalSymbols * 100) : 0; $analysis['documentation'] = [ 'hasDocumentation' => $documentedSymbols > 0, 'documentedSymbols' => $documentedSymbols, 'totalSymbols' => $totalSymbols, 'coverage' => $coverage ]; return $analysis; } function extractClasses($code, $filePath) { $classes = []; // Match class definitions with optional docblocks and attributes // Allows for docblock, then optional attributes (#[...]), then class keyword $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(?:#\[[^\]]+\]\s*)?(abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?\s*\{/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $className = $match[3][0]; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $isAbstract = !empty($match[2][0]); $extends = isset($match[4]) && $match[4][0] ? $match[4][0] : null; $implements = isset($match[5]) && $match[5][0] ? array_map('trim', explode(',', $match[5][0])) : []; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; // Extract the class body $classStart = $match[0][1] + strlen($match[0][0]); $classBody = extractClassBody($code, $classStart); $properties = extractClassProperties($classBody, $className); $constructors = extractConstructors($classBody, $className); // Extract promoted properties from constructor and merge with regular properties if (!empty($constructors)) { $promotedProps = extractPromotedProperties($classBody); // Merge and deduplicate: promoted properties take precedence $propMap = []; foreach ($properties as $prop) { $propMap[$prop['name']] = $prop; } foreach ($promotedProps as $prop) { $propMap[$prop['name']] = $prop; // Overwrites if exists } $properties = array_values($propMap); } $classes[] = [ 'name' => $className, 'documentation' => $doc, 'isExported' => true, 'isAbstract' => $isAbstract, 'extendsClass' => $extends, 'implementsInterfaces' => $implements, 'attributes' => extractAttributes(substr($code, max(0, $match[0][1] - 200), 200)), 'properties' => $properties, 'methods' => extractMethods($classBody, $className), 'constructors' => $constructors, 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $classes; } function extractClassBody($code, $startPos) { $braceCount = 1; $pos = $startPos; $length = strlen($code); while ($pos < $length && $braceCount > 0) { $char = $code[$pos]; if ($char === '{') { $braceCount++; } elseif ($char === '}') { $braceCount--; } $pos++; } return substr($code, $startPos, $pos - $startPos - 1); } function extractClassProperties($code, $className) { $properties = []; $seenProperties = []; // Match property declarations (must have visibility or type hint) // This excludes $this->property assignments in methods $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(public|private|protected)\s+(static\s+)?(readonly\s+)?(?:([\w\\\|]+)\s+)?\$(\w+)\s*(?:=|;)/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $propName = $match[6][0]; // Skip duplicates if (isset($seenProperties[$propName])) { continue; } $seenProperties[$propName] = true; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $visibility = $match[2][0]; $isStatic = !empty($match[3][0]); $isReadonly = !empty($match[4][0]); $type = isset($match[5]) && $match[5][0] ? $match[5][0] : null; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; $properties[] = [ 'name' => $propName, 'documentation' => $doc, 'type' => $type, 'visibility' => $visibility, 'isStatic' => $isStatic, 'isReadonly' => $isReadonly, 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $properties; } function extractPromotedProperties($code) { $properties = []; // Find constructor if (preg_match('/function\s+__construct\s*\((.*?)\)/s', $code, $constructorMatch)) { $constructorParams = $constructorMatch[1]; // Match promoted properties with optional inline documentation // Pattern: optional /** comment */, visibility, optional readonly, type, $name $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(public|private|protected)\s+(readonly\s+)?(?:([\w\\\|]+)\s+)?\$(\w+)/'; if (preg_match_all($pattern, $constructorParams, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $doc = isset($match[1]) && $match[1] ? trim($match[1]) : null; $visibility = $match[2]; $isReadonly = !empty($match[3]); $type = isset($match[4]) && $match[4] ? $match[4] : null; $propName = $match[5]; $properties[] = [ 'name' => $propName, 'documentation' => $doc, 'type' => $type, 'visibility' => $visibility, 'isStatic' => false, 'isReadonly' => $isReadonly, 'location' => [ 'startLine' => 4, // Approximate line in constructor 'startColumn' => 1, 'endLine' => 4, 'endColumn' => 1 ] ]; } } } return $properties; } function extractMethods($code, $className) { $methods = []; // Match method definitions with optional docblocks and attributes // Allows for: /** doc */ #[Attribute] visibility function name $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(?:#\[[^\]]+\]\s*)?(public|private|protected)?\s*(static)?\s*(abstract)?\s*function\s+(\w+)\s*\((.*?)\)\s*(?::\s*([\w\\\|]+))?\s*\{/s'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $methodName = $match[5][0]; // Skip if constructor (handled separately) if ($methodName === '__construct') { continue; } $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $visibility = isset($match[2]) && $match[2][0] ? $match[2][0] : 'public'; $isStatic = !empty($match[3][0]); $isAbstract = !empty($match[4][0]); $params = $match[6][0]; $returnType = isset($match[7]) && $match[7][0] ? $match[7][0] : null; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; $methods[] = [ 'name' => $methodName, 'documentation' => $doc, 'visibility' => $visibility, 'isStatic' => $isStatic, 'isAbstract' => $isAbstract, 'parameters' => parseParameters($params), 'returnType' => $returnType, 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $methods; } function extractConstructors($code, $className) { $constructors = []; // Match constructor $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(public|private|protected)?\s*function\s+__construct\s*\((.*?)\)/s'; if (preg_match($pattern, $code, $match)) { $doc = isset($match[1]) ? trim($match[1]) : null; $params = $match[3]; $constructors[] = [ 'documentation' => $doc, 'parameters' => parseParameters($params), 'location' => [ 'startLine' => 1, 'startColumn' => 1, 'endLine' => 1, 'endColumn' => 1 ] ]; } return $constructors; } function removeStructureBodies($code, $structureType) { // Match structure declarations (class, enum, trait, interface) $pattern = '/\b' . $structureType . '\s+\w+[^{]*\{/'; $result = ''; $pos = 0; while (preg_match($pattern, $code, $match, PREG_OFFSET_CAPTURE, $pos)) { $matchPos = $match[0][1]; $matchLen = strlen($match[0][0]); // Add everything before this match $result .= substr($code, $pos, $matchPos - $pos); // Find the closing brace by counting braces $braceCount = 1; $i = $matchPos + $matchLen; while ($i < strlen($code) && $braceCount > 0) { if ($code[$i] === '{') { $braceCount++; } elseif ($code[$i] === '}') { $braceCount--; } $i++; } // Move position past the entire structure $pos = $i; } // Add remaining code $result .= substr($code, $pos); return $result; } function extractFunctions($code, $filePath) { $functions = []; // Remove all class, enum, trait, and interface bodies to avoid matching methods as functions $codeWithoutStructures = $code; $codeWithoutStructures = removeStructureBodies($codeWithoutStructures, 'class'); $codeWithoutStructures = removeStructureBodies($codeWithoutStructures, 'enum'); $codeWithoutStructures = removeStructureBodies($codeWithoutStructures, 'trait'); $codeWithoutStructures = removeStructureBodies($codeWithoutStructures, 'interface'); // Match top-level function definitions (outside classes, enums, traits, interfaces) $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?function\s+(\w+)\s*\((.*?)\)\s*(?::\s*([\w\\\|]+))?\s*\{/s'; if (preg_match_all($pattern, $codeWithoutStructures, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $funcName = $match[2][0]; // Skip magic methods and constructors if (strpos($funcName, '__') === 0) { continue; } $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $params = $match[3][0]; $returnType = isset($match[4]) && $match[4][0] ? $match[4][0] : null; $lineNumber = substr_count(substr($codeWithoutStructures, 0, $match[0][1]), "\n") + 1; $functions[] = [ 'name' => $funcName, 'documentation' => $doc, 'isExported' => true, 'isAsync' => false, 'parameters' => parseParameters($params), 'returnType' => $returnType, 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $functions; } function extractInterfaces($code, $filePath) { $interfaces = []; $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?interface\s+(\w+)(?:\s+extends\s+([\w,\s]+))?\s*\{/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $interfaceName = $match[2][0]; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $extends = isset($match[3]) && $match[3][0] ? array_map('trim', explode(',', $match[3][0])) : []; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; $interfaces[] = [ 'name' => $interfaceName, 'documentation' => $doc, 'isExported' => true, 'extendsInterfaces' => $extends, 'properties' => [], 'methods' => [], 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $interfaces; } function extractEnums($code, $filePath) { $enums = []; // Match enum definitions (PHP 8.1+) with optional docblocks and attributes $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(?:#\[[^\]]+\]\s*)?enum\s+(\w+)(?:\s*:\s*(\w+))?\s*\{/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $enumName = $match[2][0]; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $backingType = isset($match[3]) && $match[3][0] ? $match[3][0] : null; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; // Extract enum body $enumStart = $match[0][1] + strlen($match[0][0]); $enumBody = extractClassBody($code, $enumStart); $enums[] = [ 'name' => $enumName, 'documentation' => $doc, 'isExported' => true, 'backingType' => $backingType, 'cases' => extractEnumCases($enumBody), 'methods' => extractMethods($enumBody, $enumName), 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $enums; } function extractEnumCases($code) { $cases = []; // Match enum cases $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?case\s+(\w+)(?:\s*=\s*([^;]+))?;/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $caseName = $match[2][0]; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $value = isset($match[3]) && $match[3][0] ? trim($match[3][0]) : null; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; $cases[] = [ 'name' => $caseName, 'documentation' => $doc, 'value' => $value, 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $cases; } function extractTraits($code, $filePath) { $traits = []; // Match trait definitions $pattern = '/(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?trait\s+(\w+)\s*\{/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach ($matches as $match) { $traitName = $match[2][0]; $doc = isset($match[1]) && $match[1][0] ? trim($match[1][0]) : null; $lineNumber = substr_count(substr($code, 0, $match[0][1]), "\n") + 1; // Extract trait body $traitStart = $match[0][1] + strlen($match[0][0]); $traitBody = extractClassBody($code, $traitStart); $traits[] = [ 'name' => $traitName, 'documentation' => $doc, 'isExported' => true, 'properties' => extractClassProperties($traitBody, $traitName), 'methods' => extractMethods($traitBody, $traitName), 'location' => [ 'startLine' => $lineNumber, 'startColumn' => 1, 'endLine' => $lineNumber, 'endColumn' => 1 ] ]; } } return $traits; } function extractImports($code) { $imports = []; // Match use statements $pattern = '/use\s+([\w\\\\]+)(?:\s+as\s+(\w+))?;/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $namespace = $match[1]; $alias = isset($match[2]) ? $match[2] : null; $imports[] = [ 'importedNames' => [$alias ?: basename(str_replace('\\', '/', $namespace))], 'moduleSpecifier' => $namespace, 'defaultImport' => null, 'namespaceImport' => $alias ]; } } return $imports; } function parseParameters($paramString) { $params = []; if (empty(trim($paramString))) { return $params; } // Simple parameter parsing $parts = explode(',', $paramString); foreach ($parts as $part) { $part = trim($part); // Match: type $name = defaultValue if (preg_match('/([\w\\\|]+)?\s*\$(\w+)(?:\s*=\s*(.+))?/', $part, $match)) { $type = isset($match[1]) && $match[1] ? $match[1] : null; $name = $match[2]; $defaultValue = isset($match[3]) ? trim($match[3]) : null; $params[] = [ 'name' => $name, 'type' => $type, 'isOptional' => $defaultValue !== null, 'defaultValue' => $defaultValue ]; } } return $params; } function extractAttributes($code) { $attributes = []; // Match PHP 8.0+ attributes: #[AttributeName] or #[AttributeName(args)] $pattern = '/#\[(\w+)(?:\(([^)]*)\))?\]/'; if (preg_match_all($pattern, $code, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $attributeName = $match[1]; $args = isset($match[2]) ? trim($match[2]) : null; $attributes[] = [ 'name' => $attributeName, 'arguments' => $args ]; } } return $attributes; } // Main execution if ($argc < 2) { echo json_encode(['error' => 'No file path provided']); exit(1); } $filePath = $argv[1]; $result = analyzePhpFile($filePath); echo json_encode($result, JSON_PRETTY_PRINT);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LiL-Loco/documentation-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server