Skip to content

Conversation

@ruudk
Copy link
Contributor

@ruudk ruudk commented Dec 12, 2021

In my project I'm using a custom error format that I'd like to have as the default.

I need to run PHPStan with --error-format=my-format.

The idea of this PR is to introduce an errorFormat configuration option that is picked by default, and can still be overwritten with the CLI option.

Before I fix the todo, is this something you would consider merging?

Todo

  • Fix errors
  • Documentation
  • Tests?

@ondrejmirtes
Copy link
Member

I'm interested - what's the error formatter you prefer to the default one? Thanks.

@ruudk
Copy link
Contributor Author

ruudk commented Dec 12, 2021

This one:
Screenshot 2021-12-12 at 19 37 29@2x

It has the following features:

  • No table wrapper, just plain output
  • It uses Symfony's Console Hyperlinks together with the %editorUrl% to easily navigate to my preferred editor;
  • It truncates long path names (the middle, so first 3 directories and last directory + filename: src/Core/UserBundle/.../GraphQL/UserResolver.php:41);
  • Color formatting for fully qualified class names, variables, types, methods and functions to make it easier to quickly understand certain aspects;
final class MyErrorFormatter implements ErrorFormatter
{
    private const FORMAT = "{message}\n↳ <href={editorUrl}>{shortPath}:{line}</>\n";

    public function __construct(
        private RelativePathHelper $relativePathHelper,
        private ?string $editorUrl
    ) {
    }

    public function formatErrors(AnalysisResult $analysisResult, Output $output) : int
    {
        if ($analysisResult->hasErrors() === false) {
            $output->writeLineFormatted('<fg=green;options=bold>No errors</>');
            $output->writeLineFormatted('');

            return 0;
        }

        foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) {
            $output->writeLineFormatted(sprintf('<unknown location> %s', $notFileSpecificError));
        }

        foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
            $absolutePath = $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath();

            $message = $fileSpecificError->getMessage();

            // Remove escaped wildcard that breaks coloring
            $message = str_replace('\*', '*', $message);

            // Full Qualified Class Names
            $message = (string) preg_replace(
                "/([A-Z0-9]{1}[A-Za-z0-9_\-]+[\\\]+[A-Z0-9]{1}[A-Za-z0-9_\-\\\]+)/",
                '<fg=yellow>$1</>',
                $message
            );

            // Quoted strings
            $message = (string) preg_replace(
                "/(?<=[\"'])([A-Za-z0-9_\-\\\]+)(?=[\"'])/",
                '<fg=yellow>$1</>',
                $message
            );

            // Variable
            $message = (string) preg_replace(
                "/(?<=[:]{2}|[\s\"\(])([.]{3})?(\\$[A-Za-z0-9_\\-]+)(?=[\s|\"|\)])/",
                '<fg=green>$1$2</>',
                $message
            );

            // Method
            $message = (string) preg_replace(
                '/(?<=[:]{2}|[\s])(\w+\(\))/',
                '<fg=blue>$1</>',
                $message
            );

            // Function
            $message = (string) preg_replace(
                '/(?<=function\s)(\w+)(?=\s)/',
                '<fg=blue>$1</>',
                $message
            );

            // Types
            $message = (string) preg_replace(
                '/(?<=[\s\|])(null|true|false|int|float|bool|string|array|object|mixed|resource|iterable)(?=[:]{2}|[\.\s\|<,]+)/',
                '<fg=magenta>$1</>',
                $message
            );

            $output->writeLineFormatted(
                strtr(
                    self::FORMAT,
                    [
                        '{absolutePath}' => $absolutePath,
                        '{editorUrl}' => str_replace(
                            ['%file%', '%line%'],
                            [$absolutePath, (int) $fileSpecificError->getLine()],
                            $this->editorUrl ?? ''
                        ),
                        '{shortPath}' => $this->trimPath($this->relativePathHelper->getRelativePath($fileSpecificError->getFile())),
                        '{line}' => (int) $fileSpecificError->getLine(),
                        '{message}' => $message,
                        '{identifier}' => $fileSpecificError->getIdentifier(),
                    ]
                )
            );
        }

        $output->writeLineFormatted(sprintf(
            '<bg=red;options=bold>Found %d error%s</>',
            $analysisResult->getTotalErrorsCount(),
            $analysisResult->getTotalErrorsCount() === 1 ? '' : 's'
        ));
        $output->writeLineFormatted('');

        return 1;
    }

    private function trimPath(string $path) : string
    {
        $parts = explode(DIRECTORY_SEPARATOR, $path);
        if (count($parts) < 6) {
            return $path;
        }

        if (str_contains($path, 'in context of class')) {
            return $path;
        }

        return implode(
            DIRECTORY_SEPARATOR,
            [
                ...array_slice($parts, 0, 3),
                '...',
                ...array_slice($parts, -2),
            ]
        );
    }
}

@ruudk
Copy link
Contributor Author

ruudk commented Dec 12, 2021

The above formatter is a different approach to solve my previous attempt:

@ruudk
Copy link
Contributor Author

ruudk commented Dec 16, 2021

After using the new highlighted output for a while, I'm in love. It makes it so much easier to quickly spot the important parts...

I'm now wondering if this can't be part of the core (I want to do the work). If all errors would annotate dynamic parts like variables and type info with Symfony Color Styles.

For example:

sprintf(
'Method %s::%s() should return %%s but empty return statement found.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
),

sprintf(
  'Method <class>%s</>::<method>%s()</> should return <type>%%s</type> but empty return statement found.',
  $method->getDeclaringClass()->getDisplayName(),
  $method->getName()
),

@ruudk
Copy link
Contributor Author

ruudk commented Apr 4, 2022

