src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php

Summary

Maintainability
F
1 mo
Test Coverage
<?php

declare(strict_types=1);

namespace Phan\AST\TolerantASTConverter;

use AssertionError;
use ast;
use ast\flags;
use Closure;
use Error;
use Exception;
use InvalidArgumentException;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Diagnostic;
use Microsoft\PhpParser\DiagnosticsProvider;
use Microsoft\PhpParser\FilePositionMap;
use Microsoft\PhpParser\MissingToken;
use Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression;
use Microsoft\PhpParser\Node\Expression\TernaryExpression;
use Microsoft\PhpParser\Node\SourceFileNode;
use Microsoft\PhpParser\Parser;
use Microsoft\PhpParser\Token;
use Microsoft\PhpParser\TokenKind;
use Phan\CLI;
use Phan\Library\Cache;
use RuntimeException;

use function array_merge;
use function class_exists;
use function count;
use function get_class;
use function implode;
use function is_array;
use function is_string;
use function sprintf;
use function substr;
use function var_export;

use const FILTER_FLAG_ALLOW_HEX;
use const FILTER_FLAG_ALLOW_OCTAL;
use const FILTER_VALIDATE_FLOAT;
use const FILTER_VALIDATE_INT;
use const PHP_VERSION_ID;

// If php-ast isn't loaded already, then load this file to generate equivalent
// class, constant, and function definitions.
Shim::load();

/**
 * Source: https://github.com/TysonAndre/tolerant-php-parser-to-php-ast
 *
 * Uses Microsoft/tolerant-php-parser to create an instance of ast\Node.
 * Useful if the php-ast extension isn't actually installed.
 *
 * @author Tyson Andre
 *
 * TODO: Don't need to pass in $start_line for many of these functions
 *
 * This is implemented as a collection of static methods for performance,
 * but functionality is provided through instance methods.
 * (The private methods may become instance methods if the performance impact is negligible
 * in PHP and HHVM)
 *
 * The instance methods set all of the options (static variables)
 * each time they are invoked,
 * so it's possible to have multiple callers use this without affecting each other.
 *
 * Compatibility: PHP 7.0-7.4
 *
 * ----------------------------------------------------------------------------
 *
 *
 * License for TolerantASTConverter.php:
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-2018 Tyson Andre
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
 *
 * NOTE: EchoExpression can get converted to multiple `ast\Node`s (e.g. for `echo 'first', 'second';`, which is why this has so many partial mismatches.
 * The current version of tolerant-php-parser prevents EchoExpression (and UnsetIntrinsicExpression) from being anything other than a top-level statement.
 *
 * @phan-file-suppress PhanPartialTypeMismatchReturn
 * @phan-file-suppress PhanPartialTypeMismatchArgument
 * @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
 *
 * TODO: Add a way to report notices that aren't syntax errors?
 * e.g. `The (real) cast is deprecated, use (float) instead`, ending a function call with a comma, etc.
 */
class TolerantASTConverter
{
    // The latest stable version of php-ast.
    // For something != 70, update the library's release.
    public const AST_VERSION = 70;

    // The versions that this supports
    public const SUPPORTED_AST_VERSIONS = [self::AST_VERSION];

    private const _IGNORED_STRING_TOKEN_KIND_SET = [
        TokenKind::OpenBraceDollarToken => true,
        TokenKind::OpenBraceToken => true,
        TokenKind::DollarOpenBraceToken => true,
        TokenKind::CloseBraceToken => true,
    ];

    // If this environment variable is set, this will throw.
    // (For debugging, may be removed in the future)
    public const ENV_AST_THROW_INVALID = 'AST_THROW_INVALID';

    public const INCOMPLETE_CLASS_CONST = '__INCOMPLETE_CLASS_CONST__';
    public const INCOMPLETE_PROPERTY = '__INCOMPLETE_PROPERTY__';
    public const INCOMPLETE_VARIABLE = '__INCOMPLETE_VARIABLE__';

    /**
     * @var int - A version in SUPPORTED_AST_VERSIONS
     */
    protected static $php_version_id_parsing = PHP_VERSION_ID;

    /**
     * @var int - Internal counter for declarations, to generate __declId in `ast\Node`s for declarations.
     */
    protected static $decl_id = 0;

    /** @var bool should placeholder nodes be added as child nodes instead of refusing to generate a Node for an invalid statement? */
    protected static $should_add_placeholders = false;

    /** @var string the contents of the file currently being parsed */
    protected static $file_contents = '';

    /** @var FilePositionMap maps byte offsets of the currently parsed file to line numbers */
    protected static $file_position_map;

    /** @var bool Sets equivalent static option in self::_start_parsing() */
    protected $instance_should_add_placeholders = false;

    /**
     * @var int can be used to tweak behavior for compatibility.
     * Set to a newer version to support comments on class constants, etc.
     */
    protected $instance_php_version_id_parsing = PHP_VERSION_ID;

    // No-op.
    public function __construct()
    {
    }

    /**
     * Controls whether this should add placeholders for nodes that couldn't be parsed
     * (enabled for code completion)
     */
    public function setShouldAddPlaceholders(bool $value): void
    {
        $this->instance_should_add_placeholders = $value;
    }

    /**
     * Records the PHP major+minor version id (70100, 70200, etc.)
     * that this polyfill should emulate the behavior of php-ast for.
     */
    public function setPHPVersionId(int $value): void
    {
        $this->instance_php_version_id_parsing = $value;
    }

