├── ChangeLog.md
├── LICENSE
├── README.md
├── SECURITY.md
├── composer.json
├── composer.lock
├── example
├── optimized.svg
└── unoptimized.svg
├── foal
└── src
├── Analyser.php
├── Renderer.php
├── TextRenderer.php
├── bytecode
├── Disassembler.php
├── VldDisassembler.php
└── VldParser.php
├── cli
├── Application.php
├── Configuration.php
├── ConfigurationBuilder.php
├── Factory.php
└── Version.php
├── exception
├── ConfigurationBuilderException.php
├── Exception.php
├── OpcacheNotLoadedException.php
├── PathsNotConfiguredException.php
├── ProcessException.php
└── VldNotLoadedException.php
└── file
├── File.php
├── FileCollection.php
└── FileCollectionIterator.php
/ChangeLog.md:
--------------------------------------------------------------------------------
1 | # ChangeLog
2 |
3 | All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
4 |
5 | ## [0.4.0] - 2024-MM-DD
6 |
7 | ### Added
8 |
9 | * `--paths` option to export execution paths before/after bytecode optimization in DOT format
10 |
11 | ### Changed
12 |
13 | * The PHAR-specific CLI options `--manifest`, `--sbom`, and `--composer-lock` are now included in the help output
14 |
15 | ### Removed
16 |
17 | * `--diff` option to display optimized-away lines as diff
18 |
19 | ## [0.3.0] - 2024-03-24
20 |
21 | ### Added
22 |
23 | * Support for multiple arguments (directories and/or files)
24 | * `--diff` option to display optimized-away lines as diff
25 |
26 | ### Changed
27 |
28 | * [#3](https://github.com/sebastianbergmann/foal/issues/3): Refactor `Analyser` to operate on list of files
29 |
30 | ## [0.2.1] - 2024-03-24
31 |
32 | * No functional changes
33 |
34 | ## [0.2.0] - 2024-03-24
35 |
36 | ### Removed
37 |
38 | * This tool now requires PHP 8.3
39 |
40 | ## [0.1.0] - 2018-12-24
41 |
42 | * Initial release
43 |
44 | [0.4.0]: https://github.com/sebastianbergmann/foal/compare/0.3.0...main
45 | [0.3.0]: https://github.com/sebastianbergmann/foal/compare/0.2.1...0.3.0
46 | [0.2.1]: https://github.com/sebastianbergmann/foal/compare/0.2.0...0.2.1
47 | [0.2.0]: https://github.com/sebastianbergmann/foal/compare/0.1.0...0.2.0
48 | [0.1.0]: https://github.com/sebastianbergmann/foal/compare/820e0c5e988a5f8bf09f38211174bd481d8e5dd9...0.1.0
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2018-2024, Sebastian Bergmann
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://unmaintained.tech/)
2 | [](https://github.com/sebastianbergmann/foal/actions)
3 | [](https://codecov.io/gh/sebastianbergmann/foal)
4 |
5 | # Find Optimized-Away Lines (FOAL)
6 |
7 | `foal` finds lines of code that are eliminated by OpCache's bytecode optimizer.
8 |
9 | ## Installation
10 |
11 | The recommended way to use this tool is a [PHP Archive (PHAR)](https://php.net/phar):
12 |
13 | ```bash
14 | $ wget https://phar.phpunit.de/foal.phar
15 |
16 | $ php foal.phar --version
17 | ```
18 |
19 | Furthermore, it is recommended to use [Phive](https://phar.io/) for installing and updating the tool dependencies of your project.
20 |
21 | ## Usage
22 |
23 | **`example.php`**
24 | ```php
25 | =8.2"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^11.0"
28 | },
29 | "type": "library",
30 | "extra": {
31 | "branch-alias": {
32 | "dev-main": "5.0-dev"
33 | }
34 | },
35 | "autoload": {
36 | "classmap": [
37 | "src/"
38 | ]
39 | },
40 | "notification-url": "https://packagist.org/downloads/",
41 | "license": [
42 | "BSD-3-Clause"
43 | ],
44 | "authors": [
45 | {
46 | "name": "Sebastian Bergmann",
47 | "email": "sebastian@phpunit.de",
48 | "role": "lead"
49 | }
50 | ],
51 | "description": "FilterIterator implementation that filters files based on a list of suffixes.",
52 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
53 | "keywords": [
54 | "filesystem",
55 | "iterator"
56 | ],
57 | "support": {
58 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
59 | "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
60 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.0"
61 | },
62 | "funding": [
63 | {
64 | "url": "https://github.com/sebastianbergmann",
65 | "type": "github"
66 | }
67 | ],
68 | "time": "2024-02-02T06:05:04+00:00"
69 | },
70 | {
71 | "name": "sebastian/cli-parser",
72 | "version": "3.0.1",
73 | "source": {
74 | "type": "git",
75 | "url": "https://github.com/sebastianbergmann/cli-parser.git",
76 | "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a"
77 | },
78 | "dist": {
79 | "type": "zip",
80 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/00a74d5568694711f0222e54fb281e1d15fdf04a",
81 | "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a",
82 | "shasum": ""
83 | },
84 | "require": {
85 | "php": ">=8.2"
86 | },
87 | "require-dev": {
88 | "phpunit/phpunit": "^11.0"
89 | },
90 | "type": "library",
91 | "extra": {
92 | "branch-alias": {
93 | "dev-main": "3.0-dev"
94 | }
95 | },
96 | "autoload": {
97 | "classmap": [
98 | "src/"
99 | ]
100 | },
101 | "notification-url": "https://packagist.org/downloads/",
102 | "license": [
103 | "BSD-3-Clause"
104 | ],
105 | "authors": [
106 | {
107 | "name": "Sebastian Bergmann",
108 | "email": "sebastian@phpunit.de",
109 | "role": "lead"
110 | }
111 | ],
112 | "description": "Library for parsing CLI options",
113 | "homepage": "https://github.com/sebastianbergmann/cli-parser",
114 | "support": {
115 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
116 | "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
117 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.1"
118 | },
119 | "funding": [
120 | {
121 | "url": "https://github.com/sebastianbergmann",
122 | "type": "github"
123 | }
124 | ],
125 | "time": "2024-03-02T07:26:58+00:00"
126 | },
127 | {
128 | "name": "sebastian/version",
129 | "version": "5.0.0",
130 | "source": {
131 | "type": "git",
132 | "url": "https://github.com/sebastianbergmann/version.git",
133 | "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001"
134 | },
135 | "dist": {
136 | "type": "zip",
137 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/13999475d2cb1ab33cb73403ba356a814fdbb001",
138 | "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001",
139 | "shasum": ""
140 | },
141 | "require": {
142 | "php": ">=8.2"
143 | },
144 | "type": "library",
145 | "extra": {
146 | "branch-alias": {
147 | "dev-main": "5.0-dev"
148 | }
149 | },
150 | "autoload": {
151 | "classmap": [
152 | "src/"
153 | ]
154 | },
155 | "notification-url": "https://packagist.org/downloads/",
156 | "license": [
157 | "BSD-3-Clause"
158 | ],
159 | "authors": [
160 | {
161 | "name": "Sebastian Bergmann",
162 | "email": "sebastian@phpunit.de",
163 | "role": "lead"
164 | }
165 | ],
166 | "description": "Library that helps with managing the version number of Git-hosted PHP projects",
167 | "homepage": "https://github.com/sebastianbergmann/version",
168 | "support": {
169 | "issues": "https://github.com/sebastianbergmann/version/issues",
170 | "security": "https://github.com/sebastianbergmann/version/security/policy",
171 | "source": "https://github.com/sebastianbergmann/version/tree/5.0.0"
172 | },
173 | "funding": [
174 | {
175 | "url": "https://github.com/sebastianbergmann",
176 | "type": "github"
177 | }
178 | ],
179 | "time": "2024-02-02T06:10:47+00:00"
180 | }
181 | ],
182 | "packages-dev": [],
183 | "aliases": [],
184 | "minimum-stability": "stable",
185 | "stability-flags": [],
186 | "prefer-stable": true,
187 | "prefer-lowest": false,
188 | "platform": {
189 | "php": "^8.3",
190 | "ext-zend-opcache": "*",
191 | "ext-vld": "*"
192 | },
193 | "platform-dev": [],
194 | "platform-overrides": {
195 | "php": "8.3.0"
196 | },
197 | "plugin-api-version": "2.6.0"
198 | }
199 |
--------------------------------------------------------------------------------
/example/optimized.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
92 |
--------------------------------------------------------------------------------
/example/unoptimized.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
80 |
--------------------------------------------------------------------------------
/foal:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | ')) {
21 | fwrite(
22 | STDERR,
23 | sprintf(
24 | 'foal X.Y.Z by Sebastian Bergmann.' . PHP_EOL . PHP_EOL .
25 | 'This version of FOAL requires PHP >= 8.3.' . PHP_EOL .
26 | 'You are using PHP %s (%s).' . PHP_EOL,
27 | PHP_VERSION,
28 | PHP_BINARY
29 | )
30 | );
31 |
32 | die(1);
33 | }
34 |
35 | if (isset($GLOBALS['_composer_autoload_path'])) {
36 | define('FOAL_COMPOSER_INSTALL', $GLOBALS['_composer_autoload_path']);
37 |
38 | unset($GLOBALS['_composer_autoload_path']);
39 | } else {
40 | foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) {
41 | if (file_exists($file)) {
42 | define('FOAL_COMPOSER_INSTALL', $file);
43 |
44 | break;
45 | }
46 | }
47 |
48 | unset($file);
49 | }
50 |
51 | if (!defined('FOAL_COMPOSER_INSTALL')) {
52 | fwrite(
53 | STDERR,
54 | 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
55 | ' composer install' . PHP_EOL . PHP_EOL .
56 | 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
57 | );
58 |
59 | die(1);
60 | }
61 |
62 | require FOAL_COMPOSER_INSTALL;
63 |
64 | exit((new Factory)->createApplication()->run($_SERVER['argv']));
65 |
--------------------------------------------------------------------------------
/src/Analyser.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use function array_diff;
13 | use function array_values;
14 | use function assert;
15 | use function file;
16 |
17 | /**
18 | * @internal This class is not covered by the backward compatibility promise for FOAL
19 | */
20 | final readonly class Analyser
21 | {
22 | private Disassembler $disassembler;
23 |
24 | public function __construct(Disassembler $disassembler)
25 | {
26 | $this->disassembler = $disassembler;
27 | }
28 |
29 | /**
30 | * @param non-empty-list $files
31 | */
32 | public function analyse(array $files): FileCollection
33 | {
34 | $result = [];
35 |
36 | foreach ($files as $file) {
37 | $sourceLines = file($file);
38 |
39 | assert($sourceLines !== false);
40 |
41 | $result[] = new File(
42 | $file,
43 | $sourceLines,
44 | $this->linesEliminatedByOptimizer($file),
45 | );
46 | }
47 |
48 | return FileCollection::from(...$result);
49 | }
50 |
51 | /**
52 | * @param non-empty-string $file
53 | *
54 | * @return list
55 | */
56 | private function linesEliminatedByOptimizer(string $file): array
57 | {
58 | return array_values(
59 | array_diff(
60 | $this->disassembler->linesWithOpcodesBeforeOptimization($file),
61 | $this->disassembler->linesWithOpcodesAfterOptimization($file),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Renderer.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | /**
13 | * @internal This interface is not covered by the backward compatibility promise for FOAL
14 | */
15 | interface Renderer
16 | {
17 | public function render(File $file): string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/TextRenderer.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use function array_flip;
13 | use function array_key_exists;
14 | use function rtrim;
15 | use function sprintf;
16 |
17 | /**
18 | * @internal This class is not covered by the backward compatibility promise for FOAL
19 | */
20 | final readonly class TextRenderer implements Renderer
21 | {
22 | public function render(File $file): string
23 | {
24 | $buffer = $file->path() . PHP_EOL;
25 | $sourceLines = $file->sourceLines();
26 | $eliminatedLines = array_flip($file->linesEliminatedByOptimizer());
27 | $line = 0;
28 |
29 | foreach ($sourceLines as $sourceLine) {
30 | $line++;
31 |
32 | $buffer .= sprintf(
33 | '%s %-6d %s' . PHP_EOL,
34 | array_key_exists($line, $eliminatedLines) ? '-' : ' ',
35 | $line,
36 | rtrim($sourceLine),
37 | );
38 | }
39 |
40 | return $buffer;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/bytecode/Disassembler.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | /**
13 | * @internal This interface is not covered by the backward compatibility promise for FOAL
14 | */
15 | interface Disassembler
16 | {
17 | /**
18 | * @param non-empty-string $file
19 | *
20 | * @return list
21 | */
22 | public function linesWithOpcodesBeforeOptimization(string $file): array;
23 |
24 | /**
25 | * @param non-empty-string $file
26 | *
27 | * @return list
28 | */
29 | public function linesWithOpcodesAfterOptimization(string $file): array;
30 |
31 | /**
32 | * @param non-empty-string $file
33 | *
34 | * @return non-empty-string
35 | */
36 | public function pathsBeforeOptimization(string $file): string;
37 |
38 | /**
39 | * @param non-empty-string $file
40 | *
41 | * @return non-empty-string
42 | */
43 | public function pathsAfterOptimization(string $file): string;
44 | }
45 |
--------------------------------------------------------------------------------
/src/bytecode/VldDisassembler.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use function assert;
13 | use function exec;
14 | use function extension_loaded;
15 | use function file_get_contents;
16 | use function implode;
17 | use function is_string;
18 |
19 | /**
20 | * @internal This class is not covered by the backward compatibility promise for FOAL
21 | */
22 | final readonly class VldDisassembler implements Disassembler
23 | {
24 | private const string VLD_OPTIONS_COMMON = '-d vld.active=1 -d vld.execute=0 -d vld.verbosity=0';
25 | private const string VLD_OPTIONS_BYTECODE = self::VLD_OPTIONS_COMMON . ' -d vld.format=1 -d vld.col_sep=\';\'';
26 | private const string VLD_OPTIONS_PATHS = self::VLD_OPTIONS_COMMON . ' -d vld.save_paths=1 -d vld.save_dir=/tmp';
27 | private const string OPCACHE_OPTIONS_ENABLE = '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.optimization_level=-1';
28 | private const string OPCACHE_OPTIONS_DISABLE = '-d opcache.enable=0 -d opcache.enable_cli=0';
29 | private VldParser $parser;
30 |
31 | /**
32 | * @throws OpcacheNotLoadedException
33 | * @throws VldNotLoadedException
34 | */
35 | public function __construct(VldParser $parser)
36 | {
37 | $this->ensureOpCacheIsAvailable();
38 | $this->ensureVldIsAvailable();
39 |
40 | $this->parser = $parser;
41 | }
42 |
43 | /**
44 | * @param non-empty-string $file
45 | *
46 | * @return list
47 | */
48 | public function linesWithOpcodesBeforeOptimization(string $file): array
49 | {
50 | return $this->parser->linesWithOpcodes(
51 | $this->execute(
52 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_DISABLE . ' ' . self::VLD_OPTIONS_BYTECODE . ' ' . $file,
53 | ),
54 | );
55 | }
56 |
57 | /**
58 | * @param non-empty-string $file
59 | *
60 | * @return list
61 | */
62 | public function linesWithOpcodesAfterOptimization(string $file): array
63 | {
64 | return $this->parser->linesWithOpcodes(
65 | $this->execute(
66 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_ENABLE . ' ' . self::VLD_OPTIONS_BYTECODE . ' ' . $file,
67 | ),
68 | );
69 | }
70 |
71 | /**
72 | * @param non-empty-string $file
73 | *
74 | * @return non-empty-string
75 | */
76 | public function pathsBeforeOptimization(string $file): string
77 | {
78 | $this->execute(
79 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_DISABLE . ' ' . self::VLD_OPTIONS_PATHS . ' ' . $file,
80 | );
81 |
82 | $buffer = file_get_contents('/tmp/paths.dot');
83 |
84 | assert(is_string($buffer) && $buffer !== '');
85 |
86 | return $buffer;
87 | }
88 |
89 | /**
90 | * @param non-empty-string $file
91 | *
92 | * @return non-empty-string
93 | */
94 | public function pathsAfterOptimization(string $file): string
95 | {
96 | $this->execute(
97 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_ENABLE . ' ' . self::VLD_OPTIONS_PATHS . ' ' . $file,
98 | );
99 |
100 | $buffer = file_get_contents('/tmp/paths.dot');
101 |
102 | assert(is_string($buffer) && $buffer !== '');
103 |
104 | return $buffer;
105 | }
106 |
107 | /**
108 | * @throws OpcacheNotLoadedException
109 | */
110 | private function ensureOpCacheIsAvailable(): void
111 | {
112 | if (!extension_loaded('Zend OPcache')) {
113 | // @codeCoverageIgnoreStart
114 | throw new OpcacheNotLoadedException;
115 | // @codeCoverageIgnoreEnd
116 | }
117 | }
118 |
119 | /**
120 | * @throws VldNotLoadedException
121 | */
122 | private function ensureVldIsAvailable(): void
123 | {
124 | if (!extension_loaded('vld')) {
125 | // @codeCoverageIgnoreStart
126 | throw new VldNotLoadedException;
127 | // @codeCoverageIgnoreEnd
128 | }
129 | }
130 |
131 | /**
132 | * @return list
133 | */
134 | private function execute(string $command): array
135 | {
136 | exec($command . ' 2>&1', $output, $returnValue);
137 |
138 | if ($returnValue !== 0) {
139 | // @codeCoverageIgnoreStart
140 | throw new ProcessException(implode("\r\n", $output));
141 | // @codeCoverageIgnoreEnd
142 | }
143 |
144 | return $output;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/bytecode/VldParser.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use function array_unique;
13 | use function explode;
14 | use function sort;
15 | use function str_starts_with;
16 | use function trim;
17 |
18 | /**
19 | * @internal This class is not covered by the backward compatibility promise for FOAL
20 | */
21 | final readonly class VldParser
22 | {
23 | /**
24 | * @param list $lines
25 | *
26 | * @return list
27 | */
28 | public function linesWithOpcodes(array $lines): array
29 | {
30 | $linesWithOpcodes = [];
31 | $opArray = false;
32 |
33 | foreach ($lines as $line) {
34 | if (str_starts_with($line, ';line')) {
35 | $opArray = true;
36 |
37 | continue;
38 | }
39 |
40 | if (trim($line) === ';') {
41 | $opArray = false;
42 | }
43 |
44 | if (!$opArray) {
45 | continue;
46 | }
47 |
48 | $linesWithOpcodes[] = (int) explode(';', $line)[1];
49 | }
50 |
51 | $linesWithOpcodes = array_unique($linesWithOpcodes);
52 |
53 | sort($linesWithOpcodes);
54 |
55 | return $linesWithOpcodes;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/cli/Application.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use const PHP_EOL;
13 | use function array_merge;
14 | use function array_unique;
15 | use function array_values;
16 | use function assert;
17 | use function count;
18 | use function defined;
19 | use function file_put_contents;
20 | use function is_dir;
21 | use function is_file;
22 | use function printf;
23 | use function realpath;
24 | use SebastianBergmann\FileIterator\Facade;
25 | use SebastianBergmann\FOAL\Analyser;
26 | use SebastianBergmann\FOAL\Disassembler;
27 | use SebastianBergmann\FOAL\TextRenderer;
28 |
29 | /**
30 | * @internal This class is not covered by the backward compatibility promise for FOAL
31 | */
32 | final readonly class Application
33 | {
34 | private Analyser $analyser;
35 | private Disassembler $disassembler;
36 |
37 | public function __construct(Analyser $analyser, Disassembler $disassembler)
38 | {
39 | $this->analyser = $analyser;
40 | $this->disassembler = $disassembler;
41 | }
42 |
43 | /**
44 | * @param list $argv
45 | */
46 | public function run(array $argv): int
47 | {
48 | $this->printVersion();
49 |
50 | try {
51 | $configuration = (new ConfigurationBuilder)->build($argv);
52 | // @codeCoverageIgnoreStart
53 | } catch (ConfigurationBuilderException $e) {
54 | print PHP_EOL . $e->getMessage() . PHP_EOL;
55 |
56 | return 1;
57 | // @codeCoverageIgnoreEnd
58 | }
59 |
60 | if ($configuration->version()) {
61 | return 0;
62 | }
63 |
64 | print PHP_EOL;
65 |
66 | if ($configuration->help()) {
67 | $this->help();
68 |
69 | return 0;
70 | }
71 |
72 | if ($configuration->arguments() === []) {
73 | $this->help();
74 |
75 | return 1;
76 | }
77 |
78 | $files = [];
79 |
80 | foreach ($configuration->arguments() as $argument) {
81 | $candidate = realpath($argument);
82 |
83 | if ($candidate === false) {
84 | continue;
85 | }
86 |
87 | assert($candidate !== '');
88 |
89 | if (is_file($candidate)) {
90 | $files[] = $candidate;
91 |
92 | continue;
93 | }
94 |
95 | if (is_dir($candidate)) {
96 | $files = array_merge($files, (new Facade)->getFilesAsArray($candidate, '.php'));
97 | }
98 | }
99 |
100 | if (empty($files)) {
101 | print 'No files found to analyse' . PHP_EOL;
102 |
103 | return 1;
104 | }
105 |
106 | $files = array_values(array_unique($files));
107 |
108 | if ($configuration->hasPaths()) {
109 | return $this->handlePaths($files, $configuration->paths());
110 | }
111 |
112 | return $this->handleAnalysis($files);
113 | }
114 |
115 | private function printVersion(): void
116 | {
117 | printf(
118 | 'foal %s by Sebastian Bergmann.' . PHP_EOL,
119 | Version::id(),
120 | );
121 | }
122 |
123 | private function help(): void
124 | {
125 | print <<<'EOT'
126 | Usage:
127 | foal [options] ...
128 |
129 | --paths Write execution paths before/after bytecode optimization to files in DOT format
130 |
131 | -h|--help Prints this usage information and exits
132 | --version Prints the version and exits
133 |
134 | EOT;
135 |
136 | if (defined('__FOAL_PHAR__')) {
137 | print <<<'EOT'
138 |
139 | --manifest Prints Software Bill of Materials (SBOM) in plain-text format and exits
140 | --sbom Prints Software Bill of Materials (SBOM) in CycloneDX XML format and exits
141 | --composer-lock Prints the composer.lock file used to build the PHAR and exits
142 |
143 | EOT;
144 | }
145 | }
146 |
147 | /**
148 | * @param non-empty-list $files
149 | * @param non-empty-string $target
150 | */
151 | private function handlePaths(array $files, string $target): int
152 | {
153 | if (count($files) !== 1) {
154 | print 'The --paths option only operates on a source single file' . PHP_EOL;
155 |
156 | return 1;
157 | }
158 |
159 | $unoptimizedFile = $target . '/unoptimized.dot';
160 |
161 | file_put_contents($unoptimizedFile, $this->disassembler->pathsBeforeOptimization($files[0]));
162 |
163 | printf(
164 | 'Wrote execution paths for %s to %s' . PHP_EOL,
165 | $files[0],
166 | $unoptimizedFile,
167 | );
168 |
169 | $optimizedFile = $target . '/optimized.dot';
170 |
171 | file_put_contents($optimizedFile, $this->disassembler->pathsAfterOptimization($files[0]));
172 |
173 | printf(
174 | 'Wrote optimized execution paths for %s to %s' . PHP_EOL,
175 | $files[0],
176 | $optimizedFile,
177 | );
178 |
179 | return 0;
180 | }
181 |
182 | /**
183 | * @param non-empty-list $files
184 | */
185 | private function handleAnalysis(array $files): int
186 | {
187 | $files = $this->analyser->analyse($files);
188 |
189 | $renderer = new TextRenderer;
190 | $first = true;
191 |
192 | foreach ($files as $file) {
193 | if (!$first) {
194 | print PHP_EOL;
195 | }
196 |
197 | print $renderer->render($file);
198 |
199 | $first = false;
200 | }
201 |
202 | return 0;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/src/cli/Configuration.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | /**
13 | * @internal This class is not covered by the backward compatibility promise for FOAL
14 | */
15 | final readonly class Configuration
16 | {
17 | /**
18 | * @var list
19 | */
20 | private array $arguments;
21 |
22 | /**
23 | * @var ?non-empty-string
24 | */
25 | private ?string $paths;
26 | private bool $help;
27 | private bool $version;
28 |
29 | /**
30 | * @param list $arguments
31 | * @param ?non-empty-string $paths
32 | */
33 | public function __construct(array $arguments, ?string $paths, bool $help, bool $version)
34 | {
35 | $this->arguments = $arguments;
36 | $this->help = $help;
37 | $this->paths = $paths;
38 | $this->version = $version;
39 | }
40 |
41 | /**
42 | * @return list
43 | */
44 | public function arguments(): array
45 | {
46 | return $this->arguments;
47 | }
48 |
49 | /**
50 | * @phpstan-assert-if-true !null $this->paths
51 | */
52 | public function hasPaths(): bool
53 | {
54 | return $this->paths !== null;
55 | }
56 |
57 | /**
58 | * @throws PathsNotConfiguredException
59 | *
60 | * @return non-empty-string
61 | */
62 | public function paths(): string
63 | {
64 | if ($this->paths === null) {
65 | // @codeCoverageIgnoreStart
66 | throw new PathsNotConfiguredException;
67 | // @codeCoverageIgnoreEnd
68 | }
69 |
70 | return $this->paths;
71 | }
72 |
73 | public function help(): bool
74 | {
75 | return $this->help;
76 | }
77 |
78 | public function version(): bool
79 | {
80 | return $this->version;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/cli/ConfigurationBuilder.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use function assert;
13 | use function is_array;
14 | use SebastianBergmann\CliParser\Exception as CliParserException;
15 | use SebastianBergmann\CliParser\Parser as CliParser;
16 |
17 | /**
18 | * @internal This class is not covered by the backward compatibility promise for FOAL
19 | */
20 | final readonly class ConfigurationBuilder
21 | {
22 | /**
23 | * @param list $argv
24 | *
25 | * @throws ConfigurationBuilderException
26 | */
27 | public function build(array $argv): Configuration
28 | {
29 | try {
30 | $options = (new CliParser)->parse(
31 | $argv,
32 | 'hv',
33 | [
34 | 'paths=',
35 | 'help',
36 | 'version',
37 | ],
38 | );
39 | // @codeCoverageIgnoreStart
40 | } catch (CliParserException $e) {
41 | throw new ConfigurationBuilderException(
42 | $e->getMessage(),
43 | $e->getCode(),
44 | $e,
45 | );
46 | // @codeCoverageIgnoreEnd
47 | }
48 |
49 | $paths = null;
50 | $help = false;
51 | $version = false;
52 |
53 | foreach ($options[0] as $option) {
54 | assert(is_array($option));
55 |
56 | switch ($option[0]) {
57 | case '--paths':
58 | $paths = (string) $option[1];
59 |
60 | assert($paths !== '');
61 |
62 | break;
63 |
64 | case 'h':
65 | case '--help':
66 | $help = true;
67 |
68 | break;
69 |
70 | case 'v':
71 | case '--version':
72 | $version = true;
73 |
74 | break;
75 | }
76 | }
77 |
78 | return new Configuration(
79 | $options[1],
80 | $paths,
81 | $help,
82 | $version,
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/cli/Factory.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use SebastianBergmann\FOAL\Analyser;
13 | use SebastianBergmann\FOAL\VldDisassembler;
14 | use SebastianBergmann\FOAL\VldParser;
15 |
16 | /**
17 | * @internal This class is not covered by the backward compatibility promise for FOAL
18 | */
19 | final readonly class Factory
20 | {
21 | public function createApplication(): Application
22 | {
23 | return new Application(
24 | new Analyser($this->disassembler()),
25 | $this->disassembler(),
26 | );
27 | }
28 |
29 | private function disassembler(): VldDisassembler
30 | {
31 | return new VldDisassembler(
32 | new VldParser,
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/cli/Version.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use function assert;
13 | use function dirname;
14 | use SebastianBergmann\Version as VersionId;
15 |
16 | /**
17 | * @internal This class is not covered by the backward compatibility promise for FOAL
18 | */
19 | final class Version
20 | {
21 | private static string $pharVersion = '';
22 | private static string $version = '';
23 |
24 | public static function id(): string
25 | {
26 | if (self::$pharVersion !== '') {
27 | // @codeCoverageIgnoreStart
28 | return self::$pharVersion;
29 | // @codeCoverageIgnoreEnd
30 | }
31 |
32 | if (self::$version === '') {
33 | $path = dirname(__DIR__, 2);
34 |
35 | assert($path !== '');
36 |
37 | self::$version = (new VersionId('0.4', $path))->asString();
38 | }
39 |
40 | return self::$version;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/exception/ConfigurationBuilderException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use RuntimeException;
13 | use SebastianBergmann\FOAL\Exception;
14 |
15 | /**
16 | * @internal This class is not covered by the backward compatibility promise for FOAL
17 | */
18 | final class ConfigurationBuilderException extends RuntimeException implements Exception
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/exception/Exception.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | /**
13 | * @internal This interface is not covered by the backward compatibility promise for FOAL
14 | */
15 | interface Exception
16 | {
17 | }
18 |
--------------------------------------------------------------------------------
/src/exception/OpcacheNotLoadedException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use RuntimeException;
13 |
14 | /**
15 | * @internal This class is not covered by the backward compatibility promise for FOAL
16 | */
17 | final class OpcacheNotLoadedException extends RuntimeException implements Exception
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/exception/PathsNotConfiguredException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL\CLI;
11 |
12 | use RuntimeException;
13 | use SebastianBergmann\FOAL\Exception;
14 |
15 | /**
16 | * @internal This class is not covered by the backward compatibility promise for FOAL
17 | */
18 | final class PathsNotConfiguredException extends RuntimeException implements Exception
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/exception/ProcessException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use RuntimeException;
13 |
14 | /**
15 | * @internal This class is not covered by the backward compatibility promise for FOAL
16 | */
17 | final class ProcessException extends RuntimeException implements Exception
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/exception/VldNotLoadedException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use RuntimeException;
13 |
14 | /**
15 | * @internal This class is not covered by the backward compatibility promise for FOAL
16 | */
17 | final class VldNotLoadedException extends RuntimeException implements Exception
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/file/File.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | /**
13 | * @internal This class is not covered by the backward compatibility promise for FOAL
14 | */
15 | final readonly class File
16 | {
17 | /**
18 | * @var non-empty-string
19 | */
20 | private string $path;
21 |
22 | /**
23 | * @var list
24 | */
25 | private array $sourceLines;
26 |
27 | /**
28 | * @var list
29 | */
30 | private array $linesEliminatedByOptimizer;
31 |
32 | /**
33 | * @param non-empty-string $path
34 | * @param list $sourceLines
35 | * @param list $linesEliminatedByOptimizer
36 | */
37 | public function __construct(string $path, array $sourceLines, array $linesEliminatedByOptimizer)
38 | {
39 | $this->path = $path;
40 | $this->sourceLines = $sourceLines;
41 | $this->linesEliminatedByOptimizer = $linesEliminatedByOptimizer;
42 | }
43 |
44 | /**
45 | * @return non-empty-string
46 | */
47 | public function path(): string
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return list
54 | */
55 | public function sourceLines(): array
56 | {
57 | return $this->sourceLines;
58 | }
59 |
60 | /**
61 | * @return list
62 | */
63 | public function linesEliminatedByOptimizer(): array
64 | {
65 | return $this->linesEliminatedByOptimizer;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/file/FileCollection.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use function array_values;
13 | use function count;
14 | use Countable;
15 | use IteratorAggregate;
16 |
17 | /**
18 | * @template-implements IteratorAggregate
19 | *
20 | * @immutable
21 | *
22 | * @internal This class is not covered by the backward compatibility promise for FOAL
23 | */
24 | final readonly class FileCollection implements Countable, IteratorAggregate
25 | {
26 | /**
27 | * @var list
28 | */
29 | private array $files;
30 |
31 | public static function from(File ...$files): self
32 | {
33 | return new self(array_values($files));
34 | }
35 |
36 | /**
37 | * @param list $files
38 | */
39 | private function __construct(array $files)
40 | {
41 | $this->files = $files;
42 | }
43 |
44 | /**
45 | * @return list
46 | */
47 | public function asArray(): array
48 | {
49 | return $this->files;
50 | }
51 |
52 | public function getIterator(): FileCollectionIterator
53 | {
54 | return new FileCollectionIterator($this);
55 | }
56 |
57 | public function count(): int
58 | {
59 | return count($this->files);
60 | }
61 |
62 | public function isEmpty(): bool
63 | {
64 | return empty($this->files);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/file/FileCollectionIterator.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace SebastianBergmann\FOAL;
11 |
12 | use Iterator;
13 |
14 | /**
15 | * @template-implements Iterator
16 | *
17 | * @internal This class is not covered by the backward compatibility promise for FOAL
18 | */
19 | final class FileCollectionIterator implements Iterator
20 | {
21 | /**
22 | * @var list
23 | */
24 | private readonly array $files;
25 | private int $position = 0;
26 |
27 | public function __construct(FileCollection $collection)
28 | {
29 | $this->files = $collection->asArray();
30 | }
31 |
32 | public function rewind(): void
33 | {
34 | $this->position = 0;
35 | }
36 |
37 | public function valid(): bool
38 | {
39 | return isset($this->files[$this->position]);
40 | }
41 |
42 | public function key(): int
43 | {
44 | return $this->position;
45 | }
46 |
47 | public function current(): File
48 | {
49 | return $this->files[$this->position];
50 | }
51 |
52 | public function next(): void
53 | {
54 | $this->position++;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------