TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
559 lines
18 KiB
559 lines
18 KiB
# PHPStan 设计分析:可引入 AOT 编译器的模式与模块
|
|
|
|
本文档分析了 PHPStan 项目 (`projects/phpstan-src/`) 的架构设计,识别出可引入 AOT 编译器的设计模式和模块。
|
|
|
|
---
|
|
|
|
## 概览:四大优先级的可迁移设计
|
|
|
|
| 优先级 | 设计/模块 | 实施量级 | 前置依赖 | 收益 |
|
|
|--------|----------|---------|---------|------|
|
|
| P1 | Type 对象层次 (#1) | ~1000 行 | 无 | 类型推导精度质的提升 |
|
|
| P1 | TrinaryLogic (#2) | ~80 行 | 无 | 类型查询不再给出错误答案 |
|
|
| P1 | TypeCombinator (#3) | ~300-400 行 | #1 | 消除散落的类型字符串拼接 |
|
|
| P2 | Rule 系统 (#4) | 接口+Registry ~80 行 | 基本独立 | 架构模块化,易测试易扩展 |
|
|
| P2 | Collector 双阶段分析 (#5) | ~200 行 | #4 | 跨文件全局优化 |
|
|
| P2 | Extension 注册机制 (#6) | ~100 行 | #4 | 框架级插件系统 |
|
|
| P3 | TypeSpecifier / 类型窄化 (#7) | ~200 行 | #1, #2 | if/else 分支类型精化 |
|
|
| P3 | Immutable Scope (#8) | ~300 行 | #1, #2 | 表达式级类型追踪 |
|
|
| P4 | PHPDoc Pipeline (#9) | 可复用 phpstan/phpdoc-parser | #1 | 利用现有成熟解析器 |
|
|
| P4 | NeverType (#10) | 含于 #1 | #1 | 死代码消除,矛盾类型检测 |
|
|
|
|
---
|
|
|
|
## 1. Object-Oriented Type 系统(替代基于字符串的类型表示)
|
|
|
|
### 现状
|
|
|
|
AOT 编译器用字符串常量表示类型:
|
|
|
|
```php
|
|
const TYPE_INT = 'int';
|
|
const TYPE_FLOAT = 'float';
|
|
const TYPE_STR = 'string';
|
|
// 复合类型用字符串拼接
|
|
// 'int|string', 'string|null'
|
|
```
|
|
|
|
这导致类型运算(合并、比较、查询)必须手动解析和拼接字符串。
|
|
|
|
### PHPStan 方案
|
|
|
|
PHPStan 有一个 `Type` 接口,定义约 100 个方法。每种类型是一个类:
|
|
|
|
```
|
|
Type (interface)
|
|
├── StringType
|
|
├── IntegerType
|
|
├── FloatType
|
|
├── BooleanType
|
|
├── NullType
|
|
├── MixedType (顶类型,带 subtracted type 支持)
|
|
├── NeverType (底类型,无可能值)
|
|
├── VoidType
|
|
├── ArrayType
|
|
├── ObjectType
|
|
├── CallableType
|
|
├── UnionType (A|B|C,持有 flat list<Type>)
|
|
├── IntersectionType (A&B&C)
|
|
├── ConstantStringType, ConstantIntegerType, ...
|
|
├── IntegerRangeType
|
|
└── Accessory*Type (用于 IntersectionType 中的精化)
|
|
```
|
|
|
|
**核心设计原则:永远不要用 `instanceof` 判断类型身份。** 必须使用 `is*()` 方法:
|
|
|
|
```php
|
|
// 错误 —— 漏掉了 UnionType 和 IntersectionType
|
|
$type instanceof StringType
|
|
|
|
// 正确 —— UnionType 会委托给内部类型并组合结果
|
|
$type->isString()->yes()
|
|
```
|
|
|
|
**关键子接口:**
|
|
|
|
- `CompoundType`:标记需要双向类型比较的类型(UnionType, IntersectionType)。添加 `isAcceptedBy()`, `isSubTypeOf()` 方法,实现双分派协议。
|
|
- `SubtractableType`:支持差集操作(如 `mixed~null`)。
|
|
|
|
### isSuperTypeOf / accepts 双分派协议
|
|
|
|
这是 PHPStan 类型系统最核心的设计模式:
|
|
|
|
```
|
|
简单类型 (StringType)::isSuperTypeOf(Type $otherType):
|
|
if $otherType 是 CompoundType:
|
|
return $otherType->isSubTypeOf($this) // 反向委托
|
|
// 自身逻辑 ...
|
|
return No
|
|
|
|
复合类型 (UnionType)::isSubTypeOf(Type $otherType):
|
|
foreach innerType in $this->types:
|
|
results[] = $otherType->isSuperTypeOf(innerType)
|
|
return extremeIdentity(results) // ALL 必须是 subtype
|
|
```
|
|
|
|
**要点:** 简单类型不需要理解复合类型的语义。新增复合类型时不需要修改任何简单类型的比较逻辑。
|
|
|
|
### AOT 采用建议
|
|
|
|
AOT 编译器不需要 PHPStan 全部的复杂度。第一版只需这些具体类型:
|
|
|
|
```
|
|
IntegerType, FloatType, StringType, BoolType,
|
|
NullType, MixedType, NeverType, ArrayType, ObjectType, UnionType
|
|
```
|
|
|
|
不需要:IntersectionType, AccessoryType, TemplateType, Constant*Type, IntegerRangeType, EnumType。
|
|
|
|
---
|
|
|
|
## 2. TrinaryLogic(三值逻辑)
|
|
|
|
### 现状
|
|
|
|
AOT 编译器用 boolean 判断类型属性。但 `mixed` 类型意味着 "可能是 string,也可能是 int",boolean 无法表达这种不确定性。
|
|
|
|
### PHPStan 方案
|
|
|
|
```php
|
|
class TrinaryLogic {
|
|
public function yes(): bool;
|
|
public function no(): bool;
|
|
public function maybe(): bool;
|
|
|
|
public static function createYes(): self;
|
|
public static function createNo(): self;
|
|
public static function createMaybe(): self;
|
|
|
|
public function and(self ...$others): self;
|
|
public function or(self ...$others): self;
|
|
public function extremeIdentity(self ...$others): self; // ALL yes → yes; ALL no → no
|
|
public function maxMin(self ...$others): self; // ANY yes → yes; ALL no → no
|
|
}
|
|
```
|
|
|
|
使用示例:
|
|
|
|
```php
|
|
// MixedType::isString() → maybe(mixed 可能是 string)
|
|
// IntegerType::isString() → no
|
|
// UnionType(int|string)::isString() → maybe
|
|
|
|
$type->isString()->yes() // 确定是 string
|
|
$type->isString()->no() // 确定不是 string
|
|
$type->isString()->maybe() // 不确定
|
|
```
|
|
|
|
### AOT 采用建议
|
|
|
|
直接移植,约 80 行代码,无外部依赖。所有类型查询方法用 TrinaryLogic 替代 boolean。
|
|
|
|
---
|
|
|
|
## 3. TypeCombinator —— 类型规范化引擎
|
|
|
|
### 要解决的问题
|
|
|
|
禁止直接 `new UnionType(...)`。所有 union/intersect/remove 操作必须经过 TypeCombinator,确保类型表示始终是规范化的。
|
|
|
|
### 三个核心操作
|
|
|
|
#### `union(Type ...$types): Type`
|
|
|
|
**算法流程:**
|
|
|
|
1. **Fast path**:0 个参数 → `NeverType`;1 个参数 → 直接返回;2 个参数检查 `never`/`mixed`/相同对象
|
|
2. **扁平化**:`union(A, union(B, C), D)` → 展开为 `[A, B, C, D]`
|
|
3. **过滤 NeverType**:`union(int, never, string)` → `[int, string]`
|
|
4. **分类提取**:将类型分为 scalar/array/enum/integerRange/generic 五类,分批处理
|
|
5. **标量消解**:`ConstantIntegerType(3) | IntegerType` → `IntegerType`;`true | false` → `BooleanType`
|
|
6. **Pairwise 比较**:
|
|
- `IntegerRangeType` 相邻区间合并:`int<0,5> | int<3,10>` → `int<0,10>`
|
|
- Subtype/supertype 消除:`Foo extends Bar` ⇒ `Foo | Bar` = `Bar`
|
|
- `int[] | string[]` → `(int|string)[]`
|
|
7. **收尾**:0 个→NeverType;1 个→直接返回;否则 `new UnionType(array_values($types), true)`
|
|
|
|
#### `intersect(Type ...$types): Type`
|
|
|
|
**算法流程:**
|
|
|
|
1. 有 `NeverType` → 直接返回 never
|
|
2. **分配律展开**:`A & (B|C)` → `(A&B) | (A&C)`,然后对每个子项递归
|
|
3. **扁平化**:`A & (B & C)` → 展开为 `[A, B, C]`
|
|
4. **Pairwise 双向比较**:
|
|
- `IntegerType & ConstantIntegerType(5)` → `ConstantIntegerType(5)`(Child & Parent = Child)
|
|
- `int & string` → `NeverType`(矛盾)
|
|
- SubtractableType 差集交接
|
|
5. **矛盾检测**:`isSuperTypeOf` 返回 `no` → `NeverType`
|
|
|
|
#### `remove(Type $fromType, Type $typeToRemove): Type`
|
|
|
|
```
|
|
remove(int|string, string) = int
|
|
remove(int|string|null, null) = int|string
|
|
remove(int, string) = int // 要移除的不在里面
|
|
remove(string, string) = never // 完全移除
|
|
remove(mixed, Foo) = mixed~Foo // 差集类型
|
|
```
|
|
|
|
### AOT 采用建议
|
|
|
|
精简版 TypeCombinator(~300-400 行),覆盖 AOT 需要的基础类型的 union/intersect/remove。不需要 PHPStan 复杂的数组 shape 处理、accessory type 传播、IntegerRange 合并等逻辑。
|
|
|
|
### 收益
|
|
|
|
| 场景 | 当前 | TypeCombinator 后 |
|
|
|------|------|-------------------|
|
|
| `int\|int` 的变量赋值 | 字符串 `'int\|int'` | 自动简化为 `IntegerType` |
|
|
| `mixed\|string` 的返回值 | 不知道如何简化 | 自动简化为 `MixedType` |
|
|
| 两个分支类型合并 | 手动拼接 | `union()` 自动去重去 subtype |
|
|
| `int & string` 交集 | 无法检测 | 返回 `NeverType`(编译错误) |
|
|
|
|
---
|
|
|
|
## 4. Rule-based 分析 Pass 系统
|
|
|
|
### 核心接口
|
|
|
|
```php
|
|
/**
|
|
* @template TNodeType of Node
|
|
*/
|
|
interface Rule
|
|
{
|
|
/** @return class-string<TNodeType> */
|
|
public function getNodeType(): string;
|
|
|
|
/** @param TNodeType $node */
|
|
public function processNode(Node $node, Scope $scope): array;
|
|
}
|
|
```
|
|
|
|
每个 Rule 声明它关心哪种 AST 节点类型,以及在发现该节点时做什么。
|
|
|
|
### 注册机制
|
|
|
|
通过 PHP 8 Attribute 声明级别:
|
|
|
|
```php
|
|
#[RegisteredRule(level: 0)]
|
|
final class CallMethodsRule implements Rule
|
|
{
|
|
public function getNodeType(): string { return MethodCall::class; }
|
|
public function processNode(Node $node, Scope $scope): array { ... }
|
|
}
|
|
```
|
|
|
|
### Registry 实现
|
|
|
|
`LazyRegistry` 从 DI 容器收集所有带 `phpstan.rules.rule` 标签的服务,按 `getNodeType()` 返回值建立索引。
|
|
|
|
**关键设计:** `getRules($nodeType)` 不仅匹配精确的类名,还匹配所有父类和接口:
|
|
|
|
```php
|
|
public function getRules(string $nodeType): array
|
|
{
|
|
// $nodeType = MethodCall::class
|
|
// parentNodeTypes = [MethodCall, Expr, NodeAbstract, Node, ...]
|
|
// 匹配所有注册在这些父类/接口上的 Rule
|
|
$parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
这意味着注册为 `Node\Expr` 的 rule 会匹配**所有**表达式类型。
|
|
|
|
### 运行时调度
|
|
|
|
在 AST 遍历的每个节点上:
|
|
|
|
```php
|
|
$nodeType = get_class($node);
|
|
foreach ($this->ruleRegistry->getRules($nodeType) as $rule) {
|
|
$ruleErrors = $rule->processNode($node, $scope);
|
|
// 转换和收集错误 ...
|
|
}
|
|
```
|
|
|
|
极其简洁,没有 switch,没有 if-else 链。
|
|
|
|
### Collector 双阶段分析
|
|
|
|
Collector 接口与 Rule 几乎相同,但返回收集的数据(而不是错误):
|
|
|
|
```php
|
|
interface Collector
|
|
{
|
|
public function getNodeType(): string;
|
|
/** @return TValue|null */
|
|
public function processNode(Node $node, Scope $scope);
|
|
}
|
|
```
|
|
|
|
**阶段 1(per-file):** 在每个文件被分析时,collector 收集数据。
|
|
|
|
**阶段 2(global):** 所有文件分析完后,创建 `CollectedDataNode` 封装所有数据,运行注册在其上的规则:
|
|
|
|
```php
|
|
$node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles);
|
|
foreach ($this->ruleRegistry->getRules(CollectedDataNode::class) as $rule) {
|
|
$ruleErrors = $rule->processNode($node, $scope);
|
|
}
|
|
```
|
|
|
|
`CollectedDataNode::get(string $collectorType): array<string, list<TValue>>` 按文件路径索引收集到的数据。
|
|
|
|
### AOT 编译器具体应用
|
|
|
|
| Rule | 触发节点 | 作用 |
|
|
|------|----------|------|
|
|
| `BinaryOpCodegenRule` | `Expr\BinaryOp` | 生成 C++ 运算代码 |
|
|
| `MethodCallCodegenRule` | `Expr\MethodCall` | 方法调用代码生成,虚调用/直调判断 |
|
|
| `TypeCheckInsertRule` | `Param` / `Return_` | 函数入口/出口插入运行时类型检查 |
|
|
| `DeadCodeEliminateRule` | `CollectedDataNode` | 跨文件分析,移除未调用函数 |
|
|
| `DevirtualizeRule` | `CollectedDataNode` | 单实现虚方法 → 直接调用 |
|
|
| `InlineDecisionRule` | `CollectedDataNode` | 基于调用频率和函数大小决定内联 |
|
|
| `ConstantFoldRule` | `Expr\BinaryOp` | 编译期常量折叠 |
|
|
| `BoxOptimizationRule` | `Expr\Assign` | std container 的 Box 逃逸分析 |
|
|
|
|
**O0/O1/O2 分级:**
|
|
|
|
```php
|
|
#[RegisteredRule(level: 0)] // 基础代码生成,永远必须
|
|
class BinaryOpCodegenRule implements Rule { ... }
|
|
|
|
#[RegisteredRule(level: 1)] // O1 优化
|
|
class ConstantFoldRule implements Rule { ... }
|
|
|
|
#[RegisteredRule(level: 2)] // O2 激进优化
|
|
class InlineDecisionRule implements Rule { ... }
|
|
```
|
|
|
|
### AOT 采用建议
|
|
|
|
渐进式迁移,不需要一次重写整个编译器:
|
|
|
|
1. 定义 `Rule` 接口和 `Registry`(约 80 行代码)
|
|
2. 把 `parseStmts()` 的一个独立功能提取为第一个 Rule
|
|
3. 逐步迁移剩余 switch 分支
|
|
4. 引入 `Collector` + `CollectedDataNode` 用于跨文件优化
|
|
|
|
Rule 系统可以与现有 switch 调度共存——先让 Rule 处理它能处理的节点,其他 fallback 到原有逻辑。
|
|
|
|
---
|
|
|
|
## 5. Extension 注册机制
|
|
|
|
### PHPStan 方案
|
|
|
|
PHPStan 有数十个扩展接口,通过 DI 容器的 service tag 机制注册:
|
|
|
|
```
|
|
DynamicMethodReturnTypeExtension → tag: phpstan.broker.dynamicMethodReturnTypeExtension
|
|
FunctionTypeSpecifyingExtension → tag: phpstan.typeSpecifier.functionTypeSpecifyingExtension
|
|
TypeNodeResolverExtension → tag: phpstan.phpdoc.typeNodeResolverExtension
|
|
MethodsClassReflectionExtension → tag: phpstan.broker.methodsClassReflectionExtension
|
|
PropertiesClassReflectionExtension → tag: phpstan.broker.propertiesClassReflectionExtension
|
|
...
|
|
```
|
|
|
|
扩展在核心逻辑之前被调用,有机会覆盖默认行为:
|
|
|
|
```php
|
|
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
|
|
{
|
|
foreach ($this->extensions as $extension) {
|
|
$type = $extension->resolve($typeNode, $nameScope);
|
|
if ($type !== null) {
|
|
return $type; // 扩展处理了,短路核心逻辑
|
|
}
|
|
}
|
|
// 核心逻辑 ...
|
|
}
|
|
```
|
|
|
|
### AOT 应用
|
|
|
|
框架特定的编译优化可通过扩展插件提供,而不是修改编译器核心:
|
|
|
|
```php
|
|
interface MethodCallOptimizationExtension
|
|
{
|
|
/** 返回优化后的 C++ 代码,或 null 表示不处理 */
|
|
public function optimize(MethodCall $call, Scope $scope): ?string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. TypeSpecifier —— 类型窄化引擎
|
|
|
|
### 解决的问题
|
|
|
|
当 AOT 编译器遇到 `if ($x instanceof Foo)` 时,在分支内部 `$x` 的类型应该被精化为 `Foo`。这可以生成更高效的 C++ 代码(直接调用 Foo 的方法,不走虚表)。
|
|
|
|
### PHPStan 方案
|
|
|
|
`TypeSpecifier` 分析条件表达式,决定如何在 truthy/falsy 分支中窄化类型:
|
|
|
|
| 条件 | Truthy 窄化为 | Falsey 窄化为 |
|
|
|------|-------------|--------------|
|
|
| `$x instanceof Foo` | `Foo` | 移除 `Foo` |
|
|
| `$x === null` | `NullType` | 移除 `NullType` |
|
|
| `is_array($x)` | `ArrayType` | 移除 `ArrayType` |
|
|
| `$x`(truthy) | 移除 falsey 类型 | 仅保留 falsey 类型 |
|
|
| `$x > 0` | `int<1, max>` | `int<min, 0>` |
|
|
|
|
### 类型窄化流水线
|
|
|
|
```
|
|
条件表达式
|
|
→ TypeSpecifier::specifyTypesInCondition(scope, expr, context)
|
|
→ SpecifiedTypes { sureTypes[], sureNotTypes[] }
|
|
→ MutatingScope::filterBySpecifiedTypes(types)
|
|
→ 新的 scope(类型已窄化)
|
|
```
|
|
|
|
### AOT 应用
|
|
|
|
简化版 TypeSpecifier(~200 行),处理:
|
|
- `instanceof` 窄化
|
|
- `=== null` / `!== null` 窄化
|
|
- `is_array()`、`is_string()`、`is_int()` 等函数窄化
|
|
- `BooleanAnd`/`BooleanOr` 链式窄化
|
|
|
|
---
|
|
|
|
## 7. Immutable Scope(不可变作用域)
|
|
|
|
### 现状
|
|
|
|
AOT 的 `FunctionContext` 使用可变公共数组,仅按变量名追踪类型:
|
|
|
|
```php
|
|
class FunctionContext {
|
|
public array $localVars = []; // 仅变量名 → 类型
|
|
public int $scopeLevel = 0; // 简单嵌套计数
|
|
public array $scopeLayouts = []; // ScopeContext 是空类
|
|
public bool $inLoop = false;
|
|
public bool $inClosure = false;
|
|
}
|
|
```
|
|
|
|
### PHPStan 方案
|
|
|
|
`MutatingScope` 是**不可变**的持久化数据结构。每次变更返回新实例:
|
|
|
|
```
|
|
scope.assignVariable('x', stringType) → new scope
|
|
scope.filterByTruthyValue(instanceofExpr) → new scope (窄化后的类型)
|
|
scope.filterByFalseyValue(instanceofExpr) → new scope (移除后的类型)
|
|
scope.mergeWith(elseScope) → new scope (交集)
|
|
```
|
|
|
|
按**表达式字符串**追踪类型,而非仅按变量名:
|
|
|
|
```
|
|
'$a' → ExpressionTypeHolder($a, IntegerType, Yes)
|
|
'$a[0]' → ExpressionTypeHolder($a[0], StringType, Yes)
|
|
'$a->prop' → ExpressionTypeHolder($a->prop, FooType, Yes)
|
|
'strlen($a)' → ExpressionTypeHolder(strlen($a), IntegerType, Yes)
|
|
```
|
|
|
|
这意味着给 `$a` 赋值会使 `$a[0]` 和 `$a->prop` 的类型缓存失效。
|
|
|
|
### AOT 采用建议
|
|
|
|
精简版(~300 行),核心能力:
|
|
|
|
- 不可变 scope,支持快照和合并
|
|
- 表达式键的类型追踪(至少支持变量和数组元素)
|
|
- `TrinaryLogic` 确定度追踪(分支合并后的 maybe 状态)
|
|
|
|
---
|
|
|
|
## 8. PHPDoc Pipeline
|
|
|
|
### PHPStan 方案
|
|
|
|
三阶段流水线:
|
|
|
|
```
|
|
PHPDoc 注释字符串
|
|
→ Lexer + Parser (phpstan/phpdoc-parser)
|
|
→ PhpDocNode (原始 AST)
|
|
→ TypeNodeResolver::resolve(TypeNode, NameScope)
|
|
→ PHPStan Type 对象
|
|
```
|
|
|
|
`TypeNodeResolver` 用一个 `switch` 分发到 30+ 种标识符类型:
|
|
|
|
```
|
|
'int' / 'integer' → IntegerType
|
|
'positive-int' → IntegerRangeType(1, null)
|
|
'non-empty-string' → IntersectionType[StringType, AccessoryNonEmptyStringType]
|
|
'class-string' → ClassStringType
|
|
'array' → ArrayType(MixedType, MixedType)
|
|
'list' → IntersectionType[ArrayType(int), AccessoryArrayListType]
|
|
'mixed' → MixedType(true)
|
|
'never' → NonAcceptingNeverType
|
|
...
|
|
```
|
|
|
|
### AOT 应用
|
|
|
|
AOT 可以复用 `phpstan/phpdoc-parser` 来解析 `@param`、`@return`、`@var` 注解,然后将类型 AST 节点映射到 AOT 自己的 Type 系统。`NameScope`(追踪 namespace + use imports)也是可直接复用的概念。
|
|
|
|
---
|
|
|
|
## 9. NeverType(底类型)
|
|
|
|
表示"不可能有任何值"的类型,即空集:
|
|
|
|
```
|
|
union(int, never) = int // never 是 union 的中性元
|
|
intersect(string, never) = never // never 是 intersect 的吸收元
|
|
```
|
|
|
|
### AOT 应用
|
|
|
|
- 死代码消除:表达式窄化为 NeverType → 该路径不可达
|
|
- 错误传播:矛盾的类型组合产生 NeverType
|
|
- `void` 函数返回:本质上就是 NeverType 在 return 位置
|
|
|
|
---
|
|
|
|
## 推荐采用顺序
|
|
|
|
```
|
|
Phase 1 (基础层): Type 接口 + 具体类型 + TypeCombinator + TrinaryLogic
|
|
└── 替代字符串类型表示,预计减少散落的类型字符串操作
|
|
|
|
Phase 2 (模块化): Rule 接口 + Registry + Attribute 注册
|
|
└── 分解 parseStmts/parseExpr 的巨大 switch
|
|
|
|
Phase 3 (分析层): TypeSpecifier + 基础 Immutable Scope
|
|
└── 启用 if/else 分支类型窄化
|
|
|
|
Phase 4 (优化层): Collector + CollectedDataNode
|
|
└── 启用跨文件全局优化(去虚拟化、死代码消除、内联决策)
|
|
|
|
Phase 5 (生态层): Extension 注册 + PHPDoc Pipeline
|
|
└── 启用框架插件和注解驱动的类型信息
|
|
```
|
|
|
|
每一层都建立在上一层之上,且每一层都可以独立交付价值。
|
|
|
|
---
|
|
|
|
## 不应引入的 PHPStan 特性
|
|
|
|
| 特性 | 原因 |
|
|
|------|------|
|
|
| Generics / `@template T` | 复杂度极高,AOT 当前不需要 |
|
|
| IntersectionType + Accessory types | `non-empty-string` = `string & non-empty` 这类设计过度工程化 |
|
|
| BetterReflection (静态反射) | AOT 已经加载文件,不需要避免运行时副作用 |
|
|
| 完整 MutatingScope (5000+ 行) | 不可变 scope 模式值得借鉴,但 300 行足够 |
|
|
| ConditionalExpressionHolder | 复合条件的惰性窄化,先做基础 TypeSpecifier |
|
|
| Enum / ConstantArrayType shape | 小众,初期不需要 |
|
|
|