Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ vendor/
.idea/
.phpunit.result.cache
.php-cs-fixer.cache
coverage
7 changes: 7 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
->name('.php_cs')
->exclude('vendor')
->exclude('.git')
->exclude('coverage')
->in(__DIR__);

return (new PhpCsFixer\Config())
Expand All @@ -23,5 +24,11 @@
'comment_type' => 'PHPDoc',
'header' => $header,
],
'fully_qualified_strict_types' => true,
'global_namespace_import' => true,
'no_unused_imports' => true,
'ordered_imports' => true,
'single_line_after_imports' => true,
'single_import_per_statement' => true,
])
->setFinder($finder);
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
SHELL := /bin/bash

targets=$(shell for file in `find . -name '*Test.php' -type f -printf "%P\n"`; do echo "$$file "; done;)


.PHONY: build
build: cs-fix test

Expand All @@ -7,6 +12,12 @@ cs-fix:

.PHONY: test
test:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html ./coverage --display-deprecations

.PHONY: t
t:

all: $(targets)

%Test.php: t
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --display-deprecations $@
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ To dump the graph, simply use the built-in dumper:
$dumper = new PHPCfg\Printer\Text();
echo $dumper->printScript($script);
```

## CLI

You can leverage the CLI binary to generate debug traces of the CFG for any file, or for printing GraphViz visualizations.

```shell
bin/php-cfg dot -o output.dot path/to/file.php
```
13 changes: 13 additions & 0 deletions bin/php-cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env php
<?php
require_once(__DIR__ . '/../vendor/autoload.php');

// Init App with name and version
$app = new Ahc\Cli\Application('PHP-CFG', 'v0.0.1');

$app->add(new PHPCfg\Cli\PrintCommand, 'p');
$app->add(new PHPCfg\Cli\DotCommand, 'd');

$app->add(new PHPCfg\Cli\RunTestCommand);

$app->handle($_SERVER['argv']); // if argv[1] is `i` or `init` it executes InitCommand
11 changes: 8 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
"require": {
"php": ">=8.1",
"nikic/php-parser": "^5.0",
"phpdocumentor/graphviz": "^1.0.4"
"phpdocumentor/graphviz": "^1.0.4",
"adhocore/cli": "^v1.0.0"
},
"require-dev": {
"phpunit/phpunit": ">=10.0",
"friendsofphp/php-cs-fixer": ">=3.86"
},
"autoload": {
"psr-4": {
"PHPCfg\\": "lib/PHPCfg/"
"PHPCfg\\": "lib/PHPCfg/",
"PHPCfg\\Cli\\": "src/Cli"
}
}
},
"bin": [
"bin/php-cfg"
]
}
60 changes: 0 additions & 60 deletions demo.php

This file was deleted.

24 changes: 15 additions & 9 deletions lib/PHPCfg/AbstractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@
*/
abstract class AbstractVisitor implements Visitor
{
public function enterScript(Script $script) {}
public function enterScript(Script $script): void {}

public function leaveScript(Script $script) {}
public function leaveScript(Script $script): void {}

public function enterFunc(Func $func) {}
public function enterFunc(Func $func): void {}

public function leaveFunc(Func $func) {}
public function leaveFunc(Func $func): void {}

public function enterBlock(Block $block, ?Block $prior = null) {}
public function enterBlock(Block $block, ?Block $prior = null): void {}

public function leaveBlock(Block $block, ?Block $prior = null) {}
public function leaveBlock(Block $block, ?Block $prior = null): Block|int|null
{
return null;
}

public function skipBlock(Block $block, ?Block $prior = null) {}
public function skipBlock(Block $block, ?Block $prior = null): void {}

public function enterOp(Op $op, Block $block) {}
public function enterOp(Op $op, Block $block): void {}

public function leaveOp(Op $op, Block $block) {}
public function leaveOp(Op $op, Block $block): Op|int|null
{
return null;
}
}
38 changes: 24 additions & 14 deletions lib/PHPCfg/Assertion.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace PHPCfg;

use RuntimeException;

class Assertion
{
public const MODE_NONE = 0;
Expand All @@ -19,41 +21,49 @@ class Assertion

public const MODE_INTERSECTION = 2;

public $mode = self::MODE_NONE;
public readonly int $mode;

/**
* @var Assertion[]|Operand
*/
public $value;
public readonly array|Operand $value;

/**
* @param Assertion[]|Operand $value
*/
public function __construct($value, $mode = self::MODE_NONE)
public function __construct(array|Operand $value, $mode = self::MODE_NONE)
{
if (empty($value)) {
throw new \RuntimeException('Empty value supplied for Assertion');
throw new RuntimeException('Empty value supplied for Assertion');
}
if (is_array($value)) {
foreach ($value as $v) {
if (! $v instanceof self) {
throw new \RuntimeException('Invalid array key supplied for Assertion');
throw new RuntimeException('Invalid array key supplied for Assertion');
}
}
if ($mode !== self::MODE_UNION && $mode !== self::MODE_INTERSECTION) {
throw new \RuntimeException('Invalid mode supplied for Assertion');
throw new RuntimeException('Invalid mode supplied for Assertion');
}
$this->mode = $mode;
} elseif (! $value instanceof Operand) {
throw new \RuntimeException('Invalid value supplied for Assertion: ');
$this->setMode($mode);
} else {
$this->mode = self::MODE_NONE;
$this->setMode(self::MODE_NONE);
}
$this->value = $value;
}

public function getKind()
public function getKind(): string
{
return '';
}

protected function setMode(int $mode): void
{
switch ($mode) {
case self::MODE_NONE:
case self::MODE_UNION:
case self::MODE_INTERSECTION:
break;
default:
throw new RuntimeException("Invalid mode supplied for Assertion");
}
$this->mode = $mode;
}
}
2 changes: 1 addition & 1 deletion lib/PHPCfg/Assertion/NegatedAssertion.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function __construct($value)
parent::__construct($value, self::MODE_INTERSECTION);
}

public function getKind()
public function getKind(): string
{
return 'not';
}
Expand Down
2 changes: 1 addition & 1 deletion lib/PHPCfg/Assertion/TypeAssertion.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class TypeAssertion extends Assertion
{
public function getKind()
public function getKind(): string
{
return 'type';
}
Expand Down
20 changes: 8 additions & 12 deletions lib/PHPCfg/AstVisitor/LoopResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace PHPCfg\AstVisitor;

use LogicException;
use PhpParser\Node;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Goto_;
Expand All @@ -19,11 +20,11 @@

class LoopResolver extends NodeVisitorAbstract
{
protected static $labelCounter = 0;
protected int $labelCounter = 0;

protected $continueStack = [];
protected array $continueStack = [];

protected $breakStack = [];
protected array $breakStack = [];

public function enterNode(Node $node)
{
Expand All @@ -36,7 +37,6 @@ public function enterNode(Node $node)
$lbl = $this->makeLabel();
$this->breakStack[] = $lbl;
$this->continueStack[] = $lbl;

break;
case 'Stmt_Do':
case 'Stmt_While':
Expand All @@ -57,11 +57,8 @@ public function leaveNode(Node $node)
case 'Stmt_For':
case 'Stmt_Foreach':
$node->stmts[] = new Label(array_pop($this->continueStack));

return [$node, new Label(array_pop($this->breakStack))];
case 'Stmt_Switch':
array_pop($this->continueStack);

return [$node, new Label(array_pop($this->breakStack))];
}
}
Expand All @@ -74,18 +71,17 @@ protected function resolveStack(Node $node, array $stack)
if ($node->num instanceof LNumber) {
$num = $node->num->value - 1;
if ($num >= count($stack)) {
throw new \LogicException('Too high of a count for ' . $node->getType());
throw new LogicException('Too high of a count for ' . $node->getType());
}
$loc = array_slice($stack, -1 * $num, 1);

$loc = array_slice($stack, -1 * $num - 1, 1);
return new Goto_($loc[0], $node->getAttributes());
}

throw new \LogicException('Unimplemented Node Value Type');
throw new LogicException('Unimplemented Node Value Type');
}

protected function makeLabel()
{
return 'compiled_label_' . mt_rand(0, mt_getrandmax()) . '_' . self::$labelCounter++;
return 'compiled_label_' . mt_rand(0, mt_getrandmax()) . '_' . $this->labelCounter++;
}
}
Loading