├── .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 | [](https://travis-ci.org/AJenbo/php-tracer-weaver)
4 | [](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 | [](https://codeclimate.com/github/AJenbo/php-tracer-weaver/maintainability)
6 | [](https://bettercodehub.com/)
7 | [](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 |
--------------------------------------------------------------------------------