@ondrejmirtes I'm still using my own error format and I think it's superior to the default. Do you think this PR can be merged (after I rebase + cleanup)?

@ondrejmirtes
Copy link
Member

Hi, I appreciate this and I'd like this because further down the road in PHPStan 1.x I'd like a different default error formatter to try out in bleedingEdge (and maybe make it the default one in 2.0).

To review this PR - $inceptionResult->getProjectConfigArray()['parameters']['errorFormat'] ?? 'table'; isn't the right way to go, you need $inceptionResult->getContainer()->getParameter('errorFormat').

Also - one thing to think about is the behaviour with CI-detected environment. Right now it isn't overriden if the user uses --error-format in CLI. But it'd still be overriden if the user supplied errorFormat in the config which isn't desirable.

So please do not change the error formatter if one is already supplied in the config, to match the current behaviour with --error-format.

Also please change the base to 1.6.x., thanks.

@ruudk ruudk changed the base branch from 1.5.x to 1.7.x May 21, 2022 10:13
@ruudk ruudk force-pushed the error-format-config branch from c768b40 to 5f51ddf Compare May 21, 2022 10:13
@ruudk ruudk force-pushed the error-format-config branch from 5f51ddf to dfbb0d5 Compare May 21, 2022 10:16
@ruudk ruudk marked this pull request as ready for review May 21, 2022 10:16
@ruudk
Copy link
Contributor Author

ruudk commented May 21, 2022

@ondrejmirtes I updated the PR. When --error-format is not specified, it will search for errorFormat parameter, if that is not specified, it will check for CI, or fallback to table instead.

@ondrejmirtes
Copy link
Member

Perfect now, thank you!

BTW I'm definitely interested in improving the default error formatter, and introducing a new file one which we could make the default in 2.0. I really like the one in the screenshot at https://github.com/nunomaduro/collision, so feel free to adapt that code and submit it here as a new file error formatter :)

About the syntax highlighting (totally unrelated to file vs. table formatter, it could be used by both, so it's a separate discussion/feature), the regex approach is too fuzzy for me, I worry it could break in some more complex situations, for example let's say someone has $var key in their array shape, it'd be highlighted wrong.

If all errors would annotate dynamic parts

Yes, it makes sense, but I don't want to break the original message with some Symfony Console-specific tags, because that wouldn't be compatible with other non-console formatters, it's too dangerous to do that.

But - we can attach additional information to RuleError instances created and returned by RuleErrorBuilder. So the call could look something like this:

RuleErrorBuilder::message(sprintf(
    'Method %s::%s() should return %%s but empty return statement found.',
    $method->getDeclaringClass()->getDisplayName(),
    $method->getName(),
))->richMessage(sprintf(
    'Method <class>%s</>::<method>%s()</> should return <type>%%s</type> but empty return statement found.',
    $method->getDeclaringClass()->getDisplayName(),
    $method->getName()
))->build();

WDYT?

@ondrejmirtes ondrejmirtes merged commit 94ec451 into phpstan:1.7.x May 21, 2022
@ondrejmirtes
Copy link
Member

Oh, and please update the docs in these places:

Thank you.

@ruudk ruudk deleted the error-format-config branch May 21, 2022 11:14
@ruudk
Copy link
Contributor Author

ruudk commented May 21, 2022

@ondrejmirtes Thanks, I created a PR to document it.

BTW I'm definitely interested in improving the default error formatter, and introducing a new file one which we could make the default in 2.0. I really like the one in the screenshot at https://github.com/nunomaduro/collision, so feel free to adapt that code and submit it here as a new file error formatter :)

Not really sure if I want a formatter like that. Me and my team really like the minimalistic format that we're using for the last months.

If I may suggest: please make the file formatter configurable so that one can toggle the code highlighting part.

About the syntax highlighting (totally unrelated to file vs. table formatter, it could be used by both, so it's a separate discussion/feature), the regex approach is too fuzzy for me, I worry it could break in some more complex situations, for example let's say someone has $var key in their array shape, it'd be highlighted wrong.

Yes, the regex approach is not scalable although it does a really good job. I had to work with what I was getting: raw strings 🙈

But - we can attach additional information to RuleError instances created and returned by RuleErrorBuilder. So the call could look something like this:

RuleErrorBuilder::message(sprintf(
    'Method %s::%s() should return %%s but empty return statement found.',
    $method->getDeclaringClass()->getDisplayName(),
    $method->getName(),
))->richMessage(sprintf(
    'Method <class>%s</>::<method>%s()</> should return <type>%%s</type> but empty return statement found.',
    $method->getDeclaringClass()->getDisplayName(),
    $method->getName()
))->build();

Can't we just scrub all known tags (<class>, <variable> etc) from the error message when the formatter doesn't support rich messages? I think that having to define both error messages is going to be tedious.

Maybe we can start with already adding the named color styles (https://symfony.com/doc/current/console/coloring.html#using-color-styles)? Then it's possible to use them in custom rules already.

@ondrejmirtes
Copy link
Member

Not really sure if I want a formatter like that. Me and my team really like the minimalistic format that we're using for the last months.

Could be a different formatter - something like concise or minimal. The highlighting part could be implemented in all the error formatters except for the machine-readable ones.

Can't we just scrub all known tags (, etc) from the error message when the formatter doesn't support rich messages?

You're just asking for parse issues - someone can have <class> as part of the actual type :) If we start using this, we also need to make sure that the dynamic parts of the error messages are properly escaped (I don't know if Symfony Console actually handles HTML entities correctly).

when the formatter doesn't support rich messages?

We don't know them from the outside so we need a backward-compatible approach.

I think that having to define both error messages is going to be tedious.

Tedious but necessary :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants