diff --git a/src/CRM/CivixBundle/Application.php b/src/CRM/CivixBundle/Application.php index 3adf9436..194846c2 100644 --- a/src/CRM/CivixBundle/Application.php +++ b/src/CRM/CivixBundle/Application.php @@ -51,6 +51,7 @@ public function createCommands($context = 'default') { $commands[] = new Command\UpgradeCommand(); $commands[] = new Command\InfoGetCommand(); $commands[] = new Command\InfoSetCommand(); + $commands[] = new Command\InspectFunctionCommand(); return $commands; } diff --git a/src/CRM/CivixBundle/Command/InspectFunctionCommand.php b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php new file mode 100644 index 00000000..511b58fc --- /dev/null +++ b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php @@ -0,0 +1,144 @@ +setName('inspect:fun') + ->setDescription('Search codebase for functions') + ->addOption('name', NULL, InputOption::VALUE_REQUIRED, 'Pattern describing the function-names you wnt to see') + ->addOption('body', NULL, InputOption::VALUE_REQUIRED, 'Pattern describing function bodies that you want to see') + ->addOption('files-with-matches', 'l', InputOption::VALUE_NONE, 'Print only file names') + ->addArgument('files', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'List of files') + ->setHelp('Search PHP functions + +Example: Find all functions named like "_civicrm_permission" + civix inspect:fun --name=/_civicrm_permission/ *.php + +Example: Find all functions which call civicrm_api3() + civix inspect:fun --body=/civicrm_api3/ *.php + +Example: Find all functions named like "_civicrm_permission" AND having a body with "label" + civix inspect:fun --name=/_civicrm_permission/ --body=/label/ *.php +'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $functionNamePattern = $input->getOption('name'); + if ($functionNamePattern) { + $this->assertRegex($functionNamePattern, '--name'); + } + $bodyPattern = $input->getOption('body'); + if ($bodyPattern) { + $this->assertRegex($bodyPattern, '--body'); + } + $printer = [$this, 'printMatch']; + if ($input->getOption('files-with-matches')) { + $printer = [$this, 'printFileName']; + } + + foreach ($input->getArgument('files') as $file) { + if ($output->isVeryVerbose()) { + $output->writeln("## SCAN FILE: $file"); + } + + $fileContent = file_get_contents($file); + PrimitiveFunctionVisitor::visit($fileContent, function (?string &$functionName, string &$signature, string &$body) use ($bodyPattern, $functionNamePattern, $file, $input, $printer) { + if ($functionNamePattern && !preg_match($functionNamePattern, $functionName)) { + return; + } + if ($bodyPattern && !preg_match($bodyPattern, $body)) { + return; + } + + $printer($file, $functionName, $signature, $body, $bodyPattern); + }); + } + + return 0; + } + + protected function printFileName($file, ?string $functionName, string $signature, string $code, $codePattern) { + Civix::output()->writeln($file, OutputInterface::OUTPUT_RAW); + } + + protected function printMatch($file, ?string $functionName, string $signature, string $body, $bodyPattern): void { + Civix::output()->writeln(sprintf("## FILE: %s", $file)); + Civix::output()->write(sprintf("function %s(%s) {", $functionName, $signature)); + if (!$bodyPattern) { + Civix::output()->write($body, FALSE, OutputInterface::OUTPUT_RAW); + } + else { + $hiParts = $this->splitHighlights($body, $bodyPattern); + foreach ($hiParts as $part) { + Civix::output()->write($part[0], FALSE, $part[1]); + } + } + Civix::output()->write("}\n\n"); + } + + /** + * Split a block of $code into highlighted and non-highlighted sections. + * + * @param string $code + * The code to search/highlight + * @param string $hi + * Regex identifying the expressions to highlight + * @return array + */ + protected function splitHighlights(string $code, $hi): array { + $buf = $code; + $delimQuot = preg_quote($hi[0], ';'); + if (preg_match(';' . $delimQuot . '([a-zA-Z]+)$;', $hi, $m)) { + $modifiers = $m[1]; + $hi = substr($hi, 0, -1 * strlen($modifiers)); + } + else { + $modifiers = ''; + } + + $hiPat = $hi[0] . '^(.*)(' . substr($hi, 1, -1) . ')' . $hi[0] . 'msU' . $modifiers; + $hiParts = []; + while (!empty($buf)) { + if (preg_match($hiPat, $buf, $matches)) { + $hiParts[] = [$matches[1], OutputInterface::OUTPUT_RAW]; + $hiParts[] = ['' . $matches[2] . '', OutputInterface::OUTPUT_NORMAL]; + $buf = substr($buf, strlen($matches[0])); + } + else { + $hiParts[] = [$buf, OutputInterface::OUTPUT_RAW]; + $buf = NULL; + } + } + return $hiParts; + } + + /** + * Assert that $regex is a plausible-looking regular expression. + * + * @param string $regex + * @param string $regexOption + */ + protected function assertRegex(string $regex, string $regexOption): void { + $delim = $regex[0]; + $delimQuote = preg_quote($delim, ';'); + $allowDelim = '/|:;,.#'; + if (strpos($allowDelim, $delim) === FALSE) { + throw new \Exception("Option \"$regexOption\" should have a symbolic delimiter, such as: $allowDelim"); + } + if (!preg_match(';^' . $delimQuote . '.*' . $delimQuote . '[a-zA-Z]*$;', $regex)) { + throw new \Exception("Option \"$regexOption\" should be well-formed"); + } + } + +}