PHP-CS-Fixer icon indicating copy to clipboard operation
PHP-CS-Fixer copied to clipboard

Add option to `ternary_operator_spaces` to fix to separate lines

Open mvorisek opened this issue 2 years ago • 14 comments

Feature request

Code like:

$res = $cond ? $o->a() : $o->b();

to be fixed to:

$res = $cond
    ? $o->a()
    : $o->b();

The motivation is line coverage. I do not think this CS should be default, but I would be grateful if an option for formatting on a separate lines would exist, so projects with strong coverage requirements can be measured better.

mvorisek avatar Dec 17 '23 12:12 mvorisek

By the way, there seem to be no fixers that would handle the indentation of a ternary operation, is there ? If not, should it be covered by statement_indentation ?

bachinblack avatar Dec 22 '23 10:12 bachinblack

@bachinblack yeah, seems like a good place for that 🙂. Standalone fixer would be an overkill.

Wirone avatar Dec 23 '23 12:12 Wirone

Since this issue has not had any activity within the last 90 days, I have marked it as stale.

The purpose of this action is to enforce backlog review once in a while. This is mostly for maintainers and helps with keeping repository in good condition, because stale issues and PRs can accumulate over time and make it harder for others to find relevant information. It is also possible that some changes has been made to the repo already, and issue or PR became outdated, but wasn't closed for some reason. This action helps with periodic review and closing of such stale items in automated way.

You may let maintainers handle this or verify current relevancy by yourself, to help with re-triage. Any activity will remove stale label so it won't be automatically closed at this point.

I will close it if no further activity occurs within the next 30 days.

github-actions[bot] avatar Mar 23 '24 12:03 github-actions[bot]

I would love to have this. For code coverage, but more importantly for readability 🙂 Can anyone recommend an existing fixer (or multiple ones) that is close to what is needed here as examples? Then I would give it a shot to provide one as a pull request.

christian-kolb avatar Jun 04 '24 07:06 christian-kolb

@christian-kolb probably operator_linebreak is close enough, maybe multiline_whitespace_before_semicolons. In general, you can look for a "multiline" in a docs.

Wirone avatar Jun 04 '24 07:06 Wirone

@Wirone Is there any kind of documentation on the structure and inner workings of the fixers? I've tried with a timebox but only copied stuff from other fixers without really understanding the dependencies of the moving parts. And basically everything that is necessary is marked as internal. Very challenging to get into that.

christian-kolb avatar Jun 07 '24 09:06 christian-kolb

@christian-kolb you can use anything within this repo, including internal parts. These are marked as internal only to exclude it from public API so people don't build external stuff around it (or do it on their own responsibility). Unfortunately we don't have developer docs, so you basically need to learn from existing code.

Wirone avatar Jun 07 '24 10:06 Wirone

@Wirone Thanks for the quick reply. Unfortunately my timebox to tackle that topic ran out. I might tackle in the future, but can't work on it any further for now.

If anyone else is interested, that's my current version. It does work for the very simpelst parts (simple assignment of a variable at the start of a function), but not for 80% of my codebase yet.

<?php

declare(strict_types=1);

namespace App\CSFixer;

use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\SwitchAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\WhitespacesFixerConfig;

final class TernaryOperatorMultilineFixer implements FixerInterface, WhitespacesAwareFixerInterface
{
    public const string NAME = 'Namespace/ternary_operator_multiline';

    /**
     * @var WhitespacesFixerConfig
     */
    private $whitespacesConfig;

    public function isRisky(): bool
    {
        return false;
    }

    public function getDefinition(): FixerDefinitionInterface
    {
        return new FixerDefinition(
            'Ternary operators must always be used as multiline.',
            [
                new CodeSample(
                    '<?php
$value = $condition
    ? $foo
    : $bar;
',
                ),
            ],
        );
    }

    public function getName(): string
    {
        return self::NAME;
    }

    /**
     * {@inheritdoc}
     *
     * Must run after ArraySyntaxFixer, ListSyntaxFixer, TernaryToElvisOperatorFixer.
     */
    public function getPriority(): int
    {
        return 1;
    }

    // Default
    public function supports(\SplFileInfo $file): bool
    {
        return true;
    }

    // Default
    public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
    {
        $this->whitespacesConfig = $config;
    }

    public function isCandidate(Tokens $tokens): bool
    {
        return $tokens->isAllTokenKindsFound(['?', ':']);
    }

    public function fix(
        \SplFileInfo $file,
        Tokens $tokens,
    ): void {
        $alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
        $gotoLabelAnalyzer = new GotoLabelAnalyzer();
        $ternaryOperatorIndices = [];

        foreach ($tokens as $index => $token) {
            if (!$token->equalsAny(['?', ':'])) {
                continue;
            }

            if (SwitchAnalyzer::belongsToSwitch($tokens, $index)) {
                continue;
            }

            if ($alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $index)) {
                continue;
            }

            if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, $index)) {
                continue;
            }

            $ternaryOperatorIndices[] = $index;
        }

        foreach (array_reverse($ternaryOperatorIndices) as $index) {
            $token = $tokens[$index];

            if ($token->equals('?')) {
                $nextNonWhitespaceIndex = $tokens->getNextNonWhitespace($index);
                if ($tokens[$nextNonWhitespaceIndex]->equals(':')) {
                    // Ignore ?: syntax
                    continue;
                }

                $previousNonWhitespaceIndex = $tokens->getPrevNonWhitespace($index);

                $tokens->ensureWhitespaceAtIndex(
                    $index - 1,
                    1,
                    sprintf(
                        '%s%s%s',
                        $this->whitespacesConfig->getLineEnding(),
                        WhitespacesAnalyzer::detectIndent($tokens, $previousNonWhitespaceIndex),
                        $this->whitespacesConfig->getIndent()
                    ),
                );

                continue;
            }

            if ($token->equals(':')) {
                $previousNonWhitespaceIndex = $tokens->getPrevNonWhitespace($index);
                if ($tokens[$previousNonWhitespaceIndex]->equals('?')) {
                    // Ignore ?: syntax
                    continue;
                }

                $tokens->ensureWhitespaceAtIndex(
                    $index - 1,
                    1,
                    sprintf(
                        '%s%s',
                        $this->whitespacesConfig->getLineEnding(),
                        WhitespacesAnalyzer::detectIndent($tokens, $previousNonWhitespaceIndex),
                    ),
                );
            }
        }
    }
}

