From b94591c1462e99b47fb5160373282e3072c0d174 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 20 Jun 2023 13:52:09 +0200 Subject: [PATCH 1/6] Add script to generate function map for static analysis tools --- scripts/create-functionmap.php | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 scripts/create-functionmap.php diff --git a/scripts/create-functionmap.php b/scripts/create-functionmap.php new file mode 100644 index 000000000..5f3ce6698 --- /dev/null +++ b/scripts/create-functionmap.php @@ -0,0 +1,95 @@ +hasReturnType() => (string) $function->getReturnType(), + $function->hasTentativeReturnType() => (string) $function->getTentativeReturnType(), + default => 'void', + }; + $callmapEntry = [$returnType]; + + foreach ($function->getParameters() as $parameter) { + $parameterKey = $parameter->getName(); + if ($parameter->isOptional()) { + $parameterKey .= '='; + } + + $parameterType = (string) $parameter->getType(); + if ($function->getName() === 'unserialize' && $parameter->getName() === 'serialized') { + $parameterType = 'string'; + } + + $callmapEntry[$parameterKey] = $parameterType; + } + + return $callmapEntry; +}; + +$classes = array_filter(get_declared_classes(), $filter); +$interfaces = array_filter(get_declared_interfaces(), $filter); +$functions = array_filter(get_defined_functions()['internal'], $filter); + +$callmap = []; + +// Generate call map for functions +foreach ($functions as $functionName) { + $reflectionFunction = new ReflectionFunction($functionName); + $callmap[$reflectionFunction->getName()] = $getFunctionCallMapEntry($reflectionFunction); +} + +// Generate call map for classes and interfaces +$members = array_merge($classes, $interfaces); +sort($members); + +$skippedMethods = ['__set_state', '__wakeup', '__serialize', '__unserialize']; + +foreach ($members as $member) { + $reflectionClass = new ReflectionClass($member); + + foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass() != $reflectionClass && $method->getName() != '__toString') { + continue; + } + + if (in_array($method->getName(), $skippedMethods, true)) { + continue; + } + + $methodKey = $reflectionClass->getName() . '::' . $method->getName(); + $callmap[$methodKey] = $getFunctionCallMapEntry($method); + } +} + +$map = var_export($callmap, true); + +// Format output + +$replacements = [ + '#array \(#' => '[', // Short array syntax (opening) + '#\)#' => ']', // Short array syntax (closing) + '#0 => #' => '', // Numeric index for return type + '#\\\\{2}#' => '\\', // Double backslash + '# => $\s+#m' => ' => ', // Newlines at start of array + '#\s+\[$\s+#m' => ' [', // Newlines at start of array + '#,$\s+#m' => ', ', // Newline after array elements + '#, \],\s+#' => "],\n", // Newline after array elements + '#^\s*\'#m' => ' \'', // Fix indentation +]; + +echo preg_replace( + array_keys($replacements), + array_values($replacements), + $map, +); From aa72cef22337c50d7888fc3799e62246b8df3eb1 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Wed, 21 Jun 2023 13:07:34 +0200 Subject: [PATCH 2/6] Refactor function map generation script --- Makefile.frag | 22 +++++ scripts/create-functionmap.php | 95 --------------------- scripts/generate-functionmap.php | 139 +++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 95 deletions(-) delete mode 100644 scripts/create-functionmap.php create mode 100644 scripts/generate-functionmap.php diff --git a/Makefile.frag b/Makefile.frag index d46e7e03c..f0ddb8afa 100644 --- a/Makefile.frag +++ b/Makefile.frag @@ -66,3 +66,25 @@ libmongoc-version-current: libmongocrypt-version-current: cd src/libmongocrypt/ && python etc/calc_release_version.py > ../LIBMONGOCRYPT_VERSION_CURRENT + +generate-function-map: all + @if test ! -z "$(PHP_EXECUTABLE)" && test -x "$(PHP_EXECUTABLE)"; then \ + INI_FILE=`$(PHP_EXECUTABLE) -d 'display_errors=stderr' -r 'echo php_ini_loaded_file();' 2> /dev/null`; \ + if test "$$INI_FILE"; then \ + $(EGREP) -h -v $(PHP_DEPRECATED_DIRECTIVES_REGEX) "$$INI_FILE" > $(top_builddir)/tmp-php.ini; \ + else \ + echo > $(top_builddir)/tmp-php.ini; \ + fi; \ + INI_SCANNED_PATH=`$(PHP_EXECUTABLE) -d 'display_errors=stderr' -r '$$a = explode(",\n", trim(php_ini_scanned_files())); echo $$a[0];' 2> /dev/null`; \ + if test "$$INI_SCANNED_PATH"; then \ + INI_SCANNED_PATH=`$(top_srcdir)/build/shtool path -d $$INI_SCANNED_PATH`; \ + $(EGREP) -h -v $(PHP_DEPRECATED_DIRECTIVES_REGEX) "$$INI_SCANNED_PATH"/*.ini >> $(top_builddir)/tmp-php.ini; \ + fi; \ + CC="$(CC)" \ + $(PHP_EXECUTABLE) -n -c $(top_builddir)/tmp-php.ini -n -c $(top_builddir)/tmp-php.ini -d extension_dir=$(top_builddir)/modules/ $(PHP_TEST_SHARED_EXTENSIONS) $(top_srcdir)/scripts/generate-functionmap.php; \ + RESULT_EXIT_CODE=$$?; \ + rm $(top_builddir)/tmp-php.ini; \ + exit $$RESULT_EXIT_CODE; \ + else \ + echo "ERROR: Cannot generate function maps without CLI sapi."; \ + fi diff --git a/scripts/create-functionmap.php b/scripts/create-functionmap.php deleted file mode 100644 index 5f3ce6698..000000000 --- a/scripts/create-functionmap.php +++ /dev/null @@ -1,95 +0,0 @@ -hasReturnType() => (string) $function->getReturnType(), - $function->hasTentativeReturnType() => (string) $function->getTentativeReturnType(), - default => 'void', - }; - $callmapEntry = [$returnType]; - - foreach ($function->getParameters() as $parameter) { - $parameterKey = $parameter->getName(); - if ($parameter->isOptional()) { - $parameterKey .= '='; - } - - $parameterType = (string) $parameter->getType(); - if ($function->getName() === 'unserialize' && $parameter->getName() === 'serialized') { - $parameterType = 'string'; - } - - $callmapEntry[$parameterKey] = $parameterType; - } - - return $callmapEntry; -}; - -$classes = array_filter(get_declared_classes(), $filter); -$interfaces = array_filter(get_declared_interfaces(), $filter); -$functions = array_filter(get_defined_functions()['internal'], $filter); - -$callmap = []; - -// Generate call map for functions -foreach ($functions as $functionName) { - $reflectionFunction = new ReflectionFunction($functionName); - $callmap[$reflectionFunction->getName()] = $getFunctionCallMapEntry($reflectionFunction); -} - -// Generate call map for classes and interfaces -$members = array_merge($classes, $interfaces); -sort($members); - -$skippedMethods = ['__set_state', '__wakeup', '__serialize', '__unserialize']; - -foreach ($members as $member) { - $reflectionClass = new ReflectionClass($member); - - foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - if ($method->getDeclaringClass() != $reflectionClass && $method->getName() != '__toString') { - continue; - } - - if (in_array($method->getName(), $skippedMethods, true)) { - continue; - } - - $methodKey = $reflectionClass->getName() . '::' . $method->getName(); - $callmap[$methodKey] = $getFunctionCallMapEntry($method); - } -} - -$map = var_export($callmap, true); - -// Format output - -$replacements = [ - '#array \(#' => '[', // Short array syntax (opening) - '#\)#' => ']', // Short array syntax (closing) - '#0 => #' => '', // Numeric index for return type - '#\\\\{2}#' => '\\', // Double backslash - '# => $\s+#m' => ' => ', // Newlines at start of array - '#\s+\[$\s+#m' => ' [', // Newlines at start of array - '#,$\s+#m' => ', ', // Newline after array elements - '#, \],\s+#' => "],\n", // Newline after array elements - '#^\s*\'#m' => ' \'', // Fix indentation -]; - -echo preg_replace( - array_keys($replacements), - array_values($replacements), - $map, -); diff --git a/scripts/generate-functionmap.php b/scripts/generate-functionmap.php new file mode 100644 index 000000000..3028e8c3d --- /dev/null +++ b/scripts/generate-functionmap.php @@ -0,0 +1,139 @@ +createFunctionMap($filename); +printf("Created call map in %s\n", $filename); + +class FunctionMapGenerator +{ + public function createFunctionMap(string $filename): void { + $this->writeFunctionMap($filename, $this->getFunctionMap()); + } + + public function getFunctionMap(): array { + $classes = array_filter(get_declared_classes(), $this->filterItems(...)); + $interfaces = array_filter(get_declared_interfaces(), $this->filterItems(...)); + $functions = array_filter(get_defined_functions()['internal'], $this->filterItems(...)); + + $functionMap = []; + + // Generate call map for functions + foreach ($functions as $functionName) { + $reflectionFunction = new ReflectionFunction($functionName); + $functionMap[$reflectionFunction->getName()] = $this->getFunctionMapEntry($reflectionFunction); + } + + // Generate call map for classes and interfaces + $members = array_merge($classes, $interfaces); + sort($members); + + $skippedMethods = ['__set_state', '__wakeup', '__serialize', '__unserialize']; + + foreach ($members as $member) { + $reflectionClass = new ReflectionClass($member); + + foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass() != $reflectionClass && $method->getName() != '__toString') { + continue; + } + + if (in_array($method->getName(), $skippedMethods, true)) { + continue; + } + + $methodKey = $reflectionClass->getName() . '::' . $method->getName(); + $functionMap[$methodKey] = $this->getFunctionMapEntry($method); + } + } + + return $functionMap; + } + + private function writeFunctionMap(string $filename, array $functionMap): void + { + $lines = []; + foreach ($functionMap as $methodName => $typeInfo) { + $generatedTypeInfo = implode( + ', ', + array_map( + function (string|int $key, string $value): string { + if (is_int($key)) { + return $this->removeDoubleBackslash(var_export($value, true)); + } + + return sprintf('%s => %s', var_export($key, true), $this->removeDoubleBackslash(var_export($value, true))); + }, + array_keys($typeInfo), + array_values($typeInfo) + ) + ); + + $lines[] = sprintf( + ' %s => [%s],', + $this->removeDoubleBackslash(var_export($methodName, true)), + $generatedTypeInfo + ); + } + + $fileTemplate = <<<'PHP' +hasReturnType() => (string) $function->getReturnType(), + $function->hasTentativeReturnType() => (string) $function->getTentativeReturnType(), + default => 'void', + }; + + $functionMapEntry = [$returnType]; + + foreach ($function->getParameters() as $parameter) { + $parameterKey = $parameter->getName(); + if ($parameter->isOptional()) { + $parameterKey .= '='; + } + + $parameterType = (string) $parameter->getType(); + if ($function->getName() === 'unserialize' && $parameter->getName() === 'serialized') { + $parameterType = 'string'; + } + + $functionMapEntry[$parameterKey] = $parameterType; + } + + return $functionMapEntry; + } + + private function removeDoubleBackslash(string $string): string { + return str_replace('\\\\', '\\', $string); + } +} From 8c1a23ad47face7510489597e0687bdce251eb28 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 22 Jun 2023 08:58:44 +0200 Subject: [PATCH 3/6] Remove workaround for unserialize methods --- scripts/generate-functionmap.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/generate-functionmap.php b/scripts/generate-functionmap.php index 3028e8c3d..9202ab574 100644 --- a/scripts/generate-functionmap.php +++ b/scripts/generate-functionmap.php @@ -122,12 +122,7 @@ private function getFunctionMapEntry(ReflectionFunctionAbstract $function): arra $parameterKey .= '='; } - $parameterType = (string) $parameter->getType(); - if ($function->getName() === 'unserialize' && $parameter->getName() === 'serialized') { - $parameterType = 'string'; - } - - $functionMapEntry[$parameterKey] = $parameterType; + $functionMapEntry[$parameterKey] = (string) $parameter->getType(); } return $functionMapEntry; From 8031f456fce5c5512f29ac19e0096036db25d8bc Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 22 Jun 2023 08:33:52 +0200 Subject: [PATCH 4/6] Update list of phony make targets --- Makefile.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.frag b/Makefile.frag index f0ddb8afa..6c69049b1 100644 --- a/Makefile.frag +++ b/Makefile.frag @@ -1,4 +1,4 @@ -.PHONY: coverage test-clean package package.xml format format-changed format-check +.PHONY: mv-coverage lcov-coveralls lcov-local coverage coveralls format format-changed format-check test-clean package package.xml libmongoc-version-current libmongocrypt-version-current generate-function-map ifneq (,$(realpath $(EXTENSION_DIR)/json.so)) PHP_TEST_SHARED_EXTENSIONS := "-d" "extension=$(EXTENSION_DIR)/json.so" $(PHP_TEST_SHARED_EXTENSIONS) From de4465d00624005f455e81783d47394fba9c6238 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 22 Jun 2023 08:34:04 +0200 Subject: [PATCH 5/6] Add section about generating function maps to contribution docs --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b8566850..d05269440 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,13 @@ file. Note that this requires `phpize` to be run for PHP 8.2 to make use of all features. After changing a stub file, run `./build/gen_stub.php` to regenerate the corresponding arginfo files and commit the results. +## Generating function maps for static analysis tools + +PHPStan and Psalm use function maps to provide users with correct type analysis +when using this extension. To generate the function map, run the +`generate-function-map` make target. The generated map will be stored in +`scripts/functionMap.php`. + ## Testing The extension's test use the PHPT format from PHP internals. This format is From 4880238a8116d8d771e3b3f4d3a86c60cad77b32 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 22 Jun 2023 08:34:46 +0200 Subject: [PATCH 6/6] Fix code style in function map generator --- scripts/generate-functionmap.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/generate-functionmap.php b/scripts/generate-functionmap.php index 9202ab574..54571ab76 100644 --- a/scripts/generate-functionmap.php +++ b/scripts/generate-functionmap.php @@ -11,11 +11,13 @@ class FunctionMapGenerator { - public function createFunctionMap(string $filename): void { + public function createFunctionMap(string $filename): void + { $this->writeFunctionMap($filename, $this->getFunctionMap()); } - public function getFunctionMap(): array { + private function getFunctionMap(): array + { $classes = array_filter(get_declared_classes(), $this->filterItems(...)); $interfaces = array_filter(get_declared_interfaces(), $this->filterItems(...)); $functions = array_filter(get_defined_functions()['internal'], $this->filterItems(...)); @@ -107,7 +109,8 @@ private function filterItems(string $name): bool { return false; } - private function getFunctionMapEntry(ReflectionFunctionAbstract $function): array { + private function getFunctionMapEntry(ReflectionFunctionAbstract $function): array + { $returnType = match(true) { $function->hasReturnType() => (string) $function->getReturnType(), $function->hasTentativeReturnType() => (string) $function->getTentativeReturnType(), @@ -128,7 +131,8 @@ private function getFunctionMapEntry(ReflectionFunctionAbstract $function): arra return $functionMapEntry; } - private function removeDoubleBackslash(string $string): string { + private function removeDoubleBackslash(string $string): string + { return str_replace('\\\\', '\\', $string); } }