From 44b543a5ead8461bb1d1198ca2a41011e27bdab1 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 5 Jun 2026 09:56:10 -0400 Subject: [PATCH 1/5] Add replace-tbd command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `pup replace-tbd `, the companion to the tbd check: it scans the same directories (reusing the tbd check's dirs/skip config) and replaces every TBD placeholder the check would flag — docblock tags (@since/@deprecated/ @version TBD), _deprecated_*() calls, and bare 'tbd' strings — with the given version. Running it makes `pup check:tbd` pass. The shared file-walk and TBD detection logic is extracted into a new Check\TbdScanner so the check and the command stay in sync; the tbd check is refactored to use it (behavior unchanged). Supports --dry-run to preview changes and --root, and includes docs and tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/commands.md | 25 +++++ src/App.php | 1 + src/Check/TbdScanner.php | 152 ++++++++++++++++++++++++++ src/Commands/Checks/Tbd.php | 73 +++---------- src/Commands/ReplaceTbd.php | 121 ++++++++++++++++++++ tests/cli/Commands/ReplaceTbdCest.php | 96 ++++++++++++++++ 6 files changed, 409 insertions(+), 59 deletions(-) create mode 100644 src/Check/TbdScanner.php create mode 100644 src/Commands/ReplaceTbd.php create mode 100644 tests/cli/Commands/ReplaceTbdCest.php diff --git a/docs/commands.md b/docs/commands.md index ec94b9b..c07a380 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -11,6 +11,7 @@ * [`pup i18n`](/docs/commands.md#pup-i18n) * [`pup info`](/docs/commands.md#pup-info) * [`pup package`](/docs/commands.md#pup-package) +* [`pup replace-tbd`](/docs/commands.md#pup-replace-tbd) * [`pup workflow`](/docs/commands.md#pup-workflow) * [`pup zip`](/docs/commands.md#pup-zip) * [`pup zip-name`](/docs/commands.md#pup-zip-name) @@ -273,6 +274,30 @@ composer -- pup package | `--root` | **Optional.** Run the command from a different directory from the current. | +## `pup replace-tbd` +Replaces `TBD` version placeholders in your codebase with the version you provide. + +This is the companion to the [`tbd` check](/docs/commands.md#pup-checktbd): it scans the same directories (using the `tbd` check's `dirs`, `skip_files`, and `skip_directories` configuration) and replaces every `TBD` placeholder it would have flagged — docblock tags such as `@since TBD`, `@deprecated TBD`, and `@version TBD`, `_deprecated_*()` calls, and bare `'tbd'`/`"tbd"` strings. After running it, `pup check:tbd` will report no findings. + +It's typically run during release prep, once you know the version the pending changes will ship in. + +Unlike `pup package`, this command writes the changes directly to your working files and does **not** restore them afterward. Use `--dry-run` to preview the changes first, or your version control system (e.g. `git checkout`) to undo them. + +### Usage +```bash +pup replace-tbd [--dry-run] +# or +composer -- pup replace-tbd [--dry-run] +``` + +### Arguments +| Argument | Description | +|-------------|----------------------------------------------------------------------------------------------------------| +| `version` | **Required.** The version to replace `TBD` placeholders with. | +| `--dry-run` | **Optional.** Print the files and number of replacements that would be made, without modifying anything. | +| `--root` | **Optional.** Run the command from a different directory from the current. | + + ## `pup workflow` Run a command workflow. diff --git a/src/App.php b/src/App.php index 24f0ab2..e920d13 100644 --- a/src/App.php +++ b/src/App.php @@ -62,6 +62,7 @@ public function __construct( string $version ) { $this->add( new Commands\I18n() ); $this->add( new Commands\Info() ); $this->add( new Commands\Package() ); + $this->add( new Commands\ReplaceTbd() ); $this->add( new Commands\Workflow() ); $this->add( new Commands\Zip() ); $this->add( new Commands\ZipName() ); diff --git a/src/Check/TbdScanner.php b/src/Check/TbdScanner.php new file mode 100644 index 0000000..3c3bb32 --- /dev/null +++ b/src/Check/TbdScanner.php @@ -0,0 +1,152 @@ +files_to_skip = str_replace( '.', '\.', $files_to_skip ); + $this->directories_to_skip = str_replace( '.', '\.', $directories_to_skip ); + } + + /** + * Builds a scanner from a check config array (e.g. the `tbd` check config). + * + * @param array $config + * + * @return self + */ + public static function fromConfig( array $config ): self { + $files_to_skip = ! empty( $config['skip_files'] ) ? (string) $config['skip_files'] : self::DEFAULT_SKIP_FILES; + $directories_to_skip = ! empty( $config['skip_directories'] ) ? (string) $config['skip_directories'] : self::DEFAULT_SKIP_DIRECTORIES; + + return new self( $files_to_skip, $directories_to_skip ); + } + + /** + * Returns the eligible (non-skipped) file short-paths within a scan dir. + * + * @param string $root The root directory to scan from. + * @param string $current_dir The current working directory, stripped from paths. + * @param string $scan_dir The directory (relative to root) to scan. + * + * @return string[] + */ + public function getFiles( string $root, string $current_dir, string $scan_dir ): array { + $files = []; + + $dir = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $root . '/' . $scan_dir ) ); + foreach ( $dir as $file ) { + // Skip directories like "." and ".." to avoid file_get_contents errors. + if ( $file->isDir() ) { + continue; + } + + $file_path = $file->getPathname(); + $short_path = (string) str_replace( $current_dir . '/', '', $file_path ); + + if ( preg_match( '!(' . $this->files_to_skip . ')$!', $short_path ) ) { + continue; + } + + if ( preg_match( '!(\.pup-)|(\.puprc)!', $short_path ) ) { + continue; + } + + $directory_separator = DIRECTORY_SEPARATOR; + if ( $directory_separator === '\\' ) { + $directory_separator = '\\\\'; + } + + if ( preg_match( '!(' . $this->directories_to_skip . ')' . $directory_separator . '!', $short_path ) ) { + continue; + } + + $files[] = $short_path; + } + + return $files; + } + + /** + * Whether a line contains a TBD version placeholder. + * + * @param string $line + * + * @return bool + */ + public function lineMatches( string $line ): bool { + foreach ( self::PATTERNS as $pattern ) { + if ( preg_match( $pattern, $line ) ) { + return true; + } + } + + return false; + } + + /** + * Replaces the TBD token(s) on a line with the given version. + * + * Only lines that match a TBD pattern are touched; on those, every standalone + * `tbd` token (case-insensitive) is replaced. + * + * @param string $line The line to process. + * @param string $version The version to replace TBD with. + * @param int $count Set to the number of replacements made on the line. + * + * @return string The (possibly) modified line. + */ + public function replaceInLine( string $line, string $version, int &$count = 0 ): string { + $count = 0; + + if ( ! $this->lineMatches( $line ) ) { + return $line; + } + + $replaced = preg_replace( '/\btbd\b/i', $version, $line, -1, $count ); + + return $replaced === null ? $line : $replaced; + } +} diff --git a/src/Commands/Checks/Tbd.php b/src/Commands/Checks/Tbd.php index 613de91..084cb0d 100644 --- a/src/Commands/Checks/Tbd.php +++ b/src/Commands/Checks/Tbd.php @@ -3,6 +3,7 @@ namespace StellarWP\Pup\Commands\Checks; use StellarWP\Pup\App; +use StellarWP\Pup\Check\TbdScanner; use StellarWP\Pup\Command\Io; use Symfony\Component\Console\Input\InputInterface; @@ -38,34 +39,20 @@ protected function checkExecute( InputInterface $input, Io $output ): int { $found_tbds = false; - $files_to_skip = '.min.css|.min.js|.map.js|.css|.png|.jpg|.jpeg|.svg|.gif|.ico'; - $directories_to_skip = 'bin|build|vendor|node_modules|.git|.github|tests'; - - $check_config = $this->check_config->getConfig(); - - if ( ! empty( $this->check_config->getConfig()['skip_files'] ) ) { - $files_to_skip = $this->check_config->getConfig()['skip_files']; - } - - if ( ! empty( $this->check_config->getConfig()['skip_directories'] ) ) { - $directories_to_skip = $this->check_config->getConfig()['skip_directories']; - } - - $files_to_skip = str_replace( '.', '\.', $files_to_skip ); - $directories_to_skip = str_replace( '.', '\.', $directories_to_skip ); + $config = $this->check_config->getConfig(); + $scanner = TbdScanner::fromConfig( $config ); $matched_lines = []; $current_dir = getcwd(); - $dirs = isset( $this->check_config->getConfig()['dirs'] ) ? $this->check_config->getConfig()['dirs'] : []; + $dirs = isset( $config['dirs'] ) ? $config['dirs'] : []; foreach ( $dirs as $dir ) { $results = $this->scanDir( + $scanner, $root ?: '.', $current_dir ?: '.', - $dir, - $files_to_skip, - $directories_to_skip, + $dir ); $matched_lines = array_merge( $matched_lines, $results ); @@ -96,45 +83,18 @@ protected function checkExecute( InputInterface $input, Io $output ): int { } /** - * @param string $root - * @param string $current_dir - * @param string $scan_dir - * @param string $files_to_skip - * @param string $directories_to_skip + * @param TbdScanner $scanner + * @param string $root + * @param string $current_dir + * @param string $scan_dir * * @return array>> */ - protected function scanDir( string $root, string $current_dir, string $scan_dir, string $files_to_skip, string $directories_to_skip ): array { + protected function scanDir( TbdScanner $scanner, string $root, string $current_dir, string $scan_dir ): array { $matched_lines = []; - $dir = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $root . '/' . $scan_dir ) ); - foreach ( $dir as $file ) { - // Skip directories like "." and ".." to avoid file_get_contents errors. - if ( $file->isDir() ) { - continue; - } - - $file_path = $file->getPathname(); - $short_path = (string) str_replace( $current_dir . '/', '', $file_path ); - - if ( preg_match( '!(' . $files_to_skip . ')$!', $short_path ) ) { - continue; - } - - if ( preg_match( '!(\.pup-)|(\.puprc)!', $short_path ) ) { - continue; - } - - $directory_separator = DIRECTORY_SEPARATOR; - if ( $directory_separator === '\\' ) { - $directory_separator = '\\\\'; - } - - if ( preg_match( '!(' . $directories_to_skip . ')' . $directory_separator . '!', $short_path ) ) { - continue; - } - - $content = file_get_contents( $short_path ); + foreach ( $scanner->getFiles( $root, $current_dir, $scan_dir ) as $short_path ) { + $content = file_get_contents( $short_path ); if ( ! $content ) { continue; @@ -148,12 +108,7 @@ protected function scanDir( string $root, string $current_dir, string $scan_dir, $lines[ $line ] = trim( $lines[ $line ] ); // does the line match? - if ( - preg_match( '/\*\s*\@(since|deprecated|version)\s.*tbd/i', $lines[ $line ] ) - || preg_match( '/_deprecated_\w\(.*[\'"]tbd[\'"]/i', $lines[ $line ] ) - || preg_match( '/[\'"]tbd[\'"]/i', $lines[ $line ] ) - ) { - + if ( $scanner->lineMatches( $lines[ $line ] ) ) { // if the file isn't being tracked already, add it to the array if ( ! isset( $matched_lines[ $short_path ] ) ) { $matched_lines[ $short_path ] = [ diff --git a/src/Commands/ReplaceTbd.php b/src/Commands/ReplaceTbd.php new file mode 100644 index 0000000..2befb38 --- /dev/null +++ b/src/Commands/ReplaceTbd.php @@ -0,0 +1,121 @@ +setName( 'replace-tbd' ) + ->addArgument( 'version', InputArgument::REQUIRED, 'The version to replace TBD placeholders with.' ) + ->addOption( 'dry-run', null, InputOption::VALUE_NONE, 'Preview the changes without writing to files.' ) + ->addOption( 'root', null, InputOption::VALUE_REQUIRED, 'Set the root directory for running commands.' ) + ->setDescription( 'Replaces "TBD" version placeholders (e.g. @since TBD) with the provided version.' ) + ->setHelp( 'Scans the directories configured for the tbd check and replaces TBD version placeholders with the provided version. This resolves exactly what `pup check:tbd` reports.' ); + } + + /** + * @inheritDoc + */ + protected function execute( InputInterface $input, OutputInterface $output ) { + parent::execute( $input, $output ); + + $config = App::getConfig(); + $version = $input->getArgument( 'version' ); + $dry_run = (bool) $input->getOption( 'dry-run' ); + $root = $input->getOption( 'root' ); + + $checks = $config->getChecks(); + $tbd_config = isset( $checks['tbd'] ) ? $checks['tbd']->getConfig() : []; + $scanner = TbdScanner::fromConfig( $tbd_config ); + $dirs = isset( $tbd_config['dirs'] ) ? (array) $tbd_config['dirs'] : [ 'src' ]; + + if ( $root ) { + chdir( $root ); + } + + $current_dir = getcwd() ?: '.'; + $total_files = 0; + $total_count = 0; + + if ( $dry_run ) { + $output->writeln( '[dry-run] No files will be modified.' ); + } + + foreach ( $dirs as $dir ) { + foreach ( $scanner->getFiles( '.', $current_dir, $dir ) as $short_path ) { + $content = file_get_contents( $short_path ); + + if ( ! $content ) { + continue; + } + + $lines = explode( "\n", $content ); + $file_count = 0; + + foreach ( $lines as $i => $line ) { + $count = 0; + $lines[ $i ] = $scanner->replaceInLine( $line, $version, $count ); + $file_count += $count; + } + + if ( $file_count === 0 ) { + continue; + } + + $total_files++; + $total_count += $file_count; + + $output->writeln( "{$short_path} ({$file_count} replaced)" ); + + if ( ! $dry_run ) { + $results = file_put_contents( $short_path, implode( "\n", $lines ) ); + + if ( false === $results ) { + $this->restoreCwd( $root ); + throw new BaseException( 'Could not write to file: ' . $short_path ); + } + } + } + } + + $this->restoreCwd( $root ); + + $output->writeln( '' ); + + if ( $total_count === 0 ) { + $output->writeln( 'No TBDs found to replace.' ); + } elseif ( $dry_run ) { + $output->writeln( "[dry-run] Would replace {$total_count} TBD occurrence(s) across {$total_files} file(s)." ); + } else { + $output->writeln( "Replaced {$total_count} TBD occurrence(s) across {$total_files} file(s) with {$version}." ); + } + + return 0; + } + + /** + * Restores the working directory if it was changed via --root. + * + * @param string|null $root + * + * @return void + */ + protected function restoreCwd( $root ): void { + if ( $root ) { + chdir( App::getConfig()->getWorkingDir() ); + } + } +} diff --git a/tests/cli/Commands/ReplaceTbdCest.php b/tests/cli/Commands/ReplaceTbdCest.php new file mode 100644 index 0000000..4b4e7c4 --- /dev/null +++ b/tests/cli/Commands/ReplaceTbdCest.php @@ -0,0 +1,96 @@ +restore_fixtures(); + parent::_after( $I ); + } + + /** + * Restores the git-tracked fixture files modified during a test. + * + * @return void + */ + protected function restore_fixtures(): void { + foreach ( $this->fixture_files as $file ) { + $path = $this->tests_root . '/_data/fake-project-with-tbds/' . $file; + system( 'git checkout -- ' . escapeshellarg( $path ) ); + } + } + + /** + * @test + */ + public function it_should_replace_tbds_with_the_version( CliTester $I ) { + $this->write_default_puprc( 'fake-project-with-tbds' ); + + $project = $this->tests_root . '/_data/fake-project-with-tbds'; + chdir( $project ); + + $I->runShellCommand( "php {$this->pup} replace-tbd 1.4.0" ); + $I->seeResultCodeIs( 0 ); + $I->seeInShellOutput( 'Replaced 8 TBD occurrence(s) across 2 file(s) with 1.4.0.' ); + + $plugin = (string) file_get_contents( $project . '/src/Plugin.php' ); + $I->assertStringContainsString( '@since 1.4.0', $plugin ); + $I->assertStringNotContainsString( 'TBD', $plugin ); + + $another = (string) file_get_contents( $project . '/src/Thing/AnotherFile.php' ); + $I->assertStringContainsString( "_deprecated_file( __FILE__, '1.4.0' );", $another ); + $I->assertStringContainsString( "_deprecated_function( __METHOD__, '1.4.0' );", $another ); + $I->assertStringContainsString( '@deprecated 1.4.0', $another ); + // Lower-case tbd placeholders are resolved too. + $I->assertStringNotContainsStringIgnoringCase( 'tbd', $another ); + } + + /** + * @test + */ + public function it_should_not_modify_files_on_dry_run( CliTester $I ) { + $this->write_default_puprc( 'fake-project-with-tbds' ); + + $project = $this->tests_root . '/_data/fake-project-with-tbds'; + chdir( $project ); + + $I->runShellCommand( "php {$this->pup} replace-tbd 1.4.0 --dry-run" ); + $I->seeResultCodeIs( 0 ); + $I->seeInShellOutput( 'dry-run' ); + $I->seeInShellOutput( 'Would replace 8 TBD occurrence(s) across 2 file(s).' ); + + // The file should still contain the original TBD placeholders. + $plugin = (string) file_get_contents( $project . '/src/Plugin.php' ); + $I->assertStringContainsString( '@since TBD', $plugin ); + } + + /** + * @test + */ + public function it_should_report_when_no_tbds_are_found( CliTester $I ) { + $this->write_default_puprc(); + + chdir( $this->tests_root . '/_data/fake-project' ); + + $I->runShellCommand( "php {$this->pup} replace-tbd 1.4.0" ); + $I->seeResultCodeIs( 0 ); + $I->seeInShellOutput( 'No TBDs found to replace.' ); + } +} From 81fb8706bd4632b73105a8357955ade9a4b7afbb Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 5 Jun 2026 10:13:37 -0400 Subject: [PATCH 2/5] Address review feedback on replace-tbd - Replace TBD placeholders precisely instead of every \btbd\b token on a flagged line. Replacement now targets only docblock tag values (@since/@deprecated/@version TBD) and quoted 'tbd'/"tbd" strings, so an unrelated "tbd" in prose or a trailing comment is no longer corrupted. - Escape backreference-significant characters ($, \) in the version before using it as a preg_replace replacement, so a version like "2.0$1" is written literally. - Align replace-tbd's `dirs` fallback with the tbd check ([] instead of ['src']) so it never edits files the check wouldn't examine when the tbd check is absent from a customized .puprc. - Add a regression test asserting prose/trailing-comment "tbd" is preserved; update docs to match the precise behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/commands.md | 2 +- src/Check/TbdScanner.php | 38 ++++++++++++++++++++++----- src/Commands/ReplaceTbd.php | 6 ++++- tests/cli/Commands/ReplaceTbdCest.php | 37 ++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index c07a380..ad0982d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -277,7 +277,7 @@ composer -- pup package ## `pup replace-tbd` Replaces `TBD` version placeholders in your codebase with the version you provide. -This is the companion to the [`tbd` check](/docs/commands.md#pup-checktbd): it scans the same directories (using the `tbd` check's `dirs`, `skip_files`, and `skip_directories` configuration) and replaces every `TBD` placeholder it would have flagged — docblock tags such as `@since TBD`, `@deprecated TBD`, and `@version TBD`, `_deprecated_*()` calls, and bare `'tbd'`/`"tbd"` strings. After running it, `pup check:tbd` will report no findings. +This is the companion to the [`tbd` check](/docs/commands.md#pup-checktbd): it scans the same directories (using the `tbd` check's `dirs`, `skip_files`, and `skip_directories` configuration) and resolves the `TBD` version placeholders it flags — docblock tag values such as `@since TBD`, `@deprecated TBD`, and `@version TBD`, and quoted `'tbd'`/`"tbd"` strings (including those passed to `_deprecated_*()` calls). Only the placeholder itself is replaced; an unrelated `tbd` elsewhere on a line (e.g. a word in prose) is left untouched. It's typically run during release prep, once you know the version the pending changes will ship in. diff --git a/src/Check/TbdScanner.php b/src/Check/TbdScanner.php index 3c3bb32..acb4bfa 100644 --- a/src/Check/TbdScanner.php +++ b/src/Check/TbdScanner.php @@ -127,10 +127,26 @@ public function lineMatches( string $line ): bool { } /** - * Replaces the TBD token(s) on a line with the given version. + * Replacement patterns that target the TBD *placeholder* specifically, rather + * than any `tbd` token on a flagged line. The captured context is preserved and + * `%version%` marks where the version is written. * - * Only lines that match a TBD pattern are touched; on those, every standalone - * `tbd` token (case-insensitive) is replaced. + * @var array Map of pattern => replacement template. + */ + const REPLACEMENTS = [ + // Docblock tag value: @since / @deprecated / @version TBD + '/(@(?:since|deprecated|version)\s+)tbd\b/i' => '${1}%version%', + // Quoted placeholder: 'tbd' or "tbd" (e.g. _deprecated_function( ..., 'TBD' )) + '/([\'"])tbd\1/i' => '${1}%version%${1}', + ]; + + /** + * Replaces TBD version placeholders on a line with the given version. + * + * Only genuine placeholders are touched — docblock tag values (`@since TBD`) + * and quoted strings (`'tbd'`). A non-placeholder `tbd` elsewhere on the line + * (e.g. prose in a comment) is left intact, so this never corrupts unrelated + * text. * * @param string $line The line to process. * @param string $version The version to replace TBD with. @@ -141,11 +157,19 @@ public function lineMatches( string $line ): bool { public function replaceInLine( string $line, string $version, int &$count = 0 ): string { $count = 0; - if ( ! $this->lineMatches( $line ) ) { - return $line; - } + // Escape characters that are significant in a preg replacement string so a + // version like "1.0-$1" is written literally rather than as a backreference. + $safe_version = str_replace( [ '\\', '$' ], [ '\\\\', '\\$' ], $version ); + + $patterns = array_keys( self::REPLACEMENTS ); + $replacements = array_map( + static function ( string $template ) use ( $safe_version ): string { + return str_replace( '%version%', $safe_version, $template ); + }, + array_values( self::REPLACEMENTS ) + ); - $replaced = preg_replace( '/\btbd\b/i', $version, $line, -1, $count ); + $replaced = preg_replace( $patterns, $replacements, $line, -1, $count ); return $replaced === null ? $line : $replaced; } diff --git a/src/Commands/ReplaceTbd.php b/src/Commands/ReplaceTbd.php index 2befb38..a989c6f 100644 --- a/src/Commands/ReplaceTbd.php +++ b/src/Commands/ReplaceTbd.php @@ -40,7 +40,11 @@ protected function execute( InputInterface $input, OutputInterface $output ) { $checks = $config->getChecks(); $tbd_config = isset( $checks['tbd'] ) ? $checks['tbd']->getConfig() : []; $scanner = TbdScanner::fromConfig( $tbd_config ); - $dirs = isset( $tbd_config['dirs'] ) ? (array) $tbd_config['dirs'] : [ 'src' ]; + + // Mirror the tbd check's directory resolution exactly: when the tbd check is + // absent/unconfigured it scans nothing, so replace-tbd must not edit files + // the check would never examine. + $dirs = isset( $tbd_config['dirs'] ) ? (array) $tbd_config['dirs'] : []; if ( $root ) { chdir( $root ); diff --git a/tests/cli/Commands/ReplaceTbdCest.php b/tests/cli/Commands/ReplaceTbdCest.php index 4b4e7c4..53e97af 100644 --- a/tests/cli/Commands/ReplaceTbdCest.php +++ b/tests/cli/Commands/ReplaceTbdCest.php @@ -62,6 +62,43 @@ public function it_should_replace_tbds_with_the_version( CliTester $I ) { $I->assertStringNotContainsStringIgnoringCase( 'tbd', $another ); } + /** + * @test + */ + public function it_should_only_replace_placeholders_not_surrounding_prose( CliTester $I ) { + $this->write_default_puprc( 'fake-project-with-tbds' ); + + $project = $this->tests_root . '/_data/fake-project-with-tbds'; + $tmp = $project . '/src/ProseTbd.php'; + + // A docblock line with a real version plus the word "tbd" in prose, and a + // quoted placeholder followed by an unrelated "tbd" in a trailing comment. + $contents = "runShellCommand( "php {$this->pup} replace-tbd 1.4.0" ); + $I->seeResultCodeIs( 0 ); + + $result = (string) file_get_contents( $tmp ); + + // The docblock prose (and its real version) is untouched. + $I->assertStringContainsString( '@since 5.0.0 reworked the tbd handler', $result ); + // The quoted placeholder is resolved... + $I->assertStringContainsString( "\$status = '1.4.0';", $result ); + // ...but the unrelated "tbd" in the trailing comment is preserved. + $I->assertStringContainsString( '// resolve tbd later', $result ); + } finally { + @unlink( $tmp ); + } + } + /** * @test */ From 40766d86c25f1c5c6cc52e984490f7a8111e6aaa Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 5 Jun 2026 14:04:42 -0400 Subject: [PATCH 3/5] docs: update docs to mention deprecated_function --- docs/commands.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index ad0982d..17cb181 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -76,8 +76,9 @@ composer -- pup check ### `pup check:tbd` Scans your files for `tbd` (case-insensitive) and tells you where to find them. -The `tbd` check will scan your files in relevant locations (`@since`, `@todo`, `@version`, etc) and display the files -and line numbers where they appear. +The `tbd` check will scan your files in relevant locations (`@since`, `@todo`, `@version`, etc, as well as quoted +`'tbd'`/`"tbd"` strings such as `_deprecated_function( __METHOD__, 'TBD' )`) and display the files and line numbers +where they appear. #### Usage ```bash @@ -277,7 +278,7 @@ composer -- pup package ## `pup replace-tbd` Replaces `TBD` version placeholders in your codebase with the version you provide. -This is the companion to the [`tbd` check](/docs/commands.md#pup-checktbd): it scans the same directories (using the `tbd` check's `dirs`, `skip_files`, and `skip_directories` configuration) and resolves the `TBD` version placeholders it flags — docblock tag values such as `@since TBD`, `@deprecated TBD`, and `@version TBD`, and quoted `'tbd'`/`"tbd"` strings (including those passed to `_deprecated_*()` calls). Only the placeholder itself is replaced; an unrelated `tbd` elsewhere on a line (e.g. a word in prose) is left untouched. +This is the companion to the [`tbd` check](/docs/commands.md#pup-checktbd): it scans the same directories (using the `tbd` check's `dirs`, `skip_files`, and `skip_directories` configuration) and resolves the `TBD` version placeholders it flags — docblock tag values such as `@since TBD`, `@deprecated TBD`, and `@version TBD`, and quoted `'tbd'`/`"tbd"` strings (including those passed to `_deprecated_*()` calls, e.g. `_deprecated_function( __METHOD__, 'TBD' )`). Only the placeholder itself is replaced; an unrelated `tbd` elsewhere on a line (e.g. a word in prose) is left untouched. It's typically run during release prep, once you know the version the pending changes will ship in. From 9198e382602fa14306acc01168bed8ddc18db29f Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 5 Jun 2026 14:17:05 -0400 Subject: [PATCH 4/5] refactor: rely on single replacements const --- src/Check/TbdScanner.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Check/TbdScanner.php b/src/Check/TbdScanner.php index acb4bfa..11d73ae 100644 --- a/src/Check/TbdScanner.php +++ b/src/Check/TbdScanner.php @@ -18,17 +18,6 @@ class TbdScanner { */ const DEFAULT_SKIP_DIRECTORIES = 'bin|build|vendor|node_modules|.git|.github|tests'; - /** - * Patterns that identify a TBD version placeholder on a line. - * - * @var string[] - */ - const PATTERNS = [ - '/\*\s*\@(since|deprecated|version)\s.*tbd/i', - '/_deprecated_\w\(.*[\'"]tbd[\'"]/i', - '/[\'"]tbd[\'"]/i', - ]; - /** * Regex-ready list of file patterns to skip. * @var string @@ -112,12 +101,16 @@ public function getFiles( string $root, string $current_dir, string $scan_dir ): /** * Whether a line contains a TBD version placeholder. * + * Detection is derived from {@see self::REPLACEMENTS} so that `check:tbd` + * flags exactly the placeholders `replace-tbd` is able to resolve — the two + * cannot drift apart. + * * @param string $line * * @return bool */ public function lineMatches( string $line ): bool { - foreach ( self::PATTERNS as $pattern ) { + foreach ( array_keys( self::REPLACEMENTS ) as $pattern ) { if ( preg_match( $pattern, $line ) ) { return true; } From bf58e1a66188b5315cb10806671417d53c3437c3 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 5 Jun 2026 15:34:41 -0400 Subject: [PATCH 5/5] docs: update command description --- src/Commands/ReplaceTbd.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/ReplaceTbd.php b/src/Commands/ReplaceTbd.php index a989c6f..7ff0709 100644 --- a/src/Commands/ReplaceTbd.php +++ b/src/Commands/ReplaceTbd.php @@ -22,7 +22,7 @@ protected function configure() { ->addArgument( 'version', InputArgument::REQUIRED, 'The version to replace TBD placeholders with.' ) ->addOption( 'dry-run', null, InputOption::VALUE_NONE, 'Preview the changes without writing to files.' ) ->addOption( 'root', null, InputOption::VALUE_REQUIRED, 'Set the root directory for running commands.' ) - ->setDescription( 'Replaces "TBD" version placeholders (e.g. @since TBD) with the provided version.' ) + ->setDescription('Replaces "TBD" version placeholders (e.g. @since TBD, _deprecated_function( __METHOD__, "TBD" ), "tbd") with the provided version.' ) ->setHelp( 'Scans the directories configured for the tbd check and replaces TBD version placeholders with the provided version. This resolves exactly what `pup check:tbd` reports.' ); }