PHP-CS-Fixer 一元运算符空格规则(unary_operator_spaces)完全指南:配置、源码原理与规则集集成
PHP-CS-Fixer 一元运算符空格规则(unary_operator_spaces)完全指南:配置、源码原理与规则集集成
一元运算符(如 ++、--、!、~、&、...)与操作数之间不应存在多余空格,这是 PHP 编码规范中的常见检查项。本指南基于 PHP-CS-Fixer 仓库中 unary_operator_spaces 规则文档,系统讲解该规则的修复行为、only_dec_inc 配置项、在 PER / PSR-12 / Symfony 等规则集中的默认启用情况,并结合 UnaryOperatorSpacesFixer 源码 与 测试用例 深入剖析其底层实现原理。阅读完本文,你将能熟练配置该规则、理解其与相邻规则的执行顺序,并判断它在代码库中的实际影响范围。
规则概述:一元运算符应紧贴操作数
unary_operator_spaces 规则的核心语义只有一句话:Unary operators should be placed adjacent to their operands(一元运算符应与它们的操作数相邻)。所谓"一元运算符",指的是只作用于单个操作数的运算符,包括:
- 自增 / 自减运算符:
++、-- - 逻辑非:
! - 按位取反:
~ - 引用符号:
&(取地址、引用传递、函数返回引用) - 展开运算符:
...(参数展开、可变参数声明) - 一元正负号:
+、- - 错误抑制符:
@
该规则会自动移除运算符与操作数之间多余的空格,使 $sample ++; 变为 $sample++;,! ! $a 变为 !!$a。
在 Fixer 源码 中,规则的定义位于 getDefinition() 方法(src/Fixer/Operator/UnaryOperatorSpacesFixer.php#L49-L73),同时它是实现 ConfigurableFixerInterface 的可配置规则,并使用了 ConfigurableFixerTrait 提供配置解析能力(src/Fixer/Operator/UnaryOperatorSpacesFixer.php#L44-L47)。
配置项 only_dec_inc
该规则只有一个配置项:
| 配置项 | 说明 | 允许类型 | 默认值 |
|---|---|---|---|
only_dec_inc |
仅限自增(increment)和自减(decrement)运算符 | bool |
false |
配置项在 createConfigurationDefinition() 方法 中通过 FixerOptionBuilder 声明:
(new FixerOptionBuilder('only_dec_inc', 'Limit to increment and decrement operators.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption()
两种配置模式的行为差异
only_dec_inc => false(默认):规则作用于所有一元运算符,包括!、~、&、...、@、一元正负号等;only_dec_inc => true:规则仅处理++与--,其他一元运算符(如展开运算符...)即使存在多余空格也不会被修复。
这一差异在 applyFix() 实现 中体现得非常直接:遍历 token 时,若配置为 true 且当前 token 不是 T_DEC / T_INC,则直接跳过:
if (true === $this->configuration['only_dec_inc'] && !$tokens[$index]->isGivenKind([\T_DEC, \T_INC])) {
continue;
}
修复行为示例
文档提供了三个官方示例,下面逐一解读。
示例一:默认配置(全部一元运算符)
--- Original
+++ New
<?php
-$sample ++;
---- $sample;
-$sample = ! ! $a;
-$sample = ~ $c;
-function & foo(){}
+$sample++;
+--$sample;
+$sample = !!$a;
+$sample = ~$c;
+function &foo(){}
在默认配置下,++、--、!、~、& 前后多余的空格都会被清除,包括函数返回引用 function & foo(){} 这类写法也会被规范为 function &foo(){}。
示例二:['only_dec_inc' => false]
--- Original
+++ New
<?php
-function foo($a, ... $b) { return (-- $a) * ($b ++);}
+function foo($a, ...$b) { return (--$a) * ($b++);}
注意此处显式配置 false 与默认行为等价:不仅修掉了 -- 与 $a 之间的空格、$b 与 ++ 之间的空格,还把参数展开符 ... 与 $b 之间的空格一并移除(... $b → ...$b)。
示例三:['only_dec_inc' => true]
--- Original
+++ New
<?php
-function foo($a, ... $b) { return (-- $a) * ($b ++);}
+function foo($a, ... $b) { return (--$a) * ($b++);}
与示例二对比即可看出 only_dec_inc => true 的核心差异:$a, ... $b 中的空格保留不变(因为 ... 不在处理范围内),而 (-- $a) 和 ($b ++) 被修复为 (--$a) 与 ($b++)。
源码级原理剖析
判断一元运算符的两种角色
实现的关键在于 TokensAnalyzer 提供的两个判定方法:
isUnarySuccessorOperator()(后继一元运算符,即写在操作数之后,如$a++):见 src/Tokenizer/TokensAnalyzer.php#L484-L503。仅当 token 是T_INC/T_DEC,且前一个有意义 token 是]、T_STRING、T_VARIABLE或各类括号闭合符时才返回true;isUnaryPredecessorOperator()(前驱一元运算符,即写在操作数之前,如--$a、!$a、~$c):见 src/Tokenizer/TokensAnalyzer.php#L508-L575。其中!、~、@、T_ELLIPSIS(展开符)永远被判定为前驱一元运算符;而+、-、&、T_RETURN_REF这类"既可能是一元也可能是二元"的运算符,则需要结合前一个 token 的类型来判断——如果前一个 token 是变量、字面量、闭括号等"操作数类" token,则说明当前是二元运算符,否则判为一元运算符。
修复的遍历逻辑
applyFix() 从 token 流的末尾向前遍历(src/Fixer/Operator/UnaryOperatorSpacesFixer.php#L100-L123):
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
if (true === $this->configuration['only_dec_inc'] && !$tokens[$index]->isGivenKind([\T_DEC, \T_INC])) {
continue;
}
if ($tokensAnalyzer->isUnarySuccessorOperator($index)) {
if (!$tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
$tokens->removeLeadingWhitespace($index);
}
continue;
}
if ($tokensAnalyzer->isUnaryPredecessorOperator($index)) {
$tokens->removeTrailingWhitespace($index);
continue;
}
}
具体规则:
- 对于后继一元运算符(如
$a ++中的++),调用removeLeadingWhitespace()移除其前面的空格;但有一个例外:如果该运算符的前一个非空白 token 是注释,则保留空白,避免破坏注释与代码的视觉分隔; - 对于前驱一元运算符(如
-- $a中的--),调用removeTrailingWhitespace()移除其后面的空格。
候选判定与执行优先级
isCandidate()恒返回true(src/Fixer/Operator/UnaryOperatorSpacesFixer.php#L85-L88),意味着任何 PHP 文件都会被纳入检查,规则几乎总是生效;getPriority()返回0,且文档与源码注释明确要求:必须运行在NotOperatorWithSpaceFixer与NotOperatorWithSuccessorSpaceFixer之前(src/Fixer/Operator/UnaryOperatorSpacesFixer.php#L75-L83)。这是为了确保! $a这类写法先被本规则压缩为!$a后,后续的!相关空格规则能基于统一状态继续处理,避免规则间冲突。
测试用例:向后兼容性的保障
规则文档明确说明:"The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise."(测试类定义了官方支持的行为,每个测试用例都是向后兼容承诺的一部分。)
UnaryOperatorSpacesFixerTest 通过数据提供器 provideFixCases() 覆盖了大量边界场景(tests/Fixer/Operator/UnaryOperatorSpacesFixerTest.php#L53-L173),其中包括:
- 基础修复:
$a ++→$a++、++ $a→++$a、$a --→$a--、-- $a→--$a; - 逻辑非与按位取反:
! $b→!$b、! ! $b→!!$b、~ $b→~$b; - 引用与返回引用:
& $b→&$b、function & foo(){}→function &foo(){}、function foo(& $a, array & $b, Bar & $c)→function foo(&$a, array &$b, Bar &$c); - 一元正负号与二元运算符的区分:
$a * - $b→$a * -$b(-被正确识别为一元运算符),而$a *-$b不会破坏*的二元语义; - 展开运算符:
... $b→...$b、foo(... $a)→foo(...$a)、& ... $a→&...$a; - 配置差异验证:同一段
(-- $a) * ($b ++)在only_dec_inc => false时... $b一并被修复,在only_dec_inc => true时... $b保持原样(tests/Fixer/Operator/UnaryOperatorSpacesFixerTest.php#L158-L168); - 无变更样例:
$a= 1;$a#\n++#\n;#、$a=&$b;、$a*-$b;、static fn(Token $t): bool => 8 === ($t->flags & 8);等输入在规则处理后保持原样。
这些测试不仅是验证手段,更定义了规则在处理"既可能一元也可能二元"的运算符(如 +、-、&)时不会被误伤的边界行为。
所属规则集与默认启用情况
unary_operator_spaces 被多个官方规则集收录,这意味着在常见配置下无需显式声明即可生效:
| 规则集 | 配置 | 备注 |
|---|---|---|
@PER |
['only_dec_inc' => true] |
已废弃(deprecated) |
@PER-CS |
['only_dec_inc' => true] |
当前推荐 |
@PER-CS1.0 |
['only_dec_inc' => true] |
已废弃 |
@PER-CS1x0 |
['only_dec_inc' => true] |
当前推荐 |
@PER-CS2.0 |
['only_dec_inc' => true] |
已废弃 |
@PER-CS2x0 |
['only_dec_inc' => true] |
当前推荐 |
@PER-CS3.0 |
['only_dec_inc' => true] |
已废弃 |
@PER-CS3x0 |
['only_dec_inc' => true] |
当前推荐 |
@PhpCsFixer |
默认配置(true) |
无额外参数 |
@PSR12 |
<a href="https://link.gitcode.com/i/c4e510b6260ea6e0169dbb1bdc8fe9bf" target="_blank">'only_dec_inc' => true] |
见 [PSR12Set 源码 |
@Symfony |
默认配置(true) |
见 SymfonySet 源码 |
注意一个重要细节:在 @PER、@PER-CS、@PSR12 系列规则集中,该规则被配置为 only_dec_inc => true,即只处理 ++ 与 --;而在 @PhpCsFixer、@Symfony 规则集中则使用默认配置(false),作用于全部一元运算符。这一点在 PSR12Set 源码 中可以明确看到 'unary_operator_spaces' => ['only_dec_inc' => true] 的写法。因此,当你在 PER 系规则集下期望 ! ! $a 被压缩为 !!$a 时,需要显式覆盖该规则的配置。
各个规则集的完整定义可查阅 doc/ruleSets 目录下的对应文档(如 PER-CS.rst、PSR12.rst、Symfony.rst)。
配置与使用方式
在项目的 .php-cs-fixer.php 配置文件中,你可以按以下方式启用并自定义该规则:
<?php
$finder = PhpCsFixer\Finder::create()->in(__DIR__);
return (new PhpCsFixer\Config())
->setRules([
// 使用默认配置:处理所有一元运算符
'unary_operator_spaces' => true,
// 或限定仅处理自增/自减:
// 'unary_operator_spaces' => ['only_dec_inc' => true],
])
->setFinder($finder);
然后通过命令行执行修复或检查:
# 仅检查不修改
php php-cs-fixer fix --dry-run --diff
# 实际修复
php php-cs-fixer fix
总结
unary_operator_spaces 是一个低风险、高覆盖的代码风格规则,它确保一元运算符与操作数之间不留空格,从而提升代码的可读性与一致性。其核心要点可归纳为:
- 默认处理所有一元运算符,包括
++、--、!、~、&、...、@及一元正负号; only_dec_inc可将作用范围收窄到自增/自减,PER 系与 PSR-12 规则集正是采用这一收紧配置;- 底层通过
TokensAnalyzer的isUnarySuccessorOperator/isUnaryPredecessorOperator区分一元与二元语境,保证$a * -$b这类"疑似二元"的表达式不会被误伤; - 执行顺序上有明确约束(优先于
NotOperatorWithSpaceFixer等规则),规则间不会相互覆盖。
对于以 PER-CS 或 PSR-12 为基线的项目,建议保持规则集默认的 only_dec_inc => true 行为;若追求更严格的空格统一(包括 !、~、&、...),则按上文示例覆盖配置即可。