-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #383 from totten/master-efv2-hook
EFv2 - When upgrading from EFv1, check for conflicting hooks
- Loading branch information
Showing
5 changed files
with
543 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
<?php | ||
|
||
namespace CRM\CivixBundle\Parse; | ||
|
||
/** | ||
* Given a PHP file, visit all functions in the file. Use this for primitive | ||
* checks ("does the function do anything") or primitive manipulations | ||
* ("add a new line, or rename the whole thing"). | ||
* | ||
* ## Example 1: Print out a list functions | ||
* | ||
* PrimitiveFunctionVisitor::visit(file_get_contents('foo.php', function(string $function, string $signature, string $code) { | ||
* printf("FOUND: function %s(%s) {...}\n", $function, $signature); | ||
* }); | ||
* | ||
* ## Example 2: Rename a function from 'foo' to 'bar' | ||
* | ||
* $updated = PrimitiveFunctionVisitor::visit(file_get_contents('foo.php', function(string &$function, string &$signature, string &$code) { | ||
* if ($function === 'foo') { | ||
* $function = 'bar'; | ||
* } | ||
* }); | ||
* | ||
* ## Example 3: Delete any functions with evil names | ||
* | ||
* $updated= PrimitiveFunctionVisitor::visit(file_get_contents('foo.php', function(string $function, string $signature, string $code) { | ||
* return (str_contains($function, 'evil')') ? 'DELETE' : NULL; | ||
* }); | ||
*/ | ||
class PrimitiveFunctionVisitor { | ||
|
||
/** | ||
* @var Token[] | ||
*/ | ||
private $tokens; | ||
private $filter; | ||
private $currentIndex = 0; | ||
|
||
/** | ||
* Parse a PHP script and visit all the function() declarations in the main script. | ||
* | ||
* @param string $code | ||
* Fully formed PHP code | ||
* @param callable $filter | ||
* This callback is executed against each function from the main-script. | ||
* | ||
* function(?string &$functionName, string &$signature, string &$code): ?string | ||
* | ||
* Note that inputs are all alterable. Additionally, the result may optionally specify | ||
* an action to perform on the overall function ('DELETE', 'COMMENT'). | ||
* | ||
* If the main-script has an anonymous function (long-form: `$f = function (...) use (...) { ... }`), | ||
* then it will be recognized with NULL name. However, short-form `fn()` is not. | ||
* | ||
* @return string | ||
*/ | ||
public static function visit(string $code, callable $filter): string { | ||
$instance = new self($code, $filter); | ||
return $instance->run(); | ||
} | ||
|
||
public function __construct(string $code, callable $filter) { | ||
$this->tokens = Token::tokenize($code); | ||
$this->filter = $filter; | ||
} | ||
|
||
public function run(): string { | ||
$output = ''; | ||
|
||
while (($peek = $this->peek()) !== NULL) { | ||
if ($peek->is(T_FUNCTION)) { | ||
$output .= $this->parseFunction(); | ||
} | ||
else { | ||
$output .= $this->consume()->value(); | ||
} | ||
} | ||
return $output; | ||
} | ||
|
||
private function parseFunction(): string { | ||
$this->consume()->assert(T_FUNCTION); | ||
|
||
$pad0 = $this->fastForward([T_STRING, '(']); | ||
if ($this->peek()->is(T_STRING)) { | ||
$function = $this->consume()->value(); | ||
} | ||
else { | ||
$function = NULL; | ||
} | ||
|
||
$pad1 = $this->fastForward('('); | ||
$signature = $this->parseSection('(', ')'); | ||
|
||
$pad2 = $this->fastForward('{'); | ||
$codeBlock = $this->parseSection('{', '}'); | ||
|
||
$result = ($this->filter)($function, $signature, $codeBlock); | ||
|
||
if ($result === 'DELETE') { | ||
return ''; | ||
} | ||
elseif ($result === 'COMMENT') { | ||
$code = 'function' . $pad0 . $function . $pad1 . '(' . $signature . ')' . $pad2 . '{' . $codeBlock . '}'; | ||
return "\n" . implode("\n", array_map( | ||
function($line) { | ||
return "// $line"; | ||
}, | ||
explode("\n", $code) | ||
)) . "\n"; | ||
} | ||
elseif ($function == NULL) { | ||
return 'function' . $pad0 . $pad1 . '(' . $signature . ')' . $pad2 . '{' . $codeBlock . '}'; | ||
} | ||
else { | ||
return 'function' . $pad0 . $function . $pad1 . '(' . $signature . ')' . $pad2 . '{' . $codeBlock . '}'; | ||
} | ||
} | ||
|
||
private function parseSection(string $openChar, string $closeChar): string { | ||
$this->consume()->assert($openChar); | ||
$depth = 1; | ||
$section = ''; | ||
|
||
while (($token = $this->consume()) !== NULL) { | ||
if ($token->is($closeChar)) { | ||
$depth--; | ||
if ($depth === 0) { | ||
break; | ||
} | ||
} | ||
$section .= $token->value(); | ||
if ($token->is($openChar)) { | ||
$depth++; | ||
} | ||
} | ||
return $section; | ||
} | ||
|
||
private function consume(): ?Token { | ||
if ($this->currentIndex < count($this->tokens)) { | ||
return $this->tokens[$this->currentIndex++]; | ||
} | ||
return NULL; | ||
} | ||
|
||
private function peek(): ?Token { | ||
return $this->tokens[$this->currentIndex] ?? NULL; | ||
} | ||
|
||
private function fastForward($expectedToken): string { | ||
$output = ''; | ||
while (($token = $this->peek()) !== NULL) { | ||
if ($token->is($expectedToken)) { | ||
break; | ||
} | ||
else { | ||
$output .= $token->value(); | ||
} | ||
$this->consume(); | ||
} | ||
return $output; | ||
} | ||
|
||
} |
164 changes: 164 additions & 0 deletions
164
src/CRM/CivixBundle/Parse/PrimitiveFunctionVisitorTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<?php | ||
|
||
namespace CRM\CivixBundle\Parse; | ||
|
||
/** | ||
* @group unit | ||
*/ | ||
class PrimitiveFunctionVisitorTest extends \PHPUnit\Framework\TestCase { | ||
|
||
protected function getBasicFile(): string { | ||
$code = '<' . '?php '; | ||
$code .= "// Woop\n"; | ||
$code .= "// Woop\n"; | ||
$code .= "function first() { echo 1; }\n"; | ||
$code .= "/**\n * Woop\n */\n"; | ||
$code .= " function second(array ?\$xs = []) { echo 2; }\n"; | ||
// TODO Nested block | ||
$code .= "if (foo( 'bar' )) { function third(\$a, \$b, \$c) { echo 3; } }\n"; | ||
return $code; | ||
} | ||
|
||
protected function getComplexFile(): string { | ||
$code = '<' . '?php '; | ||
$code .= "// Complex\n"; | ||
$code .= '$anon = function(int $a) { return 100; };'; | ||
$code .= '$bufA = [];'; | ||
$code .= '(new Stuff())->do(wrapping(200, function($x, $y) use ($buf) {'; | ||
$code .= ' if ($y) {'; | ||
$code .= ' if (time()) {'; | ||
$code .= ' return $x;'; | ||
$code .= ' }'; | ||
$code .= ' else {}'; | ||
$code .= ' }'; | ||
$code .= ' else { womp(); womp(); }'; | ||
$code .= '}));'; | ||
$code .= 'function zero($a) { /* nullop */ };'; | ||
$code .= '?>'; | ||
$code .= $this->getBasicFile(); | ||
$code .= 'function finale($z) {}'; | ||
return $code; | ||
} | ||
|
||
/** | ||
* Handy for interactive debugging... | ||
*/ | ||
public function testTiny(): void { | ||
$code = '<' . '?php '; | ||
$code .= "function single(\$a) { return 'bar'; }"; | ||
PrimitiveFunctionVisitor::visit($code, function (&$func, &$sig, &$code) use (&$visited) { | ||
$visited[] = ['func' => $func, 'sig' => $sig, 'code' => $code]; | ||
}); | ||
$expected = []; | ||
$expected[] = ['func' => 'single', 'sig' => '$a', 'code' => ' return \'bar\'; ']; | ||
$this->assertEquals($expected, $visited); | ||
} | ||
|
||
public function testBasicFileVisitOrder(): void { | ||
$input = $this->getBasicFile(); | ||
|
||
$visited = []; | ||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) use (&$visited) { | ||
$visited[] = ['func' => $func, 'sig' => $sig, 'code' => $code]; | ||
}); | ||
$expected = []; | ||
$expected[] = ['func' => 'first', 'sig' => '', 'code' => ' echo 1; ']; | ||
$expected[] = ['func' => 'second', 'sig' => 'array ?$xs = []', 'code' => ' echo 2; ']; | ||
$expected[] = ['func' => 'third', 'sig' => '$a, $b, $c', 'code' => ' echo 3; ']; | ||
$this->assertEquals($expected, $visited); | ||
|
||
$this->assertEquals($input, $output); | ||
} | ||
|
||
public function testBasicFileInsertions(): void { | ||
$input = $this->getBasicFile(); | ||
|
||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) { | ||
$func .= 'Func'; | ||
if (!empty($sig)) { | ||
$sig .= ', '; | ||
} | ||
$sig .= 'int $id = 0'; | ||
$code .= 'echo "00"; '; | ||
}); | ||
|
||
$expect = '<' . '?php '; | ||
$expect .= "// Woop\n"; | ||
$expect .= "// Woop\n"; | ||
$expect .= "function firstFunc(int \$id = 0) { echo 1; echo \"00\"; }\n"; | ||
$expect .= "/**\n * Woop\n */\n"; | ||
$expect .= " function secondFunc(array ?\$xs = [], int \$id = 0) { echo 2; echo \"00\"; }\n"; | ||
$expect .= "if (foo( 'bar' )) { function thirdFunc(\$a, \$b, \$c, int \$id = 0) { echo 3; echo \"00\"; } }\n"; | ||
$this->assertEquals($expect, $output); | ||
} | ||
|
||
public function testBasicFileDeletion(): void { | ||
$input = $this->getBasicFile(); | ||
|
||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) { | ||
return ($func === 'second') ? 'DELETE' : NULL; | ||
}); | ||
|
||
$expect = '<' . '?php '; | ||
$expect .= "// Woop\n"; | ||
$expect .= "// Woop\n"; | ||
$expect .= "function first() { echo 1; }\n"; | ||
$expect .= "/**\n * Woop\n */\n"; | ||
$expect .= " \n"; | ||
$expect .= "if (foo( 'bar' )) { function third(\$a, \$b, \$c) { echo 3; } }\n"; | ||
$this->assertEquals($expect, $output); | ||
} | ||
|
||
public function testBasicFileComment(): void { | ||
$input = $this->getBasicFile(); | ||
|
||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) { | ||
return ($func === 'third') ? 'COMMENT' : NULL; | ||
}); | ||
|
||
$expect = '<' . '?php '; | ||
$expect .= "// Woop\n"; | ||
$expect .= "// Woop\n"; | ||
$expect .= "function first() { echo 1; }\n"; | ||
$expect .= "/**\n * Woop\n */\n"; | ||
$expect .= " function second(array ?\$xs = []) { echo 2; }\n"; | ||
$expect .= "if (foo( 'bar' )) { \n"; | ||
$expect .= "// function third(\$a, \$b, \$c) { echo 3; }\n"; | ||
$expect .= " }\n"; | ||
$this->assertEquals($expect, $output); | ||
} | ||
|
||
public function testMinSpacing(): void { | ||
$input = '<' . '?php '; | ||
$input .= "function first(){echo 1;}"; | ||
$input .= "if(foo()){function second(){echo 3;}}"; | ||
|
||
$visited = []; | ||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) use (&$visited) { | ||
$visited[] = $func; | ||
}); | ||
$this->assertEquals(['first', 'second'], $visited); | ||
$this->assertEquals($input, $output); | ||
} | ||
|
||
public function testComplexFileVisitOrder(): void { | ||
$input = $this->getComplexFile(); | ||
|
||
$visited = []; | ||
$output = PrimitiveFunctionVisitor::visit($input, function (&$func, &$sig, &$code) use (&$visited) { | ||
$visited[] = ['func' => $func, 'sig' => $sig, 'code' => $code]; | ||
}); | ||
$expected = []; | ||
$expected[] = ['func' => NULL, 'sig' => 'int $a', 'code' => ' return 100; ']; | ||
$expected[] = ['func' => NULL, 'sig' => '$x, $y', 'code' => ' if ($y) { if (time()) { return $x; } else {} } else { womp(); womp(); }']; | ||
$expected[] = ['func' => 'zero', 'sig' => '$a', 'code' => ' /* nullop */ ']; | ||
$expected[] = ['func' => 'first', 'sig' => '', 'code' => ' echo 1; ']; | ||
$expected[] = ['func' => 'second', 'sig' => 'array ?$xs = []', 'code' => ' echo 2; ']; | ||
$expected[] = ['func' => 'third', 'sig' => '$a, $b, $c', 'code' => ' echo 3; ']; | ||
$expected[] = ['func' => 'finale', 'sig' => '$z', 'code' => '']; | ||
$this->assertEquals($expected, $visited); | ||
|
||
$this->assertEquals($input, $output); | ||
} | ||
|
||
} |
Oops, something went wrong.