Skip to content

Infer literal types for word date format characters - #6838

Merged
staabm merged 4 commits into
phpstan:2.3.xfrom
westonruter:date-format-word-types
Oct 27, 2026
Merged

staabm merged 4 commits into
phpstan:2.3.xfrom
westonruter:date-format-word-types

Conversation

@westonruter

Copy link
Copy Markdown
Contributor

The a, A, S, D, l, M, and F format characters of date(), date_format(), and DateTimeInterface::format() now produce unions of their possible values, alongside the numeric single-character formats added in #721. PHP formats these characters from hard-coded English tables in ext/date/php_date.c, so the values do not depend on the locale.

Format Type
a 'am'|'pm'
A 'AM'|'PM'
S 'nd'|'rd'|'st'|'th'
D 'Fri'|'Mon'|'Sat'|'Sun'|'Thu'|'Tue'|'Wed'
l 'Friday'|'Monday'|'Saturday'|'Sunday'|'Thursday'|'Tuesday'|'Wednesday'
M 'Apr'|'Aug'|'Dec'|'Feb'|'Jan'|'Jul'|'Jun'|'Mar'|'May'|'Nov'|'Oct'|'Sep'
F 'April'|'August'|'December'|'February'|'January'|'July'|'June'|'March'|'May'|'November'|'October'|'September'

Removal of the non-empty-string widening

Adding the cases alone is not enough: getTypeFromFormatType() ended with

if ($type->isNumericString()->no() && $formatType->isNonEmptyString()->yes()) {
	$type = TypeCombinator::union($type, new IntersectionType([
		new StringType(), new AccessoryNonEmptyStringType(),
	]));
}

which widens any non-numeric constant result to non-empty-string, so the new unions came out as non-empty-string. The same happens today for formats without format characters, e.g. date(' ') is non-empty-string rather than ' '.

This block was added in #2888 for phpstan/phpstan#10468 (a non-empty format should give a non-empty result). That guarantee does not depend on it:

  • A non-constant format is handled by the count($types) === 0 branch, which already returns non-empty-string for a non-empty-string format.
  • For a constant non-empty format, every type buildReturnTypeFromFormat() returns is already non-empty: the explicit cases, ConstantStringType($date), numeric-string, and non-falsy-string.

So the block is removed rather than worked around. The bug-10468.php and bug-6613.php tests still pass.

Tests

  • nsrt/bug-15537.php is the reproducer from the issue. It fails before this change (non-falsy-string for every assertion) and passes after.
  • nsrt/date-format.php expected non-falsy-string for D; it now expects the weekday union.

Closes phpstan/phpstan#15537

🤖 Generated with Claude Code

The `a`, `A`, `S`, `D`, `l`, `M`, and `F` format characters of `date()`,
`date_format()`, and `DateTimeInterface::format()` now produce unions of
their possible values, like the numeric single-character formats already do.
PHP formats these characters from hard-coded English tables, so the values
do not depend on the locale.

The final `union()` with `non-empty-string` in `getTypeFromFormatType()` is
removed, since it would otherwise widen these literal unions (and any other
non-numeric constant result, such as `date(' ')`) to `non-empty-string`.
A non-empty format yielding a non-empty result is already guaranteed: a
non-constant format is handled by the `non-empty-string` branch above, and
every type built for a constant non-empty format is itself non-empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "S" date format returns English ordinal suffixes, and typos reports the
"nd" string literal as a misspelling of "and".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter
westonruter marked this pull request as ready for review October 26, 2026 22:22
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@staabm
staabm requested a review from VincentLanglet October 27, 2026 05:46
@staabm

staabm commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

//cc @SanderMuller

Comment thread src/Type/Php/DateFunctionReturnTypeHelper.php
Co-authored-by: Markus Staab <maggus.staab@googlemail.com>

@SanderMuller SanderMuller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me. I checked it on 6bf825f.

  • The seven unions match what PHP returns. I collected every date() and format() value for a A S D l M F, hourly over two years plus years 1 and 9999. The sets are the same under the C, fr_FR, de_DE and ja_JP locales, on PHP 7.4 and 8.5.
  • bug-15537.php and date-format.php fail with the base helper and pass with this one.
  • Removing the non-empty-string widening only makes types narrower. With the base helper, date(' ') and date($b ? ' ' : '-') give non-empty-string. With this branch they give ' ' and ' '|'-'. A non-empty non-constant format still gives non-empty-string, and Y, d, u and 0 give the same types as before.
  • make tests, make phpstan and phpcs pass. The class is not shadowed by Turbo.
  • nextras/orm also fails on #6861 with the same two Nette DI errors, so it is not from this PR.

Performance: I timed a synthetic file with 200 functions and 4000 date format calls, 10 interleaved rounds, user CPU medians. When the calls are only evaluated, the analysis takes 2.07 s instead of 2.02 s. When the results are concatenated, it takes 2.53 s instead of 2.24 s. Most of that is the concat of the new unions, which already happens today for small numeric unions such as date('L') . date('N'). A real-world codebase of about 35000 files has 62 calls with these format characters, so I expect no measurable difference in practice.

@staabm

staabm commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

I am not allowed to change the PR. @westonruter please pick the following commit onto the PR: staabm@613f7e6

-> found a way arround it

after that we are ready to go

@staabm
staabm merged commit b713fd2 into phpstan:2.3.x Oct 27, 2026
1029 of 1030 checks passed
@staabm

staabm commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

thank you!

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.

Infer literal types for the a, A, S, D, l, M, and F date format characters

5 participants