Skip to content

Commit

Permalink
Merge pull request #383 from totten/master-efv2-hook
Browse files Browse the repository at this point in the history
EFv2 - When upgrading from EFv1, check for conflicting hooks
  • Loading branch information
totten authored Jan 21, 2025
2 parents 95b9357 + 68793a9 commit e9d3ee2
Show file tree
Hide file tree
Showing 5 changed files with 543 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/CRM/CivixBundle/Checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace CRM\CivixBundle;

use CRM\CivixBundle\Builder\Mixins;

/**
* This is a random grab-bag of conditionals that are show up when deciding how to generate code.
*/
Expand Down Expand Up @@ -73,6 +75,18 @@ public function hasUpgrader(string $pattern = '/.+/'): bool {
return $upgrader && preg_match($pattern, $upgrader);
}

/**
* @param string $pattern
* Regex.
* @return bool
* TRUE if any mixin constraint matches the regex.
*/
public function hasMixin(string $pattern = '/.+/'): bool {
$mixins = new Mixins($this->generator->infoXml, $this->generator->baseDir->string('mixin'));
$declared = $mixins->getDeclaredMixinConstraints();
return !empty(preg_grep($pattern, $declared));
}

/**
* Determine if this extension bundles-in a mixin-library.
*
Expand Down
165 changes: 165 additions & 0 deletions src/CRM/CivixBundle/Parse/PrimitiveFunctionVisitor.php
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 src/CRM/CivixBundle/Parse/PrimitiveFunctionVisitorTest.php
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);
}

}
Loading

0 comments on commit e9d3ee2

Please sign in to comment.