Skip to content

[BUGFIX] Do not generate fractor.php when a given config has no rules - #443

Open
CybotTM wants to merge 1 commit into
FriendsOfTYPO3:mainfrom
CybotTM:fix/442-no-rules-config
Open

CybotTM wants to merge 1 commit into
FriendsOfTYPO3:mainfrom
CybotTM:fix/442-no-rules-config

Conversation

@CybotTM

@CybotTM CybotTM commented Sep 26, 2026 •

Copy link
Copy Markdown

When a config registers no rules, fractor process no longer generates a fractor.php in the working directory if a config was given with --config, stdin is not a terminal, or --dry-run is set. It prints a warning instead. The interactive prompt stays for the one case left: no config at all, in a terminal, without --dry-run.

Fixes #442.

Changes

ConfigInitializer::createConfig() gets the two guards Rector's ConfigInitializer has, and one more. In order:

  1. A main config file that exists is named: Register rules or sets in your "<path>" config, with the path relative to the working directory (Path::makeRelative(); the full path if it is outside, or if getcwd() failed because the working directory was deleted). Nothing is created. Rector checks this after rector.php and rector.dist.php in the working directory and names the basename. Here it comes first and names the relative path, so --config build/fractor.php next to a fractor.php in the working directory names the file that was actually loaded.
  2. The existing check for a fractor.php in the working directory, unchanged.
  3. No terminal on stdin: No "fractor.php" config found. Create one, or pass "--config <path>". Nothing is prompted or written. Symfony does not check for a terminal: without input, QuestionHelper takes the default answer yes, so a pipe, /dev/null or CI wrote the file.
  4. --dry-run in a terminal: No "fractor.php" config found. Create one, or run without --dry-run to generate it. -n, the short option for --dry-run, is also read by Symfony's Application::configureIO() as --no-interaction, so in a terminal fractor process -n answered the prompt with the default yes and wrote the file without asking. Rector behaves the same and treats it as expected (-n (--dry-run) writes rector.php without asking when no config exists rectorphp/rector#9916), so here the PR deviates from Rector in behaviour, not only in wiring; the guard can be dropped if you prefer to follow Rector. This guard comes after the terminal guard, so without a terminal the advice is the one that works there. ProcessCommand passes the option in.

Rector's .dist guard has no counterpart, because FractorConfigsResolver knows only fractor.php.

Two pieces of wiring differ from Rector:

  • The main config file reaches ConfigInitializer as the container parameter main_config_file, set in ContainerBuilderBuilder::build() before the internal default config is filled in, and escaped so that a % in the path is not read as a placeholder. It is null when neither --config nor a fractor.php in the working directory exists. Rector injects RectorConfigsResolver and parses argv again instead. The parameter is the file the container was actually built from, and it keeps ConfigInitializer testable without setting $_SERVER['argv'].
  • The terminal check is a TerminalDetector service that takes the stream to check (STDIN by default), instead of Rector's static TerminalDetector::isInputTty(). Tests pass a pseudo terminal from /dev/ptmx or a memory stream, so both sides are tested regardless of the stdin of the test run.

The exit code stays 0 in all warning cases, as in Rector. A non-zero exit for "a given config registers no rules" would be a one-line change in ProcessCommand, if CI runs should fail on such a config. The warnings go to stdout, as the existing warning and the prompt did before; with --output-format json that is not JSON. This PR leaves that unchanged.

The PR does not reject configs by their return value. A config without return that calls FractorConfiguration::configure() is valid (ConfigurationFileWithoutCallbackTest). symfony/dependency-injection 7.4 and later map the 1 that include returns for such a file to null and also accept an array as a config format; earlier versions ignore any return value that is not callable. Rector does not reject such values either.

Known limits, unchanged by this PR and also on main: FractorConfigsResolver does not find -c inside grouped short options such as -vc, it names a symlinked config by its target (realpath()), and the paths parameter is not escaped against %.

Testing

