@@ -28,27 +28,43 @@ url="https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v${VERSION}/
2828phar_path=" $( download_tool php-cs-fixer " ${VERSION} " " ${url} " " ${asset_name} " " ${CHECKSUMS} " ) "
2929
3030# php-cs-fixer requires `--config` whenever multiple positional paths are
31- # passed and a config file exists. Auto-discover .php-cs-fixer.dist.php /
32- # .php-cs-fixer.php in the cwd and pass `--config` explicitly so prek's
33- # multi-file invocations don't error with "For multiple paths config parameter
34- # is required."
31+ # passed and a config file exists. Each file's config is auto-discovered by
32+ # walking from the file's directory up to the repo root searching for
33+ # .php-cs-fixer.dist.php / .php-cs-fixer.php. The cwd-only fallback below
34+ # applies when no input paths are passed or the caller passes an explicit
35+ # --config on the command line.
36+ #
37+ # Polyrepos that nest a PHP package inside a larger monorepo (e.g. consumer
38+ # `.php-cs-fixer.dist.php` lives at `packages/php/.php-cs-fixer.dist.php`)
39+ # break under a cwd-only search because pre-commit / prek runs the hook from
40+ # the repo root, the file gets formatted with `@PER-CS` defaults instead of
41+ # the consumer's PSR-12-derived ruleset, and the consumer's PSR-12 multi-line
42+ # class style is collapsed to PER-CS single-line — invalidating any embedded
43+ # generator hash and ping-ponging with regeneration tools that re-emit the
44+ # multi-line form.
3545extra_args=()
36- has_config=0
37- if [[ ! " $* " =~ " --config" && ! " $* " =~ " --config=" ]]; then
38- for cfg in .php-cs-fixer.dist.php .php-cs-fixer.php; do
39- if [[ -f " ${cfg} " ]]; then
40- extra_args+=(" --config=${cfg} " )
41- has_config=1
42- break
43- fi
44- done
45- else
46- has_config=1
47- fi
48- if (( has_config == 0 )) && [[ ! " $* " =~ " --rules" && ! " $* " =~ " --rules=" ]]; then
49- extra_args+=(" --rules=@PER-CS" )
46+ has_explicit_config=0
47+ if [[ " $* " =~ " --config" || " $* " =~ " --config=" ]]; then
48+ has_explicit_config=1
5049fi
5150
51+ # Walk up from a starting directory looking for the nearest config file.
52+ # Emits the absolute config path on stdout (empty string when none found).
53+ find_php_cs_fixer_config () {
54+ local start_dir=" $1 "
55+ local current=" ${start_dir} "
56+ while [[ " ${current} " != " /" && " ${current} " != " ." && -n " ${current} " ]]; do
57+ for cfg in .php-cs-fixer.dist.php .php-cs-fixer.php; do
58+ if [[ -f " ${current} /${cfg} " ]]; then
59+ printf ' %s\n' " ${current} /${cfg} "
60+ return 0
61+ fi
62+ done
63+ current=" $( dirname " ${current} " ) "
64+ done
65+ return 1
66+ }
67+
5268if [[ " ${1:- } " == " fix" ]]; then
5369 shift
5470fi
@@ -77,12 +93,64 @@ for arg in "$@"; do
7793 esac
7894done
7995
80- if (( has_config == 0 && ${# paths[@]} > 1 )) ; then
81- status=0
82- for path in " ${paths[@]} " ; do
83- php " ${phar_path} " fix " ${extra_args[@]} " " ${options[@]} " " ${path} " || status=$?
96+ # When no paths are passed and no explicit --config, fall back to the legacy
97+ # cwd-search behaviour so callers that rely on cwd discovery aren't broken.
98+ if (( has_explicit_config == 0 && ${# paths[@]} == 0 )) ; then
99+ for cfg in .php-cs-fixer.dist.php .php-cs-fixer.php; do
100+ if [[ -f " ${cfg} " ]]; then
101+ extra_args+=(" --config=${cfg} " )
102+ has_explicit_config=1
103+ break
104+ fi
84105 done
85- exit " ${status} "
106+ if (( has_explicit_config == 0 )) ; then
107+ extra_args+=(" --rules=@PER-CS" )
108+ fi
109+ exec php " ${phar_path} " fix " ${extra_args[@]} " " ${options[@]} "
110+ fi
111+
112+ # Group paths by their nearest discovered config (or no-config bucket) so each
113+ # php-cs-fixer invocation runs against the right ruleset.
114+ declare -A paths_by_config=()
115+ no_config_paths=()
116+ for path in " ${paths[@]} " ; do
117+ if (( has_explicit_config == 1 )) ; then
118+ # Caller passed --config explicitly — honour it for all paths.
119+ no_config_paths+=(" ${path} " )
120+ continue
121+ fi
122+ start_dir=" ${path} "
123+ if [[ -f " ${path} " ]]; then
124+ start_dir=" $( dirname " ${path} " ) "
125+ fi
126+ start_dir=" $( cd " ${start_dir} " 2> /dev/null && pwd || printf ' %s\n' " ${start_dir} " ) "
127+ if cfg_path=" $( find_php_cs_fixer_config " ${start_dir} " ) " ; then
128+ paths_by_config[" ${cfg_path} " ]+=" ${path} " $' \n '
129+ else
130+ no_config_paths+=(" ${path} " )
131+ fi
132+ done
133+
134+ status=0
135+ for cfg_path in " ${! paths_by_config[@]} " ; do
136+ # shellcheck disable=SC2206
137+ bucket_paths=(${paths_by_config["${cfg_path}"]} )
138+ php " ${phar_path} " fix --config=" ${cfg_path} " " ${options[@]} " " ${bucket_paths[@]} " || status=$?
139+ done
140+
141+ if (( ${# no_config_paths[@]} > 0 )) ; then
142+ if (( has_explicit_config == 0 )) ; then
143+ extra_args+=(" --rules=@PER-CS" )
144+ fi
145+ if (( ${# no_config_paths[@]} > 1 && has_explicit_config == 0 )) ; then
146+ # No discovered config + multiple paths: invoke per-file to avoid
147+ # php-cs-fixer's "For multiple paths config parameter is required" error.
148+ for path in " ${no_config_paths[@]} " ; do
149+ php " ${phar_path} " fix " ${extra_args[@]} " " ${options[@]} " " ${path} " || status=$?
150+ done
151+ else
152+ php " ${phar_path} " fix " ${extra_args[@]} " " ${options[@]} " " ${no_config_paths[@]} " || status=$?
153+ fi
86154fi
87155
88- exec php " ${phar_path} " fix " ${extra_args[@]} " " ${options[@]} " " ${paths[@] }"
156+ exit " ${status } "
0 commit comments