    /**
     * Generates an ast\Node with this converter's current settings. (caching if $cache is non-null)
     *
     * @param Diagnostic[] &$errors @phan-output-reference
     * @param ?Cache<ParseResult> $cache
     * @throws InvalidArgumentException if the requested AST version is invalid.
     */
    public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $cache = null): \ast\Node
    {
        if (!\in_array($version, self::SUPPORTED_AST_VERSIONS, true)) {
            throw new \InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", \implode(', ', self::SUPPORTED_AST_VERSIONS), $version));
        }
        $errors = [];
        $cache_key = null;
        if ($cache) {
            $cache_key = $this->generateCacheKey($file_contents, $version);
            $result = \Phan\Library\StringUtil::isNonZeroLengthString($cache_key) ? $cache->getIfExists($cache_key) : null;
            if ($result) {
                $errors = $result->diagnostics;
                return $result->node;
            }
        }
        $result = $this->parseCodeAsPHPASTUncached($file_contents, $version, $errors);
        if ($cache && \Phan\Library\StringUtil::isNonZeroLengthString($cache_key)) {
            $cache->save($cache_key, new ParseResult($result, $errors));
        }
        return $result;
    }

    /**
     * Generates an ast\Node with this converter's current settings.
     *
     * @param Diagnostic[] &$errors @phan-output-reference
     * @throws InvalidArgumentException if the requested AST version is invalid.
     */
    public function parseCodeAsPHPASTUncached(string $file_contents, int $version, array &$errors = []): \ast\Node
    {
        $parser_node = static::phpParserParse($file_contents, $errors);
        try {
            return $this->phpParserToPhpast($parser_node, $version, $file_contents);
        } finally {
            // Remove object reference cycles manually to free memory - automatic cyclic garbage collection is disabled for performance in older php 7 versions.
            self::unlinkDescendantNodes($parser_node);
        }
    }

    /**
     * Unlink the nodes manually to free memory (or to exclude them from var_export())
     *
     * Automatic cyclic garbage collection is disabled for performance in older php 7 versions.
     */
    public static function unlinkDescendantNodes(SourceFileNode $root): void
    {
        // Avoid creating cyclic data structures.
        // Node->getRoot() requires a valid parent node path to a SourceFileNode because it needs getDocCommentText() to work.
        $placeholder_root = new SourceFileNode();
        $placeholder_root->fileContents = $root->fileContents;

        foreach ($root->getDescendantNodes() as $descendant) {
            $descendant->parent = $placeholder_root;
        }
        $root->parent = null;
    }

    /**
     * @param Diagnostic[] &$errors @phan-output-reference (TODO: param-out)
     */
    public static function phpParserParse(string $file_contents, array &$errors = []): PhpParser\Node\SourceFileNode
    {
        $parser = new Parser();  // TODO: In php 7.3, we might need to provide a version, due to small changes in lexing?
        $result = $parser->parseSourceFile($file_contents);
        $errors = DiagnosticsProvider::getDiagnostics($result);
        return $result;
    }

    /**
     * Visible for testing
     *
     * @param PhpParser\Node $parser_node
     * @param int $ast_version
     * @param string $file_contents
     * @throws InvalidArgumentException if the provided AST version isn't valid
     */
    public function phpParserToPhpast(PhpParser\Node $parser_node, int $ast_version, string $file_contents): \ast\Node
    {
        if (!\in_array($ast_version, self::SUPPORTED_AST_VERSIONS, true)) {
            throw new \InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", implode(', ', self::SUPPORTED_AST_VERSIONS), $ast_version));
        }
        $this->startParsing($file_contents);
        $stmts = static::phpParserNodeToAstNode($parser_node);
        // return static::normalizeNamespaces($stmts);
        return $stmts;
    }

    protected function startParsing(string $file_contents): void
    {
        self::$decl_id = 0;
        self::$should_add_placeholders = $this->instance_should_add_placeholders;
        self::$php_version_id_parsing = $this->instance_php_version_id_parsing;
        self::$file_position_map = new FilePositionMap($file_contents);
        // $file_contents required for looking up line numbers.
        // TODO: Other data structures?
        self::$file_contents = $file_contents;
    }

    /**
     * @param null|bool|int|string|PhpParser\Node|Token|(PhpParser\Node|Token)[] $n
     * @throws Exception if node is invalid
     */
    protected static function debugDumpNodeOrToken($n): string
    {
        if (\is_scalar($n)) {
            return var_export($n, true);
        }
        if (!\is_array($n)) {
            $n = [$n];
        }
        $result = [];
        foreach ($n as $e) {
            $dumper = new NodeDumper(self::$file_contents);
            $dumper->setIncludeTokenKind(true);
            $result[] = $dumper->dumpTreeAsString($e);
        }
        return implode("\n", $result);
    }

    /**
     * @param Token|PhpParser\Node[]|PhpParser\Node\StatementNode $parser_nodes
     *        This is represented as a single node for `if` with a colon (macro style)
     * @param ?int $lineno
     * @param bool $return_null_on_empty (return null if non-array (E.g. semicolon is seen))
     * @return ?ast\Node
     * Throws RuntimeException|Exception if the statement list is invalid
     * @suppress PhanThrowTypeAbsentForCall|PhanThrowTypeMismatchForCall
     */
    private static function phpParserStmtlistToAstNode($parser_nodes, ?int $lineno, bool $return_null_on_empty = false): ?\ast\Node
    {
        if ($parser_nodes instanceof PhpParser\Node\Statement\CompoundStatementNode) {
            $parser_nodes = $parser_nodes->statements;
        } elseif ($parser_nodes instanceof PhpParser\Node\StatementNode) {
            if ($parser_nodes instanceof PhpParser\Node\Statement\EmptyStatement) {
                $parser_nodes = [];
            } else {
                $parser_nodes = [$parser_nodes];
            }
        } elseif ($parser_nodes instanceof Token) {
            if ($parser_nodes->kind === TokenKind::SemicolonToken) {
                if ($return_null_on_empty) {
                    return null;
                }
                return new ast\Node(
                    ast\AST_STMT_LIST,
                    0,
                    [],
                    $lineno ?? 0
                );
            }
        }

        if (!\is_array($parser_nodes)) {
            throw new RuntimeException("Unexpected type for statements: " . static::debugDumpNodeOrToken($parser_nodes));
        }
        $children = [];
        foreach ($parser_nodes as $parser_node) {
            try {
                $child_node = static::phpParserNodeToAstNode($parser_node);
            } catch (InvalidNodeException $_) {
                continue;
            }
            if (\is_array($child_node)) {
                // EchoExpression returns multiple children.
                foreach ($child_node as $child_node_part) {
                    $children[] = $child_node_part;
                }
            } elseif (!\is_null($child_node)) {
                $children[] = $child_node;
            }
        }
        if (!\is_int($lineno)) {
            foreach ($parser_nodes as $parser_node) {
                $child_node_line = static::getEndLine($parser_node);
                if ($child_node_line > 0) {
                    $lineno = $child_node_line;
                    break;
                }
            }
        }
        return new ast\Node(ast\AST_STMT_LIST, 0, $children, $lineno ?? 0);
    }

    private static function phpParserExprListToExprList(PhpParser\Node\DelimitedList\ExpressionList $expressions_list, int $lineno): ast\Node
    {
        $children = [];
        $expressions_children = $expressions_list->children;
        foreach ($expressions_children as $expr) {
            if ($expr instanceof Token && $expr->kind === TokenKind::CommaToken) {
                continue;
            }
            $child_node = static::phpParserNodeToAstNode($expr);
            if (\is_array($child_node)) {
                // EchoExpression returns multiple children in php-ast
                foreach ($child_node as $child_node_part) {
                    $children[] = $child_node_part;
                }
            } elseif (!\is_null($child_node)) {
                $children[] = $child_node;
            }
        }
        foreach ($expressions_children as $parser_node) {
            $child_node_line = static::getEndLine($parser_node);
            if ($child_node_line > 0) {
                $lineno = $child_node_line;
                break;
            }
        }
        return new ast\Node(
            ast\AST_EXPR_LIST,
            0,
            $children,
            $lineno
        );
    }

    /**
     * @param PhpParser\Node|Token $n - The node from PHP-Parser
     * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
     *                                                          This does not convert names to ast\AST_CONST.
     * @throws InvalidArgumentException if Phan doesn't know what $n is
     */
    protected static function phpParserNonValueNodeToAstNode($n)
    {
        static $callback_map;
        static $fallback_closure;
        if (\is_null($callback_map)) {
            $callback_map = static::initHandleMap();
            /**
             * @param PhpParser\Node|Token $n
             * @return ast\Node - Not a real node, but a node indicating the TODO
             * @throws InvalidArgumentException for invalid node classes
             * @throws Error if the environment variable AST_THROW_INVALID is set (for debugging)
             */
            $fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
                if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
                    // @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw
                    throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n));
                }
                return static::astStub($n);
            };
        }
        $callback = $callback_map[\get_class($n)] ?? $fallback_closure;
        // @phan-suppress-next-line PhanThrowTypeMismatch
        return $callback($n, self::getStartLine($n));
    }

    /**
     * @param PhpParser\Node|Token $n - The node from PHP-Parser
     * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
     *                                                          Generates a valid placeholder for invalid nodes if $should_add_placeholders is true.
     * @throws InvalidNodeException when self::$should_add_placeholders is false, like many of these methods.
     */
    protected static function phpParserNodeToAstNodeOrPlaceholderExpr($n)
    {
        if (!self::$should_add_placeholders) {
            return static::phpParserNodeToAstNode($n);
        }
        try {
            return static::phpParserNodeToAstNode($n);
        } catch (InvalidNodeException $_) {
            return static::newPlaceholderExpression($n);
        }
    }

    /**
     * @param PhpParser\Node|Token $n - The node from PHP-Parser
     * @return ast\Node|ast\Node[]|string|int|float|null - whatever ast\parse_code would return as the equivalent.
     */
    protected static function phpParserNodeToAstNode($n)
    {
        static $callback_map;
        static $fallback_closure;
        if (\is_null($callback_map)) {
            $callback_map = static::initHandleMap();
            /**
             * @param PhpParser\Node|Token $n
             * @return ast\Node - Not a real node, but a node indicating the TODO
             * @throws InvalidArgumentException|Exception for invalid node classes
             * @throws Error if the environment variable AST_THROW_INVALID is set to debug.
             */
            $fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
                if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
                    throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n));
                }

                return static::astStub($n);
            };
        }
        $callback = $callback_map[\get_class($n)] ?? $fallback_closure;
        // @phan-suppress-next-line PhanThrowTypeAbsent
        $result = $callback($n, self::$file_position_map->getStartLine($n));
        if (($result instanceof ast\Node) && $result->kind === ast\AST_NAME) {
            return new ast\Node(ast\AST_CONST, 0, ['name' => $result], $result->lineno);
        }
        return $result;
    }

    /**
     * @param PhpParser\Node|Token $n
     * @throws InvalidNodeException if this was called on an unexpected type
     */
    final protected static function getStartLine($n): int
    {
        if (\is_object($n)) {
            return self::$file_position_map->getStartLine($n);
        }
        throw new InvalidNodeException();
    }

    /**
     * @param ?PhpParser\Node|?Token $n
     * @throws InvalidNodeException if this was called on an unexpected type
     */
    final protected static function getEndLine($n): int
    {
        if (!\is_object($n)) {
            if (\is_null($n)) {
                return 0;
            }
            throw new InvalidNodeException();
        }
        return self::$file_position_map->getEndLine($n);
    }

    /**
     * This returns an array of values mapping class names to the closures which converts them to a scalar or ast\Node
     *
     * Why not a switch? Switches are slow until php 7.2, and there are dozens of class names to handle.
     *
     * - In php <= 7.1, the interpreter would loop through all possible cases, and compare against the value one by one.
     * - There are a lot of local variables to look at.
     *
     * @return array<string,Closure(object,int):(\ast\Node|int|string|float|null)>
     */
    protected static function initHandleMap(): array
    {
        $closures = [
            /** @return ?ast\Node */
            'Microsoft\PhpParser\Node\SourceFileNode' => static function (PhpParser\Node\SourceFileNode $n, int $start_line): ?\ast\Node {
                return static::phpParserStmtlistToAstNode($n->statementList, $start_line, false);
            },
            /** @return mixed */
            'Microsoft\PhpParser\Node\Expression\ArgumentExpression' => static function (PhpParser\Node\Expression\ArgumentExpression $n, int $start_line) {
                $result = static::phpParserNodeToAstNode($n->expression);
                if ($n->dotDotDotToken !== null) {
                    return new ast\Node(ast\AST_UNPACK, 0, ['expr' => $result], $start_line);
                }
                return $result;
            },
            /**
             * @return ast\Node|string|int|float
             * @throws InvalidNodeException
             */
            'Microsoft\PhpParser\Node\Expression\SubscriptExpression' => static function (PhpParser\Node\Expression\SubscriptExpression $n, int $start_line) {
                $expr = static::phpParserNodeToAstNode($n->postfixExpression);
                try {
                    return new ast\Node(
                        ast\AST_DIM,
                        ($n->openBracketOrBrace->kind ?? null) === TokenKind::OpenBraceToken ? ast\flags\DIM_ALTERNATIVE_SYNTAX : 0,
                        [
                            'expr' => $expr,
                            'dim' => $n->accessExpression !== null ? static::phpParserNodeToAstNode($n->accessExpression) : null,
                        ],
                        $start_line
                    );
                } catch (InvalidNodeException $_) {
                    return $expr;
                }
            },
            /** @return ?(ast\Node|float|int|string) */
            'Microsoft\PhpParser\Node\Expression\AssignmentExpression' => static function (PhpParser\Node\Expression\AssignmentExpression $n, int $start_line) {
                try {
                    $var_node = static::phpParserNodeToAstNode($n->leftOperand);
                } catch (InvalidNodeException $_) {
                    if (self::$should_add_placeholders) {
                        $var_node = new ast\Node(ast\AST_VAR, 0, ['name' => self::INCOMPLETE_VARIABLE], $start_line);
                    } else {
                        // convert `;= $b;` to `;$b;`
                        return static::phpParserNodeToAstNode($n->rightOperand);
                    }
                }
                $expr_node = static::phpParserNodeToAstNodeOrPlaceholderExpr($n->rightOperand);
                // FIXME switch on $n->kind
                return static::astNodeAssign(
                    $var_node,
                    $expr_node,
                    $start_line,
                    $n->byRef !== null
                );
            },
            /**
             * @return ast\Node|string|float|int (can return a non-Node if the left or right-hand side could not be parsed
             */
            'Microsoft\PhpParser\Node\Expression\BinaryExpression' => static function (PhpParser\Node\Expression\BinaryExpression $n, int $start_line) {
                static $lookup = [
                    TokenKind::AmpersandAmpersandToken              => flags\BINARY_BOOL_AND,
                    TokenKind::AmpersandToken                       => flags\BINARY_BITWISE_AND,
                    TokenKind::AndKeyword                           => flags\BINARY_BOOL_AND,
                    TokenKind::AsteriskAsteriskToken                => flags\BINARY_POW,
                    TokenKind::AsteriskToken                        => flags\BINARY_MUL,
                    TokenKind::BarBarToken                          => flags\BINARY_BOOL_OR,
                    TokenKind::BarToken                             => flags\BINARY_BITWISE_OR,
                    TokenKind::CaretToken                           => flags\BINARY_BITWISE_XOR,
                    TokenKind::DotToken                             => flags\BINARY_CONCAT,
                    TokenKind::EqualsEqualsEqualsToken              => flags\BINARY_IS_IDENTICAL,
                    TokenKind::EqualsEqualsToken                    => flags\BINARY_IS_EQUAL,
                    TokenKind::ExclamationEqualsEqualsToken         => flags\BINARY_IS_NOT_IDENTICAL,
                    TokenKind::ExclamationEqualsToken               => flags\BINARY_IS_NOT_EQUAL,
                    TokenKind::GreaterThanEqualsToken               => flags\BINARY_IS_GREATER_OR_EQUAL,
                    TokenKind::GreaterThanGreaterThanToken          => flags\BINARY_SHIFT_RIGHT,
                    TokenKind::GreaterThanToken                     => flags\BINARY_IS_GREATER,
                    TokenKind::LessThanEqualsGreaterThanToken       => flags\BINARY_SPACESHIP,
                    TokenKind::LessThanEqualsToken                  => flags\BINARY_IS_SMALLER_OR_EQUAL,
                    TokenKind::LessThanLessThanToken                => flags\BINARY_SHIFT_LEFT,
                    TokenKind::LessThanToken                        => flags\BINARY_IS_SMALLER,
                    TokenKind::MinusToken                           => flags\BINARY_SUB,
                    TokenKind::OrKeyword                            => flags\BINARY_BOOL_OR,
                    TokenKind::PercentToken                         => flags\BINARY_MOD,
                    TokenKind::PlusToken                            => flags\BINARY_ADD,
                    TokenKind::QuestionQuestionToken                => flags\BINARY_COALESCE,
                    TokenKind::SlashToken                           => flags\BINARY_DIV,
                    TokenKind::XorKeyword                           => flags\BINARY_BOOL_XOR,
                ];
                static $assign_lookup = [
                    TokenKind::AmpersandEqualsToken                 => flags\BINARY_BITWISE_AND,
                    TokenKind::AsteriskAsteriskEqualsToken          => flags\BINARY_POW,
                    TokenKind::AsteriskEqualsToken                  => flags\BINARY_MUL,
                    TokenKind::BarEqualsToken                       => flags\BINARY_BITWISE_OR,
                    TokenKind::CaretEqualsToken                     => flags\BINARY_BITWISE_XOR,
                    TokenKind::DotEqualsToken                       => flags\BINARY_CONCAT,
                    TokenKind::MinusEqualsToken                     => flags\BINARY_SUB,
                    TokenKind::PercentEqualsToken                   => flags\BINARY_MOD,
                    TokenKind::PlusEqualsToken                      => flags\BINARY_ADD,
                    TokenKind::SlashEqualsToken                     => flags\BINARY_DIV,
                    TokenKind::GreaterThanGreaterThanEqualsToken    => flags\BINARY_SHIFT_RIGHT,
                    TokenKind::LessThanLessThanEqualsToken          => flags\BINARY_SHIFT_LEFT,
                    TokenKind::QuestionQuestionEqualsToken          => flags\BINARY_COALESCE,
                ];
                $kind = $n->operator->kind;
                if ($kind === TokenKind::InstanceOfKeyword) {
                    return new ast\Node(ast\AST_INSTANCEOF, 0, [
                        'expr'  => static::phpParserNodeToAstNode($n->leftOperand),
                        'class' => static::phpParserNonValueNodeToAstNode($n->rightOperand),
                    ], $start_line);
                }
                $ast_kind = $lookup[$kind] ?? null;
                if ($ast_kind === null) {
                    $ast_kind = $assign_lookup[$kind] ?? null;
                    if ($ast_kind === null) {
                        throw new AssertionError("missing $kind (" . Token::getTokenKindNameFromValue($kind) . ")");
                    }
                    return static::astNodeAssignop($ast_kind, $n, $start_line);
                }
                return static::astNodeBinaryop($ast_kind, $n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\UnaryOpExpression' => static function (PhpParser\Node\Expression\UnaryOpExpression $n, int $start_line): ast\Node {
                static $lookup = [
                    TokenKind::TildeToken                   => flags\UNARY_BITWISE_NOT,
                    TokenKind::MinusToken                   => flags\UNARY_MINUS,
                    TokenKind::PlusToken                    => flags\UNARY_PLUS,
                    TokenKind::ExclamationToken             => flags\UNARY_BOOL_NOT,
                ];
                $kind = $n->operator->kind;
                $ast_kind = $lookup[$kind] ?? null;
                if ($ast_kind === null) {
                    throw new AssertionError("missing $kind(" . Token::getTokenKindNameFromValue($kind) . ")");
                }
                return new ast\Node(
                    ast\AST_UNARY_OP,
                    $ast_kind,
                    ['expr' => static::phpParserNodeToAstNode($n->operand)],
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\Expression\CastExpression' => static function (PhpParser\Node\Expression\CastExpression $n, int $start_line): ast\Node {
                static $lookup = [
                    // From Parser->parseCastExpression()
                    TokenKind::ArrayCastToken   => flags\TYPE_ARRAY,
                    TokenKind::BoolCastToken    => flags\TYPE_BOOL,
                    TokenKind::DoubleCastToken  => flags\TYPE_DOUBLE,
                    TokenKind::IntCastToken     => flags\TYPE_LONG,
                    TokenKind::ObjectCastToken  => flags\TYPE_OBJECT,
                    TokenKind::StringCastToken  => flags\TYPE_STRING,
                    TokenKind::UnsetCastToken   => flags\TYPE_NULL,

                    // From Parser->parseCastExpressionGranular()
                    // This is a syntax error, but try to match what the intent was
                    TokenKind::ArrayKeyword         => flags\TYPE_ARRAY,
                    TokenKind::BinaryReservedWord   => flags\TYPE_STRING,
                    TokenKind::BoolReservedWord     => flags\TYPE_BOOL,
                    TokenKind::BooleanReservedWord  => flags\TYPE_BOOL,
                    TokenKind::DoubleReservedWord   => flags\TYPE_DOUBLE,
                    TokenKind::IntReservedWord      => flags\TYPE_LONG,
                    TokenKind::IntegerReservedWord  => flags\TYPE_LONG,
                    TokenKind::FloatReservedWord    => flags\TYPE_DOUBLE,
                    TokenKind::ObjectReservedWord   => flags\TYPE_OBJECT,
                    TokenKind::RealReservedWord     => flags\TYPE_DOUBLE,
                    TokenKind::StringReservedWord   => flags\TYPE_STRING,
                    TokenKind::UnsetKeyword         => flags\TYPE_NULL,
                    TokenKind::StaticKeyword        => flags\TYPE_STATIC,
                ];
                $kind = $n->castType->kind;
                $ast_kind = $lookup[$kind] ?? null;
                if ($ast_kind === null) {
                    throw new AssertionError("missing $kind");
                }
                return new ast\Node(
                    ast\AST_CAST,
                    $ast_kind,
                    ['expr' => static::phpParserNodeToAstNode($n->operand)],
                    static::getEndLine($n) ?: $start_line
                );
            },
            'Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression' => static function (
                PhpParser\Node\Expression\AnonymousFunctionCreationExpression $n,
                int $start_line
            ): ast\Node {
                $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
                if (($ast_return_type->children['name'] ?? null) === '') {
                    $ast_return_type = null;
                }
                if ($n->questionToken !== null && $ast_return_type !== null) {
                    $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line);
                }
                $use_variable_name_list = $n->anonymousFunctionUseClause->useVariableNameList ?? null;
                if (!$use_variable_name_list instanceof PhpParser\Node\DelimitedList\UseVariableNameList) {
                    $use_variable_name_list = null;
                }
                return static::astDeclClosure(
                    $n->byRefToken !== null,
                    $n->staticModifier !== null,
                    static::phpParserParamsToAstParams($n->parameters, $start_line),
                    static::phpParserClosureUsesToAstClosureUses($use_variable_name_list, $start_line),
                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable, PhanPossiblyUndeclaredProperty return_null_on_empty is false.
                    static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon->statements, self::getStartLine($n->compoundStatementOrSemicolon), false),
                    $ast_return_type,
                    $start_line,
                    static::getEndLine($n),
                    static::resolveDocCommentForClosure($n)
                );
            },
            'Microsoft\PhpParser\Node\Expression\ArrowFunctionCreationExpression' => static function (
                PhpParser\Node\Expression\ArrowFunctionCreationExpression $n,
                int $start_line
            ): ast\Node {
                $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
                if (($ast_return_type->children['name'] ?? null) === '') {
                    $ast_return_type = null;
                }
                if ($n->questionToken !== null && $ast_return_type !== null) {
                    $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line);
                }
                $return_line = self::getStartLine($n->resultExpression);
                return static::newASTDecl(
                    ast\AST_ARROW_FUNC,
                    ($n->byRefToken !== null ? flags\FUNC_RETURNS_REF : 0) | ($n->staticModifier !== null ? flags\MODIFIER_STATIC : null),
                    [
                        'params' => static::phpParserParamsToAstParams($n->parameters, $start_line),
                        'stmts' => new ast\Node(
                            ast\AST_RETURN,
                            0,
                            ['expr' => static::phpParserNodeToAstNode($n->resultExpression)],
                            $return_line
                        ),
                        'returnType' => $ast_return_type,
                    ],
                    $start_line,
                    static::resolveDocCommentForClosure($n),
                    '{closure}',
                    static::getEndLine($n),
                    self::nextDeclId()
                );
            },
            /**
             * @throws InvalidNodeException if the resulting AST would not be analyzable by Phan
             */
            'Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression' => static function (PhpParser\Node\Expression\ScopedPropertyAccessExpression $n, int $start_line): ?\ast\Node {
                $member_name = $n->memberName;
                if ($member_name instanceof PhpParser\Node\Expression\Variable) {
                    try {
                        $prop_node = static::phpParserNodeToAstNode($member_name->name);
                    } catch (InvalidNodeException $e) {
                        if (self::$should_add_placeholders) {
                            $prop_node = '';
                        } else {
                            throw $e;
                        }
                    }
                    return new ast\Node(
                        ast\AST_STATIC_PROP,
                        0,
                        [
                            'class' => static::phpParserNonValueNodeToAstNode($n->scopeResolutionQualifier),
                            'prop' => $prop_node,
                        ],
                        $start_line
                    );
                } else {
                    if ($member_name instanceof Token) {
                        if (\get_class($member_name) !== Token::class) {
                            if (self::$should_add_placeholders) {
                                $member_name = self::INCOMPLETE_CLASS_CONST;
                            } else {
                                throw new InvalidNodeException();
                            }
                        } else {
                            $member_name = static::tokenToString($member_name);
                        }
                    } else {
                        // E.g. Node\Expression\BracedExpression
                        throw new InvalidNodeException();
                    }
                    return static::phpParserClassConstFetchToAstClassConstFetch($n->scopeResolutionQualifier, $member_name, $start_line);
                }
            },
            'Microsoft\PhpParser\Node\Expression\CloneExpression' => static function (PhpParser\Node\Expression\CloneExpression $n, int $start_line): ast\Node {
                return new ast\Node(ast\AST_CLONE, 0, ['expr' => static::phpParserNodeToAstNode($n->expression)], $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\ErrorControlExpression' => static function (PhpParser\Node\Expression\ErrorControlExpression $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_UNARY_OP,
                    flags\UNARY_SILENCE,
                    ['expr' => static::phpParserNodeToAstNode($n->operand)],
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\Expression\EmptyIntrinsicExpression' => static function (PhpParser\Node\Expression\EmptyIntrinsicExpression $n, int $start_line): ast\Node {
                return new ast\Node(ast\AST_EMPTY, 0, ['expr' => static::phpParserNodeToAstNode($n->expression)], $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\EvalIntrinsicExpression' => static function (PhpParser\Node\Expression\EvalIntrinsicExpression $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_INCLUDE_OR_EVAL,
                    flags\EXEC_EVAL,
                    ['expr' => static::phpParserNodeToAstNode($n->expression)],
                    $start_line
                );
            },
            /** @return string|ast\Node */
            'Microsoft\PhpParser\Token' => static function (PhpParser\Token $token, int $start_line) {
                $kind = $token->kind;
                $str = static::tokenToString($token);
                if ($kind === TokenKind::StaticKeyword) {
                    return new ast\Node(ast\AST_NAME, flags\NAME_NOT_FQ, ['name' => $str], $start_line);
                }
                return $str;
            },
            /**
             * @throws InvalidNodeException
             */
            'Microsoft\PhpParser\MissingToken' => static function (PhpParser\MissingToken $unused_node, int $_): void {
                throw new InvalidNodeException();
            },
            /**
             * @throws InvalidNodeException
             */
            'Microsoft\PhpParser\SkippedToken' => static function (PhpParser\SkippedToken $unused_node, int $_): void {
                throw new InvalidNodeException();
            },
            'Microsoft\PhpParser\Node\Expression\ExitIntrinsicExpression' => static function (PhpParser\Node\Expression\ExitIntrinsicExpression $n, int $start_line): ast\Node {
                $expression = $n->expression;
                $expr_node = $expression !== null ? static::phpParserNodeToAstNode($expression) : null;
                return new ast\Node(ast\AST_EXIT, 0, ['expr' => $expr_node], $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\CallExpression' => static function (PhpParser\Node\Expression\CallExpression $n, int $start_line): ast\Node {
                $callable_expression = $n->callableExpression;
                $arg_list = static::phpParserArgListToAstArgList($n->argumentExpressionList, $start_line);
                if ($callable_expression instanceof PhpParser\Node\Expression\MemberAccessExpression) {  // $a->f()
                    return static::astNodeMethodCall(
                        $callable_expression->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_METHOD_CALL : ast\AST_METHOD_CALL,
                        static::phpParserNonValueNodeToAstNode($callable_expression->dereferencableExpression),
                        static::phpParserNodeToAstNode($callable_expression->memberName),
                        $arg_list,
                        $start_line
                    );
                } elseif ($callable_expression instanceof PhpParser\Node\Expression\ScopedPropertyAccessExpression) {  // a::f()
                    return static::astNodeStaticCall(
                        static::phpParserNonValueNodeToAstNode($callable_expression->scopeResolutionQualifier),
                        static::phpParserNodeToAstNode($callable_expression->memberName),
                        $arg_list,
                        $start_line
                    );
                } else {  // f()
                    return static::astNodeCall(
                        static::phpParserNonValueNodeToAstNode($callable_expression),
                        $arg_list,
                        $start_line
                    );
                }
            },
            'Microsoft\PhpParser\Node\Expression\ScriptInclusionExpression' => static function (PhpParser\Node\Expression\ScriptInclusionExpression $n, int $start_line): ast\Node {
                // @phan-suppress-next-line PhanThrowTypeAbsentForCall should not happen
                $flags = static::phpParserIncludeTokenToAstIncludeFlags($n->requireOrIncludeKeyword);
                return new ast\Node(
                    ast\AST_INCLUDE_OR_EVAL,
                    $flags,
                    ['expr' => static::phpParserNodeToAstNode($n->expression)],
                    $start_line
                );
            },
            /**
             * @return ?ast\Node
             */
            'Microsoft\PhpParser\Node\Expression\IssetIntrinsicExpression' => static function (PhpParser\Node\Expression\IssetIntrinsicExpression $n, int $start_line): ?\ast\Node {
                $ast_issets = [];
                foreach ($n->expressions->children ?? [] as $var) {
                    if ($var instanceof Token) {
                        if ($var->kind === TokenKind::CommaToken) {
                            continue;
                        } elseif ($var->length === 0) {
                            continue;
                        }
                    }
                    $ast_issets[] = new ast\Node(ast\AST_ISSET, 0, [
                        'var' => static::phpParserNodeToAstNode($var),
                    ], $start_line);
                }
                $e = $ast_issets[0] ?? null;
                for ($i = 1; $i < \count($ast_issets); $i++) {
                    $right = $ast_issets[$i];
                    $e = new ast\Node(
                        ast\AST_BINARY_OP,
                        flags\BINARY_BOOL_AND,
                        [
                            'left' => $e,
                            'right' => $right,
                        ],
                        // $e should always be set
                        $e->lineno ?? 0
                    );
                }
                return $e;
            },
            'Microsoft\PhpParser\Node\Expression\ArrayCreationExpression' => static function (PhpParser\Node\Expression\ArrayCreationExpression $n, int $start_line): ast\Node {
                return static::phpParserArrayToAstArray($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\ListIntrinsicExpression' => static function (PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line): ast\Node {
                return static::phpParserListToAstList($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\ObjectCreationExpression' => static function (PhpParser\Node\Expression\ObjectCreationExpression $n, int $start_line): ast\Node {
                $end_line = static::getEndLine($n);
                $class_type_designator = $n->classTypeDesignator;
                if ($class_type_designator instanceof Token && $class_type_designator->kind === TokenKind::ClassKeyword) {
                    // Node of type AST_CLASS
                    $base_class = $n->classBaseClause->baseClass ?? null;
                    $class_node = static::astStmtClass(
                        flags\CLASS_ANONYMOUS,
                        null,
                        $base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null,
                        $n->classInterfaceClause,
                        static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], $start_line, false),
                        $start_line,
                        $end_line,
                        $n->getDocCommentText()
                    );
                } else {
                    $class_node = static::phpParserNonValueNodeToAstNode($class_type_designator);
                }
                return new ast\Node(ast\AST_NEW, 0, [
                    'class' => $class_node,
                    'args' => static::phpParserArgListToAstArgList($n->argumentExpressionList, $start_line),
                ], $start_line);
            },
            /** @return mixed */
            'Microsoft\PhpParser\Node\Expression\ParenthesizedExpression' => static function (PhpParser\Node\Expression\ParenthesizedExpression $n, int $_) {
                return static::phpParserNodeToAstNode($n->expression);
            },
            'Microsoft\PhpParser\Node\Expression\PrefixUpdateExpression' => static function (PhpParser\Node\Expression\PrefixUpdateExpression $n, int $start_line): ast\Node {
                $type = $n->incrementOrDecrementOperator->kind === TokenKind::PlusPlusToken ? ast\AST_PRE_INC : ast\AST_PRE_DEC;

                return new ast\Node($type, 0, ['var' => static::phpParserNodeToAstNode($n->operand)], $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\PostfixUpdateExpression' => static function (PhpParser\Node\Expression\PostfixUpdateExpression $n, int $start_line): ast\Node {
                $type = $n->incrementOrDecrementOperator->kind === TokenKind::PlusPlusToken ? ast\AST_POST_INC : ast\AST_POST_DEC;

                return new ast\Node($type, 0, ['var' => static::phpParserNodeToAstNode($n->operand)], $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\PrintIntrinsicExpression' => static function (PhpParser\Node\Expression\PrintIntrinsicExpression $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_PRINT,
                    0,
                    ['expr' => static::phpParserNodeToAstNode($n->expression)],
                    $start_line
                );
            },
            /** @return ?ast\Node */
            'Microsoft\PhpParser\Node\Expression\MemberAccessExpression' => static function (PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line): ?\ast\Node {
                return static::phpParserMemberAccessExpressionToAstProp($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\TernaryExpression' => static function (TernaryExpression $n, int $start_line): ast\Node {
                $n = self::normalizeTernaryExpression($n);
                $is_parenthesized = $n->parent instanceof PhpParser\Node\Expression\ParenthesizedExpression;
                $result = new ast\Node(
                    ast\AST_CONDITIONAL,
                    $is_parenthesized ? ast\flags\PARENTHESIZED_CONDITIONAL : 0,
                    [
                        'cond' => static::phpParserNodeToAstNode($n->condition),
                        'true' => $n->ifExpression !== null ? static::phpParserNodeToAstNode($n->ifExpression) : null,
                        'false' => static::phpParserNodeToAstNode($n->elseExpression),
                    ],
                    $start_line
                );
                if (PHP_VERSION_ID < 70400 && !$is_parenthesized) {
                    // This is a way to indicate that this AST is definitely unparenthesized in cases where the native parser would not provide this information.
                    // @phan-suppress-next-line PhanUndeclaredProperty
                    $result->is_not_parenthesized = true;
                }
                return $result;
            },
            /**
             * @throws InvalidNodeException if the variable would be unanalyzable
             * TODO: Consider ${''} as a placeholder instead?
             */
            'Microsoft\PhpParser\Node\Expression\Variable' => static function (PhpParser\Node\Expression\Variable $n, int $start_line): \ast\Node {
                $name_node = $n->name;
                // Note: there are 2 different ways to handle an Error. 1. Add a placeholder. 2. remove all of the statements in that tree.
                if ($name_node instanceof PhpParser\Node) {
                    $name_node = static::phpParserNodeToAstNode($name_node);
                } elseif ($name_node instanceof Token) {
                    if ($name_node instanceof PhpParser\MissingToken) {
                        if (self::$should_add_placeholders) {
                            $name_node = '__INCOMPLETE_VARIABLE__';
                        } else {
                            throw new InvalidNodeException();
                        }
                    } else {
                        if ($name_node->kind === TokenKind::VariableName) {
                            $name_node = static::variableTokenToString($name_node);
                        } else {
                            $name_node = static::tokenToString($name_node);
                        }
                    }
                }
                return new ast\Node(ast\AST_VAR, 0, ['name' => $name_node], $start_line);
            },
            /**
             * @return ast\Node|int|float|string
             */
            'Microsoft\PhpParser\Node\Expression\BracedExpression' => static function (PhpParser\Node\Expression\BracedExpression $n, int $_) {
                return static::phpParserNodeToAstNode($n->expression);
            },
            'Microsoft\PhpParser\Node\Expression\YieldExpression' => static function (PhpParser\Node\Expression\YieldExpression $n, int $start_line): ast\Node {
                $kind = $n->yieldOrYieldFromKeyword->kind === TokenKind::YieldFromKeyword ? ast\AST_YIELD_FROM : ast\AST_YIELD;

                $array_element = $n->arrayElement;
                $element_value = $array_element->elementValue ?? null;
                // Workaround for <= 0.0.5
                // TODO: Remove workaround?
                $ast_expr = ($element_value !== null && !($element_value instanceof MissingToken)) ? static::phpParserNodeToAstNode($array_element->elementValue) : null;
                if ($kind === \ast\AST_YIELD) {
                    $element_key = $array_element->elementKey ?? null;
                    $children = [
                        'value' => $ast_expr,
                        'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null,
                    ];
                } else {
                    $children = [
                        'expr' => $ast_expr,
                    ];
                }
                return new ast\Node(
                    $kind,
                    0,
                    $children,
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\ReservedWord' => static function (PhpParser\Node\ReservedWord $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_NAME,
                    flags\NAME_NOT_FQ,
                    ['name' => static::tokenToString($n->children)],
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\QualifiedName' => static function (PhpParser\Node\QualifiedName $n, int $start_line): ast\Node {
                $name_parts = $n->nameParts;
                if (\count($name_parts) === 1) {
                    $part = $name_parts[0];
                    '@phan-var Token $part';
                    $imploded_parts = static::tokenToString($part);
                    if ($part->kind === TokenKind::Name) {
                        if (\preg_match('@^__(LINE|FILE|DIR|FUNCTION|CLASS|TRAIT|METHOD|NAMESPACE)__$@iD', $imploded_parts) > 0) {
                            return new ast\Node(
                                ast\AST_MAGIC_CONST,
                                self::MAGIC_CONST_LOOKUP[\strtoupper($imploded_parts)],
                                [],
                                self::getStartLine($part)
                            );
                        }
                    }
                } else {
                    $imploded_parts = static::phpParserNameToString($n);
                }
                if ($n->globalSpecifier !== null) {
                    $ast_kind = flags\NAME_FQ;
                } elseif (($n->relativeSpecifier->namespaceKeyword ?? null) !== null) {
                    $ast_kind = flags\NAME_RELATIVE;
                } else {
                    $ast_kind = flags\NAME_NOT_FQ;
                }
                return new ast\Node(ast\AST_NAME, $ast_kind, ['name' => $imploded_parts], $start_line);
            },
            'Microsoft\PhpParser\Node\Parameter' => static function (PhpParser\Node\Parameter $n, int $start_line): ast\Node {
                $type_line = static::getEndLine($n->typeDeclaration) ?: $start_line;
                $default = $n->default;
                $default_node = $default !== null ? static::phpParserNodeToAstNode($default) : null;
                return static::astNodeParam(
                    $n->questionToken !== null,
                    $n->byRefToken !== null,
                    $n->dotDotDotToken !== null,
                    static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line),
                    static::variableTokenToString($n->variableName),
                    $default_node,
                    $start_line
                );
            },
            /** @return int|float */
            'Microsoft\PhpParser\Node\NumericLiteral' => static function (PhpParser\Node\NumericLiteral $n, int $_) {
                // Support php 7.4 numeric literal separators. Ignore `_`.
                $n = $n->children;
                $text = \str_replace('_', '', static::tokenToString($n));
                if (($n->kind ?? null) === TokenKind::IntegerLiteralToken) {
                    $as_int = \filter_var($text, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX);
                    if ($as_int !== false) {
                        return $as_int;
                    }
                    if (\preg_match('/^0[0-7]+$/D', $text)) {
                        // this is octal - FILTER_VALIDATE_FLOAT would treat it like decimal
                        return \intval($text, 8);
                    }
                }
                if ($text[0] === '0' && !\preg_match('/[.eE]/', $text)) {
                    $c = $text[1];
                    if ($c === 'b' || $c === 'B') {
                        return \bindec($text);
                    }
                    if ($c === 'x' || $c === 'X') {
                        return \hexdec($text);
                    }
                    return \octdec(substr($text, 0, \strcspn($text, '89')));
                }
                return (float)$text;
            },
            /**
             * @return ast\Node|string
             * @throws Exception if the tokens of the string literal are invalid, etc.
             */
            'Microsoft\PhpParser\Node\StringLiteral' => static function (PhpParser\Node\StringLiteral $n, int $start_line) {
                $children = $n->children;
                if ($children instanceof Token) {
                    $inner_node = static::parseQuotedString($n);
                } elseif (\count($children) === 0) {
                    $inner_node = '';
                } elseif (\count($children) === 1 && $children[0] instanceof Token) {
                    $inner_node = static::parseQuotedString($n);
                } else {
                    $inner_node = self::parseMultiPartString($n, $children);
                }
                if ($n->startQuote !== null && $n->startQuote->kind === TokenKind::BacktickToken) {
                    return new ast\Node(ast\AST_SHELL_EXEC, 0, ['expr' => $inner_node], isset($children[0]) ? self::getStartLine($children[0]) : $start_line);
                    // TODO: verify match
                }
                return $inner_node;
            },
            /** @return list<ast\Node|float|int|string> - Can return a node or a scalar, depending on the settings */
            'Microsoft\PhpParser\Node\Statement\CompoundStatementNode' => static function (PhpParser\Node\Statement\CompoundStatementNode $n, int $_) {
                $children = [];
                foreach ($n->statements as $parser_node) {
                    $child_node = static::phpParserNodeToAstNode($parser_node);
                    if (\is_array($child_node)) {
                        // EchoExpression returns multiple children.
                        foreach ($child_node as $child_node_part) {
                            $children[] = $child_node_part;
                        }
                    } elseif (!\is_null($child_node)) {
                        $children[] = $child_node;
                    }
                }
                return $children;
            },
            /**
             * @return int|string|ast\Node|null
             * null if incomplete
             * int|string for no-op scalar statements like `;2;`
             */
            'Microsoft\PhpParser\Node\Statement\ExpressionStatement' => static function (PhpParser\Node\Statement\ExpressionStatement $n, int $_) {
                $expression = $n->expression;
                // tolerant-php-parser uses parseExpression(..., $force=true), which can return an array.
                // It is the only thing that uses $force=true at the time of writing.
                if (!\is_object($expression)) {
                    return null;
                }
                return static::phpParserNodeToAstNode($n->expression);
            },
            'Microsoft\PhpParser\Node\Statement\BreakOrContinueStatement' => static function (PhpParser\Node\Statement\BreakOrContinueStatement $n, int $start_line): ast\Node {
                $kind = $n->breakOrContinueKeyword->kind === TokenKind::ContinueKeyword ? ast\AST_CONTINUE : ast\AST_BREAK;
                $breakout_level = $n->breakoutLevel;
                if ($breakout_level !== null) {
                    $breakout_level = static::phpParserNodeToAstNode($breakout_level);
                    if (!\is_int($breakout_level)) {
                        $breakout_level = null;
                    }
                }
                return new ast\Node($kind, 0, ['depth' => $breakout_level], $start_line);
            },
            'Microsoft\PhpParser\Node\CatchClause' => static function (PhpParser\Node\CatchClause $n, int $start_line): ast\Node {
                $qualified_name = $n->qualifiedName;
                $catch_inner_list = [];
                // Handle `catch()` syntax error
                if ($qualified_name instanceof PhpParser\Node\QualifiedName) {
                    $catch_inner_list[] = static::phpParserNonValueNodeToAstNode($qualified_name);
                }
                foreach ($n->otherQualifiedNameList as $other_qualified_name) {
                    if ($other_qualified_name instanceof PhpParser\Node\QualifiedName) {
                        $catch_inner_list[] = static::phpParserNonValueNodeToAstNode($other_qualified_name);
                    }
                }
                $catch_list_node = new ast\Node(ast\AST_NAME_LIST, 0, $catch_inner_list, $catch_inner_list[0]->lineno ?? $start_line);
                $variableName = $n->variableName;
                return static::astStmtCatch(
                    $catch_list_node,
                    $variableName !== null ? static::variableTokenToString($variableName) : null,
                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
                    static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false),
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\Statement\InterfaceDeclaration' => static function (PhpParser\Node\Statement\InterfaceDeclaration $n, int $start_line): ast\Node {
                $end_line = static::getEndLine($n) ?: $start_line;
                return static::astStmtClass(
                    flags\CLASS_INTERFACE,
                    static::tokenToString($n->name),
                    static::interfaceBaseClauseToNode($n->interfaceBaseClause),
                    null,
                    static::phpParserStmtlistToAstNode($n->interfaceMembers->interfaceMemberDeclarations ?? [], $start_line, false),
                    $start_line,
                    $end_line,
                    $n->getDocCommentText()
                );
            },
            'Microsoft\PhpParser\Node\Statement\ClassDeclaration' => static function (PhpParser\Node\Statement\ClassDeclaration $n, int $start_line): ast\Node {
                $end_line = static::getEndLine($n);
                $base_class = $n->classBaseClause->baseClass ?? null;
                return static::astStmtClass(
                    static::phpParserClassModifierToAstClassFlags($n->abstractOrFinalModifier),
                    static::tokenToString($n->name),
                    $base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null,
                    $n->classInterfaceClause,
                    static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], self::getStartLine($n->classMembers), false),
                    $start_line,
                    $end_line,
                    $n->getDocCommentText()
                );
            },
            'Microsoft\PhpParser\Node\Statement\TraitDeclaration' => static function (PhpParser\Node\Statement\TraitDeclaration $n, int $start_line): ast\Node {
                $end_line = static::getEndLine($n) ?: $start_line;
                return static::astStmtClass(
                    flags\CLASS_TRAIT,
                    static::tokenToString($n->name),
                    null,
                    null,
                    static::phpParserStmtlistToAstNode($n->traitMembers->traitMemberDeclarations ?? [], self::getStartLine($n->traitMembers), false),
                    $start_line,
                    $end_line,
                    $n->getDocCommentText()
                );
            },
            'Microsoft\PhpParser\Node\ClassConstDeclaration' => static function (PhpParser\Node\ClassConstDeclaration $n, int $start_line): ast\Node {
                return static::phpParserClassConstToAstNode($n, $start_line);
            },
            /** @return null - A stub that will be removed by the caller. */
            'Microsoft\PhpParser\Node\MissingMemberDeclaration' => static function (PhpParser\Node\MissingMemberDeclaration $unused_n, int $unused_start_line) {
                // This node type is generated for something that isn't a function/constant/property. e.g. "public example();"
                return null;
            },
            /** @return null - A stub that will be removed by the caller. */
            'Microsoft\PhpParser\Node\MissingDeclaration' => static function (PhpParser\Node\MissingDeclaration $unused_n, int $unused_start_line) {
                // This node type is generated for something that starts with an attribute but isn't a declaration.
                return null;
            },
            /**
             * @throws InvalidNodeException
             */
            'Microsoft\PhpParser\Node\MethodDeclaration' => static function (PhpParser\Node\MethodDeclaration $n, int $start_line): ast\Node {
                $statements = $n->compoundStatementOrSemicolon;
                $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
                if (($ast_return_type->children['name'] ?? null) === '') {
                    $ast_return_type = null;
                }
                $original_method_name = $n->name;
                if (!($original_method_name instanceof Token)) {
                    throw new InvalidNodeException();
                }
                if ($original_method_name->kind === TokenKind::Name) {
                    $method_name = static::tokenToString($original_method_name);
                } else {
                    $method_name = 'placeholder_' . $original_method_name->fullStart;
                }

                if ($n->questionToken !== null && $ast_return_type !== null) {
                    $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line);
                }
                return static::newAstDecl(
                    ast\AST_METHOD,
                    static::phpParserVisibilityToAstVisibility($n->modifiers) | ($n->byRefToken !== null ? flags\FUNC_RETURNS_REF : 0),
                    [
                        'params' => static::phpParserParamsToAstParams($n->parameters, $start_line),
                        'stmts' => static::phpParserStmtlistToAstNode($statements, self::getStartLine($statements), true),
                        'returnType' => $ast_return_type,
                    ],
                    $start_line,
                    $n->getDocCommentText(),
                    $method_name,
                    static::getEndLine($n),
                    self::nextDeclId()
                );
            },
            'Microsoft\PhpParser\Node\Statement\ConstDeclaration' => static function (PhpParser\Node\Statement\ConstDeclaration $n, int $start_line): ast\Node {
                return static::phpParserConstToAstNode($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Statement\DeclareStatement' => static function (PhpParser\Node\Statement\DeclareStatement $n, int $start_line): ast\Node {
                $doc_comment = $n->getDocCommentText();
                $directive = $n->declareDirective;
                if (!($directive instanceof PhpParser\Node\DeclareDirective)) {
                    throw new AssertionError("Unexpected type for directive");
                }
                return static::astStmtDeclare(
                    static::phpParserDeclareListToAstDeclares($directive, $start_line, $doc_comment),
                    $n->statements !== null ? static::phpParserStmtlistToAstNode($n->statements, $start_line, true) : null,
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\Statement\DoStatement' => static function (PhpParser\Node\Statement\DoStatement $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_DO_WHILE,
                    0,
                    [
                        'stmts' => static::phpParserStmtlistToAstNode($n->statement, $start_line, false),
                        'cond' => static::phpParserNodeToAstNode($n->expression),
                    ],
                    $start_line
                );
            },
            /**
             * @return ast\Node|ast\Node[]
             */
            'Microsoft\PhpParser\Node\Expression\EchoExpression' => static function (PhpParser\Node\Expression\EchoExpression $n, int $start_line) {
                $ast_echos = [];
                foreach ($n->expressions->children ?? [] as $expr) {
                    if ($expr instanceof Token && $expr->kind === TokenKind::CommaToken) {
                        continue;
                    }
                    $ast_echos[] = new ast\Node(
                        ast\AST_ECHO,
                        0,
                        ['expr' => static::phpParserNodeToAstNode($expr)],
                        $start_line
                    );
                }
                return \count($ast_echos) === 1 ? $ast_echos[0] : $ast_echos;
            },
            /**
             * @return ?ast\Node
             */
            'Microsoft\PhpParser\Node\ForeachKey' => static function (PhpParser\Node\ForeachKey $n, int $_): ?\ast\Node {
                $result = static::phpParserNodeToAstNode($n->expression);
                if (!$result instanceof ast\Node) {
                    return null;
                }
                return $result;
            },
            'Microsoft\PhpParser\Node\Statement\ForeachStatement' => static function (PhpParser\Node\Statement\ForeachStatement $n, int $start_line): ast\Node {
                $foreach_value = $n->foreachValue;
                $value = static::phpParserNodeToAstNode($foreach_value->expression);
                if ($foreach_value->ampersand) {
                    $value = new ast\Node(
                        ast\AST_REF,
                        0,
                        ['var' => $value],
                        $value->lineno ?? $start_line
                    );
                }
                $foreach_key = $n->foreachKey;
                return new ast\Node(
                    ast\AST_FOREACH,
                    0,
                    [
                        'expr' => static::phpParserNodeToAstNode($n->forEachCollectionName),
                        'value' => $value,
                        'key' => $foreach_key !== null ? static::phpParserNodeToAstNode($foreach_key) : null,
                        'stmts' => static::phpParserStmtlistToAstNode($n->statements, $start_line, true),
                    ],
                    $start_line
                );
                //return static::phpParserStmtlistToAstNode($n->statements, $start_line);
            },
            'Microsoft\PhpParser\Node\FinallyClause' => static function (PhpParser\Node\FinallyClause $n, int $start_line): ast\Node {
                // @phan-suppress-next-line PhanTypeMismatchReturnNullable return_null_on_empty is false.
                return static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false);
            },
            /**
             * @throws InvalidNodeException
             */
            'Microsoft\PhpParser\Node\Statement\FunctionDeclaration' => static function (PhpParser\Node\Statement\FunctionDeclaration $n, int $start_line): ast\Node {
                $end_line = static::getEndLine($n) ?: $start_line;
                $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
                if (($ast_return_type->children['name'] ?? null) === '') {
                    $ast_return_type = null;
                }
                if ($n->questionToken !== null && $ast_return_type !== null) {
                    $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line);
                }
                $name = $n->name;
                if (!($name instanceof Token)) {
                    throw new InvalidNodeException();
                }

                return static::astDeclFunction(
                    $n->byRefToken !== null,
                    static::tokenToString($name),
                    static::phpParserParamsToAstParams($n->parameters, $start_line),
                    $ast_return_type,
                    static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon, self::getStartLine($n->compoundStatementOrSemicolon), false),
                    $start_line,
                    $end_line,
                    $n->getDocCommentText()
                );
            },
            /** @return ast\Node|ast\Node[] */
            'Microsoft\PhpParser\Node\Statement\GlobalDeclaration' => static function (PhpParser\Node\Statement\GlobalDeclaration $n, int $start_line) {
                $global_nodes = [];
                foreach ($n->variableNameList->children ?? [] as $var) {
                    if ($var instanceof Token && $var->kind === TokenKind::CommaToken) {
                        continue;
                    }
                    $global_nodes[] = new ast\Node(ast\AST_GLOBAL, 0, ['var' => static::phpParserNodeToAstNode($var)], static::getEndLine($var) ?: $start_line);
                }
                return \count($global_nodes) === 1 ? $global_nodes[0] : $global_nodes;
            },
            'Microsoft\PhpParser\Node\Statement\IfStatementNode' => static function (PhpParser\Node\Statement\IfStatementNode $n, int $start_line): ast\Node {
                return static::phpParserIfStmtToAstIfStmt($n, $start_line);
            },
            /** @return ast\Node|ast\Node[] */
            'Microsoft\PhpParser\Node\Statement\InlineHtml' => static function (PhpParser\Node\Statement\InlineHtml $n, int $start_line) {
                $text = $n->text;
                if ($text === null) {
                    return [];  // For the beginning/end of files
                }
                return new ast\Node(
                    ast\AST_ECHO,
                    0,
                    ['expr' => static::tokenToRawString($n->text)],
                    $start_line
                );
            },
            /** @suppress PhanTypeMismatchArgument TODO: Make ForStatement have more accurate docs? */
            'Microsoft\PhpParser\Node\Statement\ForStatement' => static function (PhpParser\Node\Statement\ForStatement $n, int $start_line): ast\Node {
                return new ast\Node(
                    ast\AST_FOR,
                    0,
                    [
                        'init' => $n->forInitializer !== null ? static::phpParserExprListToExprList($n->forInitializer, $start_line) : null,
                        'cond' => $n->forControl !== null     ? static::phpParserExprListToExprList($n->forControl, $start_line) : null,
                        'loop' => $n->forEndOfLoop !== null   ? static::phpParserExprListToExprList($n->forEndOfLoop, $start_line) : null,
                        'stmts' => static::phpParserStmtlistToAstNode($n->statements, $start_line, true),
                    ],
                    $start_line
                );
            },
            /** @return ast\Node[] */
            'Microsoft\PhpParser\Node\Statement\NamespaceUseDeclaration' => static function (PhpParser\Node\Statement\NamespaceUseDeclaration $n, int $start_line): array {
                $use_clauses = $n->useClauses;
                $results = [];
                $parser_use_kind = $n->functionOrConst->kind ?? null;
                foreach ($use_clauses->children ?? [] as $use_clause) {
                    if (!($use_clause instanceof PhpParser\Node\NamespaceUseClause)) {
                        continue;
                    }
                    $results[] = static::astStmtUseOrGroupUseFromUseClause($use_clause, $parser_use_kind, $start_line);
                }
                return $results;
            },
            'Microsoft\PhpParser\Node\Statement\NamespaceDefinition' => static function (PhpParser\Node\Statement\NamespaceDefinition $n, int $start_line): ast\Node {
                $stmt = $n->compoundStatementOrSemicolon;
                $name_node = $n->name;
                if ($stmt instanceof PhpParser\Node) {
                    $stmts_start_line = self::getStartLine($stmt);
                    $ast_stmt = static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon, $stmts_start_line, true);
                    $start_line = $name_node !== null ? self::getStartLine($name_node) : $stmts_start_line;  // imitate php-ast
                } else {
                    $ast_stmt = null;
                }
                return new ast\Node(
                    ast\AST_NAMESPACE,
                    0,
                    [
                        'name' => $name_node !== null ? static::phpParserNameToString($name_node) : null,
                        'stmts' => $ast_stmt,
                    ],
                    $start_line
                );
            },
            /** @return array{} */
            'Microsoft\PhpParser\Node\Statement\EmptyStatement' => static function (PhpParser\Node\Statement\EmptyStatement $unused_node, int $unused_start_line): array {
                // `;;`
                return [];
            },
            'Microsoft\PhpParser\Node\PropertyDeclaration' => static function (PhpParser\Node\PropertyDeclaration $n, int $start_line): ast\Node {
                return static::phpParserPropertyToAstNode($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Statement\ReturnStatement' => static function (PhpParser\Node\Statement\ReturnStatement $n, int $start_line): ast\Node {
                $e = $n->expression;
                $expr_node = $e !== null ? static::phpParserNodeToAstNode($e) : null;
                return new ast\Node(ast\AST_RETURN, 0, ['expr' => $expr_node], $start_line);
            },
            /** @return ast\Node|ast\Node[] */
            'Microsoft\PhpParser\Node\Statement\FunctionStaticDeclaration' => static function (PhpParser\Node\Statement\FunctionStaticDeclaration $n, int $start_line) {
                $static_nodes = [];
                foreach ($n->staticVariableNameList->children ?? [] as $var) {
                    if ($var instanceof Token) {
                        continue;
                    }
                    if (!($var instanceof PhpParser\Node\StaticVariableDeclaration)) {
                        // FIXME error tolerance
                        throw new AssertionError("Expected StaticVariableDeclaration");
                    }

                    $assignment = $var->assignment;
                    $static_nodes[] = new ast\Node(ast\AST_STATIC, 0, [
                        'var' => new ast\Node(ast\AST_VAR, 0, ['name' => static::phpParserNodeToAstNode($var->variableName)], static::getEndLine($var) ?: $start_line),
                        'default' => $assignment !== null ? static::phpParserNodeToAstNode($assignment) : null,
                    ], static::getEndLine($var) ?: $start_line);
                }
                return \count($static_nodes) === 1 ? $static_nodes[0] : $static_nodes;
            },
            'Microsoft\PhpParser\Node\Statement\SwitchStatementNode' => static function (PhpParser\Node\Statement\SwitchStatementNode $n, int $_): ast\Node {
                return static::phpParserSwitchListToAstSwitch($n);
            },
            'Microsoft\PhpParser\Node\Statement\ThrowStatement' => static function (PhpParser\Node\Statement\ThrowStatement $n, int $start_line): ast\Node {
                return static::phpParserThrowToASTThrow($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\ThrowExpression' => static function (PhpParser\Node\Expression\ThrowExpression $n, int $start_line): ast\Node {
                return static::phpParserThrowToASTThrow($n, $start_line);
            },
            'Microsoft\PhpParser\Node\Expression\MatchExpression' => static function (PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node {
                return self::phpParserMatchToAstMatch($n, $start_line);
            },

            'Microsoft\PhpParser\Node\TraitUseClause' => static function (PhpParser\Node\TraitUseClause $n, int $start_line): ast\Node {
                $clauses_list_node = $n->traitSelectAndAliasClauses;
                if ($clauses_list_node instanceof PhpParser\Node\DelimitedList\TraitSelectOrAliasClauseList) {
                    $adaptations_inner = [];
                    foreach ($clauses_list_node->children as $select_or_alias_clause) {
                        if ($select_or_alias_clause instanceof Token) {
                            continue;
                        }
                        if (!($select_or_alias_clause instanceof PhpParser\Node\TraitSelectOrAliasClause)) {
                            throw new AssertionError("Expected TraitSelectOrAliasClause");
                        }
                        $result = static::phpParserNodeToAstNode($select_or_alias_clause);
                        if ($result instanceof ast\Node) {
                            $adaptations_inner[] = $result;
                        }
                    }
                    $adaptations = new ast\Node(ast\AST_TRAIT_ADAPTATIONS, 0, $adaptations_inner, $adaptations_inner[0]->lineno ?? $start_line);
                } else {
                    $adaptations = null;
                }
                return new ast\Node(
                    ast\AST_USE_TRAIT,
                    0,
                    [
                        'traits' => static::phpParserNameListToAstNameList($n->traitNameList->children ?? [], $start_line),
                        'adaptations' => $adaptations,
                    ],
                    $start_line
                );
            },

            /**
             * @return ?ast\Node
             */
            'Microsoft\PhpParser\Node\TraitSelectOrAliasClause' => static function (PhpParser\Node\TraitSelectOrAliasClause $n, int $start_line): ?\ast\Node {
                // FIXME targetName phpdoc is wrong.
                $name = $n->name;
                if ($n->asOrInsteadOfKeyword->kind === TokenKind::InsteadOfKeyword) {
                    if (!$name instanceof ScopedPropertyAccessExpression) {
                        return null;
                    }
                    $member_name_list = $name->memberName;
                    if ($member_name_list === null) {
                        return null;
                    }

                    $target_name_list = array_merge([$n->targetName], $n->remainingTargetNames ?? []);
                    if (\is_object($member_name_list)) {
                        $member_name_list = [$member_name_list];
                    }
                    // Trait::y insteadof OtherTrait
                    $trait_node = static::phpParserNonValueNodeToAstNode($name->scopeResolutionQualifier);
                    $method_node = static::phpParserNameListToAstNameList($member_name_list, $start_line);
                    $target_node = static::phpParserNameListToAstNameList($target_name_list, $start_line);
                    $outer_method_node = new ast\Node(ast\AST_METHOD_REFERENCE, 0, [
                        'class' => $trait_node,
                        'method' => $method_node->children[0]
                    ], $start_line);

                    if (\count($member_name_list) !== 1) {
                        throw new AssertionError("Expected insteadof member_name_list length to be 1");
                    }
                    $children = [
                        'method' => $outer_method_node,
                        'insteadof' => $target_node,
                    ];
                    return new ast\Node(ast\AST_TRAIT_PRECEDENCE, 0, $children, $start_line);
                } else {
                    if ($name instanceof PhpParser\Node\Expression\ScopedPropertyAccessExpression) {
                        $class_node = static::phpParserNonValueNodeToAstNode($name->scopeResolutionQualifier);
                        $method_node = static::phpParserNodeToAstNode($name->memberName);
                    } else {
                        $class_node = null;
                        $method_node = static::phpParserNameToString($name);
                    }
                    $flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false);
                    $target_name = $n->targetName;
                    $target_name = $target_name instanceof PhpParser\Node\QualifiedName ? static::phpParserNameToString($target_name) : null;
                    $children = [
                        'method' => new ast\Node(ast\AST_METHOD_REFERENCE, 0, [
                            'class' => $class_node,
                            'method' => $method_node,
                        ], $start_line),
                        'alias' => $target_name,
                    ];

                    return new ast\Node(ast\AST_TRAIT_ALIAS, $flags, $children, $start_line);
                }
            },
            'Microsoft\PhpParser\Node\Statement\TryStatement' => static function (PhpParser\Node\Statement\TryStatement $n, int $start_line): ast\Node {
                $finally_clause = $n->finallyClause;
                return static::astNodeTry(
                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
                    static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false), // $n->try
                    static::phpParserCatchlistToAstCatchlist($n->catchClauses ?? [], $start_line),
                    $finally_clause !== null ? static::phpParserStmtlistToAstNode($finally_clause->compoundStatement, self::getStartLine($finally_clause), false) : null,
                    $start_line
                );
            },
            /** @return ast\Node|ast\Node[] */
            'Microsoft\PhpParser\Node\Expression\UnsetIntrinsicExpression' => static function (PhpParser\Node\Expression\UnsetIntrinsicExpression $n, int $start_line) {
                $stmts = [];
                foreach ($n->expressions->children ?? [] as $var) {
                    if ($var instanceof Token) {
                        // Skip over ',' and invalid tokens
                        continue;
                    }
                    $stmts[] = new ast\Node(ast\AST_UNSET, 0, ['var' => static::phpParserNodeToAstNode($var)], static::getEndLine($var) ?: $start_line);
                }
                return \count($stmts) === 1 ? $stmts[0] : $stmts;
            },
            'Microsoft\PhpParser\Node\Statement\WhileStatement' => static function (PhpParser\Node\Statement\WhileStatement $n, int $start_line): ast\Node {
                return static::astNodeWhile(
                    static::phpParserNodeToAstNode($n->expression),
                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
                    static::phpParserStmtlistToAstNode($n->statements, $start_line, false),
                    $start_line
                );
            },
            'Microsoft\PhpParser\Node\Statement\GotoStatement' => static function (PhpParser\Node\Statement\GotoStatement $n, int $start_line): ast\Node {
                return new ast\Node(ast\AST_GOTO, 0, ['label' => static::tokenToString($n->name)], $start_line);
            },
            /** @return ast\Node[]|ast\Node */
            'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => static function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line) {
                $label = new ast\Node(ast\AST_LABEL, 0, ['name' => static::tokenToString($n->name)], $start_line);
                $raw_statement = $n->statement;
                if (!$raw_statement) {
                    // Hopefully, newer versions of tolerant-php-parser will treat named labels as a standlone statement
                    return $label;
                }
                $statement = static::phpParserNodeToAstNode($raw_statement);
                if (is_array($statement)) {
                    // E.g. there are multiple labels in a row.
                    \array_unshift($statement, $label);
                    return $statement;
                }
                return [$label, $statement];
            },
        ];

        foreach ($closures as $key => $_) {
            if (!(\class_exists($key))) {
                throw new AssertionError("Class $key should exist");
            }
        }
        return $closures;
    }

    /**
     * Overridden in TolerantASTConverterWithNodeMapping
     *
     * @param PhpParser\Node\NamespaceUseClause $use_clause
     * @param ?int $parser_use_kind
     * @param int $start_line
     * @throws InvalidNodeException
     */
    protected static function astStmtUseOrGroupUseFromUseClause(PhpParser\Node\NamespaceUseClause $use_clause, ?int $parser_use_kind, int $start_line): ast\Node
    {
        $namespace_name_node = $use_clause->namespaceName;
        if ($namespace_name_node instanceof PhpParser\Node\QualifiedName) {
            $namespace_name = \rtrim(static::phpParserNameToString($namespace_name_node), '\\');
        } else {
            throw new InvalidNodeException();
        }
        if ($use_clause->groupClauses !== null) {
            return static::astStmtGroupUse(
                $parser_use_kind,  // E.g. kind is FunctionKeyword or ConstKeyword or null
                $namespace_name,
                static::phpParserNamespaceUseListToAstUseList($use_clause->groupClauses->children ?? []),
                $start_line
            );
        } else {
            $alias_token = $use_clause->namespaceAliasingClause->name ?? null;
            $alias = $alias_token !== null ? static::tokenToString($alias_token) : null;
            return static::astStmtUse($parser_use_kind, $namespace_name, $alias, $start_line);
        }
    }

    private static function astNodeTry(
        \ast\Node $try_node,
        ?\ast\Node $catches_node,
        ?\ast\Node $finally_node,
        int $start_line
    ): ast\Node {
        // Return fields of $node->children in the same order as php-ast
        $children = [
            'try' => $try_node,
        ];
        if ($catches_node !== null) {
            $children['catches'] = $catches_node;
        }
        $children['finally'] = $finally_node;
        return new ast\Node(ast\AST_TRY, 0, $children, $start_line);
    }

    private static function astStmtCatch(ast\Node $types, ?string $var, \ast\Node $stmts, int $lineno): ast\Node
    {
        return new ast\Node(
            ast\AST_CATCH,
            0,
            [
                'class' => $types,
                // php 8.0 allows catch statements without variables
                'var' => is_string($var) ? new ast\Node(ast\AST_VAR, 0, ['name' => $var], $lineno) : null,
                'stmts' => $stmts,
            ],
            $lineno
        );
    }

    /**
     * @param PhpParser\Node\CatchClause[] $catches
     */
    private static function phpParserCatchlistToAstCatchlist(array $catches, int $lineno): ast\Node
    {
        $children = [];
        foreach ($catches as $parser_catch) {
            $children[] = static::phpParserNonValueNodeToAstNode($parser_catch);
        }
        return new ast\Node(ast\AST_CATCH_LIST, 0, $children, $children[0]->lineno ?? $lineno);
    }

    /**
     * @param list<Token|PhpParser\Node> $types
     */
    private static function phpParserNameListToAstNameList(array $types, int $line): ast\Node
    {
        $ast_types = [];
        foreach ($types as $type) {
            if ($type instanceof Token && $type->kind === TokenKind::CommaToken) {
                continue;
            }
            $ast_types[] = static::phpParserNonValueNodeToAstNode($type);
        }
        return new ast\Node(ast\AST_NAME_LIST, 0, $ast_types, $line);
    }

    /**
     * @param ast\Node|string|int|float $cond
     */
    private static function astNodeWhile($cond, ast\Node $stmts, int $start_line): ast\Node
    {
        return new ast\Node(
            ast\AST_WHILE,
            0,
            [
                'cond' => $cond,
                'stmts' => $stmts,
            ],
            $start_line
        );
    }

    /**
     * @param ast\Node|string|int|float $var
     * @param ast\Node|string|int|float $expr
     */
    private static function astNodeAssign($var, $expr, int $line, bool $ref): ast\Node
    {
        return new ast\Node(
            $ref ? ast\AST_ASSIGN_REF : ast\AST_ASSIGN,
            0,
            [
                'var'  => $var,
                'expr' => $expr,
            ],
            $line
        );
    }

    /**
     * @throws Error if the kind could not be found
     */
    private static function phpParserIncludeTokenToAstIncludeFlags(Token $type): int
    {
        switch ($type->kind) {
            case TokenKind::IncludeKeyword:
                return flags\EXEC_INCLUDE;
            case TokenKind::IncludeOnceKeyword:
                return flags\EXEC_INCLUDE_ONCE;
            case TokenKind::RequireKeyword:
                return flags\EXEC_REQUIRE;
            case TokenKind::RequireOnceKeyword:
                return flags\EXEC_REQUIRE_ONCE;
            default:
                throw new \Error("Unrecognized PhpParser include/require type");
        }
    }

    /**
     * @param PhpParser\Node\QualifiedName|Token|null $type
     */
    protected static function phpParserUnionTypeToAstNode($type, ?PhpParser\Node\DelimitedList\QualifiedNameList $other_types, int $line): ?\ast\Node
    {
        $types = [];
        if (!\is_null($type) && !($type instanceof Token && $type->kind === TokenKind::BarToken)) {
            $result = static::phpParserTypeToAstNode($type, $line);
            if ($result) {
                $types[] = $result;
            }
        }
        if ($other_types instanceof PhpParser\Node\DelimitedList\QualifiedNameList) {
            foreach ($other_types->children as $child) {
                if ($child instanceof Token && $child->kind === TokenKind::BarToken) {
                    continue;
                }
                $result = static::phpParserTypeToAstNode($child, static::getEndLine($child) ?: $line);
                if ($result) {
                    $types[] = $result;
                }
            }
        }
        $n = \count($types);
        if ($n === 0) {
            return null;
        } elseif ($n === 1) {
            return $types[0];
        }
        return new ast\Node(ast\AST_TYPE_UNION, 0, $types, $types[0]->lineno);
    }

    /**
     * @param PhpParser\Node\QualifiedName|Token|null $type
     */
    protected static function phpParserTypeToAstNode($type, int $line): ?ast\Node
    {
        if (\is_null($type)) {
            return null;
        }
        $original_type = $type;
        if ($type instanceof PhpParser\Node\QualifiedName) {
            $type = static::phpParserNameToString($type);
        } elseif ($type instanceof Token) {
            $type = static::tokenToString($type);
        }
        if (\is_string($type)) {
            switch (\strtolower($type)) {
                case 'null':
                    $flags = flags\TYPE_NULL;
                    break;
                case 'bool':
                    $flags = flags\TYPE_BOOL;
                    break;
                case 'int':
                    $flags = flags\TYPE_LONG;
                    break;
                case 'float':
                    $flags = flags\TYPE_DOUBLE;
                    break;
                case 'string':
                    $flags = flags\TYPE_STRING;
                    break;
                case 'array':
                    $flags = flags\TYPE_ARRAY;
                    break;
                case 'object':
                    $flags = flags\TYPE_OBJECT;
                    break;
                case 'callable':
                    $flags = flags\TYPE_CALLABLE;
                    break;
                case 'void':
                    $flags = flags\TYPE_VOID;
                    break;
                case 'iterable':
                    $flags = flags\TYPE_ITERABLE;
                    break;
                case 'false':
                    $flags = flags\TYPE_FALSE;
                    break;
                case 'static':
                    $flags = flags\TYPE_STATIC;
                    break;
                default:
                    // TODO: Refactor this into a function accepting a QualifiedName
                    if ($original_type instanceof PhpParser\Node\QualifiedName) {
                        if ($original_type->globalSpecifier !== null) {
                            $ast_kind = flags\NAME_FQ;
                        } elseif (($original_type->relativeSpecifier->namespaceKeyword ?? null) !== null) {
                            $ast_kind = flags\NAME_RELATIVE;
                        } else {
                            $ast_kind = flags\NAME_NOT_FQ;
                        }
                    } else {
                        $ast_kind = flags\NAME_NOT_FQ;
                    }
                    return new ast\Node(
                        ast\AST_NAME,
                        $ast_kind,
                        ['name' => $type],
                        $line
                    );
            }
            return new ast\Node(ast\AST_TYPE, $flags, [], $line);
        }
        return static::phpParserNodeToAstNode($type);
    }

    /**
     * @param bool $by_ref
     * @param ?ast\Node $type
     * @param string $name
     * @param ?ast\Node|?int|?string|?float $default
     */
    private static function astNodeParam(bool $is_nullable, bool $by_ref, bool $variadic, ?\ast\Node $type, string $name, $default, int $line): ast\Node
    {
        if ($is_nullable) {
            $type = new ast\Node(
                ast\AST_NULLABLE_TYPE,
                0,
                ['type' => $type],
                $line
            );
        }
        return new ast\Node(
            ast\AST_PARAM,
            ($by_ref ? flags\PARAM_REF : 0) | ($variadic ? flags\PARAM_VARIADIC : 0),
            [
                'type' => $type,
                'name' => $name,
                'default' => $default,
            ],
            $line
        );
    }

    private static function phpParserParamsToAstParams(?\Microsoft\PhpParser\node\delimitedlist\parameterdeclarationlist $parser_params, int $line): ast\Node
    {
        $new_params = [];
        foreach ($parser_params->children ?? [] as $parser_node) {
            if ($parser_node instanceof Token) {
                continue;
            }
            $new_params[] = static::phpParserNodeToAstNode($parser_node);
        }
        $result = new ast\Node(
            ast\AST_PARAM_LIST,
            0,
            $new_params,
            $new_params[0]->lineno ?? $line
        );
        if (($parser_node->kind ?? null) === TokenKind::CommaToken) {
            // @phan-suppress-next-line PhanUndeclaredProperty
            $result->polyfill_has_trailing_comma = true;
        }
        return $result;
    }

    /**
     * @param PhpParser\Node|PhpParser\Token $parser_node
     * @suppress UnusedSuppression, TypeMismatchProperty
     */
    protected static function astStub($parser_node): ast\Node
    {
        // Debugging code.
        if (\getenv(self::ENV_AST_THROW_INVALID)) {
            // @phan-suppress-next-line PhanThrowTypeAbsent only throws for debugging
            throw new \Error("TODO:" . get_class($parser_node));
        }

        $node = new ast\Node();
        $node->kind = "TODO:" . get_class($parser_node);
        $node->flags = 0;
        $node->lineno = self::getStartLine($parser_node);
        $node->children = [];
        return $node;
    }

    private static function phpParserClosureUsesToAstClosureUses(
        ?\Microsoft\PhpParser\Node\DelimitedList\UseVariableNameList $uses,
        int $line
    ): ?\ast\Node {
        $children = $uses->children ?? [];
        if (count($children) === 0) {
            return null;
        }
        $ast_uses = [];
        foreach ($children as $use) {
            if ($use instanceof Token) {
                continue;
            }
            if (!($use instanceof PhpParser\Node\UseVariableName)) {
                throw new AssertionError("Expected UseVariableName");
            }
            $ast_uses[] = new ast\Node(ast\AST_CLOSURE_VAR, $use->byRef ? ast\flags\CLOSURE_USE_REF : 0, ['name' => static::tokenToString($use->variableName)], self::getStartLine($use));
        }
        $result = new ast\Node(ast\AST_CLOSURE_USES, 0, $ast_uses, $ast_uses[0]->lineno ?? $line);
        if (($use->kind ?? null) === TokenKind::CommaToken) {
            // @phan-suppress-next-line PhanUndeclaredProperty
            $result->polyfill_has_trailing_comma = true;
        }
        return $result;
    }

    private static function resolveDocCommentForClosure(PhpParser\Node\Expression $node): ?string
    {
        $doc_comment = $node->getDocCommentText();
        if (\Phan\Library\StringUtil::isNonZeroLengthString($doc_comment)) {
            return $doc_comment;
        }
        for ($prev_node = $node; $node = $node->parent; $prev_node = $node) {
            if ($node instanceof PhpParser\Node\Expression\AssignmentExpression ||
                $node instanceof PhpParser\Node\Expression\ParenthesizedExpression ||
                $node instanceof PhpParser\Node\ArrayElement ||
                $node instanceof PhpParser\Node\Statement\ReturnStatement) {
                $doc_comment = $node->getDocCommentText();
                if (\Phan\Library\StringUtil::isNonZeroLengthString($doc_comment)) {
                    return $doc_comment;
                }
                continue;
            }
            if ($node instanceof PhpParser\Node\Expression\ArgumentExpression) {
                // Skip ArgumentExpression and the PhpParser\Node\DelimitedList\ArgumentExpressionList
                // to get to the CallExpression
                // @phan-suppress-next-line PhanPossiblyUndeclaredProperty
                $node = $node->parent->parent;
                // fall through
            }
            if ($node instanceof PhpParser\Node\Expression\MemberAccessExpression) {
                // E.g. ((Closure)->bindTo())
                if ($prev_node !== $node->dereferencableExpression) {
                    return null;
                }
                $doc_comment = $node->getDocCommentText();
                if (is_string($doc_comment)) {
                    return $doc_comment;
                }
                continue;
            }
            if ($node instanceof PhpParser\Node\Expression\CallExpression) {
                if ($prev_node === $node->callableExpression) {
                    $doc_comment = $node->getDocCommentText();
                    if (is_string($doc_comment)) {
                        return $doc_comment;
                    }
                    continue;
                }
                if ($node->callableExpression instanceof PhpParser\Node\Expression\AnonymousFunctionCreationExpression) {
                    return null;
                }
                $found = false;
                foreach ($node->argumentExpressionList->children ?? [] as $argument_expression) {
                    if (!($argument_expression instanceof PhpParser\Node\Expression\ArgumentExpression)) {
                        continue;
                    }
                    $expression = $argument_expression->expression;
                    if ($expression === $prev_node) {
                        $found = true;
                        $doc_comment = $node->getDocCommentText();
                        if (is_string($doc_comment)) {
                            return $doc_comment;
                        }
                        break;
                    }
                    if (!($expression instanceof PhpParser\Node)) {
                        continue;
                    }
                    if ($expression instanceof PhpParser\Node\ConstElement || $expression instanceof PhpParser\Node\NumericLiteral || $expression instanceof PhpParser\Node\StringLiteral) {
                        continue;
                    }
                    return null;
                }

                if ($found) {
                    continue;
                }
            }
            break;
        }
        return null;
    }

    private static function astDeclClosure(
        bool $by_ref,
        bool $static,
        ast\Node $params,
        ?\ast\Node $uses,
        ast\Node $stmts,
        ?\ast\Node $return_type,
        int $start_line,
        int $end_line,
        ?string $doc_comment
    ): ast\Node {
        return static::newAstDecl(
            ast\AST_CLOSURE,
            ($by_ref ? flags\FUNC_RETURNS_REF : 0) | ($static ? flags\MODIFIER_STATIC : 0),
            [
                'params' => $params,
                'uses' => $uses,
                'stmts' => $stmts,
                'returnType' => $return_type,
            ],
            $start_line,
            $doc_comment,
            '{closure}',
            $end_line,
            self::nextDeclId()
        );
    }

    /**
     * @param ?ast\Node $return_type
     * @param ?ast\Node $stmts (TODO: create empty statement list instead of null)
     * @param ?string $doc_comment
     */
    private static function astDeclFunction(
        bool $by_ref,
        string $name,
        ast\Node $params,
        ?\ast\Node $return_type,
        ?\ast\Node $stmts,
        int $line,
        int $end_line,
        ?string $doc_comment
    ): ast\Node {
        return static::newAstDecl(
            ast\AST_FUNC_DECL,
            $by_ref ? flags\FUNC_RETURNS_REF : 0,
            [
                'params' => $params,
                'stmts' => $stmts,
                'returnType' => $return_type,
            ],
            $line,
            $doc_comment,
            $name,
            $end_line,
            self::nextDeclId()
        );
    }

    /**
     * @param ?Token $flags
     * @throws InvalidArgumentException if the class flags were unexpected
     */
    private static function phpParserClassModifierToAstClassFlags(?Token $flags): int
    {
        if ($flags === null) {
            return 0;
        }
        switch ($flags->kind) {
            case TokenKind::AbstractKeyword:
                return flags\CLASS_ABSTRACT;
            case TokenKind::FinalKeyword:
                return flags\CLASS_FINAL;
            default:
                throw new InvalidArgumentException("Unexpected kind '" . Token::getTokenKindNameFromValue($flags->kind) . "'");
        }
    }

    private static function interfaceBaseClauseToNode(?\Microsoft\PhpParser\Node\InterfaceBaseClause $node): ?\ast\Node
    {
        if (!$node instanceof PhpParser\Node\InterfaceBaseClause) {
            // TODO: real placeholder?
            return null;
        }

        $interface_extends_name_list = [];
        foreach ($node->interfaceNameList->children ?? [] as $implement) {
            if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) {
                continue;
            }
            $interface_name_node = static::phpParserNonValueNodeToAstNode($implement);
            if (!$interface_name_node instanceof ast\Node) {
                throw new AssertionError("Expected valid node for interfaces inherited by class");
            }
            $interface_extends_name_list[] = $interface_name_node;
        }
        if (\count($interface_extends_name_list) === 0) {
            return null;
        }
        return new ast\Node(ast\AST_NAME_LIST, 0, $interface_extends_name_list, $interface_extends_name_list[0]->lineno);
    }

    private static function astStmtClass(
        int $flags,
        ?string $name,
        ?\ast\Node $extends,
        ?\Microsoft\PhpParser\node\classinterfaceclause $implements,
        ?\ast\Node $stmts,
        int $line,
        int $end_line,
        ?string $doc_comment
    ): ast\Node {

        // NOTE: `null` would be an anonymous class.
        // the empty string is a missing string we pretend is an anonymous class
        // so that Phan won't throw an UnanalyzableException during the analysis phase
        if ($name === null || $name === '') {
            $flags |= flags\CLASS_ANONYMOUS;
        }

        if (($flags & flags\CLASS_INTERFACE) > 0) {
            $children = [
                'extends'    => null,
                'implements' => $extends,
                'stmts'      => $stmts,
            ];
        } else {
            if ($implements !== null) {
                $ast_implements_inner = [];
                foreach ($implements->interfaceNameList->children ?? [] as $implement) {
                    // TODO: simplify?
                    if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) {
                        continue;
                    }
                    $implement_node = static::phpParserNonValueNodeToAstNode($implement);
                    if (!$implement_node instanceof ast\Node) {
                        continue;
                    }
                    $ast_implements_inner[] = $implement_node;
                }
                if (\count($ast_implements_inner) > 0) {
                    $ast_implements = new ast\Node(ast\AST_NAME_LIST, 0, $ast_implements_inner, $ast_implements_inner[0]->lineno);
                } else {
                    $ast_implements = null;
                }
            } else {
                $ast_implements = null;
            }
            $children = [
                'extends'    => $extends,
                'implements' => $ast_implements,
                'stmts'      => $stmts,
            ];
        }

        return static::newAstDecl(
            ast\AST_CLASS,
            $flags,
            $children,
            $line,
            $doc_comment,
            $name,
            $end_line,
            self::nextDeclId()
        );
    }

    private static function phpParserArgListToAstArgList(?\Microsoft\PhpParser\node\delimitedlist\argumentexpressionlist $args, int $line): ast\Node
    {
        $ast_args = [];
        foreach ($args->children ?? [] as $arg) {
            if ($arg instanceof Token && $arg->kind === TokenKind::CommaToken) {
                continue;
            }
            $ast_args[] = static::phpParserNodeToAstNode($arg);
        }
        $result = new ast\Node(ast\AST_ARG_LIST, 0, $ast_args, $args ? self::getStartLine($args) : $line);
        if (($arg->kind ?? null) === TokenKind::CommaToken) {
            // NOTE: This is deliberately using a dynamic property instead of a flag because other applications may use flags
            // @phan-suppress-next-line PhanUndeclaredProperty
            $result->polyfill_has_trailing_comma = true;
        }
        return $result;
    }

    /**
     * @param PhpParser\Node\Expression\ThrowExpression|PhpParser\Node\Statement\ThrowStatement $n
     */
    private static function phpParserThrowToASTThrow(object $n, int $start_line): ast\Node
    {
        $expression = $n->expression;
        if (!$expression) {
            throw new InvalidNodeException();
        }
        return new ast\Node(
            ast\AST_THROW,
            0,
            ['expr' => static::phpParserNodeToAstNode($expression)],
            $start_line
        );
    }

    protected static function phpParserMatchToAstMatch(PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node
    {
        $expression = $n->expression;
        if (!$expression) {
            throw new InvalidNodeException();
        }
        return new ast\Node(
            ast\AST_MATCH,
            0,
            [
                'cond' => static::phpParserNodeToAstNode($expression),
                'stmts' => static::phpParserMatchArmListToAstMatchArmList($n->arms, $start_line),
            ],
            $start_line
        );
    }

    protected static function phpParserMatchArmListToAstMatchArmList(?\Microsoft\PhpParser\Node\DelimitedList\MatchExpressionArmList $arms, int $start_line): ast\Node
    {
        $ast_arms = [];
        foreach ($arms->children ?? [] as $arm) {
            if (!$arm instanceof PhpParser\Node\MatchArm) {
                continue;
            }
            // @phan-suppress-next-line PhanTypeMismatchArgument
            try {
                $ast_arms[] = static::phpParserMatchArmToAstMatchArm($arm);
            } catch (InvalidNodeException $_) {
                continue;
            }
        }
        return new ast\Node(ast\AST_MATCH_ARM_LIST, 0, $ast_arms, $arms ? self::getStartLine($arms) : $start_line);
    }

    private static function phpParserMatchConditionListToAstNode(?PhpParser\Node\DelimitedList\MatchArmConditionList $condition_list): ?ast\Node
    {
        if (!$condition_list) {
            throw new InvalidNodeException();
        }
        $conditions = [];
        foreach ($condition_list->children ?? [] as $phpparser_condition) {
            if ($phpparser_condition instanceof Token) {
                switch ($phpparser_condition->kind) {
                    case TokenKind::DefaultKeyword:
                        return null;
                    case TokenKind::CommaToken:
                        continue 2;
                }
            }
            $conditions[] = static::phpParserNodeToAstNode($phpparser_condition);
        }
        if (!$conditions) {
            throw new InvalidNodeException();
        }
        return new ast\Node(ast\AST_EXPR_LIST, 0, $conditions, self::getStartLine($condition_list));
    }

    private static function phpParserMatchArmToAstMatchArm(PhpParser\Node\MatchArm $arm): ast\Node
    {
        return new ast\Node(
            ast\AST_MATCH_ARM,
            0,
            [
                'cond' => static::phpParserMatchConditionListToAstNode($arm->conditionList),
                'expr' => static::phpParserNodeToAstNode($arm->body),
            ],
            self::getStartLine($arm)
        );
    }

    /**
     * @param ?int $kind
     * @throws InvalidArgumentException if the token kind was somehow invalid
     */
    private static function phpParserNamespaceUseKindToASTUseFlags(?int $kind): int
    {
        switch ($kind ?? 0) {
            case TokenKind::FunctionKeyword:
                return flags\USE_FUNCTION;
            case TokenKind::ConstKeyword:
                return flags\USE_CONST;
            case 0:
                return flags\USE_NORMAL;
            default:
                throw new \InvalidArgumentException("Unexpected kind '" . Token::getTokenKindNameFromValue($kind ?? 0) . "'");
        }
    }

    /**
     * @param Token[]|PhpParser\Node\NamespaceUseGroupClause[]|PhpParser\Node[] $uses
     * @return ast\Node[]
     */
    private static function phpParserNamespaceUseListToAstUseList(array $uses): array
    {
        $ast_uses = [];
        foreach ($uses as $use_clause) {
            if (!($use_clause instanceof PhpParser\Node\NamespaceUseGroupClause)) {
                continue;
            }
            $raw_namespace_name = $use_clause->namespaceName;
            if (!$raw_namespace_name instanceof PhpParser\Node\QualifiedName) {
                // Invalid AST, ignore. We should have already warned about the syntax
                continue;
            }
            // ast doesn't fill in an alias if it's identical to the real name,
            // but phpParser does?
            $namespace_name = \rtrim(static::phpParserNameToString($raw_namespace_name), '\\');
            $alias_token = $use_clause->namespaceAliasingClause->name ?? null;
            $alias = $alias_token !== null ? static::tokenToString($alias_token) : null;

            $ast_uses[] = new ast\Node(
                ast\AST_USE_ELEM,
                static::phpParserNamespaceUseKindToASTUseFlags($use_clause->functionOrConst->kind ?? 0),
                [
                    'name' => $namespace_name,
                    'alias' => $alias !== $namespace_name ? $alias : null,
                ],
                self::getStartLine($use_clause)
            );
        }
        return $ast_uses;
    }

    private static function astStmtUse(?int $type, string $name, ?string $alias, int $line): ast\Node
    {
        $use_inner = new ast\Node(ast\AST_USE_ELEM, 0, ['name' => $name, 'alias' => $alias], $line);
        return new ast\Node(
            ast\AST_USE,
            static::phpParserNamespaceUseKindToASTUseFlags($type),
            [$use_inner],
            $line
        );
    }

    /**
     * @param ?int $type
     * @param ?string $prefix
     * @param list<ast\Node> $uses
     * @suppress PhanPossiblyUndeclaredProperty $use should always be a node
     */
    private static function astStmtGroupUse(?int $type, ?string $prefix, array $uses, int $line): ast\Node
    {
        $flags = static::phpParserNamespaceUseKindToASTUseFlags($type);
        $uses = new ast\Node(ast\AST_USE, 0, $uses, $line);
        if ($flags === flags\USE_NORMAL) {
            foreach ($uses->children as $use) {
                if ($use->flags !== 0) {
                    $flags = 0;
                    break;
                }
            }
        } else {
            foreach ($uses->children as $use) {
                if ($use->flags === flags\USE_NORMAL) {
                    $use->flags = 0;
                }
            }
        }

        return new ast\Node(
            ast\AST_GROUP_USE,
            $flags,
            [
                'prefix' => $prefix,
                'uses' => $uses,
            ],
            $line
        );
    }

    /**
     * @param ast\Node|string|int|float|null $cond (null for else statements)
     * @param ast\Node $stmts
     * @param int $line
     */
    private static function astIfElem($cond, \ast\Node $stmts, int $line): ast\Node
    {
        return new ast\Node(ast\AST_IF_ELEM, 0, ['cond' => $cond, 'stmts' => $stmts], $line);
    }

    private static function phpParserSwitchListToAstSwitch(PhpParser\Node\Statement\SwitchStatementNode $node): ast\Node
    {
        $stmts = [];
        $node_line = static::getStartLine($node);
        foreach ($node->caseStatements as $case) {
            if (!($case instanceof PhpParser\Node\CaseStatementNode)) {
                continue;
            }
            $case_line = static::getStartLine($case);
            $stmts[] = new ast\Node(
                ast\AST_SWITCH_CASE,
                0,
                [
                    'cond' => $case->expression !== null ? static::phpParserNodeToAstNode($case->expression) : null,
                    'stmts' => static::phpParserStmtlistToAstNode($case->statementList, $case_line, false),
                ],
                $case_line
            );
        }
        return new ast\Node(ast\AST_SWITCH, 0, [
            'cond' => static::phpParserNodeToAstNode($node->expression),
            'stmts' => new ast\Node(ast\AST_SWITCH_LIST, 0, $stmts, $stmts[0]->lineno ?? $node_line),
        ], $node_line);
    }

    /**
     * @param PhpParser\Node[]|PhpParser\Node|Token $stmts
     */
    private static function getStartLineOfStatementOrStatements($stmts): int
    {
        if (is_array($stmts)) {
            return isset($stmts[0]) ? self::getStartLine($stmts[0]) : 0;
        }
        return self::getStartLine($stmts);
    }

    private static function phpParserIfStmtToAstIfStmt(PhpParser\Node\Statement\IfStatementNode $node, int $start_line): ast\Node
    {
        $if_elem = static::astIfElem(
            static::phpParserNodeToAstNode($node->expression),
            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
            static::phpParserStmtlistToAstNode(
                $node->statements,
                self::getStartLineOfStatementOrStatements($node->statements) ?: $start_line,
                false
            ),
            $start_line
        );
        $if_elems = [$if_elem];
        foreach ($node->elseIfClauses as $else_if) {
            $if_elem_line = self::getStartLine($else_if);
            $if_elem = static::astIfElem(
                static::phpParserNodeToAstNode($else_if->expression),
                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
                static::phpParserStmtlistToAstNode(
                    $else_if->statements,
                    self::getStartLineOfStatementOrStatements($else_if->statements)
                ),
                $if_elem_line
            );
            $if_elems[] = $if_elem;
        }
        $parser_else_node = $node->elseClause;
        if ($parser_else_node) {
            $parser_else_line = self::getStartLineOfStatementOrStatements($parser_else_node->statements);
            $if_elems[] = static::astIfElem(
                null,
                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
                static::phpParserStmtlistToAstNode($parser_else_node->statements, $parser_else_line, false),
                $parser_else_line
            );
        }
        return new ast\Node(ast\AST_IF, 0, $if_elems, $start_line);
    }

    /**
     * @return ast\Node|string|int|float
     */
    private static function astNodeBinaryop(int $flags, PhpParser\Node\Expression\BinaryExpression $n, int $start_line)
    {
        try {
            $left_node = static::phpParserNodeToAstNode($n->leftOperand);
        } catch (InvalidNodeException $_) {
            if (self::$should_add_placeholders) {
                $left_node = static::newPlaceholderExpression($n->leftOperand);
            } else {
                // convert `;$b ^;` to `;$b;`
                return static::phpParserNodeToAstNode($n->rightOperand);
            }
        }
        try {
            $right_node = static::phpParserNodeToAstNode($n->rightOperand);
        } catch (InvalidNodeException $_) {
            if (self::$should_add_placeholders) {
                $right_node = static::newPlaceholderExpression($n->rightOperand);
            } else {
                // convert `;^ $b;` to `;$b;`
                return $left_node;
            }
        }

        return new ast\Node(
            ast\AST_BINARY_OP,
            $flags,
            [
                'left' => $left_node,
                'right' => $right_node,
            ],
            $start_line
        );
    }

    /**
     * Binary assignment operation such as `+=`
     *
     * @return ast\Node|string|int|float
     * (Can return non-Node for an invalid AST if the right-hand is a scalar)
     */
    private static function astNodeAssignop(int $flags, PhpParser\Node\Expression\BinaryExpression $n, int $start_line)
    {
        try {
            $var_node = static::phpParserNodeToAstNode($n->leftOperand);
        } catch (InvalidNodeException $_) {
            if (self::$should_add_placeholders) {
                $var_node = new ast\Node(ast\AST_VAR, 0, ['name' => '__INCOMPLETE_VARIABLE__'], $start_line);
            } else {
                // convert `;= $b;` to `;$b;`
                return static::phpParserNodeToAstNode($n->rightOperand);
            }
        }
        $expr_node = static::phpParserNodeToAstNode($n->rightOperand);
        return new ast\Node(
            ast\AST_ASSIGN_OP,
            $flags,
            [
                'var' => $var_node,
                'expr' => $expr_node,
            ],
            $start_line
        );
    }

    /**
     * @param PhpParser\Node\Expression\AssignmentExpression|PhpParser\Node\Expression\Variable $n
     * @param ?string $doc_comment
     * @throws InvalidNodeException if the type can't be converted to a valid AST
     * @throws InvalidArgumentException if the passed in class is completely unexpected
     */
    private static function phpParserPropelemToAstPropelem($n, ?string $doc_comment): ast\Node
    {
        if ($n instanceof PhpParser\Node\Expression\AssignmentExpression) {
            $name_node = $n->leftOperand;
            if (!($name_node instanceof PhpParser\Node\Expression\Variable)) {
                throw new InvalidNodeException();
            }
            $children = [
                'name' => static::phpParserNodeToAstNode($name_node->name),
                'default' => $n->rightOperand ? static::phpParserNodeToAstNode($n->rightOperand) : null,
            ];
        } elseif ($n instanceof PhpParser\Node\Expression\Variable) {
            $name = $n->name;
            if (!($name instanceof Token) || !$name->length) {
                throw new InvalidNodeException();
            }
            $children = [
                'name' => static::tokenToString($name),
                'default' => null,
            ];
        } else {
            // @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw
            throw new \InvalidArgumentException("Unexpected class for property element: Expected Variable or AssignmentExpression, got: " . static::debugDumpNodeOrToken($n));
        }

        $start_line = self::getStartLine($n);

        $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment;
        return new ast\Node(ast\AST_PROP_ELEM, 0, $children, $start_line);
    }

    private static function phpParserConstelemToAstConstelem(PhpParser\Node\ConstElement $n, ?string $doc_comment): ast\Node
    {
        $start_line = self::getStartLine($n);
        $children = [
            'name' => static::variableTokenToString($n->name),
            'value' => static::phpParserNodeToAstNode($n->assignment),
        ];

        $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment;
        return new ast\Node(ast\AST_CONST_ELEM, 0, $children, $start_line);
    }

    /**
     * @param Token[] $visibility
     * @throws RuntimeException if a visibility token was unexpected
     */
    private static function phpParserVisibilityToAstVisibility(array $visibility, bool $automatically_add_public = true): int
    {
        $ast_visibility = 0;
        foreach ($visibility as $token) {
            switch ($token->kind) {
                case TokenKind::VarKeyword:
                    $ast_visibility |= flags\MODIFIER_PUBLIC;
                    break;
                case TokenKind::PublicKeyword:
                    $ast_visibility |= flags\MODIFIER_PUBLIC;
                    break;
                case TokenKind::ProtectedKeyword:
                    $ast_visibility |= flags\MODIFIER_PROTECTED;
                    break;
                case TokenKind::PrivateKeyword:
                    $ast_visibility |= flags\MODIFIER_PRIVATE;
                    break;
                case TokenKind::StaticKeyword:
                    $ast_visibility |= flags\MODIFIER_STATIC;
                    break;
                case TokenKind::AbstractKeyword:
                    $ast_visibility |= flags\MODIFIER_ABSTRACT;
                    break;
                case TokenKind::FinalKeyword:
                    $ast_visibility |= flags\MODIFIER_FINAL;
                    break;
                default:
                    throw new \RuntimeException("Unexpected visibility modifier '" . Token::getTokenKindNameFromValue($token->kind) . "'");
            }
        }
        if ($automatically_add_public && !($ast_visibility & (flags\MODIFIER_PUBLIC | flags\MODIFIER_PROTECTED | flags\MODIFIER_PRIVATE))) {
            $ast_visibility |= flags\MODIFIER_PUBLIC;
        }
        return $ast_visibility;
    }

    private static function phpParserPropertyToAstNode(PhpParser\Node\PropertyDeclaration $n, int $start_line): ast\Node
    {
        $prop_elems = [];
        $doc_comment = $n->getDocCommentText();

        foreach ($n->propertyElements->children ?? [] as $i => $prop) {
            if ($prop instanceof Token) {
                continue;
            }
            // @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node
            $prop_elems[] = static::phpParserPropelemToAstPropelem($prop, $i === 0 ? $doc_comment : null);
        }
        $flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false);

        $line = $prop_elems[0]->lineno ?? (self::getStartLine($n) ?: $start_line);
        $prop_decl = new ast\Node(ast\AST_PROP_DECL, 0, $prop_elems, $line);
        $type_line = static::getEndLine($n->typeDeclaration) ?: $start_line;
        return new ast\Node(ast\AST_PROP_GROUP, $flags, [
            'type' => static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line),
            'props' => $prop_decl,
        ], $line);
    }

    private static function phpParserClassConstToAstNode(PhpParser\Node\ClassConstDeclaration $n, int $start_line): ast\Node
    {
        $const_elems = [];
        $doc_comment = $n->getDocCommentText();
        foreach ($n->constElements->children ?? [] as $i => $const_elem) {
            if ($const_elem instanceof Token) {
                continue;
            }
            // @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node
            $const_elems[] = static::phpParserConstelemToAstConstelem($const_elem, $i === 0 ? $doc_comment : null);
        }
        $flags = static::phpParserVisibilityToAstVisibility($n->modifiers);

        return new ast\Node(ast\AST_CLASS_CONST_DECL, $flags, $const_elems, $const_elems[0]->lineno ?? $start_line);
    }

    /**
     * @throws InvalidNodeException
     */
    private static function phpParserConstToAstNode(PhpParser\Node\Statement\ConstDeclaration $n, int $start_line): ast\Node
    {
        $const_elems = [];
        $doc_comment = $n->getDocCommentText();
        foreach ($n->constElements->children ?? [] as $i => $prop) {
            if ($prop instanceof Token) {
                continue;
            }
            if (!($prop instanceof PhpParser\Node\ConstElement)) {
                throw new InvalidNodeException();
            }
            $const_elems[] = static::phpParserConstelemToAstConstelem($prop, $i === 0 ? $doc_comment : null);
        }

        return new ast\Node(ast\AST_CONST_DECL, 0, $const_elems, $const_elems[0]->lineno ?? $start_line);
    }

    private static function phpParserDeclareListToAstDeclares(PhpParser\Node\DeclareDirective $declare, int $start_line, ?string $first_doc_comment): ast\Node
    {
        $ast_declare_elements = [];
        if ($declare->name->length > 0 && $declare->literal->length > 0) {
            // Skip SkippedToken or MissingToken
            $children = [
                'name' => static::tokenToString($declare->name),
                'value' => static::tokenToScalar($declare->literal),
            ];
            $doc_comment = static::extractPhpdocComment($declare) ?? $first_doc_comment;
            // $first_doc_comment = null;
            $children['docComment'] = $doc_comment;
            $node = new ast\Node(ast\AST_CONST_ELEM, 0, $children, self::getStartLine($declare));
            $ast_declare_elements[] = $node;
        }
        return new ast\Node(ast\AST_CONST_DECL, 0, $ast_declare_elements, $start_line);
    }

    private static function astStmtDeclare(ast\Node $declares, ?\ast\Node $stmts, int $start_line): ast\Node
    {
        $children = [
            'declares' => $declares,
            'stmts' => $stmts,
        ];
        return new ast\Node(ast\AST_DECLARE, 0, $children, $start_line);
    }

    /**
     * @param string|ast\Node $expr
     * @param ast\Node $args
     */
    private static function astNodeCall($expr, \ast\Node $args, int $start_line): ast\Node
    {
        if (\is_string($expr)) {
            if (substr($expr, 0, 1) === '\\') {
                $expr = substr($expr, 1);
            }
            $expr = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => $expr], $start_line);
        }
        return new ast\Node(ast\AST_CALL, 0, ['expr' => $expr, 'args' => $args], $start_line);
    }

    /**
     * @param ast\Node|string $expr (can parse non-nodes, but they'd cause runtime errors)
     * @param ast\Node|string $method
     */
    private static function astNodeMethodCall(int $kind, $expr, $method, ast\Node $args, int $start_line): ast\Node
    {
        return new ast\Node($kind, 0, ['expr' => $expr, 'method' => $method, 'args' => $args], $start_line);
    }

    /**
     * @param ast\Node|string $class
     * @param ast\Node|string $method
     */
    private static function astNodeStaticCall($class, $method, ast\Node $args, int $start_line): ast\Node
    {
        // TODO: is this applicable?
        if (\is_string($class)) {
            if (substr($class, 0, 1) === '\\') {
                $class = substr($class, 1);
            }
            $class = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => $class], $start_line);
        }
        return new ast\Node(ast\AST_STATIC_CALL, 0, ['class' => $class, 'method' => $method, 'args' => $args], $start_line);
    }

    /**
     * TODO: Get rid of this function?
     * @param string|PhpParser\Node|null|array $comments
     * @return ?string the doc comment, or null
     */
    private static function extractPhpdocComment($comments): ?string
    {
        if (\is_string($comments)) {
            return $comments;
        }
        if ($comments instanceof PhpParser\Node) {
            // TODO: Extract only the substring with doc comment text?
            return $comments->getDocCommentText() ?: null;
        }
        return null;
        // TODO: Could extract comments from elsewhere
        /*
        if ($comments === null) {
            return null;
        }
        if (!(\is_array($comments))) {
            throw new AssertionError("Expected an array of comments");
        }
        if (\count($comments) === 0) {
            return null;
        }
         */
    }

    private static function phpParserListToAstList(PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line): ast\Node
    {
        $ast_items = [];
        $prev_was_element = false;
        foreach ($n->listElements->children ?? [] as $item) {
            if ($item instanceof Token) {
                if (!$prev_was_element) {
                    $ast_items[] = null;
                    continue;
                }
                $prev_was_element = false;
                continue;
            } else {
                $prev_was_element = true;
            }
            if (!($item instanceof PhpParser\Node\ArrayElement)) {
                throw new AssertionError("Expected ArrayElement");
            }
            $element_key = $item->elementKey;
            $ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, 0, [
                'value' => static::phpParserNodeToAstNode($item->elementValue),
                'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null,
            ], self::getStartLine($item));
        }
        if (self::$php_version_id_parsing < 70100 && \count($ast_items) === 0) {
            $ast_items[] = null;
        }
        return new ast\Node(ast\AST_ARRAY, flags\ARRAY_SYNTAX_LIST, $ast_items, $start_line);
    }

    private static function phpParserArrayToAstArray(PhpParser\Node\Expression\ArrayCreationExpression $n, int $start_line): ast\Node
    {
        $ast_items = [];
        $prev_was_element = false;
        foreach ($n->arrayElements->children ?? [] as $item) {
            if ($item instanceof Token) {
                if (!$prev_was_element) {
                    $ast_items[] = null;
                    continue;
                }
                $prev_was_element = false;
                continue;
            } else {
                $prev_was_element = true;
            }
            if (!($item instanceof PhpParser\Node\ArrayElement)) {
                throw new AssertionError("Expected ArrayElement");
            }
            if ($item->dotDotDot) {
                $ast_items[] = new ast\Node(ast\AST_UNPACK, 0, [
                    'expr' => static::phpParserNodeToAstNode($item->elementValue),
                ], self::getStartLine($item));
                continue;
            }
            $flags = $item->byRef ? flags\ARRAY_ELEM_REF : 0;
            $element_key = $item->elementKey;
            $ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, $flags, [
                'value' => static::phpParserNodeToAstNode($item->elementValue),
                'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null,
            ], self::getStartLine($item));
        }
        if (self::$php_version_id_parsing < 70100) {
            $flags = 0;
        } else {
            $kind = $n->openParenOrBracket->kind;
            if ($kind === TokenKind::OpenBracketToken) {
                $flags = flags\ARRAY_SYNTAX_SHORT;
            } else {
                $flags = flags\ARRAY_SYNTAX_LONG;
            }
        }
        // Workaround for ast line choice
        return new ast\Node(ast\AST_ARRAY, $flags, $ast_items, $ast_items[0]->lineno ?? $start_line);
    }

    /**
     * @throws InvalidNodeException if the member name could not be converted
     *
     * (and various other exceptions)
     */
    private static function phpParserMemberAccessExpressionToAstProp(PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line): \ast\Node
    {
        // TODO: Check for incomplete tokens?
        $member_name = $n->memberName;
        try {
            $name = static::phpParserNodeToAstNode($member_name);  // complex expression
        } catch (InvalidNodeException $e) {
            if (self::$should_add_placeholders) {
                $name = self::INCOMPLETE_PROPERTY;
            } else {
                throw $e;
            }
        }
        return new ast\Node(
            $n->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_PROP : ast\AST_PROP,
            0,
            [
                'expr'  => static::phpParserNodeToAstNode($n->dereferencableExpression),
                'prop'  => $name,  // ast\Node|string
            ],
            $start_line
        );
    }

    /**
     * @return int|string|float|bool|null
     */
    private static function tokenToScalar(Token $n)
    {
        $str = static::tokenToString($n);
        $int = \filter_var($str, FILTER_VALIDATE_INT);
        if ($int !== false) {
            return $int;
        }
        $float = \filter_var($str, FILTER_VALIDATE_FLOAT);
        if ($float !== false) {
            return $float;
        }

        return StringUtil::parse($str);
    }

    /**
     * @throws Exception if node is invalid
     */
    private static function parseQuotedString(PhpParser\Node\StringLiteral $n): string
    {
        $start = $n->getStart();
        $text = (string)substr(self::$file_contents, $start, $n->getEndPosition() - $start);
        return StringUtil::parse($text);
    }

    /**
     * @suppress PhanPartialTypeMismatchArgumentInternal hopefully in range
     */
    private static function variableTokenToString(Token $n): string
    {
        return \ltrim(\trim($n->getText(self::$file_contents)), '$');
    }

    /**
     * @suppress PhanPartialTypeMismatchReturn this is in bounds and $file_contents is a string
     */
    private static function tokenToRawString(Token $n): string
    {
        return $n->getText(self::$file_contents);
    }

    /** @internal */
    private const MAGIC_CONST_LOOKUP = [
        '__LINE__' => flags\MAGIC_LINE,
        '__FILE__' => flags\MAGIC_FILE,
        '__DIR__' => flags\MAGIC_DIR,
        '__NAMESPACE__' => flags\MAGIC_NAMESPACE,
        '__FUNCTION__' => flags\MAGIC_FUNCTION,
        '__METHOD__' => flags\MAGIC_METHOD,
        '__CLASS__' => flags\MAGIC_CLASS,
        '__TRAIT__' => flags\MAGIC_TRAIT,
    ];

    // FIXME don't use in places expecting non-strings.
    /**
     * @phan-suppress PhanPartialTypeMismatchArgumentInternal hopefully in range
     */
    private static function tokenToString(Token $n): string
    {
        $result = \trim($n->getText(self::$file_contents));
        $kind = $n->kind;
        if ($kind === TokenKind::VariableName) {
            return \trim($result, '$');
        }
        return $result;
    }

    /**
     * @param PhpParser\Node\Expression|PhpParser\Node\QualifiedName|Token $scope_resolution_qualifier
     */
    private static function phpParserClassConstFetchToAstClassConstFetch($scope_resolution_qualifier, string $name, int $start_line): ast\Node
    {
        if (\strcasecmp($name, 'class') === 0) {
            $class_node = static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier);
            if (!$class_node instanceof ast\Node) {
                // e.g. (0)::class
                $class_node = new ast\Node(ast\AST_NAME, ast\flags\NAME_FQ, ['name' => $class_node], $start_line);
            }
            return new ast\Node(ast\AST_CLASS_NAME, 0, [
                'class' => $class_node,
            ], $start_line);
        }
        return new ast\Node(ast\AST_CLASS_CONST, 0, [
            'class' => static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier),
            'const' => $name,
        ], $start_line);
    }

    /**
     * @throws InvalidNodeException if the qualified type name could not be converted to a valid php-ast type name
     */
    private static function phpParserNameToString(PhpParser\Node\QualifiedName $name): string
    {
        $name_parts = $name->nameParts;
        // TODO: Handle error case (can there be missing parts?)
        $result = '';
        foreach ($name_parts as $part) {
            $part_as_string = static::tokenToString($part);
            if ($part_as_string !== '') {
                $result .= \trim($part_as_string);
            }
        }
        $result = \rtrim(\preg_replace('/\\\\{2,}/', '\\', $result), '\\');
        if ($result === '') {
            // Would lead to "The name cannot be empty" when parsing
            throw new InvalidNodeException();
        }
        return $result;
    }

    /**
     * @param array<mixed,ast\Node|string|int|float|null> $children
     */
    private static function newAstDecl(int $kind, int $flags, array $children, int $lineno, string $doc_comment = null, string $name = null, int $end_lineno = 0, int $decl_id = -1): ast\Node
    {
        $decl_children = [];
        $decl_children['name'] = $name;
        $decl_children['docComment'] = $doc_comment;
        $decl_children += $children;
        if ($decl_id >= 0) {
            $decl_children['__declId'] = $decl_id;
        }
        $node = new ast\Node($kind, $flags, $decl_children, $lineno);
        $node->endLineno = $end_lineno;
        return $node;
    }

    private static function nextDeclId(): int
    {
        return self::$decl_id++;
    }

    /** @param PhpParser\Node|Token $n the node or token to convert to a placeholder */
    private static function newPlaceholderExpression($n): ast\Node
    {
        $start_line = self::getStartLine($n);
        $name_node = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => '__INCOMPLETE_EXPR__'], $start_line);
        return new ast\Node(ast\AST_CONST, 0, ['name' => $name_node], $start_line);
    }

    /**
     * @param PhpParser\Node[]|PhpParser\Token[] $children $children
     */
    private static function parseMultiPartString(PhpParser\Node\StringLiteral $n, array $children): ast\Node
    {
        if ($n->startQuote->length >= 3) {
            return self::parseMultiPartHeredoc($n, $children);
        }
        return self::parseMultiPartRegularString($n, $children);
    }

    /**
     * @param PhpParser\Node[]|PhpParser\Token[] $children $children
     */
    private static function parseMultiPartRegularString(PhpParser\Node\StringLiteral $n, array $children): ast\Node
    {
        $inner_node_parts = [];
        $start_quote_text = static::tokenToString($n->startQuote);
        $end_quote_text = $n->endQuote->getText(self::$file_contents);

        foreach ($children as $part) {
            if ($part instanceof PhpParser\Node) {
                $inner_node_parts[] = static::phpParserNodeToAstNode($part);
            } else {
                $kind = $part->kind;
                if (\array_key_exists($kind, self::_IGNORED_STRING_TOKEN_KIND_SET)) {
                    continue;
                }
                // ($part->kind === TokenKind::EncapsedAndWhitespace)
                $raw_string = static::tokenToRawString($part);
                if (\strlen($start_quote_text) > 1) {
                    // I guess it depends on what's before it.
                    // TODO: Use a correct heuristic instead
                    $raw_string = "\n$raw_string\n";
                }

                // Pass in '"\\n"' and get "\n" (somewhat inefficient)
                $represented_string = StringUtil::parse($start_quote_text . $raw_string . $end_quote_text);
                $inner_node_parts[] = $represented_string;
            }
        }
        return new ast\Node(ast\AST_ENCAPS_LIST, 0, $inner_node_parts, self::getStartLine($children[0]));
    }

    /**
     * @param PhpParser\Node[]|PhpParser\Token[] $children $children
     */
    private static function parseMultiPartHeredoc(PhpParser\Node\StringLiteral $n, array $children): ast\Node
    {
        $inner_node_parts = [];
        $end_of_start_quote = self::$file_contents[$n->startQuote->start + $n->startQuote->length - 1];
        $end_quote_text = $n->endQuote->getText(self::$file_contents);

        $spaces = \strspn($end_quote_text, " \t");
        $raw_spaces = substr($end_quote_text, 0, $spaces);

        foreach ($children as $i => $part) {
            if ($part instanceof PhpParser\Node) {
                $inner_node_parts[] = static::phpParserNodeToAstNode($part);
                continue;
            }
            $kind = $part->kind;
            if (\array_key_exists($kind, self::_IGNORED_STRING_TOKEN_KIND_SET)) {
                continue;
            }
            // ($part->kind === TokenKind::EncapsedAndWhitespace)
            $raw_string = static::tokenToRawString($part);
            if ($i > 0) {
                $raw_string = $raw_spaces . $raw_string;
            }

            $represented_string = $spaces > 0 ? \preg_replace("/^" . $raw_spaces . "/m", '', $raw_string) : $raw_string;
            if ($end_of_start_quote !== "'") {
                $represented_string = StringUtil::parseEscapeSequences($represented_string, null);
            }
            $inner_node_parts[] = $represented_string;
        }
        $i = \count($inner_node_parts) - 1;
        $s = $inner_node_parts[$i];
        if (\is_string($s)) {
            $s = substr($s, 0, -1);
            // On Windows, the "\r" must also be removed from the last line of the heredoc
            if (substr($s, -1) === "\r") {
                $s = substr($s, 0, -1);
            }
            $inner_node_parts[$i] = $s;
        }

        return new ast\Node(ast\AST_ENCAPS_LIST, 0, $inner_node_parts, self::getStartLine($children[0]));
    }

    /**
     * Gets a string based on environment details that could affect parsing
     */
    private static function getEnvironmentDetails(): string
    {
        static $details = null;
        if ($details === null) {
            $details = \sha1(var_export([
                \PHP_VERSION,
                \PHP_BINARY,
                self::getDevelopmentBuildDate(),
                \phpversion('ast'),
                \ini_get('short_open_tag'),
                \sha1((string)\file_get_contents(__DIR__ . '/ast_shim.php')),
                class_exists(CLI::class) ? CLI::getDevelopmentVersionId() : 'unknown'
            ], true));
        }
        return $details;
    }

    /**
     * For development PHP versions such as 8.0.0-dev, use the build date as part of the cache key to invalidate cached ASTs when this gets rebuilt.
     * @suppress PhanImpossibleTypeComparison, PhanRedundantCondition, PhanImpossibleCondition, PhanSuspiciousValueComparison Phan evaluates the strpos to a constant, so this is either impossible or redundant
     */
    private static function getDevelopmentBuildDate(): ?string
    {
        if (\strpos(\PHP_VERSION, '-dev') === false) {
            return null;
        }
        \ob_start();
        \phpinfo(\INFO_GENERAL);
        $contents = (string)\ob_get_clean();
        \preg_match('/^Build Date.*=>\s*(.+)$/m', $contents, $matches);
        return $matches[1] ?? 'unknown';
    }

    /**
     * @return ?string - null if this should not be cached
     */
    public function generateCacheKey(string $file_contents, int $version): ?string
    {
        $details = var_export([
            \sha1($file_contents),
            $version,
            self::getEnvironmentDetails(),
            $this->instance_should_add_placeholders,
        ], true);
        return \sha1($details);
    }

    private static function normalizeTernaryExpression(TernaryExpression $n): TernaryExpression
    {
        $else = $n->elseExpression;
        if (!($else instanceof TernaryExpression)) {
            return $n;
        }
        // The else expression is an unparenthesized ternary expression. Rearrange the parts.
        // (Convert a ? b : (c ? d : e) to (a ? b : c) ? d : e)
        $inner_left = clone($n);
        // @phan-suppress-next-line PhanPartialTypeMismatchProperty pretty much all expressions can be tokens, type is incorrect
        $inner_left->elseExpression = $else->condition;
        $outer = clone($else);
        $outer->condition = $inner_left;
        return $outer;
    }
}
class_exists(TolerantASTConverterWithNodeMapping::class);