Skip to content

Commit c8549d6

Browse files
Extract ExpressionPathAnalyzer class
1 parent 34ce834 commit c8549d6

8 files changed

Lines changed: 548 additions & 398 deletions

src/Visitor/AcpathCalculator.php

Lines changed: 10 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,7 @@
1313
use function count;
1414
use function max;
1515
use PhpParser\Node\Expr;
16-
use PhpParser\Node\Expr\Assign;
17-
use PhpParser\Node\Expr\AssignOp;
18-
use PhpParser\Node\Expr\BinaryOp;
1916
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
20-
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
21-
use PhpParser\Node\Expr\BinaryOp\Coalesce;
22-
use PhpParser\Node\Expr\BinaryOp\LogicalAnd;
23-
use PhpParser\Node\Expr\BinaryOp\LogicalOr;
24-
use PhpParser\Node\Expr\BooleanNot;
25-
use PhpParser\Node\Expr\Cast;
26-
use PhpParser\Node\Expr\Match_;
27-
use PhpParser\Node\Expr\Ternary;
28-
use PhpParser\Node\Expr\UnaryMinus;
29-
use PhpParser\Node\Expr\UnaryPlus;
3017
use PhpParser\Node\Name;
3118
use PhpParser\Node\Stmt;
3219
use PhpParser\Node\Stmt\Block;
@@ -84,7 +71,7 @@ private function statements(array $statements, int $ft, int $st): array
8471
private function statement(Stmt $stmt, int $ft, int $st): array
8572
{
8673
if ($stmt instanceof Expression) {
87-
$p = $this->expressionPaths($stmt->expr)['p'];
74+
$p = ExpressionPathAnalyzer::expressionPaths($stmt->expr)['p'];
8875

8976
return ['ft' => $p * $ft, 'bp' => 0, 'cp' => 0, 'rp' => 0];
9077
}
@@ -94,7 +81,7 @@ private function statement(Stmt $stmt, int $ft, int $st): array
9481
return ['ft' => 0, 'bp' => 0, 'cp' => 0, 'rp' => $ft];
9582
}
9683

97-
$p = $this->expressionPaths($stmt->expr)['p'];
84+
$p = ExpressionPathAnalyzer::expressionPaths($stmt->expr)['p'];
9885

