From 360154fe649d5baad63d5be83b5d292cfced133b Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 21 Feb 2025 15:25:52 -0800 Subject: [PATCH 1/3] civix inspect:fun -- Add command for searching functions --- src/CRM/CivixBundle/Application.php | 1 + .../Command/InspectFunctionCommand.php | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/CRM/CivixBundle/Command/InspectFunctionCommand.php 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..5a108c2e --- /dev/null +++ b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php @@ -0,0 +1,109 @@ +setName('inspect:fun') + ->setDescription('Search codebase for functions') + ->addOption('name', NULL, InputOption::VALUE_REQUIRED, 'Pattern describing the function-names you wnt to see') + ->addOption('code', 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 --code=/civicrm_api3/ *.php + +Example: Find all functions matching the name and code patterns + civix inspect:fun --name=/_civicrm_permission/ --code=/label/ *.php +'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $functionNamePattern = $input->getOption('name'); + $codePattern = $input->getOption('code'); + $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"); + } + + $code = file_get_contents($file); + PrimitiveFunctionVisitor::visit($code, function (?string &$functionName, string &$signature, string &$code) use ($codePattern, $functionNamePattern, $file, $input, $printer) { + if ($functionNamePattern && !preg_match($functionNamePattern, $functionName)) { + return; + } + if ($codePattern && !preg_match($codePattern, $code)) { + return; + } + + $printer($file, $functionName, $signature, $code, $codePattern); + }); + } + + 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 $code, $codePattern): void { + Civix::output()->writeln(sprintf("## FILE: %s", $file)); + Civix::output()->write(sprintf("function %s(%s) {", $functionName, $signature)); + if (!$codePattern) { + Civix::output()->write($code, FALSE, OutputInterface::OUTPUT_RAW); + } + else { + $hiParts = $this->splitHighlights($code, $codePattern); + foreach ($hiParts as $part) { + Civix::output()->write($part[0], FALSE, $part[1]); + } + } + Civix::output()->write("}\n\n"); + } + + /** + * @param string $code + * @param $hi + * @param $matches + * + * @return array + */ + protected function splitHighlights(string $code, $hi): array { + $buf = $code; + $hiPat = $hi[0] . '^(.*)(' . substr($hi, 1, -1) . ')' . $hi[0] . 'msU'; + $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; + } + +} From 9be97fb1c49175da6cbc7b77d86cbd7dac667ed5 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 25 Feb 2025 20:39:39 -0800 Subject: [PATCH 2/3] civix inspect:fun - More guarded handling of regex --- .../Command/InspectFunctionCommand.php | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/CRM/CivixBundle/Command/InspectFunctionCommand.php b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php index 5a108c2e..4fc2c72c 100644 --- a/src/CRM/CivixBundle/Command/InspectFunctionCommand.php +++ b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php @@ -35,7 +35,13 @@ protected function configure() { protected function execute(InputInterface $input, OutputInterface $output): int { $functionNamePattern = $input->getOption('name'); + if ($functionNamePattern) { + $this->assertRegex($functionNamePattern, '--name'); + } $codePattern = $input->getOption('code'); + if ($codePattern) { + $this->assertRegex($codePattern, '--code'); + } $printer = [$this, 'printMatch']; if ($input->getOption('files-with-matches')) { $printer = [$this, 'printFileName']; @@ -82,15 +88,26 @@ protected function printMatch($file, ?string $functionName, string $signature, s } /** - * @param string $code - * @param $hi - * @param $matches + * 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; - $hiPat = $hi[0] . '^(.*)(' . substr($hi, 1, -1) . ')' . $hi[0] . 'msU'; + $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)) { @@ -106,4 +123,22 @@ protected function splitHighlights(string $code, $hi): array { 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"); + } + } + } From afb5f710122253beaef7116ac9351ccbdaf895c7 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 25 Feb 2025 23:06:54 -0800 Subject: [PATCH 3/3] civix inspect:fun - Rename --code to --body Everything in the file (imports, classes, functions, constants, signatures, etc) can be called "code". "body" feels more evocative (re: "function body") --- .../Command/InspectFunctionCommand.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/CRM/CivixBundle/Command/InspectFunctionCommand.php b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php index 4fc2c72c..511b58fc 100644 --- a/src/CRM/CivixBundle/Command/InspectFunctionCommand.php +++ b/src/CRM/CivixBundle/Command/InspectFunctionCommand.php @@ -17,7 +17,7 @@ protected function configure() { ->setName('inspect:fun') ->setDescription('Search codebase for functions') ->addOption('name', NULL, InputOption::VALUE_REQUIRED, 'Pattern describing the function-names you wnt to see') - ->addOption('code', NULL, InputOption::VALUE_REQUIRED, 'Pattern describing function bodies that you want 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 @@ -26,10 +26,10 @@ protected function configure() { civix inspect:fun --name=/_civicrm_permission/ *.php Example: Find all functions which call civicrm_api3() - civix inspect:fun --code=/civicrm_api3/ *.php + civix inspect:fun --body=/civicrm_api3/ *.php -Example: Find all functions matching the name and code patterns - civix inspect:fun --name=/_civicrm_permission/ --code=/label/ *.php +Example: Find all functions named like "_civicrm_permission" AND having a body with "label" + civix inspect:fun --name=/_civicrm_permission/ --body=/label/ *.php '); } @@ -38,9 +38,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($functionNamePattern) { $this->assertRegex($functionNamePattern, '--name'); } - $codePattern = $input->getOption('code'); - if ($codePattern) { - $this->assertRegex($codePattern, '--code'); + $bodyPattern = $input->getOption('body'); + if ($bodyPattern) { + $this->assertRegex($bodyPattern, '--body'); } $printer = [$this, 'printMatch']; if ($input->getOption('files-with-matches')) { @@ -52,16 +52,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("## SCAN FILE: $file"); } - $code = file_get_contents($file); - PrimitiveFunctionVisitor::visit($code, function (?string &$functionName, string &$signature, string &$code) use ($codePattern, $functionNamePattern, $file, $input, $printer) { + $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 ($codePattern && !preg_match($codePattern, $code)) { + if ($bodyPattern && !preg_match($bodyPattern, $body)) { return; } - $printer($file, $functionName, $signature, $code, $codePattern); + $printer($file, $functionName, $signature, $body, $bodyPattern); }); } @@ -72,14 +72,14 @@ protected function printFileName($file, ?string $functionName, string $signature Civix::output()->writeln($file, OutputInterface::OUTPUT_RAW); } - protected function printMatch($file, ?string $functionName, string $signature, string $code, $codePattern): void { + 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 (!$codePattern) { - Civix::output()->write($code, FALSE, OutputInterface::OUTPUT_RAW); + if (!$bodyPattern) { + Civix::output()->write($body, FALSE, OutputInterface::OUTPUT_RAW); } else { - $hiParts = $this->splitHighlights($code, $codePattern); + $hiParts = $this->splitHighlights($body, $bodyPattern); foreach ($hiParts as $part) { Civix::output()->write($part[0], FALSE, $part[1]); }