-
-
Notifications
You must be signed in to change notification settings - Fork 94
fix: Phpstan errors in current master build #413
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
8624690
5030837
f13e78b
e4bbdeb
dbaeb9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,17 @@ foreach ($gherkinLanguages as $lang => $keywords) { | |
| $words = [$words]; | ||
| } | ||
|
|
||
| assert(array_find($words, fn ($word) => is_string($word)) === null, 'Expected $words to be an array of strings'); | ||
| /** | ||
| * Here we force phpstan to recognise that we have narrowed the type of $words to a list of strings with the | ||
| * assertion above. Otherwise it will report an error from the later `implode()` call that $words must be a | ||
| * list<string>. | ||
| * | ||
| * There does not currently seem to be a way to force phpstan to detect this type narrowing at runtime | ||
| * https://github.com/phpstan/phpstan/issues/14360 | ||
| * | ||
| * @var array<string> $words | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately it can't go higher up because if it's before the Unless we only tag it, and don't assert at runtime at all, but that feels a bit like cheating |
||
| */ | ||
| if ($type === 'scenarioOutline') { | ||
| $type = 'scenario_outline'; | ||
| } | ||
|
|
@@ -32,7 +43,6 @@ foreach ($gherkinLanguages as $lang => $keywords) { | |
| $formattedKeywords = []; | ||
|
|
||
| foreach ($words as $word) { | ||
| assert(is_string($word)); | ||
| $formattedWord = trim($word); | ||
|
|
||
| if ($formattedWord === $word) { | ||
|
|
@@ -45,12 +55,14 @@ foreach ($gherkinLanguages as $lang => $keywords) { | |
| $words = $formattedKeywords; | ||
| } | ||
|
|
||
| usort($words, static function ($type1, $type2) { | ||
| assert(is_string($type1)); | ||
| assert(is_string($type2)); | ||
| foreach ($words as $word) { | ||
| assert(is_string($word)); | ||
| } | ||
|
|
||
| return [mb_strlen($type2, 'utf8'), $type1] <=> [mb_strlen($type1, 'utf8'), $type2]; | ||
| }); | ||
| usort( | ||
| $words, | ||
| static fn (string $type1, string $type2) => [mb_strlen($type2, 'utf8'), $type1] <=> [mb_strlen($type1, 'utf8'), $type2] | ||
| ); | ||
|
|
||
| $langMessages[$type] = implode('|', $words); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,19 +15,25 @@ | |
| use Behat\Gherkin\Lexer; | ||
| use Behat\Gherkin\Loader\GherkinFileLoader; | ||
| use Behat\Gherkin\Parser; | ||
| use PHPUnit\Framework\Attributes\DataProvider; | ||
| use PHPUnit\Framework\TestCase; | ||
|
|
||
| class GherkinFileLoaderTest extends TestCase | ||
| { | ||
| private GherkinFileLoader $loader; | ||
| private string $featuresPath; | ||
|
|
||
| private static function featuresPath(): string | ||
| { | ||
| return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'features'; | ||
| } | ||
|
|
||
| protected function setUp(): void | ||
| { | ||
| $parser = new Parser(new Lexer(new CucumberDialectProvider())); | ||
| $this->loader = new GherkinFileLoader($parser); | ||
|
|
||
| $this->featuresPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'features'; | ||
| $this->featuresPath = self::featuresPath(); | ||
| } | ||
|
|
||
| public function testSupports(): void | ||
|
|
@@ -93,26 +99,76 @@ public function testParsingCachedFeature(): void | |
| $this->assertEquals('cache', $features[0]); | ||
| } | ||
|
|
||
| public function testBasePath(): void | ||
| /** | ||
| * @return array<string, array{?string, array<string, bool>}> | ||
| */ | ||
| public static function providerSupportsWithBasePath(): array | ||
| { | ||
| return [ | ||
| 'with no base path set' => [ | ||
| null, | ||
| [ | ||
| // The default is the current working directory, and there are no files there | ||
| 'features' => false, | ||
| 'tables.feature' => false, | ||
| 'features/tables.feature' => false, | ||
| 'features/pystring.feature' => false, | ||
| 'features/multiline_name.feature' => false, | ||
| ], | ||
| ], | ||
| 'with base path set to features directory' => [ | ||
| self::featuresPath(), | ||
| [ | ||
| 'features' => false, | ||
| 'tables.feature' => true, | ||
| 'pystring.feature' => true, | ||
| 'features/tables.feature' => false, | ||
| 'features/pystring.feature' => false, | ||
| ], | ||
| ], | ||
| 'with base path set to parent of features directory' => [ | ||
| self::featuresPath() . '/../', | ||
| [ | ||
| 'features' => false, | ||
| 'tables.feature' => false, | ||
| 'pystring.feature' => false, | ||
| 'features/tables.feature' => true, | ||
| 'features/pystring.feature' => true, | ||
| ], | ||
| ], | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string,bool> $expected | ||
| */ | ||
| #[DataProvider('providerSupportsWithBasePath')] | ||
| public function testSupportsWithBasePath(?string $basePath, array $expected): void | ||
| { | ||
| $this->assertFalse($this->loader->supports('features')); | ||
| $this->assertFalse($this->loader->supports('tables.feature')); | ||
| if ($basePath !== null) { | ||
| $this->loader->setBasePath($basePath); | ||
| } | ||
|
|
||
| $this->loader->setBasePath($this->featuresPath . '/../'); | ||
| $this->assertFalse($this->loader->supports('features')); | ||
| $this->assertFalse($this->loader->supports('tables.feature')); | ||
| $this->assertTrue($this->loader->supports('features/tables.feature')); | ||
| $actual = []; | ||
| foreach (array_keys($expected) as $resource) { | ||
| $actual[$resource] = $this->loader->supports($resource); | ||
| } | ||
|
|
||
| $this->assertTrue($this->loader->supports('features/pystring.feature')); | ||
|
Comment on lines
-104
to
-106
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the first The second call then caused phpstan to decide that And this then caused it to decide that the next call to This appears to be because there's a conflict between:
It's fine if you call the I've created a simplified reproduction case in the phpstan playground
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the issue is our generic type.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stof thank you! That makes sense. If I modify the example case to assert the type of the resource rather than the instance, that looks like it works: |
||
| $this->assertSame($expected, $actual); | ||
| } | ||
|
|
||
| public function testLoadWithBasePath(): void | ||
| { | ||
| $this->loader->setBasePath(self::featuresPath() . '/../'); | ||
| $features = $this->loader->load('features/pystring.feature'); | ||
| $this->assertCount(1, $features); | ||
| $this->assertEquals('A py string feature', $features[0]->getTitle()); | ||
| $this->assertEquals($this->featuresPath . DIRECTORY_SEPARATOR . 'pystring.feature', $features[0]->getFile()); | ||
| $this->assertEquals(self::featuresPath() . DIRECTORY_SEPARATOR . 'pystring.feature', $features[0]->getFile()); | ||
|
|
||
| $this->loader->setBasePath($this->featuresPath); | ||
| $this->loader->setBasePath(self::featuresPath()); | ||
| $features = $this->loader->load('multiline_name.feature'); | ||
| $this->assertCount(1, $features); | ||
| $this->assertEquals('multiline', $features[0]->getTitle()); | ||
| $this->assertEquals($this->featuresPath . DIRECTORY_SEPARATOR . 'multiline_name.feature', $features[0]->getFile()); | ||
| $this->assertEquals(self::featuresPath() . DIRECTORY_SEPARATOR . 'multiline_name.feature', $features[0]->getFile()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this assertion looks weird to me. Shouldn't it assert that it cannot find any non-string value instead ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Urgh, yeah of course it should. I'd tried a whole range of options for trying to get phpstan to narrow the type itself without success, and mangled the one I'd settled on when I added it back to this file. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.