Skip to content

Align the prep-diff tool with the exporter's normalization rules - #279

Open
sirreal wants to merge 19 commits into
fix-254from
prep-diff-updates
Open

Align the prep-diff tool with the exporter's normalization rules#279
sirreal wants to merge 19 commits into
fix-254from
prep-diff-updates

Conversation

@sirreal

@sirreal sirreal commented Aug 14, 2026

Copy link
Copy Markdown
Member

Stacked on #262 (base branch: fix-254). Extracts the diff-tool work from that PR so it can be reviewed separately from the export-semantics changes.

The rule

prep-diff.php normalizes generated JSON so two exports can be compared without incidental churn. Its normalization must mirror the exporter's own rules exactly — anything it normalizes beyond them is a real behavior change it will hide.

After this branch, the tool follows the same split #262 establishes:

  • Documentation text passes through untoucheddescription, long_description, and tag content, refers, and link. The exporter now emits these as authored, so the diff tool has nothing to reconcile. Normalizing them would cancel out an intended change on both sides of a diff and report it as no change at all.
  • Expression and type fields keep the anchored global-prefix stripclass, default, extends, type, value, and the types/aliases/implements collections, matching what the exporter still normalizes.

Why it matters

Without the final commit, a corpus diff of #262 reports roughly zero documentation-text hunks — the tool strips the author-written backslash from {@see \parse_blocks()} on both sides, so exactly the change #262 exists to make becomes invisible to review. With this branch, that change shows up: the manual corpus verification quoted in #262's description was produced with this branch's prep-diff.php.

The earlier hand-rolled prose regex had the same failure mode in miniature: it matched inside string literals, rewriting 'see \Foo bar' to 'see Foo bar' on both sides. Replacing it with the exporter's own anchored \WP_Parser\strip_global_namespace_prefix() means a regression that re-introduces string corruption now surfaces in the diff instead of vanishing.

Commits

  • Normalize global names in the diff tool (02a2035) — the prep-diff changes originally bundled into Preserve documentation and hook string semantics #262's "Preserve expression output" commit: normalization for the fields whose output Preserve documentation and hook string semantics #262 changes.
  • Run the diff normalization tests and match the exporter's rules (da20168) — relocates tests/prep-diff-test.php to tests/phpunit/tests/prep-diff.php as a PHPUnit class so CI actually executes it (previously no runner invoked it), and swaps the prose regex for the exporter's shared helper.
  • Test diff normalization leaves documentation text alone (6ce9808) — pins the pass-through behavior.
  • Stop normalizing documentation text in the diff tool (9b41ff9) — drops the {@see}/{@link} rewrite and removes link and refers from the normalized keys.

Testing

  • 14 prep-diff cases across 7 test methods, covering documentation-text pass-through (5), anchored-prefix normalization of printed expressions including the string-literal cases (4), collection ordering, escape-sequence preservation, and the guard that a real content change still shows.
  • CI green on PHP 7.4 and 8.4.
  • prep-diff.php still runs standalone: php prep-diff.php < export.json.

sirreal added 19 commits August 14, 2026 11:44
`Hook_Reflector::getName()` short-circuited on `Scalar\String_` nodes and
returned the interpreted string value. Escape sequences were therefore
resolved, so `do_action( "\x09tab" )` exported a literal tab and
`do_action( "\xC0 bad" )` exported a raw 0xC0 byte. That byte is not valid
UTF-8, `json_encode()` returns `false` for it, and `wp parser export`
silently produced no JSON at all for the whole run.

Route string nodes through `Pretty_Printer` like every other expression.
The printer returns php-parser's `rawValue` attribute, which is the
source-verbatim spelling, and `cleanupName()` strips the quotes.

Also check `json_encode()` for failure in `Command::_get_phpdoc_data()` and
fail loudly with `json_last_error_msg()` instead of writing an empty file.
The pretty printer inherits an override that returns PHP-Parser's `rawValue`
attribute so escape sequences are not interpreted. PHP-Parser sets that
attribute to the body of a doc string, without the delimiters, so
`apply_filters( 'f', <<<EOT ... EOT, 2 )` exported its argument as a bare
`body` string with embedded newlines instead of PHP source.

Print heredoc and nowdoc nodes with the default printer, which reproduces
the `<<<LABEL ... LABEL` form and does not interpret escape sequences in doc
strings either.
`pName_FullyQualified()` prints single-segment fully-qualified names without
the leading backslash regardless of namespace context, so inside a namespaced
file the printed form denotes a namespaced symbol rather than the global one.
This is an accepted limitation because the parser targets global-namespace
WordPress core code.
The global namespace prefixes are stripped from inline `{@link}` and
`{@see}` references after the DocBlock text has been rendered, so the
stripping also reached into rendered code regions and silently deleted
the backslash an author had written in a verbatim code sample.

Carve out `<code>` regions before stripping, the same way `fix_newlines()`
protects the newlines in those regions, so code samples are exported as
they were written.
The quote-stripping pattern required a body free of quote characters, so
a hook name that contained one, like `do_action( "it's" );`, was exported
with the quotes that surround it in the source.

Match the opening quote and require the same quote at the end, allowing
the body to hold the other quote character or an escaped copy of the
delimiter. Only that pair is stripped; the body keeps its source spelling,
so `do_action( 'it\'s' );` exports as `it\'s`. Concatenated expressions
still fall through to the dynamic-name handling below.
`Method_Call_Reflector::_getClassMapping()` maps a handful of WordPress factory
functions to the class they return, so that `get_current_screen()->add_help_tab()`
is exported as a use of `WP_Screen::add_help_tab()`. The lookup never matched
before this branch, because the printed receiver carried a leading backslash
that the mapping keys do not have. Pin the restored behavior with a test.
Inline `{@see}`/`{@link}` references in prose and the reference tokens
of `@see`/`@link` tags are documentation text: the export should carry
the author's spelling, backslash included. Pin that, and pin the
boundary on the other side: `@param` types pass through phpDocumentor's
type resolution, which prefixes every non-keyword type with a synthetic
`\`, so the author's spelling is unrecoverable there and the synthetic
prefix must keep being stripped.
Stop stripping global namespace prefixes from documentation text:
inline `{@see}`/`{@link}` references in descriptions and tag content,
and the reference tokens of `@see`/`@link` tags. These fields carry the
author's own words — phpDocumentor hands them over verbatim — so any
leading backslash present was written by hand and any rewrite imposes
a spelling the author didn't choose. The consumer that renders these
references (the developer.wordpress.org theme re-parses the inline tags
at render time) owns display policy.

This removes the code-region protection machinery along with the strip:
with nothing rewriting the text, code samples and inline code spans are
verbatim by construction, and the existing tests for them become plain
regression pins.

Tag types and argument types keep the strip: those values pass through
phpDocumentor's type resolution, which synthesizes a leading `\` on
every non-keyword type regardless of how the author spelled it, so the
prefix there is resolver output, not authorship.
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.
`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.
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.
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.
Comment thread lib/class-command.php

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add unescaped slashes, unescaped unicode, and unescaped line terminators here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant