Skip to content

Preserve backslashes in imported tag, argument, alias, and class meta - #287

Draft
sirreal wants to merge 2 commits into
masterfrom
fix-importer-meta-slashing
Draft

Preserve backslashes in imported tag, argument, alias, and class meta#287
sirreal wants to merge 2 commits into
masterfrom
fix-importer-meta-slashing

Conversation

@sirreal

@sirreal sirreal commented Aug 18, 2026

Copy link
Copy Markdown
Member

⚠️ Gated: do not merge before the wporg-developer theme is fixed

Merging this is blocked on theme-side fixes in wporg-developer. Today the
importer's unslash bug accidentally strips backslashes out of tag/arg/class
meta, and that accidental stripping is what currently masks several
backslash-intolerant spots in the theme. Fixing the importer alone would start
delivering backslashes to code that mishandles them, turning a silent data
corruption into a visible regression.

The theme items that must land first or simultaneously (items B1/B2/B4 in the
internal follow-ups handoff):

  • B1inc/template-tags.php calls DevHub_Formatting::link_internal_element()
    directly for the deprecation notice and drops the whole "Use %s instead."
    sentence when nothing links; a backslashed @see \Some_Class either vanishes
    or emits a wrong-post-type URL.
  • B2get_used_by() compares _wp-parser_extends meta_value against
    post_name with raw SQL equality, so any \ in the stored extends breaks a
    parent class's "Used by" list. This is the hard constraint: extends must
    stay stripped until B2 is fixed.
  • B4 — the bare-class branch of link_internal_element() in
    inc/formatting.php uses an anchored regex plus an exact-match exception
    list, so \WP_Query / \wpdb do not link there.

The bug

update_post_meta() runs its value through wp_unslash()
stripslashes_deep(), so backslashes are eaten on write. The importer already
compensates for two things and nothing else:

  • _wp_parser_namespace via addslashes()
  • _wp-parser_code_snippets / _wp-parser_setup_blueprints via
    map_deep( $value, 'wp_slash' ) (added with Export interactive PHP DocBlock snippets #258, with an explanatory comment
    and a pinning test)

Everything else is corrupted live, today, for any namespaced value:

input stored
\Foo Foo
\Foo\Bar FooBar
Vendor\Foo VendorFoo

So a @param \Foo\Bar $x is stored as FooBar.

The fix

Apply the same map_deep( $value, 'wp_slash' ) treatment, with a comment in the
same style, to the remaining fields:

meta key site
_wp-parser_extends Importer::import_class()
_wp-parser_implements Importer::import_class()
_wp-parser_properties Importer::import_class()
_wp-parser_args Importer::import_item()
_wp_parser_aliases Importer::import_item()
_wp-parser_tags Importer::import_item()

_wp_parser_namespace is deliberately untouched — it is already compensated
with addslashes(), and slashing it again would double the separators. A new
assertion pins that it still round-trips as Vendor\Docs.

This is importer-side only; the exporter is not touched, so a corpus diff should
show 0 hunks.

TDD

  • Add failing tests for backslash loss in imported meta — RED
  • Slash imported tag, argument, alias, and class meta — GREEN

The tests follow the existing pinning-test pattern
(File_Import_Test::test_function_snippet_metadata_preserves_backslashes) and
cover all three shapes — single leading backslash (\Foo), interior separators
(\Foo\Bar), and no-leading-slash namespaced (Vendor\Foo) — across tags
(refers and param types), argument types and defaults, aliases, extends,
implements, and property types.

RED (before the fix), showing exactly the corruption described above:

1) WP_Parser\Tests\File_Import_Test::test_function_tag_metadata_preserves_backslashes
-            0 => '\Foo'
-            1 => '\Foo\Bar'
-            2 => 'Vendor\Foo'
+            0 => 'Foo'
+            1 => 'FooBar'
+            2 => 'VendorFoo'

2) WP_Parser\Tests\File_Import_Test::test_function_argument_metadata_preserves_backslashes
-        'type' => '\Foo'
+        'type' => 'Foo'
...
3) WP_Parser\Tests\File_Import_Test::test_function_alias_metadata_preserves_backslashes
-    'Leading' => '\Foo'
+    'Leading' => 'Foo'
...
4) WP_Parser\Tests\File_Import_Test::test_class_metadata_preserves_backslashes
-'\Foo\Bar'
+'FooBar'

FAILURES!
Tests: 153, Assertions: 403, Failures: 4.

GREEN (after the fix), full suite:

OK (153 tests, 406 assertions)

Open question — the second strip at display time

lib/class-plugin.php:238 runs stripslashes_deep again at display time, as
the last filter in the sanitize_argument() chain used by make_args_safe().
Whether that second strip should survive is undecided, and this PR deliberately
does not change it. Worth settling together with the theme work: with the
importer fixed, that filter is now the only remaining place that silently eats
backslashes out of argument data on the way to the template.

Deviation from the reference implementation

An earlier local draft of this change used bare wp_slash() rather than
map_deep( $value, 'wp_slash' ), and also rewrote _wp_parser_namespace from
addslashes() to wp_slash(). This PR uses map_deep() for consistency with
the adjacent snippet/Blueprint code (it also reaches object properties, not just
array members, should any of these values ever carry decoded JSON objects), and
leaves _wp_parser_namespace alone. That earlier draft was also mixed with
unrelated type-rendering work; this PR contains only the meta-slashing concern.

Not included

The --env-cwd hardcoding in package.json breaks npm run test:phpunit from
any worktree not literally named phpdoc-parser. That is fixed separately in
#276 and is deliberately not part of this branch.

🤖 Generated with Claude Code

WordPress metadata APIs unslash their input, so backslashes in tag, argument,
alias, extends, implements, and properties meta are silently destroyed on
import. Pin the expected values before fixing the importer.
update_post_meta() unslashes its input, which silently destroyed backslashes in
namespaced values: \Foo became Foo, \Foo\Bar became FooBar, and Vendor\Foo
became VendorFoo. Compensate with map_deep( $value, 'wp_slash' ), matching the
treatment already applied to snippets and Blueprints.

_wp_parser_namespace is left alone; it is already compensated with addslashes()
and slashing it again would double the separators.
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