From 8e19991b09f4dbdaa81ac8e9530928f09124f591 Mon Sep 17 00:00:00 2001 From: Iskander Sharipov Date: Fri, 2 Sep 2022 11:24:29 +0300 Subject: [PATCH] [compiler] implement composer classmap autoloading `autoload.classmap` is another popular method of classes autoloading in composer. Implementing this feature increases the number of composer packages that can be used by KPHP code. Classmap works like this: for a given files list (dirs or regular files), collect all files with `.inc` and `.php` extension recursively and build autoloading maps for them. Unlike PSR4, classmap doesn't require any conventions. A file can have multiple classes, its name could be anything, namespaces can be arbitrary as well. It's hard to implement this feature in KPHP without actually parsing the php files. As a compromise, we're scanning the classmap folders and require all files found during the composer autoload file inclusion. Fixes #49 --- compiler/compiler.cmake | 2 +- compiler/composer.cpp | 49 +++++++++++++++++++ compiler/composer.h | 5 ++ compiler/data/class-data.cpp | 3 +- .../composer/php/classmap/classmap_lib.php | 11 +++++ .../php/classmap/classmap_no_namespace.php | 7 +++ .../composer/php/classmap/dir/classmap.php | 9 ++++ tests/python/tests/composer/php/composer.json | 7 ++- tests/python/tests/composer/php/index.php | 10 ++++ .../php/vendor/composer/autoload_classmap.php | 5 ++ .../php/vendor/composer/autoload_static.php | 9 ++++ 11 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/python/tests/composer/php/classmap/classmap_lib.php create mode 100644 tests/python/tests/composer/php/classmap/classmap_no_namespace.php create mode 100644 tests/python/tests/composer/php/classmap/dir/classmap.php diff --git a/compiler/compiler.cmake b/compiler/compiler.cmake index 29c734f006..2e07a778da 100644 --- a/compiler/compiler.cmake +++ b/compiler/compiler.cmake @@ -240,7 +240,7 @@ add_executable(kphp2cpp ${KPHP_COMPILER_DIR}/kphp2cpp.cpp) target_include_directories(kphp2cpp PUBLIC ${KPHP_COMPILER_DIR}) prepare_cross_platform_libs(COMPILER_LIBS yaml-cpp re2) -set(COMPILER_LIBS vk::kphp2cpp_src vk::tlo_parsing_src vk::popular_common ${COMPILER_LIBS} fmt::fmt OpenSSL::Crypto pthread) +set(COMPILER_LIBS -lstdc++fs vk::kphp2cpp_src vk::tlo_parsing_src vk::popular_common ${COMPILER_LIBS} fmt::fmt OpenSSL::Crypto pthread) target_link_libraries(kphp2cpp PRIVATE ${COMPILER_LIBS}) target_link_options(kphp2cpp PRIVATE ${NO_PIE}) diff --git a/compiler/composer.cpp b/compiler/composer.cpp index 0f3187b546..afdf840af4 100644 --- a/compiler/composer.cpp +++ b/compiler/composer.cpp @@ -6,6 +6,8 @@ #include // using YAML parser to handle JSON files +#include + #include "common/algorithms/contains.h" #include "common/wrappers/fmt_format.h" #include "compiler/kphp_assert.h" @@ -123,6 +125,42 @@ class Psr0Loader : public PsrLoader { }; } // namespace +bool ComposerAutoloader::is_classmap_file(const std::string &filename) const noexcept { + return vk::contains(classmap_files_, filename); +} + +void ComposerAutoloader::scan_classmap(const std::string &filename) { + // supporting the real composer classmap is cumbersome: it requires full PHP parsing to + // fetch all classes from files (the filename doesn't have to follow any conventions); + // we could also invoke php interpreter over vendor/composer/autoload_classmap.php to + // print a JSON dump of the generated classmap and then decode that, but then + // it will be impossible to compile a kphp program that uses a classmap without php interpreter; + // as an alternative, we add all classmap files to auto-required lists that will be + // included along "autoload.files" files, if some classes are not needed, they will be + // discarded after we compute actually used symbols + // + // this approach works well as long as there is no significant side effects related to + // the files being autoloaded (otherwise those side effects will trigger at different point in time) + + const auto add_classmap_file = [&](const std::string &filename) { + classmap_files_.insert(filename); + files_to_require_.emplace_back(filename); + }; + + auto file_info = std::filesystem::status(filename); + kphp_error(file_info.type() != std::filesystem::file_type::not_found, + fmt_format("can't find {} classmap file", filename)); + if (file_info.type() == std::filesystem::file_type::directory) { + for (const auto &entry : std::filesystem::directory_iterator(filename)) { + scan_classmap(entry.path().string()); + } + } else if (file_info.type() == std::filesystem::file_type::regular) { + if (vk::string_view(filename).ends_with(".php") || vk::string_view(filename).ends_with(".inc")) { + add_classmap_file(filename); + } + } +} + std::string ComposerAutoloader::psr_lookup_nocache(const PsrMap &psr, const std::string &class_name, bool transform_underscore) { std::string prefix = class_name; // we start from a longest prefix and then try to match it @@ -253,6 +291,11 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil // "": "fallback-dir/", // <...> // }, + // "classmap": [ + // "src/", + // "lib/file.php", + // <...> + // ], // "files": [ // "file.php", // <...> @@ -315,6 +358,12 @@ void ComposerAutoloader::add_autoload_section(const YAML::Node &autoload, const Psr0Loader psr0_loader{autoload, autoload_psr0_classmap_, autoload_psr0_, pkg_root}; psr0_loader.load(); + // https://getcomposer.org/doc/04-schema.md#classmap + const auto &classmap_src = autoload["classmap"]; + for (const auto &elem : classmap_src) { + scan_classmap(pkg_root + elem.as()); + } + if (require_files) { // files that are required by the composer-generated autoload.php // https://getcomposer.org/doc/04-schema.md#files diff --git a/compiler/composer.h b/compiler/composer.h index d337e00ca0..dadcd9e0fa 100644 --- a/compiler/composer.h +++ b/compiler/composer.h @@ -48,6 +48,8 @@ class ComposerAutoloader : private vk::not_copyable { return filename == autoload_filename_; } + bool is_classmap_file(const std::string &filename) const noexcept; + const std::vector &get_files_to_require() const noexcept { return files_to_require_; } @@ -60,11 +62,14 @@ class ComposerAutoloader : private vk::not_copyable { static std::string psr_lookup_nocache(const PsrMap &psr, const std::string &class_name, bool transform_underscore = false); + void scan_classmap(const std::string &filename); + bool use_dev_; PsrMap autoload_psr4_; PsrMap autoload_psr0_; std::map autoload_psr0_classmap_; std::unordered_set deps_; + std::unordered_set classmap_files_; std::string autoload_filename_; std::vector files_to_require_; diff --git a/compiler/data/class-data.cpp b/compiler/data/class-data.cpp index b3a462e317..7673744892 100644 --- a/compiler/data/class-data.cpp +++ b/compiler/data/class-data.cpp @@ -42,7 +42,8 @@ void ClassData::set_name_and_src_name(const std::string &full_name) { std::string namespace_name = pos == std::string::npos ? "" : full_name.substr(0, pos); std::string class_name = pos == std::string::npos ? full_name : full_name.substr(pos + 1); - this->can_be_php_autoloaded = file_id && namespace_name == file_id->namespace_name && class_name == file_id->short_file_name; + this->can_be_php_autoloaded = file_id && ((namespace_name == file_id->namespace_name && class_name == file_id->short_file_name) || + (G->get_composer_autoloader().is_classmap_file(file_id->file_name))); this->can_be_php_autoloaded |= this->is_builtin(); this->is_lambda = vk::string_view{full_name}.starts_with("Lambda$") || vk::string_view{full_name}.starts_with("ITyped$"); diff --git a/tests/python/tests/composer/php/classmap/classmap_lib.php b/tests/python/tests/composer/php/classmap/classmap_lib.php new file mode 100644 index 0000000000..553bbaaf28 --- /dev/null +++ b/tests/python/tests/composer/php/classmap/classmap_lib.php @@ -0,0 +1,11 @@ +value); +$classmap_c2 = new \ClassmapLib\Classes\ClassmapClass2(); +var_dump($classmap_c2->value); +$classmap_c3 = new ClassmapNoNamespace(); +$classmap_c3->f(); +$classmap_c4 = new \OtherClassmap\OtherClassmapClass(); +$classmap_c4->g(); + $controller = new Controller(); $t = new FeedTester(); diff --git a/tests/python/tests/composer/php/vendor/composer/autoload_classmap.php b/tests/python/tests/composer/php/vendor/composer/autoload_classmap.php index 7a91153b0d..4a112bfc89 100644 --- a/tests/python/tests/composer/php/vendor/composer/autoload_classmap.php +++ b/tests/python/tests/composer/php/vendor/composer/autoload_classmap.php @@ -6,4 +6,9 @@ $baseDir = dirname($vendorDir); return array( + 'ClassmapLib\\Classes\\ClassmapClass1' => $baseDir . '/classmap/classmap_lib.php', + 'ClassmapLib\\Classes\\ClassmapClass2' => $baseDir . '/classmap/classmap_lib.php', + 'ClassmapNoNamespace' => $baseDir . '/classmap/classmap_no_namespace.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'OtherClassmap\\OtherClassmapClass' => $baseDir . '/classmap/dir/classmap.php', ); diff --git a/tests/python/tests/composer/php/vendor/composer/autoload_static.php b/tests/python/tests/composer/php/vendor/composer/autoload_static.php index cd31e9808b..fbf8708fe1 100644 --- a/tests/python/tests/composer/php/vendor/composer/autoload_static.php +++ b/tests/python/tests/composer/php/vendor/composer/autoload_static.php @@ -50,12 +50,21 @@ class ComposerStaticInit9539f736c30192217f7a8384c08769a6 1 => __DIR__ . '/..' . '/vk/utils/utils-fallback/src', ); + public static $classMap = array ( + 'ClassmapLib\\Classes\\ClassmapClass1' => __DIR__ . '/../..' . '/classmap/classmap_lib.php', + 'ClassmapLib\\Classes\\ClassmapClass2' => __DIR__ . '/../..' . '/classmap/classmap_lib.php', + 'ClassmapNoNamespace' => __DIR__ . '/../..' . '/classmap/classmap_no_namespace.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'OtherClassmap\\OtherClassmapClass' => __DIR__ . '/../..' . '/classmap/dir/classmap.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$prefixDirsPsr4; $loader->fallbackDirsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$fallbackDirsPsr4; + $loader->classMap = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$classMap; }, null, ClassLoader::class); }