Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ff28469
Add auto-boot configuration support and related tests
rcalicdan Oct 3, 2026
de8cc37
Add auto-boot configuration support and related tests
rcalicdan Oct 3, 2026
5859653
Add ViolationCollector and ViolationRecord classes for managing type …
rcalicdan Oct 3, 2026
65b6406
Enhance ViolationCollector shutdown handler to optionally exit with f…
rcalicdan Oct 3, 2026
43c94e0
Add violation handling configuration options and update related docum…
rcalicdan Oct 3, 2026
314b7cc
Refactor violation handling and configuration options; enhance tests …
rcalicdan Oct 3, 2026
c9d7f37
Enhance violation handling in RuntimeTypeChecker, CallableWrapper, It…
rcalicdan Oct 3, 2026
dd0ccab
Add comprehensive tests for violation reporting and audit mode
rcalicdan Oct 3, 2026
ec24f02
Update violation handling configuration; change report file path and …
rcalicdan Oct 3, 2026
21e24c1
Add report and report:clear commands; implement report generation and…
rcalicdan Oct 3, 2026
fab6f32
Consolidate violation report data handling; improve files affected ca…
rcalicdan Oct 3, 2026
57416dc
Add ReportingHookedFixture class and update test to use it and preven…
rcalicdan Oct 3, 2026
f617372
Enhance test suite test isolation
rcalicdan Oct 3, 2026
268a05e
fix bug on Conditional return type on a private method always takes t…
rcalicdan Oct 4, 2026
401d21b
Fixes an issue where methods and functions returning by reference had…
rcalicdan Oct 4, 2026
9b9a0a5
Fixes a compile-time fatal error where `isset(self::$prop)` was rewri…
rcalicdan Oct 4, 2026
c0dd300
fix bug on property @var not resolved when a trait in another file as…
rcalicdan Oct 4, 2026
24dc3aa
fix bug on file:// stream wrapper changes plain file I/O: feof() is t…
rcalicdan Oct 4, 2026
762f19a
Add global value redaction feature and related tests
rcalicdan Oct 4, 2026
16db51e
Enhance CI workflow by adding PHP extensions and restructuring jobs f…
rcalicdan Oct 4, 2026
08baad4
Update CI configuration to use PHP 8.5 and implement path-based mutat…
rcalicdan Oct 4, 2026
ff56465
Refactor and enhance member assignment validation
rcalicdan Oct 4, 2026
510e169
Add runtime test fixtures for param-out, self-out, and generator cont…
rcalicdan Oct 4, 2026
925957a
Implement nested traits with property resolution and add related tests
rcalicdan Oct 4, 2026
e11a6ee
Enhance BlockScopeShadowingTest to validate outer scope restoration a…
rcalicdan Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@ permissions:
contents: read

jobs:
style:
name: Check Code Style
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo
coverage: none

- name: Install Dependencies
uses: ramsey/composer-install@v3

- name: Check Code Style (Pint)
run: ./vendor/bin/pint --test

analyze:
name: Static Analysis
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo
coverage: none

- name: Install Dependencies
uses: ramsey/composer-install@v3

- name: Run Static Analysis (PHPStan)
run: ./vendor/bin/phpstan analyse --no-progress

test:
name: PHP ${{ matrix.php }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand All @@ -38,16 +80,9 @@ jobs:
with:
dependency-versions: ${{ matrix.php == '8.2' && 'lowest' || 'highest' }}

- name: Run Static Analysis (PHPStan)
run: ./vendor/bin/phpstan analyse --no-progress
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'

- name: Warm Up TypePHP Cache
run: php bin/typephp cache:warm

- name: Run Runtime Type Checking Engine Suite (Pest)
run: ./vendor/bin/pest tests/TypeChecking --compact

- name: Run Full Test Suite with Coverage (Pest)
run: ./vendor/bin/pest --coverage-clover=clover.xml --compact
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.4'
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</include>
<exclude>
<file>src/bootstrap.php</file>
<directory suffix=".php">src/internal/Cli</directory>
</exclude>
</source>
</phpunit>
207 changes: 204 additions & 3 deletions src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use TypePHP\Internal\Docblock\DocblockExtractor;
use TypePHP\Internal\Docblock\DocblockNormalizer;
Expand Down Expand Up @@ -96,12 +99,21 @@ public function enterNode(Node $node): ?array

if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
$this->markWriteContext($node->var);
} elseif ($node instanceof Node\Expr\AssignRef) {
$this->markWriteContext($node->var);
$this->markWriteContext($node->expr);
} elseif ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
$this->markWriteContext($node->var);
} elseif ($node instanceof Node\Stmt\Unset_) {
foreach ($node->vars as $v) {
$this->markWriteContext($v);
}
} elseif ($node instanceof Node\Expr\Isset_) {
foreach ($node->vars as $v) {
$this->markWriteContext($v);
}
} elseif ($node instanceof Node\Expr\Empty_) {
$this->markWriteContext($node->expr);
}

