├── .bettercodehub.yml ├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .phan └── config.php ├── .php_cs ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.markdown ├── bin └── phpweaver ├── composer.json ├── composer.lock ├── example ├── Bar.php ├── Foo.php ├── controller.php ├── presentation.sh └── readme.txt ├── phpcs.xml ├── phpmd.xml ├── phpstan.neon ├── phpunit.xml ├── psalm.xml ├── src ├── PHPWeaver │ ├── Command │ │ ├── TraceCommand.php │ │ └── WeaveCommand.php │ ├── Exceptions │ │ └── Exception.php │ ├── Scanner │ │ ├── ClassScanner.php │ │ ├── FunctionBodyScanner.php │ │ ├── FunctionParametersScanner.php │ │ ├── ModifiersScanner.php │ │ ├── NamespaceScanner.php │ │ ├── ScannerInterface.php │ │ ├── ScannerMultiplexer.php │ │ ├── Token.php │ │ ├── TokenBuffer.php │ │ ├── TokenStream.php │ │ └── TokenStreamParser.php │ ├── Signature │ │ ├── FunctionArgument.php │ │ ├── FunctionSignature.php │ │ └── Signatures.php │ ├── Transform │ │ ├── BufferEditorInterface.php │ │ ├── DocCommentEditorTransformer.php │ │ ├── TracerDocBlockEditor.php │ │ └── TransformerInterface.php │ └── Xtrace │ │ ├── FunctionTracer.php │ │ ├── Trace.php │ │ ├── TraceReader.php │ │ └── TraceSignatureLogger.php └── bootstrap.php └── tests ├── PHPWeaver ├── ClassScannerTest.php ├── CollationTest.php ├── DocCommentEditorTransformerTest.php ├── FunctionBodyScannerTest.php ├── FunctionParametersScannerTest.php ├── MockPassthruBufferEditor.php ├── ModifiersScannerTest.php ├── TokenizerTest.php └── TracerTest.php └── bootstrap.php /.bettercodehub.yml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - /.phan/.* 3 | - /examples/.* 4 | component_depth: 3 5 | languages: 6 | - php 7 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.php] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/logs/clover.xml 3 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'src', 6 | 'tests', 7 | ], 8 | 'backward_compatibility_checks' => true, 9 | 'quick_mode' => false, 10 | 'analyze_signature_compatibility' => true, 11 | 'minimum_severity' => 0, 12 | 'allow_missing_properties' => false, 13 | 'null_casts_as_any_type' => false, 14 | 'null_casts_as_array' => false, 15 | 'array_casts_as_null' => false, 16 | 'scalar_implicit_cast' => false, 17 | 'scalar_implicit_partial' => [], 18 | 'ignore_undeclared_variables_in_global_scope' => false, 19 | 'suppress_issue_types' => [ 20 | ], 21 | 'whitelist_issue_types' => [], 22 | 'skip_slow_php_options_warning' => false, 23 | ]; 24 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('somedir') 5 | ->notPath('src/Symfony/Component/Translation/Tests/fixtures/resources.php') 6 | ->in(__DIR__); 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules(json_decode('{ 10 | "visibility_required":true, 11 | "ternary_to_null_coalescing":true, 12 | "binary_operator_spaces":{ 13 | "align_double_arrow":true 14 | }, 15 | "blank_line_after_opening_tag":false, 16 | "blank_line_before_statement":{ 17 | "statements":[ 18 | "return" 19 | ] 20 | }, 21 | "braces":{ 22 | "allow_single_line_closure":true 23 | }, 24 | "cast_spaces":true, 25 | "class_definition":{ 26 | "singleLine":true 27 | }, 28 | "concat_space":{ 29 | "spacing":"one" 30 | }, 31 | "declare_equal_normalize":true, 32 | "function_typehint_space":true, 33 | "include":true, 34 | "increment_style":true, 35 | "lowercase_cast":true, 36 | "magic_constant_casing":true, 37 | "method_argument_space":true, 38 | "method_separation":true, 39 | "native_function_casing":true, 40 | "new_with_braces":true, 41 | "no_blank_lines_after_class_opening":true, 42 | "no_blank_lines_after_phpdoc":true, 43 | "no_empty_comment":true, 44 | "no_empty_phpdoc":true, 45 | "no_empty_statement":true, 46 | "no_extra_consecutive_blank_lines":{ 47 | "tokens":[ 48 | "curly_brace_block", 49 | "extra", 50 | "parenthesis_brace_block", 51 | "square_brace_block", 52 | "throw", 53 | "use" 54 | ] 55 | }, 56 | "no_leading_import_slash":true, 57 | "no_leading_namespace_whitespace":true, 58 | "no_mixed_echo_print":{ 59 | "use":"echo" 60 | }, 61 | "no_multiline_whitespace_around_double_arrow":true, 62 | "no_short_bool_cast":true, 63 | "no_singleline_whitespace_before_semicolons":true, 64 | "no_spaces_around_offset":true, 65 | "no_trailing_comma_in_list_call":true, 66 | "no_trailing_comma_in_singleline_array":true, 67 | "no_unneeded_control_parentheses":true, 68 | "no_unneeded_curly_braces":true, 69 | "no_unneeded_final_method":true, 70 | "no_unused_imports":true, 71 | "no_whitespace_before_comma_in_array":true, 72 | "no_whitespace_in_blank_line":true, 73 | "normalize_index_brace":true, 74 | "object_operator_without_whitespace":true, 75 | "ordered_imports":true, 76 | "php_unit_fqcn_annotation":true, 77 | "phpdoc_add_missing_param_annotation":true, 78 | "phpdoc_align":true, 79 | "phpdoc_annotation_without_dot":true, 80 | "phpdoc_indent":true, 81 | "phpdoc_inline_tag":true, 82 | "phpdoc_no_access":true, 83 | "phpdoc_no_alias_tag":true, 84 | "phpdoc_no_empty_return":false, 85 | "phpdoc_no_package":true, 86 | "phpdoc_no_useless_inheritdoc":true, 87 | "phpdoc_order":true, 88 | "phpdoc_return_self_reference":true, 89 | "phpdoc_scalar":true, 90 | "phpdoc_separation":true, 91 | "phpdoc_single_line_var_spacing":true, 92 | "phpdoc_summary":true, 93 | "phpdoc_to_comment":true, 94 | "phpdoc_trim":true, 95 | "phpdoc_types":true, 96 | "phpdoc_types_order":{ 97 | "null_adjustment":"always_last" 98 | }, 99 | "phpdoc_var_without_name":true, 100 | "protected_to_private":true, 101 | "return_type_declaration":true, 102 | "self_accessor":true, 103 | "semicolon_after_instruction":true, 104 | "short_scalar_cast":true, 105 | "single_blank_line_before_namespace":false, 106 | "single_class_element_per_statement":true, 107 | "single_line_comment_style":{ 108 | "comment_types":[ 109 | "hash" 110 | ] 111 | }, 112 | "single_quote":true, 113 | "space_after_semicolon":{ 114 | "remove_in_empty_for_expressions":true 115 | }, 116 | "standardize_not_equals":true, 117 | "ternary_operator_spaces":true, 118 | "trailing_comma_in_multiline_array":true, 119 | "trim_array_spaces":false, 120 | "unary_operator_spaces":true, 121 | "whitespace_after_comma_in_array":true, 122 | "yoda_style":true, 123 | "blank_line_after_namespace":true, 124 | "elseif":true, 125 | "function_declaration":true, 126 | "indentation_type":true, 127 | "line_ending":true, 128 | "lowercase_constants":true, 129 | "lowercase_keywords":true, 130 | "no_break_comment":true, 131 | "no_useless_return":true, 132 | "no_closing_tag":true, 133 | "no_spaces_after_function_name":true, 134 | "no_spaces_inside_parenthesis":true, 135 | "no_trailing_whitespace":true, 136 | "no_trailing_whitespace_in_comment":true, 137 | "single_blank_line_at_eof":true, 138 | "single_import_per_statement":true, 139 | "single_line_after_imports":true, 140 | "switch_case_semicolon_to_colon":true, 141 | "switch_case_space":true, 142 | "encoding":true, 143 | "full_opening_tag":true, 144 | "no_superfluous_elseif":true, 145 | "no_useless_else":true, 146 | "compact_nullable_typehint":true, 147 | "align_multiline_comment":true, 148 | "combine_consecutive_issets":true, 149 | "list_syntax":{ 150 | "syntax":"short" 151 | }, 152 | "array_syntax":{ 153 | "syntax":"short" 154 | }, 155 | "blank_line_before_return":true, 156 | "combine_consecutive_unsets":true, 157 | "no_multiline_whitespace_before_semicolons":true, 158 | "no_null_property_initialization":true, 159 | "no_short_echo_tag":true, 160 | "no_useless_else":true 161 | }', true)) 162 | ->setFinder($finder); 163 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | 7 | install: 8 | - composer install 9 | 10 | before_script: 11 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 12 | - chmod +x ./cc-test-reporter 13 | - ./cc-test-reporter before-build 14 | 15 | script: 16 | - vendor/bin/phpunit --coverage-clover build/logs/clover.xml 17 | - vendor/bin/psalm 18 | - vendor/bin/phpstan analyze 19 | 20 | after_success: 21 | - travis_retry php vendor/bin/php-coveralls 22 | - bash <(curl -s https://codecov.io/bash) 23 | - php vendor/bin/codacycoverage clover build/logs/clover.xml 24 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker pull php 2 | # docker built -it phpweaver . 3 | # docker run -it phpweaver 4 | # OR 5 | # docker run -it --mount type=bind,source="$(pwd)",target=/usr/src/app phpweaver 6 | FROM php:7.1-cli 7 | 8 | # ubuntu packages 9 | RUN apt-get update && \ 10 | apt-get install -y --no-install-recommends git zip libzip-dev 11 | 12 | # xdebug extension 13 | RUN pecl install xdebug-2.5.5 && \ 14 | docker-php-ext-enable xdebug 15 | 16 | # zip extension 17 | RUN pecl install zip-1.15.2 && \ 18 | docker-php-ext-enable zip 19 | 20 | # composer 21 | RUN curl --silent --show-error https://getcomposer.org/installer | php 22 | 23 | RUN mv composer.phar /usr/local/bin/composer 24 | 25 | # global app setup 26 | RUN mkdir -p /usr/src/app 27 | 28 | WORKDIR /usr/src/app 29 | 30 | COPY ./composer.* /usr/src/app/ 31 | 32 | RUN COMPOSER_ALLOW_SUPERUSER=1 composer install 33 | 34 | RUN echo 'export PATH=$PATH:./vendor/bin' >> /root/.bashrc 35 | RUN echo 'COMPOSER_ALLOW_SUPERUSER=1 composer install' >> /root/.bashrc 36 | 37 | # mount current app 38 | COPY . /usr/src/app 39 | 40 | ENTRYPOINT ["/bin/bash"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright (c) 2008-2017 Troels Knak-Nielsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | phpweaver 2 | === 3 | [![Build Status](https://travis-ci.org/AJenbo/php-tracer-weaver.svg?branch=master)](https://travis-ci.org/AJenbo/php-tracer-weaver) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/cc2ad72a9e4c47a9bbc84037a29857a8)](https://www.codacy.com/app/AJenbo/php-tracer-weaver?utm_source=github.com&utm_medium=referral&utm_content=AJenbo/php-tracer-weaver&utm_campaign=Badge_Grade) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/412a2f0203c7ed255bee/maintainability)](https://codeclimate.com/github/AJenbo/php-tracer-weaver/maintainability) 6 | [![BCH compliance](https://bettercodehub.com/edge/badge/AJenbo/php-tracer-weaver?branch=master)](https://bettercodehub.com/) 7 | [![Coverage Status](https://coveralls.io/repos/github/AJenbo/php-tracer-weaver/badge.svg?branch=master)](https://coveralls.io/github/AJenbo/php-tracer-weaver?branch=master) 8 | 9 | **phpweaver** is a tool for analysing parameter types in PHP code, using a combination of static and runtime analysis. It relies on the [xdebug extension](http://www.xdebug.org/docs/execution_trace) to trace function calls. The result of the analysis can then be used to generate docblock comments, with the proper type annotations. 10 | 11 | Usage 12 | --- 13 | 14 | The basic usage of phpweaver is to write a piece of code (If you have unit tests/examples, they would be a good candidate), that utilises the code to manipulate. Run this example with the tracer, then use weaver to generate docblocks from the trace. 15 | 16 | See the `example/` folder for a basic example. 17 | 18 | The project has two main commands: 19 | 20 | * `trace` 21 | * `weave` 22 | 23 | `trace` 24 | --- 25 | 26 | This is just a wrapper around php + xdebug. Use it in lieu of `php` to execute a php script. It will run normally, but the code is traced and the output is dumped in `dumpfile.xt`. You can also manually configure xdebug to generate the tracefile. 27 | 28 | Sample usage: 29 | 30 | phpweaver trace test.php 31 | 32 | If your script requires it's own paremeter you can stop paramerter pasing in bash with the double dash: 33 | 34 | phpweaver trace -- vendor/bin/phpunit "-c phpunit.xml" 35 | 36 | `weave` 37 | --- 38 | 39 | This command takes a dumpfile (Generated by the trace process) and a php-source path (directory or file), and injects docblock comments into the php-source, using the type-information from the trace. It will look for `dumpfile.xt` in the current directory, or a trace file specifyed using the --tracefile option, printing the modified file to stdout. 40 | 41 | Sample usage: 42 | 43 | phpweaver weave somelibrary.php 44 | 45 | The same dumpfile can be used to weave multiple files, by specifying a folder or multiple paths. 46 | 47 | Running tests 48 | --- 49 | 50 | There is a Dockerfile for getting an environment up and running. First install docker somehow, then issue: 51 | 52 | docker build -t phpweaver . 53 | docker run -it phpweaver 54 | 55 | This will log you in to the machine. Run tests with: 56 | 57 | phpunit 58 | 59 | For development, you will probably want to mount the repo into the container, so run it like this: 60 | 61 | docker run -it --mount type=bind,source="$(pwd)",target=/usr/src/app phpweaver 62 | 63 | Any changes you make inside the container will now be reflected on your host system. 64 | -------------------------------------------------------------------------------- /bin/phpweaver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setName('PHP Trace Weaver'); 17 | $application->setVersion('dev'); // TODO get tag/commit 18 | 19 | $application->add(new TraceCommand()); 20 | $application->add(new WeaveCommand()); 21 | 22 | $application->run(); 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "troelskn/phpweaver", 3 | "description": "A tool for analysing parameter types in PHP code, using a combination of static and runtime analysis", 4 | "type": "project", 5 | "license": "MIT", 6 | "keywords": ["phpdoc", "phpDocumentor", "documentation", "analyzer", "generator", "docblock"], 7 | "bin": [ 8 | "bin/phpweaver" 9 | ], 10 | "require": { 11 | "php": "^7.1", 12 | "ext-xdebug": "*", 13 | "composer/xdebug-handler": "^1.0", 14 | "symfony/console": "^3.4" 15 | }, 16 | "require-dev": { 17 | "codacy/coverage": "^1.0", 18 | "php-coveralls/php-coveralls": "^2.0", 19 | "phpstan/phpstan": "^0.12.11", 20 | "phpstan/phpstan-phpunit": "^0.12.6", 21 | "phpunit/phpunit": "^9.0", 22 | "vimeo/psalm": "^3.9", 23 | "mockery/mockery": "^1.3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "PHPWeaver\\": "src/PHPWeaver" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "PHPWeaver\\Test\\": "tests/PHPWeaver" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/Bar.php: -------------------------------------------------------------------------------- 1 | obj = $param1; 10 | 11 | return $param2 ?: 42; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/controller.php: -------------------------------------------------------------------------------- 1 | method1(new Example\Bar()); 8 | -------------------------------------------------------------------------------- /example/presentation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ../bin/phpweaver trace controller.php 3 | ../bin/phpweaver weave ./Foo.php 4 | cat Foo.php 5 | -------------------------------------------------------------------------------- /example/readme.txt: -------------------------------------------------------------------------------- 1 | prerequisites: 2 | sudo apt-get install php-cli php-xdebug diff 3 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR-2 ruleset 4 | 5 | 6 | -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/ 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-phpunit/extension.neon 3 | parameters: 4 | paths: 5 | - src/PHPWeaver 6 | - tests 7 | level: max 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/PHPWeaver/ 16 | 17 | 18 | 19 | 20 | ./src/PHPWeaver/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/PHPWeaver/Command/TraceCommand.php: -------------------------------------------------------------------------------- 1 | setName('trace') 22 | ->setDescription('Traces function signatures in a running PHP script') 23 | ->addArgument('phpscript', InputArgument::REQUIRED, 'A PHP script to execute and trace') 24 | ->addArgument('options', InputArgument::OPTIONAL, 'The PHP script is launched with these options') 25 | ->addOption('tracefile', null, InputOption::VALUE_OPTIONAL, 'Where to save trace', 'dumpfile') 26 | ->addOption('append', null, InputOption::VALUE_NONE, 'Append to an existing tracefile') 27 | ->setHelp(<<%command.name% command will execute a PHP script at save the trace data to a file: 29 | 30 | %command.full_name% vendor/bin/phpunit 31 | 32 | You can specify parameteres to be passed to the script as the secound argument: 33 | 34 | %command.full_name% vendor/bin/phpunit -- '-c tests/phpunit.xml' 35 | 36 | By default the trace will be saved to dumpfile.xt, but you can also specify a path (.xt is automattically appended): 37 | 38 | %command.full_name% vendor/bin/phpunit --tracefile=traces/unitest 39 | EOT 40 | ); 41 | } 42 | 43 | /** 44 | * Run the trace process. 45 | * 46 | * @param InputInterface $input 47 | * @param OutputInterface $output 48 | * 49 | * @return int 50 | */ 51 | protected function execute(InputInterface $input, OutputInterface $output): int 52 | { 53 | $tracefile = $input->getOption('tracefile'); 54 | $append = $input->getOption('append'); 55 | if (!is_bool($append)) { 56 | return self::RETURN_CODE_ERROR; 57 | } 58 | $phpscript = $input->getArgument('phpscript'); 59 | if (!is_string($phpscript)) { 60 | return self::RETURN_CODE_ERROR; 61 | } 62 | $options = $input->getArgument('options') ?? ''; 63 | if (!is_string($options)) { 64 | return self::RETURN_CODE_ERROR; 65 | } 66 | 67 | $command = PHP_BINARY; 68 | 69 | /** @var array $params */ 70 | $params = [ 71 | '-d xdebug.collect_includes' => 0, 72 | '-d xdebug.auto_trace' => 1, 73 | '-d xdebug.mode' => 'trace', 74 | '-d xdebug.start_with_request' => 'yes', 75 | '-d xdebug.trace_options' => $append, 76 | '-d xdebug.output_dir' => getcwd(), 77 | '-d xdebug.trace_output_dir' => getcwd(), 78 | '-d xdebug.trace_output_name' => $tracefile, 79 | '-d xdebug.trace_format' => 1, 80 | '-d xdebug.collect_params' => 3, // Track full input value format (same as return format) 81 | '-d xdebug.collect_return' => 1, 82 | '-d xdebug.var_display_max_data' => 20, // Max length of numbers 83 | '-d xdebug.var_display_max_children' => 5, // Analyse the 5 first elements when determining array sub-type 84 | '-d xdebug.var_display_max_depth' => 1, // 1 depth of array (and classes) to analyze array sub-type 85 | ]; 86 | 87 | foreach ($params as $param => $value) { 88 | $command .= ' ' . $param . '=' . (string)$value; 89 | } 90 | $command .= ' ' . $phpscript . ' ' . $options; 91 | 92 | $output->writeln('Running script with instrumentation: ' . $phpscript . ' ' . $options); 93 | passthru($command); 94 | $output->writeln('TRACE COMPLETE'); 95 | 96 | return self::RETURN_CODE_OK; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/PHPWeaver/Command/WeaveCommand.php: -------------------------------------------------------------------------------- 1 | setName('weave') 61 | ->setDescription('Analyze trace and generate phpDoc in target files') 62 | ->addArgument( 63 | 'path', 64 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 65 | 'Path to folder or files to be process' 66 | ) 67 | ->addOption('tracefile', null, InputOption::VALUE_OPTIONAL, 'Trace file to analyze', 'dumpfile') 68 | ->setHelp(<<%command.name% command will analyze function signatures and update there phpDoc: 70 | 71 | %command.full_name% src/ 72 | 73 | By default it will look for the tracefile in the current directory, but you can also specify a path (.xt is automattically appended): 74 | 75 | %command.full_name% src/ --tracefile tests/tracefile 76 | 77 | You can specify multiple paths to process, this way the trace file will only have to be processed once: 78 | 79 | %command.full_name% app/ public/index.php tests/ 80 | EOT 81 | ); 82 | } 83 | 84 | /** 85 | * Run the weave process. 86 | * 87 | * @param InputInterface $input 88 | * @param OutputInterface $output 89 | * 90 | * @return int 91 | */ 92 | protected function execute(InputInterface $input, OutputInterface $output): int 93 | { 94 | // Restart if xdebug is loaded, unless the environment variable PHPWEAVER_ALLOW_XDEBUG is set. 95 | $xdebug = new XdebugHandler('phpweaver', '--ansi'); 96 | $xdebug->check(); 97 | unset($xdebug); 98 | 99 | $pathsToWeave = $input->getArgument('path'); 100 | if (!is_array($pathsToWeave)) 101 | return self::RETURN_CODE_ERROR; 102 | $tracefile = $input->getOption('tracefile'); 103 | if (!is_string($tracefile)) 104 | return self::RETURN_CODE_ERROR; 105 | $tracefile .= '.xt'; 106 | 107 | $this->output = new SymfonyStyle($input, $output); 108 | 109 | $filesToWeave = $this->getFilesToProcess($pathsToWeave); 110 | 111 | $sigs = $this->parseTrace($tracefile); 112 | $this->transformFiles($filesToWeave, $sigs); 113 | 114 | return self::RETURN_CODE_OK; 115 | } 116 | 117 | /** 118 | * Fetch array of file names to process. 119 | * 120 | * @param string[] $pathsToWeave 121 | * 122 | * @return array 123 | */ 124 | private function getFilesToProcess(array $pathsToWeave): array 125 | { 126 | $filesToWeave = []; 127 | 128 | foreach ($pathsToWeave as $pathToWeave) { 129 | if (is_dir($pathToWeave)) { 130 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($pathToWeave)); 131 | $fileIterator = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); 132 | foreach ($fileIterator as $file) { 133 | $filesToWeave[] = $file[0]; 134 | } 135 | 136 | continue; 137 | } 138 | 139 | if (!is_file($pathToWeave)) { 140 | throw new Exception('Path (' . $pathToWeave . ') isn\'t readable'); 141 | } 142 | 143 | $filesToWeave[] = $pathToWeave; 144 | } 145 | 146 | return $filesToWeave; 147 | } 148 | 149 | /** 150 | * Parse the trace file. 151 | * 152 | * @param string $tracefile 153 | * 154 | * @return Signatures 155 | */ 156 | private function parseTrace(string $tracefile): Signatures 157 | { 158 | $sigs = new Signatures(); 159 | if (is_file($tracefile)) { 160 | $traceFile = new SplFileObject($tracefile); 161 | $trace = new TraceReader(new FunctionTracer(new TraceSignatureLogger($sigs))); 162 | 163 | $traceFile->setFlags(SplFileObject::READ_AHEAD); 164 | $this->progressBarStart(iterator_count($traceFile), 'Parsing tracefile …'); 165 | foreach ($traceFile as $line) { 166 | if (!is_string($line)) { 167 | throw new Exception('Unable to read trace file'); 168 | } 169 | $trace->processLine($line); 170 | $this->progressBarAdvance(); 171 | } 172 | 173 | $this->progressBarEnd(); 174 | } 175 | 176 | return $sigs; 177 | } 178 | 179 | /** 180 | * Process files and insert phpDoc. 181 | * 182 | * @refactor Avoid need to check if scanner and trasformer where created 183 | * 184 | * @param string[] $filesToWeave 185 | * @param Signatures $sigs 186 | * 187 | * @return void 188 | */ 189 | private function transformFiles(array $filesToWeave, Signatures $sigs): void 190 | { 191 | $this->progressBarStart(count($filesToWeave), 'Updating source files …'); 192 | 193 | foreach ($filesToWeave as $fileToWeave) { 194 | $this->setupFileProcesser($sigs); 195 | if (null === $this->scanner || null === $this->transformer) { 196 | throw new Exception('Failed to initialize scanner'); 197 | } 198 | $this->tokenizer = new TokenStreamParser(); 199 | $fileContent = file_get_contents($fileToWeave); 200 | if (false === $fileContent) { 201 | throw new Exception('Unable to read source file: ' . $fileToWeave); 202 | } 203 | $tokenStream = $this->tokenizer->scan($fileContent); 204 | $tokenStream->iterate($this->scanner); 205 | 206 | file_put_contents($fileToWeave, $this->transformer->getOutput()); 207 | 208 | $this->progressBarAdvance(); 209 | } 210 | 211 | $this->progressBarEnd(); 212 | } 213 | 214 | /** 215 | * Initialize the php parser. 216 | * 217 | * @param Signatures $sigs 218 | * 219 | * @return void 220 | */ 221 | private function setupFileProcesser(Signatures $sigs): void 222 | { 223 | $this->scanner = new ScannerMultiplexer(); 224 | $parametersScanner = new FunctionParametersScanner(); 225 | $functionBodyScanner = new FunctionBodyScanner(); 226 | $modifiersScanner = new ModifiersScanner(); 227 | $classScanner = new ClassScanner(); 228 | $namespaceScanner = new NamespaceScanner(); 229 | $editor = new TracerDocBlockEditor( 230 | $sigs, 231 | $classScanner, 232 | $functionBodyScanner, 233 | $parametersScanner, 234 | $namespaceScanner 235 | ); 236 | 237 | $this->transformer = new DocCommentEditorTransformer( 238 | $functionBodyScanner, 239 | $modifiersScanner, 240 | $parametersScanner, 241 | $editor 242 | ); 243 | 244 | $this->scanner->appendScanners([ 245 | $parametersScanner, 246 | $functionBodyScanner, 247 | $modifiersScanner, 248 | $classScanner, 249 | $namespaceScanner, 250 | $this->transformer, 251 | ]); 252 | } 253 | 254 | /** 255 | * Start a progressbar on the ouput. 256 | * 257 | * @refactor Avoid need to check if output has been created 258 | * 259 | * @param int $steps 260 | * @param string $message 261 | * 262 | * @return void 263 | */ 264 | private function progressBarStart(int $steps, string $message): void 265 | { 266 | if (!$steps) { 267 | return; 268 | } 269 | 270 | if (null === $this->output) { 271 | throw new Exception('Output not set'); 272 | } 273 | 274 | $this->progressBar = $this->output->createProgressBar(); 275 | $this->progressBar->setBarWidth(50); 276 | 277 | $this->progressBar->setMessage($message); 278 | $this->progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\n%message%"); 279 | $this->progressBar->start($steps); 280 | 281 | $this->nextSteps = 0; 282 | $this->nextUpdate = microtime(true) + self::REFRESH_RATE_INTERVAL; 283 | } 284 | 285 | /** 286 | * Advance the progress bare by steps. 287 | * 288 | * Rate limited to avoid performance issues. 289 | * 290 | * @param int $steps 291 | * 292 | * @return void 293 | */ 294 | private function progressBarAdvance(int $steps = 1): void 295 | { 296 | if (!$this->progressBar) { 297 | return; 298 | } 299 | 300 | $this->nextSteps += $steps; 301 | 302 | if (microtime(true) <= $this->nextUpdate) { 303 | return; 304 | } 305 | 306 | $this->progressBar->advance($this->nextSteps); 307 | $this->nextSteps = 0; 308 | $this->nextUpdate = microtime(true) + self::REFRESH_RATE_INTERVAL; 309 | } 310 | 311 | /** 312 | * Set the progress to 100% and clear it from the output. 313 | * 314 | * @return void 315 | */ 316 | private function progressBarEnd(): void 317 | { 318 | if (!$this->progressBar) { 319 | return; 320 | } 321 | 322 | $this->progressBar->finish(); 323 | $this->progressBar->clear(); 324 | $this->progressBar = null; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/PHPWeaver/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | isA(T_INTERFACE) || $token->isA(T_CLASS)) { 21 | $this->state = 1; 22 | 23 | return; 24 | } 25 | 26 | if ($token->isA(T_STRING) && 1 === $this->state) { 27 | $this->state = 2; 28 | $this->currentClass = $token->getText(); 29 | $this->currentClassScope = $token->getDepth(); 30 | 31 | return; 32 | } 33 | 34 | if (2 === $this->state && $token->getDepth() > $this->currentClassScope) { 35 | $this->state = 3; 36 | 37 | return; 38 | } 39 | 40 | if (3 === $this->state && $token->getDepth() === $this->currentClassScope) { 41 | $this->state = 0; 42 | $this->currentClass = ''; 43 | 44 | return; 45 | } 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getCurrentClass(): string 52 | { 53 | return $this->currentClass; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/FunctionBodyScanner.php: -------------------------------------------------------------------------------- 1 | isA(T_FUNCTION)) { 21 | $this->currentClassScope = $token->getDepth(); 22 | $this->state = 1; 23 | } elseif (1 === $this->state && $token->isA(T_STRING)) { 24 | $this->name = $token->getText(); 25 | $this->state = 2; 26 | } elseif (2 === $this->state && $token->getDepth() > $this->currentClassScope) { 27 | $this->state = 3; 28 | } elseif (3 === $this->state && $token->getDepth() === $this->currentClassScope) { 29 | $this->state = 0; 30 | } 31 | } 32 | 33 | /** 34 | * @return bool 35 | */ 36 | public function isActive(): bool 37 | { 38 | return $this->state > 2; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName(): string 45 | { 46 | return $this->name; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/FunctionParametersScanner.php: -------------------------------------------------------------------------------- 1 | isA(T_FUNCTION)) { 21 | $this->state = 1; 22 | } elseif (1 === $this->state && '(' === $token->getText()) { 23 | $this->signature = []; 24 | $this->signature[] = $token; 25 | $this->parenCount = 1; 26 | $this->state = 2; 27 | } elseif (2 === $this->state) { 28 | $this->signature[] = $token; 29 | if ('(' === $token->getText()) { 30 | ++$this->parenCount; 31 | } elseif (')' === $token->getText()) { 32 | --$this->parenCount; 33 | } 34 | if (0 === $this->parenCount) { 35 | $this->state = 0; 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * @return bool 42 | */ 43 | public function isActive(): bool 44 | { 45 | return 0 !== $this->state; 46 | } 47 | 48 | /** 49 | * @return string[] 50 | */ 51 | public function getCurrentSignatureAsTypeMap(): array 52 | { 53 | $current = null; 54 | $map = []; 55 | foreach ($this->signature as $token) { 56 | if (T_VARIABLE === $token->getToken()) { 57 | $map[$token->getText()] = $current ? $current : '???'; 58 | } elseif (',' === $token->getText()) { 59 | $current = null; 60 | } elseif (T_STRING === $token->getToken()) { 61 | $current = $token->getText(); 62 | } 63 | } 64 | 65 | return $map; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/ModifiersScanner.php: -------------------------------------------------------------------------------- 1 | isModifyer($token)) { 17 | $this->state = 1; 18 | 19 | return; 20 | } 21 | 22 | if ($this->isModifyable($token)) { 23 | $this->state = 0; 24 | 25 | return; 26 | } 27 | } 28 | 29 | /** 30 | * @param Token $token 31 | * 32 | * @return bool 33 | */ 34 | private function isModifyable(Token $token): bool 35 | { 36 | return $token->isA(T_INTERFACE) 37 | || $token->isA(T_CLASS) 38 | || $token->isA(T_FUNCTION) 39 | || $token->isA(T_VARIABLE); 40 | } 41 | 42 | /** 43 | * @param Token $token 44 | * 45 | * @return bool 46 | */ 47 | private function isModifyer(Token $token): bool 48 | { 49 | return $token->isA(T_PRIVATE) 50 | || $token->isA(T_PROTECTED) 51 | || $token->isA(T_PUBLIC) 52 | || $token->isA(T_FINAL) 53 | || $token->isA(T_STATIC) 54 | || $token->isA(T_ABSTRACT); 55 | } 56 | 57 | /** 58 | * @return bool 59 | */ 60 | public function isActive(): bool 61 | { 62 | return 1 === $this->state; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/NamespaceScanner.php: -------------------------------------------------------------------------------- 1 | isA(T_NAMESPACE)) { 19 | $this->state = 1; 20 | $this->currentNamespace = ''; 21 | 22 | return; 23 | } 24 | 25 | if (1 === $this->state && $token->isA(T_STRING)) { 26 | $this->currentNamespace .= $token->getText() . '\\'; 27 | 28 | return; 29 | } 30 | 31 | if (1 === $this->state && $token->isA(-1)) { 32 | $this->state = 0; 33 | 34 | return; 35 | } 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getCurrentNamespace(): string 42 | { 43 | return $this->currentNamespace; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/ScannerInterface.php: -------------------------------------------------------------------------------- 1 | scanners[] = $scanner; 17 | } 18 | 19 | /** 20 | * @param ScannerInterface[] $scanners 21 | * 22 | * @return void 23 | */ 24 | public function appendScanners(array $scanners): void 25 | { 26 | foreach ($scanners as $scanner) { 27 | $this->appendScanner($scanner); 28 | } 29 | } 30 | 31 | /** 32 | * @param Token $token 33 | * 34 | * @return void 35 | */ 36 | public function accept(Token $token): void 37 | { 38 | foreach ($this->scanners as $scanner) { 39 | $scanner->accept($token); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/Token.php: -------------------------------------------------------------------------------- 1 | text = $text; 21 | $this->token = $token; 22 | $this->depth = $depth; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getText(): string 29 | { 30 | return $this->text; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getToken(): int 37 | { 38 | return $this->token; 39 | } 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getDepth(): int 45 | { 46 | return $this->depth; 47 | } 48 | 49 | /** 50 | * @param int $type 51 | * 52 | * @return bool 53 | */ 54 | public function isA(int $type): bool 55 | { 56 | return $this->getToken() === $type; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/TokenBuffer.php: -------------------------------------------------------------------------------- 1 | super = $super; 17 | } 18 | 19 | /** 20 | * @param Token $token 21 | * 22 | * @return void 23 | */ 24 | public function prepend(Token $token): void 25 | { 26 | array_unshift($this->tokens, $token); 27 | } 28 | 29 | /** 30 | * @param Token $token 31 | * 32 | * @return void 33 | */ 34 | public function append(Token $token): void 35 | { 36 | $this->tokens[] = $token; 37 | } 38 | 39 | /** 40 | * @return ?Token 41 | */ 42 | public function getFirstToken(): ?Token 43 | { 44 | return isset($this->tokens[0]) ? $this->tokens[0] : null; 45 | } 46 | 47 | /** 48 | * @param Token $token 49 | * @param Token $newToken 50 | * 51 | * @return void 52 | */ 53 | public function replaceToken(Token $token, Token $newToken): void 54 | { 55 | $tmp = []; 56 | foreach ($this->tokens as $existingToken) { 57 | if ($existingToken === $token) { 58 | $tmp[] = $newToken; 59 | continue; 60 | } 61 | 62 | $tmp[] = $existingToken; 63 | } 64 | 65 | $this->tokens = $tmp; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function hasSuper(): bool 72 | { 73 | return (bool) $this->super; 74 | } 75 | 76 | /** 77 | * @return self 78 | */ 79 | public function raise(): self 80 | { 81 | return new self($this); 82 | } 83 | 84 | /** 85 | * @return self 86 | */ 87 | public function flush(): self 88 | { 89 | if (!$this->super) { 90 | return $this; 91 | } 92 | $tokens = $this->tokens; 93 | $this->tokens = []; 94 | foreach ($tokens as $token) { 95 | $this->super->append($token); 96 | } 97 | 98 | return $this->super; 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function toText(): string 105 | { 106 | $out = ''; 107 | foreach ($this->tokens as $token) { 108 | $out .= $token->getText(); 109 | } 110 | 111 | return $out; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/TokenStream.php: -------------------------------------------------------------------------------- 1 | tokens[] = $token; 17 | } 18 | 19 | /** 20 | * @param ScannerInterface $scanner 21 | * 22 | * @return void 23 | */ 24 | public function iterate(ScannerInterface $scanner): void 25 | { 26 | foreach ($this->tokens as $token) { 27 | $scanner->accept($token); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/PHPWeaver/Scanner/TokenStreamParser.php: -------------------------------------------------------------------------------- 1 | tokenstream */ 4 | class TokenStreamParser 5 | { 6 | /** 7 | * @param string $source 8 | * 9 | * @return TokenStream 10 | */ 11 | public function scan(string $source): TokenStream 12 | { 13 | //todo: track indentation 14 | $stream = new TokenStream(); 15 | $depth = 0; 16 | foreach (token_get_all($source, TOKEN_PARSE) as $token) { 17 | $text = $token; 18 | $token = -1; 19 | if (is_array($text)) { 20 | [$token, $text] = $text; 21 | } 22 | if (T_CURLY_OPEN === $token || T_DOLLAR_OPEN_CURLY_BRACES === $token || '{' === $text) { 23 | ++$depth; 24 | } elseif ('}' == $text) { 25 | --$depth; 26 | } 27 | $stream->append(new Token($text, $token, $depth)); 28 | } 29 | 30 | return $stream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PHPWeaver/Signature/FunctionArgument.php: -------------------------------------------------------------------------------- 1 | */ 8 | protected $types = []; 9 | 10 | public function __construct(int $id) 11 | { 12 | $this->id = $id; 13 | } 14 | 15 | /** 16 | * @return bool 17 | */ 18 | public function isUndefined(): bool 19 | { 20 | return !$this->types; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getType(): string 27 | { 28 | if ($this->isUndefined()) { 29 | return 'mixed'; 30 | } 31 | 32 | $types = $this->types; 33 | 34 | // Falsable to bool 35 | if (isset($types['false']) && (isset($types['bool']) || 1 === count($types))) { 36 | unset($types['false']); 37 | $types['bool'] = true; 38 | } 39 | 40 | $types = $this->orderTypes($types); 41 | 42 | $types = array_keys($types); 43 | 44 | return implode('|', $types); 45 | } 46 | 47 | /** 48 | * @param array $types 49 | * @return array 50 | */ 51 | private function orderTypes(array $types): array 52 | { 53 | ksort($types); 54 | 55 | // False should always be at the end 56 | if (isset($types['false'])) { 57 | unset($types['false']); 58 | $types['false'] = true; 59 | } 60 | 61 | // Null should always be at the end 62 | if (isset($types['null'])) { 63 | unset($types['null']); 64 | $types['null'] = true; 65 | } 66 | 67 | return $types; 68 | } 69 | 70 | /** 71 | * @param string $type 72 | * 73 | * @return void 74 | */ 75 | public function collateWith(string $type): void 76 | { 77 | if ('???' === $type) { 78 | return; 79 | } 80 | 81 | $this->types[$type] = true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/PHPWeaver/Signature/FunctionSignature.php: -------------------------------------------------------------------------------- 1 | returnType = new FunctionArgument(0); 15 | } 16 | 17 | /** 18 | * @param string[] $arguments 19 | * @param string $returnType 20 | * 21 | * @return void 22 | */ 23 | public function blend(array $arguments, string $returnType): void 24 | { 25 | foreach ($arguments as $id => $type) { 26 | $arg = $this->getArgumentById($id); 27 | $arg->collateWith($type); 28 | } 29 | 30 | if ($returnType) { 31 | $this->returnType->collateWith($returnType); 32 | } 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getReturnType(): string 39 | { 40 | return $this->returnType->getType(); 41 | } 42 | 43 | /** 44 | * @param int $id 45 | * 46 | * @return FunctionArgument 47 | */ 48 | public function getArgumentById(int $id): FunctionArgument 49 | { 50 | if (!isset($this->arguments[$id])) { 51 | $this->arguments[$id] = new FunctionArgument($id); 52 | } 53 | 54 | return $this->arguments[$id]; 55 | } 56 | 57 | /** 58 | * @return FunctionArgument[] 59 | */ 60 | public function getArguments(): array 61 | { 62 | $args = $this->arguments; 63 | ksort($args); 64 | 65 | return $args; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PHPWeaver/Signature/Signatures.php: -------------------------------------------------------------------------------- 1 | ' : '') . $func); 25 | 26 | return isset($this->signaturesArray[$name]); 27 | } 28 | 29 | /** 30 | * @param string $func 31 | * @param string $class 32 | * @param string $namespace 33 | * 34 | * @return FunctionSignature 35 | */ 36 | public function get(string $func, string $class = '', string $namespace = ''): FunctionSignature 37 | { 38 | if (!$func) { 39 | throw new Exception('Illegal identifier: {' . "$func, $class, $namespace" . '}'); 40 | } 41 | $name = strtolower($namespace . ($class ? $class . '->' : '') . $func); 42 | if (!isset($this->signaturesArray[$name])) { 43 | $this->signaturesArray[$name] = new FunctionSignature(); 44 | } 45 | 46 | return $this->signaturesArray[$name]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PHPWeaver/Transform/BufferEditorInterface.php: -------------------------------------------------------------------------------- 1 | functionBodyScanner = $functionBodyScanner; 37 | $this->modifiersScanner = $modifiersScanner; 38 | $this->parametersScanner = $parametersScanner; 39 | $this->editor = $editor; 40 | $this->buffer = new TokenBuffer(); 41 | } 42 | 43 | /** 44 | * @param Token $token 45 | * 46 | * @return void 47 | */ 48 | public function accept(Token $token): void 49 | { 50 | if ($token->isA(T_DOC_COMMENT)) { 51 | $this->state = 1; 52 | $this->raiseBuffer(); 53 | } elseif (0 === $this->state && ($this->modifiersScanner->isActive() || $token->isA(T_FUNCTION))) { 54 | $this->state = 1; 55 | $this->raiseBuffer(); 56 | } elseif ($this->state > 0 && $this->functionBodyScanner->isActive()) { 57 | $this->editor->editBuffer($this->buffer); 58 | $this->state = 0; 59 | $this->flushBuffers(); 60 | } elseif ($token->isA(T_INTERFACE) 61 | || $token->isA(T_CLASS) 62 | || ($token->isA(T_VARIABLE) && !$this->parametersScanner->isActive()) 63 | ) { 64 | $this->state = 0; 65 | $this->flushBuffers(); 66 | } 67 | $this->buffer->append($token); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | public function raiseBuffer(): void 74 | { 75 | $this->flushBuffers(); 76 | $this->buffer = $this->buffer->raise(); 77 | } 78 | 79 | /** 80 | * @return void 81 | */ 82 | public function flushBuffers(): void 83 | { 84 | while ($this->buffer->hasSuper()) { 85 | $this->buffer = $this->buffer->flush(); 86 | } 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getOutput(): string 93 | { 94 | $this->flushBuffers(); 95 | 96 | return $this->buffer->toText(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/PHPWeaver/Transform/TracerDocBlockEditor.php: -------------------------------------------------------------------------------- 1 | signatures = $signatures; 41 | $this->classScanner = $classScanner; 42 | $this->functionBodyScanner = $functionBodyScanner; 43 | $this->parametersScanner = $parametersScanner; 44 | $this->namespaceScanner = $namespaceScanner; 45 | } 46 | 47 | /** 48 | * @param string $func 49 | * @param string $class 50 | * @param string[] $params 51 | * 52 | * @return ?string 53 | */ 54 | public function generateDoc(string $func, string $class = '', array $params = [], string $namespace = ''): ?string 55 | { 56 | if ((!$params && ('__construct' === $func || '__destruct' === $func)) 57 | || !$this->signatures->has($func, $class, $namespace) 58 | ) { 59 | return null; 60 | } 61 | 62 | $signature = $this->signatures->get($func, $class, $namespace); 63 | 64 | $key = 0; 65 | $longestType = 0; 66 | $seenArguments = $signature->getArguments(); 67 | foreach ($params as $name => $type) { 68 | $seenArgument = $seenArguments[$key]; 69 | $seenArgument->collateWith($type); 70 | $longestType = max((int)mb_strlen($seenArgument->getType()), $longestType); 71 | $params[$name] = $seenArgument->getType(); 72 | ++$key; 73 | } 74 | 75 | $doc = "/**\n"; 76 | foreach ($params as $name => $type) { 77 | $doc .= ' * @param ' . $type . str_repeat(' ', $longestType - (int)mb_strlen($type) + 1) . $name . "\n"; 78 | } 79 | if ('__construct' !== $func && '__destruct' !== $func) { 80 | if ($params) { 81 | $doc .= " *\n"; 82 | } 83 | $doc .= ' * @return ' . $signature->getReturnType() . "\n"; 84 | } 85 | $doc .= ' */'; 86 | 87 | return $doc; 88 | } 89 | 90 | /** 91 | * @param TokenBuffer $buffer 92 | * 93 | * @return void 94 | */ 95 | public function editBuffer(TokenBuffer $buffer): void 96 | { 97 | $text = $this->generateDoc( 98 | $this->functionBodyScanner->getName(), 99 | $this->classScanner->getCurrentClass(), 100 | $this->parametersScanner->getCurrentSignatureAsTypeMap(), 101 | $this->namespaceScanner->getCurrentNamespace() 102 | ); 103 | if (null === $text) { 104 | return; 105 | } 106 | 107 | $firstToken = $buffer->getFirstToken(); 108 | if (null === $firstToken) { 109 | throw new Exception('Failed to find insert point for phpDoc'); 110 | } 111 | 112 | if (!$firstToken->isA(T_DOC_COMMENT)) { 113 | $buffer->prepend(new Token("\n ", -1, $firstToken->getDepth())); 114 | $buffer->prepend(new Token("\n /**\n */", T_DOC_COMMENT, $firstToken->getDepth())); 115 | } 116 | 117 | /** @var Token */ 118 | $current = $buffer->getFirstToken(); 119 | $newToken = new Token($text, $current->getToken(), $current->getDepth()); 120 | $buffer->replaceToken($current, $newToken); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/PHPWeaver/Transform/TransformerInterface.php: -------------------------------------------------------------------------------- 1 | */ 8 | protected $stack = []; 9 | /** @var string[] */ 10 | protected $internalFunctions = ['{main}', 'eval', 'include', 'include_once', 'require', 'require_once']; 11 | 12 | /** 13 | * @param TraceSignatureLogger $handler 14 | */ 15 | public function __construct(TraceSignatureLogger $handler) 16 | { 17 | $this->handler = $handler; 18 | } 19 | 20 | /** 21 | * @param array $arguments 22 | * @return void 23 | */ 24 | public function functionCall(int $id, string $function, array $arguments): void 25 | { 26 | $this->closeVoidReturn(); 27 | 28 | if (in_array($function, $this->internalFunctions, true)) { 29 | return; 30 | } 31 | 32 | $this->stack[$id] = new Trace($function, $arguments, false); 33 | } 34 | 35 | /** 36 | * Set exit to true for the given call id. 37 | * 38 | * @param int $id 39 | * 40 | * @return void 41 | */ 42 | public function markCallAsExited(int $id): void 43 | { 44 | $this->closeVoidReturn(); 45 | 46 | if (!key_exists($id, $this->stack)) { 47 | return; 48 | } 49 | 50 | $this->stack[$id]->exited = true; 51 | } 52 | 53 | /** 54 | * Match a return value with the function call and log it. 55 | * 56 | * Note: The optimizer will remove unused retun values making them look like void returns 57 | * 58 | * @param int $id 59 | * @param string $value 60 | * 61 | * @return void 62 | */ 63 | public function returnValue(int $id, string $value = 'void'): void 64 | { 65 | if (!key_exists($id, $this->stack)) { 66 | return; 67 | } 68 | 69 | $functionCall = $this->stack[$id]; 70 | unset($this->stack[$id]); 71 | 72 | $functionCall->returnValue = $value; 73 | $this->handler->log($functionCall); 74 | } 75 | 76 | /** 77 | * Set void as return type for prvious call if it has already exitede. 78 | * 79 | * @return void 80 | */ 81 | private function closeVoidReturn(): void 82 | { 83 | $last = end($this->stack); 84 | $key = key($this->stack); 85 | if (false === $last || null === $key) { 86 | return; 87 | } 88 | 89 | if ($last->exited) { 90 | $this->returnValue($key); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/PHPWeaver/Xtrace/Trace.php: -------------------------------------------------------------------------------- 1 | */ 8 | public $arguments; 9 | /** @var bool */ 10 | public $exited; 11 | /** @var string */ 12 | public $returnValue = 'void'; 13 | 14 | /** 15 | * @param string[] $arguments 16 | */ 17 | public function __construct(string $function, array $arguments, bool $exited) 18 | { 19 | $this->function = $function; 20 | $this->arguments = $arguments; 21 | $this->exited = $exited; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PHPWeaver/Xtrace/TraceReader.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 17 | } 18 | 19 | /** 20 | * Process a trace line. 21 | * 22 | * @param string $line 23 | * 24 | * @return void 25 | */ 26 | public function processLine(string $line): void 27 | { 28 | /** @var array */ 29 | $entry = str_getcsv($line, "\t"); 30 | 31 | if (!isset($entry[2])) { 32 | return; // Header or footer 33 | } 34 | 35 | switch ($entry[2]) { 36 | case '0': 37 | if ('0' === $entry[6]) { 38 | return; // Internal function 39 | } 40 | 41 | $this->handler->functionCall((int)$entry[1], $entry[5], array_slice($entry, 11)); 42 | break; 43 | case '1': 44 | $this->handler->markCallAsExited((int)$entry[1]); 45 | break; 46 | case 'R': 47 | $this->handler->returnValue((int)$entry[1], $entry[5]); 48 | break; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PHPWeaver/Xtrace/TraceSignatureLogger.php: -------------------------------------------------------------------------------- 1 | 'bool', 13 | 'FALSE' => 'false', // Falsable or tbd. bool 14 | 'NULL' => 'null', 15 | 'void' => 'void', 16 | '???' => '???', 17 | '*uninitialized*' => '???', 18 | '...' => 'array', 19 | ]; 20 | 21 | /** 22 | * @param Signatures $signatures 23 | */ 24 | public function __construct(Signatures $signatures) 25 | { 26 | $this->signatures = $signatures; 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | public function log(Trace $trace): void 33 | { 34 | $sig = $this->signatures->get($trace->function); 35 | $sig->blend( 36 | $this->parseArguments($trace->arguments), 37 | $this->parseType($trace->returnValue) 38 | ); 39 | } 40 | 41 | /** 42 | * @param string[] $arguments 43 | * 44 | * @return string[] 45 | */ 46 | private function parseArguments(array $arguments): array 47 | { 48 | $types = []; 49 | foreach ($arguments as $type) { 50 | $types[] = $this->parseType($type); 51 | } 52 | 53 | return $types; 54 | } 55 | 56 | /** 57 | * @param string $type 58 | * 59 | * @todo fuzzy type detection (float or int in string) 60 | * 61 | * @throws Exception 62 | * 63 | * @return string 64 | */ 65 | public function parseType(string $type): string 66 | { 67 | if (isset($this->typeMapping[$type])) { 68 | return $this->typeMapping[$type]; 69 | } 70 | 71 | $typeTransforms = ['~^(array) \(.*\)$~s', '~^class (\S+)~s', '~^(resource)\(\d+\)~s']; 72 | foreach ($typeTransforms as $regex) { 73 | if (preg_match($regex, $type, $match)) { 74 | if ('array' === $match[1]) { 75 | return $this->getArrayType($type); 76 | } 77 | 78 | return $match[1]; 79 | } 80 | } 81 | 82 | if (preg_match('~^\[.*\]$~s', $type, $match)) { 83 | return $this->getArrayType($type, true); 84 | } 85 | 86 | if (is_numeric($type)) { 87 | if (preg_match('~^-?\d+$~', $type)) { 88 | return 'int'; 89 | } 90 | 91 | return 'float'; 92 | } 93 | 94 | if ("'" === substr($type, 0, 1)) { 95 | return 'string'; 96 | } 97 | 98 | throw new Exception('Unknown type: ' . $type); 99 | } 100 | 101 | /** 102 | * Determin the array sub-type. 103 | * 104 | * @param string $arrayType 105 | * 106 | * @return string 107 | */ 108 | public function getArrayType(string $arrayType, bool $xdebug3 = false): string 109 | { 110 | $subTypes = []; 111 | $elementTypes = $this->getArrayElements($arrayType, $xdebug3); 112 | foreach ($elementTypes as $elementType) { 113 | $subTypes[$this->parseType($elementType)] = true; 114 | } 115 | 116 | return $this->formatArrayType($subTypes); 117 | } 118 | 119 | /** 120 | * Extract the array elements from an array trace. 121 | * 122 | * @param string $type 123 | * 124 | * @return array 125 | */ 126 | private function getArrayElements(string $type, bool $xdebug3 = false): array 127 | { 128 | // Remove array wrapper 129 | if ($xdebug3) { 130 | preg_match('~^\[(.*?)(?:, )?\.{0,3}\]$~s', $type, $match); 131 | } else { 132 | preg_match('~^array \((.*?)(?:, )?\.{0,3}\)$~s', $type, $match); 133 | } 134 | if (empty($match[1])) { 135 | return []; 136 | } 137 | 138 | // Find each string|int key followed by double arrow, taking \' into account 139 | $rawSubTypes = preg_split('~(?:, |^)(?:(?:\'.+?(?:(? ~s', $match[1]); 140 | if (false === $rawSubTypes) { 141 | throw new Exception('Unable to build regex'); 142 | } 143 | unset($rawSubTypes[0]); // Remove split at first key 144 | 145 | return array_values($rawSubTypes); 146 | } 147 | 148 | /** 149 | * Format an array of types as an array with a sub-type. 150 | * 151 | * @todo Find common class/interface/trait for object types 152 | * 153 | * @param array $subTypes 154 | * 155 | * @return string 156 | */ 157 | private function formatArrayType(array $subTypes): string 158 | { 159 | if (!$subTypes) { 160 | return 'array'; 161 | } 162 | 163 | ksort($subTypes); 164 | $type = implode('|', array_keys($subTypes)); 165 | if (count($subTypes) > 1) { 166 | $type = '(' . $type . ')'; 167 | } 168 | 169 | return $type . '[]'; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | /** 14 | * @param string $file 15 | * 16 | * @return ?object 17 | */ 18 | function includeIfExists(string $file) 19 | { 20 | /** @psalm-suppress UnresolvableInclude */ 21 | return file_exists($file) ? include $file : null; 22 | } 23 | 24 | if ((!$loader = includeIfExists(__DIR__ . '/../vendor/autoload.php')) 25 | && (!$loader = includeIfExists(__DIR__ . '/../../../autoload.php')) 26 | ) { 27 | echo 'You must set up the project dependencies using `composer install`' . PHP_EOL . 28 | 'See https://getcomposer.org/download/ for instructions on installing Composer' . PHP_EOL; 29 | exit(1); 30 | } 31 | 32 | return $loader; 33 | -------------------------------------------------------------------------------- /tests/PHPWeaver/ClassScannerTest.php: -------------------------------------------------------------------------------- 1 | scan('iterate($scanner); 18 | static::assertSame('', $scanner->getCurrentClass()); 19 | } 20 | 21 | /** 22 | * @return void 23 | */ 24 | public function testForgetsClassWhenScopeEndsWithinNestedScopes(): void 25 | { 26 | $scanner = new ClassScanner(); 27 | $tokenizer = new TokenStreamParser(); 28 | $tokenStream = $tokenizer->scan('iterate($scanner); 30 | static::assertSame('', $scanner->getCurrentClass()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/PHPWeaver/CollationTest.php: -------------------------------------------------------------------------------- 1 | curdir = getcwd() ?: ''; 41 | $dirSandbox = $this->sandbox(); 42 | mkdir($dirSandbox); 43 | $sourceMain = 'curdir); 62 | $dirSandbox = $this->sandbox(); 63 | unlink($dirSandbox . '/main.php'); 64 | if (is_file($dirSandbox . '/dumpfile.xt')) { 65 | unlink($dirSandbox . '/dumpfile.xt'); 66 | } 67 | rmdir($dirSandbox); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | public function testCanCollateClasses(): void 74 | { 75 | chdir($this->sandbox()); 76 | $commandTester = new CommandTester(new TraceCommand()); 77 | $commandTester->execute(['phpscript' => $this->sandbox() . '/main.php']); 78 | $sigs = new Signatures(); 79 | $trace = new TraceReader(new FunctionTracer(new TraceSignatureLogger($sigs))); 80 | foreach (new SplFileObject($this->sandbox() . '/dumpfile.xt') as $line) { 81 | static::assertIsString($line); 82 | $trace->processLine($line); 83 | } 84 | static::assertSame('Bar|Cuux', $sigs->get('do_stuff')->getArgumentById(0)->getType()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/PHPWeaver/DocCommentEditorTransformerTest.php: -------------------------------------------------------------------------------- 1 | appendScanner($parametersScanner); 30 | $functionBodyScanner = new FunctionBodyScanner(); 31 | $scanner->appendScanner($functionBodyScanner); 32 | $modifiersScanner = new ModifiersScanner(); 33 | $scanner->appendScanner($modifiersScanner); 34 | $transformer = new DocCommentEditorTransformer( 35 | $functionBodyScanner, 36 | $modifiersScanner, 37 | $parametersScanner, 38 | $editor 39 | ); 40 | $scanner->appendScanner($transformer); 41 | $tokenizer = new TokenStreamParser(); 42 | $tokenStream = $tokenizer->scan($source); 43 | $tokenStream->iterate($scanner); 44 | 45 | return $transformer; 46 | } 47 | 48 | /** 49 | * @return void 50 | */ 51 | public function testInputReturnsOutput(): void 52 | { 53 | $source = 'scan($source); 55 | static::assertSame($source, $transformer->getOutput()); 56 | } 57 | 58 | /** 59 | * @return void 60 | */ 61 | public function testInvokesEditorOnFunction(): void 62 | { 63 | $source = 'scan($source, $mockEditor); 66 | assert($mockEditor->buffer instanceof TokenBuffer); 67 | static::assertSame('function bar($x) ', $mockEditor->buffer->toText()); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | public function testInvokesEditorOnFunctionModifiers(): void 74 | { 75 | $source = 'scan($source, $mockEditor); 78 | assert($mockEditor->buffer instanceof TokenBuffer); 79 | static::assertSame('abstract function bar($x) ', $mockEditor->buffer->toText()); 80 | } 81 | 82 | /** 83 | * @return void 84 | */ 85 | public function testDoesntInvokeEditorOnClassModifiers(): void 86 | { 87 | $source = 'scan($source, $mockEditor); 90 | static::assertNull($mockEditor->buffer); 91 | } 92 | 93 | /** 94 | * @return void 95 | */ 96 | public function testInvokesEditorOnDocblock(): void 97 | { 98 | $source = 'scan($source, $mockEditor); 101 | assert($mockEditor->buffer instanceof TokenBuffer); 102 | $token = $mockEditor->buffer->getFirstToken(); 103 | assert($token instanceof Token); 104 | static::assertTrue($token->isA(T_DOC_COMMENT)); 105 | static::assertSame('/** Lorem Ipsum */' . "\n" . 'function bar($x) ', $mockEditor->buffer->toText()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/PHPWeaver/FunctionBodyScannerTest.php: -------------------------------------------------------------------------------- 1 | scan('iterate($scanner); 18 | static::assertFalse($scanner->isActive()); 19 | } 20 | 21 | /** 22 | * @return void 23 | */ 24 | public function testCanTrackFunctionName(): void 25 | { 26 | $scanner = new FunctionBodyScanner(); 27 | $tokenizer = new TokenStreamParser(); 28 | $tokenStream = $tokenizer->scan('iterate($scanner); 30 | static::assertSame('bar', $scanner->getName()); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function testCanTrackEndOfScopedFunctionBody(): void 37 | { 38 | $scanner = new FunctionBodyScanner(); 39 | $tokenizer = new TokenStreamParser(); 40 | $tokenStream = $tokenizer->scan('iterate($scanner); 42 | static::assertFalse($scanner->isActive()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/PHPWeaver/FunctionParametersScannerTest.php: -------------------------------------------------------------------------------- 1 | scan('iterate($scanner); 18 | static::assertSame(['$x' => '???'], $scanner->getCurrentSignatureAsTypeMap()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/PHPWeaver/MockPassthruBufferEditor.php: -------------------------------------------------------------------------------- 1 | buffer = clone $buffer; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/PHPWeaver/ModifiersScannerTest.php: -------------------------------------------------------------------------------- 1 | scan('isActive()); 18 | $tokenStream->iterate($scanner); 19 | static::assertFalse($scanner->isActive()); 20 | } 21 | 22 | /** 23 | * @return void 24 | */ 25 | public function testEndsOnFunction(): void 26 | { 27 | $scanner = new ModifiersScanner(); 28 | $tokenizer = new TokenStreamParser(); 29 | $tokenStream = $tokenizer->scan('iterate($scanner); 31 | static::assertFalse($scanner->isActive()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/PHPWeaver/TokenizerTest.php: -------------------------------------------------------------------------------- 1 | resources .. 12 | * use static typehints for parameter types 13 | * use docblock comments for parameter types 14 | * merge with existing docblock comments. 15 | */ 16 | class TokenizerTest extends TestCase 17 | { 18 | /** 19 | * @return void 20 | */ 21 | public function testTokenizePhpWithoutErrors(): void 22 | { 23 | $tokenizer = new TokenStreamParser(); 24 | $tokenStream = $tokenizer->scan(''); 25 | $tokenStream->iterate(new class implements ScannerInterface { 26 | /** @var int */ 27 | private $count = 0; 28 | public function __destruct () { 29 | TokenizerTest::assertSame(12, $this->count); 30 | } 31 | public function accept(Token $token): void 32 | { 33 | $this->count++; 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/PHPWeaver/TracerTest.php: -------------------------------------------------------------------------------- 1 | curdir = getcwd() ?: ''; 40 | $dirSandbox = $this->sandbox(); 41 | mkdir($dirSandbox); 42 | $sourceInclude = 'val = $val;' . "\n" . 50 | ' }' . "\n" . 51 | '}' . "\n" . 52 | 'class Bar {' . "\n" . 53 | '}'; 54 | file_put_contents($dirSandbox . '/include.php', $sourceInclude); 55 | $sourceMain = 'curdir); 67 | $dirSandbox = $this->sandbox(); 68 | unlink($dirSandbox . '/include.php'); 69 | unlink($dirSandbox . '/main.php'); 70 | if (is_file($dirSandbox . '/dumpfile.xt')) { 71 | unlink($dirSandbox . '/dumpfile.xt'); 72 | } 73 | rmdir($dirSandbox); 74 | } 75 | 76 | /** 77 | * @return void 78 | */ 79 | public function testCanExecuteSandboxCode(): void 80 | { 81 | chdir($this->sandbox()); 82 | exec('php ' . escapeshellarg($this->sandbox() . '/main.php'), $output, $returnVar); 83 | static::assertEmpty($output); 84 | static::assertSame(0, $returnVar); 85 | } 86 | 87 | /** 88 | * @return void 89 | */ 90 | public function testCanExecuteSandboxCodeWithInstrumentation(): void 91 | { 92 | chdir($this->sandbox()); 93 | $commandTester = new CommandTester(new TraceCommand()); 94 | $commandTester->execute(['phpscript' => $this->sandbox() . '/main.php']); 95 | $output = $commandTester->getDisplay(); 96 | static::assertRegExp('~TRACE COMPLETE\n$~', $output); 97 | } 98 | 99 | /** 100 | * @return void 101 | */ 102 | public function testInstrumentationCreatesTracefile(): void 103 | { 104 | chdir($this->sandbox()); 105 | $commandTester = new CommandTester(new TraceCommand()); 106 | $commandTester->execute(['phpscript' => $this->sandbox() . '/main.php']); 107 | static::assertTrue(is_file($this->sandbox() . '/dumpfile.xt')); 108 | } 109 | 110 | /** 111 | * @return void 112 | */ 113 | public function testCanParseTracefile(): void 114 | { 115 | chdir($this->sandbox()); 116 | $commandTester = new CommandTester(new TraceCommand()); 117 | $commandTester->execute(['phpscript' => $this->sandbox() . '/main.php']); 118 | $sigs = new Signatures(); 119 | static::assertFalse($sigs->has('callit')); 120 | $collector = new TraceSignatureLogger($sigs); 121 | $trace = new TraceReader(new FunctionTracer($collector)); 122 | foreach (new SplFileObject($this->sandbox() . '/dumpfile.xt') as $line) { 123 | static::assertIsString($line); 124 | $trace->processLine($line); 125 | } 126 | static::assertTrue($sigs->has('callit')); 127 | } 128 | 129 | /** 130 | * @return void 131 | */ 132 | public function testCanParseClassArg(): void 133 | { 134 | chdir($this->sandbox()); 135 | $commandTester = new CommandTester(new TraceCommand()); 136 | $commandTester->execute(['phpscript' => $this->sandbox() . '/main.php']); 137 | $sigs = new Signatures(); 138 | $collector = new TraceSignatureLogger($sigs); 139 | $trace = new TraceReader(new FunctionTracer($collector)); 140 | foreach (new SplFileObject($this->sandbox() . '/dumpfile.xt') as $line) { 141 | static::assertIsString($line); 142 | $trace->processLine($line); 143 | } 144 | static::assertSame('Foo', $sigs->get('callit')->getArgumentById(0)->getType()); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | error_reporting(E_ALL); 14 | 15 | if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { 16 | date_default_timezone_set(@date_default_timezone_get()); 17 | } 18 | 19 | require __DIR__ . '/../src/bootstrap.php'; 20 | --------------------------------------------------------------------------------