diff --git a/.gitattributes b/.gitattributes index 9670e954e..ed8103553 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,10 @@ -.gitattributes export-ignore -.gitignore export-ignore -.github export-ignore -ncs.* export-ignore -phpstan.neon export-ignore -tests/ export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +CLAUDE.md export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore +tests/ export-ignore -*.sh eol=lf -*.php* diff=php linguist-language=PHP +*.php* diff=php +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index bb7a71c46..bb2775cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /node_modules /package-lock.json /tools/create-phar/tracy.phar +tests/lock diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..d95b0fa5f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,523 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Tracy is a debugging and error visualization library for PHP (8.2-8.5). It provides beautiful error pages (BlueScreen), an interactive debug toolbar (Bar), advanced variable dumping, and production-ready error logging. + +**Key features:** +- BlueScreen: Beautiful error/exception visualization with stack traces +- Tracy Bar: Floating debug toolbar with extensible panel system +- Dumper: Advanced variable dumping with multiple output formats +- Logger: Error logging with email notifications +- Production/development mode auto-detection + +## Essential Commands + +### Testing +```bash +# Run all tests +composer run tester + +# Run tests manually with specific SAPI +vendor/bin/tester tests -p php -s -C +vendor/bin/tester tests -p php-cgi -s -C + +# Run single test file +vendor/bin/tester tests/Tracy/Debugger.timer().phpt -s -C + +# Run tests in specific directory +vendor/bin/tester tests/Dumper/ -s -C +``` + +### Code Quality +```bash +# Run PHPStan static analysis (level 5) +composer run phpstan + +# Lint JavaScript assets +npm run lint +npm run lint:fix +``` + +## Core Architecture + +### Main Components (Facade Pattern) + +**Tracy\Debugger** (src/Tracy/Debugger/Debugger.php) - Central facade +- `enable()` - Initialize Tracy +- `dump()` / `bdump()` - Variable dumping +- `log()` - Error logging +- `timer()` - Performance profiling +- Global functions available: `dump()`, `dumpe()`, `bdump()` + +**Strategy Pattern:** +- `DevelopmentStrategy` - Shows full debug info, Tracy Bar +- `ProductionStrategy` - Logs errors, shows user-friendly messages +- Auto-detection: localhost = development, otherwise production + +### Core Components + +**BlueScreen** (src/Tracy/BlueScreen/) +- Error/exception page rendering with stack traces +- Multiple output formats (HTML, CLI, text) +- Extensible via `addPanel()` callbacks +- Main class: `BlueScreen.php` (513 lines) + +**Bar** (src/Tracy/Bar/) +- Debug toolbar with panel system +- Interface: `IBarPanel` for custom panels +- Built-in panels: dumps, info, warnings +- AJAX request tracking via session storage +- Main class: `Bar.php` (164 lines) + +**Dumper** (src/Tracy/Dumper/) +- Component architecture: + - `Describer` - Analyzes variable structure + - `Exposer` - Extracts object properties (including private/protected) + - `Renderer` - Formats output (HTML/CLI/Text) + - `Value` - Represents dumped values +- Supports lazy loading, themes (light/dark) +- Main class: `Dumper.php` (264 lines) + +**Logger** (src/Tracy/Logger/) +- File-based error logging +- Email notifications via `ILogger` interface +- Severity filtering +- PSR-3 compatible via adapters in `Bridges/Psr/` +- Main class: `Logger.php` (198 lines) + +**Session Storage** (src/Tracy/Session/) +- `FileSession` - Custom file-based storage (default) +- `NativeSession` - PHP session integration +- Used for AJAX/redirect request tracking + +### Directory Structure + +``` +src/ +├── Bridges/ # Framework integrations +│ ├── Nette/ # Nette DI, Mail integration +│ └── Psr/ # PSR-3 logger adapters +└── Tracy/ # Core library + ├── Bar/ # Debug toolbar + ├── BlueScreen/ # Error visualization + ├── Debugger/ # Main facade & strategies + ├── Dumper/ # Variable dumping engine + ├── Logger/ # Error logging + ├── Session/ # Session storage + ├── OutputDebugger/ # Output buffer debugging + └── assets/ # Shared JavaScript utilities + +tests/ # 118 .phpt test files +examples/ # Live examples and demos +tools/ # Utilities (phar creation, editor integration) +``` + +## Testing Conventions + +**Test Framework:** Nette Tester (not PHPUnit) + +**Test file structure:** +```php + $object->method(), + ExpectedException::class, + 'Expected message with %a% placeholders', +); +``` + +**CI Testing:** +- Tests run on PHP 8.2, 8.3, 8.4, 8.5 +- Both `php` and `php-cgi` SAPI +- Ubuntu and Windows + +## Code Style + +**Nette Coding Standard** (based on PSR-12): +- `declare(strict_types=1)` in every PHP file +- Tab indentation +- Return type and opening brace on separate lines: +```php +public function example( + string $param, + array $options, +): ReturnType +{ + // method body +} +``` +- Configuration: `ncs.php` +- No space before parentheses in arrow functions: `fn($a) => $b` + +**JavaScript:** +- ESLint with `@nette/eslint-plugin` +- Consistent with Nette JavaScript patterns + +## Configuration + +### Logger Configuration +```php +$logger = Debugger::getLogger(); + +// Email notifications +$logger->email = 'dev@example.com'; // (string|string[]) email(s) for error notifications +$logger->fromEmail = 'me@example.com'; // (string) sender email +$logger->mailer = /* callable */; // custom email sender, defaults to mail() +$logger->emailSnooze = '2 days'; // minimum interval for sending emails + +// Log severity - which error levels are logged with HTML report +Debugger::$logSeverity = E_WARNING | E_NOTICE; +``` + +### Dumper Configuration +```php +Debugger::$maxLength = 150; // maximum string length in dumps +Debugger::$maxDepth = 10; // maximum nesting depth +Debugger::$keysToHide = ['password', 'secret', 'token']; // hide sensitive keys +Debugger::$dumpTheme = 'dark'; // 'light' or 'dark' +Debugger::$showLocation = true; // show dump() call location +``` + +### Other Configuration +```php +Debugger::$strictMode = true; // display notices/warnings as BlueScreen +Debugger::$scream = true; // display silenced (@) errors +Debugger::$editor = 'editor://open/?file=%file&line=%line'; // editor link format +Debugger::$errorTemplate = 'path/to/500.phtml'; // custom error 500 page +Debugger::$showBar = true; // show Tracy Bar + +// Editor path mapping (e.g., for Docker/remote servers) +Debugger::$editorMapping = [ + '/var/www/html' => '/local/project/path', + '/home/web' => '/Users/dev/projects', +]; +``` + +### Nette Framework Configuration + +In `config/common.neon`: +```neon +tracy: + # Logging + email: dev@example.com + fromEmail: robot@example.com + emailSnooze: 2 days + logSeverity: [E_WARNING, E_NOTICE] + + # Dumper + maxLength: 150 + maxDepth: 10 + keysToHide: [password, pass, secret] + dumpTheme: dark + showLocation: true + + # Other + strictMode: true + scream: false + editor: 'editor://open/?file=%file&line=%line' + showBar: true + + # Custom panels + bar: + - MyPanel(@MyService) + - Nette\Bridges\DITracy\ContainerPanel + + # BlueScreen extensions + blueScreen: + - DoctrinePanel::renderException + + editorMapping: + /var/www/html: /local/path +``` + +**DI Services available:** +- `tracy.logger` (Tracy\ILogger) +- `tracy.blueScreen` (Tracy\BlueScreen) +- `tracy.bar` (Tracy\Bar) + +## Extension Points + +### Custom Tracy Bar Panels + +Implement `Tracy\IBarPanel` interface: +```php +class MyPanel implements Tracy\IBarPanel +{ + public function getTab(): string + { + // Tab HTML (small label on Bar) + return << + ... + My Panel + + HTML; + } + + public function getPanel(): string + { + // Panel HTML (popup content) + return <<My Panel Title +
+
+ + +
InfoValue
+
+
+ HTML; + } +} + +// Register +Tracy\Debugger::getBar()->addPanel(new MyPanel); +``` + +**Panel styling:** +- Use classes, not IDs: `tracy-addons-[-optional]` +- Prefix selectors: `#tracy-debug .your-class` +- Elements ``, ``, `
`, `` have predefined styles
+- Toggle elements: use `tracy-toggle` class with matching `href` and `id`
+
+### BlueScreen Extensions
+
+Add custom sections to error pages:
+```php
+Tracy\Debugger::getBlueScreen()->addPanel(function (?Throwable $e) {
+	// Called twice: first with exception, then with null
+	// First call renders at top, second call below call stack
+	return [
+		'tab' => 'Database Queries',
+		'panel' => '

Queries

' . implode("\n", $queries) . '
', + 'bottom' => true, // render at very bottom + ]; +}); +``` + +### Custom Loggers + +Implement `Tracy\ILogger` interface: +```php +class SlackLogger implements Tracy\ILogger +{ + public function log($value, $priority = self::INFO) + { + // Send to Slack, Sentry, etc. + } +} + +Tracy\Debugger::setLogger(new SlackLogger); +``` + +**Monolog integration:** +```php +$monolog = new Monolog\Logger('main-channel'); +$monolog->pushHandler(new Monolog\Handler\StreamHandler($logFilePath)); + +$tracyLogger = new Tracy\Bridges\Psr\PsrToTracyLoggerAdapter($monolog); +Debugger::setLogger($tracyLogger); +``` + +### Custom Scrubber (Hide Sensitive Data) + +```php +// Prevent dumping password values +$scrubber = function(string $key, $value, ?string $class): bool { + return preg_match('#password|secret|token#i', $key) && $value !== null; +}; + +Tracy\Debugger::getBlueScreen()->scrubber = $scrubber; +``` + +### Custom Dump Formatting + +Add object exporters via `Dumper::addExporter()` + +## Integration Patterns + +**Basic usage:** +```php +Tracy\Debugger::enable(); // Auto-detect mode +Tracy\Debugger::enable(Tracy\Debugger::Development); // Force mode +Tracy\Debugger::enable('secret@123.45.67.89'); // IP + cookie +``` + +**Nette Framework:** +- `TracyExtension` provides DI integration +- Automatic configuration via NEON + +**PSR-3 Logging:** +- `PsrToTracyLoggerAdapter` - Use PSR-3 logger with Tracy +- `TracyToPsrLoggerAdapter` - Use Tracy as PSR-3 logger + +## Asset Management + +JavaScript and templates are embedded in PHP source files: +- Bar assets: `src/Tracy/Bar/assets/` +- BlueScreen assets: `src/Tracy/BlueScreen/assets/` +- Dumper assets: `src/Tracy/Dumper/assets/` +- Shared utilities: `src/Tracy/assets/` + +## Important Notes + +**Current version:** 2.11.0 (branch: v2.11) + +**Development directories:** +- `exam/` - Experimental/development files +- `x/` - Scratch work (git-ignored) + +**Mode detection:** +- Development: localhost (127.0.0.1 or ::1) without proxy +- Production: all other environments +- Override with `Debugger::enable($mode)` or IP addresses + +**Error handling:** +- Tracy changes error reporting to E_ALL on enable +- Use `Debugger::$strictMode` to display notices as errors +- Production mode logs errors instead of displaying them + +## Practical Recipes + +### AJAX Request Debugging + +Tracy automatically captures AJAX requests made with jQuery or native `fetch` API. They appear as additional rows in Tracy Bar. + +**Disable automatic capture:** +```js +window.TracyAutoRefresh = false; +``` + +**Manual AJAX monitoring:** +```js +fetch(url, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Tracy-Ajax': Tracy.getAjaxHeader(), + } +}) +``` + +### Content Security Policy (CSP) + +Tracy requires CSP adjustments to work properly: + +**Nette Framework:** +```neon +http: + csp: + script-src: [nonce, strict-dynamic] +``` + +**Pure PHP:** +```php +$nonce = base64_encode(random_bytes(20)); +header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic';"); +``` + +**Note:** `style-src` doesn't support nonce; use `'unsafe-inline'` (avoid in production) + +### Performance Optimization + +If slow scripts delay Tracy loading, render the loader early: + +```html + + + + Page Title + + + + +``` + +### Session Storage + +**Use native PHP session:** +```php +session_start(); +Debugger::setSessionStorage(new Tracy\NativeSession); +Debugger::enable(); +``` + +**Complex session initialization:** +```php +Debugger::setSessionStorage(new Tracy\NativeSession); +Debugger::enable(); + +// Custom session initialization +session_start(); + +Debugger::dispatch(); // Inform Tracy session is ready +``` + +### nginx Configuration + +If Tracy doesn't work on nginx, fix the `try_files` directive: + +```nginx +# Wrong +try_files $uri $uri/ /index.php; + +# Correct +try_files $uri $uri/ /index.php$is_args$args; +``` + +### IDE Integration + +Tracy can open files directly in your editor when clicking file names in error pages. + +**Editor integration scripts:** +- Windows: `tools/open-in-editor/windows/` +- Linux: `tools/open-in-editor/linux/` +- macOS: Use built-in URL schemes + +**Built-in editor URLs (macOS):** +```php +// PhpStorm +Tracy\Debugger::$editor = 'phpstorm://open?file=%file&line=%line'; + +// VS Code +Tracy\Debugger::$editor = 'vscode://file/%file:%line'; + +// TextMate +Tracy\Debugger::$editor = 'txmt://open/?url=file://%file&line=%line'; +``` + +**Editor path mapping for remote/Docker:** +```php +Debugger::$editorMapping = [ + '/var/www/html' => 'W:\\Projects\\myapp', // Docker to Windows + '/app' => '/Users/dev/projects/myapp', // Container to macOS +]; +``` diff --git a/composer.json b/composer.json index 00df1da0a..9e04b3a90 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.11-dev" + "dev-master": "3.0-dev" } } } diff --git a/examples/dump.php b/examples/dump.php index fc007ea74..0037d1365 100644 --- a/examples/dump.php +++ b/examples/dump.php @@ -35,6 +35,14 @@ echo "

Objects

\n"; echo "

Hover over the name \$baz to see property declaring class and over the hash #5 to see same objects.

\n"; +enum Suit +{ + case Clubs; + case Diamonds; + case Hearts; + case Spades; +} + class ParentClass { public $foo = [10, 20]; @@ -42,6 +50,8 @@ class ParentClass protected $bar = 30; private $baz = 'parent'; + + public Suit $enum = Suit::Clubs; } #[\AllowDynamicProperties] @@ -87,7 +97,8 @@ class ChildClass extends ParentClass $arr = [ fopen(__FILE__, 'r'), - new class {}, + new class { +}, function ($x, $y) use (&$arr, $obj) {}, ]; diff --git a/src/Bridges/Nette/Bridge.php b/src/Bridges/Nette/Bridge.php index 1f095716c..daeed1000 100644 --- a/src/Bridges/Nette/Bridge.php +++ b/src/Bridges/Nette/Bridge.php @@ -24,11 +24,12 @@ class Bridge public static function initialize(): void { $blueScreen = Tracy\Debugger::getBlueScreen(); - $blueScreen->addAction([self::class, 'renderMemberAccessException']); - $blueScreen->addPanel([self::class, 'renderNeonError']); + $blueScreen->addAction(self::renderMemberAccessException(...)); + $blueScreen->addPanel(self::renderNeonError(...)); } + /** @return array{link: string, label: string}|null */ public static function renderMemberAccessException(?\Throwable $e): ?array { if (!$e instanceof Nette\MemberAccessException && !$e instanceof \LogicException) { @@ -61,6 +62,7 @@ public static function renderMemberAccessException(?\Throwable $e): ?array } + /** @return array{tab: string, panel: string}|null */ public static function renderNeonError(?\Throwable $e): ?array { if (!$e instanceof Nette\Neon\Exception || !preg_match('#line (\d+)#', $e->getMessage(), $m)) { diff --git a/src/Bridges/Nette/MailSender.php b/src/Bridges/Nette/MailSender.php index fdb8bc081..23f212ab3 100644 --- a/src/Bridges/Nette/MailSender.php +++ b/src/Bridges/Nette/MailSender.php @@ -18,20 +18,13 @@ */ class MailSender { - private Nette\Mail\Mailer $mailer; - - /** sender of email notifications */ - private ?string $fromEmail = null; - - /** actual host on which notification occurred */ - private ?string $host = null; - - - public function __construct(Nette\Mail\Mailer $mailer, ?string $fromEmail = null, ?string $host = null) - { - $this->mailer = $mailer; - $this->fromEmail = $fromEmail; - $this->host = $host; + public function __construct( + private readonly Nette\Mail\Mailer $mailer, + /** sender of email notifications */ + private readonly ?string $fromEmail = null, + /** actual host on which notification occurred */ + private readonly ?string $host = null, + ) { } @@ -42,7 +35,7 @@ public function send(mixed $message, string $email): void $mail = new Nette\Mail\Message; $mail->setHeader('X-Mailer', 'Tracy'); if ($this->fromEmail || Nette\Utils\Validators::isEmail("noreply@$host")) { - $mail->setFrom($this->fromEmail ?: "noreply@$host"); + $mail->setFrom($this->fromEmail ?? "noreply@$host"); } foreach (explode(',', $email) as $item) { diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php index d4244cc16..30185bc6e 100644 --- a/src/Bridges/Nette/TracyExtension.php +++ b/src/Bridges/Nette/TracyExtension.php @@ -25,8 +25,8 @@ class TracyExtension extends Nette\DI\CompilerExtension public function __construct( - private bool $debugMode = false, - private bool $cliMode = false, + private readonly bool $debugMode = false, + private readonly bool $cliMode = false, ) { } @@ -130,8 +130,8 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void if ($this->debugMode) { foreach ($this->config->bar as $item) { - if (is_string($item) && substr($item, 0, 1) === '@') { - $item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]); + if (is_string($item) && str_starts_with($item, '@')) { + $item = new Statement(['@' . $builder::ThisContainer, 'getService'], [substr($item, 1)]); } elseif (is_string($item)) { $item = new Statement($item); } diff --git a/src/Bridges/Psr/PsrToTracyLoggerAdapter.php b/src/Bridges/Psr/PsrToTracyLoggerAdapter.php index 8bf66781a..33a843a32 100644 --- a/src/Bridges/Psr/PsrToTracyLoggerAdapter.php +++ b/src/Bridges/Psr/PsrToTracyLoggerAdapter.php @@ -31,12 +31,12 @@ class PsrToTracyLoggerAdapter implements Tracy\ILogger public function __construct( - private Psr\Log\LoggerInterface $psrLogger, + private readonly Psr\Log\LoggerInterface $psrLogger, ) { } - public function log(mixed $value, string $level = self::INFO) + public function log(mixed $value, string $level = self::INFO): void { if ($value instanceof \Throwable) { $message = get_debug_type($value) . ': ' . $value->getMessage() . ($value->getCode() ? ' #' . $value->getCode() : '') . ' in ' . $value->getFile() . ':' . $value->getLine(); diff --git a/src/Bridges/Psr/TracyToPsrLoggerAdapter.php b/src/Bridges/Psr/TracyToPsrLoggerAdapter.php index 7b1f578e9..a52fe283a 100644 --- a/src/Bridges/Psr/TracyToPsrLoggerAdapter.php +++ b/src/Bridges/Psr/TracyToPsrLoggerAdapter.php @@ -32,7 +32,7 @@ class TracyToPsrLoggerAdapter extends Psr\Log\AbstractLogger public function __construct( - private Tracy\ILogger $tracyLogger, + private readonly Tracy\ILogger $tracyLogger, ) { } diff --git a/src/Tracy/Bar/Bar.php b/src/Tracy/Bar/Bar.php index ddc3a47a8..994715422 100644 --- a/src/Tracy/Bar/Bar.php +++ b/src/Tracy/Bar/Bar.php @@ -105,13 +105,14 @@ public function render(DeferredContent $defer): void } else { $nonceAttr = Helpers::getNonceAttr(); $async = false; - Debugger::removeOutputBuffers(false); + Debugger::removeOutputBuffers(errorOccurred: false); require __DIR__ . '/assets/loader.phtml'; } } } + /** @return array{bar: string, panels: string} */ private function renderPartial(string $type, string $suffix = ''): array { $panels = $this->renderPanels($suffix); @@ -127,6 +128,7 @@ private function renderPartial(string $type, string $suffix = ''): array } + /** @return \stdClass[] */ private function renderPanels(string $suffix = ''): array { set_error_handler(function (int $severity, string $message, string $file, int $line) { diff --git a/src/Tracy/Bar/DefaultBarPanel.php b/src/Tracy/Bar/DefaultBarPanel.php index 262bc4c13..d968b8ee2 100644 --- a/src/Tracy/Bar/DefaultBarPanel.php +++ b/src/Tracy/Bar/DefaultBarPanel.php @@ -19,12 +19,10 @@ class DefaultBarPanel implements IBarPanel { public $data; - private $id; - - public function __construct(string $id) - { - $this->id = $id; + public function __construct( + private readonly string $id, + ) { } diff --git a/src/Tracy/BlueScreen/BlueScreen.php b/src/Tracy/BlueScreen/BlueScreen.php index 4596b7bb7..5753a2751 100644 --- a/src/Tracy/BlueScreen/BlueScreen.php +++ b/src/Tracy/BlueScreen/BlueScreen.php @@ -30,7 +30,7 @@ class BlueScreen public int $maxLength = 150; public int $maxItems = 100; - /** @var callable|null a callable returning true for sensitive data; fn(string $key, mixed $val): bool */ + /** @var (callable(string $key, mixed $value, ?string $class): bool)|null */ public $scrubber; /** @var string[] */ @@ -41,12 +41,16 @@ class BlueScreen public bool $showEnvironment = true; - /** @var callable[] */ + /** @var array<\Closure(?\Throwable): ?array{tab: string, panel: string}> */ private array $panels = []; - /** @var callable[] functions that returns action for exceptions */ + /** @var array<\Closure(\Throwable): ?array{link: string, label: string}> */ private array $actions = []; + + /** @var array<\Closure(string, ?string): ?string> */ private array $fileGenerators = []; + + /** @var Dumper\Value[]|null */ private ?array $snapshot = null; /** @var \WeakMap<\Fiber|\Generator> */ @@ -58,18 +62,20 @@ public function __construct() $this->collapsePaths = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy/BlueScreen$#', strtr(__DIR__, '\\', '/'), $m) ? [$m[1] . '/tracy', $m[1] . '/nette', $m[1] . '/latte'] : [dirname(__DIR__)]; - $this->fileGenerators[] = [self::class, 'generateNewPhpFileContents']; + $this->fileGenerators[] = self::generateNewPhpFileContents(...); $this->fibers = new \WeakMap; } /** - * Add custom panel as function (?\Throwable $e): ?array + * Add custom panel. + * @param callable(?\Throwable): ?array{tab: string, panel: string} $panel * @return static */ public function addPanel(callable $panel): self { - if (!in_array($panel, $this->panels, true)) { + $panel = $panel(...); + if (!in_array($panel, $this->panels, strict: true)) { $this->panels[] = $panel; } @@ -79,23 +85,24 @@ public function addPanel(callable $panel): self /** * Add action. + * @param callable(\Throwable): ?array{link: string, label: string} $action * @return static */ public function addAction(callable $action): self { - $this->actions[] = $action; + $this->actions[] = $action(...); return $this; } /** * Add new file generator. - * @param callable(string): ?string $generator + * @param callable(string, ?string): ?string $generator * @return static */ public function addFileGenerator(callable $generator): self { - $this->fileGenerators[] = $generator; + $this->fileGenerators[] = $generator(...); return $this; } @@ -167,7 +174,7 @@ private function renderTemplate(\Throwable $exception, string $template, bool $t if (function_exists('apache_request_headers')) { $httpHeaders = apache_request_headers(); } else { - $httpHeaders = array_filter($_SERVER, fn($k) => strncmp($k, 'HTTP_', 5) === 0, ARRAY_FILTER_USE_KEY); + $httpHeaders = array_filter($_SERVER, fn($k) => str_starts_with($k, 'HTTP_'), ARRAY_FILTER_USE_KEY); $httpHeaders = array_combine(array_map(fn($k) => strtolower(strtr(substr($k, 5), '_', '-')), array_keys($httpHeaders)), $httpHeaders); } @@ -175,7 +182,7 @@ private function renderTemplate(\Throwable $exception, string $template, bool $t $snapshot = []; $dump = $this->getDumper(); - $css = array_map('file_get_contents', array_merge([ + $css = array_map(file_get_contents(...), array_merge([ __DIR__ . '/../assets/reset.css', __DIR__ . '/assets/bluescreen.css', __DIR__ . '/../assets/toggle.css', @@ -250,7 +257,7 @@ private function renderActions(\Throwable $ex): array if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{2,})+)\1#i', $ex->getMessage(), $m)) { $class = $m[2]; if ( - !class_exists($class, false) && !interface_exists($class, false) && !trait_exists($class, false) + !class_exists($class, autoload: false) && !interface_exists($class, autoload: false) && !trait_exists($class, autoload: false) && ($file = Helpers::guessClassFile($class)) && !@is_file($file) // @ - may trigger error ) { [$content, $line] = $this->generateNewFileContents($file, $class); @@ -321,7 +328,7 @@ public static function highlightFile( ? CodeHighlighter::highlightPhp($source, $line, $column) : '
' . CodeHighlighter::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $column) . '
'; - if ($editor = Helpers::editorUri($file, $line)) { + if ($editor = Helpers::editorUri($file, line: $line, column: $column)) { $source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); } @@ -356,7 +363,7 @@ public function isCollapsed(string $file): bool $file = strtr($file, '\\', '/') . '/'; foreach ($this->collapsePaths as $path) { $path = strtr($path, '\\', '/') . '/'; - if (strncmp($file, $path, strlen($path)) === 0) { + if (str_starts_with($file, $path)) { return true; } } @@ -397,7 +404,7 @@ public function formatMessage(\Throwable $exception): string function ($m) { if (isset($m[2]) && method_exists($m[1], $m[2])) { $r = new \ReflectionMethod($m[1], $m[2]); - } elseif (class_exists($m[1], false) || interface_exists($m[1], false)) { + } elseif (class_exists($m[1], autoload: false) || interface_exists($m[1], autoload: false)) { $r = new \ReflectionClass($m[1]); } @@ -441,7 +448,10 @@ private function renderPhpInfo(): void } - /** @internal */ + /** + * @return array{string, int} + * @internal + */ private function generateNewFileContents(string $file, ?string $class = null): array { foreach (array_reverse($this->fileGenerators) as $generator) { @@ -465,7 +475,7 @@ private function generateNewFileContents(string $file, ?string $class = null): a /** @internal */ public static function generateNewPhpFileContents(string $file, ?string $class = null): ?string { - if (substr($file, -4) !== '.php') { + if (!str_ends_with($file, '.php')) { return null; } @@ -483,6 +493,7 @@ public static function generateNewPhpFileContents(string $file, ?string $class = } + /** @return array{array, array} */ private function findGeneratorsAndFibers(object $object): array { $generators = $fibers = []; diff --git a/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml b/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml index 74b3744d8..f11007713 100644 --- a/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml +++ b/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml @@ -57,7 +57,7 @@ if (!$stack) {
- +
diff --git a/src/Tracy/BlueScreen/assets/section-stack-sourceFile.phtml b/src/Tracy/BlueScreen/assets/section-stack-sourceFile.phtml index 492a5269d..bde95a747 100644 --- a/src/Tracy/BlueScreen/assets/section-stack-sourceFile.phtml +++ b/src/Tracy/BlueScreen/assets/section-stack-sourceFile.phtml @@ -33,7 +33,7 @@ $sourceMapped = $sourceOriginal ? Debugger::mapSource($file, $line) : null;

File:

- +
diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 23b3c2f26..197f05a5a 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -19,7 +19,7 @@ */ class Debugger { - public const Version = '2.11.0'; + public const Version = '3.0-dev'; /** server modes for Debugger::enable() */ public const @@ -29,19 +29,19 @@ class Debugger public const CookieSecret = 'tracy-debug'; - /** @deprecated use Debugger::Version */ + #[\Deprecated('use Debugger::Version')] public const VERSION = self::Version; - /** @deprecated use Debugger::Development */ + #[\Deprecated('use Debugger::Development')] public const DEVELOPMENT = self::Development; - /** @deprecated use Debugger::Production */ + #[\Deprecated('use Debugger::Production')] public const PRODUCTION = self::Production; - /** @deprecated use Debugger::Detect */ + #[\Deprecated('use Debugger::Detect')] public const DETECT = self::Detect; - /** @deprecated use Debugger::CookieSecret */ + #[\Deprecated('use Debugger::CookieSecret')] public const COOKIE_SECRET = self::CookieSecret; /** in production mode is suppressed any debugging output */ @@ -72,7 +72,7 @@ class Debugger /** disables the @ (shut-up) operator so that notices and warnings are no longer hidden; if integer than it's matched against error severity */ public static bool|int $scream = false; - /** @var callable[] functions that are automatically called after fatal error */ + /** @var array functions that are automatically called after fatal error */ public static array $onFatalError = []; /********************* Debugger::dump() ****************d*g**/ @@ -126,7 +126,7 @@ class Debugger /** URI pattern mask to open editor */ public static ?string $editor = 'editor://%action/?file=%file&line=%line&search=%search&replace=%replace'; - /** replacements in path */ + /** @var array replacements in path */ public static array $editorMapping = []; /** command to open browser (use 'start ""' in Windows) */ @@ -141,8 +141,8 @@ class Debugger /** @var string[] */ public static array $customJsFiles = []; - /** @var callable[] */ - private static $sourceMappers = []; + /** @var array<\Closure(string, int): ?array{file: string, line: int, column?: int}> */ + private static array $sourceMappers = []; private static ?array $cpuUsage = null; @@ -185,7 +185,7 @@ public static function enable( } self::$reserved ??= str_repeat('t', self::$reservedMemorySize); - self::$time ??= $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true); + self::$time ??= $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(as_float: true); self::$obLevel ??= ob_get_level(); self::$cpuUsage ??= !self::$productionMode && function_exists('getrusage') ? getrusage() : null; @@ -226,7 +226,7 @@ public static function enable( self::exceptionHandler($e); exit(255); }); - set_error_handler([self::class, 'errorHandler']); + set_error_handler(self::errorHandler(...)); foreach ([ 'Bar/Bar', @@ -285,7 +285,7 @@ public static function isEnabled(): bool public static function shutdownHandler(): void { $error = error_get_last(); - if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) { + if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], strict: true)) { $e = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']); if (!empty($error['trace'])) { (new \ReflectionClass(\Exception::class))->getProperty('trace')->setValue($e, $error['trace']); @@ -317,12 +317,12 @@ public static function exceptionHandler(\Throwable $exception): void { $firstTime = (bool) self::$reserved; self::$reserved = null; - self::$obStatus = ob_get_status(true); + self::$obStatus = ob_get_status(full_status: true); @http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'MSIE ') ? 503 : 500); // may not have an effect Helpers::improveException($exception); - self::removeOutputBuffers(true); + self::removeOutputBuffers(errorOccurred: true); self::getStrategy()->handleException($exception, $firstTime); @@ -375,7 +375,7 @@ public static function removeOutputBuffers(bool $errorOccurred): void { while (ob_get_level() > self::$obLevel) { $status = ob_get_status(); - if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) { + if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], strict: true)) { break; } @@ -525,7 +525,7 @@ public static function dump(mixed $var, bool $return = false): mixed public static function timer(?string $name = null): float { static $time = []; - $now = hrtime(true); + $now = hrtime(as_number: true); $name ??= ''; $delta = isset($time[$name]) ? $now - $time[$name] : 0; $time[$name] = $now; @@ -579,14 +579,17 @@ public static function tryLog(mixed $message, string $level = ILogger::INFO): ?\ } - /** @internal */ + /** + * @param callable(string, int): ?array{file: string, line: int, column?: int} $mapper + * @internal + */ public static function addSourceMapper(callable $mapper): void { - self::$sourceMappers[] = $mapper; + self::$sourceMappers[] = $mapper(...); } - /** @return array{file: string, line: int, label: string, active: bool} */ + /** @return array{file: string, line: int, column: int, label: string, active: bool} */ public static function mapSource(string $file, int $line): ?array { foreach (self::$sourceMappers as $mapper) { @@ -618,6 +621,6 @@ public static function detectDebugMode(string|array|null $list = null): bool $list[] = '[::1]'; // workaround for PHP < 7.3.4 } - return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true); + return in_array($addr, $list, strict: true) || in_array("$secret@$addr", $list, strict: true); } } diff --git a/src/Tracy/Debugger/DeferredContent.php b/src/Tracy/Debugger/DeferredContent.php index 57260a5ae..ed8d30c0b 100644 --- a/src/Tracy/Debugger/DeferredContent.php +++ b/src/Tracy/Debugger/DeferredContent.php @@ -18,14 +18,13 @@ */ final class DeferredContent { - private SessionStorage $sessionStorage; - private string $requestId; + private readonly string $requestId; private bool $useSession = false; - public function __construct(SessionStorage $sessionStorage) - { - $this->sessionStorage = $sessionStorage; + public function __construct( + private readonly SessionStorage $sessionStorage, + ) { $this->requestId = $_SERVER['HTTP_X_TRACY_AJAX'] ?? Helpers::createId(); } @@ -113,7 +112,7 @@ public function sendAssets(): bool private function buildJsCss(): string { - $css = array_map('file_get_contents', array_merge([ + $css = array_map(file_get_contents(...), array_merge([ __DIR__ . '/../assets/reset.css', __DIR__ . '/../Bar/assets/bar.css', __DIR__ . '/../assets/toggle.css', @@ -133,7 +132,7 @@ private function buildJsCss(): string __DIR__ . '/../Dumper/assets/dumper.js', __DIR__ . '/../BlueScreen/assets/bluescreen.js', ]); - $js2 = array_map('file_get_contents', Debugger::$customJsFiles); + $js2 = array_map(file_get_contents(...), Debugger::$customJsFiles); $str = "'use strict'; (function(){ diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index e6bc46d38..4fd4e37c9 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -19,9 +19,9 @@ final class DevelopmentStrategy { public function __construct( - private Bar $bar, - private BlueScreen $blueScreen, - private DeferredContent $defer, + private readonly Bar $bar, + private readonly BlueScreen $blueScreen, + private readonly DeferredContent $defer, ) { } diff --git a/src/Tracy/Debugger/ProductionStrategy.php b/src/Tracy/Debugger/ProductionStrategy.php index 64fbd02c3..cd9f9e7c3 100644 --- a/src/Tracy/Debugger/ProductionStrategy.php +++ b/src/Tracy/Debugger/ProductionStrategy.php @@ -39,7 +39,7 @@ public function handleException(\Throwable $exception, bool $firstTime): void header('Content-Type: text/html; charset=UTF-8'); } - (fn($logged) => require Debugger::$errorTemplate ?: __DIR__ . '/assets/error.500.phtml')(!$e); + (fn($logged) => require Debugger::$errorTemplate ?? __DIR__ . '/assets/error.500.phtml')(!$e); } elseif (Helpers::isCli() && is_resource(STDERR)) { fwrite(STDERR, "ERROR: {$exception->getMessage()}\n" @@ -65,7 +65,7 @@ public function handleError( $err = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message) . " in $file:$line"; } - Debugger::tryLog($err, Debugger::ERROR); + Debugger::tryLog($err, Debugger::WARNING); } diff --git a/src/Tracy/Dumper/Describer.php b/src/Tracy/Dumper/Describer.php index 57fbed9e5..fc48f00a0 100644 --- a/src/Tracy/Dumper/Describer.php +++ b/src/Tracy/Dumper/Describer.php @@ -34,7 +34,7 @@ final class Describer public bool $debugInfo = false; public array $keysToHide = []; - /** @var (callable(string, mixed): bool)|null */ + /** @var (callable(string $key, mixed $value, ?string $class): bool)|null */ public $scrubber; public bool $location = false; @@ -54,7 +54,7 @@ final class Describer public function describe(mixed $var): \stdClass { - uksort($this->objectExposers, fn($a, $b): int => $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1); + uksort($this->objectExposers, fn($a, $b): int => $b === '' || (class_exists($a, autoload: false) && is_subclass_of($a, $b)) ? -1 : 1); try { return (object) [ @@ -118,6 +118,7 @@ private function describeString(string $s, int $depth = 0): Value|string } + /** @return Value|array */ private function describeArray(array $arr, int $depth = 0, ?int $refId = null): Value|array { if ($refId) { @@ -261,6 +262,7 @@ public function addPropertyTo( } + /** @return array|null */ private function exposeObject(object $obj, Value $value): ?array { foreach ($this->objectExposers as $type => $dumper) { @@ -312,6 +314,7 @@ public function describeEnumProperty(string $class, string $property, mixed $val } + /** @param array $arr */ public function getReferenceId(array $arr, string|int $key): ?int { return ($rr = \ReflectionReference::fromArrayElement($arr, $key)) @@ -330,6 +333,12 @@ private static function findLocation(): ?array $location = $item; continue; } elseif (isset($item['function'])) { + $exists = isset($item['class']) + ? method_exists($item['class'], $item['function']) + : function_exists($item['function']); + if (!$exists) { + continue; + } try { $reflection = isset($item['class']) ? new \ReflectionMethod($item['class'], $item['function']) diff --git a/src/Tracy/Dumper/Dumper.php b/src/Tracy/Dumper/Dumper.php index ca7938f2a..d56526525 100644 --- a/src/Tracy/Dumper/Dumper.php +++ b/src/Tracy/Dumper/Dumper.php @@ -66,12 +66,14 @@ class Dumper 'indent' => '1;30', ]; + /** @var array */ public static array $resources = [ 'stream' => 'stream_get_meta_data', 'stream-context' => 'stream_context_get_options', 'curl' => 'curl_getinfo', ]; + /** @var array */ public static array $objectExporters = [ \Closure::class => [Exposer::class, 'exposeClosure'], \UnitEnum::class => [Exposer::class, 'exposeEnum'], @@ -189,7 +191,7 @@ private function __construct(array $options = []) $describer->maxItems = (int) ($options[self::ITEMS] ?? $describer->maxItems); $describer->debugInfo = (bool) ($options[self::DEBUGINFO] ?? $describer->debugInfo); $describer->scrubber = $options[self::SCRUBBER] ?? $describer->scrubber; - $describer->keysToHide = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE] ?? [])); + $describer->keysToHide = array_flip(array_map(strtolower(...), $options[self::KEYS_TO_HIDE] ?? [])); $describer->resourceExposers = ($options['resourceExporters'] ?? []) + self::$resources; $describer->objectExposers = ($options[self::OBJECT_EXPORTERS] ?? []) + self::$objectExporters; $describer->enumProperties = self::$enumProperties; @@ -247,6 +249,7 @@ private function asTerminal(mixed $var, array $colors = []): string } + /** @param array{0?: Dumper\Value[], 1?: array} $snapshot */ public static function formatSnapshotAttribute(array &$snapshot): string { $res = "'" . Renderer::jsonEncode($snapshot[0] ?? []) . "'"; @@ -255,6 +258,7 @@ public static function formatSnapshotAttribute(array &$snapshot): string } + /** @param string[] $constants */ public static function addEnumProperty(string $class, string $property, array $constants, bool $set = false): void { self::$enumProperties["$class::$property"] = [$set, $constants]; diff --git a/src/Tracy/Dumper/Exposer.php b/src/Tracy/Dumper/Exposer.php index 773f54124..712a43c34 100644 --- a/src/Tracy/Dumper/Exposer.php +++ b/src/Tracy/Dumper/Exposer.php @@ -21,6 +21,11 @@ final class Exposer { public static function exposeObject(object $obj, Value $value, Describer $describer): void { + if (PHP_VERSION_ID >= 80400 && (new \ReflectionClass($obj))->isUninitializedLazyObject($obj)) { + self::exposeLazyObject($obj, $describer, $value); + return; + } + $values = get_mangled_object_vars($obj); $props = self::getProperties($obj::class); @@ -63,6 +68,7 @@ class: $class, } + /** @return array */ private static function getProperties(string $class): array { static $cache; @@ -190,6 +196,7 @@ public static function exposeFiber(\Fiber $fiber, Value $value, Describer $descr } + /** @return array{path: string} */ public static function exposeSplFileInfo(\SplFileInfo $obj): array { return ['path' => $obj->getPathname()]; @@ -276,4 +283,23 @@ public static function exposeDsMap( $describer->addPropertyTo($value, (string) $i++, new Ds\Pair($k, $v)); } } + + + private static function exposeLazyObject(object $obj, Describer $describer, Value $value): void + { + $rc = new \ReflectionClass($obj); + foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { + if (!$prop->isLazy($obj)) { + $describer->addPropertyTo( + $value, + $prop->getName(), + $prop->getValue($obj), + Value::PropertyPublic, + described: $describer->describeEnumProperty($obj::class, $prop->getName(), $prop->getValue($obj)), + ); + } + } + + $value->value .= ' (lazy)'; + } } diff --git a/src/Tracy/Dumper/Renderer.php b/src/Tracy/Dumper/Renderer.php index 5d9c9ecdc..662aba23f 100644 --- a/src/Tracy/Dumper/Renderer.php +++ b/src/Tracy/Dumper/Renderer.php @@ -38,7 +38,11 @@ final class Renderer /** @var Value[]|null */ private ?array $snapshotSelection = null; + + /** @var array */ private array $parents = []; + + /** @var array */ private array $above = []; @@ -104,7 +108,7 @@ public function renderAsText(\stdClass $model, array $colors = []): string $s = $colors ? Helpers::htmlToAnsi($s, $colors) : Helpers::htmlToText($s); $s = str_replace('…', '...', $s); - $s .= substr($s, -1) === "\n" ? '' : "\n"; + $s .= str_ends_with($s, "\n") ? '' : "\n"; if ($this->sourceLocation && ([$file, $line] = $model->location)) { $s .= "in $file:$line\n"; @@ -263,7 +267,7 @@ private function renderArray(array|Value $array, int $depth): string . ' => ' . ($ref && $this->hash ? '&' . $ref . ' ' : '') . ($tmp = $this->renderVar($v, $depth + 1)) - . (substr($tmp, -6) === '' ? '' : "\n"); + . (str_ends_with($tmp, '') ? '' : "\n"); } if ($count > count($items)) { @@ -340,7 +344,7 @@ private function renderObject(Value $object, int $depth): string . ': ' . ($ref && $this->hash ? '&' . $ref . ' ' : '') . ($tmp = $this->renderVar($v, $depth + 1)) - . (substr($tmp, -6) === '' ? '' : "\n"); + . (str_ends_with($tmp, '') ? '' : "\n"); } if ($object->length > count($object->items)) { @@ -377,7 +381,7 @@ private function renderResource(Value $resource, int $depth): string . $this->renderVar($k, $depth + 1, Value::PropertyVirtual) . ': ' . ($tmp = $this->renderVar($v, $depth + 1)) - . (substr($tmp, -6) === '' ? '' : "\n"); + . (str_ends_with($tmp, '') ? '' : "\n"); } return $out . ''; diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php index 8264fb6f1..973539ad0 100644 --- a/src/Tracy/Helpers.php +++ b/src/Tracy/Helpers.php @@ -57,6 +57,7 @@ public static function editorUri( string $action = 'open', string $search = '', string $replace = '', + ?int $column = null, ): ?string { if (Debugger::$editor && $file && ($action === 'create' || @is_file($file))) { // @ - may trigger error @@ -67,7 +68,7 @@ public static function editorUri( return strtr(Debugger::$editor, [ '%action' => $action, '%file' => rawurlencode($file), - '%line' => $line ?: 1, + '%line' => ($line ?: 1) . ($column ? ':' . $column : ''), '%search' => rawurlencode($search), '%replace' => rawurlencode($replace), ]); @@ -98,6 +99,11 @@ public static function htmlToText(string $s): string } + /** + * @param array $trace + * @param string|string[] $method + * @return array{file?: string, line?: int, class?: string, function?: string, args?: array}|null + */ public static function findTrace(array $trace, array|string $method, ?int &$index = null): ?array { $m = is_array($method) ? $method : explode('::', $method); @@ -145,7 +151,7 @@ public static function getSource(): string { if (self::isCli()) { return 'CLI (PID: ' . getmypid() . ')' - . (isset($_SERVER['argv']) ? ': ' . implode(' ', array_map([self::class, 'escapeArg'], $_SERVER['argv'])) : ''); + . (isset($_SERVER['argv']) ? ': ' . implode(' ', array_map(self::escapeArg(...), $_SERVER['argv'])) : ''); } elseif (isset($_SERVER['REQUEST_URI'])) { return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://') @@ -178,7 +184,7 @@ public static function improveException(\Throwable $e): void $message = str_replace($m[2], "but class '$arg[0]' does not exist", $message); } elseif (is_array($arg) && !method_exists($arg[0], $arg[1])) { $hint = self::getSuggestion(get_class_methods($arg[0]) ?: [], $arg[1]); - $class = is_object($arg[0]) ? get_class($arg[0]) : $arg[0]; + $class = is_object($arg[0]) ? $arg[0]::class : $arg[0]; $message = str_replace($m[2], "but method $class::$arg[1]() does not exist" . ($hint ? " (did you mean $hint?)" : ''), $message); } elseif (is_string($arg) && !function_exists($arg)) { $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']); @@ -585,10 +591,11 @@ public static function detectColors(): bool } + /** @return \Throwable[] */ public static function getExceptionChain(\Throwable $ex): array { $res = [$ex]; - while (($ex = $ex->getPrevious()) && !in_array($ex, $res, true)) { + while (($ex = $ex->getPrevious()) && !in_array($ex, $res, strict: true)) { $res[] = $ex; } @@ -596,6 +603,7 @@ public static function getExceptionChain(\Throwable $ex): array } + /** @param array $skip */ public static function traverseValue(mixed $val, callable $callback, array &$skip = [], ?string $refId = null): void { if (is_object($val)) { @@ -622,7 +630,11 @@ public static function traverseValue(mixed $val, callable $callback, array &$ski } - /** @internal */ + /** + * @param string[] $constants + * @return string[]|null + * @internal + */ public static function decomposeFlags(int $flags, bool $set, array $constants): ?array { $res = null; diff --git a/src/Tracy/Logger/Logger.php b/src/Tracy/Logger/Logger.php index 43fd3d201..77cd4fde8 100644 --- a/src/Tracy/Logger/Logger.php +++ b/src/Tracy/Logger/Logger.php @@ -18,23 +18,22 @@ */ class Logger implements ILogger { - /** @var string|null name of the directory where errors should be logged */ - public $directory; + /** name of the directory where errors should be logged */ + public ?string $directory = null; - /** @var string|array|null email or emails to which send error notifications */ - public $email; + /** email or emails to which send error notifications */ + public string|array|null $email = null; - /** @var string|null sender of email notifications */ - public $fromEmail; + /** sender of email notifications */ + public ?string $fromEmail = null; - /** @var mixed interval for sending email is 2 days */ - public $emailSnooze = '2 days'; + /** interval for sending email is 2 days */ + public mixed $emailSnooze = '2 days'; - /** @var callable handler for sending emails */ + /** @var callable(mixed $message, string $email): void handler for sending emails */ public $mailer; - /** @var BlueScreen|null */ - private $blueScreen; + private ?BlueScreen $blueScreen = null; public function __construct(?string $directory, string|array|null $email = null, ?BlueScreen $blueScreen = null) @@ -42,7 +41,7 @@ public function __construct(?string $directory, string|array|null $email = null, $this->directory = $directory; $this->email = $email; $this->blueScreen = $blueScreen; - $this->mailer = [$this, 'defaultMailer']; + $this->mailer = $this->defaultMailer(...); } @@ -73,7 +72,7 @@ public function log(mixed $message, string $level = self::INFO) $this->logException($message, $exceptionFile); } - if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) { + if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], strict: true)) { $this->sendEmail($message); } @@ -81,10 +80,7 @@ public function log(mixed $message, string $level = self::INFO) } - /** - * @param mixed $message - */ - public static function formatMessage($message): string + public static function formatMessage(mixed $message): string { if ($message instanceof \Throwable) { foreach (Helpers::getExceptionChain($message) as $exception) { @@ -104,10 +100,7 @@ public static function formatMessage($message): string } - /** - * @param mixed $message - */ - public static function formatLogLine($message, ?string $exceptionFile = null): string + public static function formatLogLine(mixed $message, ?string $exceptionFile = null): string { return implode(' ', [ date('[Y-m-d H-i-s]'), @@ -148,17 +141,14 @@ public function getExceptionFile(\Throwable $exception, string $level = self::EX */ protected function logException(\Throwable $exception, ?string $file = null): string { - $file = $file ?: $this->getExceptionFile($exception); - $bs = $this->blueScreen ?: new BlueScreen; + $file ??= $this->getExceptionFile($exception); + $bs = $this->blueScreen ?? new BlueScreen; $bs->renderToFile($exception, $file); return $file; } - /** - * @param mixed $message - */ - protected function sendEmail($message): void + protected function sendEmail(mixed $message): void { $snooze = is_numeric($this->emailSnooze) ? $this->emailSnooze @@ -177,10 +167,9 @@ protected function sendEmail($message): void /** * Default mailer. - * @param mixed $message * @internal */ - public function defaultMailer($message, string $email): void + public function defaultMailer(mixed $message, string $email): void { $host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n')); mail( @@ -188,7 +177,7 @@ public function defaultMailer($message, string $email): void "PHP: An error occurred on the server $host", static::formatMessage($message) . "\n\nsource: " . Helpers::getSource(), implode("\r\n", [ - 'From: ' . ($this->fromEmail ?: "noreply@$host"), + 'From: ' . ($this->fromEmail ?? "noreply@$host"), 'X-Mailer: Tracy', 'Content-Type: text/plain; charset=UTF-8', 'Content-Transfer-Encoding: 8bit', diff --git a/src/Tracy/OutputDebugger/OutputDebugger.php b/src/Tracy/OutputDebugger/OutputDebugger.php index fe9559086..26e532813 100644 --- a/src/Tracy/OutputDebugger/OutputDebugger.php +++ b/src/Tracy/OutputDebugger/OutputDebugger.php @@ -39,7 +39,7 @@ public function start(): void } } - ob_start([$this, 'handler'], 1); + ob_start($this->handler(...), 1); } diff --git a/src/Tracy/Session/FileSession.php b/src/Tracy/Session/FileSession.php index 6dd85ce45..b04b52544 100644 --- a/src/Tracy/Session/FileSession.php +++ b/src/Tracy/Session/FileSession.php @@ -22,16 +22,15 @@ class FileSession implements SessionStorage /** probability that the clean() routine is started */ public float $gcProbability = 0.03; - private string $dir; /** @var resource */ private $file; private array $data = []; - public function __construct(string $dir) - { - $this->dir = $dir; + public function __construct( + private readonly string $dir, + ) { } diff --git a/src/Tracy/Session/SessionStorage.php b/src/Tracy/Session/SessionStorage.php index 801012a73..2f3c69acc 100644 --- a/src/Tracy/Session/SessionStorage.php +++ b/src/Tracy/Session/SessionStorage.php @@ -14,5 +14,6 @@ interface SessionStorage { function isAvailable(): bool; + /** @return array */ function &getData(): array; } diff --git a/src/Tracy/functions.php b/src/Tracy/functions.php index ab00b0d7a..0228d8dfa 100644 --- a/src/Tracy/functions.php +++ b/src/Tracy/functions.php @@ -14,7 +14,7 @@ */ function dump(mixed $var): mixed { - array_map([Tracy\Debugger::class, 'dump'], func_get_args()); + array_map(Tracy\Debugger::dump(...), func_get_args()); return $var; } } @@ -26,7 +26,7 @@ function dump(mixed $var): mixed */ function dumpe(mixed $var): void { - array_map([Tracy\Debugger::class, 'dump'], func_get_args()); + array_map(Tracy\Debugger::dump(...), func_get_args()); if (!Tracy\Debugger::$productionMode) { exit; } diff --git a/tests/Tracy/Dumper.debugInfo.phpt b/tests/Dumper/Dumper.debugInfo.phpt similarity index 100% rename from tests/Tracy/Dumper.debugInfo.phpt rename to tests/Dumper/Dumper.debugInfo.phpt diff --git a/tests/Tracy/Dumper.dump().cli.phpt b/tests/Dumper/Dumper.dump().cli.phpt similarity index 100% rename from tests/Tracy/Dumper.dump().cli.phpt rename to tests/Dumper/Dumper.dump().cli.phpt diff --git a/tests/Tracy/Dumper.dump().html.phpt b/tests/Dumper/Dumper.dump().html.phpt similarity index 100% rename from tests/Tracy/Dumper.dump().html.phpt rename to tests/Dumper/Dumper.dump().html.phpt diff --git a/tests/Tracy/Dumper.dump().text.phpt b/tests/Dumper/Dumper.dump().text.phpt similarity index 100% rename from tests/Tracy/Dumper.dump().text.phpt rename to tests/Dumper/Dumper.dump().text.phpt diff --git a/tests/Tracy/Dumper.exposeObject.phpt b/tests/Dumper/Dumper.exposeObject.phpt similarity index 100% rename from tests/Tracy/Dumper.exposeObject.phpt rename to tests/Dumper/Dumper.exposeObject.phpt diff --git a/tests/Tracy/Dumper.keys.phpt b/tests/Dumper/Dumper.keys.phpt similarity index 100% rename from tests/Tracy/Dumper.keys.phpt rename to tests/Dumper/Dumper.keys.phpt diff --git a/tests/Tracy/Dumper.keysToHide.phpt b/tests/Dumper/Dumper.keysToHide.phpt similarity index 100% rename from tests/Tracy/Dumper.keysToHide.phpt rename to tests/Dumper/Dumper.keysToHide.phpt diff --git a/tests/Tracy/Dumper.nesting-error.phpt b/tests/Dumper/Dumper.nesting-error.phpt similarity index 100% rename from tests/Tracy/Dumper.nesting-error.phpt rename to tests/Dumper/Dumper.nesting-error.phpt diff --git a/tests/Tracy/Dumper.objectExporters.phpt b/tests/Dumper/Dumper.objectExporters.phpt similarity index 100% rename from tests/Tracy/Dumper.objectExporters.phpt rename to tests/Dumper/Dumper.objectExporters.phpt diff --git a/tests/Tracy/Dumper.references74.phpt b/tests/Dumper/Dumper.references74.phpt similarity index 100% rename from tests/Tracy/Dumper.references74.phpt rename to tests/Dumper/Dumper.references74.phpt diff --git a/tests/Tracy/Dumper.scrubber.phpt b/tests/Dumper/Dumper.scrubber.phpt similarity index 100% rename from tests/Tracy/Dumper.scrubber.phpt rename to tests/Dumper/Dumper.scrubber.phpt diff --git a/tests/Tracy/Dumper.sensitiveValue.phpt b/tests/Dumper/Dumper.sensitiveValue.phpt similarity index 100% rename from tests/Tracy/Dumper.sensitiveValue.phpt rename to tests/Dumper/Dumper.sensitiveValue.phpt diff --git a/tests/Tracy/Dumper.toHtml().collapse.phpt b/tests/Dumper/Dumper.toHtml().collapse.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().collapse.phpt rename to tests/Dumper/Dumper.toHtml().collapse.phpt diff --git a/tests/Tracy/Dumper.toHtml().depth.phpt b/tests/Dumper/Dumper.toHtml().depth.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().depth.phpt rename to tests/Dumper/Dumper.toHtml().depth.phpt diff --git a/tests/Tracy/Dumper.toHtml().key.phpt b/tests/Dumper/Dumper.toHtml().key.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().key.phpt rename to tests/Dumper/Dumper.toHtml().key.phpt diff --git a/tests/Tracy/Dumper.toHtml().lazy.auto.phpt b/tests/Dumper/Dumper.toHtml().lazy.auto.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().lazy.auto.phpt rename to tests/Dumper/Dumper.toHtml().lazy.auto.phpt diff --git a/tests/Tracy/Dumper.toHtml().lazy.no.phpt b/tests/Dumper/Dumper.toHtml().lazy.no.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().lazy.no.phpt rename to tests/Dumper/Dumper.toHtml().lazy.no.phpt diff --git a/tests/Tracy/Dumper.toHtml().lazy.phpt b/tests/Dumper/Dumper.toHtml().lazy.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().lazy.phpt rename to tests/Dumper/Dumper.toHtml().lazy.phpt diff --git a/tests/Tracy/Dumper.toHtml().live.phpt b/tests/Dumper/Dumper.toHtml().live.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().live.phpt rename to tests/Dumper/Dumper.toHtml().live.phpt diff --git a/tests/Tracy/Dumper.toHtml().locale.phpt b/tests/Dumper/Dumper.toHtml().locale.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().locale.phpt rename to tests/Dumper/Dumper.toHtml().locale.phpt diff --git a/tests/Tracy/Dumper.toHtml().location.phpt b/tests/Dumper/Dumper.toHtml().location.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().location.phpt rename to tests/Dumper/Dumper.toHtml().location.phpt diff --git a/tests/Tracy/Dumper.toHtml().phpt b/tests/Dumper/Dumper.toHtml().phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().phpt rename to tests/Dumper/Dumper.toHtml().phpt diff --git a/tests/Tracy/Dumper.toHtml().recursion.phpt b/tests/Dumper/Dumper.toHtml().recursion.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().recursion.phpt rename to tests/Dumper/Dumper.toHtml().recursion.phpt diff --git a/tests/Tracy/Dumper.toHtml().references.phpt b/tests/Dumper/Dumper.toHtml().references.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().references.phpt rename to tests/Dumper/Dumper.toHtml().references.phpt diff --git a/tests/Tracy/Dumper.toHtml().see.phpt b/tests/Dumper/Dumper.toHtml().see.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().see.phpt rename to tests/Dumper/Dumper.toHtml().see.phpt diff --git a/tests/Tracy/Dumper.toHtml().snapshot.phpt b/tests/Dumper/Dumper.toHtml().snapshot.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().snapshot.phpt rename to tests/Dumper/Dumper.toHtml().snapshot.phpt diff --git a/tests/Tracy/Dumper.toHtml().specialChars.phpt b/tests/Dumper/Dumper.toHtml().specialChars.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().specialChars.phpt rename to tests/Dumper/Dumper.toHtml().specialChars.phpt diff --git a/tests/Dumper/Dumper.toHtml().specials.lazyObject.phpt b/tests/Dumper/Dumper.toHtml().specials.lazyObject.phpt new file mode 100644 index 000000000..0ed950832 --- /dev/null +++ b/tests/Dumper/Dumper.toHtml().specials.lazyObject.phpt @@ -0,0 +1,56 @@ +newLazyGhost(function () {}); + +// new ghost +Assert::match( + <<<'XX' +
LazyClass (lazy) #%d%
+ XX, + Dumper::toHtml($ghost, [Dumper::DEPTH => 3]), +); + +// preinitialized property +$rc->getProperty('id')->setRawValueWithoutLazyInitialization($ghost, 123); + +Assert::match( + <<<'XX' +
LazyClass (lazy) #%d%
+		
id: 123 +
+ XX, + Dumper::toHtml($ghost, [Dumper::DEPTH => 3]), +); + +// proxy +$proxy = $rc->newLazyProxy(fn() => new LazyClass); +Assert::match( + <<<'XX' +
LazyClass (lazy) #%d%
+ XX, + Dumper::toHtml($proxy, [Dumper::DEPTH => 3]), +); diff --git a/tests/Tracy/Dumper.toHtml().specials.phpt b/tests/Dumper/Dumper.toHtml().specials.phpt similarity index 100% rename from tests/Tracy/Dumper.toHtml().specials.phpt rename to tests/Dumper/Dumper.toHtml().specials.phpt diff --git a/tests/Tracy/Dumper.toTerminal().phpt b/tests/Dumper/Dumper.toTerminal().phpt similarity index 100% rename from tests/Tracy/Dumper.toTerminal().phpt rename to tests/Dumper/Dumper.toTerminal().phpt diff --git a/tests/Tracy/Dumper.toText().depth.phpt b/tests/Dumper/Dumper.toText().depth.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().depth.phpt rename to tests/Dumper/Dumper.toText().depth.phpt diff --git a/tests/Tracy/Dumper.toText().enumProperty.PhpToken.phpt b/tests/Dumper/Dumper.toText().enumProperty.PhpToken.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().enumProperty.PhpToken.phpt rename to tests/Dumper/Dumper.toText().enumProperty.PhpToken.phpt diff --git a/tests/Tracy/Dumper.toText().enumProperty.phpt b/tests/Dumper/Dumper.toText().enumProperty.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().enumProperty.phpt rename to tests/Dumper/Dumper.toText().enumProperty.phpt diff --git a/tests/Tracy/Dumper.toText().locale.phpt b/tests/Dumper/Dumper.toText().locale.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().locale.phpt rename to tests/Dumper/Dumper.toText().locale.phpt diff --git a/tests/Tracy/Dumper.toText().location.phpt b/tests/Dumper/Dumper.toText().location.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().location.phpt rename to tests/Dumper/Dumper.toText().location.phpt diff --git a/tests/Tracy/Dumper.toText().nohash.phpt b/tests/Dumper/Dumper.toText().nohash.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().nohash.phpt rename to tests/Dumper/Dumper.toText().nohash.phpt diff --git a/tests/Tracy/Dumper.toText().phpt b/tests/Dumper/Dumper.toText().phpt similarity index 100% rename from tests/Tracy/Dumper.toText().phpt rename to tests/Dumper/Dumper.toText().phpt diff --git a/tests/Tracy/Dumper.toText().recursion.phpt b/tests/Dumper/Dumper.toText().recursion.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().recursion.phpt rename to tests/Dumper/Dumper.toText().recursion.phpt diff --git a/tests/Tracy/Dumper.toText().references.phpt b/tests/Dumper/Dumper.toText().references.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().references.phpt rename to tests/Dumper/Dumper.toText().references.phpt diff --git a/tests/Tracy/Dumper.toText().see.phpt b/tests/Dumper/Dumper.toText().see.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().see.phpt rename to tests/Dumper/Dumper.toText().see.phpt diff --git a/tests/Tracy/Dumper.toText().specialChars.phpt b/tests/Dumper/Dumper.toText().specialChars.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specialChars.phpt rename to tests/Dumper/Dumper.toText().specialChars.phpt diff --git a/tests/Tracy/Dumper.toText().specials.dom.php84.phpt b/tests/Dumper/Dumper.toText().specials.dom.php84.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.dom.php84.phpt rename to tests/Dumper/Dumper.toText().specials.dom.php84.phpt diff --git a/tests/Tracy/Dumper.toText().specials.dom.phpt b/tests/Dumper/Dumper.toText().specials.dom.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.dom.phpt rename to tests/Dumper/Dumper.toText().specials.dom.phpt diff --git a/tests/Tracy/Dumper.toText().specials.ds.phpt b/tests/Dumper/Dumper.toText().specials.ds.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.ds.phpt rename to tests/Dumper/Dumper.toText().specials.ds.phpt diff --git a/tests/Tracy/Dumper.toText().specials.enum.phpt b/tests/Dumper/Dumper.toText().specials.enum.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.enum.phpt rename to tests/Dumper/Dumper.toText().specials.enum.phpt diff --git a/tests/Tracy/Dumper.toText().specials.fiber.phpt b/tests/Dumper/Dumper.toText().specials.fiber.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.fiber.phpt rename to tests/Dumper/Dumper.toText().specials.fiber.phpt diff --git a/tests/Tracy/Dumper.toText().specials.generator.phpt b/tests/Dumper/Dumper.toText().specials.generator.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.generator.phpt rename to tests/Dumper/Dumper.toText().specials.generator.phpt diff --git a/tests/Dumper/Dumper.toText().specials.lazyObject.phpt b/tests/Dumper/Dumper.toText().specials.lazyObject.phpt new file mode 100644 index 000000000..a64bce661 --- /dev/null +++ b/tests/Dumper/Dumper.toText().specials.lazyObject.phpt @@ -0,0 +1,54 @@ +newLazyGhost(function () {}); + +// new ghost +Assert::match( + <<<'XX' + LazyClass (lazy) #%d% + XX, + Dumper::toText($ghost, [Dumper::DEPTH => 3]), +); + +// preinitialized property +$rc->getProperty('id')->setRawValueWithoutLazyInitialization($ghost, 123); + +Assert::match( + <<<'XX' + LazyClass (lazy) #%d% + id: 123 + XX, + Dumper::toText($ghost, [Dumper::DEPTH => 3]), +); + +// proxy +$proxy = $rc->newLazyProxy(fn() => new LazyClass); +Assert::match( + <<<'XX' + LazyClass (lazy) #%d% + XX, + Dumper::toText($proxy, [Dumper::DEPTH => 3]), +); diff --git a/tests/Tracy/Dumper.toText().specials.phpt b/tests/Dumper/Dumper.toText().specials.phpt similarity index 100% rename from tests/Tracy/Dumper.toText().specials.phpt rename to tests/Dumper/Dumper.toText().specials.phpt diff --git a/tests/Tracy/dump().cli.phpt b/tests/Dumper/dump().cli.phpt similarity index 100% rename from tests/Tracy/dump().cli.phpt rename to tests/Dumper/dump().cli.phpt diff --git a/tests/Tracy/dump().html.phpt b/tests/Dumper/dump().html.phpt similarity index 100% rename from tests/Tracy/dump().html.phpt rename to tests/Dumper/dump().html.phpt diff --git a/tests/Tracy/dump().text.phpt b/tests/Dumper/dump().text.phpt similarity index 100% rename from tests/Tracy/dump().text.phpt rename to tests/Dumper/dump().text.phpt diff --git a/tests/Tracy/fixtures/DumpClass.php b/tests/Dumper/fixtures/DumpClass.php similarity index 100% rename from tests/Tracy/fixtures/DumpClass.php rename to tests/Dumper/fixtures/DumpClass.php diff --git a/tools/open-in-editor/linux/open-editor.sh b/tools/open-in-editor/linux/open-editor.sh index 69e37be83..8cabf2789 100755 --- a/tools/open-in-editor/linux/open-editor.sh +++ b/tools/open-in-editor/linux/open-editor.sh @@ -6,7 +6,7 @@ declare -A mapping # # Visual Studio Code -#editor='code --goto "$FILE":"$LINE"' +#editor='code --goto "$FILE":"$LINE":"$COLUMN"' # Emacs #editor='emacs +$LINE "$FILE"' # gVim @@ -17,11 +17,9 @@ declare -A mapping #editor='pluma +$LINE "$FILE"' # PHPStorm # To enable PHPStorm command-line interface, folow this guide: https://www.jetbrains.com/help/phpstorm/working-with-the-ide-features-from-command-line.html -#editor='phpstorm --line $LINE "$FILE"' +#editor='phpstorm --line $LINE --column $COLUMN "$FILE"' # VS Codium #editor='codium --goto "$FILE":"$LINE"' -# Visual Studio Code -#editor='code --goto "$FILE":"$LINE"' # # Optionally configure custom mapping here: @@ -56,16 +54,21 @@ action=`echo $url | sed -r "s/$regex/\1/i"` uri_params=`echo $url | sed -r "s/$regex/\2/i"` file=`get_param $uri_params "file"` -line=`get_param $uri_params "line"` +line_param=`get_param $uri_params "line"` search=`get_param $uri_params "search"` replace=`get_param $uri_params "replace"` +# Parse line and column from line parameter (format: "12:5" or just "12") +IFS=':' read -r line column <<< "$line_param" +column="${column:-1}" + # Debug? #echo "action '$action'" #echo "file '$file'" #echo "line '$line'" #echo "search '$search'" #echo "replace '$replace'" +#echo "column '$column'" # Convert URI encoded codes to normal characters (e.g. '%2F' => '/'). printf -v file "${file//%/\\x}" @@ -102,6 +105,7 @@ fi # Format the command according to the selected editor. command="${editor//\$FILE/$file}" command="${command//\$LINE/$line}" +command="${command//\$COLUMN/$column}" # Debug? #echo $command diff --git a/tools/open-in-editor/windows/open-editor.js b/tools/open-in-editor/windows/open-editor.js index ac1093e70..86f699483 100644 --- a/tools/open-in-editor/windows/open-editor.js +++ b/tools/open-in-editor/windows/open-editor.js @@ -1,7 +1,7 @@ var settings = { // PhpStorm - // editor: '"C:\\Program Files\\JetBrains\\PhpStorm 2018.1.2\\bin\\phpstorm64.exe" --line %line% "%file%"', + // editor: '"C:\\Program Files\\JetBrains\\PhpStorm 2018.1.2\\bin\\phpstorm64.exe" --line %line% --column %column% "%file%"', // title: 'PhpStorm', // NetBeans @@ -14,7 +14,7 @@ var settings = { // editor: '"C:\\Program Files\\SciTE\\scite.exe" "-open:%file%" -goto:%line%', // EmEditor - // editor: '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line%', + // editor: '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line% /cl %column%', // PSPad Editor // editor: '"C:\\Program Files\\PSPad editor\\PSPad.exe" -%line% "%file%"', @@ -26,7 +26,7 @@ var settings = { // editor: '"C:\\Program Files\\Sublime Text 2\\sublime_text.exe" "%file%:%line%"', // Visual Studio Code / VSCodium - // editor: '"C:\\Program Files\\Microsoft VS Code\\Code.exe" --goto "%file%:%line%"', + // editor: '"C:\\Program Files\\Microsoft VS Code\\Code.exe" --goto "%file%:%line%:%column%"', mappings: { // '/remotepath': '/localpath' @@ -41,7 +41,7 @@ if (!settings.editor) { } var url = WScript.Arguments(0); -var match = /^editor:\/\/(open|create|fix)\/?\?file=([^&]+)&line=(\d+)(?:&search=([^&]*)&replace=([^&]*))?/.exec(url); +var match = /^editor:\/\/(open|create|fix)\/?\?file=([^&]+)&line=(\d+)(?::(\d+))?(?:&search=([^&]*)&replace=([^&]*))?/.exec(url); if (!match) { WScript.Echo('Unexpected URI ' + url); WScript.Quit(); @@ -53,8 +53,9 @@ for (var i in match) { var action = match[1]; var file = match[2]; var line = match[3]; -var search = match[4]; -var replace = match[5]; +var column = match[4] || 1; +var search = match[5]; +var replace = match[6]; var shell = new ActiveXObject('WScript.Shell'); var fileSystem = new ActiveXObject('Scripting.FileSystemObject'); @@ -76,7 +77,7 @@ if (action === 'create' && !fileSystem.FileExists(file)) { fileSystem.OpenTextFile(file, 2).Write(lines.join('\n')); } -var command = settings.editor.replace(/%line%/, line).replace(/%file%/, file); +var command = settings.editor.replace(/%line%/, line).replace(/%column%/, column).replace(/%file%/, file); shell.Exec(command); if (settings.title) {