Skip to content

Add IDE type narrowing to generated validator mixins#1802

Draft
alganet wants to merge 1 commit into
mainfrom
ide-narrowing
Draft

Add IDE type narrowing to generated validator mixins#1802
alganet wants to merge 1 commit into
mainfrom
ide-narrowing

Conversation

@alganet

@alganet alganet commented Jun 26, 2026

Copy link
Copy Markdown
Member

Part of a PR group: Fluent - FluentGen - Validation


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; 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 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.

This PR group adds advanced static (no PHPStan extension needed) type support and narrowing for fluent chains, pioneering it for Validation (StringFormatter should also be compatible with this approach)

image

In the example above, the devsense extension for VSCode is correctly infering an int[] (iterable int) from a each(int()) fluent validation chain.

More examples in tests/inference/assertions/static-narrowing.php.


Notes:

  • Most of the big diff is generated mixins, the PR is actually small.
  • This work is related, but distinct from Integrate Fluent, FluentGen and FluentAnalysis #1730. FluentAnalysis offers dynamic type narrowing with much richer narrowings (but IDEs can't run the extension). This one offers basic, static narrowing for IDEs such as devsense (tested) and IntelliJ (untested).

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.12%. Comparing base (1b31207) to head (3ec0d33).

Additional details and impacted files
@@            Coverage Diff            @@
##               main    #1802   +/-   ##
=========================================
  Coverage     97.12%   97.12%           
  Complexity     1067     1067           
=========================================
  Files           198      198           
  Lines          2501     2501           
=========================================
  Hits           2429     2429           
  Misses           72       72           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alganet

alganet commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@henriquemoody these are in draft because of the branch-alias (we need to amend the real composer versions when we do the release dance).

However, although not packagist-ready yet, they are open for review of the code. I requested review only here to avoid spam but you're free to comment in any of the 3.

@henriquemoody henriquemoody left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is trully trully awesome!!

Comment thread src/Validators/TrueVal.php
Comment thread src/Validators/Undef.php Outdated
Comment thread src/Validators/BoolVal.php
'For comparison with {{now|raw}}, {{subject}} must not be a datetime in the format {{sample|raw}}',
self::TEMPLATE_WRONG_FORMAT,
)]
#[Assurance(type: ['string', DateTimeInterface::class])]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
#[Assurance(type: ['string', DateTimeInterface::class])]
#[Assurance(type: ['string', 'integer', DateTimeInterface::class])]

I think we also allow passing timestamps

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's not on the tests though, so we might want to tackle refining that later?

Comment thread src/Validators/FalseVal.php
'{{subject}} must be present',
'{{subject}} must not be present',
)]
#[Assurance(type: ['array', ArrayAccess::class])]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't we have any "array key exists" assurance? It would be quite important

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a great question.

For IDE support, no (I don't think it's possible just with mixin annotations).

For phpstan analyze, we can do that (but it's not fully annotated in KeyExists).

'{{subject}} must be present',
'{{subject}} must not be present',
)]
#[Assurance(type: 'object')]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't we have any assurance for "property exists"?

Comment thread src/Validators/ScalarVal.php Outdated
'{{subject}} must be a scalar',
'{{subject}} must not be a scalar',
)]
#[Assurance(type: 'int|float|bool|string', exact: true)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
#[Assurance(type: 'int|float|bool|string', exact: true)]
#[Assurance(type: 'scalar', exact: true)]

Or am I missing something?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For this PR, it's the same.

For FluentAnalysis, there are some things we don't do yet (like unpacking scalar into the list for multi-node hop narrowing).

So, in the future, we might have to pre-unpack all types in declaration instead of using the more elegant one, just to give FluentAnalysis more surface. Or we might want to implement the unpacking in FluentAnalysis.


#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Assurance(compose: AssuranceCompose::Intersect, exact: true)]
final readonly class ShortCircuit implements Validator

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Because of how this validator works, I'm not sure we should assure something here. What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The FluentAnalysis extension can do this one. I ported the #[Assurance blocks from that PR, and some of them have no outcome in this PR.

This is one of them.

Perhaps it will be better if I remove the ones that do not yield any significant narrowing for this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understood that it can, but should it? Because we short circuit, isn't it arguable if it will make all those assertions? I'm not sure, not saying we shouldn't, but I'm not sure

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For this PR, it's a null-op. It only matters for FluentAnalysis. So I'll remove it, and we discuss when I integrate it!

Comment thread src/Validators/Templated.php Outdated

#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Assurance(compose: AssuranceCompose::Intersect, composeRange: [1, 1], exact: true)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just want to say that it's incredibly awesome that you managed to make this work!

@alganet alganet Jun 26, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Don't get too excited, this has no deep effect in this PR.

I'm considering now dropping the Assurances that would only work on #1730 to avoid confusion, then adding them later.

@alganet alganet force-pushed the ide-narrowing branch 2 times, most recently from b1b9c68 to 07ccc7d Compare June 26, 2026 19:40
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.
@alganet

alganet commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

I have removed the #[Assurance attributes that are not relevant for this PR, keeping only the ones that actively contribute to the IDE-only static narrowing. The ones removed belong to the full FluentAnalysis phpstan-extension integration that doesn't really affect IDE behavior.

Hopefully, this would make clear what IDEs can do and what they cannot (the static-narrowing.php file is a good reference card though, and remains the same).

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