9986
return ['ft' => 0, 'bp' => 0, 'cp' => 0, 'rp' => $p * $ft];
10087
}
@@ -148,7 +135,7 @@ private function statement(Stmt $stmt, int $ft, int $st): array
148135
*/
149136
private function processIf(If_ $stmt, int $ft, int $st): array
150137
{
151-
['t' => $t, 'f' => $f, 'p' => $p] = $this->expressionPaths($stmt->cond);
138+
['t' => $t, 'f' => $f, 'p' => $p] = ExpressionPathAnalyzer::expressionPaths($stmt->cond);
152139

153140
if ($stmt->elseifs !== []) {
154141
// Desugar elseif chains to nested if/else
@@ -200,7 +187,7 @@ private function processIf(If_ $stmt, int $ft, int $st): array
200187
*/
201188
private function processSwitch(Switch_ $stmt, int $ft, int $st): array
202189
{
203-
$p = $this->expressionPaths($stmt->cond)['p'];
190+
$p = ExpressionPathAnalyzer::expressionPaths($stmt->cond)['p'];
204191
$switchSt = $p * $ft;
205192
$hasDefault = false;
206193

@@ -256,8 +243,8 @@ private function processSwitchBody(array $cases, int $ft, int $st): array
256243
*/
257244
private function processWhile(While_ $stmt, int $ft, int $st): array
258245
{
259-
['t' => $t, 'f' => $f, 'p' => $p] = $this->expressionPaths($stmt->cond);
260-
['tf' => $tf] = $this->expressionPathsDouble($stmt->cond);
246+
['t' => $t, 'f' => $f, 'p' => $p] = ExpressionPathAnalyzer::expressionPaths($stmt->cond);
247+
['tf' => $tf] = ExpressionPathAnalyzer::expressionPathsDouble($stmt->cond);
261248
['ft' => $ftS, 'bp' => $bpS, 'cp' => $cpS, 'rp' => $rpS] = $this->statements($stmt->stmts, $ft, $st);
262249

263250
$ftOut = $f * $ft + $bpS * $t + ($ftS + $cpS) * $tf;
@@ -270,7 +257,7 @@ private function processWhile(While_ $stmt, int $ft, int $st): array
270257
*/
271258
private function processDo(Do_ $stmt, int $ft, int $st): array
272259
{
273-
['f' => $f] = $this->expressionPaths($stmt->cond);
260+
['f' => $f] = ExpressionPathAnalyzer::expressionPaths($stmt->cond);
274261
['ft' => $ftS, 'bp' => $bpS, 'cp' => $cpS, 'rp' => $rpS] = $this->statements($stmt->stmts, $ft, $st);
275262

276263
$ftOut = $f * $ftS + $bpS;
@@ -286,7 +273,7 @@ private function processFor(For_ $stmt, int $ft, int $st): array
286273
// Desugar: for(E1; E2; E3) S => E1; while(E2) { S; E3; }
287274
// Process init expressions
288275
foreach ($stmt->init as $initExpr) {
289-
$ft = $this->expressionPaths($initExpr)['p'] * $ft;
276+
$ft = ExpressionPathAnalyzer::expressionPaths($initExpr)['p'] * $ft;
290277
}
291278

292279
// Build the condition: use first cond expression, or treat as leaf if empty
@@ -308,8 +295,8 @@ private function processFor(For_ $stmt, int $ft, int $st): array
308295
$bodyStmts[] = new Expression($loopExpr);
309296
}
310297

311-
['t' => $t, 'f' => $f, 'p' => $p] = $this->expressionPaths($condExpr);
312-
['tf' => $tf] = $this->expressionPathsDouble($condExpr);
298+
['t' => $t, 'f' => $f, 'p' => $p] = ExpressionPathAnalyzer::expressionPaths($condExpr);
299+
['tf' => $tf] = ExpressionPathAnalyzer::expressionPathsDouble($condExpr);
313300
['ft' => $ftS, 'bp' => $bpS, 'cp' => $cpS, 'rp' => $rpS] = $this->statements($bodyStmts, $ft, $st);
314301

315302
$ftOut = $f * $ft + $bpS * $t + ($ftS + $cpS) * $tf;
@@ -362,203 +349,4 @@ private function processTryCatch(TryCatch $stmt, int $ft, int $st): array
362349

363350
return ['ft' => $ftOut, 'bp' => $bp, 'cp' => $cp, 'rp' => $rp];
364351
}
365-
366-
/**
367-
* Returns {t, f, p} for an expression.
368-
*
369-
* t = paths that evaluate to true
370-
* f = paths that evaluate to false
371-
* p = total paths (t + f for boolean, or just paths for non-boolean)
372-
*
373-
* @return array{t: int, f: int, p: int}
374-
*/
375-
private function expressionPaths(Expr $expr): array
376-
{
377-
if ($expr instanceof BooleanNot) {
378-
['t' => $t, 'f' => $f, 'p' => $p] = $this->expressionPaths($expr->expr);
379-
380-
return ['t' => $f, 'f' => $t, 'p' => $p];
381-
}
382-
383-
if ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
384-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->left);
385-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->right);
386-
387-
return [
388-
't' => $t1 * $t2,
389-
'f' => $f1 + $t1 * $f2,
390-
'p' => $f1 + $t1 * $p2,
391-
];
392-
}
393-
394-
if ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
395-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->left);
396-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->right);
397-
398-
return [
399-
't' => $t1 + $f1 * $t2,
400-
'f' => $f1 * $f2,
401-
'p' => $t1 + $f1 * $p2,
402-
];
403-
}
404-
405-
if ($expr instanceof Ternary) {
406-
if ($expr->if === null) {
407-
// Elvis operator (E1 ?: E2) - same as ||
408-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->cond);
409-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->else);
410-
411-
return [
412-
't' => $t1 + $f1 * $t2,
413-
'f' => $f1 * $f2,
414-
'p' => $t1 + $f1 * $p2,
415-
];
416-
}
417-
418-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->cond);
419-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->if);
420-
['t' => $t3, 'f' => $f3, 'p' => $p3] = $this->expressionPaths($expr->else);
421-
422-
return [
423-
't' => $t1 * $t2 + $f1 * $t3,
424-
'f' => $t1 * $f2 + $f1 * $f3,
425-
'p' => $t1 * $p2 + $f1 * $p3,
426-
];
427-
}
428-
429-
if ($expr instanceof Coalesce) {
430-
// Same as ||
431-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->left);
432-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->right);
433-
434-
return [
435-
't' => $t1 + $f1 * $t2,
436-
'f' => $f1 * $f2,
437-
'p' => $t1 + $f1 * $p2,
438-
];
439-
}
440-
441-
if ($expr instanceof Match_) {
442-
$totalP = 0;
443-
444-
foreach ($expr->arms as $arm) {
445-
if ($arm->conds !== null) {
446-
foreach ($arm->conds as $cond) {
447-
$totalP += $this->expressionPaths($cond)['p'];
448-
}
449-
}
450-
451-
$totalP += $this->expressionPaths($arm->body)['p'];
452-
}
453-
454-
return ['t' => $totalP, 'f' => $totalP, 'p' => $totalP];
455-
}
456-
457-
if ($expr instanceof Assign || $expr instanceof AssignOp) {
458-
$pVar = $this->expressionPaths($expr->var)['p'];
459-
$pExpr = $this->expressionPaths($expr->expr)['p'];
460-
$p = $pVar * $pExpr;
461-
462-
return ['t' => $p, 'f' => $p, 'p' => $p];
463-
}
464-
465-
if ($expr instanceof BinaryOp) {
466-
$p1 = $this->expressionPaths($expr->left)['p'];
467-
$p2 = $this->expressionPaths($expr->right)['p'];
468-
$p = $p1 * $p2;
469-
470-
return ['t' => $p, 'f' => $p, 'p' => $p];
471-
}
472-
473-
if ($expr instanceof Cast || $expr instanceof UnaryMinus || $expr instanceof UnaryPlus) {
474-
$p = $this->expressionPaths($expr->expr)['p'];
475-
476-
return ['t' => $p, 'f' => $p, 'p' => $p];
477-
}
478-
479-
// Leaf expression
480-
return ['t' => 1, 'f' => 1, 'p' => 1];
481-
}
482-
483-
/**
484-
* Returns {tt, tf, ff, pp} for double-traversal path counting.
485-
*
486-
* Needed for while/do-while/foreach loop conditions.
487-
*
488-
* @return array{tt: int, tf: int, ff: int, pp: int}
489-
*/
490-
private function expressionPathsDouble(Expr $expr): array
491-
{
492-
if ($expr instanceof BooleanNot) {
493-
['tt' => $tt, 'tf' => $tf, 'ff' => $ff, 'pp' => $pp] = $this->expressionPathsDouble($expr->expr);
494-
495-
return ['tt' => $ff, 'tf' => $tf, 'ff' => $tt, 'pp' => $pp];
496-
}
497-
498-
if ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
499-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->left);
500-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->right);
501-
['tt' => $tt1, 'tf' => $tf1, 'ff' => $ff1, 'pp' => $pp1] = $this->expressionPathsDouble($expr->left);
502-
['tt' => $tt2, 'tf' => $tf2, 'ff' => $ff2, 'pp' => $pp2] = $this->expressionPathsDouble($expr->right);
503-
504-
return [
505-
'tt' => $tt1 * $tt2,
506-
'tf' => $tf1 * $t2 + $tt1 * $tf2,
507-
'ff' => $ff1 + 2 * $tf1 * $f2 + $tt1 * $ff2,
508-
'pp' => $ff1 + 2 * $tf1 * $p2 + $tt1 * $pp2,
509-
];
510-
}
511-
512-
if ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
513-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->left);
514-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->right);
515-
['tt' => $tt1, 'tf' => $tf1, 'ff' => $ff1, 'pp' => $pp1] = $this->expressionPathsDouble($expr->left);
516-
['tt' => $tt2, 'tf' => $tf2, 'ff' => $ff2, 'pp' => $pp2] = $this->expressionPathsDouble($expr->right);
517-
518-
return [
519-
'tt' => $tt1 + 2 * $tf1 * $t2 + $ff1 * $tt2,
520-
'tf' => $tf1 * $f2 + $ff1 * $tf2,
521-
'ff' => $ff1 * $ff2,
522-
'pp' => $tt1 + 2 * $tf1 * $p2 + $ff1 * $pp2,
523-
];
524-
}
525-
526-
if ($expr instanceof Ternary) {
527-
if ($expr->if === null) {
528-
// Elvis operator - same as ||
529-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->cond);
530-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->else);
531-
['tt' => $tt1, 'tf' => $tf1, 'ff' => $ff1, 'pp' => $pp1] = $this->expressionPathsDouble($expr->cond);
532-
['tt' => $tt2, 'tf' => $tf2, 'ff' => $ff2, 'pp' => $pp2] = $this->expressionPathsDouble($expr->else);
533-
534-
return [
535-
'tt' => $tt1 + 2 * $tf1 * $t2 + $ff1 * $tt2,
536-
'tf' => $tf1 * $f2 + $ff1 * $tf2,
537-
'ff' => $ff1 * $ff2,
538-
'pp' => $tt1 + 2 * $tf1 * $p2 + $ff1 * $pp2,
539-
];
540-
}
541-
542-
['t' => $t1, 'f' => $f1, 'p' => $p1] = $this->expressionPaths($expr->cond);
543-
['t' => $t2, 'f' => $f2, 'p' => $p2] = $this->expressionPaths($expr->if);
544-
['t' => $t3, 'f' => $f3, 'p' => $p3] = $this->expressionPaths($expr->else);
545-
['tt' => $tt1, 'tf' => $tf1, 'ff' => $ff1, 'pp' => $pp1] = $this->expressionPathsDouble($expr->cond);
546-
['tt' => $tt2, 'tf' => $tf2, 'ff' => $ff2, 'pp' => $pp2] = $this->expressionPathsDouble($expr->if);
547-
['tt' => $tt3, 'tf' => $tf3, 'ff' => $ff3, 'pp' => $pp3] = $this->expressionPathsDouble($expr->else);
548-
549-
return [
550-
'tt' => $tt1 * $tt2 + 2 * $tf1 * $t2 * $t3 + $ff1 * $tt3,
551-
'tf' => $tt1 * $tf2 + $ff1 * $tf3 + $tf1 * ($t2 * $f3 + $f2 * $t3),
552-
'ff' => $tt1 * $ff2 + 2 * $tf1 * $f2 * $f3 + $ff1 * $ff3,
553-
'pp' => $tt1 * $pp2 + 2 * $tf1 * $p2 * $p3 + $ff1 * $pp3,
554-
];
555-
}
556-
557-
// For non-boolean expressions (comparisons, arithmetic, assignments, etc.),
558-
// use single-traversal path count as tf. This correctly handles comparisons
559-
// like $i < 10 used as while conditions.
560-
$p = $this->expressionPaths($expr)['p'];
561-
562-
return ['tt' => 0, 'tf' => $p, 'ff' => 0, 'pp' => 0];
563-
}
564352
}

0 commit comments

Comments
 (0)