From 02a20350efc2986629aaf10eda27e8f5239e2614 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 23 Jul 2026 22:41:34 +0200 Subject: [PATCH 1/4] Normalize global names in the diff tool Extracted from the original "Preserve expression output" commit so the diff tool's changes can land separately from the exporter's. The diff normalization must follow the same rules the exporter uses when it strips PHP-Parser's synthetic global namespace prefixes, otherwise the normalized output no longer matches what the exporter produces. --- prep-diff.php | 46 ++++++++++++++++++++++++++++++++++++++-- tests/prep-diff-test.php | 21 +++++++++++++----- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/prep-diff.php b/prep-diff.php index 42e69c8..be02fbc 100644 --- a/prep-diff.php +++ b/prep-diff.php @@ -75,10 +75,11 @@ function wp_parser_prep_diff_is_simple_name_record_list( array $list ) { * Normalizes scalar values that should not affect output comparisons. * * @param mixed $value Scalar value. + * @param array $path Current JSON path. * @param string|null $key Parent object key. * @return mixed Normalized value. */ -function wp_parser_prep_diff_normalize_scalar( $value, $key ) { +function wp_parser_prep_diff_normalize_scalar( $value, array $path, $key ) { if ( in_array( $key, array( 'line', 'end_line', 'startLine', 'endLine' ), true ) ) { return 0; } @@ -91,6 +92,47 @@ function wp_parser_prep_diff_normalize_scalar( $value, $key ) { return $value; } + if ( in_array( $key, array( 'content', 'description', 'long_description' ), true ) ) { + return preg_replace_callback( + '~{@(?:link|see)\s+([^}\s]+)~', + static function( $matches ) { + return str_replace( + $matches[1], + wp_parser_prep_diff_normalize_global_names( $matches[1] ), + $matches[0] + ); + }, + $value + ); + } + + $parent_key = 1 < count( $path ) ? $path[ count( $path ) - 2 ] : null; + $normalize = in_array( + $key, + array( 'class', 'default', 'extends', 'link', 'refers', 'type', 'value' ), + true + ); + + $normalize = $normalize + || in_array( $parent_key, array( 'aliases', 'implements', 'types' ), true ) + || ( + 'name' === $key + && ( + wp_parser_prep_diff_path_ends_with( $path, array( 'uses', 'functions', '[]', 'name' ) ) + || wp_parser_prep_diff_path_ends_with( $path, array( 'uses', 'methods', '[]', 'name' ) ) + ) + ); + + return $normalize ? wp_parser_prep_diff_normalize_global_names( $value ) : $value; +} + +/** + * Remove synthetic global namespace prefixes from semantic names. + * + * @param string $value Name or expression to normalize. + * @return string Normalized value. + */ +function wp_parser_prep_diff_normalize_global_names( $value ) { // "\wp_kses()" -> "wp_kses()". $without_global_namespace = preg_replace( '~(^|\p{Z})\\\\([A-Z_a-z\x80-\xFF][0-9A-Z_a-z\x80-\xFF]*)([:(\p{Z}]|->|$)~', @@ -222,7 +264,7 @@ function wp_parser_prep_diff_should_sort_list( array $path, array $list ) { */ function wp_parser_prep_diff_normalize( $value, array $path = array(), $key = null ) { if ( ! is_array( $value ) ) { - return wp_parser_prep_diff_normalize_scalar( $value, $key ); + return wp_parser_prep_diff_normalize_scalar( $value, $path, $key ); } if ( wp_parser_prep_diff_is_list( $value ) ) { diff --git a/tests/prep-diff-test.php b/tests/prep-diff-test.php index e4860e5..5e968a3 100644 --- a/tests/prep-diff-test.php +++ b/tests/prep-diff-test.php @@ -65,16 +65,19 @@ function assert_true( $condition, $message ) { 'name' => 'beta', 'namespace' => 'global', 'arguments' => array( - array( 'name' => '$first', 'type' => '' ), + array( 'name' => '$first', 'type' => '\\Global_Type', 'default' => '\\false' ), array( 'name' => '$second', 'type' => '' ), ), + 'hooks' => array( + array( 'name' => '\\x09tab', 'type' => 'action', 'line' => 10, 'end_line' => 10 ), + ), 'doc' => array( 'tags' => array( array( 'name' => 'since', 'content' => '1.0.0' ), array( 'name' => 'param', 'content' => 'First.', 'variable' => '$first' ), ), 'long_description' => '', - 'description' => 'Calls \\alpha().', + 'description' => 'Calls {@see \\alpha()}; preserves \\xC0.', ), ), ), @@ -105,7 +108,7 @@ function assert_true( $condition, $message ) { 'name' => 'beta', 'line' => 98, 'doc' => array( - 'description' => 'Calls alpha().', + 'description' => 'Calls {@see alpha()}; preserves \\xC0.', 'long_description' => '', 'tags' => array( array( 'name' => 'since', 'content' => '1.0.0' ), @@ -113,9 +116,12 @@ function assert_true( $condition, $message ) { ), ), 'arguments' => array( - array( 'type' => '', 'name' => '$first' ), + array( 'default' => 'false', 'type' => 'Global_Type', 'name' => '$first' ), array( 'type' => '', 'name' => '$second' ), ), + 'hooks' => array( + array( 'end_line' => 100, 'line' => 100, 'type' => 'action', 'name' => '\\x09tab' ), + ), 'uses' => array( 'functions' => array( array( 'end_line' => 30, 'line' => 30, 'name' => 'alpha' ), @@ -140,7 +146,12 @@ function assert_true( $condition, $message ) { assert_true( array( '$first', '$second' ) === array_column( $decoded[1]['functions'][0]['arguments'], 'name' ), 'Function argument order should be preserved.' ); assert_true( array( 'since', 'param' ) === array_column( $decoded[1]['functions'][0]['doc']['tags'], 'name' ), 'Doc tag order should be preserved.' ); assert_true( array( 'alpha', 'zeta' ) === array_column( $decoded[1]['functions'][0]['uses']['functions'], 'name' ), 'Function uses should sort by name.' ); -assert_true( array_keys( $decoded[1]['functions'][0] ) === array( 'arguments', 'doc', 'line', 'name', 'namespace', 'uses' ), 'Object keys should be sorted.' ); +assert_true( array_keys( $decoded[1]['functions'][0] ) === array( 'arguments', 'doc', 'hooks', 'line', 'name', 'namespace', 'uses' ), 'Object keys should be sorted.' ); +assert_true( + 'Calls {@see alpha()}; preserves \\xC0.' === $decoded[1]['functions'][0]['doc']['description'], + 'Documentation escape sequences should be preserved.' +); +assert_true( '\\x09tab' === $decoded[1]['functions'][0]['hooks'][0]['name'], 'Hook names should be preserved.' ); $changed = json_decode( $b, true ); $changed[1]['functions'][0]['name'] = 'changed'; From da20168b54e74e88f7bd1ab05679ad746ca7f7a2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 10 Aug 2026 19:36:47 +0400 Subject: [PATCH 2/4] Run the diff normalization tests and match the exporter's rules `tests/prep-diff-test.php` was a standalone script that nothing executed: `phpunit.xml.dist` only loads `tests/phpunit/tests/`, and both `composer test` and CI run PHPUnit with that configuration. Move it under `tests/phpunit/tests/` as a test case so that it runs with the rest of the suite. `prep-diff.php` normalized printed expressions with its own prose-oriented regular expression, which also matched global names appearing inside string literals. An argument default of `'see \Foo bar'` was rewritten to `'see Foo bar'`, which is exactly the mangled output the script is meant to expose, so a regression in expression printing diffed clean. Reuse the exporter's `WP_Parser\strip_global_namespace_prefix()` instead, which only strips a prefix at the start of a value. Inline documentation references are unaffected: they never contain whitespace, so both rules produce the same result for them. Diffs of output generated before and after this branch now show the string-literal fix instead of hiding it, which is intended. --- prep-diff.php | 28 +-- tests/phpunit/tests/prep-diff.php | 273 ++++++++++++++++++++++++++++++ tests/prep-diff-test.php | 164 ------------------ 3 files changed, 282 insertions(+), 183 deletions(-) create mode 100644 tests/phpunit/tests/prep-diff.php delete mode 100644 tests/prep-diff-test.php diff --git a/prep-diff.php b/prep-diff.php index be02fbc..e564130 100644 --- a/prep-diff.php +++ b/prep-diff.php @@ -14,6 +14,13 @@ * diff -u before.norm.json after.norm.json */ +/* + * The exporter's own normalization rules are reused so that this script cannot + * hide a difference in them. The file only declares namespaced functions, so it + * is safe to load without the Composer autoloader. + */ +require_once __DIR__ . '/lib/runner.php'; + /** * Checks if an array is a JSON list. * @@ -98,7 +105,7 @@ function wp_parser_prep_diff_normalize_scalar( $value, array $path, $key ) { static function( $matches ) { return str_replace( $matches[1], - wp_parser_prep_diff_normalize_global_names( $matches[1] ), + \WP_Parser\strip_global_namespace_prefix( $matches[1] ), $matches[0] ); }, @@ -123,24 +130,7 @@ static function( $matches ) { ) ); - return $normalize ? wp_parser_prep_diff_normalize_global_names( $value ) : $value; -} - -/** - * Remove synthetic global namespace prefixes from semantic names. - * - * @param string $value Name or expression to normalize. - * @return string Normalized value. - */ -function wp_parser_prep_diff_normalize_global_names( $value ) { - // "\wp_kses()" -> "wp_kses()". - $without_global_namespace = preg_replace( - '~(^|\p{Z})\\\\([A-Z_a-z\x80-\xFF][0-9A-Z_a-z\x80-\xFF]*)([:(\p{Z}]|->|$)~', - '$1$2$3', - $value - ); - - return null === $without_global_namespace ? $value : $without_global_namespace; + return $normalize ? \WP_Parser\strip_global_namespace_prefix( $value ) : $value; } /** diff --git a/tests/phpunit/tests/prep-diff.php b/tests/phpunit/tests/prep-diff.php new file mode 100644 index 0000000..56ed090 --- /dev/null +++ b/tests/phpunit/tests/prep-diff.php @@ -0,0 +1,273 @@ + array( 'pipe', 'r' ), + 1 => array( 'pipe', 'w' ), + 2 => array( 'pipe', 'w' ), + ); + + $process = proc_open( + escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( $script ), + $descriptor_spec, + $pipes + ); + + $this->assertIsResource( $process, 'Unable to start prep-diff.php.' ); + + fwrite( $pipes[0], $json ); + fclose( $pipes[0] ); + + $output = stream_get_contents( $pipes[1] ); + $error = stream_get_contents( $pipes[2] ); + + fclose( $pipes[1] ); + fclose( $pipes[2] ); + + $status = proc_close( $process ); + + $this->assertSame( 0, $status, trim( $error ) ); + + return $output; + } + + /** + * Returns generated JSON for a build. + * + * @return string JSON with parser output in source order. + */ + protected function get_json() { + + return json_encode( + array( + array( + 'root' => '/tmp/build-a', + 'path' => 'beta.php', + 'call_graph' => array( + array( 'name' => 'zeta', 'line' => 9, 'end_line' => 9 ), + array( 'name' => 'alpha', 'line' => 3, 'end_line' => 3 ), + ), + 'functions' => array( + array( + 'uses' => array( + 'functions' => array( + array( 'name' => 'zeta', 'line' => 9, 'end_line' => 9 ), + array( 'name' => 'alpha', 'line' => 3, 'end_line' => 3 ), + ), + ), + 'line' => 20, + 'name' => 'beta', + 'namespace' => 'global', + 'arguments' => array( + array( 'name' => '$first', 'type' => '\\Global_Type', 'default' => '\\false' ), + array( 'name' => '$second', 'type' => '' ), + ), + 'hooks' => array( + array( 'name' => '\\x09tab', 'type' => 'action', 'line' => 10, 'end_line' => 10 ), + ), + 'doc' => array( + 'tags' => array( + array( 'name' => 'since', 'content' => '1.0.0' ), + array( 'name' => 'param', 'content' => 'First.', 'variable' => '$first' ), + ), + 'long_description' => '', + 'description' => 'Calls {@see \\alpha()}; preserves \\xC0.', + ), + ), + ), + ), + array( + 'path' => 'alpha.php', + 'root' => '/tmp/build-a', + ), + ) + ); + } + + /** + * Returns generated JSON for an equivalent build with shuffled output. + * + * @return string JSON with the same content in a different order. + */ + protected function get_shuffled_json() { + + return json_encode( + array( + array( + 'root' => '/tmp/build-b', + 'path' => 'alpha.php', + ), + array( + 'path' => 'beta.php', + 'root' => '/tmp/build-b', + 'call_graph' => array( + array( 'end_line' => 90, 'line' => 90, 'name' => 'zeta' ), + array( 'end_line' => 30, 'line' => 30, 'name' => 'alpha' ), + ), + 'functions' => array( + array( + 'namespace' => 'global', + 'name' => 'beta', + 'line' => 98, + 'doc' => array( + 'description' => 'Calls {@see alpha()}; preserves \\xC0.', + 'long_description' => '', + 'tags' => array( + array( 'name' => 'since', 'content' => '1.0.0' ), + array( 'name' => 'param', 'content' => 'First.', 'variable' => '$first' ), + ), + ), + 'arguments' => array( + array( 'default' => 'false', 'type' => 'Global_Type', 'name' => '$first' ), + array( 'type' => '', 'name' => '$second' ), + ), + 'hooks' => array( + array( 'end_line' => 100, 'line' => 100, 'type' => 'action', 'name' => '\\x09tab' ), + ), + 'uses' => array( + 'functions' => array( + array( 'end_line' => 30, 'line' => 30, 'name' => 'alpha' ), + array( 'end_line' => 90, 'line' => 90, 'name' => 'zeta' ), + ), + ), + ), + ), + ), + ) + ); + } + + /** + * Test that equivalent output with incidental differences normalizes identically. + */ + public function test_equivalent_output_normalizes_identically() { + + $this->assertSame( + $this->normalize( $this->get_json() ), + $this->normalize( $this->get_shuffled_json() ) + ); + } + + /** + * Test that unordered parser collections are sorted. + */ + public function test_unordered_collections_are_sorted() { + + $decoded = json_decode( $this->normalize( $this->get_json() ), true ); + + $this->assertSame( 'alpha.php', $decoded[0]['path'] ); + $this->assertSame( + array( 'alpha', 'zeta' ), + array_column( $decoded[1]['call_graph'], 'name' ) + ); + $this->assertSame( + array( 'alpha', 'zeta' ), + array_column( $decoded[1]['functions'][0]['uses']['functions'], 'name' ) + ); + $this->assertSame( + array( 'arguments', 'doc', 'hooks', 'line', 'name', 'namespace', 'uses' ), + array_keys( $decoded[1]['functions'][0] ) + ); + } + + /** + * Test that ordered documentation data is left in source order. + */ + public function test_documentation_order_is_preserved() { + + $decoded = json_decode( $this->normalize( $this->get_json() ), true ); + + $this->assertSame( + array( '$first', '$second' ), + array_column( $decoded[1]['functions'][0]['arguments'], 'name' ) + ); + $this->assertSame( + array( 'since', 'param' ), + array_column( $decoded[1]['functions'][0]['doc']['tags'], 'name' ) + ); + } + + /** + * Test that literal escape sequences are preserved. + */ + public function test_literal_escape_sequences_are_preserved() { + + $decoded = json_decode( $this->normalize( $this->get_json() ), true ); + + $this->assertSame( + 'Calls {@see alpha()}; preserves \\xC0.', + $decoded[1]['functions'][0]['doc']['description'] + ); + $this->assertSame( '\\x09tab', $decoded[1]['functions'][0]['hooks'][0]['name'] ); + } + + /** + * Test that printed expressions only lose an anchored global prefix. + * + * Printed expressions are normalized with the same rule the exporter applies, + * so that global names appearing inside string literals are left alone. + * + * @dataProvider data_printed_expressions + * + * @param string $key Key holding the printed expression. + * @param string $value Printed expression. + * @param string $expected Expected normalized expression. + */ + public function test_printed_expressions_normalize_anchored_prefixes( $key, $value, $expected ) { + + $decoded = json_decode( $this->normalize( json_encode( array( $key => $value ) ) ), true ); + + $this->assertSame( $expected, $decoded[ $key ] ); + } + + /** + * Data provider for printed expressions. + * + * @return array[] Key, printed expression, and expected normalized expression. + */ + public function data_printed_expressions() { + + return array( + 'string literal containing a global name' => array( 'default', "'see \\Foo bar'", "'see \\Foo bar'" ), + 'string literal in a constant value' => array( 'value', "'see \\Foo bar'", "'see \\Foo bar'" ), + 'printed global name' => array( 'default', '\\Foo', 'Foo' ), + 'printed namespaced name' => array( 'default', '\\Vendor\\Thing', '\\Vendor\\Thing' ), + ); + } + + /** + * Test that real content changes remain visible. + */ + public function test_content_changes_remain_visible() { + + $changed = json_decode( $this->get_shuffled_json(), true ); + + $changed[1]['functions'][0]['name'] = 'changed'; + + $this->assertNotSame( + $this->normalize( $this->get_json() ), + $this->normalize( json_encode( $changed ) ) + ); + } +} diff --git a/tests/prep-diff-test.php b/tests/prep-diff-test.php deleted file mode 100644 index 5e968a3..0000000 --- a/tests/prep-diff-test.php +++ /dev/null @@ -1,164 +0,0 @@ - array( 'pipe', 'r' ), - 1 => array( 'pipe', 'w' ), - 2 => array( 'pipe', 'w' ), - ); - - $process = proc_open( - escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( $script ), - $descriptor_spec, - $pipes - ); - - if ( ! is_resource( $process ) ) { - throw new RuntimeException( 'Unable to start prep-diff.php.' ); - } - - fwrite( $pipes[0], $json ); - fclose( $pipes[0] ); - - $output = stream_get_contents( $pipes[1] ); - $error = stream_get_contents( $pipes[2] ); - - fclose( $pipes[1] ); - fclose( $pipes[2] ); - - $status = proc_close( $process ); - - if ( 0 !== $status ) { - throw new RuntimeException( trim( $error ) ); - } - - return $output; -} - -function assert_true( $condition, $message ) { - if ( ! $condition ) { - fwrite( STDERR, $message . PHP_EOL ); - exit( 1 ); - } -} - -$a = json_encode( - array( - array( - 'root' => '/tmp/build-a', - 'path' => 'beta.php', - 'call_graph' => array( - array( 'name' => 'zeta', 'line' => 9, 'end_line' => 9 ), - array( 'name' => 'alpha', 'line' => 3, 'end_line' => 3 ), - ), - 'functions' => array( - array( - 'uses' => array( - 'functions' => array( - array( 'name' => 'zeta', 'line' => 9, 'end_line' => 9 ), - array( 'name' => 'alpha', 'line' => 3, 'end_line' => 3 ), - ), - ), - 'line' => 20, - 'name' => 'beta', - 'namespace' => 'global', - 'arguments' => array( - array( 'name' => '$first', 'type' => '\\Global_Type', 'default' => '\\false' ), - array( 'name' => '$second', 'type' => '' ), - ), - 'hooks' => array( - array( 'name' => '\\x09tab', 'type' => 'action', 'line' => 10, 'end_line' => 10 ), - ), - 'doc' => array( - 'tags' => array( - array( 'name' => 'since', 'content' => '1.0.0' ), - array( 'name' => 'param', 'content' => 'First.', 'variable' => '$first' ), - ), - 'long_description' => '', - 'description' => 'Calls {@see \\alpha()}; preserves \\xC0.', - ), - ), - ), - ), - array( - 'path' => 'alpha.php', - 'root' => '/tmp/build-a', - ), - ) -); - -$b = json_encode( - array( - array( - 'root' => '/tmp/build-b', - 'path' => 'alpha.php', - ), - array( - 'path' => 'beta.php', - 'root' => '/tmp/build-b', - 'call_graph' => array( - array( 'end_line' => 90, 'line' => 90, 'name' => 'zeta' ), - array( 'end_line' => 30, 'line' => 30, 'name' => 'alpha' ), - ), - 'functions' => array( - array( - 'namespace' => 'global', - 'name' => 'beta', - 'line' => 98, - 'doc' => array( - 'description' => 'Calls {@see alpha()}; preserves \\xC0.', - 'long_description' => '', - 'tags' => array( - array( 'name' => 'since', 'content' => '1.0.0' ), - array( 'name' => 'param', 'content' => 'First.', 'variable' => '$first' ), - ), - ), - 'arguments' => array( - array( 'default' => 'false', 'type' => 'Global_Type', 'name' => '$first' ), - array( 'type' => '', 'name' => '$second' ), - ), - 'hooks' => array( - array( 'end_line' => 100, 'line' => 100, 'type' => 'action', 'name' => '\\x09tab' ), - ), - 'uses' => array( - 'functions' => array( - array( 'end_line' => 30, 'line' => 30, 'name' => 'alpha' ), - array( 'end_line' => 90, 'line' => 90, 'name' => 'zeta' ), - ), - ), - ), - ), - ), - ) -); - -$a_normalized = normalize_with_prep_diff( $script, $a ); -$b_normalized = normalize_with_prep_diff( $script, $b ); - -assert_true( $a_normalized === $b_normalized, 'Equivalent shuffled JSON should normalize identically.' ); - -$decoded = json_decode( $a_normalized, true ); - -assert_true( 'alpha.php' === $decoded[0]['path'], 'Top-level files should sort by path.' ); -assert_true( array( 'alpha', 'zeta' ) === array_column( $decoded[1]['call_graph'], 'name' ), 'Simple name records should sort by name.' ); -assert_true( array( '$first', '$second' ) === array_column( $decoded[1]['functions'][0]['arguments'], 'name' ), 'Function argument order should be preserved.' ); -assert_true( array( 'since', 'param' ) === array_column( $decoded[1]['functions'][0]['doc']['tags'], 'name' ), 'Doc tag order should be preserved.' ); -assert_true( array( 'alpha', 'zeta' ) === array_column( $decoded[1]['functions'][0]['uses']['functions'], 'name' ), 'Function uses should sort by name.' ); -assert_true( array_keys( $decoded[1]['functions'][0] ) === array( 'arguments', 'doc', 'hooks', 'line', 'name', 'namespace', 'uses' ), 'Object keys should be sorted.' ); -assert_true( - 'Calls {@see alpha()}; preserves \\xC0.' === $decoded[1]['functions'][0]['doc']['description'], - 'Documentation escape sequences should be preserved.' -); -assert_true( '\\x09tab' === $decoded[1]['functions'][0]['hooks'][0]['name'], 'Hook names should be preserved.' ); - -$changed = json_decode( $b, true ); -$changed[1]['functions'][0]['name'] = 'changed'; - -assert_true( - $a_normalized !== normalize_with_prep_diff( $script, json_encode( $changed ) ), - 'Real content changes should remain visible.' -); - -echo "prep-diff tests passed\n"; From 6ce9808dbe7011667eac44daa12172509ddef04b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 18 Aug 2026 18:27:40 +0400 Subject: [PATCH 3/4] Test diff normalization leaves documentation text alone The exporter now hands documentation text through as authored: inline references in descriptions and tag content, and the reference tokens of `@see`/`@link` tags. A difference in those fields between two builds is a real behavior change, so the diff normalization must not erase it. --- tests/phpunit/tests/prep-diff.php | 38 +++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/prep-diff.php b/tests/phpunit/tests/prep-diff.php index 56ed090..e21d7bb 100644 --- a/tests/phpunit/tests/prep-diff.php +++ b/tests/phpunit/tests/prep-diff.php @@ -131,7 +131,7 @@ protected function get_shuffled_json() { 'name' => 'beta', 'line' => 98, 'doc' => array( - 'description' => 'Calls {@see alpha()}; preserves \\xC0.', + 'description' => 'Calls {@see \\alpha()}; preserves \\xC0.', 'long_description' => '', 'tags' => array( array( 'name' => 'since', 'content' => '1.0.0' ), @@ -216,12 +216,46 @@ public function test_literal_escape_sequences_are_preserved() { $decoded = json_decode( $this->normalize( $this->get_json() ), true ); $this->assertSame( - 'Calls {@see alpha()}; preserves \\xC0.', + 'Calls {@see \\alpha()}; preserves \\xC0.', $decoded[1]['functions'][0]['doc']['description'] ); $this->assertSame( '\\x09tab', $decoded[1]['functions'][0]['hooks'][0]['name'] ); } + /** + * Test that documentation text passes through as authored. + * + * The exporter no longer rewrites documentation text, so a difference in + * these fields is a real behavior change that the diff must show. + * + * @dataProvider data_documentation_text + * + * @param string $key Key holding documentation text. + * @param string $value Documentation text. + */ + public function test_documentation_text_passes_through( $key, $value ) { + + $decoded = json_decode( $this->normalize( json_encode( array( $key => $value ) ) ), true ); + + $this->assertSame( $value, $decoded[ $key ] ); + } + + /** + * Data provider for documentation text. + * + * @return array[] Key and documentation text. + */ + public function data_documentation_text() { + + return array( + 'inline reference in a description' => array( 'description', 'Calls {@see \\alpha()}.' ), + 'inline reference in a long description' => array( 'long_description', 'Calls {@link \\alpha()}.' ), + 'inline reference in tag content' => array( 'content', 'See {@see \\Widget::render()}.' ), + 'see tag reference' => array( 'refers', '\\alpha()' ), + 'link tag target' => array( 'link', '\\alpha()' ), + ); + } + /** * Test that printed expressions only lose an anchored global prefix. * From 9b41ff95a023587f43a5fa697e660b632dcbf45f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 18 Aug 2026 18:28:17 +0400 Subject: [PATCH 4/4] Stop normalizing documentation text in the diff tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the exporter: documentation text is exported as authored, so the diff tool has nothing to reconcile in descriptions, tag content, or the `refers`/`link` tokens — and normalizing them would hide a real change in exactly the fields the exporter promises not to touch. Expression and type fields keep the anchored-prefix normalization the exporter still applies. --- prep-diff.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/prep-diff.php b/prep-diff.php index e564130..7b224d0 100644 --- a/prep-diff.php +++ b/prep-diff.php @@ -99,24 +99,10 @@ function wp_parser_prep_diff_normalize_scalar( $value, array $path, $key ) { return $value; } - if ( in_array( $key, array( 'content', 'description', 'long_description' ), true ) ) { - return preg_replace_callback( - '~{@(?:link|see)\s+([^}\s]+)~', - static function( $matches ) { - return str_replace( - $matches[1], - \WP_Parser\strip_global_namespace_prefix( $matches[1] ), - $matches[0] - ); - }, - $value - ); - } - $parent_key = 1 < count( $path ) ? $path[ count( $path ) - 2 ] : null; $normalize = in_array( $key, - array( 'class', 'default', 'extends', 'link', 'refers', 'type', 'value' ), + array( 'class', 'default', 'extends', 'type', 'value' ), true );