From 90e892417d4770bbfc5aba874c5fc822b847b853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 7 Jun 2026 02:37:02 +0200 Subject: [PATCH 01/12] Export method code snippets --- lib/runner.php | 43 ++++++++++++++++++++++++ tests/phpunit/tests/export/docblocks.inc | 15 +++++++++ tests/phpunit/tests/export/docblocks.php | 20 +++++++++++ 3 files changed, 78 insertions(+) diff --git a/lib/runner.php b/lib/runner.php index ba3efdd4..e8fa8477 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -188,6 +188,41 @@ function ( $matches ) use ( $replacement_string ) { return $text; } +/** + * Extract fenced code snippets from a DocBlock's raw long description. + * + * @param string $text + * + * @return array + */ +function export_docblock_code_snippets( $text ) { + if ( ! preg_match_all( '/(^|\n)```([^\r\n`]*)\r?\n(.*?)\r?\n```(?=\r?\n|$)/s', $text, $matches, PREG_SET_ORDER ) ) { + return array(); + } + + $snippets = array(); + + foreach ( $matches as $match ) { + $language = trim( $match[2] ); + if ( preg_match( '/^\S+/', $language, $language_matches ) ) { + $language = $language_matches[0]; + } + + $snippet = array( + 'language' => $language, + 'code' => rtrim( preg_replace( "/\r\n?/", "\n", $match[3] ), "\n" ), + ); + + if ( 'php' === strtolower( $language ) ) { + $snippet['type'] = 'php-code-snippet'; + } + + $snippets[] = $snippet; + } + + return $snippets; +} + /** * @param BaseReflector|ReflectionAbstract $element * @@ -335,6 +370,14 @@ function export_methods( array $methods ) { 'doc' => export_docblock( $method ), ); + $docblock = $method->getDocBlock(); + if ( $docblock ) { + $code_snippets = export_docblock_code_snippets( $docblock->getLongDescription()->getContents() ); + if ( ! empty( $code_snippets ) ) { + $method_data['doc']['code_snippets'] = $code_snippets; + } + } + if ( ! empty( $method->uses ) ) { $method_data['uses'] = export_uses( $method->uses ); diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index 05686f51..8337a8b0 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -61,6 +61,21 @@ class Test_Class { public function test_method( $var, $arr ) { return $var; } + + /** + * This is a method docblock with a code snippet. + * + * Use this example: + * + * ```php + * assertMethodHasDocs( + 'Test_Class' + , 'test_method_with_code_snippet' + , array( + 'code_snippets' => array( + array( + 'language' => 'php', + 'code' => " 'php-code-snippet', + ), + ), + ) + ); + } + /** * Test that function docs are exported. */ From 412fc815414a755abe142f8e351fabb46f73e883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 7 Jun 2026 19:04:11 +0200 Subject: [PATCH 02/12] Handle markdown-style code fences --- lib/runner.php | 32 +++++++--- tests/phpunit/tests/export/docblocks.php | 76 ++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index e8fa8477..b59990ef 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -196,21 +196,39 @@ function ( $matches ) use ( $replacement_string ) { * @return array */ function export_docblock_code_snippets( $text ) { - if ( ! preg_match_all( '/(^|\n)```([^\r\n`]*)\r?\n(.*?)\r?\n```(?=\r?\n|$)/s', $text, $matches, PREG_SET_ORDER ) ) { - return array(); - } - + $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); $snippets = array(); - foreach ( $matches as $match ) { - $language = trim( $match[2] ); + for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { + if ( ! preg_match( '/^[ \t]{0,3}(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { + continue; + } + + $fence = $opening[1]; + $language = trim( $opening[2] ); + $code_lines = array(); + + for ( $j = $i + 1; $j < $line_count; $j++ ) { + // Match the exact opening fence so shorter fences can appear inside longer fences. + if ( preg_match( '/^[ \t]{0,3}' . preg_quote( $fence, '/' ) . '[ \t]*$/', $lines[ $j ] ) ) { + $i = $j; + break; + } + + $code_lines[] = $lines[ $j ]; + } + + if ( $j === $line_count ) { + continue; + } + if ( preg_match( '/^\S+/', $language, $language_matches ) ) { $language = $language_matches[0]; } $snippet = array( 'language' => $language, - 'code' => rtrim( preg_replace( "/\r\n?/", "\n", $match[3] ), "\n" ), + 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), ); if ( 'php' === strtolower( $language ) ) { diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index a671c255..a3e284da 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -151,6 +151,82 @@ public function test_method_code_snippets() { ); } + /** + * Test tricky Markdown-like code fence parsing rules. + */ + public function test_code_snippet_fence_parser_edge_cases() { + + $description = implode( + "\n", + array( + 'Inline ```php is not a fence.', + '', + '``', + 'Two backticks are too short.', + '``', + '', + '````php title="outer.php"', + 'assertEquals( + array( + array( + 'language' => 'php', + 'code' => " 'php-code-snippet', + ), + array( + 'language' => 'php', + 'code' => " 'php-code-snippet', + ), + array( + 'language' => 'php', + 'code' => " 'php-code-snippet', + ), + array( + 'language' => 'js', + 'code' => 'console.log("another snippet");', + ), + ), + \WP_Parser\export_docblock_code_snippets( $description ) + ); + } + /** * Test that function docs are exported. */ From ff4a8c88031b9aee946240cfa9d35008d43dd3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 7 Jun 2026 20:37:40 +0200 Subject: [PATCH 03/12] Loosen docblock fence indentation --- lib/runner.php | 23 ++++++++++++++++------- tests/phpunit/tests/export/docblocks.php | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index b59990ef..c1ca27d1 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -191,6 +191,10 @@ function ( $matches ) use ( $replacement_string ) { /** * Extract fenced code snippets from a DocBlock's raw long description. * + * Backtick fences may be indented in DocBlocks or nested Markdown lists. The + * closing fence must use the same number of backticks as the opener so + * different-length fences can appear inside a fenced snippet. + * * @param string $text * * @return array @@ -200,26 +204,31 @@ function export_docblock_code_snippets( $text ) { $snippets = array(); for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { - if ( ! preg_match( '/^[ \t]{0,3}(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { + if ( ! preg_match( '/^([ \t]*)(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { continue; } - $fence = $opening[1]; - $language = trim( $opening[2] ); + $indent = $opening[1]; + $fence = $opening[2]; + $language = trim( $opening[3] ); $code_lines = array(); for ( $j = $i + 1; $j < $line_count; $j++ ) { - // Match the exact opening fence so shorter fences can appear inside longer fences. - if ( preg_match( '/^[ \t]{0,3}' . preg_quote( $fence, '/' ) . '[ \t]*$/', $lines[ $j ] ) ) { + // Match the exact opening fence so different-length fences stay in the snippet. + if ( preg_match( '/^[ \t]*' . preg_quote( $fence, '/' ) . '[ \t]*$/', $lines[ $j ] ) ) { $i = $j; break; } - $code_lines[] = $lines[ $j ]; + if ( '' !== $indent && 0 === strpos( $lines[ $j ], $indent ) ) { + $code_lines[] = substr( $lines[ $j ], strlen( $indent ) ); + } else { + $code_lines[] = $lines[ $j ]; + } } if ( $j === $line_count ) { - continue; + break; } if ( preg_match( '/^\S+/', $language, $language_matches ) ) { diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index a3e284da..7be9ad2c 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -175,7 +175,7 @@ public function test_code_snippet_fence_parser_edge_cases() { '', '```php', ' 'php', - 'code' => " " 'php-code-snippet', + ), + array( + 'language' => 'php', + 'code' => " 'php-code-snippet', ), array( From a0167391f96b0f44b7bbb293c0fd9a311f036617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 7 Jun 2026 22:24:00 +0200 Subject: [PATCH 04/12] Move code snippet parser near method export --- lib/runner.php | 124 ++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index c1ca27d1..092eb22a 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -188,68 +188,6 @@ function ( $matches ) use ( $replacement_string ) { return $text; } -/** - * Extract fenced code snippets from a DocBlock's raw long description. - * - * Backtick fences may be indented in DocBlocks or nested Markdown lists. The - * closing fence must use the same number of backticks as the opener so - * different-length fences can appear inside a fenced snippet. - * - * @param string $text - * - * @return array - */ -function export_docblock_code_snippets( $text ) { - $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); - $snippets = array(); - - for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { - if ( ! preg_match( '/^([ \t]*)(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { - continue; - } - - $indent = $opening[1]; - $fence = $opening[2]; - $language = trim( $opening[3] ); - $code_lines = array(); - - for ( $j = $i + 1; $j < $line_count; $j++ ) { - // Match the exact opening fence so different-length fences stay in the snippet. - if ( preg_match( '/^[ \t]*' . preg_quote( $fence, '/' ) . '[ \t]*$/', $lines[ $j ] ) ) { - $i = $j; - break; - } - - if ( '' !== $indent && 0 === strpos( $lines[ $j ], $indent ) ) { - $code_lines[] = substr( $lines[ $j ], strlen( $indent ) ); - } else { - $code_lines[] = $lines[ $j ]; - } - } - - if ( $j === $line_count ) { - break; - } - - if ( preg_match( '/^\S+/', $language, $language_matches ) ) { - $language = $language_matches[0]; - } - - $snippet = array( - 'language' => $language, - 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), - ); - - if ( 'php' === strtolower( $language ) ) { - $snippet['type'] = 'php-code-snippet'; - } - - $snippets[] = $snippet; - } - - return $snippets; -} - /** * @param BaseReflector|ReflectionAbstract $element * @@ -419,6 +357,68 @@ function export_methods( array $methods ) { return $output; } +/** + * Extract fenced code snippets from a DocBlock's raw long description. + * + * Backtick fences may be indented in DocBlocks or nested Markdown lists. The + * closing fence must use the same number of backticks as the opener so + * different-length fences can appear inside a fenced snippet. + * + * @param string $text + * + * @return array + */ +function export_docblock_code_snippets( $text ) { + $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); + $snippets = array(); + + for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { + if ( ! preg_match( '/^([ \t]*)(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { + continue; + } + + $indent = $opening[1]; + $fence = $opening[2]; + $language = trim( $opening[3] ); + $code_lines = array(); + + for ( $j = $i + 1; $j < $line_count; $j++ ) { + // Match the exact opening fence so different-length fences stay in the snippet. + if ( preg_match( '/^[ \t]*' . preg_quote( $fence, '/' ) . '[ \t]*$/', $lines[ $j ] ) ) { + $i = $j; + break; + } + + if ( '' !== $indent && 0 === strpos( $lines[ $j ], $indent ) ) { + $code_lines[] = substr( $lines[ $j ], strlen( $indent ) ); + } else { + $code_lines[] = $lines[ $j ]; + } + } + + if ( $j === $line_count ) { + break; + } + + if ( preg_match( '/^\S+/', $language, $language_matches ) ) { + $language = $language_matches[0]; + } + + $snippet = array( + 'language' => $language, + 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), + ); + + if ( 'php' === strtolower( $language ) ) { + $snippet['type'] = 'php-code-snippet'; + } + + $snippets[] = $snippet; + } + + return $snippets; +} + /** * Export the list of elements used by a file or structure. * From 25bfa96fc72d258be147dc29970011d5a60af501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 7 Jun 2026 23:48:38 +0200 Subject: [PATCH 05/12] Export expected output and blueprints --- lib/runner.php | 108 +++++++++++++++++++-- tests/phpunit/tests/export/docblocks.inc | 19 +++- tests/phpunit/tests/export/docblocks.php | 115 ++++++++++++++++++++--- 3 files changed, 224 insertions(+), 18 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 092eb22a..3c4c0cda 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -358,11 +358,13 @@ function export_methods( array $methods ) { } /** - * Extract fenced code snippets from a DocBlock's raw long description. + * Extract runnable PHP snippets from a DocBlock's raw long description. * * Backtick fences may be indented in DocBlocks or nested Markdown lists. The * closing fence must use the same number of backticks as the opener so - * different-length fences can appear inside a fenced snippet. + * different-length fences can appear inside a fenced snippet. Blueprint fences + * before a PHP fence apply to that fence, while immediately following metadata + * fences apply to the preceding PHP fence. * * @param string $text * @@ -370,6 +372,7 @@ function export_methods( array $methods ) { */ function export_docblock_code_snippets( $text ) { $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); + $fences = array(); $snippets = array(); for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { @@ -404,13 +407,66 @@ function export_docblock_code_snippets( $text ) { $language = $language_matches[0]; } - $snippet = array( - 'language' => $language, + $fences[] = array( + 'language' => strtolower( $language ), + 'info' => strtolower( trim( $opening[3] ) ), 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), ); + } + + $pending_blueprint = null; + $consumed_fences = array(); + $fence_count = count( $fences ); + + for ( $i = 0; $i < $fence_count; $i++ ) { + if ( isset( $consumed_fences[ $i ] ) ) { + continue; + } + + if ( is_docblock_blueprint_fence( $fences[ $i ] ) ) { + $pending_blueprint = decode_docblock_blueprint( $fences[ $i ]['code'] ); + continue; + } + + if ( 'php' !== $fences[ $i ]['language'] ) { + $pending_blueprint = null; + continue; + } + + $snippet = array( + 'type' => 'php-code-snippet', + 'code' => $fences[ $i ]['code'], + 'expected_output' => '', + ); + $has_expected_output = false; + + if ( null !== $pending_blueprint ) { + $snippet['blueprint'] = $pending_blueprint; + $pending_blueprint = null; + } + + for ( $j = $i + 1; $j < $fence_count; $j++ ) { + if ( 'php' === $fences[ $j ]['language'] ) { + break; + } + + if ( is_docblock_expected_output_fence( $fences[ $j ] ) ) { + if ( ! $has_expected_output ) { + $snippet['expected_output'] = $fences[ $j ]['code']; + $has_expected_output = true; + $consumed_fences[ $j ] = true; + } + + break; + } - if ( 'php' === strtolower( $language ) ) { - $snippet['type'] = 'php-code-snippet'; + if ( is_docblock_blueprint_fence( $fences[ $j ] ) && ! array_key_exists( 'blueprint', $snippet ) ) { + $snippet['blueprint'] = decode_docblock_blueprint( $fences[ $j ]['code'] ); + $consumed_fences[ $j ] = true; + continue; + } + + break; } $snippets[] = $snippet; @@ -419,6 +475,46 @@ function export_docblock_code_snippets( $text ) { return $snippets; } +/** + * Checks whether a parsed DocBlock fence contains snippet expected output. + * + * @param array $fence + * + * @return bool + */ +function is_docblock_expected_output_fence( $fence ) { + return in_array( $fence['language'], array( 'expected-output', 'expected_output', 'output', 'text/expected-output' ), true ); +} + +/** + * Checks whether a parsed DocBlock fence contains a WordPress Playground Blueprint. + * + * @param array $fence + * + * @return bool + */ +function is_docblock_blueprint_fence( $fence ) { + return in_array( $fence['language'], array( 'blueprint', 'setup-blueprint' ), true ) + || ( 'json' === $fence['language'] && false !== strpos( ' ' . $fence['info'] . ' ', ' blueprint ' ) ); +} + +/** + * Decodes a Blueprint fence into the structure exported to JSON. + * + * @param string $blueprint + * + * @return array|string + */ +function decode_docblock_blueprint( $blueprint ) { + $decoded = json_decode( $blueprint, true ); + + if ( is_array( $decoded ) ) { + return $decoded; + } + + return $blueprint; +} + /** * Export the list of elements used by a file or structure. * diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index 8337a8b0..de073f08 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -67,9 +67,26 @@ class Test_Class { * * Use this example: * + * ```blueprint + * { + * "steps": [ + * { + * "step": "writeFile", + * "path": "/wordpress/wp-content/mu-plugins/docs-fixture.php", + * "data": " array( array( - 'language' => 'php', - 'code' => " 'php-code-snippet', + 'code' => " 'Hello from a method', + 'blueprint' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/wordpress/wp-content/mu-plugins/docs-fixture.php', + 'data' => "assertEquals( array( array( - 'language' => 'php', - 'code' => " 'php-code-snippet', + 'code' => " 'outer', ), array( - 'language' => 'php', - 'code' => " 'php-code-snippet', + 'code' => " 'different-length', ), array( - 'language' => 'php', - 'code' => " 'php-code-snippet', + 'code' => " 'indented', ), array( - 'language' => 'php', + 'type' => 'php-code-snippet', 'code' => " 'three leading spaces', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " 'no blueprint from before JS', + ), + array( 'type' => 'php-code-snippet', + 'code' => " 'blueprint before', + 'blueprint' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/one.php', + 'data' => ' 'js', - 'code' => 'console.log("another snippet");', + 'type' => 'php-code-snippet', + 'code' => " 'blueprint after', + 'blueprint' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/two.php', + 'data' => ' Date: Mon, 8 Jun 2026 00:15:20 +0200 Subject: [PATCH 06/12] Add focused code snippet parser tests --- tests/phpunit/tests/export/docblocks.php | 155 +++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index 34cce01f..5b857cce 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -329,6 +329,161 @@ public function test_code_snippet_fence_parser_edge_cases() { ); } + /** + * Test that PHP snippets export the renderer fields even without metadata. + */ + public function test_code_snippet_without_metadata() { + + $this->assertEquals( + array( + array( + 'type' => 'php-code-snippet', + 'code' => " '', + ), + ), + \WP_Parser\export_docblock_code_snippets( + implode( + "\n", + array( + '```php', + 'assertEquals( + array( + array( + 'type' => 'php-code-snippet', + 'code' => " 'case fixture', + 'blueprint' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/case.php', + 'data' => 'assertEquals( + array( + array( + 'type' => 'php-code-snippet', + 'code' => " '', + 'blueprint' => 'not-json', + ), + ), + \WP_Parser\export_docblock_code_snippets( + implode( + "\n", + array( + '```blueprint', + 'not-json', + '```', + '```php', + 'assertEquals( + array( + array( + 'type' => 'php-code-snippet', + 'code' => " 'First', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " '', + 'blueprint' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/second.php', + 'data' => ' Date: Mon, 8 Jun 2026 00:19:44 +0200 Subject: [PATCH 07/12] Allow legacy PHPUnit install in CI --- composer.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aa09726d..a8713797 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,11 @@ "composer/installers": true }, "_process-timeout-comment": "Work around `test:watch` timeout, see https://github.com/spatie/phpunit-watcher/issues/63#issuecomment-545633709", - "process-timeout": 0 + "process-timeout": 0, + "policy": { + "advisories": { + "block": false + } + } } } From 8c233f7252007d4253d464607412c7bba21a9ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 00:30:04 +0200 Subject: [PATCH 08/12] Support reusable setup blueprints --- lib/runner.php | 106 +++++++++++++++-- tests/phpunit/tests/export/docblocks.inc | 40 +++++++ tests/phpunit/tests/export/docblocks.php | 141 +++++++++++++++++++++++ 3 files changed, 279 insertions(+), 8 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 3c4c0cda..adf417be 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -337,10 +337,14 @@ function export_methods( array $methods ) { $docblock = $method->getDocBlock(); if ( $docblock ) { - $code_snippets = export_docblock_code_snippets( $docblock->getLongDescription()->getContents() ); + $setup_blueprints = array(); + $code_snippets = export_docblock_code_snippets( $docblock->getLongDescription()->getContents(), $setup_blueprints ); if ( ! empty( $code_snippets ) ) { $method_data['doc']['code_snippets'] = $code_snippets; } + if ( ! empty( $setup_blueprints ) ) { + $method_data['doc']['setup_blueprints'] = $setup_blueprints; + } } if ( ! empty( $method->uses ) ) { @@ -364,13 +368,15 @@ function export_methods( array $methods ) { * closing fence must use the same number of backticks as the opener so * different-length fences can appear inside a fenced snippet. Blueprint fences * before a PHP fence apply to that fence, while immediately following metadata - * fences apply to the preceding PHP fence. + * fences apply to the preceding PHP fence. Named setup Blueprint fences are + * exported once and snippets refer to them by name. * - * @param string $text + * @param string $text Raw DocBlock long description. + * @param array $setup_blueprints Optional. Named setup Blueprints keyed by reference name. * * @return array */ -function export_docblock_code_snippets( $text ) { +function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); $fences = array(); $snippets = array(); @@ -409,7 +415,7 @@ function export_docblock_code_snippets( $text ) { $fences[] = array( 'language' => strtolower( $language ), - 'info' => strtolower( trim( $opening[3] ) ), + 'info' => trim( $opening[3] ), 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), ); } @@ -417,12 +423,24 @@ function export_docblock_code_snippets( $text ) { $pending_blueprint = null; $consumed_fences = array(); $fence_count = count( $fences ); + $setup_blueprints = array(); + + foreach ( $fences as $fence ) { + $setup_blueprint_name = get_docblock_setup_blueprint_name( $fence ); + if ( null !== $setup_blueprint_name ) { + $setup_blueprints[ $setup_blueprint_name ] = decode_docblock_blueprint( $fence['code'] ); + } + } for ( $i = 0; $i < $fence_count; $i++ ) { if ( isset( $consumed_fences[ $i ] ) ) { continue; } + if ( null !== get_docblock_setup_blueprint_name( $fences[ $i ] ) ) { + continue; + } + if ( is_docblock_blueprint_fence( $fences[ $i ] ) ) { $pending_blueprint = decode_docblock_blueprint( $fences[ $i ]['code'] ); continue; @@ -440,8 +458,15 @@ function export_docblock_code_snippets( $text ) { ); $has_expected_output = false; + $referenced_blueprint_name = get_docblock_referenced_blueprint_name( $fences[ $i ] ); + if ( null !== $referenced_blueprint_name ) { + $snippet['blueprint'] = $referenced_blueprint_name; + } + if ( null !== $pending_blueprint ) { - $snippet['blueprint'] = $pending_blueprint; + if ( ! array_key_exists( 'blueprint', $snippet ) ) { + $snippet['blueprint'] = $pending_blueprint; + } $pending_blueprint = null; } @@ -460,6 +485,10 @@ function export_docblock_code_snippets( $text ) { break; } + if ( null !== get_docblock_setup_blueprint_name( $fences[ $j ] ) ) { + break; + } + if ( is_docblock_blueprint_fence( $fences[ $j ] ) && ! array_key_exists( 'blueprint', $snippet ) ) { $snippet['blueprint'] = decode_docblock_blueprint( $fences[ $j ]['code'] ); $consumed_fences[ $j ] = true; @@ -494,8 +523,14 @@ function is_docblock_expected_output_fence( $fence ) { * @return bool */ function is_docblock_blueprint_fence( $fence ) { - return in_array( $fence['language'], array( 'blueprint', 'setup-blueprint' ), true ) - || ( 'json' === $fence['language'] && false !== strpos( ' ' . $fence['info'] . ' ', ' blueprint ' ) ); + if ( null !== get_docblock_setup_blueprint_name( $fence ) ) { + return false; + } + + $info = strtolower( $fence['info'] ); + + return in_array( $fence['language'], array( 'blueprint', 'setup-blueprint', 'setupblueprint' ), true ) + || ( 'json' === $fence['language'] && false !== strpos( ' ' . $info . ' ', ' blueprint ' ) ); } /** @@ -515,6 +550,61 @@ function decode_docblock_blueprint( $blueprint ) { return $blueprint; } +/** + * Returns the reference name for a reusable setup Blueprint fence. + * + * @param array $fence + * + * @return string|null + */ +function get_docblock_setup_blueprint_name( $fence ) { + $info_parts = get_docblock_fence_info_parts( $fence ); + + if ( in_array( $fence['language'], array( 'setup-blueprint', 'setupblueprint' ), true ) && isset( $info_parts[1] ) ) { + return $info_parts[1]; + } + + if ( 'json' === $fence['language'] && isset( $info_parts[1] ) && in_array( strtolower( $info_parts[1] ), array( 'setup-blueprint', 'setupblueprint' ), true ) && isset( $info_parts[2] ) ) { + return $info_parts[2]; + } + + return null; +} + +/** + * Returns the setup Blueprint reference from a PHP fence info string. + * + * @param array $fence + * + * @return string|null + */ +function get_docblock_referenced_blueprint_name( $fence ) { + foreach ( get_docblock_fence_info_parts( $fence ) as $part ) { + if ( preg_match( '/^(?:blueprint|setup-blueprint|setupblueprint)=(.+)$/i', $part, $matches ) ) { + return $matches[1]; + } + } + + return null; +} + +/** + * Splits the full fence info string into whitespace-delimited parts. + * + * @param array $fence + * + * @return array + */ +function get_docblock_fence_info_parts( $fence ) { + $info = trim( $fence['info'] ); + + if ( '' === $info ) { + return array(); + } + + return preg_split( '/\s+/', $info ); +} + /** * Export the list of elements used by a file or structure. * diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index de073f08..c686383a 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -93,6 +93,46 @@ class Test_Class { */ public function test_method_with_code_snippet() { } + + /** + * This is a method docblock with a reused setup Blueprint. + * + * ```setupblueprint shared-greeting + * { + * "steps": [ + * { + * "step": "writeFile", + * "path": "/wordpress/wp-content/mu-plugins/shared-greeting.php", + * "data": "assertMethodHasDocs( + 'Test_Class' + , 'test_method_with_reused_setup_blueprint' + , array( + 'setup_blueprints' => array( + 'shared-greeting' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/wordpress/wp-content/mu-plugins/shared-greeting.php', + 'data' => " array( + array( + 'type' => 'php-code-snippet', + 'code' => " 'Hello, first', + 'blueprint' => 'shared-greeting', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " 'Hello, second', + 'blueprint' => 'shared-greeting', + ), + ), + ) + ); + } + /** * Test tricky Markdown-like code fence parsing rules. */ @@ -484,6 +522,109 @@ public function test_code_snippet_metadata_boundaries() { ); } + /** + * Test named setup Blueprint definitions and references. + */ + public function test_code_snippet_named_setup_blueprints() { + + $setup_blueprints = array(); + $snippets = \WP_Parser\export_docblock_code_snippets( + implode( + "\n", + array( + '```setup-blueprint shared', + '{"steps":[{"step":"writeFile","path":"/tmp/shared.php","data":"assertEquals( + array( + 'shared' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/shared.php', + 'data' => ' array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/tmp/json-shared.php', + 'data' => 'assertEquals( + array( + array( + 'type' => 'php-code-snippet', + 'code' => " 'first', + 'blueprint' => 'shared', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " 'no leaked inline blueprint', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " 'second', + 'blueprint' => 'json-shared', + ), + array( + 'type' => 'php-code-snippet', + 'code' => " '', + 'blueprint' => 'shared', + ), + ), + $snippets + ); + } + /** * Test that function docs are exported. */ From a4042c1ee342870bea17dd2a1e8bb998f3536b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 12:55:53 +0200 Subject: [PATCH 09/12] Store code snippet metadata during import --- lib/class-importer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/class-importer.php b/lib/class-importer.php index bc723723..44ad563e 100644 --- a/lib/class-importer.php +++ b/lib/class-importer.php @@ -760,6 +760,8 @@ public function import_item( array $data, $parent_post_id = 0, $import_ignored = $anything_updated[] = update_post_meta( $post_id, '_wp-parser_line_num', (string) $data['line'] ); $anything_updated[] = update_post_meta( $post_id, '_wp-parser_end_line_num', (string) $data['end_line'] ); $anything_updated[] = update_post_meta( $post_id, '_wp-parser_tags', $data['doc']['tags'] ); + $anything_updated[] = update_post_meta( $post_id, '_wp-parser_code_snippets', $data['doc']['code_snippets'] ?? array() ); + $anything_updated[] = update_post_meta( $post_id, '_wp-parser_setup_blueprints', $data['doc']['setup_blueprints'] ?? array() ); $anything_updated[] = update_post_meta( $post_id, '_wp-parser_last_parsed_wp_version', $this->version ); // If the post didn't need to be updated, but meta or tax changed, update it to bump last modified. From 4e043645072320f9b424223cfe35853e3e5e7240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 13:07:52 +0200 Subject: [PATCH 10/12] Tighten snippet export metadata flow --- .github/workflows/unit-test.yml | 3 +- composer.json | 7 +- lib/runner.php | 182 +++++++++++++++++++---- tests/phpunit/tests/export/docblocks.inc | 30 ++++ tests/phpunit/tests/export/docblocks.php | 49 +++++- 5 files changed, 231 insertions(+), 40 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 9e5ea6a9..bfdb84c1 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -27,7 +27,8 @@ jobs: - name: Setup Environment run: | rm composer.lock - npm run setup + npm run start + npm run composer -- install --no-audit - name: Test run: npm run test diff --git a/composer.json b/composer.json index a8713797..aa09726d 100644 --- a/composer.json +++ b/composer.json @@ -47,11 +47,6 @@ "composer/installers": true }, "_process-timeout-comment": "Work around `test:watch` timeout, see https://github.com/spatie/phpunit-watcher/issues/63#issuecomment-545633709", - "process-timeout": 0, - "policy": { - "advisories": { - "block": false - } - } + "process-timeout": 0 } } diff --git a/lib/runner.php b/lib/runner.php index adf417be..e81071c9 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -55,9 +55,12 @@ function parse_files( $files, $root ) { $file->process(); + $file_doc = export_docblock( $file ); + $file_setup_blueprints = $file_doc['setup_blueprints'] ?? array(); + // TODO proper exporter $out = array( - 'file' => export_docblock( $file ), + 'file' => $file_doc, 'path' => str_replace( DIRECTORY_SEPARATOR, '/', $file->getFilename() ), 'root' => $root, ); @@ -94,7 +97,7 @@ function parse_files( $files, $root ) { 'line' => $function->getLineNumber(), 'end_line' => $function->getNode()->getAttribute( 'endLine' ), 'arguments' => export_arguments( $function->getArguments() ), - 'doc' => export_docblock( $function ), + 'doc' => export_docblock( $function, $file_setup_blueprints ), 'hooks' => array(), ); @@ -110,6 +113,9 @@ function parse_files( $files, $root ) { } foreach ( $file->getClasses() as $class ) { + $class_doc = export_docblock( $class, $file_setup_blueprints ); + $class_setup_blueprints = array_merge( $file_setup_blueprints, $class_doc['setup_blueprints'] ?? array() ); + $class_data = array( 'name' => $class->getShortName(), 'namespace' => $class->getNamespace(), @@ -120,8 +126,8 @@ function parse_files( $files, $root ) { 'extends' => $class->getParentClass(), 'implements' => $class->getInterfaces(), 'properties' => export_properties( $class->getProperties() ), - 'methods' => export_methods( $class->getMethods() ), - 'doc' => export_docblock( $class ), + 'methods' => export_methods( $class->getMethods(), $class_setup_blueprints ), + 'doc' => $class_doc, ); $out['classes'][] = $class_data; @@ -190,10 +196,11 @@ function ( $matches ) use ( $replacement_string ) { /** * @param BaseReflector|ReflectionAbstract $element + * @param array $inherited_setup_blueprints Optional. Setup Blueprints inherited from the file or class DocBlock. * * @return array */ -function export_docblock( $element ) { +function export_docblock( $element, array $inherited_setup_blueprints = array() ) { $docblock = $element->getDocBlock(); if ( ! $docblock ) { return array( @@ -203,12 +210,27 @@ function export_docblock( $element ) { ); } + $raw_long_description = $docblock->getLongDescription()->getContents(); + $setup_blueprints = array(); + $code_snippets = export_docblock_code_snippets( $raw_long_description, $setup_blueprints ); + $setup_blueprints = array_merge( + get_referenced_setup_blueprints( $code_snippets, $inherited_setup_blueprints ), + $setup_blueprints + ); + $output = array( 'description' => preg_replace( '/[\n\r]+/', ' ', $docblock->getShortDescription() ), - 'long_description' => fix_newlines( $docblock->getLongDescription()->getFormattedContents() ), + 'long_description' => format_long_description( strip_docblock_code_snippet_fences( $raw_long_description ) ), 'tags' => array(), ); + if ( ! empty( $code_snippets ) ) { + $output['code_snippets'] = $code_snippets; + } + if ( ! empty( $setup_blueprints ) ) { + $output['setup_blueprints'] = $setup_blueprints; + } + foreach ( $docblock->getTags() as $tag ) { $tag_data = array( 'name' => $tag->getName(), @@ -313,10 +335,11 @@ function export_properties( array $properties ) { /** * @param MethodReflector[] $methods + * @param array $inherited_setup_blueprints Optional. Setup Blueprints inherited from the file or class DocBlock. * * @return array */ -function export_methods( array $methods ) { +function export_methods( array $methods, array $inherited_setup_blueprints = array() ) { $output = array(); foreach ( $methods as $method ) { @@ -332,21 +355,9 @@ function export_methods( array $methods ) { 'static' => $method->isStatic(), 'visibility' => $method->getVisibility(), 'arguments' => export_arguments( $method->getArguments() ), - 'doc' => export_docblock( $method ), + 'doc' => export_docblock( $method, $inherited_setup_blueprints ), ); - $docblock = $method->getDocBlock(); - if ( $docblock ) { - $setup_blueprints = array(); - $code_snippets = export_docblock_code_snippets( $docblock->getLongDescription()->getContents(), $setup_blueprints ); - if ( ! empty( $code_snippets ) ) { - $method_data['doc']['code_snippets'] = $code_snippets; - } - if ( ! empty( $setup_blueprints ) ) { - $method_data['doc']['setup_blueprints'] = $setup_blueprints; - } - } - if ( ! empty( $method->uses ) ) { $method_data['uses'] = export_uses( $method->uses ); @@ -362,24 +373,15 @@ function export_methods( array $methods ) { } /** - * Extract runnable PHP snippets from a DocBlock's raw long description. + * Returns Markdown-like backtick fences from a DocBlock's raw long description. * - * Backtick fences may be indented in DocBlocks or nested Markdown lists. The - * closing fence must use the same number of backticks as the opener so - * different-length fences can appear inside a fenced snippet. Blueprint fences - * before a PHP fence apply to that fence, while immediately following metadata - * fences apply to the preceding PHP fence. Named setup Blueprint fences are - * exported once and snippets refer to them by name. - * - * @param string $text Raw DocBlock long description. - * @param array $setup_blueprints Optional. Named setup Blueprints keyed by reference name. + * @param string $text Raw DocBlock long description. * * @return array */ -function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { +function get_docblock_code_fences( $text ) { $lines = explode( "\n", preg_replace( "/\r\n?/", "\n", $text ) ); $fences = array(); - $snippets = array(); for ( $i = 0, $line_count = count( $lines ); $i < $line_count; $i++ ) { if ( ! preg_match( '/^([ \t]*)(`{3,})([^`]*)$/', $lines[ $i ], $opening ) ) { @@ -390,6 +392,7 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { $fence = $opening[2]; $language = trim( $opening[3] ); $code_lines = array(); + $start_line = $i; for ( $j = $i + 1; $j < $line_count; $j++ ) { // Match the exact opening fence so different-length fences stay in the snippet. @@ -417,9 +420,33 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { 'language' => strtolower( $language ), 'info' => trim( $opening[3] ), 'code' => rtrim( implode( "\n", $code_lines ), "\n" ), + 'start' => $start_line, + 'end' => $i, ); } + return $fences; +} + +/** + * Extract runnable PHP snippets from a DocBlock's raw long description. + * + * Backtick fences may be indented in DocBlocks or nested Markdown lists. The + * closing fence must use the same number of backticks as the opener so + * different-length fences can appear inside a fenced snippet. Blueprint fences + * before a PHP fence apply to that fence, while immediately following metadata + * fences apply to the preceding PHP fence. Named setup Blueprint fences are + * exported once and snippets refer to them by name. + * + * @param string $text Raw DocBlock long description. + * @param array $setup_blueprints Optional. Named setup Blueprints keyed by reference name. + * + * @return array + */ +function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { + $fences = get_docblock_code_fences( $text ); + $snippets = array(); + $pending_blueprint = null; $consumed_fences = array(); $fence_count = count( $fences ); @@ -504,6 +531,79 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { return $snippets; } +/** + * Removes snippet and snippet-metadata fences from the rendered description. + * + * Once a PHP fence becomes structured `code_snippets` data, leaving the same + * fence in `long_description` would make the theme render both the raw Markdown + * code block and the runnable snippet. + * + * @param string $text Raw DocBlock long description. + * + * @return string + */ +function strip_docblock_code_snippet_fences( $text ) { + $text = preg_replace( "/\r\n?/", "\n", $text ); + $lines = explode( "\n", $text ); + $remove_lines = array(); + + foreach ( get_docblock_code_fences( $text ) as $fence ) { + if ( ! is_docblock_code_snippet_fence( $fence ) ) { + continue; + } + + for ( $i = $fence['start']; $i <= $fence['end']; $i++ ) { + $remove_lines[ $i ] = true; + } + } + + foreach ( $lines as $line_number => $line ) { + if ( isset( $remove_lines[ $line_number ] ) ) { + unset( $lines[ $line_number ] ); + } + } + + return trim( implode( "\n", $lines ) ); +} + +/** + * Returns inherited setup Blueprints referenced by the snippets. + * + * @param array $snippets Exported code snippets. + * @param array $setup_blueprints Setup Blueprints available from parent DocBlocks. + * + * @return array + */ +function get_referenced_setup_blueprints( $snippets, $setup_blueprints ) { + $referenced_setup_blueprints = array(); + + foreach ( $snippets as $snippet ) { + if ( ! is_string( $snippet['blueprint'] ?? null ) ) { + continue; + } + + if ( array_key_exists( $snippet['blueprint'], $setup_blueprints ) ) { + $referenced_setup_blueprints[ $snippet['blueprint'] ] = $setup_blueprints[ $snippet['blueprint'] ]; + } + } + + return $referenced_setup_blueprints; +} + +/** + * Checks whether a parsed DocBlock fence is represented by code snippet JSON. + * + * @param array $fence + * + * @return bool + */ +function is_docblock_code_snippet_fence( $fence ) { + return 'php' === $fence['language'] + || is_docblock_expected_output_fence( $fence ) + || is_docblock_blueprint_fence( $fence ) + || null !== get_docblock_setup_blueprint_name( $fence ); +} + /** * Checks whether a parsed DocBlock fence contains snippet expected output. * @@ -664,6 +764,24 @@ function export_uses( array $uses ) { return $out; } +/** + * Format the given long description with Markdown blocks. + * + * @param string $description Description. + * @return string Description as Markdown if the Parsedown class exists, otherwise return + * the given description text. + */ +function format_long_description( $description ) { + if ( class_exists( 'Parsedown' ) ) { + $parsedown = \Parsedown::instance(); + $description = $parsedown->text( $description ); + } + + $description = fix_newlines( $description ); + + return $description; +} + /** * Format the given description with Markdown. * diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index c686383a..4fedc1c5 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -7,6 +7,18 @@ * fact, this one does. It spans more than two full lines, continuing on to the * third line. * + * ```setupblueprint file-greeting + * { + * "steps": [ + * { + * "step": "writeFile", + * "path": "/wordpress/wp-content/mu-plugins/file-greeting.php", + * "data": "assertFileHasDocs( - array( 'description' => 'This is the file-level docblock summary.' ) + array( + 'description' => 'This is the file-level docblock summary.', + 'setup_blueprints' => array( + 'file-greeting' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/wordpress/wp-content/mu-plugins/file-greeting.php', + 'data' => " '

Use this example:

', ) ); } @@ -198,6 +212,39 @@ public function test_method_reused_setup_blueprint() { ); } + /** + * Test that methods can reference setup Blueprints from the file DocBlock. + */ + public function test_method_file_setup_blueprint() { + + $this->assertMethodHasDocs( + 'Test_Class' + , 'test_method_with_file_setup_blueprint' + , array( + 'setup_blueprints' => array( + 'file-greeting' => array( + 'steps' => array( + array( + 'step' => 'writeFile', + 'path' => '/wordpress/wp-content/mu-plugins/file-greeting.php', + 'data' => " array( + array( + 'type' => 'php-code-snippet', + 'code' => " 'Hello from the file setup', + 'blueprint' => 'file-greeting', + ), + ), + 'long_description' => '', + ) + ); + } + /** * Test tricky Markdown-like code fence parsing rules. */ From 654fbb9226f2c7b7c246e4fa7e595f9969d3af26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 13:10:16 +0200 Subject: [PATCH 11/12] Use Composer security-blocking flag in CI --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index bfdb84c1..bac77065 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,7 +28,7 @@ jobs: run: | rm composer.lock npm run start - npm run composer -- install --no-audit + npm run composer -- install --no-security-blocking - name: Test run: npm run test From e780f6f08c03282fd9c6e30eedd40521548d2e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 16:31:05 +0200 Subject: [PATCH 12/12] Omit expected output when DocBlocks do not provide it --- lib/runner.php | 5 ++--- tests/phpunit/tests/export/docblocks.php | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index e81071c9..db20f8ba 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -479,9 +479,8 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null ) { } $snippet = array( - 'type' => 'php-code-snippet', - 'code' => $fences[ $i ]['code'], - 'expected_output' => '', + 'type' => 'php-code-snippet', + 'code' => $fences[ $i ]['code'], ); $has_expected_output = false; diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index aeb7b4ef..60190246 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -415,7 +415,7 @@ public function test_code_snippet_fence_parser_edge_cases() { } /** - * Test that PHP snippets export the renderer fields even without metadata. + * Test that PHP snippets can omit optional metadata. */ public function test_code_snippet_without_metadata() { @@ -424,7 +424,6 @@ public function test_code_snippet_without_metadata() { array( 'type' => 'php-code-snippet', 'code' => " '', ), ), \WP_Parser\export_docblock_code_snippets( @@ -497,7 +496,6 @@ public function test_code_snippet_string_blueprint() { array( 'type' => 'php-code-snippet', 'code' => " '', 'blueprint' => 'not-json', ), ), @@ -533,7 +531,6 @@ public function test_code_snippet_metadata_boundaries() { array( 'type' => 'php-code-snippet', 'code' => " '', 'blueprint' => array( 'steps' => array( array( @@ -664,7 +661,6 @@ public function test_code_snippet_named_setup_blueprints() { array( 'type' => 'php-code-snippet', 'code' => " '', 'blueprint' => 'shared', ), ),