if ($node instanceof Node\Stmt\Class_) {
Expand Down Expand Up @@ -207,6 +219,12 @@ private function markWriteContext(Node $node): void
$node->setAttribute('typephp_write_context', true);
if ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
$this->markWriteContext($node->var);
} elseif ($node instanceof Node\Expr\Array_ || $node instanceof Node\Expr\List_) {
foreach ($node->items as $item) {
if ($item !== null) {
$this->markWriteContext($item->value);
}
}
}
}

Expand Down Expand Up @@ -515,9 +533,9 @@ private function propagateShapeTypesToDestructuring(Node\Expr\List_|Node\Expr\Ar
$autoIdx = 0;
foreach ($parsed->items as $item) {
$key = match (true) {
$item->keyName instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode => $item->keyName->value,
$item->keyName instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode => (int) $item->keyName->value,
$item->keyName instanceof \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode => $item->keyName->name,
$item->keyName instanceof ConstExprStringNode => $item->keyName->value,
$item->keyName instanceof ConstExprIntegerNode => (int) $item->keyName->value,
$item->keyName instanceof IdentifierTypeNode => $item->keyName->name,
$item->keyName !== null => (string) $item->keyName,
default => $autoIdx,
};
Expand Down Expand Up @@ -600,6 +618,31 @@ private function handleAssign(Node\Expr\Assign $node): void

$node->expr = $this->wrapVariableCheck($expr, $typeString, $varName, $node->var->getStartLine());
}
} elseif (($chainInfo = $this->extractMemberChain($node->var)) !== null) {
$root = $chainInfo['root'];
$chain = $chainInfo['chain'];

if ($root instanceof Node\Expr\Variable && \is_string($root->name)) {
$varName = $root->name;
$typeString = $this->scopeManager->getVarTypeFromScope($varName);

if ($typeString !== null && $this->shouldHandleMemberAssignCheck($typeString, $chain)) {
$node->expr = $this->wrapMemberAssignCheck($node->expr, $root, $chain, $typeString, $varName, $node->var->getStartLine());

return;
}
}

// Fallback for direct single property access
if ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
$node->expr = $this->wrapPropertyCheck($node->expr, $node->var->var, $node->var->name->toString(), $node->var->getStartLine());
} elseif ($node->var instanceof Node\Expr\StaticPropertyFetch && $node->var->name instanceof Node\VarLikeIdentifier) {
$classArg = $node->var->class instanceof Node\Name
? new Node\Expr\ClassConstFetch($node->var->class, 'class')
: $node->var->class;

$node->expr = $this->wrapPropertyCheck($node->expr, $classArg, $node->var->name->toString(), $node->var->getStartLine());
}
} elseif ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
$node->expr = $this->wrapPropertyCheck($node->expr, $node->var->var, $node->var->name->toString(), $node->var->getStartLine());
} elseif ($node->var instanceof Node\Expr\StaticPropertyFetch && $node->var->name instanceof Node\VarLikeIdentifier) {
Expand Down Expand Up @@ -630,6 +673,37 @@ private function handleAssignOp(Node\Expr\AssignOp $node): ?Node\Expr\Assign
$this->wrapVariableCheck($binaryExpr, $typeString, $varName, $node->var->getStartLine())
);
}
} elseif (($chainInfo = $this->extractMemberChain($node->var)) !== null) {
$root = $chainInfo['root'];
$chain = $chainInfo['chain'];

if ($root instanceof Node\Expr\Variable && \is_string($root->name)) {
$varName = $root->name;
$typeString = $this->scopeManager->getVarTypeFromScope($varName);

if ($typeString !== null && $this->shouldHandleMemberAssignCheck($typeString, $chain)) {
return new Node\Expr\Assign(
$node->var,
$this->wrapMemberAssignCheck($binaryExpr, $root, $chain, $typeString, $varName, $node->var->getStartLine())
);
}
}

if ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
return new Node\Expr\Assign(
$node->var,
$this->wrapPropertyCheck($binaryExpr, $node->var->var, $node->var->name->toString(), $node->var->getStartLine())
);
} elseif ($node->var instanceof Node\Expr\StaticPropertyFetch && $node->var->name instanceof Node\VarLikeIdentifier) {
$classArg = $node->var->class instanceof Node\Name
? new Node\Expr\ClassConstFetch($node->var->class, 'class')
: $node->var->class;

return new Node\Expr\Assign(
$node->var,
$this->wrapPropertyCheck($binaryExpr, $classArg, $node->var->name->toString(), $node->var->getStartLine())
);
}
} elseif ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
return new Node\Expr\Assign(
$node->var,
Expand Down Expand Up @@ -665,6 +739,37 @@ private function handleIncDec(Node\Expr\PreInc|Node\Expr\PostInc|Node\Expr\PreDe
$this->wrapVariableCheck($binaryExpr, $typeString, $varName, $node->var->getStartLine())
);
}
} elseif (($chainInfo = $this->extractMemberChain($node->var)) !== null) {
$root = $chainInfo['root'];
$chain = $chainInfo['chain'];

if ($root instanceof Node\Expr\Variable && \is_string($root->name)) {
$varName = $root->name;
$typeString = $this->scopeManager->getVarTypeFromScope($varName);

if ($typeString !== null && $this->shouldHandleMemberAssignCheck($typeString, $chain)) {
return new Node\Expr\Assign(
$node->var,
$this->wrapMemberAssignCheck($binaryExpr, $root, $chain, $typeString, $varName, $node->var->getStartLine())
);
}
}

if ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
return new Node\Expr\Assign(
$node->var,
$this->wrapPropertyCheck($binaryExpr, $node->var->var, $node->var->name->toString(), $node->var->getStartLine())
);
} elseif ($node->var instanceof Node\Expr\StaticPropertyFetch && $node->var->name instanceof Node\VarLikeIdentifier) {
$classArg = $node->var->class instanceof Node\Name
? new Node\Expr\ClassConstFetch($node->var->class, 'class')
: $node->var->class;

return new Node\Expr\Assign(
$node->var,
$this->wrapPropertyCheck($binaryExpr, $classArg, $node->var->name->toString(), $node->var->getStartLine())
);
}
} elseif ($node->var instanceof Node\Expr\PropertyFetch && $node->var->name instanceof Node\Identifier) {
return new Node\Expr\Assign(
$node->var,
Expand All @@ -684,6 +789,101 @@ private function handleIncDec(Node\Expr\PreInc|Node\Expr\PostInc|Node\Expr\PreDe
return null;
}

/**
* Unpacks any nested chain of ArrayDimFetch and PropertyFetch down to its root expression.
*
* @return array{root: Node\Expr, chain: list<array{0: 'dim'|'prop', 1: Node\Expr|string|null}>}|null
*/
private function extractMemberChain(Node\Expr $target): ?array
{
$curr = $target;
$chain = [];

while ($curr instanceof Node\Expr\ArrayDimFetch || $curr instanceof Node\Expr\PropertyFetch) {
if ($curr instanceof Node\Expr\ArrayDimFetch) {
array_unshift($chain, ['dim', $curr->dim]);
$curr = $curr->var;
} elseif ($curr instanceof Node\Expr\PropertyFetch) {
if ($curr->name instanceof Node\Identifier) {
array_unshift($chain, ['prop', $curr->name->toString()]);
$curr = $curr->var;
} else {
return null;
}
}
}

if ($chain === []) {
return null;
}

return [
'root' => $curr,
'chain' => $chain,
];
}

/**
* @param list<array{0: 'dim'|'prop', 1: Node\Expr|string|null}> $chain
*/
private function shouldHandleMemberAssignCheck(string $typeString, array $chain): bool
{
foreach ($chain as $step) {
if ($step[0] === 'dim') {
return true;
}
}

return str_contains($typeString, '{');
}

/**
* @param list<array{0: 'dim'|'prop', 1: Node\Expr|string|null}> $chain
*/
private function wrapMemberAssignCheck(
Node\Expr $valueExpr,
Node\Expr $rootExpr,
array $chain,
string $typeString,
string $varName,
int $line
): Node\Expr\Ternary {
$chainItems = [];
foreach ($chain as $step) {
$kind = new Node\Scalar\String_($step[0]);
if ($step[0] === 'dim') {
$dimExpr = $step[1] instanceof Node\Expr ? $step[1] : new Node\Expr\ConstFetch(new Node\Name('null'));
} elseif (\is_string($step[1])) {
$dimExpr = new Node\Scalar\String_($step[1]);
} elseif ($step[1] instanceof Node\Expr) {
$dimExpr = $step[1];
} else {
$dimExpr = new Node\Scalar\String_('');
}

$chainItems[] = new Node\ArrayItem(
new Node\Expr\Array_([
new Node\ArrayItem($kind),
new Node\ArrayItem($dimExpr),
])
);
}

$chainAst = new Node\Expr\Array_($chainItems);

$checkCall = NodeBuilder::createMemberAssignCheckCall(
$rootExpr,
$chainAst,
$valueExpr,
$typeString,
$varName,
$this->getCurrentCallerExpr(),
$this->getCurrentThisExpr()
);

return NodeBuilder::createTernaryThrowExpr($checkCall, $line);
}

private function wrapVariableCheck(Node\Expr $expr, string $typeString, string $varName, int $line): Node\Expr\Ternary
{
$checkCall = NodeBuilder::createVariableCheckCall(
Expand Down Expand Up @@ -731,6 +931,7 @@ private function wrapClone(Node\Expr\Clone_ $node): ?Node\Expr\FuncCall
private function isSoftScopeBoundary(Node $node): bool
{
return $node instanceof Node\Expr\ArrowFunction
|| $node instanceof Node\Stmt\Block
|| $node instanceof Node\Stmt\If_
|| $node instanceof Node\Stmt\Else_
|| $node instanceof Node\Stmt\ElseIf_
Expand Down
Loading
Loading