parser = (new ParserFactory())->createForNewestSupportedVersion(); } /** * Parse PHP code into AST */ public function parse(string $code, string $filePath = ''): ?array { try { $ast = $this->parser->parse($code); if ($ast === null) { return null; } // Resolve names to fully qualified $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver()); $ast = $traverser->traverse($ast); return $ast; } catch (Error $e) { $this->errors[] = [ 'file' => $filePath, 'message' => $e->getMessage(), 'line' => $e->getStartLine(), ]; return null; } } /** * Parse a Blade template */ public function parseBlade(string $code, string $filePath): array { $result = [ 'raw_outputs' => [], // {!! $var !!} 'escaped_outputs' => [], // {{ $var }} 'php_blocks' => [], // @php ... @endphp 'includes' => [], // @include, @extends, @component 'forms' => [], //
tags 'csrf_tokens' => [], // @csrf directives ]; // Find raw (unescaped) outputs - security risk preg_match_all('/\{!!\s*(.+?)\s*!!\}/s', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[1] as $match) { $result['raw_outputs'][] = [ 'expression' => $match[0], 'line' => $this->getLineNumber($code, $match[1]), ]; } // Find escaped outputs preg_match_all('/\{\{\s*(.+?)\s*\}\}/s', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[1] as $match) { $result['escaped_outputs'][] = [ 'expression' => $match[0], 'line' => $this->getLineNumber($code, $match[1]), ]; } // Find @php blocks preg_match_all('/@php\s*(.*?)\s*@endphp/s', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[1] as $match) { $result['php_blocks'][] = [ 'code' => $match[0], 'line' => $this->getLineNumber($code, $match[1]), ]; } // Find includes preg_match_all('/@(include|extends|component)\s*\(\s*[\'"](.+?)[\'"]/s', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[2] as $i => $match) { $result['includes'][] = [ 'type' => $matches[1][$i][0], 'path' => $match[0], 'line' => $this->getLineNumber($code, $match[1]), ]; } // Find forms preg_match_all('/]*>/i', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $match) { $line = $this->getLineNumber($code, $match[1]); $result['forms'][] = [ 'tag' => $match[0], 'line' => $line, 'has_method' => stripos($match[0], 'method=') !== false, ]; } // Find CSRF tokens preg_match_all('/@csrf/i', $code, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $match) { $result['csrf_tokens'][] = [ 'line' => $this->getLineNumber($code, $match[1]), ]; } return $result; } /** * Get line number from offset */ private function getLineNumber(string $code, int $offset): int { return substr_count(substr($code, 0, $offset), "\n") + 1; } /** * Get parse errors */ public function getErrors(): array { return $this->errors; } }