christian-kolb avatar Jun 07 '24 10:06 christian-kolb

The PR should support ?: as well as ?? - via an configurable option and probably disabled by default, as the syntax for these is not very often multiline by default.

mvorisek avatar Jun 07 '24 10:06 mvorisek

@mvorisek I don't agree. I don't see a point in making ?: multiline, and ?? is not a ternary, so it does not belong to such a fixer (under the name related to ternary).

@christian-kolb sorry to hear that. I understand you worked on it in your work time and had some specific timeframe, but maybe you could continue in your free time 🙂? Anyway, thanks for your work.

Wirone avatar Jun 07 '24 10:06 Wirone

@mvorisek I don't agree. I don't see a point in making ?: multiline, and ?? is not a ternary, so it does not belong to such a fixer (under the name related to ternary).

Given this issue is motivated with improved CS but also better coverage measure, it is closely related and the coverage motivation applies to ?? as well.

mvorisek avatar Jun 07 '24 11:06 mvorisek

@Wirone Good that you ask, I wasn't clear enough. I do intend on finishing it. But due to my family with two small children and 3 open source packages (which I'm primarily maintaining), there won't be much time (which it needs to get into the complex code base without documentation) 🙂. So I will still try to finish it, but I don't want "block" anyone else from trying their hand on it expecting, that I'll have it finished within the next few days or weeks. That's also the reason I copied my version in here, so that whoever might take it over, doesn't have to start from scratch.

@mvorisek I guess it's personal taste, but I wouldn't use those just for the reduced readability (in my opinion). So when I'll finish it, I won't include the configuration in the first version. Maybe in future ones, but I would at least do it step by step. First making it work with the "normal" ternary operator then perhaps improving it in the future. Please don't take it as bashing on your idea, just trying to manage expectations 🙂.

christian-kolb avatar Jun 07 '24 12:06 christian-kolb

Since this issue has not had any activity within the last 90 days, I have marked it as stale.

The purpose of this action is to enforce backlog review once in a while. This is mostly for maintainers and helps with keeping repository in good condition, because stale issues and PRs can accumulate over time and make it harder for others to find relevant information. It is also possible that some changes has been made to the repo already, and issue or PR became outdated, but wasn't closed for some reason. This action helps with periodic review and closing of such stale items in automated way.

You may let maintainers handle this or verify current relevancy by yourself, to help with re-triage. Any activity will remove stale label so it won't be automatically closed at this point.

I will close it if no further activity occurs within the next 30 days.

github-actions[bot] avatar Sep 06 '24 12:09 github-actions[bot]

This topic is still very relevant, it's just that nobody tackled it yet 🙂

christian-kolb avatar Sep 06 '24 12:09 christian-kolb

Since this issue has not had any activity within the last 90 days, I have marked it as stale.

The purpose of this action is to enforce backlog review once in a while. This is mostly for maintainers and helps with keeping repository in good condition, because stale issues and PRs can accumulate over time and make it harder for others to find relevant information. It is also possible that some changes has been made to the repo already, and issue or PR became outdated, but wasn't closed for some reason. This action helps with periodic review and closing of such stale items in automated way.

You may let maintainers handle this or verify current relevancy by yourself, to help with re-triage. Any activity will remove stale label so it won't be automatically closed at this point.

I will close it if no further activity occurs within the next 30 days.

github-actions[bot] avatar Dec 19 '24 12:12 github-actions[bot]

This topic is still very relevant, it's just that nobody tackled it yet 🙂

christian-kolb avatar Dec 19 '24 12:12 christian-kolb

Since this issue has not had any activity within the last 90 days, I have marked it as stale.

The purpose of this action is to enforce backlog review once in a while. This is mostly for maintainers and helps with keeping repository in good condition, because stale issues and PRs can accumulate over time and make it harder for others to find relevant information. It is also possible that some changes has been made to the repo already, and issue or PR became outdated, but wasn't closed for some reason. This action helps with periodic review and closing of such stale items in automated way.

You may let maintainers handle this or verify current relevancy by yourself, to help with re-triage. Any activity will remove stale label so it won't be automatically closed at this point.

I will close it if no further activity occurs within the next 30 days.

github-actions[bot] avatar Mar 20 '25 12:03 github-actions[bot]

This topic is still very relevant, it's just that nobody tackled it yet 🙂

christian-kolb avatar Mar 20 '25 12:03 christian-kolb

closing as I am against any bot noise - help welcomed

mvorisek avatar Mar 23 '25 14:03 mvorisek

Is it because I copied the answer I gave last time? The bot noice is coming from the the project, not from me 🙂

christian-kolb avatar Mar 23 '25 17:03 christian-kolb

@christian-kolb definitely nothing personal, actually thanks ❤, I just do not agree with https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.73.1/.github/workflows/issues_and_pull_requests.yml workflow. Valid issues should never go stale IMO and I do not want to get any notifications except only meaningful communication to move things forward. I expressed this multiple times to the maintainers, but ATM all I can do is to respect their decision.

mvorisek avatar Mar 23 '25 18:03 mvorisek