phpstan-developer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePHPStan Extension Builder
PHPStan扩展构建指南
PHPStan finds bugs by traversing the PHP-Parser AST, resolving types via PHPStan's type system, and reporting errors from .
processNode()PHPStan通过遍历PHP-Parser的AST(抽象语法树)、利用自身类型系统解析类型,并通过方法报告错误来发现代码缺陷。
processNode()Workflow
工作流程
- Identify the PHP-Parser node type to target — use with
var_dump(get_class($node))as a temporaryNode::classto discover node types, or check the php-parser docsgetNodeType() - For cross-file analysis (e.g. "find unused things", "check all calls to X"), use a Collector to gather data and a rule to report — see references/collectors.md
CollectedDataNode - Write the Rule class extending nothing — implement interface directly
Rule - Write the test class extending with fixture PHP files
RuleTestCase - Register the rule in a neon config file
- 确定要针对的PHP-Parser节点类型——可以临时将返回
getNodeType(),并使用Node::class来发现节点类型,也可以查看php-parser文档var_dump(get_class($node)) - 对于跨文件分析(例如“查找未使用的内容”、“检查所有对X的调用”),使用收集器来收集数据,并通过规则报告错误——详见references/collectors.md
CollectedDataNode - 编写规则类,无需继承任何类——直接实现接口
Rule - 编写测试类,继承并搭配测试用的PHP fixture文件
RuleTestCase - 在neon配置文件中注册该规则
Rule Skeleton
规则模板
php
<?php
declare(strict_types=1);
namespace App\PHPStan\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\IdentifierRuleError;
/**
* @implements Rule<MethodCall>
*/
final class MyRule implements Rule
{
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
// Return [] for no error, or build errors:
return [
RuleErrorBuilder::message('Something is wrong.')
->identifier('myRule.something') // required: camelCase.dotSeparated
->build(),
];
}
}php
<?php
declare(strict_types=1);
namespace App\PHPStan\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\IdentifierRuleError;
/**
* @implements Rule<MethodCall>
*/
final class MyRule implements Rule
{
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
// Return [] for no error, or build errors:
return [
RuleErrorBuilder::message('Something is wrong.')
->identifier('myRule.something') // required: camelCase.dotSeparated
->build(),
];
}
}processNode()
Return Values
processNode()processNode()
返回值说明
processNode()| Return | Effect |
|---|---|
| No errors — node is fine |
| Report one or more errors |
Return type is always . Never return a single object — always wrap in an array.
list<IdentifierRuleError>| 返回值 | 效果 |
|---|---|
| 无错误——节点代码合规 |
| 报告一个或多个错误 |
返回类型必须是。绝不能返回单个对象——必须始终用数组包裹。
list<IdentifierRuleError>RuleErrorBuilder API
RuleErrorBuilder API说明
php
RuleErrorBuilder::message('Error message text.') // required
->identifier('category.specific') // required; pattern: /[a-z][a-z0-9]*(\.[a-z0-9]+)*/
->line($node->getStartLine()) // override line number
->tip('Suggestion to fix this.') // optional tip shown to user
->addTip('Additional tip.') // add more tips
->discoveringSymbolsTip() // standard "class not found" tip
->nonIgnorable() // cannot be suppressed with @phpstan-ignore
->fixNode($node, fn (Node $n) => $modified) // experimental: provide an automatic fix
->build() // returns IdentifierRuleErrorFixable errors — attaches an AST transformation callable to the error. When the user runs (or their editor's PHPStan integration applies fixes), PHPStan replaces the original node with the result of the callable. The callable receives the original node and must return a replacement node of the same type. This is marked in the source but is used throughout PHPStan core. See references/testing.md for how to test fixes.
->fixNode()phpstan analyse --fix@internal ExperimentalWhen the fix is complex, use Rector instead.is limited to replacing a single node in-place. If the fix needs to add imports, restructure multiple nodes, move code, or make changes across more than one location in the file, write a Rector rule instead. Rector is purpose-built for multi-step AST transformations and handles pretty-printing, import resolution, and edge cases thatfixNode()cannot. PHPStan finds the problem; Rector fixes it.fixNode()
For CollectedDataNode rules (cross-file), you must set file and line explicitly:
php
RuleErrorBuilder::message('...')
->file('/path/to/file.php')
->line(42)
->identifier('myRule.something')
->build()php
RuleErrorBuilder::message('Error message text.') // required
->identifier('category.specific') // required; pattern: /[a-z][a-z0-9]*(\.[a-z0-9]+)*/
->line($node->getStartLine()) // override line number
->tip('Suggestion to fix this.') // optional tip shown to user
->addTip('Additional tip.') // add more tips
->discoveringSymbolsTip() // standard "class not found" tip
->nonIgnorable() // cannot be suppressed with @phpstan-ignore
->fixNode($node, fn (Node $n) => $modified) // experimental: provide an automatic fix
->build() // returns IdentifierRuleError可修复错误——方法会将AST转换的回调函数附加到错误上。当用户运行(或编辑器的PHPStan集成工具应用修复)时,PHPStan会用回调函数的返回结果替换原始节点。回调函数接收原始节点,必须返回同类型的替换节点。该功能在源码中标记为(内部实验性功能),但已在PHPStan核心代码中广泛使用。有关如何测试修复功能,请参阅references/testing.md。
->fixNode()phpstan analyse --fix@internal Experimental当修复逻辑复杂时,请使用Rector替代。仅支持原地替换单个节点。如果修复需要添加导入语句、重构多个节点、移动代码或修改文件中多个位置的内容,请编写Rector规则。Rector专为多步骤AST转换设计,能处理代码格式化、导入解析以及fixNode()无法应对的边缘情况。PHPStan负责发现问题;Rector负责修复问题。fixNode()
对于CollectedDataNode规则(跨文件分析),必须显式设置文件路径和行号:
php
RuleErrorBuilder::message('...')
->file('/path/to/file.php')
->line(42)
->identifier('myRule.something')
->build()Common Scope Methods
常用Scope方法
php
$scope->getType($node) // Type of any Expr node
$scope->isInClass() // Currently inside a class?
$scope->getClassReflection() // ClassReflection|null
$scope->getFunction() // FunctionReflection|null
$scope->isInAnonymousFunction() // Inside a closure?
$scope->hasVariableType('varName') // TrinaryLogic: yes/maybe/no
$scope->getVariableType('varName') // Type of $varName
$scope->filterByTruthyValue($expr) // Narrowed scope when $expr is true
$scope->isDeclareStrictTypes() // strict_types=1 active?
$scope->resolveName($nameNode) // Resolve self/parent/static to FQCNTrinaryLogic — the result of all and checks. Has three states:
is*()has*()- — definitely true; use when you want zero false positives
->yes() - — definitely false; use as an early-return guard to skip inapplicable nodes
->no() - — uncertain (mixed/union); use for softer warnings or combined checks
->maybe()
See references/trinary-logic.md for the full decision guide, logical operations, and patterns.
php
$scope->getType($node) // Type of any Expr node
$scope->isInClass() // Currently inside a class?
$scope->getClassReflection() // ClassReflection|null
$scope->getFunction() // FunctionReflection|null
$scope->isInAnonymousFunction() // Inside a closure?
$scope->hasVariableType('varName') // TrinaryLogic: yes/maybe/no
$scope->getVariableType('varName') // Type of $varName
$scope->filterByTruthyValue($expr) // Narrowed scope when $expr is true
$scope->isDeclareStrictTypes() // strict_types=1 active?
$scope->resolveName($nameNode) // Resolve self/parent/static to FQCNTrinaryLogic(三值逻辑)——所有和方法的返回结果包含三种状态:
is*()has*()- — 肯定为真;用于需要零误报的场景
->yes() - — 肯定为假;用作提前返回的守卫条件,跳过不适用的节点
->no() - — 不确定(混合/联合类型);用于软警告或组合检查
->maybe()
有关完整的决策指南、逻辑运算和使用模式,请参阅references/trinary-logic.md。
Common Type Methods
常用Type方法
Never use on PHPStan types — always use the methods:
instanceofis*()php
$type = $scope->getType($node);
$type->isString()->yes() // Is definitely a string?
$type->isObject()->yes() // Is definitely an object?
$type->isNull()->yes() // Is always null?
$type->isArray()->yes() // Is always an array?
$type->getObjectClassNames() // list<string> of class names
$type->getConstantStrings() // list<ConstantStringType>
$type->describe(VerbosityLevel::typeOnly()) // Human-readable type description绝不要对PHPStan的类型使用——请始终使用方法:
instanceofis*()php
$type = $scope->getType($node);
$type->isString()->yes() // Is definitely a string?
$type->isObject()->yes() // Is definitely an object?
$type->isNull()->yes() // Is always null?
$type->isArray()->yes() // Is always an array?
$type->getObjectClassNames() // list<string> of class names
$type->getConstantStrings() // list<ConstantStringType>
$type->describe(VerbosityLevel::typeOnly()) // Human-readable type descriptionWriting Tests
编写测试
Every rule needs a test class and at least one fixture file. Use one fixture file per scenario.
Test class ():
tests/Rules/MyRuleTest.phpphp
<?php
declare(strict_types=1);
namespace App\Tests\PHPStan\Rules;
use App\PHPStan\Rules\MyRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
/**
* @extends RuleTestCase<MyRule>
*/
final class MyRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new MyRule();
}
public function testRule(): void
{
$this->analyse(
[__DIR__ . '/data/my-rule.php'],
[
['Error message text.', 10], // [message, line]
['Another error.', 25, 'A tip.'], // [message, line, tip] (optional)
]
);
}
public function testNoErrors(): void
{
$this->analyse([__DIR__ . '/data/my-rule-clean.php'], []);
}
}Fixture file () — plain PHP file with code that triggers the rule:
tests/Rules/data/my-rule.phpphp
<?php
declare(strict_types=1);
namespace App\Tests\PHPStan\Rules\Data;
// This call should trigger the rule on line 10:
$obj->forbiddenMethod();Key rules:
- One scenario per fixture file — do not mix multiple unrelated scenarios in one file
- Fixture files live in a subdirectory relative to the test class
data/ - The assertion fails if any unexpected errors appear, or expected errors are missing
analyse() - If a rule has constructor dependencies, create them manually in
getRule()
See references/testing.md for: additional config files, injecting services, TypeInferenceTestCase.
每个规则都需要一个测试类和至少一个fixture文件。每个场景对应一个fixture文件。
测试类():
tests/Rules/MyRuleTest.phpphp
<?php
declare(strict_types=1);
namespace App\Tests\PHPStan\Rules;
use App\PHPStan\Rules\MyRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
/**
* @extends RuleTestCase<MyRule>
*/
final class MyRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new MyRule();
}
public function testRule(): void
{
$this->analyse(
[__DIR__ . '/data/my-rule.php'],
[
['Error message text.', 10], // [message, line]
['Another error.', 25, 'A tip.'], // [message, line, tip] (optional)
]
);
}
public function testNoErrors(): void
{
$this->analyse([__DIR__ . '/data/my-rule-clean.php'], []);
}
}Fixture文件()——触发规则的普通PHP文件:
tests/Rules/data/my-rule.phpphp
<?php
declare(strict_types=1);
namespace App\Tests\PHPStan\Rules\Data;
// This call should trigger the rule on line 10:
$obj->forbiddenMethod();核心规则:
- 每个fixture文件对应一个场景——不要在一个文件中混合多个不相关的场景
- Fixture文件存放在测试类所在目录的子目录下
data/ - 如果出现未预期的错误或缺少预期的错误,断言会失败
analyse() - 如果规则有构造函数依赖,请在方法中手动创建依赖实例
getRule()
有关额外配置文件、服务注入、TypeInferenceTestCase的内容,请参阅references/testing.md。
Registration (phpstan.neon / extension.neon)
注册规则(phpstan.neon / extension.neon)
Shorthand (simple rules with no constructor dependencies):
neon
rules:
- App\PHPStan\Rules\MyRuleFull service registration (for rules with dependencies):
neon
services:
-
class: App\PHPStan\Rules\MyRule
tags:
- phpstan.rules.rule
-
class: App\PHPStan\Collectors\MyCollector
tags:
- phpstan.collector简写方式(无构造函数依赖的简单规则):
neon
rules:
- App\PHPStan\Rules\MyRule完整服务注册(适用于有依赖的规则):
neon
services:
-
class: App\PHPStan\Rules\MyRule
tags:
- phpstan.rules.rule
-
class: App\PHPStan\Collectors\MyCollector
tags:
- phpstan.collectorReference Files
参考文档
- references/trinary-logic.md — TrinaryLogic in depth: when to use yes/no/maybe, and/or/negate, patterns
- references/collectors.md — Collector interface, cross-file analysis, CollectedDataNode pattern
- references/testing.md — Full test structure, injecting services, additional config files, TypeInferenceTestCase
- references/scope-api.md — Full Scope API, ReflectionProvider, ClassReflection methods
- references/virtual-nodes.md — PHPStan virtual nodes (InClassNode, InClassMethodNode, FileNode, etc.)
- references/extensions.md — Dynamic return type extensions, type specifying extensions, reflection extensions, neon service tags
- references/trinary-logic.md — 深入讲解TrinaryLogic:yes/no/maybe的使用场景、逻辑运算及模式
- references/collectors.md — Collector接口、跨文件分析、CollectedDataNode模式
- references/testing.md — 完整测试结构、服务注入、额外配置文件、TypeInferenceTestCase
- references/scope-api.md — 完整Scope API、ReflectionProvider、ClassReflection方法
- references/virtual-nodes.md — PHPStan虚拟节点(InClassNode、InClassMethodNode、FileNode等)
- references/extensions.md — 动态返回类型扩展、类型指定扩展、反射扩展、neon服务标签