Skip to content

Commit b1b9c68

Browse files
committed
Add IDE type narrowing to generated validator mixins
Make validator chains narrow types for IDEs and PHPStan without the FluentAnalysis extension, driven entirely by the generated src/Mixins PHPDoc. - Annotate src/Validators with #[Assurance] / #[AssuranceSubject] declaring each rule's assured type. - Regenerate src/Mixins: Chain becomes generic (@template-covariant TSure); static entry methods narrow to Chain<concrete>; assert()/check() carry an unconditional @phpstan-assert TSure. Container rules (key/property/length/ max/min) and the concrete prefix forms (nullOrIntType, keyIntType, allIntType) narrow; argument-wrapping and compose forms stay Chain<mixed> so a raw (non-fluent) Validator argument is still accepted. - isValid() intentionally does not narrow: its only conditional form is a two-way guard, unsound for inexact rules on the false branch. - Add the tests/inference static-narrowing suite (no extension config) and run it in CI; scope the phpstan/phpcs accommodations for generated mixins.
1 parent 1b31207 commit b1b9c68

157 files changed

Lines changed: 3972 additions & 163 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-code.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727

2828
- run: composer phpunit
2929
- run: composer pest
30+
- run: composer inference
3031

3132
code-coverage:
3233
name: Code Coverage

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"type": "library",
66
"homepage": "http://respect.github.io/",
77
"license": "MIT",
8+
"minimum-stability": "dev",
9+
"prefer-stable": true,
810
"authors": [
911
{
1012
"name": "Respect/Validation Contributors",
@@ -23,7 +25,7 @@
2325
"php": ">=8.5",
2426
"psr/container": "^2.0",
2527
"respect/config": "^3.0",
26-
"respect/fluent": "^2.0",
28+
"respect/fluent": "3.0.x-dev",
2729
"respect/parameter": "^3.0",
2830
"respect/string-formatter": "^1.7",
2931
"respect/stringifier": "^3.0",
@@ -45,7 +47,7 @@
4547
"psr/http-message": "^1.0 || ^2.0",
4648
"ramsey/uuid": "^4",
4749
"respect/coding-standard": "^5.0",
48-
"respect/fluentgen": "^2.0",
50+
"respect/fluentgen": "2.1.x-dev",
4951
"sebastian/diff": "^7.0",
5052
"sokil/php-isocodes": "^4.2.1",
5153
"sokil/php-isocodes-db-only": "^4.0",
@@ -87,6 +89,7 @@
8789
"phpcs": "vendor/bin/phpcs",
8890
"phpstan": "vendor/bin/phpstan analyze",
8991
"phpunit": "vendor/bin/phpunit --testsuite=unit",
92+
"inference": "vendor/bin/phpunit --testsuite=inference",
9093
"smoke-complete": "bin/console smoke-tests:check-complete",
9194
"spdx-lint": "bin/console lint:spdx",
9295
"qa": [
@@ -95,6 +98,7 @@
9598
"@phpstan",
9699
"@phpunit",
97100
"@pest",
101+
"@inference",
98102
"@docs",
99103
"@smoke-complete"
100104
]

composer.lock

Lines changed: 45 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpcs.xml.dist

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
<file>src-dev/</file>
1616
<file>tests/</file>
1717

18+
<!-- PHPStan type-inference fixtures: intentional global functions + assertType() calls. -->
19+
<exclude-pattern>tests/inference/assertions/*</exclude-pattern>
20+
1821
<rule ref="Respect" />
1922

2023
<!-- Exclusions -->
@@ -28,6 +31,21 @@
2831
<exclude-pattern>src/Mixins/</exclude-pattern>
2932
<exclude-pattern>tests/feature/</exclude-pattern>
3033
</rule>
34+
<!-- Generated mixin docblocks carry @template/@param/@return narrowing types whose
35+
formatting (annotation groups, fully-qualified class names, bare iterable item
36+
types, null ordering) is the generator's concern, not hand-maintained style. -->
37+
<rule ref="SlevomatCodingStandard.Commenting.DocCommentSpacing">
38+
<exclude-pattern>src/Mixins/</exclude-pattern>
39+
</rule>
40+
<rule ref="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName">
41+
<exclude-pattern>src/Mixins/</exclude-pattern>
42+
</rule>
43+
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification">
44+
<exclude-pattern>src/Mixins/</exclude-pattern>
45+
</rule>
46+
<rule ref="SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition">
47+
<exclude-pattern>src/Mixins/</exclude-pattern>
48+
</rule>
3149
<rule ref="SlevomatCodingStandard.Functions.StaticClosure.ClosureNotStatic">
3250
<exclude-pattern>tests/Pest.php</exclude-pattern>
3351
<exclude-pattern>tests/feature</exclude-pattern>

phpstan.neon.dist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ parameters:
2323
path: tests/src/Message/TestingHandler.php
2424
- message: '/Property Respect\\Validation\\Test\\Stubs\\.+::\$[a-zA-Z]+ is never read, only written./'
2525
path: tests/src/Stubs
26+
-
27+
# Why: src/Mixins/* are generated typing aids. Their @return Chain<...> carry the
28+
# narrowing types derived from #[Assurance] (the same ones FluentAnalysis computes
29+
# at analysis time), e.g. bare `array` or `ArrayAccess`. Fully parameterising those
30+
# generics would only clutter the IDE-facing types without adding narrowing value.
31+
identifier: missingType.iterableValue
32+
path: src/Mixins/*
33+
-
34+
identifier: missingType.generics
35+
path: src/Mixins/*
2636
level: 8
2737
treatPhpDocTypesAsCertain: false
2838
paths:

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
<testsuite name="feature">
1515
<directory suffix="Test.php">./tests/feature</directory>
1616
</testsuite>
17+
<testsuite name="inference">
18+
<directory suffix="Test.php">tests/inference/</directory>
19+
</testsuite>
1720
</testsuites>
1821
<source>
1922
<include>

src-dev/Commands/LintMixinCommand.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Respect\FluentGen\Fluent\MethodBuilder;
1919
use Respect\FluentGen\Fluent\MixinGenerator;
2020
use Respect\FluentGen\Fluent\PrefixConstantsGenerator;
21+
use Respect\FluentGen\Fluent\TerminalMethod;
2122
use Respect\FluentGen\NamespaceScanner;
2223
use Respect\Validation\Mixins\Chain;
2324
use Respect\Validation\Validator;
@@ -82,13 +83,37 @@ interfaces: [
8283
suffix: 'Builder',
8384
returnType: Chain::class,
8485
static: true,
86+
emitNarrowing: true,
8587
),
8688
new InterfaceConfig(
8789
suffix: 'Chain',
8890
returnType: Chain::class,
8991
rootExtends: [Validator::class],
9092
rootComment: '@mixin ValidatorBuilder',
9193
rootUses: [ValidatorBuilder::class],
94+
emitNarrowing: true,
95+
templateParam: 'TSure',
96+
terminalMethods: [
97+
new TerminalMethod(
98+
name: 'assert',
99+
returnType: 'void',
100+
parameters: ['input' => 'mixed'],
101+
comments: ['@phpstan-assert TSure $input', '@psalm-assert TSure $input'],
102+
optionalParameters: ['template' => 'mixed'],
103+
),
104+
new TerminalMethod(
105+
name: 'check',
106+
returnType: 'void',
107+
parameters: ['input' => 'mixed'],
108+
comments: ['@phpstan-assert TSure $input', '@psalm-assert TSure $input'],
109+
optionalParameters: ['template' => 'mixed'],
110+
),
111+
new TerminalMethod(
112+
name: 'isValid',
113+
returnType: 'bool',
114+
parameters: ['input' => 'mixed'],
115+
),
116+
],
92117
),
93118
],
94119
);

0 commit comments

Comments
 (0)