PHP-CS-Fixer 一元运算符空格规则(unary_operator_spaces)完全指南:配置、源码原理与规则集集成

原创2026-09-22 15:10:271,666 阅读
文章标签:开发工具CLI代码质量Lint格式化

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() 移除其后面的空格。

候选判定与执行优先级

测试用例:向后兼容性的保障

规则文档明确说明:"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 是一个低风险、高覆盖的代码风格规则,它确保一元运算符与操作数之间不留空格,从而提升代码的可读性与一致性。其核心要点可归纳为:

  1. 默认处理所有一元运算符,包括 ++、--、!、~、&、...、@ 及一元正负号;
  2. only_dec_inc 可将作用范围收窄到自增/自减,PER 系与 PSR-12 规则集正是采用这一收紧配置;
  3. 底层通过 TokensAnalyzer 的 isUnarySuccessorOperator / isUnaryPredecessorOperator 区分一元与二元语境,保证 $a * -$b 这类"疑似二元"的表达式不会被误伤;
  4. 执行顺序上有明确约束(优先于 NotOperatorWithSpaceFixer 等规则),规则间不会相互覆盖。

对于以 PER-CS 或 PSR-12 为基线的项目,建议保持规则集默认的 only_dec_inc => true 行为;若追求更严格的空格统一(包括 !、~、&、...),则按上文示例覆盖配置即可。

登录后查看全文
PHP-CS-Fixer