Skip to content

Printer drops the * prefix on continuation lines of a changed multi-line PhpDocTextNode #317

Description

@janedbal

Printer::printFormatPreserving() and Printer::print() both emit a multi-line PhpDocTextNode verbatim. PhpDocParser::parseText() joins continuation lines with a bare \n and strips the * prefix, so any text node with more than one line prints as invalid docblock formatting as soon as it is not reused from the original tokens.

Reproduction

phpdoc-parser 2.3.5, PHP 8.5.

<?php declare(strict_types = 1);

use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeTraverser;
use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;
use PHPStan\PhpDocParser\Printer\Printer;

require __DIR__ . '/vendor/autoload.php';

$config = new ParserConfig(['lines' => true, 'indexes' => true]);
$lexer = new Lexer($config);
$constExprParser = new ConstExprParser($config);
$parser = new PhpDocParser($config, new TypeParser($config, $constExprParser), $constExprParser);

$doc = <<<'DOC'
/**
     * First line Foo
     * second line Foo
     *
     * @param int $a
     */
DOC;

$tokens = $lexer->tokenize($doc);
$ast = $parser->parse(new TokenIterator($tokens));

$visitor = new class extends AbstractNodeVisitor {
    public function enterNode(Node $node): ?Node
    {
        if ($node instanceof PhpDocTextNode) {
            $node->text = str_replace('Foo', 'Bar', $node->text);
        }
        return null;
    }
};

[$newAst] = (new NodeTraverser([new CloningVisitor(), $visitor]))->traverse([$ast]);

$printer = new Printer();
echo $printer->printFormatPreserving($newAst, $ast, new TokenIterator($tokens)), "\n\n";
echo $printer->print($newAst), "\n";

Actual output

/**
     * First line Bar
second line Bar
     *
     * @param int $a
     */

/**
 * First line Bar
second line Bar
 *
 * @param int $a
 */

Expected output

/**
     * First line Bar
     * second line Bar
     *
     * @param int $a
     */

/**
 * First line Bar
 * second line Bar
 *
 * @param int $a
 */

Cause

  • PhpDocParser::parseText() appends $tokens->getDetectedNewline() ?? "\n" between continuation lines and drops the * prefix, so PhpDocTextNode::$text is "First line Foo\nsecond line Foo".
  • Printer::print() returns $node->text for a PhpDocTextNode as is. printNodeFormatPreserving() falls back to print() when a non-node sub-node such as $text changed, so the format-preserving path has the same result.
  • printArrayFormatPreserving() already computes $beforeAsteriskIndent and $afterAsteriskIndent via isMultiline() for inserted nodes, but does not apply them to newlines inside a printed child.

The same applies to multi-line description strings in GenericTagValueNode, ParamTagValueNode, DeprecatedTagValueNode and the other tag value nodes, which also hold bare \n.

Suggested fix

Re-insert the line prefix at every newline inside a printed child. In print() the prefix is "\n * ". In the format-preserving path it is newline . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, which printArrayFormatPreserving() already has in scope. PR follows.

Claude

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions