Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Implement PHPStan Preg::match() extensions #8103

Merged
merged 8 commits into from
Sep 24, 2024

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Jul 5, 2024

In PHPStan 1.11.6 I have implemented array-shape inference for preg_match in PHPStan.
See a in detail write-up in my Blog: https://staabm.github.io/2024/07/05/array-shapes-for-preg-match-matches.html

TL;DR: PHPStan is able to figure out the type $matches has after a preg_match call.

The underlying feature was implemented in a way, that it can be re-used in 3rd party libs, which wrap calls to preg_match in a API-compatible way. With this PR I am providing the necessary PHPStan extensions so calls to PHP-CS-Fixers own Preg::match static method can utilize the new capabilities.

This means code like e.g.

$currentVersion = $this->getApplication()->getVersion();
Preg::match('/^v?(?<major>\d+)\./', $currentVersion, $matches);
$currentMajor = (int) $matches['major'];

PHPStan is able to infer the type of $matches - like dumpType($matches);:

before this PR

------ ------------------------------------------- 
  Line   src/Console/Command/SelfUpdateCommand.php  
 ------ ------------------------------------------- 
  100    Dumped type: array<string>                 
 ------ ------------------------------------------- 

after this PR

 ------ ------------------------------------------------------------ 
  Line   src/Console/Command/SelfUpdateCommand.php                   
 ------ ------------------------------------------------------------ 
  100    Dumped type: array{0?: string, major?: string, 1?: string}  
 ------ ------------------------------------------------------------ 

After all this means PHPStan is able to determine more precise typings while analyzing PHP-CS-Fixer


Update: PHPStan 1.11.9 introduced additional type narrowing for preg_match_all(). This was also incorporated in this PR.

@coveralls
Copy link

Coverage Status

coverage: 95.021% (-0.1%) from 95.169%
when pulling ac2b5a9 on staabm:stan
into f391652 on PHP-CS-Fixer:master.

@coveralls
Copy link

Coverage Status

coverage: 95.021% (-0.1%) from 95.169%
when pulling fa496b8 on staabm:stan
into f391652 on PHP-CS-Fixer:master.

@staabm staabm changed the title chore: Implement PHPStan Preg::match() extensions chore: Implement PHPStan Preg::match() extensions Jul 5, 2024
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;

final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder how we can avoid those custom extensions.
Preg::match is proxy to preg_match, can't we rely on php-stan knowledge of preg_match typing?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what this extension does, can't see how it could be any shorter.

Copy link
Member

@keradus keradus Jul 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can avoid extension at all and have in-place (close to definition of Preg::match) declaration, something like @returns ReturnType<preg_match> (if I would do it in typescript - sorry, not that deeply familiar with PHPStan typing)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible. This is the closest we can get and still be flexible. Some preg_match "replacements" sometimes have a bit different behaviour in regard to some flags for example. Some replacements still use by-ref parameters, some other replacements return matches instead.

@Wirone
Copy link
Member

Wirone commented Jul 7, 2024

@staabm could you please rebase this PR and resolve conflicts? I can do it too but later (AFK now).

@staabm
Copy link
Contributor Author

staabm commented Jul 7, 2024

could you please rebase this PR and resolve conflicts?

@Wirone done

@coveralls
Copy link

coveralls commented Jul 7, 2024

Coverage Status

coverage: 95.13%. remained the same
when pulling d2da3a8 on staabm:stan
into f65e6a2 on PHP-CS-Fixer:master.

@Wirone
Copy link
Member

Wirone commented Jul 7, 2024

Auto-review tests have to be adjusted. But I believe PHPStan extensions classes have to be placed outside of the src, as this is not a production code that should be delivered to end users in releases. Maybe dev-tools/phpstan/extensions would be a good place, with autoload-dev entry in composer.json? But it also would require adding this path to other tools' configs, to cover them with QA.

@keradus

This comment was marked as duplicate.

@staabm
Copy link
Contributor Author

staabm commented Jul 11, 2024

moved the code to a dev-tools/phpstan. should I regenerate the baseline? I did not yet do that as this might easily produce conflicts with other PRs.

the CI job looks like with this added type knowledge, a lot of false positive will be deleted from the baseline :-)

@mvorisek
Copy link
Contributor

Yes, failing CI cannot be reviewed :)

@staabm
Copy link
Contributor Author

staabm commented Jul 15, 2024

just updated the PR to the latest RegexArrayShapeMatcher api (which adds support for preg_quoted() patterns), which means this PR will require PHPStan 1.11.8 as a minimum version (not yet released)

@Wirone
Copy link
Member

Wirone commented Jul 15, 2024

PR looks good to me, we just need to clean up some CI jobs to make this PR green. @staabm yes, baseline must be regenerated as I don't have rights to merge PRs with failed jobs (other PRs have to resolve conflicts).

@staabm
Copy link
Contributor Author

staabm commented Aug 5, 2024

added support for matchAll
(requires another PHPStan update)

@Wirone
Copy link
Member

Wirone commented Sep 23, 2024

FYI: I did an interactive rebase and cleaned up commits a little (kept only meaningful changes).

@staabm
Copy link
Contributor Author

staabm commented Sep 23, 2024

let me know whether there is anything I can do to help getting this landed.

I believe we need proper inner structure for PHPStan-related files, with more precise namespaces. In the future we may need classes that act as a PHPStan services (not extensions), tests are also nice to have 😉.
Personally I find it better than if/else with similar calls to different methods with exact same parameters.
@Wirone
Copy link
Member

Wirone commented Sep 23, 2024

@staabm Instead of doing classic review I just applied changes I would like to see. Please get familiar with the changes and let me know if you're OK with them 🙂.

@staabm
Copy link
Contributor Author

staabm commented Sep 23, 2024

tests are also nice to have

I will add tests sometime this week

$flagsType = $scope->getType($flagsArg->value);
}

$matcherMethod = ('match' === $methodReflection->getName()) ? 'matchExpr' : 'matchAllExpr';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$matcherMethod = ('match' === $methodReflection->getName()) ? 'matchExpr' : 'matchAllExpr';
$matcherMethod = 'match' === $methodReflection->getName()
? 'matchExpr'
: 'matchAllExpr';

pure cs fix, one more in another file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If CS tool does not complain, then it's a personal preference - I use multiline ternaries only when these are longer than the max line length (in IDE), parentheses are superfluous but make the expression more readable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #7587

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am aware of this feature request, but this is not part of current Fixer's coding standard, so it's not applicable 🙂.

@Wirone Wirone merged commit 3e921f2 into PHP-CS-Fixer:master Sep 24, 2024
27 checks passed
@Wirone
Copy link
Member

Wirone commented Sep 24, 2024

Thank you @staabm 🍻!

use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;

final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have noticed Preg::replaceCallback is not passing the shape to the callback, would you mind writing an extension for it as well?

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.

7 participants