Files
rspade_system/bin/formatters/php-formatter
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

1029 lines
38 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* RSX Code Formatter
*
* A PHP code formatter that:
* 1. Uses PHP CS Fixer for reliable PHP formatting
* 2. Manages use statements with automatic imports
* 3. Corrects namespaces based on file path
*
* This formatter uses PHP tokenizer and string manipulation
* instead of regex for reliability.
*/
class RsxFormatter
{
private $project_root;
private $php_cs_fixer_bin;
private $verbose = false;
public function __construct()
{
$this->project_root = dirname(__DIR__, 2); // Go up two directories to project root
$this->php_cs_fixer_bin = $this->project_root . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'php-cs-fixer';
// Check if PHP CS Fixer is installed
if (!file_exists($this->php_cs_fixer_bin)) {
$this->error("PHP CS Fixer not found. Run: composer require --dev friendsofphp/php-cs-fixer");
exit(1);
}
}
/**
* Main entry point
*/
public function run($argv)
{
// Parse command line arguments
$files = [];
for ($i = 1; $i < count($argv); $i++) {
$arg = $argv[$i];
if ($arg === '--verbose' || $arg === '-v') {
$this->verbose = true;
} elseif ($arg === '--help' || $arg === '-h') {
$this->show_help();
exit(0);
} elseif (substr($arg, 0, 1) !== '-') {
$files[] = $arg;
}
}
// If no files specified, format current directory
if (empty($files)) {
$files = ['.'];
}
$this->format_files($files);
}
/**
* Check PHP syntax of a file
*/
private function check_php_syntax($file_path)
{
$command = 'php -l ' . escapeshellarg($file_path) . ' 2>&1';
$output = shell_exec($command);
return strpos($output, 'No syntax errors detected') !== false;
}
/**
* Format specified files or directories
*/
private function format_files($paths)
{
foreach ($paths as $path) {
// Make path absolute if relative (works on both Windows and Unix)
if (!$this->is_absolute_path($path)) {
$path = $this->project_root . DIRECTORY_SEPARATOR . $path;
}
if (is_file($path)) {
$this->format_file($path);
} elseif (is_dir($path)) {
$this->format_directory($path);
} else {
$this->warning("Path not found: $path");
}
}
}
/**
* Check if file is in rsx directory
*/
private function is_rsx_file($file_path)
{
$relative_path = $this->get_relative_path($file_path);
// get_relative_path always returns forward slashes for consistency
return substr($relative_path, 0, 4) === 'rsx/';
}
/**
* Check if a path is absolute
*/
private function is_absolute_path($path)
{
// Unix absolute path
if (substr($path, 0, 1) === '/') {
return true;
}
// Windows absolute path (C:\ or C:/)
if (preg_match('/^[a-zA-Z]:[\\/\\\\]/', $path)) {
return true;
}
return false;
}
/**
* Format a single PHP file
*/
private function format_file($file_path)
{
// Handle .formatting.tmp files from VS Code
$actual_extension = '';
if (substr($file_path, -15) === '.formatting.tmp') {
// Get the actual extension from before .formatting.tmp
$actual_path = substr($file_path, 0, -15);
$actual_extension = pathinfo($actual_path, PATHINFO_EXTENSION);
}
// Skip non-PHP files
if ($actual_extension !== 'php' && substr($file_path, -4) !== '.php') {
return;
}
// Skip blade templates
if (substr($file_path, -10) === '.blade.php') {
return;
}
$relative_path = $this->get_relative_path($file_path);
// Check PHP syntax first
if (!$this->check_php_syntax($file_path)) {
// Try removing Rsx use directives and test again
$content = file_get_contents($file_path);
$temp_file = tempnam(sys_get_temp_dir(), 'phpfmt_') . '.php';
try {
// Remove Rsx use directives for testing
$lines = explode("\n", $content);
$filtered_lines = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if (substr($trimmed, 0, 8) !== 'use Rsx\\') {
$filtered_lines[] = $line;
}
}
$temp_content = implode("\n", $filtered_lines);
file_put_contents($temp_file, $temp_content);
if (!$this->check_php_syntax($temp_file)) {
// Both versions have syntax errors
echo "Syntax error preventing auto-formatting\n";
return;
}
} finally {
if (file_exists($temp_file)) {
unlink($temp_file);
}
}
}
// Only process RSX files with our custom logic
// COMMENTED OUT: Namespace/use fixing moved to Php_Fixer.php in manifest build
// if ($this->is_rsx_file($file_path)) {
// if ($this->should_update_rsx_file($file_path)) {
// $this->update_rsx_file($file_path);
// }
// }
// Then run PHP CS Fixer
// On Windows, we need to invoke the PHP script with php
// For .formatting.tmp files, try to work around permission issues
if (substr($file_path, -15) === '.formatting.tmp') {
// For temp files, make a copy that we can definitely write to
$work_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'rsx_fmt_' . uniqid() . '.php';
copy($file_path, $work_file);
$command = sprintf(
'php %s fix %s --config=%s --quiet',
escapeshellarg($this->php_cs_fixer_bin),
escapeshellarg($work_file),
escapeshellarg($this->project_root . '/.php-cs-fixer.php')
);
if ($this->verbose) {
$command = str_replace('--quiet', '--verbose', $command);
}
exec($command, $output, $return_code);
if ($return_code === 0) {
// Copy the formatted content back
copy($work_file, $file_path);
unlink($work_file);
$this->success("✓ Formatted: $relative_path");
} else {
unlink($work_file);
$this->error("✗ Failed to format: $relative_path");
if ($this->verbose && !empty($output)) {
foreach ($output as $line) {
$this->output(" $line");
}
}
}
} else {
// Regular files - format in place
$command = sprintf(
'php %s fix %s --config=%s --quiet',
escapeshellarg($this->php_cs_fixer_bin),
escapeshellarg($file_path),
escapeshellarg($this->project_root . '/.php-cs-fixer.php')
);
if ($this->verbose) {
$command = str_replace('--quiet', '--verbose', $command);
}
exec($command, $output, $return_code);
if ($return_code === 0) {
$this->success("✓ Formatted: $relative_path");
} else {
$this->error("✗ Failed to format: $relative_path");
if ($this->verbose && !empty($output)) {
foreach ($output as $line) {
$this->output(" $line");
}
}
}
}
}
/**
* Format all PHP files in a directory
*/
private function format_directory($dir_path)
{
$relative_path = $this->get_relative_path($dir_path);
$this->info("Formatting directory: $relative_path");
// Process RSX files first
if ($this->is_rsx_file($dir_path)) {
$this->update_rsx_files_in_directory($dir_path);
}
// Run PHP CS Fixer on the directory
// On Windows, we need to invoke the PHP script with php
$command = sprintf(
'php %s fix %s --config=%s',
escapeshellarg($this->php_cs_fixer_bin),
escapeshellarg($dir_path),
escapeshellarg($this->project_root . '/.php-cs-fixer.php')
);
if (!$this->verbose) {
$command .= ' --quiet';
}
exec($command, $output, $return_code);
if ($return_code === 0) {
$this->success("✓ Formatted directory: $relative_path");
} else {
$this->error("✗ Failed to format directory: $relative_path");
}
if ($this->verbose && !empty($output)) {
foreach ($output as $line) {
$this->output(" $line");
}
}
}
/**
* Check if RSX file needs updating
*/
private function should_update_rsx_file($file_path)
{
$content = file_get_contents($file_path);
// Use tokenizer to check if it has a class
$tokens = token_get_all($content);
$has_class = false;
foreach ($tokens as $token) {
if (is_array($token) && $token[0] === T_CLASS) {
$has_class = true;
break;
}
}
return $has_class;
}
/**
* Update RSX file with proper namespace and use statements
*/
private function update_rsx_file($file_path)
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
// Find class name
$class_name = $this->extract_class_name($tokens);
if (!$class_name) {
return; // No class found
}
// Calculate expected namespace from path
$expected_namespace = $this->get_namespace_from_path($file_path);
// Extract current namespace
$current_namespace = $this->extract_namespace($tokens);
// Extract existing use statements (non-Rsx ones)
$existing_uses = $this->extract_non_rsx_uses($tokens);
// Find referenced classes
$referenced_classes = $this->find_referenced_classes($tokens, $class_name);
// Build manifest and resolve classes
$manifest = $this->build_class_manifest();
$resolved_uses = $this->resolve_class_references($referenced_classes, $existing_uses, $manifest, $expected_namespace);
// Combine all use statements
$all_uses = array_unique(array_merge($resolved_uses, $existing_uses));
sort($all_uses);
// Rebuild the file
$new_content = $this->rebuild_file_content($tokens, $expected_namespace, $all_uses);
if ($new_content !== $content) {
file_put_contents($file_path, $new_content);
}
}
/**
* Extract class name from tokens
*/
private function extract_class_name($tokens)
{
for ($i = 0; $i < count($tokens); $i++) {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_CLASS) {
// Find the next T_STRING which should be the class name
for ($j = $i + 1; $j < count($tokens); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
return $tokens[$j][1];
}
}
}
}
return null;
}
/**
* Extract namespace from tokens
*/
private function extract_namespace($tokens)
{
for ($i = 0; $i < count($tokens); $i++) {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_NAMESPACE) {
$namespace = '';
$i++; // Move past T_NAMESPACE
while ($i < count($tokens) && $tokens[$i] !== ';') {
if (is_array($tokens[$i])) {
// Handle PHP 8's T_NAME_QUALIFIED token
if (defined('T_NAME_QUALIFIED') && $tokens[$i][0] === T_NAME_QUALIFIED) {
$namespace .= $tokens[$i][1];
}
// Handle PHP 8's T_NAME_FULLY_QUALIFIED token
elseif (defined('T_NAME_FULLY_QUALIFIED') && $tokens[$i][0] === T_NAME_FULLY_QUALIFIED) {
$namespace .= $tokens[$i][1];
}
// Handle older PHP versions
elseif ($tokens[$i][0] === T_STRING || $tokens[$i][0] === T_NS_SEPARATOR) {
$namespace .= $tokens[$i][1];
}
}
$i++;
}
return trim($namespace);
}
}
return null;
}
/**
* Extract non-Rsx use statements
*/
private function extract_non_rsx_uses($tokens)
{
$uses = [];
$class_found = false;
$i = 0;
while ($i < count($tokens)) {
// Stop looking after we find the class
if (is_array($tokens[$i]) && $tokens[$i][0] === T_CLASS) {
break;
}
if (is_array($tokens[$i]) && $tokens[$i][0] === T_USE) {
$use_statement = '';
$j = $i + 1; // Start after T_USE
while ($j < count($tokens) && $tokens[$j] !== ';') {
if (is_array($tokens[$j])) {
// Handle PHP 8's T_NAME_QUALIFIED token
if (defined('T_NAME_QUALIFIED') && $tokens[$j][0] === T_NAME_QUALIFIED) {
$use_statement .= $tokens[$j][1];
}
// Handle PHP 8's T_NAME_FULLY_QUALIFIED token
elseif (defined('T_NAME_FULLY_QUALIFIED') && $tokens[$j][0] === T_NAME_FULLY_QUALIFIED) {
$use_statement .= $tokens[$j][1];
}
// Handle older PHP versions
elseif ($tokens[$j][0] === T_STRING || $tokens[$j][0] === T_NS_SEPARATOR) {
$use_statement .= $tokens[$j][1];
}
}
$j++;
}
$use_statement = trim($use_statement);
// Only keep non-Rsx uses
if ($use_statement && substr($use_statement, 0, 4) !== 'Rsx\\') {
$uses[] = $use_statement;
}
// Move past the entire use statement
$i = $j;
}
$i++;
}
return $uses;
}
/**
* Find referenced classes in tokens
*/
private function find_referenced_classes($tokens, $current_class)
{
$classes = [];
$i = 0;
while ($i < count($tokens)) {
$token = $tokens[$i];
if (is_array($token)) {
// Check for extends/implements
if ($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS) {
$i++;
while ($i < count($tokens) && $tokens[$i] !== '{' && $tokens[$i] !== ',' && $tokens[$i] !== ';') {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
$classes[] = $tokens[$i][1];
}
$i++;
}
}
// Check for static calls (ClassName::method)
elseif ($token[0] === T_STRING && $i + 1 < count($tokens) && is_array($tokens[$i + 1]) && $tokens[$i + 1][0] === T_DOUBLE_COLON) {
$class_name = $token[1];
if ($class_name !== 'self' && $class_name !== 'parent' && $class_name !== 'static') {
$classes[] = $class_name;
}
}
// Check for new ClassName
elseif ($token[0] === T_NEW) {
$i++;
while ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
$classes[] = $tokens[$i][1];
}
}
// Check for instanceof
elseif ($token[0] === T_INSTANCEOF) {
$i++;
while ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
$classes[] = $tokens[$i][1];
}
}
// Check for catch blocks
elseif ($token[0] === T_CATCH) {
$i++;
while ($i < count($tokens) && $tokens[$i] !== '(') {
$i++;
}
$i++; // Skip (
while ($i < count($tokens) && $tokens[$i] !== ')') {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
$classes[] = $tokens[$i][1];
}
$i++;
}
}
// Check for type hints in function parameters and return types
elseif ($token[0] === T_FUNCTION) {
$i++;
// Skip to parameters
while ($i < count($tokens) && $tokens[$i] !== '(') {
$i++;
}
// Process parameters
while ($i < count($tokens) && $tokens[$i] !== ')') {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
// Check if it's followed by a variable (making it a type hint)
$j = $i + 1;
while ($j < count($tokens) && is_array($tokens[$j]) && $tokens[$j][0] === T_WHITESPACE) {
$j++;
}
if ($j < count($tokens) && is_array($tokens[$j]) && $tokens[$j][0] === T_VARIABLE) {
$classes[] = $tokens[$i][1];
}
}
$i++;
}
// Check for return type
if ($i < count($tokens) && $tokens[$i] === ')') {
$i++;
while ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < count($tokens) && $tokens[$i] === ':') {
$i++;
while ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < count($tokens)) {
// Handle nullable types
if ($tokens[$i] === '?') {
$i++;
while ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
}
if ($i < count($tokens) && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
$classes[] = $tokens[$i][1];
}
}
}
}
}
}
$i++;
}
// Filter out built-in types and current class
$built_in_types = ['string', 'int', 'float', 'bool', 'array', 'void', 'mixed', 'object', 'callable', 'iterable', 'null', 'true', 'false', 'never'];
$classes = array_unique($classes);
$filtered = [];
foreach ($classes as $class) {
if (!in_array(strtolower($class), $built_in_types) && $class !== $current_class) {
$filtered[] = $class;
}
}
return $filtered;
}
/**
* Build a manifest of all available classes
*/
private function build_class_manifest()
{
$manifest = [];
// Get scan directories
$scan_dirs = $this->get_scan_directories();
foreach ($scan_dirs as $dir) {
$full_path = $this->project_root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $dir);
if (!is_dir($full_path)) {
continue;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile() && substr($file->getPathname(), -4) === '.php') {
$content = file_get_contents($file->getPathname());
$tokens = token_get_all($content);
// Extract namespace
$namespace = $this->extract_namespace($tokens);
// Extract class name
$class_name = $this->extract_class_name($tokens);
if ($class_name) {
$fqcn = $namespace ? $namespace . '\\' . $class_name : $class_name;
$manifest[$class_name] = [
'class' => $class_name,
'namespace' => $namespace,
'fqcn' => $fqcn,
'file' => $file->getPathname()
];
}
}
}
}
return $manifest;
}
/**
* Get scan directories from config
*/
private function get_scan_directories()
{
return [
'rsx',
'app/RSpade/Core/Base',
'app/RSpade/Core/Attributes'
];
}
/**
* Resolve class references to fully qualified names
*/
private function resolve_class_references($referenced_classes, $existing_uses, $manifest, $namespace)
{
$resolved = [];
// Get simple names from existing uses
$existing_simple_names = [];
foreach ($existing_uses as $use) {
$parts = explode('\\', $use);
$existing_simple_names[] = end($parts);
}
// Common Laravel/Framework classes
$framework_classes = [
'Rsx_Controller' => 'App\\RSpade\\Core\\Controller\\Rsx_Controller',
'Rsx_Api' => 'App\\RSpade\\Core\\Base\\Rsx_Api',
'Model' => 'Illuminate\\Database\\Eloquent\\Model',
'Controller' => 'App\\Http\\Controllers\\Controller',
'Request' => 'Illuminate\\Http\\Request',
'Response' => 'Illuminate\\Http\\Response',
'Collection' => 'Illuminate\\Support\\Collection',
'Str' => 'Illuminate\\Support\\Str',
'File' => 'Illuminate\\Support\\Facades\\File',
'DB' => 'Illuminate\\Support\\Facades\\DB',
'Auth' => 'Illuminate\\Support\\Facades\\Auth',
'Session' => 'Illuminate\\Support\\Facades\\Session',
'Cache' => 'Illuminate\\Support\\Facades\\Cache',
'Log' => 'Illuminate\\Support\\Facades\\Log',
'Route' => 'Illuminate\\Support\\Facades\\Route',
];
foreach ($referenced_classes as $class) {
// Skip if already in existing uses
if (in_array($class, $existing_simple_names)) {
continue;
}
// Check framework classes first
if (isset($framework_classes[$class])) {
$resolved[] = $framework_classes[$class];
continue;
}
// Check manifest
if (isset($manifest[$class])) {
$fqcn = $manifest[$class]['fqcn'];
// Don't add if it's in the same namespace
if ($manifest[$class]['namespace'] !== $namespace) {
// Don't add Rsx namespace uses
if (substr($fqcn, 0, 4) !== 'Rsx\\') {
$resolved[] = $fqcn;
}
}
}
}
return $resolved;
}
/**
* Rebuild file content with updated namespace and uses
*/
private function rebuild_file_content($tokens, $namespace, $uses)
{
$output = '';
$namespace_written = false;
$uses_written = false;
$class_found = false;
$pre_class_content = ''; // Content that goes before class (comments, constants, etc.)
$attributes = ''; // Attributes that go directly above class
// First pass: collect structure information
$collecting_pre_class = false;
$in_attribute = false;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
// Handle PHP open tag
if ($token[0] === T_OPEN_TAG) {
$output .= $token[1];
continue;
}
// Handle namespace
if ($token[0] === T_NAMESPACE) {
if (!$namespace_written) {
$output .= "\nnamespace $namespace;";
$namespace_written = true;
// Skip to semicolon
while ($i < count($tokens) && $tokens[$i] !== ';') {
$i++;
}
$collecting_pre_class = true; // Start collecting after namespace
continue;
}
}
// Handle use statements - skip them all, we'll write our own
if ($token[0] === T_USE && !$class_found) {
// Skip entire use statement including the semicolon
while ($i < count($tokens) && $tokens[$i] !== ';') {
$i++;
}
// Skip the semicolon itself
if ($i < count($tokens) && $tokens[$i] === ';') {
$i++;
}
// Decrement $i because the for loop will increment it
$i--;
$collecting_pre_class = true; // Start collecting after use statements
continue;
}
// Handle attributes (PHP 8+)
if ($token[0] === T_ATTRIBUTE) {
$in_attribute = true;
$attr_content = $token[1];
$i++;
// Collect entire attribute
$bracket_count = 1;
while ($i < count($tokens) && $bracket_count > 0) {
if (is_array($tokens[$i])) {
$attr_content .= $tokens[$i][1];
} else {
$attr_content .= $tokens[$i];
if ($tokens[$i] === '[') {
$bracket_count++;
} elseif ($tokens[$i] === ']') {
$bracket_count--;
}
}
$i++;
}
$i--; // Back up one since for loop will increment
// Check if next significant token is class
$j = $i + 1;
while ($j < count($tokens) && is_array($tokens[$j]) && $tokens[$j][0] === T_WHITESPACE) {
$j++;
}
if ($j < count($tokens) && is_array($tokens[$j]) && $tokens[$j][0] === T_CLASS) {
// This attribute is directly above class
$attributes .= $attr_content . "\n";
} else {
// This attribute is somewhere else (might be on a method)
if ($collecting_pre_class && !$class_found) {
$pre_class_content .= $attr_content;
} else {
$output .= $attr_content;
}
}
$in_attribute = false;
continue;
}
// Handle class - this is where we organize everything
if ($token[0] === T_CLASS) {
if (!$uses_written) {
// Write namespace if not written yet
if (!$namespace_written && $namespace) {
$output = rtrim($output) . "\n\nnamespace $namespace;";
$namespace_written = true;
}
// Add use statements
if (!empty($uses)) {
$output = rtrim($output) . "\n";
foreach ($uses as $use) {
$output .= "\nuse $use;";
}
}
// Add pre-class content (comments, constants, etc.)
if (trim($pre_class_content) !== '') {
// Clean up multiple blank lines in pre-class content
$pre_class_lines = explode("\n", $pre_class_content);
$cleaned_pre_class = [];
$prev_blank = false;
foreach ($pre_class_lines as $line) {
$is_blank = trim($line) === '';
if (!$is_blank || !$prev_blank) {
$cleaned_pre_class[] = $line;
}
$prev_blank = $is_blank;
}
$pre_class_content = implode("\n", $cleaned_pre_class);
$output = rtrim($output) . "\n\n" . ltrim($pre_class_content);
}
// Add blank line before attributes/class if there's content above
if (trim($output) !== '<?php' && trim($output) !== '') {
$output = rtrim($output) . "\n\n";
}
// Add attributes directly above class
if ($attributes !== '') {
$output .= rtrim($attributes) . "\n";
}
$uses_written = true;
}
$class_found = true;
$output .= $token[1];
continue;
}
// Collect pre-class content
if ($collecting_pre_class && !$class_found && !$in_attribute) {
// Skip namespace and use statements (already handled)
if ($token[0] !== T_NAMESPACE && $token[0] !== T_USE) {
$pre_class_content .= $token[1];
}
} else if (!$in_attribute) {
// Output other tokens
$output .= $token[1];
}
} else {
// Handle non-array tokens (like ; or {)
if ($collecting_pre_class && !$class_found && !$in_attribute) {
$pre_class_content .= $token;
} else if (!$in_attribute) {
$output .= $token;
}
}
}
// Clean up multiple blank lines in final output
$lines = explode("\n", $output);
$cleaned_lines = [];
$prev_blank = false;
foreach ($lines as $line) {
$is_blank = trim($line) === '';
if (!$is_blank || !$prev_blank) {
$cleaned_lines[] = $line;
}
$prev_blank = $is_blank;
}
return implode("\n", $cleaned_lines);
}
/**
* Get namespace from file path
*/
private function get_namespace_from_path($file_path)
{
$relative_path = $this->get_relative_path($file_path);
// Remove .php extension
if (substr($relative_path, -4) === '.php') {
$relative_path = substr($relative_path, 0, -4);
}
// Convert path to namespace
$parts = explode('/', $relative_path);
// Remove 'rsx' and capitalize parts
if ($parts[0] === 'rsx') {
array_shift($parts);
}
// Remove filename (last part)
array_pop($parts);
// Capitalize and join
$namespace_parts = [];
foreach ($parts as $part) {
// Replace underscores and hyphens with spaces, capitalize each word, then remove spaces
$words = explode(' ', str_replace(['_', '-'], ' ', $part));
$capitalized = '';
foreach ($words as $word) {
$capitalized .= ucfirst($word);
}
$namespace_parts[] = $capitalized;
}
$result = 'Rsx';
if (!empty($namespace_parts)) {
$result .= '\\' . implode('\\', $namespace_parts);
}
return $result;
}
/**
* Update RSX files in directory
*/
private function update_rsx_files_in_directory($dir_path)
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir_path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile() && substr($file->getPathname(), -4) === '.php') {
if (substr($file->getPathname(), -10) !== '.blade.php') {
if ($this->should_update_rsx_file($file->getPathname())) {
$this->update_rsx_file($file->getPathname());
}
}
}
}
}
/**
* Get relative path from project root
*/
private function get_relative_path($path)
{
// Normalize both paths to use forward slashes for comparison
$normalized_path = str_replace('\\', '/', $path);
$normalized_root = str_replace('\\', '/', $this->project_root) . '/';
if (substr($normalized_path, 0, strlen($normalized_root)) === $normalized_root) {
$relative = substr($normalized_path, strlen($normalized_root));
// Always return with forward slashes for consistency in namespace calculation
return $relative;
}
// Return with forward slashes for consistency
return str_replace('\\', '/', $path);
}
/**
* Show help message
*/
private function show_help()
{
$this->output("RSX Code Formatter");
$this->output("");
$this->output("Usage: rsx-format [options] [files...]");
$this->output("");
$this->output("Options:");
$this->output(" -v, --verbose Show detailed output");
$this->output(" -h, --help Show this help message");
$this->output("");
$this->output("Examples:");
$this->output(" rsx-format # Format all files in current directory");
$this->output(" rsx-format app/ # Format all files in app directory");
$this->output(" rsx-format app/Models/User.php # Format specific file");
}
/**
* Output helpers
*/
private function output($message)
{
echo $message . PHP_EOL;
}
private function success($message)
{
echo "\033[32m" . $message . "\033[0m" . PHP_EOL;
}
private function info($message)
{
echo "\033[36m" . $message . "\033[0m" . PHP_EOL;
}
private function warning($message)
{
echo "\033[33m" . $message . "\033[0m" . PHP_EOL;
}
private function error($message)
{
echo "\033[31m" . $message . "\033[0m" . PHP_EOL;
}
}
// Run the formatter
$formatter = new RsxFormatter();
$formatter->run($argv);