Local runs, PHP 8.5.10, highest dependency versions, unless stated otherwise. In a review run on PHP 8.2.33 with the lowest dependency versions (php:8.2-cli, symfony/console 6.4.25, symfony/dependency-injection 6.4.0, symfony/filesystem 6.4.24), composer test:php passed with 344 tests and no skips, and PHPStan and Rector passed. CI has not run yet.

  • ConfigInitializerTest, 14 cases:
    • a given config is named by its relative path: without a terminal, in a terminal, with another fractor.php in the working directory, and with --dry-run; outside the working directory, and with an empty working directory, by its full path
    • a given config that no longer exists falls through to the non-interactive warning
    • an existing fractor.php in the working directory is named, without a main config and with --dry-run, and stays unchanged
    • without a terminal: warning, no prompt; with --dry-run as well, the same warning
    • --dry-run in a terminal: warning, no prompt
    • in a terminal the prompt appears, answer no writes nothing, answer yes writes fractor.php
      The cases that must not write assert that no fractor.php exists. The terminal cases skip themselves where /dev/ptmx gives no terminal.
  • TerminalDetectorTest: the injected stream is checked, a closed stream is no terminal, and without a stream STDIN is checked, in a child process with a pseudo terminal and with a pipe as its stdin.
  • ProcessCommandWithoutRulesTest: ProcessCommand passes --dry-run on (dry-run warning, no prompt) and not without it (prompt).
  • ConfigInitializerWiringTest: the container passes the config file on. It reads the private property, because the service's output is silenced in tests and without a terminal both remaining paths write nothing.
  • ContainerBuilderBuilderTest: the parameter is the given config, stays literal in directories named %placeholder% and %%, and is null without a config.
  • composer test:php: OK, 344 tests; ConfigInitializerTest also with a 146-character TMPDIR, where the warning wraps inside the path. style:php:check, analyze:php, rector, style:composer:normalize: exit 0. e2e/run-test.sh: exit 0, 6 cases.
  • The real binary:
    • --config build/cfg.php < /dev/null, with a config returning 42, names build/cfg.php and exits 0 without writing a file.
    • process -n < /dev/null gives the --config advice; process -n in a terminal (via script) gives the dry-run advice. Neither writes a file.
    • In a directory named p%q%r, a config with a rule finds its change, and a config without rules is named.
  • 29 mutations against the full PHPUnit suite (stdin /dev/null, as in CI), plus an unmutated control run that passes. All 29 are caught, among them: each guard removed, the guards reordered in five ways, no return after a warning, note() instead of warning(), the basename or the full path instead of the relative path, the relative path without the check for an absolute working directory, a missing space in a message after or between quoted parts, the prompt replaced by yes, the detector ignoring the injected stream or losing its STDIN fallback or its is_resource() check, the parameter not escaped, set after the default or not set, the wiring passing null, and ProcessCommand passing --dry-run never or always.

Assisted by claude-code:claude-opus-5-5 — Session

@CybotTM
CybotTM force-pushed the fix/442-no-rules-config branch 5 times, most recently from e5c1740 to d0258a3 Compare September 27, 2026 00:12
@CybotTM
CybotTM marked this pull request as ready for review September 27, 2026 00:22
@CybotTM
CybotTM marked this pull request as draft September 27, 2026 06:55
When no rules were loaded, "fractor process" asked whether to generate
a fractor.php in the working directory. It asked even when a config was
passed with --config and only registers no rules, and without a
terminal on stdin Symfony took the default answer "yes" and wrote the
file, also with --dry-run.

ConfigInitializer::createConfig() now has the two guards Rector's
ConfigInitializer has, and one more, in this order:

- a main config file that exists is named in a warning, by its path
  relative to the working directory, and no config is created; this is
  checked before a fractor.php in the working directory, so --config
  pointing elsewhere names the loaded file
- the existing check for a fractor.php in the working directory
- without a terminal on stdin, a warning says to create a config or to
  pass --config, and nothing is prompted or written
- in a terminal with --dry-run, a warning says to create a config or to
  run without --dry-run; its short option -n also makes Symfony answer
  every question with the default, so the terminal was no protection -
  Rector uses -n for --dry-run as well and shares this exposure

The exit code stays 0 in these cases, as in Rector.

The main config file reaches ConfigInitializer as the container
parameter main_config_file, escaped against "%" placeholders and set
before the internal default config is filled in, so it is null when
neither --config nor a fractor.php in the working directory exists. The
terminal check is a TerminalDetector service that takes the stream to
check, so tests can pass a pseudo terminal or a memory stream.

Fixes FriendsOfTYPO3#442

Assisted-by: claude-code:claude-opus-5-5
Agent-Session: https://claude.ai/code/session_016aBEJdgVbFutUUNEwFEMQK
Agent-Host: 0493f0
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
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.

[BUG] a config that registers no rules makes process generate fractor.php, even with --config and without a terminal

1 participant