skip to content

In PHP CS Fixer, what makes a rule risky, and how should a team enable risky rules with --allow-risky or setRiskyAllowed()?

level: seniorimportance: should knowfreq 35%

answer

  1. may change runtime behaviour
  2. off unless explicitly allowed
  3. strict_param adds true to in_array
  4. configured but disallowed: exit 16
  5. review risky diffs on their own

basics

~10 s

A risky PHP CS Fixer rule is one whose rewrite can change how the code behaves, such as strict_param or declare_strict_types; they run only when the config calls setRiskyAllowed(true) or the command passes --allow-risky=yes.

solid answer

~40 s

Most rules only move whitespace or swap equivalent syntax. A **risky** rule can change behaviour: `strict_param` adds `true` to `in_array()` and `array_search()`, so `in_array('1', [1])` stops matching; `strict_comparison` turns `==` into `===`; `declare_strict_types` adds `declare(strict_types=1)`, turning silent coercions into `TypeError`s; `native_function_invocation` breaks code that shadows a function in a namespace. Risky rules are off by default. The config enables them with `setRiskyAllowed(true)`, or a run passes `--allow-risky=yes`; `--allow-risky=no` overrides a config that allows them. Configuring a risky rule or a `:risky` set without allowing it fails with a configuration error, exit code 16. Teams usually enable risky sets separately from formatting, run the tests, and review the diff as a behaviour change.

code

php · 13 lines
php
<?php

declare(strict_types=1);

return (new PhpCsFixer\Config())
    ->setRiskyAllowed(true)
    ->setRules([
        '@PER-CS' => true,
        '@PER-CS:risky' => true,
        'strict_param' => true,
        'declare_strict_types' => true,
    ])
    ->setFinder((new PhpCsFixer\Finder())->in(__DIR__ . '/src'));

go deeper

for a junior

Recall that risky rules can change behaviour, are off by default, and are enabled with setRiskyAllowed(true) or --allow-risky=yes.

for a middle

Explain concretely why strict_param, strict_comparison and declare_strict_types can change results, and what happens when a risky rule is configured but disallowed.

for a senior

Show a safe rollout: separate commits, one risky set at a time, diff review, full tests and static analysis before merging.

for a principal

Weigh the value of each risky rule against the review and test cost for the codebase, and decide which ones the team adopts permanently.

## What risky means Every PHP CS Fixer rule declares whether it is **risky**. The documentation defines it simply: *a rule is considered risky if it could change code behaviour*. The rewrite is correct for most code, but there are realistic programs where it changes the result, and the tool cannot prove your code is not one of them. Non-risky rules move whitespace, reorder imports, or swap syntax that PHP treats identically (`array()` to `[]`). Risky rules touch **semantics**. ## Examples and why each is risky | Rule | What it rewrites | Why it can change behaviour | |---|---|---| | `strict_param` | adds `true` as the strict flag to `in_array`, `array_search`, `array_keys`, `base64_decode`, `mb_detect_encoding` | loose matches such as `in_array('1', [1])` stop matching | | `strict_comparison` | `==` to `===`, `!=` to `!==` | comparisons that relied on type juggling flip | | `declare_strict_types` | adds `declare(strict_types=1);` | scalar arguments that were silently coerced now throw `TypeError` | | `native_function_invocation` | `strlen()` to `\strlen()` | a namespaced function that shadows a global one stops being called | | `no_alias_functions` | alias functions to their master names | risky when an alias has been overridden | | `modern_serialization_methods` | `__sleep`/`__wakeup` to `__serialize`/`__unserialize` | code calling the old methods directly, or relying on their logic, changes | | `final_class` | adds `final` to classes | any existing subclass stops compiling | Rule sets follow the same convention: behaviour-changing rules live in a `:risky` companion set such as `@PER-CS:risky`, `@PHP8x5Migration:risky` (which adds `modern_serialization_methods`) or `@PhpCsFixer:risky`. ## How risky rules are enabled Risky rules are **off by default**. There are two switches: - in the config: `->setRiskyAllowed(true)`; - on the command line: `--allow-risky=yes`, or `--allow-risky=no` to override a config that allows them. If the configured rules contain a risky rule, directly or through a `:risky` set, and risky rules are not allowed, PHP CS Fixer does **not** silently skip them. It stops with *"The rules contain risky fixers (...), but they are not allowed to run. Perhaps you forget to use --allow-risky=yes option?"* and exits with code **16**, a configuration error. That is deliberate: a config that says "apply `strict_comparison`" should never quietly do nothing. ## Adopting risky rules safely 1. **Separate the change.** Commit formatting (non-risky) first and risky rules in a later, separate commit, so reviewers can read the risky diff as a behaviour change and `git bisect` can isolate it. 2. **Enable one rule or set at a time**, and read the `--diff` output rather than skimming it. 3. **Run the full test suite** after applying them. `strict_param` and `strict_comparison` failures often only show up in edge-case data. 4. **Use static analysis as a safety net.** Type errors introduced by `declare_strict_types` are exactly what a type checker finds before production does. 5. **Keep the permission.** Once the codebase is clean, leave `setRiskyAllowed(true)` in the committed config so `check` keeps enforcing the risky rules on new code. ## Finding out whether a rule is risky - `vendor/bin/php-cs-fixer describe strict_param` prints the rule's description and a **risky** warning with the reason. - Each rule's documentation page carries the same warning, and lists the sets that contain it. - Any set whose name ends in `:risky` should be treated as containing behaviour-changing rules. - A useful dry run is `vendor/bin/php-cs-fixer check --diff --allow-risky=yes` on a branch: it shows what the risky rules would change, without committing to the permission in the shared config. ## A common misunderstanding Risky does not mean *buggy* or *experimental*. The rules are well tested; the risk lies in **your** code's assumptions, such as loose comparisons you relied on, functions you shadowed, or classes someone extends. That is why the decision is left to the project rather than made by the tool.

  • What happens if the config enables @PHP8x5Migration:risky but setRiskyAllowed() is never called?
    PHP CS Fixer refuses to run. It reports that the rules contain risky fixers that are not allowed, names them, suggests `--allow-risky=yes`, and exits with 16, the configuration-error flag. It never silently drops the risky rules.
  • Why is declare_strict_types risky when strict types are considered good practice?
    With `declare(strict_types=1)` in a file, scalar arguments passed from that file are no longer coerced: passing `'5'` to an `int` parameter throws a `TypeError` instead of converting. Code that worked through coercion starts failing at runtime, so the rule needs tests, and ideally static analysis, behind it.

saying these in an interview costs you the question

  • Risky rules are experimental and may produce invalid PHP
  • Risky rules are skipped silently when not allowed
  • strict_param only changes formatting of function calls
  • --allow-risky=no cannot override a config that allows risky rules
  • It is fine to mix risky rewrites into a whitespace-only commit