Skip to content

Report non-zero integer literals passed directly to parameters with allowed constants - #6565

Open
phpstan-bot wants to merge 2 commits into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-9iikj0l
Open

phpstan-bot wants to merge 2 commits into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-9iikj0l

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Following up on the allowed-constants checks for flag parameters, this reports hardcoded integer literals that are passed directly (in the AST) to parameters that expect a constant, or a bitmask of constants. For example json_encode($x, 1), json_encode($x, 1 | 2), and the 2 in json_encode($x, $one | 2) are now reported.

Changes

  • src/Rules/FunctionCallParametersCheck.php: new findNonZeroIntegerLiterals() walks BitwiseOr trees and collects non-zero Scalar\Int_ leaves. The check reports each of them as Integer literal %s is not allowed for %s of … (identifier argument.integerLiteral, tip "Use constants instead.") when the parameter has getAllowedConstants() !== null. 0 is still allowed because it is the usual "no flags" value, which an existing test already expects.
  • New message parameter wired through every caller of the shared check: CallToFunctionParametersRule, CallMethodsRule, CallStaticMethodsRule, InstantiationRule, AttributesCheck, CallCallablesRule, and CallUserFuncRule.

Root cause

The allowed-constants check only looked at arguments made entirely of constant fetches (resolveConstantReflections()). An integer literal, or a | expression that mixed in literals or variables, returned null, so the argument wasn't checked at all.

Test

  • tests/PHPStan/Rules/Functions/data/bug-14727.php: the playground reproducer, plus cases for 0, a mix of constant and literal, named arguments, a single-value parameter (array_unique), and json_decode's $flags.
  • Added a literal case to each of the existing constant-parameter-check data files for methods, static methods, constructors, first-class callables and call_user_func(). They share the same code path and all report the literal.
  • Updated the expectations in testRoundModePhp84 and testNamedParametersForMultiVariantFunctions, which pass literal modes.

Fixes phpstan/phpstan#14727

🤖 Generated with Claude Code

@VincentLanglet

Copy link
Copy Markdown
Contributor

I have three thought about this @staabm

  1. This seems opinionated to me so should be behind an option/flag

  2. I don't have literal-string param in mind, but shouldn't it be the same ?

@staabm

staabm commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

this PR adds one more case to a rule which was implemented in #5256

  1. This seems opinionated to me so should be behind an option/flag

why do you think its opinionated?

  1. I don't have literal-string param in mind, but shouldn't it be the same ?

the rules in question are about bitmasks(int) and single-value ints.
all variants we support atm can be found in resources/constantToFunctionParameterMap.php

I think its all about ints atm

@VincentLanglet

Copy link
Copy Markdown
Contributor

this PR adds one more case to a rule which was implemented in #5256

  1. This seems opinionated to me so should be behind an option/flag

why do you think its opinionated?

That's not the same idea than #5256 to me.

The rule was about reporting

json_decode($json, true, JSON_THROW_ON_ERROR);

because JSON_THROW_ON_ERROR is meant to be used as a flag for the fourth parameter so the developer certainly made a mistake and wants

json_decode($json, true, flags: JSON_THROW_ON_ERROR);

But, if someone writes

json_decode($json, true, flags: 4194304);

this is a perfectly valid code which does the same ; that's why it could be a flag only enabled on strict-rules...

I agree that it's not recommended to use an integer rather than the constant and I dunno if there is usecase where people does. But, still, to me it's like a new rule (the issue is flagged as feature-request) and not a bugfix. So it will require ondrej review and I prefer it because I don't feel confident merging this by myself.

@staabm staabm 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.

the goal of this PR is not to report any literal value beeing passed into a callable.

instead we want new errors reported when a parameter is known to be bitmask of constants, and the effective value based on literal values beeing passed is not a valid value which can be constructed by the valid constants

@VincentLanglet

Copy link
Copy Markdown
Contributor

the goal of this PR is not to report any literal value beeing passed into a callable.

instead we want new errors reported when a parameter is known to be bitmask of constants, and the effective value based on literal values beeing passed is not a valid value which can be constructed by the valid constants

I don't think it's the initial request

Expected behaviour: When a parameter is known to be bitmask of constants, report any argument that is directly hardcoded literal in AST.

@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I changed the check as staabm asked: an integer literal is now reported only when its value can't be produced from the parameter's allowed constants. I pushed this as one commit, d48c9f2. make tests, make phpstan and make cs all pass.

How it works now (src/Rules/FunctionCallParametersCheck.php):

  • Combined value: the check ORs together all integer literals in the argument, including literals inside a | chain. Variables and constants are left out of the value, so $one | 8192 is checked as 8192.
  • Constant values: each allowed constant's value comes from $scope->getType(). Constants that aren't integers are skipped: enum cases like RoundingMode::HalfEven, and constants missing on the running PHP version.
  • Single-value parameters: the value must equal one of the allowed constants.
  • Bitmask parameters: the value must be buildable with |. It passes if OR-ing every allowed constant that fits entirely within its bits gives back exactly the value. This also handles constants that use several bits, like FILEINFO_MIME (1040).
  • Message: Integer %s does not correspond to constants allowed for %s of …, identifier argument.invalidIntegerLiteral. The "Use constants instead." tip is removed, because a valid literal like json_encode($x, 1) is no longer reported.
  • New getter: I added getConstants() to ParameterAllowedConstants, which is @api. Adding a method there doesn't break anything for extensions.

Behaviour changes to note in the PR thread:

  • Now accepted: json_encode($x, 1), 1 | 2, JSON_PRETTY_PRINT | 64, array_unique([], 2) and setFetchMode(5). They correspond to real constants, so they're no longer reported.
  • Now reported: json_encode($x, 4096), 1 | 4096 (reported as 4097), array_unique([], 3) and json_decode(..., 4).
  • 0 on single-value parameters: 0 is now reported when no allowed constant equals 0. So round($f, 0, 0) gets this error in addition to the existing type error on the same line.
  • Exclusive groups: they aren't enforced for literal values. For example, a literal that combines two mutually exclusive PDO::FETCH_* modes isn't flagged.

Tests: I rewrote bug-14727.php around valid and invalid values, including named arguments and a literal mixed with a variable. In the method, static method, constructor, callable and call_user_func() data files, the literal is now an invalid value. I removed the two setFetchMode() expectations and added the 0 case in testRoundModePhp84.

VincentLanglet's point is still open: whether this should sit behind an option or strict-rules, and whether Ondřej should review it. That's for the maintainers to decide.

phpstan-bot and others added 2 commits September 24, 2026 09:28
…llowed constants

- FunctionCallParametersCheck: for parameters that have an allowed-constants
  definition (single or bitmask), walk the argument's `|` tree and report every
  hardcoded non-zero `Scalar\Int_` leaf (identifier `argument.integerLiteral`,
  tip "Use constants instead."). `0` stays allowed as the "no flags" value;
  variables and other dynamic operands are ignored.
- Added the new message to all callers of the shared check: functions,
  methods, static methods, constructors, attributes, callables and
  call_user_func().
- Updated existing expectations for round() mode and
  PDOStatement::setFetchMode() which pass integer literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Instead of reporting every non-zero integer literal passed to a parameter
with allowed constants, resolve the effective value of the literals in the
argument (including literals combined with `|`) and report it only when it
does not match an allowed constant (single-value parameters) or cannot be
constructed by combining allowed constants (bitmask parameters).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@staabm
staabm force-pushed the create-pull-request/patch-9iikj0l branch from d48c9f2 to 4896818 Compare September 24, 2026 07:28
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.

Report directly hardcoded constant literals for flag params

3 participants