├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README-CUSTOM.md ├── README.md ├── bin └── php-styler ├── composer.json ├── php-styler.php ├── phpstan.neon ├── phpunit.php ├── phpunit.xml ├── resources └── php-styler.php ├── src ├── Command │ ├── Apply.php │ ├── ApplyOptions.php │ ├── Check.php │ ├── CheckOptions.php │ ├── Command.php │ ├── Preview.php │ └── PreviewOptions.php ├── Config.php ├── Exception.php ├── Files.php ├── Line.php ├── Nesting.php ├── Parser.php ├── Printable │ ├── Args.php │ ├── ArrayDim.php │ ├── Array_.php │ ├── ArrowFunction.php │ ├── As_.php │ ├── AttributeGroup.php │ ├── AttributeGroups.php │ ├── Body.php │ ├── BodyEmpty.php │ ├── Break_.php │ ├── Cast.php │ ├── ClassConst.php │ ├── ClassMethod.php │ ├── ClassProperty.php │ ├── Class_.php │ ├── Closure.php │ ├── ClosureUse.php │ ├── Comment.php │ ├── Comments.php │ ├── Cond.php │ ├── Const_.php │ ├── Continue_.php │ ├── DeclareDirective.php │ ├── Declare_.php │ ├── Do_.php │ ├── DoubleArrow.php │ ├── ElseIf_.php │ ├── Else_.php │ ├── Encapsed.php │ ├── End.php │ ├── EnumCase.php │ ├── Enum_.php │ ├── Expr.php │ ├── Expression.php │ ├── Extends_.php │ ├── False_.php │ ├── For_.php │ ├── Foreach_.php │ ├── Function_.php │ ├── Goto_.php │ ├── HaltCompiler.php │ ├── Heredoc.php │ ├── If_.php │ ├── Implements_.php │ ├── Infix.php │ ├── InfixOp.php │ ├── InlineComment.php │ ├── InlineHtml.php │ ├── InstanceOp.php │ ├── Interface_.php │ ├── Label.php │ ├── MatchArm.php │ ├── Match_.php │ ├── MemberOp.php │ ├── Modifiers.php │ ├── Namespace_.php │ ├── New_.php │ ├── Nowdoc.php │ ├── Null_.php │ ├── ParamName.php │ ├── Params.php │ ├── PostfixOp.php │ ├── Precedence.php │ ├── PrefixOp.php │ ├── Printable.php │ ├── ReservedArg.php │ ├── ReservedStmt.php │ ├── ReservedWord.php │ ├── ReturnType.php │ ├── Return_.php │ ├── Separator.php │ ├── StaticOp.php │ ├── SwitchCase.php │ ├── SwitchCaseDefault.php │ ├── Switch_.php │ ├── Ternary.php │ ├── Throw_.php │ ├── Trait_.php │ ├── True_.php │ ├── TryCatch.php │ ├── TryFinally.php │ ├── Try_.php │ ├── Unset_.php │ ├── UseImport.php │ ├── UseTrait.php │ ├── UseTraitAs.php │ ├── UseTraitInsteadof.php │ ├── While_.php │ └── Yield_.php ├── Printer.php ├── Service.php ├── Split.php ├── Styler.php ├── Visitor.php ├── Whitespace.php └── Whitespace │ ├── Condense.php │ └── Rtrim.php └── tests ├── ConfigTest.php ├── Examples ├── _vendors_00001.php ├── _vendors_00002.php ├── arithmetic.php ├── array.php ├── arrow-function.php ├── assignment.php ├── attribute.php ├── auto-blank-lines.php ├── backticks.php ├── bitwise.php ├── boolean.php ├── break.php ├── cast.php ├── class.php ├── closure-args.php ├── closure.php ├── coalesce.php ├── comments-inline.php ├── comments.php ├── comparison.php ├── concat.php ├── const.php ├── continue.php ├── declare.php ├── do-line-spacing.php ├── do.php ├── empty.php ├── encapsed.php ├── enum.php ├── expansive-annotation.php ├── flow.php ├── fluent-calls.php ├── for-line-spacing.php ├── for.php ├── foreach-line-spacing.php ├── foreach.php ├── function-call.php ├── function.php ├── halt-compiler.php ├── heredoc.php ├── if-line-spacing.php ├── if.php ├── inline-html.php ├── interface.php ├── literals.php ├── magic-const.php ├── match.php ├── member-fetch.php ├── names.php ├── namespace-body.php ├── namespace.php ├── new-anonymous.php ├── new.php ├── nowdoc.php ├── numbers.php ├── operator-splits.php ├── other-expr.php ├── quoted-strings.php ├── reserved.php ├── return-direct.php ├── return-split.php ├── shift.php ├── strings.php ├── switch-line-spacing.php ├── switch.php ├── ternary.php ├── trait.php ├── try-line-spacing.php ├── try.php ├── types.php ├── use-trait.php ├── use.php ├── variables.php ├── variadic.php ├── while-line-spacing.php └── while.php ├── ExamplesTest.php ├── FilesTest.php ├── IrkStyler.php ├── IrkStylerTest.php ├── IssuesTest.php ├── LineTest.php ├── NestingTest.php ├── TestCase.php └── TypesTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: pmjones/php-styler 2 | 3 | on: 4 | push: 5 | branches: [ 0.x ] 6 | pull_request: 7 | branches: [ 0.x ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.operating-system }} 13 | strategy: 14 | matrix: 15 | operating-system: [ubuntu-latest, windows-latest] 16 | php-versions: ['8.1', '8.2', '8.3'] 17 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | coverage: xdebug 27 | - name: Check PHP Version 28 | run: php -v 29 | 30 | - name: Validate composer.json and composer.lock 31 | run: composer validate --strict 32 | 33 | - name: Cache Composer packages 34 | id: composer-cache 35 | uses: actions/cache@v3 36 | with: 37 | path: vendor 38 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-php- 41 | 42 | - name: Install dependencies 43 | run: composer install 44 | 45 | - name: QA checks 46 | run: composer check 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.php-styler.cache 2 | /.phpunit.cache 3 | /composer.lock 4 | /tmp 5 | /vendor 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.16.0 4 | 5 | Previously, PHP-Styler converted `else if` to `elseif` as part of 6 | the _Printer_ operations on parsed _Node_ objects. Now, it converts them as 7 | part of a custom _Parser_ pre-processing step by modifying the original code 8 | PHP tokens themselves. Doing so required a minor change to the testing of 9 | typehints and `declare` values. 10 | 11 | Also renamed the default config file from `resources/php-styler.dist.php` to 12 | `resources/php-styler.php`. 13 | 14 | ## 0.15.0 15 | 16 | - Make line control more explicit. 17 | 18 | - Remove Printable::$hasAttribute, $hasComment, $isFirst, hasAttribute(), 19 | hasComment(), and isFirst(). 20 | 21 | - Remove Styler::$atFirstInBody, $hadComment, $hadAttribute, 22 | forceSingleNewline(), maybeDoubleNewline(), and rtrim(). 23 | 24 | - The Line object now tracks if a margin above or below is advised for 25 | addition, and if a margin above or below is allowed. 26 | 27 | - The Styler methods now specify margin additions and allowances on the 28 | current Line object. 29 | 30 | - Protect various Styler methods that were mistakenly public. 31 | 32 | - Add method Styler::blankLine() to create new Line instances. 33 | 34 | - Rename Line::newline() to Line::blankLine(). 35 | 36 | - Styler::newline() now applies one line of margin above and below auto-split 37 | lines, subject to margin allowances; updated tests to reflect this 38 | expectation. 39 | 40 | - Fix a logic-breaking bug where final `else` gets lost after `else if` 41 | (refs #4). 42 | 43 | ## 0.14.0 44 | 45 | - Can now `apply` styling to arbitrary paths by passing file and directory 46 | names at the command line. 47 | 48 | ## 0.13.0 49 | 50 | - Force visibility on class members 51 | 52 | - Rename Property to ClassProperty 53 | 54 | - Style ClassMethod separately from Function 55 | 56 | ## 0.12.0 57 | 58 | - Make heredoc and nowdoc expansive in args and arrays 59 | 60 | - Comments now track what kind of Node they are on 61 | 62 | ## 0.11.0 63 | 64 | - Improve presentation of inline comments. 65 | 66 | - Rename Styler::functionBodyClipWhen() to functionBodyCondenseWhen(). 67 | 68 | - Rename Styler::maybeNewline() to maybeDoubleNewline(). 69 | 70 | - In Styler, replace clip+newline idiom with forceSingleNewline() method. 71 | 72 | ## 0.10.1 73 | 74 | Fixes a logic-breaking bug with inline docblock comments. 75 | 76 | Previously, this source code ... 77 | 78 | ```php 79 | // set callbacks 80 | $foo = 81 | /** @param array $bar */ 82 | function (array $bar) : string { 83 | return baz($bar); 84 | }; 85 | ``` 86 | 87 | ... would be presented as ... 88 | 89 | ```php 90 | // set callbacks$foo = 91 | 92 | /** @param array $bar */ 93 | function (array $bar) : string { 94 | return baz($bar); 95 | }; 96 | ``` 97 | 98 | ... thereby breaking the code. With this fix, it is presented as ... 99 | 100 | ```php 101 | // set callbacks 102 | $foo = 103 | 104 | /** @param array $bar */ 105 | function (array $bar) : string { 106 | return baz($bar); 107 | }; 108 | ``` 109 | 110 | ... which corrects the logic-breaking bug, though the presentation leaves something to be desired. 111 | 112 | ## 0.10.0 113 | 114 | - Inline comments, including end-of-line comments, are now presented with greater fidelity to the original code. 115 | 116 | - Other internal QA tooling changes and additions. 117 | 118 | ## 0.9.0 119 | 120 | - Fix testing on Windows, with related Github workflow changes. 121 | 122 | - Add `check` command, to see if any files need styling. 123 | 124 | ## 0.8.0 125 | 126 | - Fix #4 (`else if` now presented as `elseif`) 127 | 128 | - Rename `Styler::lastSeparatorChar()` to `lastSeparator()` 129 | 130 | - Add methods `Styler::lastArgSeparator()`, `lastArraySeparator()`, `lastParamSeparator()`, `lastMatchSeparator()` to allow for different last-item-separators on different constructs. 131 | 132 | - Updated docs & tests 133 | 134 | ## 0.7.0 135 | 136 | - Add Styler::lastSeparatorChar() to specify comma (or no comma) on last item of split list. 137 | 138 | - Fix `apply` command to honor the `--force` option again. 139 | 140 | - Floats, integers, single-quoted strings, and non-interpolated double-quoted strings now display their original raw value, not a reconstructed value. 141 | 142 | - A file that starts with a `return` now has the return on the same line as the opening ``. 211 | 212 | - `new` is no longer expansive. 213 | 214 | - Address some addcslashes() handling of newlines when escaping strings. 215 | 216 | ## 0.3.0 217 | 218 | - Complete rewrite of code reassembly process using a Line object that splits into sub-Line objects, and applies splits to each Line independently. 219 | 220 | - Improved expansiveness handling. 221 | 222 | - Split rules now operate on a shared generic level rather than separate independent levels. 223 | 224 | - Styler sets default operators in constructor now; no need to call parent::setOperator() in extended Styler classes. 225 | 226 | - Something of a performance reduction (runs about 25% slower and uses about 25% more memory). When running PHP-Styler against itself: 227 | 228 | - 0.2.0 styled 125 files in 1.234 seconds (0.0099 seconds/file, 19.31 MB peak memory usage), 229 | 230 | - 0.3.0 this release styles 107 files in 1.308 seconds (0.0122 seconds/file, 23.61 MB peak memory usage). 231 | 232 | ## 0.2.0 233 | 234 | - Roughly 8x speed improvement from removing `php -l` linting in favor of PHP-Parser linting. 235 | 236 | - Cache is now ignored, though cache config remains. 237 | 238 | - Substantial improvements to line splitting and configurability of operators. 239 | 240 | - Still some problems with splitting when there are intervening unsplittable lines. 241 | 242 | ## 0.1.0 243 | 244 | Initial release. 245 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to review any contributions you want to make. The time between 4 | submitting a contribution and its review one may be extensive; do not be 5 | discouraged if there is not immediate feedback. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Paul M. Jones and Nikita Popov. 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-CUSTOM.md: -------------------------------------------------------------------------------- 1 | # Customizing PHP-Styler 2 | 3 | ## Overview 4 | 5 | 1. [Custom _Styler_ Class](#custom-styler-class) 6 | 7 | 2. [Method Overrides](#method-overrides) 8 | 9 | 3. [Operator Spacing](#operator-spacing) 10 | 11 | 4. [Brace Placement](#brace-placement) 12 | 13 | 5. [Trailing Comma](#trailing-comma) 14 | 15 | 6. [Function Signatures](#function-signatures) 16 | 17 | 7. [Finished Output](#finished-output) 18 | 19 | 20 | ## Custom Styler Class 21 | 22 | The easiest way to start is with an empty anonymous extension of _Styler_ in your `php-styler.php` config file; remember to include various _PhpParser_ amd _PhpStyler_ imports. 23 | 24 | ```php 25 | use PhpParser\Node\Expr; 26 | use PhpParser\Node\Stmt; 27 | use PhpStyler\Config; 28 | use PhpStyler\Files; 29 | use PhpStyler\Printable as P; 30 | use PhpStyler\Printable\Printable; 31 | use PhpStyler\Styler; 32 | 33 | return new Config( 34 | files: new Files(__DIR__ . '/src'), 35 | styler: new class (lineLen: 88) extends Styler { 36 | }, 37 | ); 38 | ``` 39 | 40 | You might also create an entirely separate class, then load and instantiate it as the `$styler` argument. 41 | 42 | ```php 43 | use PhpParser\Node\Expr; 44 | use PhpParser\Node\Stmt; 45 | use PhpStyler\Config; 46 | use PhpStyler\Files; 47 | use PhpStyler\Printable as P; 48 | use PhpStyler\Printable\Printable; 49 | use PhpStyler\Styler; 50 | 51 | class MyStyler extends Styler 52 | { 53 | } 54 | 55 | return new Config( 56 | files: new Files(__DIR__ . '/src'), 57 | styler: new MyStyler(lineLen: 88), 58 | ); 59 | ``` 60 | 61 | Then invoke the `php-styler preview` command to make sure it works without errors. 62 | 63 | ## Method Overrides 64 | 65 | In general, override the `Styler::s*()` method for styling the relevant _Printable_. See the _Styler_ itself to get an idea of the very large number of methods available for override. There is really quite a lot here; you will be well-served by experimenting with trial-and-error when attempting customizations. 66 | 67 | ## Operator Spacing 68 | 69 | The `$this->operators` property describes the spacing around operation strings. This property is used by the `Styler::sInfix*()`, `Styler::sPrefix*()`, and `Styler::sPostfix*()` method families. 70 | 71 | Each `$this->operators` key is the class name of the operation, and each value is a three-element array consisting of the space before the operator, the operator itself, and the space after the operator. 72 | 73 | You can modify the spacing around operators by overriding the `Styler::modOperators()` method returning the operators to modify. (Cf. the `Styler::$operators` property for all operator strings.) For example, to make sure there is no space around `!`: 74 | 75 | ```php 76 | protected function modOperators() : array 77 | { 78 | return [ 79 | Expr\BooleanNot::class => ['', '!', ''], 80 | ]; 81 | } 82 | ``` 83 | 84 | ## Brace Placement 85 | 86 | The _Styler_ comes with several methods dedicated to brace placement. 87 | 88 | - `braceOnNextLine()` puts an opening brace on the next line. 89 | - `braceOnSameLine()` puts an opening brace on the same line. 90 | - `braceEnd()` puts a closing brace on the same line. 91 | 92 | Use these methods to place braces when overiding a `Styler::s*()` method. 93 | 94 | In addition, the standard _Styler_ uses two common methods for brace placement on class-like structures and control-flow structures: 95 | 96 | - `classBrace()` defines brace placement on class-like structures (`class`, `interface`, `trait`, etc.); defaults to `braceOnNextLine()` 97 | - `controlBrace()` defines brace placement on control-flow structures (`if`, `do`, `foreach`, etc.); defaults to `braceOnSameLine()` 98 | 99 | Override `classBrace()` to change brace placement on all class-like structures. Likewise, override `controlBrace()` to change brace placement on all control-flow structures. Finally, if you want to, you can override the class-like and control flow `Styler::s*()` methods to handle brace placement on each individual structure. 100 | 101 | ## Function Signatures 102 | 103 | The default presentation behavior for a function signature with expansive parameters and a return typehint is to ... 104 | 105 | - put a space on either side of the return typehint colon, and 106 | - put the opening brace on the next line. 107 | 108 | This keeps the parameters and return typehint lined up vertically with 4-space indents, and presents visual blank space between the signature and the body, like so: 109 | 110 | ```php 111 | public function veryLongFunctionName( 112 | $veryLongParameter1, 113 | $veryLongParameter2, 114 | $veryLongParameter3, 115 | $veryLongParameter4, 116 | $veryLongParameter5, 117 | ) : ReturnTypeHint 118 | { 119 | // ... 120 | } 121 | ``` 122 | 123 | To change the spacing around the return typehint colon, override the method that adds the colon and spaces: 124 | 125 | ```php 126 | protected function sReturnType(P\ReturnType $p) : void 127 | { 128 | $this->line[] = ': '; 129 | } 130 | ``` 131 | 132 | To present the brace on the same line, override the method that sets the condition for when the space between the function signature and the function body should be condensed: 133 | 134 | ```php 135 | protected function functionBodyCondenseWhen(): callable 136 | { 137 | return fn (string $lastLine): bool => str_starts_with(trim($lastLine), ')'); 138 | } 139 | ``` 140 | 141 | Changing the colon and brace placement in that manner will de-align the return typehint from the rest of the signature, and remove the visual blank space between the signature and the body: 142 | 143 | ```php 144 | public function veryLongFunctionName( 145 | $veryLongParameter1, 146 | $veryLongParameter2, 147 | $veryLongParameter3, 148 | $veryLongParameter4, 149 | $veryLongParameter5, 150 | ): ReturnTypeHint { 151 | // ... 152 | } 153 | ``` 154 | 155 | 156 | ## Trailing Comma 157 | 158 | By default, the _Styler_ adds a trailing comma to the last item in an argument, parameter, or array listing, when that listing has been split across lines. 159 | 160 | To not-add the trailing comma, override `Styler::lastSeparator()` to return an empty string: 161 | 162 | ```php 163 | protected function lastSeparator() : string 164 | { 165 | return ''; 166 | } 167 | ``` 168 | 169 | To not-add the comma only in some lists but not others, override the `Styler::last*Separator()` method for that type of list: 170 | 171 | ```php 172 | protected function lastArgSeparator() : string 173 | { 174 | // no trailing comma for args 175 | return ''; 176 | } 177 | 178 | protected function lastArraySeparator() : string 179 | { 180 | // trailing comma for arrays 181 | return ','; 182 | } 183 | 184 | protected function lastParamSeparator() : string 185 | { 186 | // no trailing comma for params 187 | return ''; 188 | } 189 | ``` 190 | 191 | ## Finished Output 192 | 193 | The finished output of styled code is handled by the `finish()` method. This is where you can add or trim lines around the code. For example, to make sure there is always a double-newline at the top of the file, and no line ending at the end: 194 | 195 | ```php 196 | protected function finish(string $code) : string 197 | { 198 | return '<' . '?php' . $this->eol . $this->eol . trim($code); 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Styler 2 | 3 | **WARNING!!!** 4 | 5 | PHP-Styler will **completely reformat** your PHP code, discarding any previous formatting entirely. 6 | 7 | > McCoy: What if this thing were used where [formatting] already exists? 8 | > 9 | > Spock: It would destroy such [formatting] in favor of its new matrix. 10 | > 11 | > McCoy: Its new matrix? Do you have any idea what you're saying? 12 | > 13 | > Spock: I was not attempting to evaulate its [aesthetic] implications. 14 | > 15 | > -- *Star Trek II: The Wrath of Khan* (paraphrased) 16 | 17 | You can try an online demonstration of PHP-Styler at . 18 | 19 | * * * 20 | 21 | ## Introduction 22 | 23 | PHP-Styler is a companion to [PHP-Parser](https://github.com/nikic/PHP-Parser) for reconstructing PHP code after it has been deconstructed into an abstract syntax tree. 24 | 25 | Whereas the PHP-Parser pretty printer does not have output customization as a main design goal, PHP-Styler does. (Please review [README-CUSTOM.md](./README-CUSTOM.md) for more information.) 26 | 27 | PHP-Styler is targeted toward declaration/definition files (class, interface, enum, trait) and script files. 28 | 29 | PHP-Styler is **not appropriate** for PHP-based templates, as it does not use the alternative control structures. Perhaps a future release will include a custom _AlternativeStyler_ for PHP-based templates using alternative control structures. 30 | 31 | ### How It Works 32 | 33 | PHP-Styler uses a multiple-pass system to reformat and style PHP code: 34 | 35 | 1. The _Parser_ converts the code to an abstract syntax tree of _Node_ elements, applying transformations from a _Visitor_ along the way. 36 | 2. The _Printer_ flattens the _Node_ tree into a list of _Printable_ elements. 37 | 3. The _Styler_ converts each _Printable_ back into text using a series of _Line_ objects; it applies horizontal spacing, vertical spacing, and line-splitting rules as it goes. 38 | 39 | > Note: 40 | > 41 | > The _Parser_ additionally converts all uses of `else if` (with a space between 42 | > the keywords) to `elseif` (without the space) as a pre-processing step; this 43 | > is both a practical and a stylistic matter. Cf. 44 | > and . 45 | 46 | ### Design Goals 47 | 48 | - **Logic Preservation.** Restructured PHP code will continue to operate as before. 49 | 50 | - **Horizontal and Vertical Spacing.** Automatic indenting and blank-line placement. 51 | 52 | - **Line Length Control.** Automatic splitting across multiple lines when a single line is too long. 53 | 54 | - **Diff-Friendly.** Default output should aid noise-reduction in diffs. 55 | 56 | - **Customization.** Change the output style of printable elements by extending the _Styler_ and overriding the method for each _Printable_ you want to change. 57 | 58 | - **Comment Preservation.** As much as the PHP-Parser will allow. 59 | 60 | ### Styling Examples 61 | 62 | See the [Examples](./tests/Examples) directory for a nearly-exhaustive series of styling examples, or try the safe `preview` command on one of your own source files. 63 | 64 | ### Comparable Offerings 65 | 66 | [PHP CS Fixer](https://cs.symfony.com/) is the category leader for PHP here. It offers a huge range of customization options to fix (or not fix) specific elements of PHP code. However, it is extremely complex, and can be difficult to modify. 67 | 68 | The oldest PHP code fixer I know of is [PHP_Beautifier](https://pear.php.net/package/PHP_Beautifier). Other newer fixers include [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)/[PHPCBF](https://phpqa.io/projects/phpcbf.html) and [ECS](https://github.com/easy-coding-standard/easy-coding-standard). 69 | 70 | The [Black](https://black.readthedocs.io/en/stable/) formatter for Python appears to have similar design goals and operation as PHP-Styler. 71 | 72 | Likewise, [dart_style](https://pub.dev/packages/dart_style) is a formatter for Dart. (Read more about how it works [here](https://journal.stuffwithstuff.com/2015/09/08/the-hardest-program-ive-ever-written/).) 73 | 74 | Finally, there is a [PHP plugin for Prettier](https://github.com/prettier/plugin-php) that uses JavaScript to replace all PHP code formatting using its own rules. 75 | 76 | ## Usage 77 | 78 | ### Installation 79 | 80 | Use `composer` to add PHP-Styler as a dev requirement: 81 | 82 | ``` 83 | composer require --dev pmjones/php-styler 0.x@dev 84 | ``` 85 | 86 | Copy the default `php-styler.php` config file to your package root: 87 | 88 | ``` 89 | cp ./vendor/pmjones/php-styler/resources/php-styler.php . 90 | ``` 91 | 92 | ### Preview Formatting 93 | 94 | Safely preview how PHP-Styler will restructure a source PHP file: 95 | 96 | ``` 97 | ./vendor/bin/php-styler preview ./src/My/Source/File.php 98 | ``` 99 | 100 | Pass `-c` or `--config` to specify an alternative config file: 101 | 102 | ``` 103 | ./vendor/bin/php-styler preview \ 104 | -c /path/to/other/php-styler.php \ 105 | ./src/My/Source/File.php 106 | ``` 107 | 108 | Pass `--debug-parser` to dump the PHP-Parser AST _Node_ objects into the preview, and/or `--debug-printer` to dump the PHP-Styler array of _Printable_ objects into the preview. 109 | 110 | ### Apply Formatting 111 | 112 | Apply PHP-Styler to all files identified in the config file, overwriting them with new formatting: 113 | 114 | ``` 115 | ./vendor/bin/php-styler apply 116 | ``` 117 | 118 | Pass `-c` or `--config` to specify an alternative config file: 119 | 120 | ``` 121 | ./vendor/bin/php-styler apply -c /path/to/other/php-styler.php 122 | ``` 123 | 124 | PHP-Styler will only apply formatting to files with a modification time *later* than the cache file. To force formatting on all files regardless of modification time, pass the `--force` option: 125 | 126 | ``` 127 | ./vendor/bin/php-styler apply --force 128 | ``` 129 | 130 | Changing the config file after `apply` will invalidate the cache, implying `--force` and thereby causing PHP-Styler to apply formatting to all files. 131 | 132 | To explictly apply styling to paths other than those specified in the config file, pass a space-separated list of files and directories as arguments: 133 | 134 | ``` 135 | ./vendor/bin/php-styler apply ./src/File.php ./resources/ 136 | ``` 137 | 138 | When explicitly specifying paths, the cache time is not honored, just as if the `--force` option had been passed. 139 | 140 | ### Check Formatting 141 | 142 | Check all files identified in the config file to see if they need formatting, without changing any of the files: 143 | 144 | ``` 145 | ./vendor/bin/php-styler check 146 | ``` 147 | 148 | Pass `-c` or `--config` to specify an alternative config file: 149 | 150 | ``` 151 | ./vendor/bin/php-styler apply -c /path/to/other/php-styler.php 152 | ``` 153 | 154 | If all files look OK, the return code is `0`. If one or more files look like they need to be styled, the return code is `1`. 155 | 156 | ### Configuration 157 | 158 | The default `php-styler.php` config file looks like this: 159 | 160 | ```php 161 | .) 222 | 223 | ### Line Splitting 224 | 225 | #### Automatic 226 | 227 | At first, PHP-Styler builds each statement/instruction as a single line. If that line is "too long" (88 characters by default) the _Styler_ reconstructs the code by trying to split it across multiple lines. It does so by applying one or more rules in order: 228 | 229 | - `implements` are split at commas. 230 | - Arrow functions are split at `=>`. 231 | - String concatenations are split at dots. 232 | - Conditions are split at parentheses. 233 | - Precedence-indicating parentheses are split. 234 | - Ternaries are split at `?`, `:`, and `?:`. 235 | - Boolean `||` and logical `or` operators are split. 236 | - Boolean `&&` and logical `and` operators are split. 237 | - Array elements are split at commas. 238 | - Argument lists are split at commas. 239 | - Coalesce `??` operators are split. 240 | - Member operators are split at `::`, `::$`, `->` and `?->`. 241 | - Parameter lists are split at commas. 242 | 243 | If the first rule does not make the line short enough, the second rule is applied in addition, then the third, and so on. 244 | 245 | Finally, PHP-Styler will add one blank line of margin around each line that has been automatically split. 246 | 247 | The line splitting logic attempts to be idiomatic; that is, PHP-Styler tries to take common line-splitting idioms into account, rather than making weighted calculations of elements. Reference projects were: 248 | 249 | - cakephp/database 250 | - laminas/laminas-mvc 251 | - nette/application 252 | - qiq/qiq 253 | - sapien/sapien 254 | - slim/slim 255 | - symfony/http-foundation 256 | 257 | #### Annotated 258 | 259 | Sometimes you may want to force lines to split expansively across lines. For example, a deeply-nested array with many elements per nesting level may look better when every element is on its own line, regardless of how short that element may be. 260 | 261 | To force expansiveness of line splitting, add the annotation `@php-styler-expansive` above the line in question. For example, this array ... 262 | 263 | ```php 264 | $foo = ['bar', 'baz', 'dib']; 265 | ``` 266 | 267 | ... would normally be presented on a single line. However, when adding the `@php-styler-expansive` annotation ... 268 | 269 | ```php 270 | /** @php-styler-expansive */ 271 | $foo = [ 272 | 'bar', 273 | 'baz', 274 | 'dib', 275 | ]; 276 | ``` 277 | 278 | ... the elements are made to split expansively across lines. 279 | 280 | PHP-Styler recognizes the one-liner annotations `/** @php-styler-expansive */` and `// @php-styler-expansive`, as well as typical docblock annotations: 281 | 282 | ```php 283 | /** 284 | * @php-styler-expansive 285 | */ 286 | ``` 287 | 288 | ### Fixing Mangled Output 289 | 290 | If PHP-Styler generates "ugly" or "weird" or "mangled" results, it might be a problem with how PHP-Styler works; please submit an issue. 291 | 292 | Alternatively, it may be an indication that the source line(s) should be refactored. Here are some suggestions: 293 | 294 | - Increase the maximum line length. The default length is 88 characters (10% more than the commonly-suggested 80-character length to allow some wiggle room). However, some codebases tend to prefer much longer lines, so increasing the line length may result in more-agreeable line splits. 295 | 296 | - Remove comments from within parameter and argument lists. 297 | 298 | - Move inline comments from the beginning or end of the line to *above* the line. 299 | 300 | - Break up a single long line into multiple shorter lines. 301 | 302 | - Assign closures embedded in arguments to separate variables. 303 | 304 | - Assign function calls embedded in concatenations to separate variables. 305 | 306 | - Assign multiple ternaries embedded in a single statement to separate variables. 307 | 308 | Unfortunately, because of how PHP-Parser handles double-quoted strings with interpolated variables ("encapsed" strings), newlines and some other whitespace characters (`\f`, `\r`, `\t`, `\v`) render as a literal `\n` (etc.) within the string. For example, this code ... 309 | 310 | ```php 311 | $sql = " 312 | SELECT * 313 | FROM {$table} 314 | "; 315 | ``` 316 | 317 | ... will be rendered as ... 318 | 319 | ```php 320 | $sql = "\n SELECT TABLE_NAME\n FROM {$table}\n"; 321 | ``` 322 | 323 | ... which is not what I would expect to see. 324 | 325 | Until there is a change to how PHP-Parser works, the only solution I can think of is to use heredoc syntax instead. Then this code ... 326 | 327 | ```php 328 | $sql = <<. 449 | 450 | Likewise, a final inline comment on a final array element may disappear: 451 | 452 | ```php 453 | $map = [ 454 | 34 => 'quot', // quotation mark 455 | 38 => 'amp', // ampersand 456 | 60 => 'lt', // less-than sign 457 | 62 => 'gt', // greater-than sign -- this comment disappears 458 | ]; 459 | ``` 460 | 461 | This too appears to be an issue with PHP-Parser iself. 462 | -------------------------------------------------------------------------------- /bin/php-styler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/php-styler.php: -------------------------------------------------------------------------------- 1 | configFile ?? $this->findConfigFile(); 29 | echo "Loading config file " . $configFile . PHP_EOL; 30 | $config = $this->loadConfigFile($configFile); 31 | 32 | // load cache time 33 | $cacheTime = $options->force 34 | ? 0 35 | : $this->getCacheTime($configFile, $config->cache); 36 | 37 | // apply styling 38 | try { 39 | $count = $this->applyStyle($config, $paths, $cacheTime); 40 | } catch (Error $e) { 41 | echo $e->getMessage() . PHP_EOL; 42 | return 1; 43 | } 44 | 45 | // update cache time 46 | if ($config->cache && ! $paths) { 47 | touch($config->cache); 48 | } 49 | 50 | // statistics 51 | $time = (hrtime(true) - $start) / 1000000000; 52 | $sum = number_format($time, 3); 53 | $avg = $count ? number_format($time / $count, 4) : 'NAN'; 54 | $mem = number_format(memory_get_peak_usage() / 1000000, 2); 55 | 56 | // report 57 | $noun = $count === 1 ? 'file' : 'files'; 58 | echo "Styled {$count} {$noun} in {$sum} seconds"; 59 | 60 | if ($count) { 61 | echo " ({$avg} seconds/file, {$mem} MB peak memory usage)"; 62 | } 63 | 64 | echo '.' . PHP_EOL; 65 | return 0; 66 | } 67 | 68 | protected function getCacheTime(string $configFile, ?string $cacheFile) : int 69 | { 70 | if (! $cacheFile) { 71 | echo "No cache file specified." . PHP_EOL; 72 | return 0; 73 | } 74 | 75 | if (! file_exists($cacheFile)) { 76 | echo "Creating cache file {$cacheFile}" . PHP_EOL; 77 | touch($cacheFile); 78 | return 0; 79 | } 80 | 81 | echo "Using cache file {$cacheFile}" . PHP_EOL; 82 | $cacheTime = (int) filemtime($cacheFile); 83 | $configTime = (int) filemtime($configFile); 84 | 85 | if ($configTime > $cacheTime) { 86 | echo "Config file modified after last cache time." . PHP_EOL; 87 | return 0; 88 | } 89 | 90 | return $cacheTime; 91 | } 92 | 93 | /** 94 | * @param string[] $paths 95 | */ 96 | protected function applyStyle( 97 | Config $config, 98 | array $paths, 99 | int|false $cacheTime, 100 | ) : int 101 | { 102 | $count = 0; 103 | $service = new Service($config->styler); 104 | 105 | if ($paths) { 106 | $cacheTime = false; 107 | $files = new Files(...$paths); 108 | } else { 109 | $files = $config->files; 110 | } 111 | 112 | /** @var string $file */ 113 | foreach ($files as $file) { 114 | $file = (string) $file; 115 | $fileTime = filemtime($file); 116 | 117 | if ($cacheTime && $fileTime <= $cacheTime) { 118 | continue; 119 | } 120 | 121 | $count ++; 122 | echo $file . PHP_EOL; 123 | $code = $service((string) file_get_contents($file)); 124 | file_put_contents($file, $code); 125 | } 126 | 127 | return $count; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Command/ApplyOptions.php: -------------------------------------------------------------------------------- 1 | failure = []; 22 | $start = hrtime(true); 23 | 24 | // load config 25 | $configFile = $options->configFile ?? $this->findConfigFile(); 26 | echo "Loading config file " . $configFile . PHP_EOL; 27 | $config = $this->loadConfigFile($configFile); 28 | 29 | // apply styling 30 | try { 31 | $count = $this->checkStyle($config); 32 | } catch (Error $e) { 33 | echo $e->getMessage() . PHP_EOL; 34 | return 1; 35 | } 36 | 37 | // statistics 38 | $time = (hrtime(true) - $start) / 1000000000; 39 | $sum = number_format($time, 3); 40 | $avg = $count ? number_format($time / $count, 4) : 'NAN'; 41 | $mem = number_format(memory_get_peak_usage() / 1000000, 2); 42 | 43 | // report 44 | $noun = $count === 1 ? 'file' : 'files'; 45 | echo "Checked {$count} {$noun} in {$sum} seconds"; 46 | 47 | if ($count) { 48 | echo " ({$avg} seconds/file, {$mem} MB peak memory usage)"; 49 | } 50 | 51 | echo '.' . PHP_EOL; 52 | $failed = count($this->failure); 53 | 54 | /** @phpstan-ignore-next-line */ 55 | $phrase = $failed === 1 ? 'file appears' : 'files appear'; 56 | echo "{$failed} {$phrase} to need styling." . PHP_EOL; 57 | return (int) $this->failure; 58 | } 59 | 60 | protected function checkStyle(Config $config) : int 61 | { 62 | $count = 0; 63 | $service = new Service($config->styler); 64 | 65 | foreach ($config->files as $file) { 66 | $file = (string) $file; 67 | $count ++; 68 | $source = (string) file_get_contents($file); 69 | $styled = $service($source); 70 | 71 | if ($source !== $styled) { 72 | echo $file . PHP_EOL; 73 | $this->failure[] = $file; 74 | } 75 | } 76 | 77 | return $count; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Command/CheckOptions.php: -------------------------------------------------------------------------------- 1 | configFile ?? $this->findConfigFile(); 23 | $config = $this->loadConfigFile($configFile); 24 | 25 | $service = new Service( 26 | $config->styler, 27 | $options->debugParser ?? false, 28 | $options->debugPrinter ?? false, 29 | $options->debugStyler ?? false, 30 | ); 31 | 32 | echo $service((string) file_get_contents($sourceFile)); 33 | return 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Command/PreviewOptions.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Files implements IteratorAggregate 18 | { 19 | /** 20 | * @var string[] 21 | */ 22 | protected array $paths = []; 23 | 24 | public function __construct(string ...$paths) 25 | { 26 | $this->paths = $paths; 27 | } 28 | 29 | public function getIterator() : Generator 30 | { 31 | foreach ($this->paths as $path) { 32 | if (is_file($path) && str_ends_with($path, '.php')) { 33 | yield $path; 34 | continue; 35 | } 36 | 37 | $files = new RecursiveIteratorIterator( 38 | new RecursiveCallbackFilterIterator( 39 | new RecursiveDirectoryIterator($path), 40 | fn ($c, $k, $i) => $this->filter($c, $k, $i), 41 | ), 42 | ); 43 | 44 | /** @var SplFileInfo $file */ 45 | foreach ($files as $file) { 46 | yield $file->getPathname(); 47 | } 48 | } 49 | } 50 | 51 | protected function filter( 52 | SplFileInfo $current, 53 | string $key, 54 | RecursiveDirectoryIterator $iterator, 55 | ) : bool 56 | { 57 | return $iterator->hasChildren() || str_ends_with((string) $current, '.php'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Line.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Line implements ArrayAccess 17 | { 18 | protected const RULES = [ 19 | P\Implements_::class, 20 | P\ArrowFunction::class, 21 | Expr\BinaryOp\Concat::class, 22 | P\Cond::class, 23 | P\Precedence::class, 24 | Expr\Ternary::class, 25 | Expr\BinaryOp\BooleanOr::class, 26 | Expr\BinaryOp\LogicalOr::class, 27 | Expr\BinaryOp\BooleanAnd::class, 28 | Expr\BinaryOp\LogicalAnd::class, 29 | P\Array_::class, 30 | P\Args::class, 31 | Expr\BinaryOp\Coalesce::class, 32 | P\MemberOp::class, 33 | P\Params::class, 34 | ]; 35 | 36 | protected string $append = ''; 37 | 38 | protected string $indent = ''; 39 | 40 | /** 41 | * @var mixed[] 42 | */ 43 | protected array $parts = []; 44 | 45 | protected Line $line; 46 | 47 | /** 48 | * @var Line[] 49 | */ 50 | protected array $lines = []; 51 | 52 | protected bool $addMarginAbove = false; 53 | 54 | protected bool $addMarginBelow = false; 55 | 56 | protected bool $allowMarginAbove = true; 57 | 58 | protected bool $allowMarginBelow = true; 59 | 60 | public function __construct( 61 | protected string $eol, 62 | protected int $indentNum, 63 | protected int $indentLen, 64 | protected bool $indentTab, 65 | protected int $lineLen, 66 | ) { 67 | } 68 | 69 | public function offsetSet(mixed $offset, mixed $value) : void 70 | { 71 | if ($offset !== null) { 72 | throw new Exception(__CLASS__ . ' is append-only.'); 73 | } 74 | 75 | $this->parts[] = $value; 76 | } 77 | 78 | public function offsetGet(mixed $offset) : mixed 79 | { 80 | throw new Exception(__CLASS__ . ' is write-only.'); 81 | } 82 | 83 | public function offsetExists(mixed $offset) : bool 84 | { 85 | return isset($this->parts[$offset]); 86 | } 87 | 88 | public function offsetUnset(mixed $offset) : void 89 | { 90 | throw new Exception(__CLASS__ . ' is append-only.'); 91 | } 92 | 93 | public function indent() : void 94 | { 95 | $this->indentNum ++; 96 | } 97 | 98 | public function outdent() : void 99 | { 100 | $this->indentNum --; 101 | } 102 | 103 | /** 104 | * This is advisory, and applies only when allowed. 105 | */ 106 | public function addMarginAbove() : void 107 | { 108 | $this->addMarginAbove = true; 109 | } 110 | 111 | public function hasMarginAbove() : bool 112 | { 113 | return $this->addMarginAbove; 114 | } 115 | 116 | /** 117 | * This is advisory, and applies only when allowed. 118 | */ 119 | public function addMarginBelow() : void 120 | { 121 | $this->addMarginBelow = true; 122 | } 123 | 124 | public function hasMarginBelow() : bool 125 | { 126 | return $this->addMarginBelow; 127 | } 128 | 129 | public function allowMarginAbove(bool $allowMarginAbove) : void 130 | { 131 | $this->allowMarginAbove = $allowMarginAbove; 132 | } 133 | 134 | public function marginAllowedAbove() : bool 135 | { 136 | return $this->allowMarginAbove; 137 | } 138 | 139 | public function allowMarginBelow(bool $allowMarginBelow) : void 140 | { 141 | $this->allowMarginBelow = $allowMarginBelow; 142 | } 143 | 144 | public function marginAllowedBelow() : bool 145 | { 146 | return $this->allowMarginBelow; 147 | } 148 | 149 | public function isBlank() : bool 150 | { 151 | return empty($this->parts); 152 | } 153 | 154 | public function autoAddMargins() : void 155 | { 156 | $tmp = clone $this; 157 | $output = ''; 158 | $tmp->append($output); 159 | 160 | if ($tmp->lines || strpos(trim($output), $this->eol)) { 161 | $this->addMarginAbove(); 162 | $this->addMarginBelow(); 163 | } 164 | } 165 | 166 | public function append(string &$output) : void 167 | { 168 | list($level, $rule) = $this->listLevelRule(); 169 | 170 | if ($this->fitsOnSingleLine($output) || ! $rule) { 171 | $output .= rtrim($this->append) . $this->eol; 172 | return; 173 | } 174 | 175 | $this->splitLines($output, $level, $rule); 176 | } 177 | 178 | protected function splitLines(string &$output, int $level, string $rule) : void 179 | { 180 | $this->lines = []; 181 | $this->line = $this->blankLine(); 182 | 183 | foreach ($this->parts as $part) { 184 | if ( 185 | $part instanceof Split 186 | && $part->level === $level 187 | && $part->rule === $rule 188 | ) { 189 | $method = lcfirst($part->type . 'Split'); 190 | $this->{$method}($part); 191 | } else { 192 | $this->line[] = $part; 193 | } 194 | } 195 | 196 | if ($this->line->parts) { 197 | $this->lines[] = $this->line; 198 | } 199 | 200 | foreach ($this->lines as $line) { 201 | $line->append($output); 202 | } 203 | } 204 | 205 | protected function blankLine() : Line 206 | { 207 | return new Line( 208 | $this->eol, 209 | $this->indentNum, 210 | $this->indentLen, 211 | $this->indentTab, 212 | $this->lineLen, 213 | ); 214 | } 215 | 216 | protected function incrSplit(Split $part) : void 217 | { 218 | $this->lines[] = $this->line; 219 | $this->line = $this->blankLine(); 220 | $this->line->indentNum ++; 221 | } 222 | 223 | protected function condenseSplit(Split $part) : void 224 | { 225 | $this->lines[] = $this->line; 226 | $this->line = $this->blankLine(); 227 | $this->line->indentNum ++; 228 | $this->line[] = new W\Condense(when: fn () => true); 229 | } 230 | 231 | protected function sameSplit(Split $part) : void 232 | { 233 | if ($part->char) { 234 | $this->line[] = $part->char; 235 | } 236 | 237 | $this->lines[] = $this->line; 238 | $this->line = $this->blankLine(); 239 | } 240 | 241 | protected function fitsOnSingleLine(string &$output) : bool 242 | { 243 | $indentStr = $this->indentTab ? "\t" : str_pad('', $this->indentLen); 244 | $this->append = str_repeat($indentStr, $this->indentNum); 245 | $oldOutput = $output; 246 | 247 | foreach ($this->parts as $part) { 248 | if ($part instanceof Whitespace) { 249 | $method = lcfirst(substr((string) strrchr(get_class($part), '\\'), 1)) 250 | . 'Whitespace'; 251 | 252 | $this->{$method}($part, $output); 253 | } elseif (is_string($part)) { 254 | $this->append .= $part; 255 | } 256 | } 257 | 258 | if (strlen($this->append) <= $this->lineLen) { 259 | return true; 260 | } 261 | 262 | $output = $oldOutput; 263 | return false; 264 | } 265 | 266 | /** 267 | * @return array{int, string} 268 | */ 269 | protected function listLevelRule() : array 270 | { 271 | $rules = []; 272 | 273 | foreach ($this->parts as $part) { 274 | if ($part instanceof Split) { 275 | if (! in_array($part->rule, static::RULES)) { 276 | throw new Exception("No such split rule: {$part->rule}"); 277 | } 278 | 279 | $rules[$part->level][] = $part->rule; 280 | } 281 | } 282 | 283 | if (! $rules) { 284 | return [0, '']; 285 | } 286 | 287 | // get the highest-priority rule at the earliest level 288 | ksort($rules); 289 | $level = key($rules); 290 | $rules = current($rules); 291 | $rules = array_intersect(static::RULES, $rules); 292 | $rule = current($rules); 293 | return [$level, $rule]; 294 | } 295 | 296 | protected function rtrimWhitespace(W\Rtrim $rtrim, string &$output) : void 297 | { 298 | $this->append = rtrim($this->append); 299 | 300 | if ($this->append === '') { 301 | $output = rtrim($output); 302 | } 303 | } 304 | 305 | protected function condenseWhitespace(W\Condense $condense, string &$output) : void 306 | { 307 | $trimmed = rtrim($output); 308 | $pos = strrpos($trimmed, $this->eol); 309 | $len = strlen($this->eol); 310 | $lastLine = substr($trimmed, $pos + $len); 311 | 312 | if (call_user_func($condense->when, $lastLine)) { 313 | $output = $trimmed; 314 | $this->append = ltrim($this->append) . $condense->append; 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/Nesting.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | protected array $types = []; 14 | 15 | public function incr(string $type) : void 16 | { 17 | $this->level ++; 18 | $this->types[$type] ??= 0; 19 | $this->types[$type] ++; 20 | } 21 | 22 | public function decr(string $type) : void 23 | { 24 | $this->level --; 25 | $this->types[$type] ??= 0; 26 | 27 | if (! $this->types[$type]) { 28 | throw new Exception("Cannot decrease {$type} nesting level below zero"); 29 | } 30 | 31 | $this->types[$type] --; 32 | } 33 | 34 | public function in(string $type) : bool 35 | { 36 | $this->types[$type] ??= 0; 37 | return (bool) $this->types[$type]; 38 | } 39 | 40 | public function level(?string $type = null) : int 41 | { 42 | $this->types[$type] ??= 0; 43 | return $type ? $this->types[$type] : $this->level; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | convertElseIf($code); 19 | return parent::parse($code, $errorHandler); 20 | } 21 | 22 | /** 23 | * Converts `else if` to `elseif` directly in the original code. 24 | */ 25 | protected function convertElseIf(string $original) : string 26 | { 27 | $converted = ''; 28 | $tokens = PhpToken::tokenize($original, TOKEN_PARSE); 29 | $k = count($tokens); 30 | 31 | for ($i = 0; $i < $k; $i ++) { 32 | $token = $tokens[$i]; 33 | $plus1 = $tokens[$i + 1] ?? null; 34 | $plus2 = $tokens[$i + 2] ?? null; 35 | 36 | if ( 37 | $token->id === T_ELSE 38 | && $plus1?->id === T_WHITESPACE 39 | && $plus2?->id === T_IF 40 | ) { 41 | $converted .= 'elseif'; 42 | $i += 2; 43 | continue; 44 | } 45 | 46 | $converted .= $token; 47 | } 48 | 49 | return $converted; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Printable/Args.php: -------------------------------------------------------------------------------- 1 | type = rtrim(substr((string) strrchr(get_class($orig), '\\'), 1), '_'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Printable/EnumCase.php: -------------------------------------------------------------------------------- 1 | fluentEnd > 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Printable/Modifiers.php: -------------------------------------------------------------------------------- 1 | isExpansive; 14 | } 15 | 16 | $this->isExpansive = $isExpansive; 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Printable/ReservedArg.php: -------------------------------------------------------------------------------- 1 | parser = new Parser(new Lexer\Emulative()); 26 | $this->printer = new Printer(); 27 | $this->nodeTraverser = new NodeTraverser(); 28 | $this->nodeTraverser->addVisitor(new Visitor()); 29 | } 30 | 31 | public function __invoke(string $code) : string 32 | { 33 | $debug = ''; 34 | 35 | /** @var Stmt[] */ 36 | $stmts = $this->parser->parse($code); 37 | $this->nodeTraverser->traverse($stmts); 38 | 39 | if ($this->debugParser) { 40 | $debug .= $this->dump("Parser Nodes: ", $stmts); 41 | } 42 | 43 | $printables = $this->printer->__invoke($stmts); 44 | 45 | if ($this->debugPrinter) { 46 | $debug .= $this->dump("Printables: ", $printables); 47 | } 48 | 49 | return $debug . $this->styler->__invoke($printables, $this->debugStyler); 50 | } 51 | 52 | protected function dump(string $label, mixed $value) : string 53 | { 54 | ob_start(); 55 | var_dump($value); 56 | return $label . ob_get_clean(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Split.php: -------------------------------------------------------------------------------- 1 | enterNodeFluency($node); 25 | $this->enterNodeExpansive($node); 26 | return null; 27 | } 28 | 29 | protected function enterNodeFluency(Node $node) : void 30 | { 31 | if ( 32 | $node instanceof Expr\MethodCall 33 | || $node instanceof Expr\New_ 34 | || $node instanceof Expr\NullsafeMethodCall 35 | || $node instanceof Expr\NullsafePropertyFetch 36 | || $node instanceof Expr\PropertyFetch 37 | || $node instanceof Expr\StaticCall 38 | || $node instanceof Expr\StaticPropertyFetch 39 | ) { 40 | $this->fluentRev[$this->fluentIdx] ??= 0; 41 | $this->fluentRev[$this->fluentIdx] ++; 42 | $node->setAttribute('fluentIdx', $this->fluentIdx); 43 | $node->setAttribute('fluentNum', null); 44 | $node->setAttribute('fluentEnd', null); 45 | $node->setAttribute('fluentRev', $this->fluentRev[$this->fluentIdx]); 46 | } else { 47 | $this->fluentIdx ++; 48 | } 49 | } 50 | 51 | protected function enterNodeExpansive(Node $node) : ?bool 52 | { 53 | return $this->enterNodeExpansiveAnnotation($node) 54 | ?? $this->enterNodeExpansiveCall($node) 55 | ?? $this->enterNodeExpansiveParams($node) 56 | ?? $this->enterNodeExpansiveArray($node) 57 | ?? null; 58 | } 59 | 60 | protected function enterNodeExpansiveAnnotation(Node $node) : ?bool 61 | { 62 | if ($this->expansiveAnnotation) { 63 | $this->expansiveAnnotation ++; 64 | return $this->setExpansive($node); 65 | } 66 | 67 | $comments = $node->getComments(); 68 | 69 | if (! $comments) { 70 | return null; 71 | } 72 | 73 | $oneLiners = [ 74 | '/^\/\*+\s*@php-styler-expansive\s?/', 75 | '/^\/\/+\s+@php-styler-expansive\s?/', 76 | ]; 77 | 78 | foreach ($comments as $comment) { 79 | $text = trim($comment->getText()); 80 | 81 | foreach ($oneLiners as $regex) { 82 | if (preg_match($regex, $text)) { 83 | $this->expansiveAnnotation ++; 84 | return $this->setExpansive($node); 85 | } 86 | } 87 | 88 | if (! str_starts_with($text, '/**')) { 89 | return null; 90 | } 91 | 92 | if (preg_match('/^\s*\*\s*@php-styler-expansive\s?/m', $text)) { 93 | $this->expansiveAnnotation ++; 94 | return $this->setExpansive($node); 95 | } 96 | } 97 | 98 | return null; 99 | } 100 | 101 | protected function enterNodeExpansiveCall(Node $node) : ?bool 102 | { 103 | if ( 104 | $node instanceof Expr\FuncCall 105 | || $node instanceof Expr\MethodCall 106 | || $node instanceof Expr\New_ 107 | || $node instanceof Expr\NullsafeMethodCall 108 | || $node instanceof Expr\NullsafePropertyFetch 109 | || $node instanceof Expr\StaticCall 110 | ) { 111 | $args = $node->args ?? []; 112 | 113 | // expansive only if multiple args. 114 | if (count($args) > 1) { 115 | foreach ($args as $arg) { 116 | if ($arg->getComments()) { 117 | return $this->setExpansive($node); 118 | } 119 | 120 | $value = $arg->value ?? null; 121 | 122 | if ($this->isExpansive($value)) { 123 | return $this->setExpansive($node); 124 | } 125 | } 126 | } 127 | } 128 | 129 | return null; 130 | } 131 | 132 | protected function enterNodeExpansiveParams(Node $node) : ?bool 133 | { 134 | foreach ($node->params ?? [] as $param) { 135 | if ($param?->getComments()) { 136 | return $this->setExpansive($node); 137 | } 138 | 139 | if ($param->attrGroups ?? []) { 140 | return $this->setExpansive($node); 141 | } 142 | } 143 | 144 | return null; 145 | } 146 | 147 | protected function enterNodeExpansiveArray(Node $node) : ?bool 148 | { 149 | if (! $node instanceof Expr\Array_) { 150 | return null; 151 | } 152 | 153 | foreach ($node->items as $item) { 154 | if ($item?->getComments() ?? false) { 155 | return $this->setExpansive($node); 156 | } 157 | 158 | $value = $item->value ?? null; 159 | 160 | if ($this->isExpansive($value)) { 161 | return $this->setExpansive($node); 162 | } 163 | } 164 | 165 | return null; 166 | } 167 | 168 | /** 169 | * @return null|int|Node|Node[] 170 | */ 171 | public function leaveNode(Node $node) : null|int|Node|array 172 | { 173 | $this->leaveNodeFluency($node); 174 | $this->leaveNodeExpansive($node); 175 | return null; 176 | } 177 | 178 | protected function leaveNodeFluency(Node $node) : void 179 | { 180 | if ( 181 | $node instanceof Expr\MethodCall 182 | || $node instanceof Expr\NullsafeMethodCall 183 | || $node instanceof Expr\NullsafePropertyFetch 184 | || $node instanceof Expr\PropertyFetch 185 | || $node instanceof Expr\StaticCall 186 | || $node instanceof Expr\StaticPropertyFetch 187 | ) { 188 | // visitor encounters the nodes in reverse order, so reverse 189 | // the fluentRev to get a count up instead of a count down 190 | $fluentIdx = $node->getAttribute('fluentIdx'); 191 | $fluentEnd = $this->fluentRev[$fluentIdx]; 192 | $fluentRev = $node->getAttribute('fluentRev'); 193 | $node->setAttribute('fluentEnd', $fluentEnd); 194 | $node->setAttribute('fluentNum', $fluentEnd - $fluentRev + 1); 195 | } 196 | } 197 | 198 | protected function leaveNodeExpansive(Node $node) : void 199 | { 200 | if ($this->expansiveAnnotation) { 201 | $this->expansiveAnnotation --; 202 | } 203 | } 204 | 205 | protected function isExpansive(?Node $value) : bool 206 | { 207 | if ($value === null) { 208 | return false; 209 | } 210 | 211 | if ($value instanceof Expr\ArrowFunction) { 212 | return true; 213 | } 214 | 215 | if ($value instanceof Expr\Closure && $value->stmts) { 216 | return true; 217 | } 218 | 219 | if ( 220 | $value instanceof Scalar\String_ 221 | && ( 222 | $value->getAttribute('kind') === Scalar\String_::KIND_HEREDOC 223 | || $value->getAttribute('kind') === Scalar\String_::KIND_NOWDOC 224 | ) 225 | ) { 226 | return true; 227 | } 228 | 229 | return false; 230 | } 231 | 232 | protected function setExpansive(Node $node) : bool 233 | { 234 | $node->setAttribute('expansive', true); 235 | return true; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Whitespace.php: -------------------------------------------------------------------------------- 1 | assertInstanceof(Config::class, $actual); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Examples/_vendors_00001.php: -------------------------------------------------------------------------------- 1 | createPipeFromSpec( 6 | $foo, 7 | is_array($middleware) ? $middleware : [$middleware], 8 | ); 9 | } catch (InvalidMiddlewareException $invalidMiddlewareException) { 10 | } 11 | } 12 | } 13 | 14 | if (true) { 15 | if ( 16 | in_array( 17 | $veryVeryVeryveryVeryVeryVeryLongerParameterveryVeryVeryVeryLongerParameter, 18 | ['foo', 'bar', 'baz'], 19 | ) 20 | && $veryVeryVeryVeryLongerParameterveryVeryVeryVeryLongerParameter 21 | && $veryVeryVeryVeryLongerParameterveryVeryVeryVeryLongerParameter 22 | ) { 23 | // whatever 24 | } 25 | } 26 | 27 | function isFunctionCall(int $i) : bool 28 | { 29 | return $this->phpTokens[$i]->is(T_STRING) 30 | && $this->nextSignificantToken($i)?->is('(') 31 | && ! $this 32 | ->prevSignificantToken($i) 33 | ?->is([ 34 | T_OBJECT_OPERATOR, 35 | T_NULLSAFE_OBJECT_OPERATOR, 36 | T_DOUBLE_COLON, 37 | T_FUNCTION, 38 | ]); 39 | } 40 | 41 | // colaesce, ternary, and array 42 | if (true) { 43 | if (true) { 44 | if (true) { 45 | $value = $default ?? ""; 46 | 47 | $placeholderAttr = [ 48 | 'value' => $value, 49 | 'disabled' => true, 50 | 'selected' => $selected == $default, 51 | ]; 52 | 53 | throw new Exception\FileNotFound( 54 | PHP_EOL 55 | . "File: {$name}" 56 | . PHP_EOL 57 | . "Extension: {$this->extension}" 58 | . PHP_EOL 59 | . "Collection: " 60 | . ($collection === '' ? '(default)' : $collection) 61 | . PHP_EOL 62 | . "Paths: " 63 | . print_r($this->paths[$collection], true) 64 | . PHP_EOL 65 | . "Catalog class: " 66 | . print_r(get_class($this), true), 67 | ); 68 | } 69 | } 70 | } 71 | 72 | // expansives 73 | if (true) { 74 | if (true) { 75 | $this->options = array_merge([], $options); 76 | 77 | $this->options = array_merge( 78 | [ 79 | 'id_field' => '_id', 80 | 'data_field' => 'data', 81 | 'time_field' => 'time', 82 | 'expiry_field' => 'expires_at', 83 | ], 84 | $options, 85 | ); 86 | 87 | $this 88 | ->getCollection() 89 | ->updateOne( 90 | [$this->options['id_field'] => $sessionId], 91 | ['$set' => $fields], 92 | ['upsert' => true], 93 | ); 94 | 95 | $this->foobar($bar, new Foo()); 96 | $this->foobar($bar, new Foo('bar', 'baz')); 97 | } 98 | } 99 | 100 | if (true) { 101 | if (true) { 102 | if (true) { 103 | throw new ServiceNotCreatedException( 104 | sprintf( 105 | 'Plugin manager configuration for "%s" is invalid; must be an array, received "%s"', 106 | $name, 107 | get_debug_type($options), 108 | ), 109 | ); 110 | } 111 | } 112 | } 113 | 114 | if (true) { 115 | if (true) { 116 | foreach ($this->paths as $collection => $paths) { 117 | foreach ($paths as $path) { 118 | $files = new RecursiveIteratorIterator( 119 | new RecursiveDirectoryIterator( 120 | $path, 121 | FilesystemIterator::SKIP_DOTS, 122 | ), 123 | RecursiveIteratorIterator::CHILD_FIRST, 124 | ); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Examples/_vendors_00002.php: -------------------------------------------------------------------------------- 1 | escaper->escapeHtmlAttr($key) 7 | . '="' 8 | . $this->escaper->escapeHtml((string) $val) 9 | . '"'; 10 | } 11 | } 12 | } 13 | } 14 | 15 | if (true) { 16 | if (true) { 17 | if (true) { 18 | if ( 19 | 0 === stripos($headers->get('Content-Type') ?? '', 'text/') 20 | && false === stripos($headers->get('Content-Type') ?? '', 'charset') 21 | ) { 22 | } 23 | 24 | return Payload::created([ 25 | 'source' => $this->executeVeryLongMethodName( 26 | fn () : VeryLongClassName 27 | => $this->activate($source, $target, $user), 28 | ), 29 | ]); 30 | } 31 | } 32 | } 33 | 34 | if (true) { 35 | if (true) { 36 | if (true) { 37 | if (true) { 38 | sprintf( 39 | 'A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', 40 | __METHOD__, 41 | get_debug_type($options['options'] ?? null), 42 | ); 43 | } 44 | } 45 | } 46 | } 47 | 48 | $attr = array_merge(['id' => null], $attr); 49 | 50 | if (true) { 51 | if (true) { 52 | if (true) { 53 | if (true) { 54 | $str .= '; expires=' 55 | . gmdate('D, d M Y H:i:s T', $this->getExpiresTime()) 56 | . '; Max-Age=' 57 | . $this->getMaxAge(); 58 | } 59 | } 60 | } 61 | } 62 | 63 | if (true) { 64 | if (true) { 65 | $this->language 66 | ->evaluate( 67 | $this->expression, 68 | [ 69 | 'request' => $request, 70 | 'method' => $request->getMethod(), 71 | 'path' => rawurldecode($request->getPathInfo()), 72 | ], 73 | ); 74 | } 75 | } 76 | 77 | if (true) { 78 | if (true) { 79 | $target = rtrim($directory, '\\') 80 | . \DIRECTORY_SEPARATOR 81 | . (null === $name ? $this->getBasename() : $this->getName($name)); 82 | } 83 | } 84 | 85 | if (true) { 86 | if (true) { 87 | $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ( 88 | $flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE 89 | ); 90 | } 91 | } 92 | 93 | if (true) { 94 | if (true) { 95 | $quotedSeparators = preg_quote($separators, '/'); 96 | 97 | preg_match_all( 98 | ' 99 | / 100 | (?!\s) 101 | (?: 102 | # quoted-string 103 | "(?:[^"\]|\.)*(?:"|\|$) 104 | | 105 | # token 106 | [^"' 107 | . $quotedSeparators 108 | . ']+ 109 | )+ 110 | (?[' 115 | . $quotedSeparators 116 | . ']) 117 | \s* 118 | /x', 119 | trim($header), 120 | $matches, 121 | \PREG_SET_ORDER, 122 | ); 123 | 124 | groupParts($matches, $separators); 125 | } 126 | } 127 | 128 | if (1) { 129 | if (1) { 130 | $query 131 | ->select([ 132 | '_very_long_element_' => new UnaryExpression( 133 | 'ROW_NUMBER() OVER', 134 | $order, 135 | ), 136 | ]) 137 | ->limit(null) 138 | ->offset(null) 139 | ->order([], true); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Examples/arithmetic.php: -------------------------------------------------------------------------------- 1 | 'zim']; 3 | $zim = $foo['bar'][$baz][1]; 4 | 5 | $long = [ 6 | 'veryLongElement', 7 | 'veryLongElement', 8 | 'veryLongElement', 9 | 'veryLongElement', 10 | 'veryLongElement', 11 | 'veryLongElement', 12 | 'veryLongElement', 13 | 'veryLongElement', 14 | 'veryLongElement', 15 | 'veryLongElement', 16 | ]; 17 | 18 | $longWithComments = [ 19 | // one 20 | 'veryLongElement', 21 | 22 | // two 23 | 'veryLongElement', 24 | 'veryLongElement', 25 | 26 | // three 27 | 'veryLongElement', 28 | 'veryLongElement', 29 | 'veryLongElement', 30 | 31 | // four 32 | 'veryLongElement', 33 | 'veryLongElement', 34 | 'veryLongElement', 35 | 'veryLongElement', 36 | ]; 37 | 38 | $veryVeryVeryVeryVeryVeryLongVariableName = [ 39 | 34 => 'quot', 40 | 38 => 'amp', 41 | 60 => 'lt', 42 | 62 => 'gt', 43 | ]; 44 | 45 | if (true) { 46 | if (true) { 47 | if (true) { 48 | $buckets[$quality][] = [ 49 | 'value' => trim($value), 50 | 'quality' => $quality, 51 | 'params' => $params, 52 | ]; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Examples/arrow-function.php: -------------------------------------------------------------------------------- 1 | $x; 3 | $bar = static fn () : int => $x; 4 | 5 | $veryVeryVeryVeryVeryVeryLongVariableName = array_filter( 6 | $cookies, 7 | fn (Cookie $cookie) 8 | => $cookie->getName() === $this->name 9 | && $cookie->getPath() === $this->path 10 | && $cookie->getDomain() === $this->domain, 11 | ); 12 | 13 | // arrow as arg in method call in array in method call 14 | function foo() 15 | { 16 | $payload = Payload::updated([ 17 | 'result' => $this->veryLongMethodName( 18 | fn () : string => $this->anotherMethodName($source, $target), 19 | ), 20 | ]); 21 | } 22 | 23 | if (true) { 24 | $config = [ 25 | Gateway::class => fn (DatabaseConnection $db) : Gateway => new Gateway($db), 26 | ]; 27 | } 28 | 29 | $longArrowFunctionName = fn (LongParam $longVar1, LongParam $longVar2) : ReturnType 30 | => foo('bar'); 31 | -------------------------------------------------------------------------------- /tests/Examples/assignment.php: -------------------------------------------------------------------------------- 1 | "value"])] 9 | #[MyAttribute(MyAttribute::VALUE)] 10 | function foo() 11 | { 12 | } 13 | 14 | function bar( 15 | #[MyAttribute] 16 | $bar, 17 | ) { 18 | } 19 | 20 | function baz( 21 | #[MyVeryVeryVeryVeryLongAttribute] 22 | $bar, 23 | 24 | #[MyVeryVeryVeryVeryLongAttribute] 25 | $baz, 26 | ) { 27 | } 28 | 29 | function dib( 30 | #[MyVeryVeryVeryVeryLongAttribute( 31 | veryLongNamedProperty1: 'foo', 32 | veryLongNamedProperty2: 'bar', 33 | )] 34 | $bar, 35 | 36 | #[MyVeryVeryVeryVeryLongAttribute] 37 | $baz, 38 | ) { 39 | } 40 | -------------------------------------------------------------------------------- /tests/Examples/auto-blank-lines.php: -------------------------------------------------------------------------------- 1 | getETags()) && null !== ($etag = $this->getEtag())) { 3 | if (0 == strncmp($etag, 'W/', 2)) { 4 | $etag = substr($etag, 2); 5 | } 6 | 7 | // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2. 8 | foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) { 9 | if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) { 10 | $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2); 11 | } 12 | 13 | if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) { 14 | $notModified = true; 15 | break; 16 | } 17 | } 18 | 19 | // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3. 20 | } elseif ($modifiedSince && $lastModified) { 21 | $notModified = strtotime($modifiedSince) >= strtotime($lastModified); 22 | // make sure comment stays 23 | } else { 24 | $fooBarBase = 'do something else'; 25 | } 26 | 27 | /** 28 | * Short line 29 | * Also a short line 30 | * A very very very very very very very very very very very very very very very very very very long line 31 | */ 32 | if (true) { 33 | $foo1->language 34 | ->evaluate( 35 | $this->expression, 36 | [ 37 | 'request' => $request, 38 | 'method' => $request->getMethod(), 39 | 'path' => rawurldecode($request->getPathInfo()), 40 | ], 41 | ); 42 | 43 | $foo2->language; 44 | 45 | $foo3->language 46 | ->evaluate( 47 | $this->expression, 48 | [ 49 | 'request' => $request, 50 | 'method' => $request->getMethod(), 51 | 'path' => rawurldecode($request->getPathInfo()), 52 | ], 53 | ); 54 | 55 | $foo4->language; 56 | 57 | $foo5->language 58 | ->evaluate( 59 | $this->expression, 60 | [ 61 | 'request' => $request, 62 | 'method' => $request->getMethod(), 63 | 'path' => rawurldecode($request->getPathInfo()), 64 | ], 65 | ); 66 | } 67 | 68 | class Foo 69 | { 70 | /** 71 | * @var array 72 | */ 73 | protected array $operators = [ 74 | Expr\Assign::class => [' ', '=', ' '], 75 | Expr\AssignOp\BitwiseAnd::class => [' ', '&=', ' '], 76 | Expr\AssignOp\BitwiseOr::class => [' ', '|=', ' '], 77 | Expr\AssignOp\BitwiseXor::class => [' ', '^=', ' '], 78 | Expr\AssignOp\Coalesce::class => [' ', '??=', ' '], 79 | Expr\AssignOp\Concat::class => [' ', '.=', ' '], 80 | Expr\AssignOp\Div::class => [' ', '/=', ' '], 81 | Expr\AssignOp\Minus::class => [' ', '-=', ' '], 82 | ]; 83 | } 84 | -------------------------------------------------------------------------------- /tests/Examples/backticks.php: -------------------------------------------------------------------------------- 1 | veryLongProperty->veryLongMethod( 31 | $veryLongVariableName, 32 | 33 | new VeryLongClassName( 34 | static function () : void { 35 | throw VeryLongException::create(); 36 | }, 37 | $this->veryLongVariableName, 38 | ), 39 | ); 40 | 41 | $shortVar = array_reduce( 42 | $foo, 43 | function ($addr, $addrs) { 44 | if ('REMOTE_ADDR' !== $addr) { 45 | $addrs[] = $addr; 46 | } elseif (isset($_SERVER['REMOTE_ADDR'])) { 47 | $addrs[] = $_SERVER['REMOTE_ADDR']; 48 | } 49 | 50 | return $addr; 51 | }, 52 | [], 53 | ); 54 | 55 | // expansive closure 56 | $foo = foo( 57 | $value, 58 | function ($value) { 59 | // do whatever 60 | }, 61 | ); 62 | 63 | // closure without body 64 | $foo = foo( 65 | $value, 66 | function ($value) {}, 67 | ); 68 | 69 | class foo 70 | { 71 | public function bar(baz $e) 72 | { 73 | $result = $this->func->proc( 74 | $psr7Request, 75 | 76 | new VeryLongClassName( 77 | static function () : void { 78 | throw ReachedFinalHandlerException::create(); 79 | }, 80 | $this->veryLongProperty, 81 | ), 82 | ); 83 | 84 | $e->setResult($result); 85 | return $result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Examples/closure.php: -------------------------------------------------------------------------------- 1 | function (DatabaseConnection $db) : Gateway { 79 | return new Gateway($db); 80 | }, 81 | ]; 82 | } 83 | -------------------------------------------------------------------------------- /tests/Examples/coalesce.php: -------------------------------------------------------------------------------- 1 | veryLongMethod() 3 | ?? $this->veryLongMethod() 4 | ?? $this->veryLongMethod() 5 | ?? $this->veryLongMethod() 6 | ?? $this->veryLongMethod(); 7 | 8 | // coalesce with ternary 9 | function foo() 10 | { 11 | // precedences with coalesce looks off 12 | $maxlifetime = (int) ( 13 | ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) 14 | ?? \ini_get('session.gc_maxlifetime') 15 | ); 16 | 17 | // fix by separating the precedences 18 | $ttl = $this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl; 19 | $maxlifetime = (int) $ttl ?? \ini_get('session.gc_maxlifetime'); 20 | } 21 | 22 | class foo 23 | { 24 | public function get(string $value) : ?AcceptHeaderItem 25 | { 26 | return $this->items[$value] 27 | ?? $this->items[explode('/', $value)[0] . '/*'] 28 | ?? $this->items['*/*'] 29 | ?? $this->items['*'] 30 | ?? null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Examples/comments-inline.php: -------------------------------------------------------------------------------- 1 | htmlAttrMatcher = /** @param array $matches */ 37 | 38 | function (array $matches) : string { 39 | return $this->htmlAttrMatcher($matches); 40 | }; 41 | 42 | $this->jsMatcher = /** @param array $matches */ 43 | 44 | function (array $matches) : string { 45 | return $this->jsMatcher($matches); 46 | }; 47 | 48 | $this->cssMatcher = /** @param array $matches */ 49 | 50 | function (array $matches) : string { 51 | return $this->cssMatcher($matches); 52 | }; 53 | -------------------------------------------------------------------------------- /tests/Examples/comments.php: -------------------------------------------------------------------------------- 1 | $b; 7 | $a > $b; 8 | $a >= $b; 9 | $a < $b; 10 | $a <= $b; 11 | $a ?? $b; 12 | -------------------------------------------------------------------------------- /tests/Examples/concat.php: -------------------------------------------------------------------------------- 1 | getMethod(), 23 | $this->getRequestUri(), 24 | $this->server->get('SERVER_PROTOCOL'), 25 | ); 26 | 27 | function concat_after_function() 28 | { 29 | // concat after function call looks off 30 | $message = sprintf( 31 | '%s %s %s', 32 | $this->getMethod(), 33 | $this->getRequestUri(), 34 | $this->server->get('SERVER_PROTOCOL'), 35 | ) 36 | . "\r\n" 37 | . $this->headers 38 | . $cookieHeader 39 | . "\r\n" 40 | . $content; 41 | 42 | // fix by extracting the function call 43 | $statusLine = sprintf( 44 | '%s %s %s', 45 | $this->getMethod(), 46 | $this->getRequestUri(), 47 | $this->server->get('SERVER_PROTOCOL'), 48 | ); 49 | 50 | return $statusLine . "\r\n" . $this->headers . $cookieHeader . "\r\n" . $content; 51 | } 52 | 53 | if (true) { 54 | if (true) { 55 | $foo = [ 56 | 'client' => 'required|numeric|model:' . Client::class, 57 | 'location' => 'numeric|model:' . Location::class, 58 | 'department' => 'numeric|model:' . Department::class, 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Examples/const.php: -------------------------------------------------------------------------------- 1 | scheme}://{$info}{$this->host}{$port}{$this->path}{$query}{$fragment}"; 3 | -------------------------------------------------------------------------------- /tests/Examples/enum.php: -------------------------------------------------------------------------------- 1 | [10, 1]]; 5 | 6 | /** @php-styler-expansive */ 7 | protected $prop2 = [ 8 | Cast\Int_::class => [ 9 | 10, 10 | 1, 11 | ], 12 | [ 13 | ], 14 | ]; 15 | 16 | protected $prop3 = [Cast\Int_::class => [10, 1]]; 17 | 18 | /* @php-styler-expansive */ 19 | protected $prop4 = [ 20 | Cast\Int_::class => [ 21 | 10, 22 | 1, 23 | ], 24 | [ 25 | ], 26 | ]; 27 | 28 | protected $prop5 = [Cast\Int_::class => [10, 1]]; 29 | 30 | // @php-styler-expansive 31 | protected $prop6 = [ 32 | Cast\Int_::class => [ 33 | 10, 34 | 1, 35 | ], 36 | [ 37 | ], 38 | ]; 39 | 40 | protected $prop7 = [Cast\Int_::class => [10, 1]]; 41 | 42 | /** 43 | * @php-styler-expansive 44 | */ 45 | protected $prop8 = [ 46 | Cast\Int_::class => [ 47 | 10, 48 | 1, 49 | ], 50 | [ 51 | ], 52 | ]; 53 | } 54 | -------------------------------------------------------------------------------- /tests/Examples/flow.php: -------------------------------------------------------------------------------- 1 | 'bar'; 25 | } 26 | 27 | function f() 28 | { 29 | goto LABEL; 30 | 31 | LABEL: 32 | $i ++; 33 | } 34 | 35 | function g() 36 | { 37 | throw new Exception(); 38 | } 39 | -------------------------------------------------------------------------------- /tests/Examples/fluent-calls.php: -------------------------------------------------------------------------------- 1 | veryLongPropertyName 3 | ->veryLongMethodName($foo, $bar) 4 | ->veryLongMethodName( 5 | $veryLongArg, 6 | $veryLongArg, 7 | $veryLongArg, 8 | $veryLongArg, 9 | $veryLongArg, 10 | ) 11 | ->veryLongMethodName(new VeryLongClassName($veryLongArg, $veryLongArg)) 12 | ->veryLongMethodName( 13 | new VeryLongClassName( 14 | $veryLongArg, 15 | $veryLongArg, 16 | $veryLongArg, 17 | $veryLongArg, 18 | $veryLongArg, 19 | ), 20 | ) 21 | ->veryLongPropertyName; 22 | 23 | // statics in fluent call 24 | function static_fluency() 25 | { 26 | // static method 27 | $result = DB::select() 28 | ->where() 29 | ->andWhere() 30 | ->groupBy() 31 | ->having() 32 | ->orHaving() 33 | ->orderBy() 34 | ->limit(); 35 | 36 | // static property 37 | $something = ClassName::$veryLongPropertyName 38 | ->veryLongMethodName() 39 | ->veryLongMethodName(); 40 | 41 | if (true) { 42 | if (true) { 43 | $foo = FooBar::fromFoo($veryVeryLongVariable, ResponseStatus::INVALID) 44 | ->setError(Error::ALREADY_RESPONDED); 45 | 46 | $bar = FooBar::fromBar($e->getResponse()) 47 | ->setRequest($e->getRequest()) 48 | ->setError((string) $e->getResponse()->getBody()) 49 | ->setException($e); 50 | 51 | $baz = FooBar::fromBaz( 52 | $response, 53 | $overrideStatus ?? DomainStatus::UNAUTHORIZED, 54 | ) 55 | ->setException($e); 56 | 57 | $payload = FooBar::fromResponse($e->getResponse()) 58 | ->setRequest($e->getRequest()) 59 | ->setError((string) $e->getResponse()->getBody()) 60 | ->setException($e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Examples/for-line-spacing.php: -------------------------------------------------------------------------------- 1 | $val) { 7 | ++ $k; 8 | } 9 | -------------------------------------------------------------------------------- /tests/Examples/function-call.php: -------------------------------------------------------------------------------- 1 | foo($i); 3 | $this?->foo($i, $j); 4 | self::foo(); 5 | foo(); 6 | $this->{$a}(); 7 | $this->{$a . $b}(); 8 | self::$a(); 9 | self::{$a . $b}(); 10 | $a(); 11 | -------------------------------------------------------------------------------- /tests/Examples/function.php: -------------------------------------------------------------------------------- 1 | vars} 14 | and then the end 15 | END; 16 | } 17 | } 18 | 19 | function bar() 20 | { 21 | $foo = 'bar'; 22 | 23 | $query = << $replacement) { 89 | if ( 90 | // Allow disabling rule by setting value to false since config 91 | // merging have no feature to remove entries 92 | false == $replacement 93 | || ! ( 94 | $controller === $namespace 95 | || str_starts_with($controller, $namespace . '\\') 96 | ) 97 | ) { 98 | // whatever 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/Examples/if.php: -------------------------------------------------------------------------------- 1 | some html 6 | more html 7 | 'zim', 4 | 'dir', 'irk' => 'doom', 5 | default => 'foo', 6 | }; 7 | 8 | $veryLongVariableName = match ($veryLongVariableName) { 9 | 'veryLongElement', 10 | 'veryLongElement', 11 | 'veryLongElement', 12 | 'veryLongElement' => 'veryLongElement', 13 | 'veryLongElement', 14 | 'veryLongElement', 15 | 'veryLongElement', 16 | 'veryLongElement' => 'veryLongElement', 17 | 'veryLongElement', 18 | 'veryLongElement', 19 | 'veryLongElement', 20 | 'veryLongElement' => 'veryLongElement', 21 | }; 22 | -------------------------------------------------------------------------------- /tests/Examples/member-fetch.php: -------------------------------------------------------------------------------- 1 | foo; 3 | $this->{$foo}; 4 | $this->{$a . $b}; 5 | $this?->foo; 6 | $this?->{$foo}; 7 | $this?->{$a . $b}; 8 | self::${$foo}; 9 | self::${$a . $b}; 10 | self::FOO; 11 | -------------------------------------------------------------------------------- /tests/Examples/names.php: -------------------------------------------------------------------------------- 1 | num = $num; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /tests/Examples/new.php: -------------------------------------------------------------------------------- 1 | y)(); 8 | new ((x)::$y)(); 9 | $x instanceof ('a' . 'b'); 10 | $x instanceof ($y ++); 11 | -------------------------------------------------------------------------------- /tests/Examples/nowdoc.php: -------------------------------------------------------------------------------- 1 | veryLongMethodName() 11 | && $this->veryLongMethodName() 12 | && $this->veryLongMethodName() 13 | && $this->veryLongMethodName() 14 | && $this->veryLongMethodName(); 15 | 16 | $foo = $this->veryLongMethodName() 17 | || $this->veryLongMethodName() 18 | || $this->veryLongMethodName() 19 | || $this->veryLongMethodName() 20 | || $this->veryLongMethodName(); 21 | 22 | $foo = $veryveryLongVariableName1 23 | ? $veryveryLongVariableName2 24 | : $veryveryLongVariableName3; 25 | -------------------------------------------------------------------------------- /tests/Examples/other-expr.php: -------------------------------------------------------------------------------- 1 | get(HiddenField::class) 7 | ->__invoke($name, $value, $attr, ...$__attr); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Examples/shift.php: -------------------------------------------------------------------------------- 1 | >= $b; 4 | $a << $b; 5 | $a >> $b; 6 | -------------------------------------------------------------------------------- /tests/Examples/strings.php: -------------------------------------------------------------------------------- 1 | getResponse(); 9 | 10 | // ternary vs member operator 11 | $foo = str_starts_with($veryLongVariableName, '.') 12 | ? $this->relative($name, $this->dirname(end($this->names))) 13 | : $veryLongVariableName; 14 | 15 | // ternary vs arguments 16 | $veryLongVariableName['selected'] = is_array($selected) 17 | ? in_array($veryLongVariableName['value'], $selected) 18 | : $veryLongVariableName['value'] == $selected; 19 | 20 | // ternary vs arguments 21 | $veryLongVariableName = is_array($veryLongVariableName) 22 | ? $veryLongVariableName[key($veryLongVariableName)] 23 | : $veryLongVariableName; 24 | 25 | // ternary in new 26 | $useTraitAs = new P\UseTraitAs( 27 | $node->trait ? $this->name($node->trait) : null, 28 | $this->name($node->method), 29 | $node->newModifier, 30 | $node->newName ? $this->name($node->newName) : null, 31 | ); 32 | 33 | // short ternary in new 34 | $useTraitAs = new P\UseTraitAs( 35 | $node->trait ?: $this->name($node->trait), 36 | $this->name($node->method), 37 | $node->newModifier, 38 | $node->newName ?: $this->name($node->newName), 39 | ); 40 | 41 | // ternary in method 42 | $useTraitAs = $this->veryLongFunctionName( 43 | $node->trait ? $this->name($node->trait) : null, 44 | $this->name($node->method), 45 | $node->newModifier, 46 | $node->newName ? $this->name($node->newName) : null, 47 | ); 48 | 49 | // short ternary in func 50 | $useTraitAs = $this->veryLongFunctionName( 51 | $node->trait ?: $this->name($node->trait), 52 | $this->name($node->method), 53 | $node->newModifier, 54 | $node->newName ?: $this->name($node->newName), 55 | ); 56 | 57 | // ternary embedded in argument with boolean looks off 58 | $sourceDirs = explode('/', isset($basePath[0]) 59 | && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); 60 | 61 | // fix by extracting the condition 62 | $condition = isset($basePath[0]) && '/' === $basePath[0]; 63 | $sourceDirs = explode('/', $condition ? substr($basePath, 1) : $basePath); 64 | 65 | if (true) { 66 | if (true) { 67 | return implode( 68 | '', 69 | [ 70 | $flags & Stmt\Class_::MODIFIER_FINAL ? 'final ' : '', 71 | $flags & Stmt\Class_::MODIFIER_ABSTRACT ? 'abstract ' : '', 72 | $flags & Stmt\Class_::MODIFIER_PUBLIC ? 'public ' : '', 73 | $flags & Stmt\Class_::MODIFIER_PROTECTED ? 'protected ' : '', 74 | $flags & Stmt\Class_::MODIFIER_PRIVATE ? 'private ' : '', 75 | $flags & Stmt\Class_::MODIFIER_STATIC ? 'static ' : '', 76 | $flags & Stmt\Class_::MODIFIER_READONLY ? 'readonly ' : '', 77 | ], 78 | ); 79 | } 80 | } 81 | 82 | if (true) { 83 | if (true) { 84 | return implode( 85 | '', 86 | [ 87 | $flags & Stmt\Class_::MODIFIER_FINAL ?: 'final ', 88 | $flags & Stmt\Class_::MODIFIER_ABSTRACT ?: 'abstract ', 89 | $flags & Stmt\Class_::MODIFIER_PUBLIC ?: 'public ', 90 | $flags & Stmt\Class_::MODIFIER_PROTECTED ?: 'protected ', 91 | $flags & Stmt\Class_::MODIFIER_PRIVATE ?: 'private ', 92 | $flags & Stmt\Class_::MODIFIER_STATIC ?: 'static ', 93 | $flags & Stmt\Class_::MODIFIER_READONLY ?: 'readonly ', 94 | ], 95 | ); 96 | } 97 | } 98 | 99 | // embedded assignments look off 100 | $newPath = ! isset($path[0]) 101 | || '/' === $path[0] 102 | || false !== ($colonPos = strpos($path, ':')) 103 | && ( 104 | $colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos 105 | ) ? "./{$path}" : $path; 106 | 107 | // fix by extracting the assignments 108 | $colonPos = strpos($path, ':'); 109 | $slashPos = strpos($path, '/'); 110 | 111 | $cond = ! isset($path[0]) 112 | || '/' === $path[0] 113 | || false !== $colonPos && $colonPos < $slashPos 114 | || false === $slashPos; 115 | 116 | $path = $cond ? "./{$path}" : $path; 117 | 118 | // split ternary before concat 119 | if (true) { 120 | if (true) { 121 | $uri = $queryString !== '' 122 | ? $endpoint->getUri() . $uriGlue . $queryString 123 | : $endpoint->getUri(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/Examples/trait.php: -------------------------------------------------------------------------------- 1 | b; 9 | $a->b(); 10 | $a->b($c); 11 | $a->{$b}(); 12 | $a->{$b}[$c](); 13 | ${$a->b}; 14 | $a[$b]; 15 | $a[$b](); 16 | ${$a[$b]}; 17 | $a::B; 18 | $a::$b(); 19 | $a::b(); 20 | $a::b($c); 21 | $a::$b[$c]; 22 | $a::$b[$c]($d); 23 | $a::{$b[$c]}($d); 24 | $a::{$b->c}(); 25 | A::${$b}[$c](); 26 | a(); 27 | $a(); 28 | $a()[$b]; 29 | $a->b()[$c]; 30 | $a::$b()[$c]; 31 | (new A())->b(); 32 | (new ${$a}())[$b]; 33 | (new $a->b())->c; 34 | -------------------------------------------------------------------------------- /tests/Examples/variadic.php: -------------------------------------------------------------------------------- 1 | foo(...$bar); 13 | 14 | // first-class callable 15 | $foo = foo(...); 16 | -------------------------------------------------------------------------------- /tests/Examples/while-line-spacing.php: -------------------------------------------------------------------------------- 1 | assertPrint($source, $source); 15 | } 16 | 17 | public static function provideExample() : array 18 | { 19 | $provide = []; 20 | $sourceFiles = glob(__DIR__ . '/Examples/*.php'); 21 | 22 | foreach ($sourceFiles as $sourceFile) { 23 | $key = ltrim( 24 | strrchr(str_replace('.source.php', '', $sourceFile), '/'), 25 | '/', 26 | ); 27 | 28 | $provide[$key] = [$sourceFile]; 29 | } 30 | 31 | return $provide; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/FilesTest.php: -------------------------------------------------------------------------------- 1 | assertSame($this->getExpect(), $actual); 22 | } 23 | 24 | public function testFile() : void 25 | { 26 | $dir = dirname(__DIR__) . '/'; 27 | $len = strlen($dir); 28 | $files = new Files($dir . 'php-styler.php'); 29 | $actual = []; 30 | 31 | /** @var string $file */ 32 | foreach ($files as $file) { 33 | $actual[] = substr($file, $len); 34 | } 35 | 36 | sort($actual); 37 | $this->assertSame(['php-styler.php'], $actual); 38 | } 39 | 40 | /** 41 | * @return string[] 42 | */ 43 | protected function getExpect() : array 44 | { 45 | $expect = [ 46 | 'Command/Apply.php', 47 | 'Command/ApplyOptions.php', 48 | 'Command/Check.php', 49 | 'Command/CheckOptions.php', 50 | 'Command/Command.php', 51 | 'Command/Preview.php', 52 | 'Command/PreviewOptions.php', 53 | 'Config.php', 54 | 'Exception.php', 55 | 'Files.php', 56 | 'Line.php', 57 | 'Nesting.php', 58 | 'Parser.php', 59 | 'Printable/Args.php', 60 | 'Printable/ArrayDim.php', 61 | 'Printable/Array_.php', 62 | 'Printable/ArrowFunction.php', 63 | 'Printable/As_.php', 64 | 'Printable/AttributeGroup.php', 65 | 'Printable/AttributeGroups.php', 66 | 'Printable/Body.php', 67 | 'Printable/BodyEmpty.php', 68 | 'Printable/Break_.php', 69 | 'Printable/Cast.php', 70 | 'Printable/ClassConst.php', 71 | 'Printable/ClassMethod.php', 72 | 'Printable/ClassProperty.php', 73 | 'Printable/Class_.php', 74 | 'Printable/Closure.php', 75 | 'Printable/ClosureUse.php', 76 | 'Printable/Comment.php', 77 | 'Printable/Comments.php', 78 | 'Printable/Cond.php', 79 | 'Printable/Const_.php', 80 | 'Printable/Continue_.php', 81 | 'Printable/DeclareDirective.php', 82 | 'Printable/Declare_.php', 83 | 'Printable/Do_.php', 84 | 'Printable/DoubleArrow.php', 85 | 'Printable/ElseIf_.php', 86 | 'Printable/Else_.php', 87 | 'Printable/Encapsed.php', 88 | 'Printable/End.php', 89 | 'Printable/EnumCase.php', 90 | 'Printable/Enum_.php', 91 | 'Printable/Expr.php', 92 | 'Printable/Expression.php', 93 | 'Printable/Extends_.php', 94 | 'Printable/False_.php', 95 | 'Printable/For_.php', 96 | 'Printable/Foreach_.php', 97 | 'Printable/Function_.php', 98 | 'Printable/Goto_.php', 99 | 'Printable/HaltCompiler.php', 100 | 'Printable/Heredoc.php', 101 | 'Printable/If_.php', 102 | 'Printable/Implements_.php', 103 | 'Printable/Infix.php', 104 | 'Printable/InfixOp.php', 105 | 'Printable/InlineComment.php', 106 | 'Printable/InlineHtml.php', 107 | 'Printable/InstanceOp.php', 108 | 'Printable/Interface_.php', 109 | 'Printable/Label.php', 110 | 'Printable/MatchArm.php', 111 | 'Printable/Match_.php', 112 | 'Printable/MemberOp.php', 113 | 'Printable/Modifiers.php', 114 | 'Printable/Namespace_.php', 115 | 'Printable/New_.php', 116 | 'Printable/Nowdoc.php', 117 | 'Printable/Null_.php', 118 | 'Printable/ParamName.php', 119 | 'Printable/Params.php', 120 | 'Printable/PostfixOp.php', 121 | 'Printable/Precedence.php', 122 | 'Printable/PrefixOp.php', 123 | 'Printable/Printable.php', 124 | 'Printable/ReservedArg.php', 125 | 'Printable/ReservedStmt.php', 126 | 'Printable/ReservedWord.php', 127 | 'Printable/ReturnType.php', 128 | 'Printable/Return_.php', 129 | 'Printable/Separator.php', 130 | 'Printable/StaticOp.php', 131 | 'Printable/SwitchCase.php', 132 | 'Printable/SwitchCaseDefault.php', 133 | 'Printable/Switch_.php', 134 | 'Printable/Ternary.php', 135 | 'Printable/Throw_.php', 136 | 'Printable/Trait_.php', 137 | 'Printable/True_.php', 138 | 'Printable/TryCatch.php', 139 | 'Printable/TryFinally.php', 140 | 'Printable/Try_.php', 141 | 'Printable/Unset_.php', 142 | 'Printable/UseImport.php', 143 | 'Printable/UseTrait.php', 144 | 'Printable/UseTraitAs.php', 145 | 'Printable/UseTraitInsteadof.php', 146 | 'Printable/While_.php', 147 | 'Printable/Yield_.php', 148 | 'Printer.php', 149 | 'Service.php', 150 | 'Split.php', 151 | 'Styler.php', 152 | 'Visitor.php', 153 | 'Whitespace.php', 154 | 'Whitespace/Condense.php', 155 | 'Whitespace/Rtrim.php', 156 | ]; 157 | 158 | foreach ($expect as $key => $val) { 159 | $expect[$key] = str_replace('/', DIRECTORY_SEPARATOR, $val); 160 | } 161 | 162 | return $expect; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/IrkStyler.php: -------------------------------------------------------------------------------- 1 | str_starts_with(trim($lastLine), ')'); 13 | } 14 | 15 | protected function sReturnType(P\ReturnType $p) : void 16 | { 17 | $this->line[] = ': '; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/IrkStylerTest.php: -------------------------------------------------------------------------------- 1 | service = new Service(new IrkStyler()); 11 | 12 | $code = <<<'CODE' 13 | assertPrint($code, $code); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/IssuesTest.php: -------------------------------------------------------------------------------- 1 | assertPrint($expect, $source); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/LineTest.php: -------------------------------------------------------------------------------- 1 | line = new Line( 13 | eol: PHP_EOL, 14 | indentNum: 0, 15 | indentLen: 4, 16 | indentTab: false, 17 | lineLen: 88, 18 | ); 19 | } 20 | 21 | public function testOffsetSet() : void 22 | { 23 | $this->line[] = 'foo'; 24 | $this->expectException(Exception::class); 25 | $this->expectExceptionMessage(Line::class . ' is append-only.'); 26 | $this->line[1] = 'baz'; 27 | } 28 | 29 | public function testOffsetGet() : void 30 | { 31 | $this->line[] = 'foo'; 32 | $this->expectException(Exception::class); 33 | $this->expectExceptionMessage(Line::class . ' is write-only.'); 34 | $foo = $this->line[0]; 35 | } 36 | 37 | public function testOffsetExists() : void 38 | { 39 | $this->line[] = 'foo'; 40 | $this->assertTrue(isset($this->line[0])); 41 | $this->assertFalse(isset($this->line[1])); 42 | } 43 | 44 | public function testOffsetUnset() : void 45 | { 46 | $this->line[] = 'foo'; 47 | $this->expectException(Exception::class); 48 | $this->expectExceptionMessage(Line::class . ' is append-only.'); 49 | unset($this->line[0]); 50 | } 51 | 52 | public function testNoSuchSplit() : void 53 | { 54 | $this->line[] = 'fake fake fake fake fake fake fake fake fake fake fake fake fake fake fake fake fake fake '; 55 | $this->line[] = new Split(0, 'fake', 'fake', null); 56 | $output = ''; 57 | $this->expectException(Exception::class); 58 | $this->expectExceptionMessage('No such split rule: fake'); 59 | $this->line->append($output); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/NestingTest.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 12 | $this->expectExceptionMessage('Cannot decrease fake nesting level below zero'); 13 | $nesting->decr('fake'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | service = new Service(new Styler()); 18 | } 19 | 20 | protected function print(string $source) : string 21 | { 22 | return $this->service->__invoke($source); 23 | } 24 | 25 | protected function assertPrint(string $expect, string $source) : void 26 | { 27 | $actual = $this->print($source); 28 | $actual = str_replace("\r\n", "\n", $actual); 29 | $expect = str_replace("\r\n", "\n", $expect); 30 | $this->assertSame($expect, $actual); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/TypesTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Cannot test complex types before PHP 8.1'); 12 | } 13 | 14 | $source = <<<'SOURCE' 15 | assertPrint($source, $source); 23 | } 24 | } 25 | --------------------------------------------------------------------------------