diff --git a/.gitignore b/.gitignore index e89db83..96699cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .*.swp .*.swo tags +vendor/ diff --git a/.phan/config.php b/.phan/config.php index 46434f3..5283aa2 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -1,6 +1,7 @@ false, + 'allow_missing_properties' => false, // Allow null to be cast as any type and for any // type to be cast to null. - "null_casts_as_any_type" => false, + 'null_casts_as_any_type' => false, + + // Allow null to be cast as any array-like type + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'null_casts_as_array' => false, + + // Allow any array-like type to be cast to null. + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'array_casts_as_null' => false, + + // If enabled, Phan will warn if **any** type in a method's object expression + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => true, + + // If enabled, Phan will warn if **any** type in the argument's type + // cannot be cast to a type in the parameter's expected type. + // Setting this to true will introduce a large number of false positives (and some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_param_checking' => true, + + // If enabled, Phan will warn if **any** type in a property assignment's type + // cannot be cast to a type in the property's expected type. + // Setting this to true will introduce a large number of false positives (and some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_property_checking' => true, + + // If enabled, Phan will warn if **any** type in the return statement's union type + // cannot be cast to a type in the method's declared return type. + // Setting this to true will introduce a large number of false positives (and some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_return_checking' => true, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => true, // If enabled, scalars (int, float, bool, string, null) // are treated as if they can cast to each other. + // This does not affect checks of array keys. See scalar_array_key_cast. 'scalar_implicit_cast' => false, // If true, seemingly undeclared variables in the global @@ -48,12 +89,77 @@ // Backwards Compatibility Checking 'backward_compatibility_checks' => false, + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. This process is + // slow. + 'check_docblock_signature_return_type_match' => true, + + // If true, check to make sure the param types declared + // in the doc-block (if any) matches the param types + // declared in the method signature. + 'check_docblock_signature_param_type_match' => true, + + // (*Requires check_docblock_signature_param_type_match to be true*) + // If true, make narrowed types from phpdoc params override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // Affects analysis of the body of the method and the param types passed in by callers. + 'prefer_narrowed_phpdoc_param_type' => true, + + // (*Requires check_docblock_signature_return_type_match to be true*) + // If true, make narrowed types from phpdoc returns override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // Affects analysis of return statements in the body of the method and the return types passed in by callers. + 'prefer_narrowed_phpdoc_return_type' => true, + // If enabled, check all methods that override a // parent method to make sure its signature is // compatible with the parent's. This check // can add quite a bit of time to the analysis. + // This will also check if final methods are overridden, etc. 'analyze_signature_compatibility' => true, + // Set this to true to allow contravariance in real parameter types of method overrides (Introduced in php 7.2) + // See https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening + // (Users may enable this if analyzing projects that support only php 7.2+) + // This is false by default. (Will warn if real parameter types are omitted in an override) + 'allow_method_param_type_widening' => false, + + // Set this to true to make Phan guess that undocumented parameter types + // (for optional parameters) have the same type as default values + // (Instead of combining that type with `mixed`). + // E.g. `function($x = 'val')` would make Phan infer that $x had a type of `string`, not `string|mixed`. + // Phan will not assume it knows specific types if the default value is false or null. + 'guess_unknown_parameter_type_using_default' => false, + + // When enabled, infer that the types of the properties of `$this` are equal to their default values at the start of `__construct()`. + // This will have some false positives due to Phan not checking for setters and initializing helpers. + // This does not affect inherited properties. + 'infer_default_properties_in_construct' => true, + + // Set this to true to enable the plugins that Phan uses to infer more accurate return types of `implode`, `json_decode`, and many other functions. + // + // Phan is slightly faster when these are disabled. + 'enable_extended_internal_return_type_plugins' => true, + + // This setting maps case insensitive strings to union types. + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // If the corresponding value is the empty string, Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // If the corresponding value is not empty, Phan will act as though it saw the corresponding union type when the keys show up in a UnionType of @param, @return, @var, @property, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: ['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => ''] + 'phpdoc_type_mapping' => [ ], + // Set to true in order to attempt to detect dead // (unreferenced) code. Keep in mind that the // results will only be a guess given that classes, @@ -67,17 +173,99 @@ // dead_code_detection will also enable unused variable detection. 'unused_variable_detection' => true, - // Run a quick version of checks that takes less - // time - "quick_mode" => false, + // Set to true in order to force tracking references to elements + // (functions/methods/consts/protected). + // dead_code_detection is another option which also causes references + // to be tracked. + 'force_tracking_references' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => true, + + // Enable this to warn about harmless redundant use for classes and namespaces such as `use Foo\bar` in namespace Foo. + // + // Note: This does not affect warnings about redundant uses in the global namespace. + 'warn_about_redundant_use_namespaced_class' => true, + + // If true, then run a quick version of checks that takes less time. + // False by default. + 'quick_mode' => false, + + // If true, then before analysis, try to simplify AST into a form + // which improves Phan's type inference in edge cases. + // + // This may conflict with 'dead_code_detection'. + // When this is true, this slows down analysis slightly. + // + // E.g. rewrites `if ($a = value() && $a > 0) {...}` + // into $a = value(); if ($a) { if ($a > 0) {...}}` + 'simplify_ast' => true, + + // If true, Phan will read `class_alias` calls in the global scope, + // then (1) create aliases from the *parsed* files if no class definition was found, + // and (2) emit issues in the global scope if the source or target class is invalid. + // (If there are multiple possible valid original classes for an aliased class name, + // the one which will be created is unspecified.) + // NOTE: THIS IS EXPERIMENTAL, and the implementation may change. + 'enable_class_alias_support' => false, // Enable or disable support for generic templated // class types. 'generic_types_enabled' => true, + // If enabled, warn about throw statement where the exception types + // are not documented in the PHPDoc of functions, methods, and closures. + 'warn_about_undocumented_throw_statements' => true, + + // If enabled (and warn_about_undocumented_throw_statements is enabled), + // warn about function/closure/method calls that have (at)throws + // without the invoking method documenting that exception. + 'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => true, + + // If this is a list, Phan will not warn about lack of documentation of (at)throws + // for any of the listed classes or their subclasses. + // This setting only matters when warn_about_undocumented_throw_statements is true. + // The default is the empty array (Warn about every kind of Throwable) + 'exception_classes_with_optional_throws_phpdoc' => [ + 'LogicException', + 'RuntimeException', + 'InvalidArgumentException', + 'AssertionError', + 'TypeError', + 'TolerantASTConverter\InvalidNodeException', // This is used internally in TolerantASTConverter + + // phpunit + 'PHPUnit\Framework\ExpectationFailedException', + 'SebastianBergmann\RecursionContext\InvalidArgumentException', + ], + + // Increase this to properly analyze require_once statements + 'max_literal_string_type_length' => 1000, + + // Setting this to true makes the process assignment for file analysis + // as predictable as possible, using consistent hashing. + // Even if files are added or removed, or process counts change, + // relatively few files will move to a different group. + // (use when the number of files is much larger than the process count) + // NOTE: If you rely on Phan parsing files/directories in the order + // that they were provided in this config, don't use this) + // See https://github.com/phan/phan/wiki/Different-Issue-Sets-On-Different-Numbers-of-CPUs + 'consistent_hashing_file_order' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => true, + // Override to hardcode existence and types of (non-builtin) globals. - // Class names must be prefixed with '\\'. - 'globals_type_map' => ['var' => 'string'], + // Class names should be prefixed with '\\'. + // (E.g. ['_FOO' => '\\FooClass', 'page' => '\\PageClass', 'userId' => 'int']) + 'globals_type_map' => [], // The minimum severity level to report on. This can be // set to Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL or @@ -87,98 +275,27 @@ // Add any issue types (such as 'PhanUndeclaredMethod') // here to inhibit them from being reported 'suppress_issue_types' => [ - // 'PhanUndeclaredMethod', + 'PhanUnreferencedClosure', // False positives seen with closures in arrays, TODO: move closure checks closer to what is done by unused variable plugin + 'PhanPluginNoCommentOnProtectedMethod', + 'PhanPluginDescriptionlessCommentOnProtectedMethod', + 'PhanPluginNoCommentOnPrivateMethod', + 'PhanPluginDescriptionlessCommentOnPrivateMethod', + 'PhanPluginDescriptionlessCommentOnPrivateProperty', + // TODO: Fix edge cases in --automatic-fix for PhanPluginRedundantClosureComment + 'PhanPluginRedundantClosureComment', ], // If empty, no filter against issues types will be applied. // If non-empty, only issues within the list will be emitted // by Phan. + // + // See https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan + // for the full list of issues that Phan detects. + // + // Phan is capable of detecting hundreds of types of issues. + // Projects should almost always use `suppress_issue_types` instead. 'whitelist_issue_types' => [ - // 'PhanAccessMethodPrivate', - // 'PhanAccessMethodProtected', - // 'PhanAccessNonStaticToStatic', - // 'PhanAccessPropertyPrivate', - // 'PhanAccessPropertyProtected', - // 'PhanAccessSignatureMismatch', - // 'PhanAccessSignatureMismatchInternal', - // 'PhanAccessStaticToNonStatic', - // 'PhanCompatibleExpressionPHP7', - // 'PhanCompatiblePHP7', - // 'PhanContextNotObject', - // 'PhanDeprecatedClass', - // 'PhanDeprecatedFunction', - // 'PhanDeprecatedProperty', - // 'PhanEmptyFile', - // 'PhanNonClassMethodCall', - // 'PhanNoopArray', - // 'PhanNoopClosure', - // 'PhanNoopConstant', - // 'PhanNoopProperty', - // 'PhanNoopVariable', - // 'PhanParamRedefined', - // 'PhanParamReqAfterOpt', - // 'PhanParamSignatureMismatch', - // 'PhanParamSignatureMismatchInternal', - // 'PhanParamSpecial1', - // 'PhanParamSpecial2', - // 'PhanParamSpecial3', - // 'PhanParamSpecial4', - // 'PhanParamTooFew', - // 'PhanParamTooFewInternal', - // 'PhanParamTooMany', - // 'PhanParamTooManyInternal', - // 'PhanParamTypeMismatch', - // 'PhanParentlessClass', - // 'PhanRedefineClass', - // 'PhanRedefineClassInternal', - // 'PhanRedefineFunction', - // 'PhanRedefineFunctionInternal', - // 'PhanStaticCallToNonStatic', - // 'PhanSyntaxError', - // 'PhanTraitParentReference', - // 'PhanTypeArrayOperator', - // 'PhanTypeArraySuspicious', - // 'PhanTypeComparisonFromArray', - // 'PhanTypeComparisonToArray', - // 'PhanTypeConversionFromArray', - // 'PhanTypeInstantiateAbstract', - // 'PhanTypeInstantiateInterface', - // 'PhanTypeInvalidLeftOperand', - // 'PhanTypeInvalidRightOperand', - // 'PhanTypeMismatchArgument', - // 'PhanTypeMismatchArgumentInternal', - // 'PhanTypeMismatchDefault', - // 'PhanTypeMismatchForeach', - // 'PhanTypeMismatchProperty', - // 'PhanTypeMismatchReturn', - // 'PhanTypeMissingReturn', - // 'PhanTypeNonVarPassByRef', - // 'PhanTypeParentConstructorCalled', - // 'PhanTypeVoidAssignment', - // 'PhanUnanalyzable', // 'PhanUndeclaredClass', - // 'PhanUndeclaredClassCatch', - // 'PhanUndeclaredClassConstant', - // 'PhanUndeclaredClassInstanceof', - // 'PhanUndeclaredClassMethod', - // 'PhanUndeclaredClassReference', - // 'PhanUndeclaredConstant', - // 'PhanUndeclaredExtendedClass', - // 'PhanUndeclaredFunction', - // 'PhanUndeclaredInterface', - // 'PhanUndeclaredMethod', - // 'PhanUndeclaredProperty', - // 'PhanUndeclaredStaticMethod', - // 'PhanUndeclaredStaticProperty', - // 'PhanUndeclaredTrait', - // 'PhanUndeclaredTypeParameter', - // 'PhanUndeclaredTypeProperty', - // 'PhanUndeclaredVariable', - // 'PhanUnreferencedClass', - // 'PhanUnreferencedConstant', - // 'PhanUnreferencedMethod', - // 'PhanUnreferencedProperty', - // 'PhanVariableUseClause', ], // A list of files to include in analysis @@ -188,6 +305,29 @@ 'exclude_analysis_file_list' => [ 'src/util.php', ], + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. '@Test\.php$@', or '@vendor/.*/(tests|Tests)/@') + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // A list of include paths to check when checking if `require_once`, `include`, etc. are valid. + // + // To refer to the directory of the file being analyzed, use `'.'` + // To refer to the project root directory, you must use \Phan\Config::getProjectRootDirectory() + // + // (E.g. `['.', \Phan\Config::getProjectRootDirectory() . '/src/folder-added-to-include_path']`) + 'include_paths' => ['.'], + + // Enable this to warn about the use of relative paths in `require_once`, `include`, etc. + // Relative paths are harder to reason about, and opcache may have issues with relative paths in edge cases. + 'warn_about_relative_include_statement' => true, // The number of processes to fork off during the analysis // phase. @@ -229,7 +369,44 @@ 'vendor/', ], + // Set this to false to emit PhanUndeclaredFunction issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or the internal functions used to run phan (may lead to false positives if an extension isn't loaded) + // If this is true(default), then Phan will not warn. + // Also see 'autoload_internal_extension_signatures' for an alternative way to fix this type of issue. + 'ignore_undeclared_functions_with_known_signatures' => false, + + 'plugin_config' => [ + // A list of 1 or more PHP binaries (Absolute path or program name found in $PATH) + // to use to analyze your files with PHP's native `--syntax-check`. + // + // This can be used to simultaneously run PHP's syntax checks with multiple PHP versions. + // e.g. `'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]` + // if all of those programs can be found in $PATH + + // 'php_native_syntax_check_binaries' => [PHP_BINARY], + + // The maximum number of `php --syntax-check` processes to run at any point in time (Minimum: 1). + // This may be temporarily higher if php_native_syntax_check_binaries has more elements than this process count. + 'php_native_syntax_check_max_processes' => 4, + + // blacklist of methods to warn about for HasPHPDocPlugin + 'has_phpdoc_method_ignore_regex' => '@^TolerantASTConverter\\\\Tests\\\\.*::(test.*|.*Provider)$@', + // Warn about duplicate descriptions for methods and property groups within classes. + // (This skips over deprecated methods) + // This may not apply to all code bases, + // but is useful in avoiding copied and pasted descriptions that may be inapplicable or too vague. + 'has_phpdoc_check_duplicates' => true, + + // If true, then never allow empty statement lists, even if there is a TODO/FIXME/"deliberately empty" comment. + 'empty_statement_list_ignore_todos' => true, + + // Automatically infer which methods are pure (i.e. should have no side effects) in UseReturnValuePlugin. + 'infer_pure_methods' => true, + ], + // A list of plugin files to execute + // NOTE: values can be the base name without the extension for plugins bundled with Phan (E.g. 'AlwaysReturnPlugin') + // or relative/absolute paths to the plugin (Relative to the project root). 'plugins' => [ 'AlwaysReturnPlugin', 'DollarDollarPlugin', @@ -238,5 +415,36 @@ 'DuplicateArrayKeyPlugin', 'PregRegexCheckerPlugin', 'PrintfCheckerPlugin', + 'PHPUnitAssertionPlugin', // analyze assertSame/assertInstanceof/assertTrue/assertFalse + 'UseReturnValuePlugin', + + // UnknownElementTypePlugin warns about unknown types in element signatures. + 'UnknownElementTypePlugin', + 'DuplicateExpressionPlugin', + // warns about carriage returns("\r"), trailing whitespace, and tabs in PHP files. + 'WhitespacePlugin', + // Warn about inline HTML anywhere in the files. + 'InlineHTMLPlugin', + //////////////////////////////////////////////////////////////////////// + // Plugins for Phan's self-analysis + //////////////////////////////////////////////////////////////////////// + + // Warns about the usage of assert() for Phan's self-analysis. See https://github.com/phan/phan/issues/288 + 'NoAssertPlugin', + + 'HasPHPDocPlugin', + 'PHPDocToRealTypesPlugin', // suggests replacing (at)return void with `: void` in the declaration, etc. + 'PHPDocRedundantPlugin', + 'PreferNamespaceUsePlugin', + 'EmptyStatementListPlugin', + + // Report empty (not overridden or overriding) methods and functions + // 'EmptyMethodAndFunctionPlugin', + + // Warn about using the same loop variable name as a loop variable of an outer loop. + 'LoopVariableReusePlugin', + // These are specific to Phan's coding style + 'StrictComparisonPlugin', + ], ]; diff --git a/.travis.yml b/.travis.yml index edb0ae6..cdd5b22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: php php: - - 7.0 - 7.1 - 7.2 - 7.3 + - 7.4snapshot dist: xenial @@ -22,7 +22,7 @@ install: # Install phan separately because the tolerant-php-parser version may be behind. script: - - composer create-project phan/phan=1.2.1 ~/phan-install; ~/phan-install/phan + - composer create-project phan/phan=2.3.0 ~/phan-install; ~/phan-install/phan - ./test branches: diff --git a/README.md b/README.md index 2a371ee..2d7c37f 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,9 @@ Tolerant-PHP-Parser to php-ast [![Build Status](https://travis-ci.org/TysonAndre/tolerant-php-parser-to-php-ast.svg?branch=master)](https://travis-ci.org/TysonAndre/tolerant-php-parser-to-php-ast) -This project uses Microsoft/tolerant-php-parser to generate a tree with error tolerance, then converts from that tree to ast\Node: https://github.com/Microsoft/tolerant-php-parser/issues/113 +This project uses Microsoft/tolerant-php-parser to generate a tree with error tolerance, then converts from that tree to ast\Node from [php-ast](https://github.com/nikic/php-ast) -This is 90% done. Current test cases pass, but some cases aren't yet tested. - -- Still being ported from https://github.com/TysonAndre/php-parser-to-php-ast -- The test suite is not yet comprehensive, may need to handle tokens - -[Current Issues](https://github.com/TysonAndre/tolerant-php-parser-to-php-ast/issues/) - -- The test suite this is based off of covers common cases for Phan, but edge cases still remain. - See https://github.com/TysonAndre/tolerant-php-parser-to-php-ast/issues/4 +This release (0.1.0) supports AST version 70. Usage ----- @@ -28,11 +20,12 @@ Using it as an error-tolerant substitute for php-ast (e.g. for use in IDEs) - Omitting errors only handles some common cases that come up while editing a file. - Placeholders may change in the future. - [tests/ASTConverter/ErrorTolerantConversionTest.php](https://github.com/TysonAndre/tolerant-php-parser-to-php-ast/blob/master/tests/ASTConverter/ErrorTolerantConversionTest.php) +- [Phan's](https://github.com/phan/phan) Language Server uses this code to do that. Running unit tests ------------------ -To run unit tests, you must install [nikic/php-ast](https://github.com/nikic/php-ast) 0.1.5+ (for the expected results to be created). +To run unit tests, you must install [nikic/php-ast](https://github.com/nikic/php-ast) 1.0.1+ (for the expected results to be created). You must also run `composer install` if you haven't already done so. - Then run `vendor/bin/phpunit` diff --git a/composer.json b/composer.json index 522978b..acd56ba 100644 --- a/composer.json +++ b/composer.json @@ -13,19 +13,19 @@ "config": { "sort-packages": true, "platform": { - "php": "7.0.24" + "php": "7.1.28" } }, "require": { "php": ">=7.0", - "Microsoft/tolerant-php-parser": "0.0.16" + "microsoft/tolerant-php-parser": "0.0.18" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^6.4", + "phan/phan": "^2.3.0" }, "suggest": { - "ext-ast": "~0.1.5||~1.0.0", - "phan/phan": "^1.2.1" + "ext-ast": "~1.0.1" }, "autoload": { "psr-4": {"TolerantASTConverter\\": "src/TolerantASTConverter"} diff --git a/composer.lock b/composer.lock index d73b5b3..205988c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "498dc562d1bd1c641eaaa51dcebc7ce8", + "content-hash": "b73b7b9b0fc7897e204151a06d4ccde2", "packages": [ { "name": "microsoft/tolerant-php-parser", - "version": "v0.0.16", + "version": "v0.0.18", "source": { "type": "git", - "url": "https://github.com/Microsoft/tolerant-php-parser.git", - "reference": "b662587eb797685a98239d1d52d25168a03fdfb2" + "url": "https://github.com/microsoft/tolerant-php-parser.git", + "reference": "e255aa978b45729094da2a1a6f9954044a244ff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Microsoft/tolerant-php-parser/zipball/b662587eb797685a98239d1d52d25168a03fdfb2", - "reference": "b662587eb797685a98239d1d52d25168a03fdfb2", + "url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/e255aa978b45729094da2a1a6f9954044a244ff2", + "reference": "e255aa978b45729094da2a1a6f9954044a244ff2", "shasum": "" }, "require": { @@ -45,38 +45,146 @@ } ], "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", - "time": "2018-12-29T00:31:32+00:00" + "time": "2019-07-01T02:21:00+00:00" } ], "packages-dev": [ + { + "name": "composer/semver", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2019-03-19T17:25:45+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2019-05-27T17:52:04+00:00" + }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "a2c590166b2133a4633738648b6b064edae0814a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -96,34 +204,78 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2019-03-17T17:37:11+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.0.4", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "23366dd0cab0a0f3fd3016bf3c0b36dec74348e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/23366dd0cab0a0f3fd3016bf3c0b36dec74348e7", + "reference": "23366dd0cab0a0f3fd3016bf3c0b36dec74348e7", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0", + "php": ">=7.0", + "phpdocumentor/reflection-docblock": "^4.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "time": "2019-09-12T22:41:08+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.7.0", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { @@ -146,7 +298,125 @@ "object", "object graph" ], - "time": "2017-10-19T19:58:43+00:00" + "time": "2019-08-09T12:45:53+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "0d4d1b48d682a93b6bfedf60b88c7750e9cb0b06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/0d4d1b48d682a93b6bfedf60b88c7750e9cb0b06", + "reference": "0d4d1b48d682a93b6bfedf60b88c7750e9cb0b06", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "time": "2019-08-15T19:41:25+00:00" + }, + { + "name": "phan/phan", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phan/phan.git", + "reference": "cd72de4bde30606a8da3aaec7dd1b1ba365e8627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phan/phan/zipball/cd72de4bde30606a8da3aaec7dd1b1ba365e8627", + "reference": "cd72de4bde30606a8da3aaec7dd1b1ba365e8627", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4", + "composer/xdebug-handler": "^1.3.2", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.4", + "microsoft/tolerant-php-parser": "0.0.18", + "php": "^7.1.0", + "sabre/event": "^5.0", + "symfony/console": "^2.3|^3.0|~4.0", + "symfony/polyfill-mbstring": "^1.11.0" + }, + "require-dev": { + "brianium/paratest": "^2.2.0", + "phpunit/phpunit": "^7.5.0" + }, + "suggest": { + "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed.", + "ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable", + "ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-tokenizer": "Needed for non-AST support and file/line-based suppressions." + }, + "bin": [ + "phan", + "phan_client", + "tocheckstyle" + ], + "type": "project", + "autoload": { + "psr-4": { + "Phan\\": "src/Phan" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + }, + { + "name": "Rasmus Lerdorf" + }, + { + "name": "Andrew S. Morrison" + } + ], + "description": "A static analyzer for PHP", + "keywords": [ + "analyzer", + "php", + "static" + ], + "time": "2019-10-14T02:32:36+00:00" }, { "name": "phar-io/manifest", @@ -252,35 +522,33 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "~6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -302,30 +570,30 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", + "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", "phpunit/phpunit": "^6.4" }, @@ -353,41 +621,40 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" + "time": "2019-09-12T14:27:41+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -400,26 +667,27 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpspec/prophecy", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -434,8 +702,8 @@ } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -463,7 +731,7 @@ "spy", "stub" ], - "time": "2018-08-05T17:53:17+00:00" + "time": "2019-10-03T11:07:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -716,16 +984,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.13", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -796,7 +1064,7 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -855,8 +1123,165 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2018-11-20T15:27:04+00:00" + }, + { + "name": "sabre/event", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "f5cf802d240df1257866d8813282b98aee3bc548" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/f5cf802d240df1257866d8813282b98aee3bc548", + "reference": "f5cf802d240df1257866d8813282b98aee3bc548", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": ">=6", + "sabre/cs": "~1.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "time": "2018-03-05T13:55:47+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.1", @@ -1070,16 +1495,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -1106,6 +1531,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1114,17 +1543,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -1133,7 +1558,7 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/global-state", @@ -1416,18 +1841,93 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "symfony/console", + "version": "v4.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "929ddf360d401b958f611d44e726094ab46a7369" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/929ddf360d401b958f611d44e726094ab46a7369", + "reference": "929ddf360d401b958f611d44e726094ab46a7369", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/service-contracts": "^1.1" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<4.3", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "^4.3", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0", + "symfony/var-dumper": "^4.3" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2019-10-07T12:36:49+00:00" + }, { "name": "symfony/polyfill-ctype", - "version": "v1.10.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "shasum": "" }, "require": { @@ -1439,7 +1939,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -1456,12 +1956,12 @@ ], "authors": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { - "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for ctype functions", @@ -1472,20 +1972,195 @@ "polyfill", "portable" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188", + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffcde9615dc5bb4825b9f6aed07716f1f57faae0", + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-09-17T11:12:18+00:00" }, { "name": "theseer/tokenizer", - "version": "1.1.0", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { @@ -1512,20 +2187,20 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" + "time": "2019-06-13T22:48:21+00:00" }, { "name": "webmozart/assert", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", "shasum": "" }, "require": { @@ -1533,8 +2208,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", "extra": { @@ -1563,7 +2237,7 @@ "check", "validate" ], - "time": "2018-12-25T11:19:39+00:00" + "time": "2019-08-24T08:43:50+00:00" } ], "aliases": [], @@ -1576,6 +2250,6 @@ }, "platform-dev": [], "platform-overrides": { - "php": "7.0.24" + "php": "7.1.28" } } diff --git a/dump.php b/dump.php index ab75cce..9d4657c 100644 --- a/dump.php +++ b/dump.php @@ -21,6 +21,7 @@ * 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 PhanMissingRequireFile one or the other exists */ if (file_exists(__DIR__ . "/../../../vendor/autoload.php")) { @@ -33,8 +34,11 @@ dump_main(); -// Dumps a snippet provided on stdin. -function dump_main() { +/** + * Dumps a snippet provided on stdin. + * @throws Exception + */ +function dump_main() : void { error_reporting(E_ALL); global $argv; @@ -58,12 +62,17 @@ function dump_main() { dump_expr($expr); } -function dump_expr_as_ast(string $expr) { +/** Dumps the ast of the expression to stdout. */ +function dump_expr_as_ast(string $expr) : void { require_once __DIR__ . '/src/util.php'; $ast_data = (new \TolerantASTConverter\TolerantASTConverter())->parseCodeAsPHPAST($expr, 50); echo ast_dump($ast_data, AST_DUMP_LINENOS); } -function dump_expr(string $expr) { +/** + * Dumps the tolerant-php-parser AST to stdout + * @throws Exception + */ +function dump_expr(string $expr) : void { // Instantiate new parser instance $parser = new Parser(); // Return and print an AST from string contents diff --git a/src/TolerantASTConverter/Cache.php b/src/TolerantASTConverter/Cache.php new file mode 100644 index 0000000..0f3dfa1 --- /dev/null +++ b/src/TolerantASTConverter/Cache.php @@ -0,0 +1,26 @@ +indent = $indent; } - /** @return void */ - public function setIncludeOffset(bool $include_offset) + /** + * Should this include the byte offset in the file where the node occurred? + */ + public function setIncludeOffset(bool $include_offset) : void { $this->include_offset = $include_offset; } - /** @return void */ - public function setIncludeTokenKind(bool $include_token_kind) + /** + * Should this include the token kind (default is just the text of the token) + */ + public function setIncludeTokenKind(bool $include_token_kind) : void { $this->include_token_kind = $include_token_kind; } - /** @return void */ - public function setIndent(string $indent) + /** + * Sets the text used for indentation (e.g. 4 spaces) + * @suppress PhanUnreferencedPublicMethod + */ + public function setIndent(string $indent) : void { $this->indent = $indent; } - public function dumpClassName(Node $ast_node) : string + /** + * Converts the class name of $ast_node to a short string describing that class name. + * Removes the common `Microsoft\\PhpParser\\` prefix + */ + public static function dumpClassName(Node $ast_node) : string { $name = get_class($ast_node); - if (stripos($name, 'Microsoft\\PhpParser\\') === 0) { - $name = substr($name, 20); + if (\stripos($name, 'Microsoft\\PhpParser\\') === 0) { + // Remove the PhpParser namespace + $name = (string)substr($name, 20); } return $name; } - public function dumpTokenClassName(Token $ast_node) : string + /** + * Converts the class name of $token to a short string describing that class name. + * Removes the common `Microsoft\\PhpParser\\` prefix + */ + public static function dumpTokenClassName(Token $token) : string { - $name = get_class($ast_node); - if (stripos($name, 'Microsoft\\PhpParser\\') === 0) { - $name = substr($name, 20); + $name = get_class($token); + if (\stripos($name, 'Microsoft\\PhpParser\\') === 0) { + // Remove the PhpParser namespace + $name = (string)substr($name, 20); } return $name; } /** - * @param Node|Token $ast_node + * @param Node|Token|null $ast_node * @param string $padding (to be echoed before the current node - * @return string + * @throws Exception for invalid $ast_node values */ public function dumpTreeAsString($ast_node, string $key = '', string $padding = '') : string { @@ -98,25 +122,25 @@ public function dumpTreeAsString($ast_node, string $key = '', string $padding = "%s%s%s%s\n", $padding, $key !== '' ? $key . ': ' : '', - $this->dumpClassName($ast_node), + self::dumpClassName($ast_node), $this->include_offset ? ' (@' . $ast_node->getStart() . ')' : '' ); - $result = [$first_part]; + $result = $first_part; foreach ($ast_node->getChildNodesAndTokens() as $name => $child) { - $result[] = $this->dumpTreeAsString($child, $name, $padding . $this->indent); + $result .= $this->dumpTreeAsString($child, (string) $name, $padding . $this->indent); } - return \implode('', $result); + return $result; } elseif ($ast_node instanceof Token) { return \sprintf( "%s%s%s: %s%s%s: %s\n", $padding, $key !== '' ? $key . ': ' : '', - $this->dumpTokenClassName($ast_node), + self::dumpTokenClassName($ast_node), $ast_node->getTokenKindNameFromValue($ast_node->kind), $this->include_token_kind ? '(' . $ast_node->kind . ')' : '', $this->include_offset ? ' (@' . $ast_node->start . ')' : '', - \json_encode(\substr($this->file_contents, $ast_node->fullStart, $ast_node->length)) + StringUtil::jsonEncode(\substr($this->file_contents, $ast_node->fullStart, $ast_node->length)) ); } elseif (\is_scalar($ast_node) || $ast_node === null) { return \var_export($ast_node, true); @@ -129,9 +153,10 @@ public function dumpTreeAsString($ast_node, string $key = '', string $padding = /** * @param Node|Token $ast_node * @param string $padding (to be echoed before the current node - * @return void + * @throws Exception for invalid $ast_node values + * @suppress PhanUnreferencedPublicMethod */ - public function dumpTree($ast_node, string $key = '', string $padding = '') + public function dumpTree($ast_node, string $key = '', string $padding = '') : void { echo $this->dumpTreeAsString($ast_node, $key, $padding); } diff --git a/src/TolerantASTConverter/ParseResult.php b/src/TolerantASTConverter/ParseResult.php new file mode 100644 index 0000000..65aeab1 --- /dev/null +++ b/src/TolerantASTConverter/ParseResult.php @@ -0,0 +1,29 @@ +node = $node; + $this->diagnostics = $diagnostics; + } +} diff --git a/src/TolerantASTConverter/StringUtil.php b/src/TolerantASTConverter/StringUtil.php index cd8737b..5af3419 100644 --- a/src/TolerantASTConverter/StringUtil.php +++ b/src/TolerantASTConverter/StringUtil.php @@ -2,10 +2,16 @@ namespace TolerantASTConverter; -use Error; use function chr; use function hexdec; +use function is_string; use function octdec; +use function preg_replace; +use function str_replace; +use function strpos; +use function strrpos; +use function strspn; +use function substr; /** * This class is based on code from https://github.com/nikic/PHP-Parser/blob/master/lib/PhpParser/Node/Scalar/String_.php @@ -64,66 +70,99 @@ final class StringUtil * Parses a string token. * * @param string $str String token content - * @param bool $parse_unicode_escape Whether to parse PHP 7 \u escapes * * @return string The parsed string */ - public static function parse(string $str, bool $parse_unicode_escape = true) : string + public static function parse(string $str) : string { + $c = $str[0]; + if ($c === '<') { + return self::parseHeredoc($str); + } $binary_length = 0; - if ('b' === $str[0] || 'B' === $str[0]) { + if ('b' === $c || 'B' === $c) { $binary_length = 1; } if ('\'' === $str[$binary_length]) { - return \str_replace( + return str_replace( ['\\\\', '\\\''], ['\\', '\''], - \substr($str, $binary_length + 1, -1) + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + substr($str, $binary_length + 1, -1) ); } else { return self::parseEscapeSequences( substr($str, $binary_length + 1, -1), - '"', - $parse_unicode_escape + '"' ); } } /** - * @internal - * + * Converts a fragment of raw (possibly indented) + * heredoc to the string that the PHP interpreter would treat it as. + */ + public static function parseHeredoc(string $str) : string + { + // TODO: handle dos newlines + // TODO: Parse escape sequences + $first_line_index = (int)strpos($str, "\n"); + $last_line_index = (int)strrpos($str, "\n"); + // $last_line = substr($str, $last_line_index + 1); + $spaces = strspn($str, " \t", $last_line_index + 1); + + // On Windows, the "\r" must also be removed from the last line of the heredoc + $inner = (string)substr($str, $first_line_index + 1, $last_line_index - ($first_line_index + 1) - ($str[$last_line_index - 1] === "\r" ? 1 : 0)); + + if ($spaces > 0) { + $inner = preg_replace("/^" . substr($str, $last_line_index + 1, $spaces) . "/m", '', $inner); + } + if (strpos(substr($str, 0, $first_line_index), "'") === false) { + // If the start of the here/nowdoc doesn't contain a "'", it's heredoc. + // The contents have to be unescaped. + return self::parseEscapeSequences($inner, null); + } + return $inner; + } + + /** * Parses escape sequences in strings (all string types apart from single quoted). * - * @param string $str String without quotes + * @param string|false $str String without quotes * @param null|string $quote Quote type - * @param bool $parse_unicode_escape Whether to parse PHP 7 \u escapes * * @return string String with escape sequences parsed + * @throws InvalidNodeException for invalid code points */ - public static function parseEscapeSequences(string $str, $quote, bool $parse_unicode_escape = true) : string + public static function parseEscapeSequences($str, ?string $quote) : string { - if (null !== $quote) { - $str = \str_replace('\\' . $quote, $quote, $str); + if (!is_string($str)) { + // Invalid AST input; give up + return ''; } - - $extra = ''; - if ($parse_unicode_escape) { - $extra = '|u\{([0-9a-fA-F]+)\}'; + if (null !== $quote) { + $str = str_replace('\\' . $quote, $quote, $str); } return \preg_replace_callback( - '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}' . $extra . ')~', - function ($matches) { + '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~', + /** + * @param list $matches + */ + static function (array $matches) : string { $str = $matches[1]; if (isset(self::REPLACEMENTS[$str])) { return self::REPLACEMENTS[$str]; } elseif ('x' === $str[0] || 'X' === $str[0]) { - return chr(hexdec($str)); + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal, PhanPossiblyFalseTypeArgumentInternal + return chr(hexdec(substr($str, 1))); } elseif ('u' === $str[0]) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument return self::codePointToUtf8(hexdec($matches[2])); } else { + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal return chr(octdec($str)); } }, @@ -138,7 +177,7 @@ function ($matches) { * * @return string UTF-8 representation of code point * - * @throws \Error for invalid code points + * @throws InvalidNodeException for invalid code points */ private static function codePointToUtf8(int $num) : string { @@ -155,6 +194,17 @@ private static function codePointToUtf8(int $num) : string return chr(($num >> 18) + 0xF0) . chr((($num >> 12) & 0x3F) + 0x80) . chr((($num >> 6) & 0x3F) + 0x80) . chr(($num & 0x3F) + 0x80); } - throw new Error('Invalid UTF-8 codepoint escape sequence: Codepoint too large'); + throw new InvalidNodeException('Invalid UTF-8 codepoint escape sequence: Codepoint too large'); + } + + /** + * JSON encodes a value - Guaranteed to return a string. + * @param string|int|float|bool|null|array|object $value + * Source: Phan\Library\StringUtil + */ + public static function jsonEncode($value) : string + { + $result = \json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PARTIAL_OUTPUT_ON_ERROR); + return \is_string($result) ? $result : '(invalid data)'; } } diff --git a/src/TolerantASTConverter/TolerantASTConverter.php b/src/TolerantASTConverter/TolerantASTConverter.php index 0e7ea31..5d2de0b 100644 --- a/src/TolerantASTConverter/TolerantASTConverter.php +++ b/src/TolerantASTConverter/TolerantASTConverter.php @@ -5,6 +5,7 @@ use AssertionError; use ast; use ast\flags; +use Closure; use Error; use Exception; use InvalidArgumentException; @@ -13,11 +14,26 @@ 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\Parser; use Microsoft\PhpParser\Token; use Microsoft\PhpParser\TokenKind; 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 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. @@ -45,7 +61,7 @@ * each time they are invoked, * so it's possible to have multiple callers use this without affecting each other. * - * Compatibility: PHP 7.0-7.2 + * Compatibility: PHP 7.0-7.4 * * ---------------------------------------------------------------------------- * @@ -81,12 +97,15 @@ * @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 > 50, update the library's release. - const AST_VERSION = 50; + // For something != 70, update the library's release. + const AST_VERSION = 70; // The versions that this supports // TODO: Also enable support for version 60 once there is a stable 1.0.0 release. (Issue #2038) @@ -126,9 +145,6 @@ class TolerantASTConverter /** @var FilePositionMap maps byte offsets of the currently parsed file to line numbers */ protected static $file_position_map; - /** @var bool if true, force all doc comments to be parsed */ - private static $parse_all_doc_comments = false; - /** @var bool Sets equivalent static option in self::_start_parsing() */ protected $instance_should_add_placeholders = false; @@ -138,56 +154,75 @@ class TolerantASTConverter */ protected $instance_php_version_id_parsing = PHP_VERSION_ID; - /** - * @var bool if true, force all doc comments to be parsed - */ - private $instance_parse_all_doc_comments = false; - // No-op. public function __construct() { } - /** @return void */ - public function setShouldAddPlaceholders(bool $value) + /** + * 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; } - /** @return void */ - public function setPHPVersionId(int $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; } /** - * Parse all doc comments, even the ones the current php version's php-ast would be incapable of providing. - * @return void + * Generates an ast\Node with this converter's current settings. (caching if $cache is non-null) + * + * @param Diagnostic[] &$errors @phan-output-reference + * @param ?Cache $cache + * @throws InvalidArgumentException if the requested AST version is invalid. */ - public function setParseAllDocComments(bool $value) + public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $cache = null) : \ast\Node { - $this->instance_parse_all_doc_comments = $value; + 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 = $cache_key ? $cache->getIfExists($cache_key) : null; + if ($result) { + $errors = $result->diagnostics; + return $result->node; + } + } + $result = $this->parseCodeAsPHPASTUncached($file_contents, $version, $errors); + if ($cache && $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 - * @return ast\Node * @throws InvalidArgumentException if the requested AST version is invalid. */ - public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = []) + public function parseCodeAsPHPASTUncached(string $file_contents, int $version, array &$errors = []) : \ast\Node { - if (!\in_array($version, self::SUPPORTED_AST_VERSIONS)) { - throw new \InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", \implode(', ', self::SUPPORTED_AST_VERSIONS), $version)); - } // Aside: this can be implemented as a stub. $parser_node = static::phpParserParse($file_contents, $errors); return $this->phpParserToPhpast($parser_node, $version, $file_contents); } /** - * @return PhpParser\Node + * @param Diagnostic[] &$errors @phan-output-reference (TODO: param-out) */ - public static function phpParserParse(string $file_contents, array &$errors = null) : PhpParser\Node + public static function phpParserParse(string $file_contents, array &$errors = []) : PhpParser\Node { $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); @@ -195,19 +230,17 @@ public static function phpParserParse(string $file_contents, array &$errors = nu return $result; } - /** * Visible for testing * * @param PhpParser\Node $parser_node * @param int $ast_version * @param string $file_contents - * @return ast\Node * @throws InvalidArgumentException if the provided AST version isn't valid */ - public function phpParserToPhpast(PhpParser\Node $parser_node, int $ast_version, string $file_contents) + public function phpParserToPhpast(PhpParser\Node $parser_node, int $ast_version, string $file_contents) : \ast\Node { - if (!\in_array($ast_version, self::SUPPORTED_AST_VERSIONS)) { + 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); @@ -216,13 +249,11 @@ public function phpParserToPhpast(PhpParser\Node $parser_node, int $ast_version, return $stmts; } - /** @return void */ - protected function startParsing(string $file_contents) + 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::$parse_all_doc_comments = $this->instance_parse_all_doc_comments; self::$file_position_map = new FilePositionMap($file_contents); // $file_contents required for looking up line numbers. // TODO: Other data structures? @@ -259,7 +290,7 @@ protected static function debugDumpNodeOrToken($n) : string * Throws RuntimeException|Exception if the statement list is invalid * @suppress PhanThrowTypeAbsentForCall|PhanThrowTypeMismatchForCall */ - private static function phpParserStmtlistToAstNode($parser_nodes, $lineno, bool $return_null_on_empty = false) + 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; @@ -351,6 +382,7 @@ private static function phpParserExprListToExprList(PhpParser\Node\DelimitedList /** * @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) @@ -365,7 +397,7 @@ protected static function phpParserNonValueNodeToAstNode($n) * @throws InvalidArgumentException for invalid node classes * @throws Error if the environment variable AST_THROW_INVALID is set (for debugging) */ - $fallback_closure = function ($n, int $unused_start_line) { + $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)); @@ -374,13 +406,14 @@ protected static function phpParserNonValueNodeToAstNode($n) }; } $callback = $callback_map[\get_class($n)] ?? $fallback_closure; - // @phan-suppress-next-line PhanThrowTypeMismatch, PhanThrowTypeAbsentForCall + // @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) @@ -411,7 +444,7 @@ protected static function phpParserNodeToAstNode($n) * @throws InvalidArgumentException|Exception for invalid node classes * @throws Error if the environment variable AST_THROW_INVALID is set to debug. */ - $fallback_closure = function ($n, int $unused_start_line) { + $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)); } @@ -463,17 +496,17 @@ final protected static function getEndLine($n) : int * - 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 \Closure[] + * @return array */ protected static function initHandleMap() : array { $closures = [ /** @return ?ast\Node */ - 'Microsoft\PhpParser\Node\SourceFileNode' => function (PhpParser\Node\SourceFileNode $n, int $start_line) { + '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' => function (PhpParser\Node\Expression\ArgumentExpression $n, int $start_line) { + '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); @@ -484,19 +517,24 @@ protected static function initHandleMap() : array * @return ast\Node|string|int|float * @throws InvalidNodeException */ - 'Microsoft\PhpParser\Node\Expression\SubscriptExpression' => function (PhpParser\Node\Expression\SubscriptExpression $n, int $start_line) { + '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, 0, [ + 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); + ], + $start_line + ); } catch (InvalidNodeException $_) { return $expr; } }, /** @return ?ast\Node */ - 'Microsoft\PhpParser\Node\Expression\AssignmentExpression' => function (PhpParser\Node\Expression\AssignmentExpression $n, int $start_line) { + 'Microsoft\PhpParser\Node\Expression\AssignmentExpression' => static function (PhpParser\Node\Expression\AssignmentExpression $n, int $start_line) : ?\ast\Node { try { $var_node = static::phpParserNodeToAstNode($n->leftOperand); } catch (InvalidNodeException $_) { @@ -519,7 +557,7 @@ protected static function initHandleMap() : array /** * @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' => function (PhpParser\Node\Expression\BinaryExpression $n, int $start_line) { + '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, @@ -580,7 +618,7 @@ protected static function initHandleMap() : array } return static::astNodeBinaryop($ast_kind, $n, $start_line); }, - 'Microsoft\PhpParser\Node\Expression\UnaryOpExpression' => function (PhpParser\Node\Expression\UnaryOpExpression $n, int $start_line) : ast\Node { + '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, @@ -599,7 +637,7 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\Expression\CastExpression' => function (PhpParser\Node\Expression\CastExpression $n, int $start_line) : ast\Node { + '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, @@ -637,7 +675,10 @@ protected static function initHandleMap() : array static::getEndLine($n) ?: $start_line ); }, - 'Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression' => function (PhpParser\Node\Expression\AnonymousFunctionCreationExpression $n, int $start_line) : ast\Node { + 'Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression' => static function ( + PhpParser\Node\Expression\AnonymousFunctionCreationExpression $n, + int $start_line + ) : ast\Node { $ast_return_type = static::phpParserTypeToAstNode($n->returnType, static::getEndLine($n->returnType) ?: $start_line); if (($ast_return_type->children['name'] ?? null) === '') { $ast_return_type = null; @@ -650,6 +691,7 @@ protected static function initHandleMap() : array $n->staticModifier !== null, static::phpParserParamsToAstParams($n->parameters, $start_line), static::phpParserClosureUsesToAstClosureUses($n->anonymousFunctionUseClause->useVariableNameList ?? null, $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, @@ -657,11 +699,42 @@ protected static function initHandleMap() : array static::resolveDocCommentForClosure($n) ); }, + 'Microsoft\PhpParser\Node\Expression\ArrowFunctionCreationExpression' => static function ( + PhpParser\Node\Expression\ArrowFunctionCreationExpression $n, + int $start_line + ) : ast\Node { + $ast_return_type = static::phpParserTypeToAstNode($n->returnType, 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() + ); + }, /** - * @return ?ast\Node * @throws InvalidNodeException if the resulting AST would not be analyzable by Phan */ - 'Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression' => function (PhpParser\Node\Expression\ScopedPropertyAccessExpression $n, int $start_line) { + '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 { @@ -700,10 +773,10 @@ protected static function initHandleMap() : array return static::phpParserClassconstfetchToAstClassconstfetch($n->scopeResolutionQualifier, $member_name, $start_line); } }, - 'Microsoft\PhpParser\Node\Expression\CloneExpression' => function (PhpParser\Node\Expression\CloneExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\ErrorControlExpression $n, int $start_line) : ast\Node { + '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, @@ -711,10 +784,10 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\Expression\EmptyIntrinsicExpression' => function (PhpParser\Node\Expression\EmptyIntrinsicExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\EvalIntrinsicExpression $n, int $start_line) : ast\Node { + '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, @@ -723,7 +796,7 @@ protected static function initHandleMap() : array ); }, /** @return string|ast\Node */ - 'Microsoft\PhpParser\Token' => function (PhpParser\Token $token, int $start_line) { + 'Microsoft\PhpParser\Token' => static function (PhpParser\Token $token, int $start_line) { $kind = $token->kind; $str = static::tokenToString($token); if ($kind === TokenKind::StaticKeyword) { @@ -734,21 +807,21 @@ protected static function initHandleMap() : array /** * @throws InvalidNodeException */ - 'Microsoft\PhpParser\MissingToken' => function (PhpParser\MissingToken $unused_node, int $_) { + 'Microsoft\PhpParser\MissingToken' => static function (PhpParser\MissingToken $unused_node, int $_) : void { throw new InvalidNodeException(); }, /** * @throws InvalidNodeException */ - 'Microsoft\PhpParser\SkippedToken' => function (PhpParser\SkippedToken $unused_node, int $_) { + 'Microsoft\PhpParser\SkippedToken' => static function (PhpParser\SkippedToken $unused_node, int $_) : void { throw new InvalidNodeException(); }, - 'Microsoft\PhpParser\Node\Expression\ExitIntrinsicExpression' => function (PhpParser\Node\Expression\ExitIntrinsicExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\CallExpression $n, int $start_line) : ast\Node { + '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() @@ -773,7 +846,7 @@ protected static function initHandleMap() : array ); } }, - 'Microsoft\PhpParser\Node\Expression\ScriptInclusionExpression' => function (PhpParser\Node\Expression\ScriptInclusionExpression $n, int $start_line) : ast\Node { + '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( @@ -786,7 +859,7 @@ protected static function initHandleMap() : array /** * @return ?ast\Node */ - 'Microsoft\PhpParser\Node\Expression\IssetIntrinsicExpression' => function (PhpParser\Node\Expression\IssetIntrinsicExpression $n, int $start_line) { + '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) { @@ -810,26 +883,28 @@ protected static function initHandleMap() : array 'left' => $e, 'right' => $right, ], - $e->lineno + // $e should always be set + $e->lineno ?? 0 ); } return $e; }, - 'Microsoft\PhpParser\Node\Expression\ArrayCreationExpression' => function (PhpParser\Node\Expression\ArrayCreationExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\ObjectCreationExpression $n, int $start_line) : ast\Node { + '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, - $n->classBaseClause !== null ? static::phpParserNonValueNodeToAstNode($n->classBaseClause->baseClass) : null, + $base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null, $n->classInterfaceClause, static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], $start_line, false), $start_line, @@ -845,20 +920,20 @@ protected static function initHandleMap() : array ], $start_line); }, /** @return mixed */ - 'Microsoft\PhpParser\Node\Expression\ParenthesizedExpression' => function (PhpParser\Node\Expression\ParenthesizedExpression $n, int $_) { + 'Microsoft\PhpParser\Node\Expression\ParenthesizedExpression' => static function (PhpParser\Node\Expression\ParenthesizedExpression $n, int $_) { return static::phpParserNodeToAstNode($n->expression); }, - 'Microsoft\PhpParser\Node\Expression\PrefixUpdateExpression' => function (PhpParser\Node\Expression\PrefixUpdateExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\PostfixUpdateExpression $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Expression\PrintIntrinsicExpression $n, int $start_line) : ast\Node { + '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, @@ -867,13 +942,15 @@ protected static function initHandleMap() : array ); }, /** @return ?ast\Node */ - 'Microsoft\PhpParser\Node\Expression\MemberAccessExpression' => function (PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line) { + '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' => function (PhpParser\Node\Expression\TernaryExpression $n, int $start_line) : ast\Node { - return new ast\Node( + '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, - 0, + $is_parenthesized ? ast\flags\PARENTHESIZED_CONDITIONAL : 0, [ 'cond' => static::phpParserNodeToAstNode($n->condition), 'true' => $n->ifExpression !== null ? static::phpParserNodeToAstNode($n->ifExpression) : null, @@ -881,13 +958,19 @@ protected static function initHandleMap() : array ], $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; }, /** * @return ?ast\Node * @throws InvalidNodeException if the variable would be unanalyzable * TODO: Consider ${''} as a placeholder instead? */ - 'Microsoft\PhpParser\Node\Expression\Variable' => function (PhpParser\Node\Expression\Variable $n, int $start_line) { + '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) { @@ -910,12 +993,12 @@ protected static function initHandleMap() : array return new ast\Node(ast\AST_VAR, 0, ['name' => $name_node], $start_line); }, /** - * @return ast\Node + * @return ast\Node|int|float|string */ - 'Microsoft\PhpParser\Node\Expression\BracedExpression' => function (PhpParser\Node\Expression\BracedExpression $n, int $_) { + 'Microsoft\PhpParser\Node\Expression\BracedExpression' => static function (PhpParser\Node\Expression\BracedExpression $n, int $_) { return static::phpParserNodeToAstNode($n->expression); }, - 'Microsoft\PhpParser\Node\Expression\YieldExpression' => function (PhpParser\Node\Expression\YieldExpression $n, int $start_line) : ast\Node { + '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; @@ -941,7 +1024,7 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\ReservedWord' => function (PhpParser\Node\ReservedWord $n, int $start_line) : ast\Node { + '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, @@ -949,7 +1032,7 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\QualifiedName' => function (PhpParser\Node\QualifiedName $n, int $start_line) : ast\Node { + '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]; @@ -977,7 +1060,7 @@ protected static function initHandleMap() : array } return new ast\Node(ast\AST_NAME, $ast_kind, ['name' => $imploded_parts], $start_line); }, - 'Microsoft\PhpParser\Node\Parameter' => function (PhpParser\Node\Parameter $n, int $start_line) : ast\Node { + '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; @@ -992,8 +1075,9 @@ protected static function initHandleMap() : array ); }, /** @return int|float */ - 'Microsoft\PhpParser\Node\NumericLiteral' => function (PhpParser\Node\NumericLiteral $n, int $_) { - $text = static::tokenToString($n->children); + 'Microsoft\PhpParser\Node\NumericLiteral' => static function (PhpParser\Node\NumericLiteral $n, int $_) { + // Support php 7.4 numeric literal separators. Ignore `_`. + $text = \str_replace('_', '', static::tokenToString($n->children)); $as_int = \filter_var($text, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX); if ($as_int !== false) { return $as_int; @@ -1004,7 +1088,7 @@ protected static function initHandleMap() : array * @return ast\Node|string * @throws Exception if the tokens of the string literal are invalid, etc. */ - 'Microsoft\PhpParser\Node\StringLiteral' => function (PhpParser\Node\StringLiteral $n, int $start_line) { + '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); @@ -1022,7 +1106,7 @@ protected static function initHandleMap() : array return $inner_node; }, /** @return mixed - Can return a node or a scalar, depending on the settings */ - 'Microsoft\PhpParser\Node\Statement\CompoundStatementNode' => function (PhpParser\Node\Statement\CompoundStatementNode $n, int $_) { + 'Microsoft\PhpParser\Node\Statement\CompoundStatementNode' => static function (PhpParser\Node\Statement\CompoundStatementNode $n, int $_) { $children = []; foreach ($n->statements as $parser_node) { // @phan-suppress-next-line PhanPossiblyNullTypeArgument @@ -1043,7 +1127,7 @@ protected static function initHandleMap() : array * null if incomplete * int|string for no-op scalar statements like `;2;` */ - 'Microsoft\PhpParser\Node\Statement\ExpressionStatement' => function (PhpParser\Node\Statement\ExpressionStatement $n, int $_) { + '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. @@ -1052,7 +1136,7 @@ protected static function initHandleMap() : array } return static::phpParserNodeToAstNode($n->expression); }, - 'Microsoft\PhpParser\Node\Statement\BreakOrContinueStatement' => function (PhpParser\Node\Statement\BreakOrContinueStatement $n, int $start_line) : ast\Node { + '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) { @@ -1063,7 +1147,7 @@ protected static function initHandleMap() : array } return new ast\Node($kind, 0, ['depth' => $breakout_level], $start_line); }, - 'Microsoft\PhpParser\Node\CatchClause' => function (PhpParser\Node\CatchClause $n, int $start_line) : ast\Node { + '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 @@ -1081,11 +1165,12 @@ protected static function initHandleMap() : array return static::astStmtCatch( $catch_list_node, static::variableTokenToString($n->variableName), - static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, true), + // @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' => function (PhpParser\Node\Statement\InterfaceDeclaration $n, int $start_line) : ast\Node { + '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, @@ -1098,7 +1183,7 @@ protected static function initHandleMap() : array $n->getDocCommentText() ); }, - 'Microsoft\PhpParser\Node\Statement\ClassDeclaration' => function (PhpParser\Node\Statement\ClassDeclaration $n, int $start_line) : ast\Node { + '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( @@ -1112,7 +1197,7 @@ protected static function initHandleMap() : array $n->getDocCommentText() ); }, - 'Microsoft\PhpParser\Node\Statement\TraitDeclaration' => function (PhpParser\Node\Statement\TraitDeclaration $n, int $start_line) : ast\Node { + '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, @@ -1125,18 +1210,18 @@ protected static function initHandleMap() : array $n->getDocCommentText() ); }, - 'Microsoft\PhpParser\Node\ClassConstDeclaration' => function (PhpParser\Node\ClassConstDeclaration $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\MissingMemberDeclaration $unused_n, int $unused_start_line) { + '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; }, /** * @throws InvalidNodeException */ - 'Microsoft\PhpParser\Node\MethodDeclaration' => function (PhpParser\Node\MethodDeclaration $n, int $start_line) : ast\Node { + 'Microsoft\PhpParser\Node\MethodDeclaration' => static function (PhpParser\Node\MethodDeclaration $n, int $start_line) : ast\Node { $statements = $n->compoundStatementOrSemicolon; $ast_return_type = static::phpParserTypeToAstNode($n->returnType, static::getEndLine($n->returnType) ?: $start_line); if (($ast_return_type->children['name'] ?? null) === '') { @@ -1160,7 +1245,6 @@ protected static function initHandleMap() : array static::phpParserVisibilityToAstVisibility($n->modifiers) | ($n->byRefToken !== null ? flags\FUNC_RETURNS_REF : 0), [ 'params' => static::phpParserParamsToAstParams($n->parameters, $start_line), - 'uses' => null, 'stmts' => static::phpParserStmtlistToAstNode($statements, self::getStartLine($statements), true), 'returnType' => $ast_return_type, ], @@ -1171,10 +1255,10 @@ protected static function initHandleMap() : array self::nextDeclId() ); }, - 'Microsoft\PhpParser\Node\Statement\ConstDeclaration' => function (PhpParser\Node\Statement\ConstDeclaration $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Statement\DeclareStatement $n, int $start_line) : ast\Node { + '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)) { @@ -1186,7 +1270,7 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\Statement\DoStatement' => function (PhpParser\Node\Statement\DoStatement $n, int $start_line) : ast\Node { + '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, @@ -1200,7 +1284,7 @@ protected static function initHandleMap() : array /** * @return ast\Node|ast\Node[] */ - 'Microsoft\PhpParser\Node\Expression\EchoExpression' => function (PhpParser\Node\Expression\EchoExpression $n, int $start_line) { + '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) { @@ -1218,14 +1302,14 @@ protected static function initHandleMap() : array /** * @return ?ast\Node */ - 'Microsoft\PhpParser\Node\ForeachKey' => function (PhpParser\Node\ForeachKey $n, int $_) { + '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' => function (PhpParser\Node\Statement\ForeachStatement $n, int $start_line) : ast\Node { + '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) { @@ -1250,13 +1334,14 @@ protected static function initHandleMap() : array ); //return static::phpParserStmtlistToAstNode($n->statements, $start_line); }, - 'Microsoft\PhpParser\Node\FinallyClause' => function (PhpParser\Node\FinallyClause $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Statement\FunctionDeclaration $n, int $start_line) : ast\Node { + '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::phpParserTypeToAstNode($n->returnType, static::getEndLine($n->returnType) ?: $start_line); if (($ast_return_type->children['name'] ?? null) === '') { @@ -1282,7 +1367,7 @@ protected static function initHandleMap() : array ); }, /** @return ast\Node|ast\Node[] */ - 'Microsoft\PhpParser\Node\Statement\GlobalDeclaration' => function (PhpParser\Node\Statement\GlobalDeclaration $n, int $start_line) { + '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) { @@ -1292,11 +1377,11 @@ protected static function initHandleMap() : array } return \count($global_nodes) === 1 ? $global_nodes[0] : $global_nodes; }, - 'Microsoft\PhpParser\Node\Statement\IfStatementNode' => function (PhpParser\Node\Statement\IfStatementNode $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Statement\InlineHtml $n, int $start_line) { + '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 @@ -1309,7 +1394,7 @@ protected static function initHandleMap() : array ); }, /** @suppress PhanTypeMismatchArgument TODO: Make ForStatement have more accurate docs? */ - 'Microsoft\PhpParser\Node\Statement\ForStatement' => function (PhpParser\Node\Statement\ForStatement $n, int $start_line) : ast\Node { + '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, @@ -1323,7 +1408,7 @@ protected static function initHandleMap() : array ); }, /** @return ast\Node[] */ - 'Microsoft\PhpParser\Node\Statement\NamespaceUseDeclaration' => function (PhpParser\Node\Statement\NamespaceUseDeclaration $n, int $start_line) { + '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; @@ -1335,7 +1420,7 @@ protected static function initHandleMap() : array } return $results; }, - 'Microsoft\PhpParser\Node\Statement\NamespaceDefinition' => function (PhpParser\Node\Statement\NamespaceDefinition $n, int $start_line) : ast\Node { + '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) { @@ -1355,20 +1440,21 @@ protected static function initHandleMap() : array $start_line ); }, - 'Microsoft\PhpParser\Node\Statement\EmptyStatement' => function (PhpParser\Node\Statement\EmptyStatement $unused_node, int $unused_start_line) : array { + /** @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' => function (PhpParser\Node\PropertyDeclaration $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Statement\ReturnStatement $n, int $start_line) : ast\Node { + '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' => function (PhpParser\Node\Statement\FunctionStaticDeclaration $n, int $start_line) { + '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) { @@ -1387,10 +1473,10 @@ protected static function initHandleMap() : array } return \count($static_nodes) === 1 ? $static_nodes[0] : $static_nodes; }, - 'Microsoft\PhpParser\Node\Statement\SwitchStatementNode' => function (PhpParser\Node\Statement\SwitchStatementNode $n, int $start_line) : ast\Node { - return static::phpParserSwitchListToAstSwitch($n, $start_line); + 'Microsoft\PhpParser\Node\Statement\SwitchStatementNode' => static function (PhpParser\Node\Statement\SwitchStatementNode $n, int $_) : ast\Node { + return static::phpParserSwitchListToAstSwitch($n); }, - 'Microsoft\PhpParser\Node\Statement\ThrowStatement' => function (PhpParser\Node\Statement\ThrowStatement $n, int $start_line) : ast\Node { + 'Microsoft\PhpParser\Node\Statement\ThrowStatement' => static function (PhpParser\Node\Statement\ThrowStatement $n, int $start_line) : ast\Node { return new ast\Node( ast\AST_THROW, 0, @@ -1399,7 +1485,7 @@ protected static function initHandleMap() : array ); }, - 'Microsoft\PhpParser\Node\TraitUseClause' => function (PhpParser\Node\TraitUseClause $n, int $start_line) : ast\Node { + '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 = []; @@ -1433,11 +1519,14 @@ protected static function initHandleMap() : array /** * @return ?ast\Node */ - 'Microsoft\PhpParser\Node\TraitSelectOrAliasClause' => function (PhpParser\Node\TraitSelectOrAliasClause $n, int $start_line) { + '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) { - $member_name_list = $name->memberName ?? null; + if (!$name instanceof ScopedPropertyAccessExpression) { + return null; + } + $member_name_list = $name->memberName; if ($member_name_list === null) { return null; } @@ -1485,9 +1574,10 @@ protected static function initHandleMap() : array return new ast\Node(ast\AST_TRAIT_ALIAS, $flags, $children, $start_line); } }, - 'Microsoft\PhpParser\Node\Statement\TryStatement' => function (PhpParser\Node\Statement\TryStatement $n, int $start_line) : ast\Node { + '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, @@ -1495,7 +1585,7 @@ protected static function initHandleMap() : array ); }, /** @return ast\Node|ast\Node[] */ - 'Microsoft\PhpParser\Node\Expression\UnsetIntrinsicExpression' => function (PhpParser\Node\Expression\UnsetIntrinsicExpression $n, int $start_line) { + '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) { @@ -1506,18 +1596,19 @@ protected static function initHandleMap() : array } return \count($stmts) === 1 ? $stmts[0] : $stmts; }, - 'Microsoft\PhpParser\Node\Statement\WhileStatement' => function (PhpParser\Node\Statement\WhileStatement $n, int $start_line) : ast\Node { + 'Microsoft\PhpParser\Node\Statement\WhileStatement' => static function (PhpParser\Node\Statement\WhileStatement $n, int $start_line) : ast\Node { return static::astNodeWhile( static::phpParserNodeToAstNode($n->expression), - static::phpParserStmtlistToAstNode($n->statements, $start_line, true), + // @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' => function (PhpParser\Node\Statement\GotoStatement $n, int $start_line) : ast\Node { + '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[] */ - 'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line) { + 'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => static function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line) : array { $label = new ast\Node(ast\AST_LABEL, 0, ['name' => static::tokenToString($n->name)], $start_line); $statement = static::phpParserNodeToAstNode($n->statement); return [$label, $statement]; @@ -1538,11 +1629,9 @@ protected static function initHandleMap() : array * @param PhpParser\Node\NamespaceUseClause $use_clause * @param ?int $parser_use_kind * @param int $start_line - * @return ast\Node - * * @throws InvalidNodeException */ - protected static function astStmtUseOrGroupUseFromUseClause(PhpParser\Node\NamespaceUseClause $use_clause, $parser_use_kind, int $start_line) : ast\Node + 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) { @@ -1564,15 +1653,10 @@ protected static function astStmtUseOrGroupUseFromUseClause(PhpParser\Node\Names } } - /** - * @param ast\Node $try_node - * @param ?ast\Node $catches_node - * @param ?ast\Node $finally_node - */ private static function astNodeTry( - $try_node, - $catches_node, - $finally_node, + \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 @@ -1586,10 +1670,7 @@ private static function astNodeTry( return new ast\Node(ast\AST_TRY, 0, $children, $start_line); } - /** - * @param ast\Node $stmts - */ - private static function astStmtCatch(ast\Node $types, string $var, $stmts, int $lineno) : ast\Node + private static function astStmtCatch(ast\Node $types, string $var, \ast\Node $stmts, int $lineno) : ast\Node { return new ast\Node( ast\AST_CATCH, @@ -1615,6 +1696,9 @@ private static function phpParserCatchlistToAstCatchlist(array $catches, int $li return new ast\Node(ast\AST_CATCH_LIST, 0, $children, $children[0]->lineno ?? $lineno); } + /** + * @param list $types + */ private static function phpParserNameListToAstNameList(array $types, int $line) : ast\Node { $ast_types = []; @@ -1681,9 +1765,8 @@ private static function phpParserIncludeTokenToAstIncludeFlags(Token $type) : in /** * @param PhpParser\Node\QualifiedName|Token|null $type - * @return ?ast\Node */ - protected static function phpParserTypeToAstNode($type, int $line) + protected static function phpParserTypeToAstNode($type, int $line) : ?\ast\Node { if (\is_null($type)) { return null; @@ -1757,7 +1840,7 @@ protected static function phpParserTypeToAstNode($type, int $line) * @param string $name * @param ?ast\Node|?int|?string|?float $default */ - private static function astNodeParam(bool $is_nullable, bool $by_ref, bool $variadic, $type, $name, $default, int $line) : ast\Node + 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( @@ -1779,8 +1862,7 @@ private static function astNodeParam(bool $is_nullable, bool $by_ref, bool $vari ); } - /** @param ?PhpParser\Node\DelimitedList\ParameterDeclarationList $parser_params */ - private static function phpParserParamsToAstParams($parser_params, int $line) : ast\Node + private static function phpParserParamsToAstParams(?\Microsoft\PhpParser\node\delimitedlist\parameterdeclarationlist $parser_params, int $line) : ast\Node { $new_params = []; foreach ($parser_params->children ?? [] as $parser_node) { @@ -1817,20 +1899,16 @@ protected static function astStub($parser_node) : ast\Node return $node; } - /** - * @param ?PhpParser\Node\DelimitedList\UseVariableNameList $uses - * @param int $line - * @return ?ast\Node - */ private static function phpParserClosureUsesToAstClosureUses( - $uses, + ?\Microsoft\PhpParser\Node\DelimitedList\UseVariableNameList $uses, int $line - ) { - if (count($uses->children ?? []) === 0) { + ) : ?\ast\Node { + $children = $uses->children ?? []; + if (count($children) === 0) { return null; } $ast_uses = []; - foreach ($uses->children as $use) { + foreach ($children as $use) { if ($use instanceof Token) { continue; } @@ -1842,10 +1920,7 @@ private static function phpParserClosureUsesToAstClosureUses( return new ast\Node(ast\AST_CLOSURE_USES, 0, $ast_uses, $ast_uses[0]->lineno ?? $line); } - /** - * @return ?string - */ - private static function resolveDocCommentForClosure(PhpParser\Node\Expression\AnonymousFunctionCreationExpression $node) + private static function resolveDocCommentForClosure(PhpParser\Node\Expression $node) : ?string { $doc_comment = $node->getDocCommentText(); if ($doc_comment) { @@ -1865,6 +1940,7 @@ private static function resolveDocCommentForClosure(PhpParser\Node\Expression\An 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 } @@ -1922,21 +1998,16 @@ private static function resolveDocCommentForClosure(PhpParser\Node\Expression\An return null; } - /** - * @param ?ast\Node $uses - * @param ?ast\Node $return_type - * @param ?string $doc_comment - */ private static function astDeclClosure( bool $by_ref, bool $static, ast\Node $params, - $uses, + ?\ast\Node $uses, ast\Node $stmts, - $return_type, + ?\ast\Node $return_type, int $start_line, int $end_line, - $doc_comment + ?string $doc_comment ) : ast\Node { return static::newAstDecl( ast\AST_CLOSURE, @@ -1964,18 +2035,17 @@ private static function astDeclFunction( bool $by_ref, string $name, ast\Node $params, - $return_type, - $stmts, + ?\ast\Node $return_type, + ?\ast\Node $stmts, int $line, int $end_line, - $doc_comment + ?string $doc_comment ) : ast\Node { return static::newAstDecl( ast\AST_FUNC_DECL, $by_ref ? flags\FUNC_RETURNS_REF : 0, [ 'params' => $params, - 'uses' => null, 'stmts' => $stmts, 'returnType' => $return_type, ], @@ -1991,7 +2061,7 @@ private static function astDeclFunction( * @param ?Token $flags * @throws InvalidArgumentException if the class flags were unexpected */ - private static function phpParserClassModifierToAstClassFlags($flags) : int + private static function phpParserClassModifierToAstClassFlags(?Token $flags) : int { if ($flags === null) { return 0; @@ -2006,11 +2076,7 @@ private static function phpParserClassModifierToAstClassFlags($flags) : int } } - /** - * @param ?PhpParser\Node\InterfaceBaseClause $node - * @return ?ast\Node - */ - private static function interfaceBaseClauseToNode($node) + private static function interfaceBaseClauseToNode(?\Microsoft\PhpParser\Node\InterfaceBaseClause $node) : ?\ast\Node { if (!$node instanceof PhpParser\Node\InterfaceBaseClause) { // TODO: real placeholder? @@ -2022,7 +2088,11 @@ private static function interfaceBaseClauseToNode($node) if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) { continue; } - $interface_extends_name_list[] = static::phpParserNonValueNodeToAstNode($implement); + $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; @@ -2030,25 +2100,15 @@ private static function interfaceBaseClauseToNode($node) return new ast\Node(ast\AST_NAME_LIST, 0, $interface_extends_name_list, $interface_extends_name_list[0]->lineno); } - /** - * @param int $flags - * @param ?string $name - * @param ?ast\Node $extends - * @param ?PhpParser\Node\ClassInterfaceClause $implements - * @param ?ast\Node $stmts - * @param int $line - * @param int $end_line - * @param ?string $doc_comment - */ private static function astStmtClass( int $flags, - $name, - $extends, - $implements, - $stmts, + ?string $name, + ?\ast\Node $extends, + ?\Microsoft\PhpParser\node\classinterfaceclause $implements, + ?\ast\Node $stmts, int $line, int $end_line, - $doc_comment + ?string $doc_comment ) : ast\Node { // NOTE: `null` would be an anonymous class. @@ -2072,7 +2132,11 @@ private static function astStmtClass( if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) { continue; } - $ast_implements_inner[] = static::phpParserNonValueNodeToAstNode($implement); + $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); @@ -2101,10 +2165,7 @@ private static function astStmtClass( ); } - /** - * @param ?PhpParser\Node\DelimitedList\ArgumentExpressionList $args - */ - private static function phpParserArgListToAstArgList($args, int $line) : ast\Node + private static function phpParserArgListToAstArgList(?\Microsoft\PhpParser\node\delimitedlist\argumentexpressionlist $args, int $line) : ast\Node { $ast_args = []; foreach ($args->children ?? [] as $arg) { @@ -2120,7 +2181,7 @@ private static function phpParserArgListToAstArgList($args, int $line) : ast\Nod * @param ?int $kind * @throws InvalidArgumentException if the token kind was somehow invalid */ - private static function phpParserNamespaceUseKindToASTUseFlags($kind) : int + private static function phpParserNamespaceUseKindToASTUseFlags(?int $kind) : int { switch ($kind ?? 0) { case TokenKind::FunctionKeyword: @@ -2169,12 +2230,7 @@ private static function phpParserNamespaceUseListToAstUseList(array $uses) : arr return $ast_uses; } - /** - * @param ?int $type - * @param ?string $alias - * @return ast\Node - */ - private static function astStmtUse($type, string $name, $alias, int $line) : ast\Node + 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( @@ -2188,8 +2244,10 @@ private static function astStmtUse($type, string $name, $alias, int $line) : ast /** * @param ?int $type * @param ?string $prefix + * @param list $uses + * @suppress PhanPossiblyUndeclaredProperty $use should always be a node */ - private static function astStmtGroupUse($type, $prefix, array $uses, int $line) : ast\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); @@ -2224,15 +2282,15 @@ private static function astStmtGroupUse($type, $prefix, array $uses, int $line) * @param ast\Node $stmts * @param int $line */ - private static function astIfElem($cond, $stmts, int $line) : ast\Node + 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, int $start_line) : ast\Node + private static function phpParserSwitchListToAstSwitch(PhpParser\Node\Statement\SwitchStatementNode $node) : ast\Node { $stmts = []; - $node_line = static::getEndLine($node) ?? $start_line; + $node_line = static::getStartLine($node); foreach ($node->caseStatements as $case) { if (!($case instanceof PhpParser\Node\CaseStatementNode)) { continue; @@ -2245,7 +2303,7 @@ private static function phpParserSwitchListToAstSwitch(PhpParser\Node\Statement\ 'cond' => $case->expression !== null ? static::phpParserNodeToAstNode($case->expression) : null, 'stmts' => static::phpParserStmtlistToAstNode($case->statementList, $case_line, false), ], - $case_line ?? $node_line + $case_line ); } return new ast\Node(ast\AST_SWITCH, 0, [ @@ -2269,10 +2327,11 @@ private static function phpParserIfStmtToAstIfStmt(PhpParser\Node\Statement\IfSt { $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, - true + false ), $start_line ); @@ -2281,6 +2340,7 @@ private static function phpParserIfStmtToAstIfStmt(PhpParser\Node\Statement\IfSt $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) @@ -2294,7 +2354,8 @@ private static function phpParserIfStmtToAstIfStmt(PhpParser\Node\Statement\IfSt $parser_else_line = self::getStartLineOfStatementOrStatements($parser_else_node->statements); $if_elems[] = static::astIfElem( null, - static::phpParserStmtlistToAstNode($parser_else_node->statements, $parser_else_line), + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode($parser_else_node->statements, $parser_else_line, false), $parser_else_line ); } @@ -2374,7 +2435,7 @@ private static function astNodeAssignop(int $flags, PhpParser\Node\Expression\Bi * @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, $doc_comment) : ast\Node + private static function phpParserPropelemToAstPropelem($n, ?string $doc_comment) : ast\Node { if ($n instanceof PhpParser\Node\Expression\AssignmentExpression) { $name_node = $n->leftOperand; @@ -2405,10 +2466,7 @@ private static function phpParserPropelemToAstPropelem($n, $doc_comment) : ast\N return new ast\Node(ast\AST_PROP_ELEM, 0, $children, $start_line); } - /** - * @param ?string $doc_comment - */ - private static function phpParserConstelemToAstConstelem(PhpParser\Node\ConstElement $n, $doc_comment) : ast\Node + private static function phpParserConstelemToAstConstelem(PhpParser\Node\ConstElement $n, ?string $doc_comment) : ast\Node { $start_line = self::getStartLine($n); $children = [ @@ -2416,9 +2474,7 @@ private static function phpParserConstelemToAstConstelem(PhpParser\Node\ConstEle 'value' => static::phpParserNodeToAstNode($n->assignment), ]; - if (self::$php_version_id_parsing >= 70100 || self::$parse_all_doc_comments) { - $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment; - } + $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment; return new ast\Node(ast\AST_CONST_ELEM, 0, $children, $start_line); } @@ -2476,7 +2532,13 @@ private static function phpParserPropertyToAstNode(PhpParser\Node\PropertyDeclar } $flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false); - return new ast\Node(ast\AST_PROP_DECL, $flags, $prop_elems, $prop_elems[0]->lineno ?? (self::getStartLine($n) ?: $start_line)); + $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::phpParserTypeToAstNode($n->typeDeclaration, $type_line), + 'props' => $prop_decl, + ], $line); } private static function phpParserClassConstToAstNode(PhpParser\Node\ClassConstDeclaration $n, int $start_line) : ast\Node @@ -2515,10 +2577,7 @@ private static function phpParserConstToAstNode(PhpParser\Node\Statement\ConstDe return new ast\Node(ast\AST_CONST_DECL, 0, $const_elems, $const_elems[0]->lineno ?? $start_line); } - /** - * @param ?string $first_doc_comment - */ - private static function phpParserDeclareListToAstDeclares(PhpParser\Node\DeclareDirective $declare, int $start_line, $first_doc_comment) : ast\Node + 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) { @@ -2529,19 +2588,14 @@ private static function phpParserDeclareListToAstDeclares(PhpParser\Node\Declare ]; $doc_comment = static::extractPhpdocComment($declare) ?? $first_doc_comment; // $first_doc_comment = null; - if (self::$php_version_id_parsing >= 70100 || self::$parse_all_doc_comments) { - $children['docComment'] = $doc_comment; - } + $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); } - /** - * @param ?ast\Node $stmts - */ - private static function astStmtDeclare(ast\Node $declares, $stmts, int $start_line) : ast\Node + private static function astStmtDeclare(ast\Node $declares, ?\ast\Node $stmts, int $start_line) : ast\Node { $children = [ 'declares' => $declares, @@ -2554,7 +2608,7 @@ private static function astStmtDeclare(ast\Node $declares, $stmts, int $start_li * @param string|ast\Node $expr * @param ast\Node $args */ - private static function astNodeCall($expr, $args, int $start_line) : ast\Node + private static function astNodeCall($expr, \ast\Node $args, int $start_line) : ast\Node { if (\is_string($expr)) { if (substr($expr, 0, 1) === '\\') { @@ -2566,8 +2620,8 @@ private static function astNodeCall($expr, $args, int $start_line) : ast\Node } /** - * @param ast\Node $expr - * @param ast\Node $method + * @param ast\Node|string $expr (can parse non-nodes, but they'd cause runtime errors) + * @param ast\Node|string $method */ private static function astNodeMethodCall($expr, $method, ast\Node $args, int $start_line) : ast\Node { @@ -2576,7 +2630,7 @@ private static function astNodeMethodCall($expr, $method, ast\Node $args, int $s /** * @param ast\Node|string $class - * @param ast\Node $method + * @param ast\Node|string $method */ private static function astNodeStaticCall($class, $method, ast\Node $args, int $start_line) : ast\Node { @@ -2595,7 +2649,7 @@ private static function astNodeStaticCall($class, $method, ast\Node $args, int $ * @param string|PhpParser\Node|null|array $comments * @return ?string the doc comment, or null */ - private static function extractPhpdocComment($comments) + private static function extractPhpdocComment($comments) : ?string { if (\is_string($comments)) { return $comments; @@ -2604,6 +2658,9 @@ private static function extractPhpdocComment($comments) // 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; } @@ -2613,6 +2670,7 @@ private static function extractPhpdocComment($comments) if (\count($comments) === 0) { return null; } + */ } private static function phpParserListToAstList(PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line) : ast\Node @@ -2663,6 +2721,12 @@ private static function phpParserArrayToAstArray(PhpParser\Node\Expression\Array 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\PARAM_REF : 0; $element_key = $item->elementKey; $ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, $flags, [ @@ -2690,7 +2754,7 @@ private static function phpParserArrayToAstArray(PhpParser\Node\Expression\Array * * (and various other exceptions) */ - private static function phpParserMemberAccessExpressionToAstProp(PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line) + private static function phpParserMemberAccessExpressionToAstProp(PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line) : ?\ast\Node { // TODO: Check for incomplete tokens? $member_name = $n->memberName; @@ -2784,6 +2848,11 @@ private static function tokenToString(Token $n) : string */ private static function phpParserClassconstfetchToAstClassconstfetch($scope_resolution_qualifier, string $name, int $start_line) : ast\Node { + if (\strcasecmp($name, 'class') === 0) { + return new ast\Node(ast\AST_CLASS_NAME, 0, [ + 'class' => static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier), + ], $start_line); + } return new ast\Node(ast\AST_CLASS_CONST, 0, [ 'class' => static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier), 'const' => $name, @@ -2791,7 +2860,6 @@ private static function phpParserClassconstfetchToAstClassconstfetch($scope_reso } /** - * @return string * @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 @@ -2815,9 +2883,7 @@ private static function phpParserNameToString(PhpParser\Node\QualifiedName $name } /** - * NOTE: this may be removed in the future. - * - * @return ast\Node + * @param array $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 { @@ -2829,9 +2895,7 @@ private static function newAstDecl(int $kind, int $flags, array $children, int $ $decl_children['__declId'] = $decl_id; } $node = new ast\Node($kind, $flags, $decl_children, $lineno); - if (\is_int($end_lineno)) { - $node->endLineno = $end_lineno; - } + $node->endLineno = $end_lineno; return $node; } @@ -2938,5 +3002,70 @@ private static function parseMultiPartHeredoc(PhpParser\Node\StringLiteral $n, a 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'), + '2.3.1-dev-standalone' + ], 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 + */ + private static function getDevelopmentBuildDate() : ?string + { + if (\stripos(\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); diff --git a/src/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php b/src/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php index 7d787ad..b1d3ec5 100644 --- a/src/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php +++ b/src/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php @@ -11,7 +11,7 @@ use Microsoft\PhpParser\Token; use Microsoft\PhpParser\TokenKind; use Throwable; - +use function is_string; use function preg_match; /** @@ -52,11 +52,17 @@ class TolerantASTConverterWithNodeMapping extends TolerantASTConverter /** * @var ?Token - * TODO: If this is null, then just use TolerantASTConverter's node generation logic to be a bit faster + * This is the closest node or token from tolerant-php-parser + * (among the nodes being parsed **that will have a corresponding ast\Node be created**) + * + * (duplicated to be accessed by static methods, for performance) */ private static $closest_node_or_token_symbol; - /** @var int the byte offset we are looking for, to mark the corresponding Node as within the selected location */ + /** + * @var int the byte offset we are looking for, to mark the corresponding Node as within the selected location. + * (duplicated to be accessed by static methods, for performance) + */ private static $desired_byte_offset; /** @var int the byte offset we are looking for, to mark the corresponding Node as within the selected location */ @@ -65,6 +71,8 @@ class TolerantASTConverterWithNodeMapping extends TolerantASTConverter /** * @var ?Closure(ast\Node):void This is optional. If it is set, this is invoked on the Node we marked. * Currently, this is used to add plugin methods at runtime (limited to what is needed to handle that node's kind) + * + * (duplicated to be accessed by static methods, for performance) */ private static $handle_selected_node; @@ -87,21 +95,18 @@ public function __construct(int $desired_byte_offset, Closure $handle_selected_n /** * @param Diagnostic[] &$errors @phan-output-reference - * - * @return \ast\Node - * * @throws InvalidArgumentException for invalid $version * @throws Throwable (after logging) if anything is thrown by the parser */ - public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = []) + public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $unused_cache = null) : \ast\Node { // Force the byte offset to be within the $byte_offset = \max(0, \min(\strlen($file_contents), $this->instance_desired_byte_offset)); self::$desired_byte_offset = $byte_offset; self::$handle_selected_node = $this->instance_handle_selected_node; - if (!\in_array($version, self::SUPPORTED_AST_VERSIONS)) { - throw new InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", \implode(', ', self::SUPPORTED_AST_VERSIONS), $version)); + 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)); } // Aside: this can be implemented as a stub. @@ -120,14 +125,20 @@ public function parseCodeAsPHPAST(string $file_contents, int $version, array &$e } } + /** + * @return ?string - null if this should not be cached + */ + public function generateCacheKey(string $unused_file_contents, int $unused_version) : ?string + { + return null; + } + /** * Records the closest node or token to the given offset. * Heuristics are used to ensure that this can map to an ast\Node. * TODO: Finish implementing - * - * @return void */ - private static function findNodeAtOffset(PhpParser\Node $parser_node, int $offset) + private static function findNodeAtOffset(PhpParser\Node $parser_node, int $offset) : void { self::$closest_node_or_token = null; self::$closest_node_or_token_symbol = null; @@ -149,7 +160,7 @@ private static function findNodeAtOffset(PhpParser\Node $parser_node, int $offse * @param PhpParser\Node $parser_node * @return bool|PhpParser\Node|PhpParser\Token (Returns $parser_node if that node was what the cursor is pointing directly to) */ - private static function findNodeAtOffsetRecursive($parser_node, int $offset) + private static function findNodeAtOffsetRecursive(\Microsoft\PhpParser\Node $parser_node, int $offset) { foreach ($parser_node->getChildNodesAndTokens() as $key => $node_or_token) { if ($node_or_token instanceof Token) { @@ -256,7 +267,7 @@ protected static function phpParserNodeToAstNodeOrPlaceholderExpr($n) * @param PhpParser\Node|Token $n @phan-unused-param the tolerant-php-parser node that generated the $ast_node * @param mixed $ast_node the node that was selected because it was under the cursor */ - private static function markNodeAsSelected($n, $ast_node) + private static function markNodeAsSelected($n, $ast_node) : void { // fwrite(STDERR, "Marking corresponding node as flagged: " . json_encode($n) . "\n" . \Phan\Debug::nodeToString($ast_node) . "\n"); // fflush(STDERR); @@ -299,7 +310,7 @@ private static function markNodeAsSelected($n, $ast_node) * TODO: Support variables? * TODO: Implement support for going to function definitions if no class could be found */ - private static function extractFragmentFromCommentLike() + private static function extractFragmentFromCommentLike() : ?string { $offset = self::$desired_byte_offset; $contents = self::$file_contents; @@ -343,7 +354,7 @@ protected static function phpParserNodeToAstNode($n) * @throws InvalidArgumentException for invalid token classes * @suppress PhanThrowTypeMismatchForCall can throw if debugDumpNodeOrToken fails */ - $fallback_closure = function ($n, int $unused_start_line) : ast\Node { + $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)); } @@ -382,7 +393,7 @@ protected static function phpParserNonValueNodeToAstNode($n) * @param PhpParser\Node|Token $n * @throws InvalidArgumentException for invalid token classes */ - $fallback_closure = function ($n, int $unused_start_line) : ast\Node { + $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)); @@ -403,7 +414,7 @@ protected static function phpParserNonValueNodeToAstNode($n) */ protected static function astStmtUseOrGroupUseFromUseClause( PhpParser\Node\NamespaceUseClause $use_clause, - $parser_use_kind, + ?int $parser_use_kind, int $start_line ) : ast\Node { // fwrite(STDERR, "Calling astStmtUseOrGroupUseFromUseClause for " . json_encode($use_clause) . "\n"); @@ -419,10 +430,9 @@ protected static function astStmtUseOrGroupUseFromUseClause( /** * @param PhpParser\Node\QualifiedName|Token|null $type - * @return ?ast\Node * @override */ - protected static function phpParserTypeToAstNode($type, int $line) + protected static function phpParserTypeToAstNode($type, int $line) : ?\ast\Node { $ast_node = parent::phpParserTypeToAstNode($type, $line); if ($type === self::$closest_node_or_token && $type !== null) { diff --git a/src/ast_shim.php b/src/ast_shim.php index 010e2cb..2bed1f3 100644 --- a/src/ast_shim.php +++ b/src/ast_shim.php @@ -10,7 +10,7 @@ * With modifications to be a functional replacement for the data * structures and global constants of ext-ast. (for class ast\Node) * - * This supports AST version 50 + * This supports AST version 70 * * However, this file does not define any global functions such as * ast\parse_code() and ast\parse_file(). (to avoid confusion) @@ -18,7 +18,8 @@ * TODO: Make it so that constant values will be identical to php-ast * for PHP 7.0-7.3 * - * @phan-file-suppress PhanUnreferencedConstant - Plugins may reference some of these constants + * @phan-file-suppress PhanUnreferencedConstant, UnusedPluginFileSuppression - Plugins may reference some of these constants + * @phan-file-suppress PhanPluginUnknownArrayPropertyType, PhanPluginUnknownArrayMethodParamType this is a stub * * @author Tyson Andre */ @@ -27,7 +28,6 @@ namespace ast; const AST_ARG_LIST = 128; -const AST_LIST = 255; const AST_ARRAY = 129; const AST_ENCAPS_LIST = 130; const AST_EXPR_LIST = 131; @@ -50,6 +50,7 @@ const AST_CLOSURE = 67; const AST_METHOD = 68; const AST_CLASS = 69; +const AST_ARROW_FUNC = 71; const AST_MAGIC_CONST = 0; const AST_TYPE = 1; const AST_VAR = 256; @@ -80,6 +81,7 @@ const AST_GOTO = 284; const AST_BREAK = 285; const AST_CONTINUE = 286; +const AST_CLASS_NAME = 287; const AST_DIM = 512; const AST_PROP = 513; const AST_STATIC_PROP = 514; @@ -109,6 +111,7 @@ const AST_USE_ELEM = 541; const AST_TRAIT_ALIAS = 542; const AST_GROUP_USE = 543; +const AST_PROP_GROUP = 545; const AST_METHOD_CALL = 768; const AST_STATIC_CALL = 769; const AST_CONDITIONAL = 770; @@ -202,6 +205,8 @@ const ARRAY_SYNTAX_LIST = 1; const ARRAY_SYNTAX_LONG = 2; const ARRAY_SYNTAX_SHORT = 3; +const DIM_ALTERNATIVE_SYNTAX = 2; +const PARENTHESIZED_CONDITIONAL = 1; // END AST FLAG CONSTANTS namespace ast; @@ -209,7 +214,7 @@ // The parse_file(), parse_code(), get_kind_name(), and kind_uses_flags() are deliberately omitted from this stub. // Use Phan\Debug and Phan\AST\Parser instead. -if (!class_exists('\ast\Node')) { +if (!\class_exists('\ast\Node')) { /** * This class describes a single node in a PHP AST. * @suppress PhanRedefineClassInternal @@ -234,7 +239,7 @@ class Node /** * A constructor which validates data types but not the values themselves. * For backwards compatibility reasons, all values are optional and properties default to null - * @suppress PhanTypeMismatchProperty + * @suppress PhanPossiblyNullTypeMismatchProperty */ public function __construct(int $kind = null, int $flags = null, array $children = null, int $lineno = null) { @@ -246,7 +251,7 @@ public function __construct(int $kind = null, int $flags = null, array $children } } -if (!class_exists('ast\Metadata')) { +if (!\class_exists('ast\Metadata')) { /** * Metadata entry for a single AST kind, as returned by ast\get_metadata(). * @suppress PhanRedefineClassInternal @@ -267,7 +272,7 @@ class Metadata public $name; /** - * @var array Array of supported flags. The flags are given as names of constants, such as + * @var list Array of supported flags. The flags are given as names of constants, such as * "ast\flags\TYPE_STRING". * @suppress PhanUnreferencedPublicProperty */ @@ -278,6 +283,7 @@ class Metadata * using ===, while combinable flags should be checked using &. * @suppress PhanUnreferencedPublicProperty */ + // phpcs:ignore Phan.NamingConventions.ValidUnderscoreVariableName.MemberVarNotUnderscore public $flagsCombinable; } } diff --git a/src/util.php b/src/util.php index 2e1a0ee..144d736 100644 --- a/src/util.php +++ b/src/util.php @@ -1,208 +1,234 @@ >,1:associative-array>} + * Returns [string[][] $exclusive, string[][] $combinable]. + */ function get_flag_info() : array { + // TODO: Use AST's built in flag info if available. static $exclusive, $combinable; - if ($exclusive !== null) { - return [$exclusive, $combinable]; - } - - $modifiers = [ - flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', - flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', - flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', - flags\MODIFIER_STATIC => 'MODIFIER_STATIC', - flags\MODIFIER_ABSTRACT => 'MODIFIER_ABSTRACT', - flags\MODIFIER_FINAL => 'MODIFIER_FINAL', - flags\FUNC_RETURNS_REF => 'FUNC_RETURNS_REF', - flags\FUNC_GENERATOR => 'FUNC_GENERATOR', - ]; - $types = [ - flags\TYPE_NULL => 'TYPE_NULL', - flags\TYPE_BOOL => 'TYPE_BOOL', - flags\TYPE_LONG => 'TYPE_LONG', - flags\TYPE_DOUBLE => 'TYPE_DOUBLE', - flags\TYPE_STRING => 'TYPE_STRING', - flags\TYPE_ARRAY => 'TYPE_ARRAY', - flags\TYPE_OBJECT => 'TYPE_OBJECT', - flags\TYPE_CALLABLE => 'TYPE_CALLABLE', - flags\TYPE_VOID => 'TYPE_VOID', - flags\TYPE_ITERABLE => 'TYPE_ITERABLE', - ]; - $use_types = [ - flags\USE_NORMAL => 'USE_NORMAL', - flags\USE_FUNCTION => 'USE_FUNCTION', - flags\USE_CONST => 'USE_CONST', - ]; - $shared_binary_ops = [ - flags\BINARY_BITWISE_OR => 'BINARY_BITWISE_OR', - flags\BINARY_BITWISE_AND => 'BINARY_BITWISE_AND', - flags\BINARY_BITWISE_XOR => 'BINARY_BITWISE_XOR', - flags\BINARY_CONCAT => 'BINARY_CONCAT', - flags\BINARY_ADD => 'BINARY_ADD', - flags\BINARY_SUB => 'BINARY_SUB', - flags\BINARY_MUL => 'BINARY_MUL', - flags\BINARY_DIV => 'BINARY_DIV', - flags\BINARY_MOD => 'BINARY_MOD', - flags\BINARY_POW => 'BINARY_POW', - flags\BINARY_SHIFT_LEFT => 'BINARY_SHIFT_LEFT', - flags\BINARY_SHIFT_RIGHT => 'BINARY_SHIFT_RIGHT', - ]; - - $exclusive = [ - ast\AST_NAME => [ - flags\NAME_FQ => 'NAME_FQ', - flags\NAME_NOT_FQ => 'NAME_NOT_FQ', - flags\NAME_RELATIVE => 'NAME_RELATIVE', - ], - ast\AST_CLASS => [ - flags\CLASS_ABSTRACT => 'CLASS_ABSTRACT', - flags\CLASS_FINAL => 'CLASS_FINAL', - flags\CLASS_TRAIT => 'CLASS_TRAIT', - flags\CLASS_INTERFACE => 'CLASS_INTERFACE', - flags\CLASS_ANONYMOUS => 'CLASS_ANONYMOUS', - ], - ast\AST_PARAM => [ - flags\PARAM_REF => 'PARAM_REF', - flags\PARAM_VARIADIC => 'PARAM_VARIADIC', - ], - ast\AST_TYPE => $types, - ast\AST_CAST => $types, - ast\AST_UNARY_OP => [ - flags\UNARY_BOOL_NOT => 'UNARY_BOOL_NOT', - flags\UNARY_BITWISE_NOT => 'UNARY_BITWISE_NOT', - flags\UNARY_MINUS => 'UNARY_MINUS', - flags\UNARY_PLUS => 'UNARY_PLUS', - flags\UNARY_SILENCE => 'UNARY_SILENCE', - ], - ast\AST_BINARY_OP => $shared_binary_ops + [ - flags\BINARY_BOOL_AND => 'BINARY_BOOL_AND', - flags\BINARY_BOOL_OR => 'BINARY_BOOL_OR', - flags\BINARY_BOOL_XOR => 'BINARY_BOOL_XOR', - flags\BINARY_IS_IDENTICAL => 'BINARY_IS_IDENTICAL', - flags\BINARY_IS_NOT_IDENTICAL => 'BINARY_IS_NOT_IDENTICAL', - flags\BINARY_IS_EQUAL => 'BINARY_IS_EQUAL', - flags\BINARY_IS_NOT_EQUAL => 'BINARY_IS_NOT_EQUAL', - flags\BINARY_IS_SMALLER => 'BINARY_IS_SMALLER', - flags\BINARY_IS_SMALLER_OR_EQUAL => 'BINARY_IS_SMALLER_OR_EQUAL', - flags\BINARY_IS_GREATER => 'BINARY_IS_GREATER', - flags\BINARY_IS_GREATER_OR_EQUAL => 'BINARY_IS_GREATER_OR_EQUAL', - flags\BINARY_SPACESHIP => 'BINARY_SPACESHIP', + // Write this in a way that lets Phan infer the value of $combinable at the end. + if ($exclusive === null) { + $function_modifiers = [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + flags\MODIFIER_STATIC => 'MODIFIER_STATIC', + flags\MODIFIER_ABSTRACT => 'MODIFIER_ABSTRACT', + flags\MODIFIER_FINAL => 'MODIFIER_FINAL', + flags\FUNC_RETURNS_REF => 'FUNC_RETURNS_REF', + flags\FUNC_GENERATOR => 'FUNC_GENERATOR', + ]; + $property_modifiers = [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + flags\MODIFIER_STATIC => 'MODIFIER_STATIC', + flags\MODIFIER_ABSTRACT => 'MODIFIER_ABSTRACT', + flags\MODIFIER_FINAL => 'MODIFIER_FINAL', + ]; + $types = [ + flags\TYPE_NULL => 'TYPE_NULL', + flags\TYPE_BOOL => 'TYPE_BOOL', + flags\TYPE_LONG => 'TYPE_LONG', + flags\TYPE_DOUBLE => 'TYPE_DOUBLE', + flags\TYPE_STRING => 'TYPE_STRING', + flags\TYPE_ARRAY => 'TYPE_ARRAY', + flags\TYPE_OBJECT => 'TYPE_OBJECT', + flags\TYPE_CALLABLE => 'TYPE_CALLABLE', + flags\TYPE_VOID => 'TYPE_VOID', + flags\TYPE_ITERABLE => 'TYPE_ITERABLE', + ]; + $use_types = [ + flags\USE_NORMAL => 'USE_NORMAL', + flags\USE_FUNCTION => 'USE_FUNCTION', + flags\USE_CONST => 'USE_CONST', + ]; + $shared_binary_ops = [ + flags\BINARY_BITWISE_OR => 'BINARY_BITWISE_OR', + flags\BINARY_BITWISE_AND => 'BINARY_BITWISE_AND', + flags\BINARY_BITWISE_XOR => 'BINARY_BITWISE_XOR', + flags\BINARY_CONCAT => 'BINARY_CONCAT', + flags\BINARY_ADD => 'BINARY_ADD', + flags\BINARY_SUB => 'BINARY_SUB', + flags\BINARY_MUL => 'BINARY_MUL', + flags\BINARY_DIV => 'BINARY_DIV', + flags\BINARY_MOD => 'BINARY_MOD', + flags\BINARY_POW => 'BINARY_POW', + flags\BINARY_SHIFT_LEFT => 'BINARY_SHIFT_LEFT', + flags\BINARY_SHIFT_RIGHT => 'BINARY_SHIFT_RIGHT', flags\BINARY_COALESCE => 'BINARY_COALESCE', - ], - ast\AST_ASSIGN_OP => $shared_binary_ops, - ast\AST_MAGIC_CONST => [ - flags\MAGIC_LINE => 'MAGIC_LINE', - flags\MAGIC_FILE => 'MAGIC_FILE', - flags\MAGIC_DIR => 'MAGIC_DIR', - flags\MAGIC_NAMESPACE => 'MAGIC_NAMESPACE', - flags\MAGIC_FUNCTION => 'MAGIC_FUNCTION', - flags\MAGIC_METHOD => 'MAGIC_METHOD', - flags\MAGIC_CLASS => 'MAGIC_CLASS', - flags\MAGIC_TRAIT => 'MAGIC_TRAIT', - ], - ast\AST_USE => $use_types, - ast\AST_GROUP_USE => $use_types, - ast\AST_USE_ELEM => $use_types, - ast\AST_INCLUDE_OR_EVAL => [ - flags\EXEC_EVAL => 'EXEC_EVAL', - flags\EXEC_INCLUDE => 'EXEC_INCLUDE', - flags\EXEC_INCLUDE_ONCE => 'EXEC_INCLUDE_ONCE', - flags\EXEC_REQUIRE => 'EXEC_REQUIRE', - flags\EXEC_REQUIRE_ONCE => 'EXEC_REQUIRE_ONCE', - ], - ast\AST_ARRAY => [ - flags\ARRAY_SYNTAX_LIST => 'ARRAY_SYNTAX_LIST', - flags\ARRAY_SYNTAX_LONG => 'ARRAY_SYNTAX_LONG', - flags\ARRAY_SYNTAX_SHORT => 'ARRAY_SYNTAX_SHORT', - ], - ast\AST_CLOSURE_VAR => [ - flags\CLOSURE_USE_REF => 'CLOSURE_USE_REF', - ], - ]; + ]; - $combinable = []; - $combinable[ast\AST_METHOD] = $combinable[ast\AST_FUNC_DECL] = $combinable[ast\AST_CLOSURE] - = $combinable[ast\AST_PROP_DECL] = $combinable[ast\AST_CLASS_CONST_DECL] - = $combinable[ast\AST_TRAIT_ALIAS] = $modifiers; + $exclusive = [ + ast\AST_NAME => [ + flags\NAME_FQ => 'NAME_FQ', + flags\NAME_NOT_FQ => 'NAME_NOT_FQ', + flags\NAME_RELATIVE => 'NAME_RELATIVE', + ], + ast\AST_CLASS => [ + flags\CLASS_ABSTRACT => 'CLASS_ABSTRACT', + flags\CLASS_FINAL => 'CLASS_FINAL', + flags\CLASS_TRAIT => 'CLASS_TRAIT', + flags\CLASS_INTERFACE => 'CLASS_INTERFACE', + flags\CLASS_ANONYMOUS => 'CLASS_ANONYMOUS', + ], + ast\AST_TYPE => $types, + ast\AST_CAST => $types, + ast\AST_UNARY_OP => [ + flags\UNARY_BOOL_NOT => 'UNARY_BOOL_NOT', + flags\UNARY_BITWISE_NOT => 'UNARY_BITWISE_NOT', + flags\UNARY_MINUS => 'UNARY_MINUS', + flags\UNARY_PLUS => 'UNARY_PLUS', + flags\UNARY_SILENCE => 'UNARY_SILENCE', + ], + ast\AST_BINARY_OP => $shared_binary_ops + [ + flags\BINARY_BOOL_AND => 'BINARY_BOOL_AND', + flags\BINARY_BOOL_OR => 'BINARY_BOOL_OR', + flags\BINARY_BOOL_XOR => 'BINARY_BOOL_XOR', + flags\BINARY_IS_IDENTICAL => 'BINARY_IS_IDENTICAL', + flags\BINARY_IS_NOT_IDENTICAL => 'BINARY_IS_NOT_IDENTICAL', + flags\BINARY_IS_EQUAL => 'BINARY_IS_EQUAL', + flags\BINARY_IS_NOT_EQUAL => 'BINARY_IS_NOT_EQUAL', + flags\BINARY_IS_SMALLER => 'BINARY_IS_SMALLER', + flags\BINARY_IS_SMALLER_OR_EQUAL => 'BINARY_IS_SMALLER_OR_EQUAL', + flags\BINARY_IS_GREATER => 'BINARY_IS_GREATER', + flags\BINARY_IS_GREATER_OR_EQUAL => 'BINARY_IS_GREATER_OR_EQUAL', + flags\BINARY_SPACESHIP => 'BINARY_SPACESHIP', + ], + ast\AST_ASSIGN_OP => $shared_binary_ops, + ast\AST_MAGIC_CONST => [ + flags\MAGIC_LINE => 'MAGIC_LINE', + flags\MAGIC_FILE => 'MAGIC_FILE', + flags\MAGIC_DIR => 'MAGIC_DIR', + flags\MAGIC_NAMESPACE => 'MAGIC_NAMESPACE', + flags\MAGIC_FUNCTION => 'MAGIC_FUNCTION', + flags\MAGIC_METHOD => 'MAGIC_METHOD', + flags\MAGIC_CLASS => 'MAGIC_CLASS', + flags\MAGIC_TRAIT => 'MAGIC_TRAIT', + ], + ast\AST_USE => $use_types, + ast\AST_GROUP_USE => $use_types, + ast\AST_USE_ELEM => $use_types, + ast\AST_INCLUDE_OR_EVAL => [ + flags\EXEC_EVAL => 'EXEC_EVAL', + flags\EXEC_INCLUDE => 'EXEC_INCLUDE', + flags\EXEC_INCLUDE_ONCE => 'EXEC_INCLUDE_ONCE', + flags\EXEC_REQUIRE => 'EXEC_REQUIRE', + flags\EXEC_REQUIRE_ONCE => 'EXEC_REQUIRE_ONCE', + ], + ast\AST_ARRAY => [ + flags\ARRAY_SYNTAX_LIST => 'ARRAY_SYNTAX_LIST', + flags\ARRAY_SYNTAX_LONG => 'ARRAY_SYNTAX_LONG', + flags\ARRAY_SYNTAX_SHORT => 'ARRAY_SYNTAX_SHORT', + ], + ast\AST_ARRAY_ELEM => [ + flags\ARRAY_ELEM_REF => 'ARRAY_ELEM_REF', + ], + ast\AST_CLOSURE_VAR => [ + flags\CLOSURE_USE_REF => 'CLOSURE_USE_REF', + ], + ]; + + $combinable = [ + ast\AST_METHOD => $function_modifiers, + ast\AST_FUNC_DECL => $function_modifiers, + ast\AST_CLOSURE => $function_modifiers, + ast\AST_ARROW_FUNC => $function_modifiers, + ast\AST_CLASS_CONST_DECL => [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + ], + ast\AST_PROP_GROUP => $property_modifiers, + ast\AST_TRAIT_ALIAS => $property_modifiers, + ast\AST_DIM => [ + flags\DIM_ALTERNATIVE_SYNTAX => 'DIM_ALTERNATIVE_SYNTAX', + ], + ast\AST_CONDITIONAL => [ + flags\PARENTHESIZED_CONDITIONAL => 'PARENTHESIZED_CONDITIONAL', + ], + ast\AST_PARAM => [ + flags\PARAM_REF => 'PARAM_REF', + flags\PARAM_VARIADIC => 'PARAM_VARIADIC', + ], + ]; + } return [$exclusive, $combinable]; } +/** + * Computes a string representation of AST node flags such as + * 'ASSIGN_DIV|TYPE_ARRAY' + */ function format_flags(int $kind, int $flags) : string { - list($exclusive, $combinable) = get_flag_info(); + [$exclusive, $combinable] = get_flag_info(); + $flag_names = []; if (isset($exclusive[$kind])) { - $flagInfo = $exclusive[$kind]; - if (isset($flagInfo[$flags])) { - return "{$flagInfo[$flags]} ($flags)"; + $flag_info = $exclusive[$kind]; + if (isset($flag_info[$flags])) { + $flag_names[] = $flag_info[$flags]; } } elseif (isset($combinable[$kind])) { - $flagInfo = $combinable[$kind]; - $names = []; - foreach ($flagInfo as $flag => $name) { + $flag_info = $combinable[$kind]; + foreach ($flag_info as $flag => $name) { if ($flags & $flag) { - $names[] = $name; + $flag_names[] = $name; } } - if (!empty($names)) { - return implode(" | ", $names) . " ($flags)"; - } } - return (string) $flags; + + return \implode('|', $flag_names); } /** * Dumps abstract syntax tree - * @suppress PhanUndeclaredProperty + * Source: https://github.com/nikic/php-ast/blob/master/util.php + * @param Node|string|int|float|null $ast + * @param int $options (AST_DUMP_*) */ function ast_dump($ast, int $options = 0) : string { - if ($ast instanceof ast\Node) { - $kind = $ast->kind; + if ($ast instanceof Node) { // $kind can be invalid for placeholder nodes or unexpected tolerant-php-parser classes + $kind = $ast->kind; $result = is_int($kind) ? ast\get_kind_name($kind) : ("INVALID KIND: " . var_export($kind, true)); if ($options & AST_DUMP_LINENOS) { $result .= " @ $ast->lineno"; - if (isset($ast->endLineno)) { - $result .= "-$ast->endLineno"; + $end_lineno = $ast->endLineno ?? null; + if (!\is_null($end_lineno)) { + $result .= "-$end_lineno"; } } - if (\is_int($kind) && ast\kind_uses_flags($kind)) { - $result .= "\n flags: " . format_flags($kind, $ast->flags); - } - if (isset($ast->name)) { - $result .= "\n name: $ast->name"; - } - if (isset($ast->docComment)) { - $result .= "\n docComment: $ast->docComment"; - } - $children = $ast->children; - if (\is_array($children)) { - foreach ($children as $i => $child) { - $result .= "\n $i: " . \str_replace("\n", "\n ", ast_dump($child, $options)); + if (is_int($kind) && ast\kind_uses_flags($kind)) { + $flags = $ast->flags; + if ($flags != 0) { + $result .= "\n flags: " . format_flags($kind, $flags); } - } else { - $result .= "\n children: INVALID (type=" . (\is_object($children) ? \get_class($children) : \gettype($children)) . ")"; + } + foreach ($ast->children as $i => $child) { + $result .= "\n $i: " . \str_replace("\n", "\n ", ast_dump($child, $options)); } return $result; } elseif ($ast === null) { return 'null'; - } elseif (\is_scalar($ast)) { - return \var_export($ast, true); + } elseif (\is_string($ast)) { + return "\"$ast\""; } else { - ob_start(); - try { - \var_dump($ast); - } finally { - $repr = ob_get_clean(); - } - return "INVALID_TYPE: " . $repr; + return (string) $ast; } } diff --git a/test_files/src/0014_parent_property.php b/test_files/src/0014_parent_property.php index 371d721..ebfbb36 100644 --- a/test_files/src/0014_parent_property.php +++ b/test_files/src/0014_parent_property.php @@ -3,12 +3,12 @@ class A { public static $alpha = 42; public $beta = 'string'; - const FOURTY_TWO = 42; + const FORTY_TWO = 42; } class B extends A { public static $gamma = parent::$alpha; // FIXME: PHP Fatal error: Constant expression contains invalid operations, but this test expects no Issues. public $delta = parent::$beta; // Note: This is not a valid way to fetch a parent instance property. Emit an issue here as well. - public $epsilon = parent::FOURTY_TWO; + public $epsilon = parent::FORTY_TWO; } // FIXME: this test should access parent properties within functions. Property declaration defaults expect constant expressions. diff --git a/test_files/src/php71_or_newer/array_destructuring.php b/test_files/src/array_destructuring.php similarity index 100% rename from test_files/src/php71_or_newer/array_destructuring.php rename to test_files/src/array_destructuring.php diff --git a/test_files/src/ast_class_name.php b/test_files/src/ast_class_name.php new file mode 100644 index 0000000..e7ddf60 --- /dev/null +++ b/test_files/src/ast_class_name.php @@ -0,0 +1,3 @@ + + + diff --git a/test_files/src/namespaced_call_in_arg_list.php b/test_files/src/namespaced_call_in_arg_list.php new file mode 100644 index 0000000..20c083d --- /dev/null +++ b/test_files/src/namespaced_call_in_arg_list.php @@ -0,0 +1,2 @@ + 2]; +['key' => &$value] = $a; diff --git a/test_files/src/php73_or_newer/calls_with_trailing_comma.php b/test_files/src/php73_or_newer/calls_with_trailing_comma.php new file mode 100644 index 0000000..7e2381e --- /dev/null +++ b/test_files/src/php73_or_newer/calls_with_trailing_comma.php @@ -0,0 +1,8 @@ + $x; +fn(): int => $x; +fn($x = 42) => yield $x; +fn(&$x) => $x; +fn&($x) => $x; +fn($x, ...$rest) => $rest; +static fn() => 1; +$f = static fn() => 2; diff --git a/test_files/src/php74_or_newer/arrowFunctionTypes.php b/test_files/src/php74_or_newer/arrowFunctionTypes.php new file mode 100644 index 0000000..bc8ab32 --- /dev/null +++ b/test_files/src/php74_or_newer/arrowFunctionTypes.php @@ -0,0 +1,12 @@ + $x; +$fn2 = fn(): int => ($x); +$fn3 = fn($x = 42) => yield $x; +$fn4 = fn(&$x) => $x; +$fn5 = fn&(Ns\MyClass $x) : Ns\MyClass => $x; +$fn = fn($str) => preg_match($regex, $str, $matches) && ($matches[1] % 7 == 0); +$cb = fn() => fn() => $undef; +$fn = fn() => call_user_func(function () use ($arg) { + var_export($arg); + return $arg; +}); diff --git a/test_files/src/php71_or_newer/use_declarations.php b/test_files/src/use_declarations.php similarity index 100% rename from test_files/src/php71_or_newer/use_declarations.php rename to test_files/src/use_declarations.php diff --git a/tests/TolerantASTConverter/ConversionTest.php b/tests/TolerantASTConverter/ConversionTest.php index 98056fc..cd89b2d 100644 --- a/tests/TolerantASTConverter/ConversionTest.php +++ b/tests/TolerantASTConverter/ConversionTest.php @@ -2,6 +2,7 @@ namespace TolerantASTConverter\Tests; +use AssertionError; use ast; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -9,33 +10,46 @@ use TolerantASTConverter\NodeDumper; use TolerantASTConverter\TolerantASTConverter; +use function count; +use function get_class; +use function in_array; +use function is_array; +use function is_int; +use function is_string; + require_once __DIR__ . '/../../src/util.php'; +/** + * Tests that the polyfill works with valid ASTs + */ class ConversionTest extends \PHPUnit\Framework\TestCase { - protected function _scanSourceDirForPHP(string $source_dir) : array + /** + * @return list + */ + protected function scanSourceDirForPHP(string $source_dir) : array { $files = []; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source_dir)) as $file_path => $file_info) { $filename = $file_info->getFilename(); if ($filename && !in_array($filename, ['.', '..'], true) && - substr($filename, 0, 1) !== '.' && - strpos($filename, '.') !== false && - pathinfo($filename)['extension'] === 'php') { + \substr($filename, 0, 1) !== '.' && + \strpos($filename, '.') !== false && + \pathinfo($filename)['extension'] === 'php') { $files[] = $file_path; } } if (count($files) === 0) { - throw new \InvalidArgumentException(sprintf("RecursiveDirectoryIterator iteration returned no files for %s\n", $source_dir)); + throw new \InvalidArgumentException(\sprintf("RecursiveDirectoryIterator iteration returned no files for %s\n", $source_dir)); } return $files; } /** - * @return bool + * @return bool does php-ast support $ast_version */ - public static function hasNativeASTSupport(int $ast_version) + public static function hasNativeASTSupport(int $ast_version) : bool { try { ast\parse_code('', $ast_version); @@ -49,15 +63,18 @@ public static function hasNativeASTSupport(int $ast_version) * This is used to sort by token count, so that the failures with the fewest token * (i.e. simplest ASTs) appear first. * @param string[] $files - * @return void */ - private static function sortByTokenCount(array &$files) + private static function sortByTokenCount(array &$files) : void { $token_counts = []; foreach ($files as $file) { - $token_counts[$file] = count(token_get_all(file_get_contents($file))); + $contents = \file_get_contents($file); + if (!is_string($contents)) { + throw new AssertionError("Failed to read $file"); + } + $token_counts[$file] = count(\token_get_all($contents)); } - usort($files, function (string $path1, string $path2) use ($token_counts) { + \usort($files, static function (string $path1, string $path2) use ($token_counts) : int { return $token_counts[$path1] <=> $token_counts[$path2]; }); } @@ -65,28 +82,30 @@ private static function sortByTokenCount(array &$files) /** * Asserts that valid files get parsed the same way by php-ast and the polyfill. * - * @return string[]|int[] [string $file_path, int $ast_version] - * @suppress PhanPluginUnusedVariable + * @return array{0:string,1:int}[] array of [string $file_path, int $ast_version] */ - public function astValidFileExampleProvider() + public function astValidFileExampleProvider() : array { $tests = []; - $source_dir = dirname(dirname(realpath(__DIR__))) . '/test_files/src'; - $paths = $this->_scanSourceDirForPHP($source_dir); + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $source_dir = \dirname(\realpath(__DIR__), 2) . '/test_files/src'; + $paths = $this->scanSourceDirForPHP($source_dir); self::sortByTokenCount($paths); - $supports50 = self::hasNativeASTSupport(50); - if (!$supports50) { - throw new RuntimeException("Version 50 is not natively supported"); + $supports70 = self::hasNativeASTSupport(70); + if (!$supports70) { + throw new RuntimeException("Version 70 is not natively supported"); } foreach ($paths as $path) { - $tests[] = [$path, 50]; + $tests[] = [$path, 70]; } return $tests; } - /** @return void */ - private static function normalizeOriginalAST($node) + /** + * @param ast\Node|int|string|float|null $node + */ + private static function normalizeOriginalAST($node) : void { if ($node instanceof ast\Node) { $kind = $node->kind; @@ -107,6 +126,10 @@ private static function normalizeOriginalAST($node) // TODO: TolerantPHPParser gets more information than PHP-Parser for statement lists, // so this step may be unnecessary + /** + * Set all of the line numbers to constants, + * so that minor differences in line numbers won't cause tests to fail. + */ public static function normalizeLineNumbers(ast\Node $node) : ast\Node { $node = clone($node); @@ -121,26 +144,60 @@ public static function normalizeLineNumbers(ast\Node $node) : ast\Node return $node; } + private const FUNCTION_DECLARATION_KINDS = [ + ast\AST_FUNC_DECL, + ast\AST_METHOD, + ast\AST_CLOSURE, + ast\AST_ARROW_FUNC, + ]; + + /** + * Normalizes the flags on function declaration caused by \ast\flags\FUNC_GENERATOR. + * + * Phan does not use these flags because they are not natively provided in all PHP versions. + * TODO: Shouldn't they be available in PHP 7.1+ + */ + public static function normalizeYieldFlags(ast\Node $node) : void + { + if (\in_array($node->kind, self::FUNCTION_DECLARATION_KINDS, true)) { + // Alternately, could make Phan do this. + $node->flags &= ~ast\flags\FUNC_GENERATOR; + } + // @phan-suppress-next-line PhanUndeclaredProperty + unset($node->is_not_parenthesized); + + foreach ($node->children as $v) { + if ($v instanceof ast\Node) { + self::normalizeYieldFlags($v); + } + } + } + /** @dataProvider astValidFileExampleProvider */ - public function testFallbackFromParser(string $file_name, int $ast_version) + public function testFallbackFromParser(string $file_name, int $ast_version) : void { - $test_folder_name = basename(dirname($file_name)); - if (PHP_VERSION_ID < 70100 && $test_folder_name === 'php71_or_newer') { - $this->markTestIncomplete('php-ast cannot parse php7.1 syntax when running in php7.0'); + $test_folder_name = \basename(\dirname($file_name)); + if (\PHP_VERSION_ID < 70300 && $test_folder_name === 'php73_or_newer') { + $this->markTestIncomplete('php-ast cannot parse php7.3 syntax when running in php7.2 or older'); + } + if (\PHP_VERSION_ID < 70400 && $test_folder_name === 'php74_or_newer') { + $this->markTestIncomplete('php-ast cannot parse php7.4 syntax when running in php7.3 or older'); } - $contents = file_get_contents($file_name); + $contents = \file_get_contents($file_name); if ($contents === false) { $this->fail("Failed to read $file_name"); + return; // unreachable } $ast = ast\parse_code($contents, $ast_version, $file_name); self::normalizeOriginalAST($ast); - $this->assertInstanceOf('\ast\Node', $ast, 'Examples must be syntactically valid PHP parseable by php-ast'); + $this->assertInstanceOf('\ast\Node', $ast, 'Examples must be syntactically valid PHP parsable by php-ast'); $converter = new TolerantASTConverter(); - $converter->setPHPVersionId(PHP_VERSION_ID); + $converter->setPHPVersionId(\PHP_VERSION_ID); try { $fallback_ast = $converter->parseCodeAsPHPAST($contents, $ast_version); } catch (\Throwable $e) { - throw new \RuntimeException("Error parsing $file_name with ast version $ast_version", $e->getCode(), $e); + $code = $e->getCode(); + throw new \RuntimeException("Error parsing $file_name with ast version $ast_version", is_int($code) ? $code : 1, $e); } $this->assertInstanceOf('\ast\Node', $fallback_ast, 'The fallback must also return a tree of php-ast nodes'); @@ -148,9 +205,11 @@ public function testFallbackFromParser(string $file_name, int $ast_version) $fallback_ast = self::normalizeLineNumbers($fallback_ast); $ast = self::normalizeLineNumbers($ast); } + self::normalizeYieldFlags($ast); + self::normalizeYieldFlags($fallback_ast); // TODO: Remove $ast->parent recursively - $fallback_ast_repr = var_export($fallback_ast, true); - $original_ast_repr = var_export($ast, true); + $fallback_ast_repr = \var_export($fallback_ast, true); + $original_ast_repr = \var_export($ast, true); if ($fallback_ast_repr !== $original_ast_repr) { $node_dumper = new NodeDumper($contents); @@ -162,9 +221,9 @@ public function testFallbackFromParser(string $file_name, int $ast_version) } catch (\Throwable $e) { $dump = 'could not dump PhpParser Node: ' . get_class($e) . ': ' . $e->getMessage() . "\n" . $e->getTraceAsString(); } - $original_ast_dump = \ast_dump($ast, AST_DUMP_LINENOS); + $original_ast_dump = \ast_dump($ast); try { - $fallback_ast_dump = \ast_dump($fallback_ast, AST_DUMP_LINENOS); + $fallback_ast_dump = \ast_dump($fallback_ast); } catch (\Throwable $e) { $fallback_ast_dump = 'could not dump php-ast Node: ' . get_class($e) . ': ' . $e->getMessage() . "\n" . $e->getTraceAsString(); } diff --git a/tests/TolerantASTConverter/ErrorTolerantConversionTest.php b/tests/TolerantASTConverter/ErrorTolerantConversionTest.php index 7389c93..54d2ec4 100644 --- a/tests/TolerantASTConverter/ErrorTolerantConversionTest.php +++ b/tests/TolerantASTConverter/ErrorTolerantConversionTest.php @@ -1,14 +1,19 @@ -_testFallbackFromParser($incomplete_contents, $valid_contents); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents); } - public function testIncompleteVarWithPlaceholderShort() + public function testIncompleteVarWithPlaceholderShort() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testIncompleteVarWithPlaceholder() + public function testIncompleteVarWithPlaceholder() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testIncompleteProperty() + public function testIncompleteProperty() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents); } - public function testIncompletePropertyWithPlaceholder() + public function testIncompletePropertyWithPlaceholder() : void { $incomplete_contents = <<<'EOT' __INCOMPLETE_PROPERTY__; } EOT; - $this->_testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testIncompleteMethod() + public function testIncompleteMethod() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents); } - public function testIncompleteMethodWithPlaceholder() + public function testIncompleteMethodWithPlaceholder() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testMiscNoise() + public function testMiscNoise() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents); } - public function testMiscNoiseWithPlaceholders() + public function testMiscNoiseWithPlaceholders() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testIncompleteArithmeticWithPlaceholders() + public function testIncompleteArithmeticWithPlaceholders() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, true); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); } - public function testIncompleteArithmeticWithoutPlaceholders() + public function testIncompleteArithmeticWithoutPlaceholders() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, false); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, false); } - public function testMissingMember() + public function testMissingMember() : void { + // in 0.0.17, this starts trying to parse a typed property declaration + // and discards all of it because it's invalid. $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, false); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, false); + } + + public function testIncompleteTypedProperty() : void + { + $incomplete_contents = <<<'EOT' +runTestFallbackFromParser($incomplete_contents, $valid_contents, false); } - public function testEmptyConstList() + public function testEmptyConstList() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents, false); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, false); } - public function testMissingSemicolon() + public function testMissingSemicolon() : void { $incomplete_contents = <<<'EOT' _testFallbackFromParser($incomplete_contents, $valid_contents); + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents); + } + + public function testIncompleteMethodCallBeforeIfWithPlaceholders() : void + { + $incomplete_contents = <<<'EOT' + +if (true) { + $y; +} +$obj-> +if (true) { + echo "example"; +} +EOT; + $valid_contents = <<<'EOT' + +if (true){ + $y +}; +$obj-> +if (true); +echo "example"; +EOT; + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, true); + } + + public function testIncompleteMethodCallBeforeIfWithoutPlaceholders() : void + { + $incomplete_contents = <<<'EOT' + +if (true) { + foo(); +} +$obj-> +if (true) { + echo "example"; +} +EOT; + $valid_contents = <<<'EOT' + +if (true){ + foo() +}; +$obj-> +if (true); +echo "example"; +EOT; + $this->runTestFallbackFromParser($incomplete_contents, $valid_contents, false); } // Another test (Won't work with php-parser, might work with tolerant-php-parser @@ -281,33 +361,51 @@ public function bar() { EOT; */ - private function _testFallbackFromParser(string $incomplete_contents, string $valid_contents, bool $should_add_placeholders = false) + private static function normalizePolyfillAST(ast\Node $ast) : void + { + switch ($ast->kind) { + case ast\AST_DIM: + if (\PHP_VERSION_ID < 70400) { + $ast->flags = 0; + } + break; + } + foreach ($ast->children as $c) { + if ($c instanceof ast\Node) { + self::normalizePolyfillAST($c); + } + } + } + + private function runTestFallbackFromParser(string $incomplete_contents, string $valid_contents, bool $should_add_placeholders = false) : void { - $supports50 = ConversionTest::hasNativeASTSupport(50); - if (!$supports50) { + $supports70 = ConversionTest::hasNativeASTSupport(70); + if (!$supports70) { $this->fail('No supported AST versions to test'); } - $this->_testFallbackFromParserForASTVersion($incomplete_contents, $valid_contents, 50, $should_add_placeholders); + $this->runTestFallbackFromParserForASTVersion($incomplete_contents, $valid_contents, 70, $should_add_placeholders); } - private function _testFallbackFromParserForASTVersion(string $incomplete_contents, string $valid_contents, int $ast_version, bool $should_add_placeholders) + private function runTestFallbackFromParserForASTVersion(string $incomplete_contents, string $valid_contents, int $ast_version, bool $should_add_placeholders) : void { $ast = \ast\parse_code($valid_contents, $ast_version); - $this->assertInstanceOf('\ast\Node', $ast, 'Examples(for validContents) must be syntactically valid PHP parseable by php-ast'); + $this->assertInstanceOf('\ast\Node', $ast, 'Examples(for validContents) must be syntactically valid PHP parsable by php-ast'); $errors = []; $converter = new TolerantASTConverter(); $converter->setShouldAddPlaceholders($should_add_placeholders); $php_parser_node = $converter->phpParserParse($incomplete_contents, $errors); $fallback_ast = $converter->phpParserToPhpAst($php_parser_node, $ast_version, $incomplete_contents); + self::normalizePolyfillAST($fallback_ast); $this->assertInstanceOf('\ast\Node', $fallback_ast, 'The fallback must also return a tree of php-ast nodes'); - $fallback_ast_repr = var_export($fallback_ast, true); - $original_ast_repr = var_export($ast, true); + $fallback_ast_repr = \var_export($fallback_ast, true); + $original_ast_repr = \var_export($ast, true); if ($fallback_ast_repr !== $original_ast_repr) { $placeholders_used_str = $should_add_placeholders ? 'Yes' : 'No'; $dumper = new NodeDumper($incomplete_contents); $dumper->setIncludeTokenKind(true); $dumper->setIncludeOffset(true); + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should not happen and unit test would fail $dump = $dumper->dumpTreeAsString($php_parser_node); $original_ast_dump = \ast_dump($ast); $modified_ast_dump = \ast_dump($fallback_ast);