├── .gitattributes ├── .gitignore ├── hh_autoload.json ├── CODE_OF_CONDUCT.md ├── src ├── Parser.hack ├── ConfigurationException.hack ├── AutoloadMap.hack ├── IncludedRoots.hack ├── Exception.hack ├── Config.hack ├── builders │ ├── Builder.hack │ ├── Scanner.hack │ ├── RootImporter.hack │ ├── HHImporter.hack │ └── FactParseScanner.hack ├── Merger.hack ├── FailureHandler.hack ├── TypeAssert.hack ├── ConfigurationLoader.hack ├── HHClientFallbackHandler.hack └── Writer.hack ├── bin ├── hh-autoload └── hh-autoload.hack ├── .github └── workflows │ ├── install-composer.sh │ ├── hhvm.gpg.key │ └── build-and-test.yml ├── .hhconfig ├── composer.json ├── LICENSE ├── CONTRIBUTING.md ├── ComposerPlugin.php └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | testdata/ export-ignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /vendor/ 3 | hh_autoload.tmp.* 4 | composer.lock 5 | .*.hhast.*cache 6 | .var/ 7 | -------------------------------------------------------------------------------- /hh_autoload.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "src/" 4 | ], 5 | "devRoots": [ 6 | "tests/" 7 | ], 8 | "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler" 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | -------------------------------------------------------------------------------- /src/Parser.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Which parser to use to scan file contents */ 13 | enum Parser: string { 14 | EXT_FACTPARSE = 'ext-factparse'; 15 | } 16 | -------------------------------------------------------------------------------- /src/ConfigurationException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Exception raised when the configuration file is invalid. */ 13 | final class ConfigurationException extends Exception { 14 | } 15 | -------------------------------------------------------------------------------- /src/AutoloadMap.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** The main shape of an autoload map. 13 | * 14 | * Must match `\HH\autoload_set_paths()` 15 | */ 16 | type AutoloadMap = dict>; 17 | -------------------------------------------------------------------------------- /bin/hh-autoload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | namespace Facebook\AutoloadMap; 12 | 13 | <<__EntryPoint>> 14 | function cli_main_UNSAFE(): noreturn { 15 | (() ==> { 16 | // HHAST-generated to avoid pseudomain local leaks 17 | require_once(__DIR__.'/hh-autoload.hack'); 18 | })(); 19 | cli_main(); 20 | } 21 | -------------------------------------------------------------------------------- /src/IncludedRoots.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** What root directories to include */ 13 | enum IncludedRoots: int { 14 | /** Only include prod-suitable directories (e.g. `src/`) */ 15 | PROD_ONLY = 0; 16 | /** Additionally include development-only directories (e.g. `test/`) */ 17 | DEV_AND_PROD = 1; 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Base class for all exceptions thrown by `hhvm-autoload` */ 13 | class Exception extends \Exception { 14 | public function __construct( 15 | \HH\FormatString<\PlainSprintf> $format, 16 | mixed ...$args 17 | ) { 18 | parent::__construct(\vsprintf($format, $args)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/install-composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # From https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md 4 | 5 | EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" 6 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 7 | ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" 8 | 9 | if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ] 10 | then 11 | >&2 echo 'ERROR: Invalid installer checksum' 12 | rm composer-setup.php 13 | exit 1 14 | fi 15 | 16 | php composer-setup.php "$@" 17 | RESULT=$? 18 | rm composer-setup.php 19 | exit $RESULT 20 | -------------------------------------------------------------------------------- /src/Config.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Shape of `hh_autoload.json` */ 13 | type Config = shape( 14 | 'roots' => vec, 15 | 'devRoots' => vec, 16 | 'includeVendor' => bool, 17 | 'extraFiles' => vec, 18 | 'parser' => Parser, 19 | 'failureHandler' => ?string, 20 | 'devFailureHandler' => ?string, 21 | 'relativeAutoloadRoot' => bool, 22 | 'useFactsIfAvailable' => bool, 23 | ); 24 | -------------------------------------------------------------------------------- /src/builders/Builder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** 13 | * Base interface for everything that exposes an autoload map. 14 | */ 15 | interface Builder { 16 | /** Returns the actual autoload map created by this builder */ 17 | public function getAutoloadMap(): AutoloadMap; 18 | /** Returns any additional files that should be explicitly required on 19 | * start */ 20 | public function getFiles(): vec; 21 | } 22 | -------------------------------------------------------------------------------- /.hhconfig: -------------------------------------------------------------------------------- 1 | assume_php=false 2 | enable_experimental_tc_features = no_fallback_in_namespaces, unpacking_check_arity, disallow_refs_in_array, disallow_untyped_lambda_as_non_function_type 3 | ignored_paths = [ "vendor/.+/tests/.+", "vendor/.+/bin/.+", "testdata/fixtures/.+\.tmp\..+\.hack", "testdata/fixtures/xhp-class/.+" ] 4 | safe_array = true 5 | safe_vector_array = true 6 | disallow_destruct = true 7 | disallow_stringish_magic = true 8 | disable_primitive_refinement = true 9 | user_attributes= 10 | disable_static_local_variables = true 11 | disallow_array_literal = true 12 | disallow_silence = true 13 | allowed_decl_fixme_codes=2053,4045 14 | allowed_fixme_codes_strict=2011,2049,2050,2053,4027,4045,4106,4107,4108,4110,4128,4135,4188,4240,4323,4390,4401 15 | disable_modes=true 16 | ; enable_strict_string_concat_interp=true 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hhvm/hhvm-autoload", 3 | "license": "MIT", 4 | "replace": { 5 | "facebook/hhvm-autoload": "1.*" 6 | }, 7 | "type": "composer-plugin", 8 | "bin": [ 9 | "bin/hh-autoload", 10 | "bin/hh-autoload.hack" 11 | ], 12 | "require": { 13 | "composer-plugin-api": "^1.0|^2.0", 14 | "hhvm": "^4.153" 15 | }, 16 | "require-dev": { 17 | "hhvm/hacktest": "^2.0", 18 | "facebook/fbexpect": "^2.1" 19 | }, 20 | "autoload": { 21 | "classmap": [ 22 | "ComposerPlugin.php" 23 | ] 24 | }, 25 | "extra": { 26 | "class": [ 27 | "Facebook\\AutoloadMap\\ComposerPlugin" 28 | ], 29 | "branch-alias": { 30 | "dev-master": "3.x-dev", 31 | "dev-main": "3.x-dev", 32 | "dev-CI_current_pull_request": "3.x-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/builders/Scanner.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Create a `Builder` by scanning file contents. 13 | */ 14 | abstract final class Scanner { 15 | /** Construct a builder for a single file */ 16 | public static function fromFile(string $path, Parser $parser): Builder { 17 | switch ($parser) { 18 | case Parser::EXT_FACTPARSE: 19 | return FactParseScanner::fromFile($path); 20 | } 21 | } 22 | 23 | /** Construct a builder for a tree */ 24 | public static function fromTree(string $path, Parser $parser): Builder { 25 | switch ($parser) { 26 | case Parser::EXT_FACTPARSE: 27 | return FactParseScanner::fromTree($path); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-present, Facebook, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Merger.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use namespace HH\Lib\Vec; 13 | 14 | /** Class for merging multiple autoload maps. 15 | * 16 | * For example, we may merge: 17 | * - the root autoload map 18 | * - additional autoload maps for each vendored dependency 19 | */ 20 | abstract final class Merger { 21 | /** Return a new map containing all the entries from the input maps. 22 | * 23 | * In the case of duplicates, the last definition is used. 24 | */ 25 | public static function merge(vec $maps): AutoloadMap { 26 | return dict[ 27 | 'class' => self::mergeImpl(Vec\map($maps, $map ==> $map['class'])), 28 | 'function' => self::mergeImpl(Vec\map($maps, $map ==> $map['function'])), 29 | 'type' => self::mergeImpl(Vec\map($maps, $map ==> $map['type'])), 30 | 'constant' => self::mergeImpl(Vec\map($maps, $map ==> $map['constant'])), 31 | ]; 32 | } 33 | 34 | private static function mergeImpl( 35 | Traversable> $maps, 36 | ): dict { 37 | $out = dict[]; 38 | foreach ($maps as $map) { 39 | foreach ($map as $def => $file) { 40 | $out[$def] = $file; 41 | } 42 | } 43 | return $out; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to hhvm-autoload 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | 7 | All development is direclty on GitHub. 8 | 9 | ## Pull Requests 10 | We actively welcome your pull requests. 11 | 12 | 1. Fork the repo and create your branch from `main`. 13 | 2. If you've added code that should be tested, add tests. 14 | 3. If you've changed APIs, update the documentation. 15 | 4. Ensure the test suite passes. 16 | 5. Make sure your code lints. 17 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 18 | 19 | We are unlikely to accept pull requests that add additional dependencies; 20 | this is because if `hhvm-autoload` depends on something, it effectively 21 | pins the ecosystem to the current major version of that dependency, and 22 | upgrading becomes extremely difficult. 23 | 24 | ## Contributor License Agreement ("CLA") 25 | In order to accept your pull request, we need you to submit a CLA. You only need 26 | to do this once to work on any of Facebook's open source projects. 27 | 28 | Complete your CLA here: 29 | 30 | ## Issues 31 | We use GitHub issues to track public bugs. Please ensure your description is 32 | clear and has sufficient instructions to be able to reproduce the issue. 33 | 34 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 35 | disclosure of security bugs. In those cases, please go through the process 36 | outlined on that page and do not file a public issue. 37 | 38 | ## Coding Style 39 | 40 | Coding should match `hh_format`/`hackfmt` where practical; the key parts are: 41 | 42 | * 2 spaces for indentation rather than tabs 43 | * 80 character line length 44 | * indent, don't align 45 | 46 | ## License 47 | 48 | By contributing to hhvm-autoload, you agree that your contributions will be licensed 49 | under the LICENSE file in the root directory of this source tree. 50 | -------------------------------------------------------------------------------- /src/builders/RootImporter.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use namespace HH\Lib\Vec; 13 | 14 | /** Build an autoload map for the project root. 15 | * 16 | * This will: 17 | * - create an `HHImporter` for the current directory 18 | * - create `HHImporter`s for every project under `vendor/` that has 19 | * `hh_autoload.json` 20 | * 21 | * Previously we also supported projects without `hh_autoload.json` by 22 | * simulating Composer's autoload behavior, but we no longer do because that 23 | * mostly applied to PHP files which HHVM can no longer parse. 24 | */ 25 | final class RootImporter implements Builder { 26 | private vec $builders = vec[]; 27 | private HHImporter $hh_importer; 28 | 29 | public function __construct( 30 | string $root, 31 | IncludedRoots $included = IncludedRoots::PROD_ONLY, 32 | ) { 33 | $this->hh_importer = new HHImporter($root, $included); 34 | $this->builders[] = $this->hh_importer; 35 | $config = $this->hh_importer->getConfig(); 36 | 37 | if (!$config['includeVendor']) { 38 | return; 39 | } 40 | 41 | foreach (\glob($root.'/vendor/*/*/') as $dependency) { 42 | if (\file_exists($dependency.'/hh_autoload.json')) { 43 | $this->builders[] = new HHImporter( 44 | $dependency, 45 | IncludedRoots::PROD_ONLY, 46 | ); 47 | } 48 | } 49 | } 50 | 51 | public function getAutoloadMap(): AutoloadMap { 52 | return Merger::merge( 53 | Vec\map($this->builders, $builder ==> $builder->getAutoloadMap()), 54 | ); 55 | } 56 | 57 | public function getFiles(): vec { 58 | return Vec\map($this->builders, $builder ==> $builder->getFiles()) 59 | |> Vec\flatten($$); 60 | } 61 | 62 | public function getConfig(): Config { 63 | return $this->hh_importer->getConfig(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/hhvm.gpg.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | mQINBFn8koEBEAC2tPtkphj8gZYHI9mTNUHfQalDo+MNWTGUTNB42asjhTNjipzM 5 | VSxjaZSl5cMLg5YCRuT0AbSIe529FH23yEElc03cGVGgoEnmXtE4+2v7Xa30wCGO 6 | 5oUxKfbVatsxEs1y8QEr5Gt+CUFmsApOKgiZq0MsPYmFAuC9CbWdXYa8+E00bXOa 7 | cHCpe+GncCxQmExm7TlrUnURnf3RnNWSEkuPKED/aVggzxNVN6RgRRm4ssZJasM3 8 | TwoI1nVysO5jMfPClvupYscoktO44HBZzH2EeEdpjSV+toD3aZCbmWzXyZjogrFN 9 | j4k5Mme0Xqr4DvRPk5M9SxcQASsCQ8VTyu+ZBUG6zJbddLDEA1BMNIZOG5MyX58O 10 | zed255Q85SAyjHu8pltkfGLd56+MYsckpHaBPMFoCFM4iPcpXOlgcU96pdXJbrR2 11 | mjYI4Le9qRJYYP2kGPkopPwK8nbZJ5Wr7xaclxEc/ODH3mv57KJD7lzmwpnvvmsn 12 | kR/wUHOqwrXojp/oZCUK8KembLiT+MMkY3bne+IY9ef/1qwu4flVBP1CpoaMQEwh 13 | dqzihfwyQ+57ATZHJaj8V9pKAxWh/Df4iFN5mMWA15eBLhRMbAWKJIoLQLcCYwBF 14 | gH3HiO34/uQUHaX6VhRHllA38WUoZNhKmw/Kcd/FDQWlbzbgmI89LJEJuwARAQAB 15 | tC1ISFZNIFBhY2thZ2UgU2lnbmluZyA8b3BlbnNvdXJjZStoaHZtQGZiLmNvbT6J 16 | Ak4EEwEIADgWIQQFg0HGj8jeYBfXdaG0ESWF04brlAUCWfySgQIbAwULCQgHAgYV 17 | CAkKCwIEFgIDAQIeAQIXgAAKCRC0ESWF04brlMp8D/4ia7wLi6OQEtR8uPIrtCdg 18 | ClHvXTX0zihHPDomn77lRSfqEVapKcsvpyc9YTjv27EuRvymUG+o7971RY+rYes4 19 | +POdsjlxJF5ZkNi8YxpUNEw2hTWC66o6vd4Gv4dJgugkZ5dvHKEwec7+mQna9O/p 20 | F4rY/VVmh+4YJUzuuKMb2ZLHsZ3LJv/WBL9Ps+sRFHUN5lDfV00wAsfzEW+dxyh1 21 | kkqXwTk70r8m5m+nCdf0z+giAU7XWRkbJV2HTatSgY1ozOYARe4v0MGyLwp74I6R 22 | lrWPY97C9k4emF7WP2mglcBu+Eg2Q6A0Y3OgEiGnqkgRJEnrfpHa4wXM1sEUf4MV 23 | 5FQgyroZg45c375okr/RLP/pC4/x8ZM6GqLv4qTEOk6qWM7hWXhPRJ1TSVgCHv19 24 | jki5AkwV4EcROpFmJzfW6V9i4swJKJvYXLr58W0vogsUc8zqII4Sl7JUKZ/oN4jQ 25 | QX138r85fLawla/R0i30njmY7fJYKRwHeshgwHg6vqKobTiPuLarwn0Arv7G7ILP 26 | RjbH/8Pi+U2l8Fm/SjHMZA6gcJteRHjTgjkxSAZ19MyA08YqahJafRUVDY9QhUJb 27 | FkHhptZRf9qRji3+Njhog6s8EGACJSEOwmngAViFVz+UUyOXY94yoHvb19meNecj 28 | ArL3604gOqX3TSSWD1Dcu4kBMwQTAQgAHRYhBDau9k0CB+fu41LUh1oW5ygb56RJ 29 | BQJZ/JVnAAoJEFoW5ygb56RJ15oH/0g4hrylc79TD9xA1vEUexyOdWniY4lwH9yI 30 | /DaFznIMsE1uxmZ0FE9VX5Ks8IFR+3P9mNDQVf9xlVhnR7N597aKtU5GrpbvtlJy 31 | CoQVtzBqYKcuLC4ZFRiB33HwZrZIxTPH27UUaj1QBz748zIMC6wvtldshjNAAeRr 32 | Jz28twPO2D7svNIaPt2+OXAuRs2yUhitcsDLBV0UlOQ8xH+hzWANyhaJAS7p0k35 33 | kyFOG+n6+2qQkGdlHHuqEzdCL3EiOiK6RrvbWNUnwiG3BdZWgs43hZZBAseX3CHu 34 | MM3vIX/Fc/kuuaCWi2ysyKf7jyi/RiVIAKuLbxAB8eHsyo2G5lA= 35 | =3DTP 36 | -----END PGP PUBLIC KEY BLOCK----- 37 | -------------------------------------------------------------------------------- /ComposerPlugin.php: -------------------------------------------------------------------------------- 1 | io = $io; 39 | $vendor = $composer->getConfig()->get('vendor-dir'); 40 | 41 | $this->vendor = $vendor; 42 | $this->root = dirname($vendor); 43 | } 44 | 45 | /** Tell composer what events we're interested in. 46 | * 47 | * In this case, we want to run whenever composer's own autoload map is updated. 48 | */ 49 | public static function getSubscribedEvents() { 50 | return [ScriptEvents::POST_AUTOLOAD_DUMP => [['onPostAutoloadDump', 0]]]; 51 | } 52 | 53 | /** Callback for after the main composer autoload map has been updated. 54 | * 55 | * Here we update our autoload map. 56 | */ 57 | public function onPostAutoloadDump(Event $event) { 58 | $args = $event->isDevMode() ? '' : ' --no-dev'; 59 | $executor = new ProcessExecutor($this->io); 60 | $command = ProcessExecutor::escape($this->vendor.'/bin/hh-autoload.hack'). 61 | $args; 62 | $executor->execute($command); 63 | } 64 | 65 | /** Does nothing but required by Composer 2.0 */ 66 | public function deactivate(Composer $composer, IOInterface $io) { 67 | } 68 | 69 | /** Does nothing but required by Composer 2.0 */ 70 | public function uninstall(Composer $composer, IOInterface $io) { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/FailureHandler.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | /** Handle autoload requests for definitions that aren't in the map. 13 | * 14 | * If the handlers load a definition, then no error will be raised and the 15 | * autoload will be considered successful. 16 | */ 17 | <<__ConsistentConstruct>> 18 | abstract class FailureHandler { 19 | // Required for coeffects/capabilities to be defaults rather than pure 20 | public function __construct() { 21 | } 22 | 23 | /** 24 | * Called exactly once, once the autoload map has been set. 25 | */ 26 | public function initialize(): void {} 27 | 28 | /** If the handler should be used. 29 | * If you have a fallback method (e.g. HHClientFallbackHandler), you might 30 | * want to return false if running in CI. 31 | */ 32 | public static function isEnabled(): bool { 33 | return true; 34 | } 35 | 36 | /** Handle a class, typedef, enum etc */ 37 | abstract public function handleFailedType(string $name): void; 38 | 39 | /** Handle a function (not methods) */ 40 | abstract public function handleFailedFunction(string $name): void; 41 | 42 | /** Handle a constant lookup */ 43 | abstract public function handleFailedConstant(string $name): void; 44 | 45 | /** Main entry point. 46 | * 47 | * Parameters exactly match the expected parameters for a fallback function 48 | * in `HH\autoload_set_paths()`. 49 | */ 50 | final public function handleFailure(string $kind, string $name): void { 51 | if ($kind === 'class' || $kind === 'type') { 52 | $this->handleFailedType($name); 53 | return; 54 | } 55 | if ($kind === 'function') { 56 | $idx = \strrpos($name, '\\'); 57 | if ($idx !== false) { 58 | $suffix = \substr($name, $idx + 1); 59 | if (\function_exists($suffix, /* autoload = */ false)) { 60 | return; 61 | } 62 | } 63 | $this->handleFailedFunction($name); 64 | return; 65 | } 66 | if ($kind === 'constant') { 67 | $idx = \strrpos($name, '\\'); 68 | if ($idx !== false) { 69 | $suffix = \substr($name, $idx + 1); 70 | if (\defined($suffix, /* autoload = */ false)) { 71 | return; 72 | } 73 | } 74 | $this->handleFailedConstant($name); 75 | return; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '42 15 * * *' 7 | jobs: 8 | build: 9 | name: HHVM ${{matrix.hhvm}} - ${{matrix.os}} 10 | strategy: 11 | # Run tests on all OS's and HHVM versions, even if one fails 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu ] 15 | hhvm: 16 | - '4.153' 17 | - latest 18 | - nightly 19 | runs-on: ${{matrix.os}}-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Install Composer 23 | run: .github/workflows/install-composer.sh --install-dir=${{runner.temp}} 24 | - name: Install HHVM (apt) 25 | if: matrix.os == 'ubuntu' 26 | run: | 27 | set -ex 28 | export DEBIAN_FRONTEND=noninteractive 29 | sudo apt-get update 30 | sudo apt-get install -y software-properties-common apt-transport-https 31 | sudo apt-key add .github/workflows/hhvm.gpg.key 32 | if [ "${{matrix.hhvm}}" = "nightly" ]; then 33 | sudo add-apt-repository https://dl.hhvm.com/ubuntu 34 | sudo apt-get install -y hhvm-nightly 35 | elif [ "${{matrix.hhvm}}" = "latest" ]; then 36 | sudo add-apt-repository https://dl.hhvm.com/ubuntu 37 | sudo apt-get install -y hhvm 38 | else 39 | DISTRO=$(lsb_release --codename --short) 40 | sudo add-apt-repository \ 41 | "deb https://dl.hhvm.com/ubuntu ${DISTRO}-${{matrix.hhvm}} main" 42 | sudo apt-get remove -y hhvm || true 43 | sudo apt-get install -y hhvm 44 | fi 45 | - name: Inspect HHVM and Hack versions 46 | run: | 47 | hhvm --version 48 | hh_client --version 49 | - name: Create branch for version alias 50 | run: git checkout -b CI_current_pull_request 51 | - name: Install project dependencies 52 | run: php ${{runner.temp}}/composer.phar install 53 | - name: Run autoloader 54 | run: bin/hh-autoload 55 | - name: Show autoload map 56 | run: cat vendor/autoload.hack 57 | - name: Typecheck 58 | run: hh_client 59 | - name: Run most tests 60 | run: | 61 | # Work around https://github.com/hhvm/hacktest/issues/103 62 | vendor/bin/hacktest \ 63 | $(ls -1 tests/*hack | grep -v Fallback) 64 | - name: Run fallback handler test 65 | run: | 66 | ENABLE_HH_CLIENT_AUTOLOAD=true vendor/bin/hacktest \ 67 | tests/FallbackHandlerTest.hack 68 | - name: Test XHP configuration permutations 69 | run: | 70 | # FactParseScanner should work with any combination of enable_xhp_class_modifier 71 | # and disable_xhp_element_mangling 72 | export HHVM_CONFIG_FILE=$(mktemp) 73 | for A in false true; do 74 | for B in false true; do 75 | echo hhvm.hack.lang.enable_xhp_class_modifier=$A > $HHVM_CONFIG_FILE 76 | echo hhvm.hack.lang.disable_xhp_element_mangling=$B >> $HHVM_CONFIG_FILE 77 | vendor/bin/hacktest tests/ScannerTest.hack 78 | done 79 | done 80 | -------------------------------------------------------------------------------- /src/builders/HHImporter.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use namespace HH\Lib\Vec; 13 | 14 | /** Create an autoload map for a directory that contains an 15 | * `hh_autoload.json`. 16 | * 17 | * This may be used for the project root, and for any projects in 18 | * `vendor/` that are designed for use with `hhvm-autoload`. 19 | */ 20 | final class HHImporter implements Builder { 21 | private vec $builders = vec[]; 22 | private vec $files = vec[]; 23 | private Config $config; 24 | 25 | public function __construct(string $root, IncludedRoots $included_roots) { 26 | $config_file = $root.'/hh_autoload.json'; 27 | if (!\file_exists($config_file)) { 28 | $roots = Vec\filter(vec['src', 'lib'], $x ==> \is_dir($root.'/'.$x)); 29 | $dev_roots = Vec\filter( 30 | vec['test', 'tests', 'examples', 'example'], 31 | $x ==> \is_dir($root.'/'.$x), 32 | ); 33 | \file_put_contents( 34 | $config_file, 35 | \json_encode( 36 | shape( 37 | 'roots' => $roots, 38 | 'devRoots' => $dev_roots, 39 | 'devFailureHandler' => HHClientFallbackHandler::class, 40 | ), 41 | \JSON_PRETTY_PRINT, 42 | ). 43 | "\n", 44 | ); 45 | \fprintf( 46 | \STDERR, 47 | "An hh_autoload.json is required; a skeleton has been written to %s.\n". 48 | "If changes are needed, run vendor/bin/hh-autoload after editing.\n". 49 | "\n". 50 | "*** WARNING ***\n". 51 | "This project will not work correctly unless vendor/hh_autoload.php is required.\n". 52 | "*** WARNING ***\n". 53 | "\n", 54 | $config_file, 55 | ); 56 | } 57 | $config = ConfigurationLoader::fromFile($config_file); 58 | $this->config = $config; 59 | 60 | switch ($included_roots) { 61 | case IncludedRoots::PROD_ONLY: 62 | $roots = $config['roots']; 63 | break; 64 | case IncludedRoots::DEV_AND_PROD: 65 | $roots = Vec\concat($config['roots'], $config['devRoots']); 66 | break; 67 | } 68 | 69 | foreach ($roots as $tree) { 70 | if ($tree[0] !== '/') { 71 | $tree = $root.'/'.$tree; 72 | } 73 | $this->builders[] = Scanner::fromTree($tree, $config['parser']); 74 | } 75 | 76 | foreach ($config['extraFiles'] as $file) { 77 | if ($file[0] !== '/') { 78 | $file = $root.'/'.$file; 79 | } 80 | $this->files[] = $file; 81 | } 82 | } 83 | 84 | public function getAutoloadMap(): AutoloadMap { 85 | return Merger::merge( 86 | Vec\map($this->builders, $builder ==> $builder->getAutoloadMap()), 87 | ); 88 | } 89 | 90 | public function getFiles(): vec { 91 | return Vec\map($this->builders, $builder ==> $builder->getFiles()) 92 | |> Vec\concat(vec[$this->files], $$) 93 | |> Vec\flatten($$); 94 | } 95 | 96 | public function getConfig(): Config { 97 | return $this->config; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/TypeAssert.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | // Special-casing minimal reimplementation to avoid the dependency 11 | namespace Facebook\AutoloadMap\_Private\TypeAssert; 12 | 13 | function is_string(mixed $value, string $field): string { 14 | invariant($value is string, '%s should be a string', $field); 15 | return $value; 16 | } 17 | 18 | function is_nullable_string(mixed $value, string $field): ?string { 19 | if ($value === null) { 20 | return null; 21 | } 22 | invariant($value is string, '%s should be a ?string', $field); 23 | return $value; 24 | } 25 | 26 | function is_nullable_bool(mixed $value, string $field): ?bool { 27 | if ($value === null) { 28 | return null; 29 | } 30 | invariant($value is bool, '%s should be a ?bool', $field); 31 | return $value; 32 | } 33 | 34 | function is_array_of_strings(mixed $value, string $field): varray { 35 | invariant($value is Container<_>, '%s should be an array', $field); 36 | $out = varray[]; 37 | foreach ($value as $it) { 38 | invariant($it is string, '%s should be an array', $field); 39 | $out[] = $it; 40 | } 41 | return $out; 42 | } 43 | 44 | function is_vec_like_of_strings(mixed $value, string $field): vec { 45 | invariant($value is Traversable<_>, '%s should be a vec', $field); 46 | $out = vec[]; 47 | foreach ($value as $el) { 48 | invariant($el is string, '%s should be a vec', $field); 49 | $out[] = $el; 50 | } 51 | return $out; 52 | } 53 | 54 | function is_nullable_vec_like_of_strings( 55 | mixed $value, 56 | string $field, 57 | ): ?vec { 58 | if ($value === null) { 59 | return null; 60 | } 61 | 62 | invariant($value is Traversable<_>, '%s should be an ?vec', $field); 63 | $out = vec[]; 64 | foreach ($value as $it) { 65 | invariant($it is string, '%s should be an ?vec', $field); 66 | $out[] = $it; 67 | } 68 | return $out; 69 | } 70 | 71 | /* HH_IGNORE_ERROR[2053] enum usage */ 72 | function is_nullable_enum>( 73 | classname $what, 74 | mixed $value, 75 | string $field, 76 | ): ?Tval { 77 | if ($value === null) { 78 | return null; 79 | } 80 | $value = $what::coerce($value); 81 | invariant($value !== null, '%s should be a %s value', $field, $what); 82 | return $value; 83 | } 84 | 85 | function is_array_of_shapes_with_name_field_and_kind( 86 | mixed $value, 87 | string $field, 88 | ): varray string, 'kindOf' => string)> { 89 | $msg = 90 | $field.'should be a vec string, \'kindOf\' => string)>'; 91 | invariant($value is Traversable<_>, '%s', $msg); 92 | $out = varray[]; 93 | foreach ($value as $it) { 94 | invariant($it is KeyedContainer<_, _>, '%s', $msg); 95 | $name = $it['name'] ?? null; 96 | invariant($name is string, '%s', $msg); 97 | $kind_of = $it['kindOf'] ?? null; 98 | invariant($kind_of is string, '%s', $msg); 99 | $out[] = shape('name' => $name, 'kindOf' => $kind_of); 100 | } 101 | return $out; 102 | } 103 | -------------------------------------------------------------------------------- /src/ConfigurationLoader.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use Facebook\AutoloadMap\_Private\TypeAssert; 13 | 14 | /** Load configuration from JSON */ 15 | abstract final class ConfigurationLoader { 16 | /** Load configuration from a JSON file */ 17 | public static function fromFile(string $path): Config { 18 | invariant( 19 | \is_readable($path), 20 | 'Tried to load configuration file %s, but it is not readable.', 21 | $path, 22 | ); 23 | return self::fromJSON(\file_get_contents($path), $path); 24 | } 25 | 26 | /** Load configuration from a JSON string 27 | * 28 | * @param $path arbitrary string - used to create clearer error messages 29 | */ 30 | public static function fromJSON(string $json, string $path): Config { 31 | $decoded = \json_decode($json, /* as array = */ true); 32 | invariant( 33 | $decoded is KeyedContainer<_, _>, 34 | 'Expected configuration file to contain a JSON object, got %s', 35 | \gettype($decoded), 36 | ); 37 | return self::fromData($decoded, $path); 38 | } 39 | 40 | /** Load configuration from decoded data. 41 | * 42 | * @param $path arbitrary string - used to create clearer error messages 43 | */ 44 | public static function fromData( 45 | KeyedContainer $data, 46 | string $path, 47 | ): Config { 48 | $failure_handler = TypeAssert\is_nullable_string( 49 | $data['failureHandler'] ?? null, 50 | 'failureHandler', 51 | ); 52 | 53 | return shape( 54 | 'roots' => 55 | TypeAssert\is_vec_like_of_strings($data['roots'] ?? null, 'roots'), 56 | 'devRoots' => TypeAssert\is_nullable_vec_like_of_strings( 57 | $data['devRoots'] ?? null, 58 | 'devRoots', 59 | ) ?? 60 | vec[], 61 | 'relativeAutoloadRoot' => TypeAssert\is_nullable_bool( 62 | $data['relativeAutoloadRoot'] ?? null, 63 | 'relativerAutoloadRoot', 64 | ) ?? 65 | true, 66 | 'includeVendor' => TypeAssert\is_nullable_bool( 67 | $data['includeVendor'] ?? null, 68 | 'includeVendor', 69 | ) ?? 70 | true, 71 | 'extraFiles' => TypeAssert\is_nullable_vec_like_of_strings( 72 | $data['extraFiles'] ?? null, 73 | 'extraFiles', 74 | ) ?? 75 | vec[], 76 | 'parser' => TypeAssert\is_nullable_enum( 77 | Parser::class, 78 | $data['parser'] ?? null, 79 | 'parser', 80 | ) ?? 81 | self::getDefaultParser(), 82 | 'failureHandler' => $failure_handler, 83 | 'devFailureHandler' => TypeAssert\is_nullable_string( 84 | $data['devFailureHandler'] ?? null, 85 | 'devFailureHandler', 86 | ) ?? 87 | $failure_handler, 88 | 'useFactsIfAvailable' => ( 89 | TypeAssert\is_nullable_bool( 90 | $data['useFactsIfAvailable'] ?? null, 91 | 'useFactsIfAvailable', 92 | ) ?? 93 | false 94 | ) && 95 | \HH\Facts\enabled(), 96 | ); 97 | } 98 | 99 | private static function getDefaultParser(): Parser { 100 | invariant(\extension_loaded('factparse'), 'ext_factparse is now required'); 101 | return Parser::EXT_FACTPARSE; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/builders/FactParseScanner.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use Facebook\AutoloadMap\_Private\TypeAssert; 13 | use namespace HH\Lib\{C, Vec}; 14 | 15 | /** Create an autoload map from a directory using `ext_factparse`. */ 16 | final class FactParseScanner implements Builder { 17 | const type TFacts = darray varray string, 20 | 'kindOf' => string, 21 | )>, 22 | 'constants' => varray, 23 | 'functions' => varray, 24 | 'typeAliases' => varray, 25 | )>; 26 | 27 | private static function untypedToShape(mixed $data): self::TFacts { 28 | invariant( 29 | $data is KeyedTraversable<_, _>, 30 | 'FactsParse did not give us an array', 31 | ); 32 | 33 | $out = darray[]; 34 | foreach ($data as $file => $facts) { 35 | invariant($file is string, 'FactsParse data is not string-keyed'); 36 | invariant( 37 | $facts is KeyedContainer<_, _>, 38 | 'FactsParse data for file "%s" is not a KeyedContainer', 39 | $file, 40 | ); 41 | 42 | try { 43 | $types = TypeAssert\is_array_of_shapes_with_name_field_and_kind( 44 | $facts['types'] ?? vec[], 45 | 'FactParse types', 46 | ); 47 | $out[$file] = shape( 48 | 'types' => $types, 49 | 'constants' => TypeAssert\is_array_of_strings( 50 | $facts['constants'] ?? vec[], 51 | 'FactParse constants', 52 | ), 53 | 'functions' => TypeAssert\is_array_of_strings( 54 | $facts['functions'] ?? vec[], 55 | 'FactParse functions', 56 | ), 57 | 'typeAliases' => TypeAssert\is_array_of_strings( 58 | $facts['typeAliases'] ?? vec[], 59 | 'FactParse typeAliases', 60 | ), 61 | ); 62 | 63 | // On hhvm >4.160, typeAliases may not be present, 64 | // we can extract type aliases from `types` where `kindOf` === `typeAlias`. 65 | if (!C\contains_key($facts, 'typeAliases')) { 66 | $out[$file]['typeAliases'] = 67 | Vec\filter($types, $shape ==> $shape['kindOf'] === 'typeAlias') 68 | |> Vec\map($$, $shape ==> $shape['name']); 69 | } 70 | } catch (\Exception $e) { 71 | $error_level = \error_reporting(0); 72 | $file_is_empty = \filesize($file) === 0; 73 | \error_reporting($error_level); 74 | if ($file_is_empty) { 75 | continue; 76 | } 77 | throw new \Exception("Failed to parse '".$file.'"', $e->getCode(), $e); 78 | } 79 | } 80 | return $out; 81 | } 82 | 83 | private function __construct( 84 | private string $root, 85 | private vec $paths, 86 | ) { 87 | $version = (int)\phpversion('factparse'); 88 | invariant( 89 | $version === 3, 90 | 'Factparse version 3 is required, got %d', 91 | $version, 92 | ); 93 | } 94 | 95 | public static function fromFile(string $path): Builder { 96 | return new FactParseScanner('', vec[$path]); 97 | } 98 | 99 | public static function fromTree(string $root): Builder { 100 | $paths = vec[]; 101 | $rdi = new \RecursiveDirectoryIterator($root); 102 | $rii = new \RecursiveIteratorIterator($rdi); 103 | foreach ($rii as $info) { 104 | if (!$info->isFile()) { 105 | continue; 106 | } 107 | if (!$info->isReadable()) { 108 | continue; 109 | } 110 | $ext = $info->getExtension(); 111 | if ( 112 | $ext !== 'php' && 113 | $ext !== 'hh' && 114 | $ext !== 'xhp' && 115 | $ext !== 'hack' && 116 | $ext !== 'hck' 117 | ) { 118 | continue; 119 | } 120 | $paths[] = $info->getPathname(); 121 | } 122 | 123 | return new FactParseScanner($root, $paths); 124 | } 125 | 126 | public function getAutoloadMap(): AutoloadMap { 127 | $facts = \HH\facts_parse( 128 | $this->root, 129 | varray($this->paths), 130 | /* force_hh = */ false, 131 | /* multithreaded = */ true, 132 | ); 133 | $facts = self::untypedToShape($facts); 134 | 135 | $classes = dict[]; 136 | $functions = dict[]; 137 | $types = dict[]; 138 | $constants = dict[]; 139 | foreach ($facts as $file => $file_facts) { 140 | foreach ($file_facts['types'] as $type) { 141 | $classes[\strtolower($type['name'])] = $file; 142 | } 143 | foreach ($file_facts['constants'] as $const) { 144 | $constants[$const] = $file; 145 | } 146 | foreach ($file_facts['functions'] as $func) { 147 | $functions[\strtolower($func)] = $file; 148 | } 149 | foreach ($file_facts['typeAliases'] as $alias) { 150 | $types[\strtolower($alias)] = $file; 151 | } 152 | } 153 | return dict[ 154 | 'class' => $classes, 155 | 'function' => $functions, 156 | 'type' => $types, 157 | 'constant' => $constants, 158 | ]; 159 | } 160 | 161 | public function getFiles(): vec { 162 | return vec[]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /bin/hh-autoload.hack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | namespace Facebook\AutoloadMap; 12 | 13 | use namespace HH\Lib\Vec; 14 | 15 | final class GenerateScript { 16 | const type TOptions = shape( 17 | 'dev' => bool, 18 | 'no-facts' => bool, 19 | ); 20 | 21 | private static function initBootstrapAutoloader(): void { 22 | // NO HSL HERE - autoloader is not yet initialized 23 | $hsl_root = \getcwd().'/vendor/hhvm/hsl'; 24 | if (!\file_exists($hsl_root)) { 25 | // the HSL uses hhvm-autoload, but then the HSL is the root project, 26 | // not in vendor/ 27 | $hsl_root = \getcwd(); 28 | } 29 | $have_hsl = \file_get_contents($hsl_root.'/composer.json') 30 | |> \json_decode( 31 | $$, 32 | /* assoc = */ true, 33 | /* depth = */ 10, 34 | \JSON_FB_HACK_ARRAYS, 35 | ) 36 | |> $$['name'] ?? null 37 | |> $$ === 'hhvm/hsl'; 38 | if (!$have_hsl) { 39 | \fwrite(\STDERR, "Unable to find the Hack Standard Library"); 40 | exit(1); 41 | } 42 | $roots = vec[ 43 | \realpath(__DIR__.'/../src'), 44 | $hsl_root.'/src', 45 | ]; 46 | $paths = varray[]; 47 | foreach ($roots as $root) { 48 | foreach (self::getFileList($root) as $path) { 49 | $paths[] = $path; 50 | } 51 | } 52 | 53 | $facts = \HH\facts_parse( 54 | '/', 55 | $paths, 56 | /* force_hh = */ false, 57 | /* use_threads = */ true, 58 | ); 59 | 60 | $map = dict[ 61 | 'class' => dict[], 62 | 'function' => dict[], 63 | 'type' => dict[], 64 | 'constant' => dict[], 65 | ]; 66 | 67 | foreach ($facts as $path => $file_facts) { 68 | if ($file_facts === null) { 69 | continue; 70 | } 71 | $file_facts = $file_facts as dynamic; 72 | foreach ($file_facts['types'] ?? vec[] as $type) { 73 | $map['class'][\strtolower($type['name'] as string)] = $path; 74 | } 75 | foreach ($file_facts['constants'] ?? vec[] as $const) { 76 | $map['constant'][$const as string] = $path; 77 | } 78 | foreach ($file_facts['functions'] ?? vec[] as $fun) { 79 | $map['function'][\strtolower($fun as string)] = $path; 80 | } 81 | foreach ($file_facts['typeAliases'] ?? vec[] as $type) { 82 | $map['type'][\strtolower($type as string)] = $path; 83 | } 84 | } 85 | \HH\autoload_set_paths($map, '/'); 86 | } 87 | 88 | public static function main(vec $argv): void { 89 | self::checkRoot(); 90 | self::initBootstrapAutoloader(); 91 | $options = self::parseOptions($argv); 92 | self::generateAutoloader($options); 93 | } 94 | 95 | private static function parseOptions(vec $argv): self::TOptions { 96 | $options = shape( 97 | 'dev' => true, 98 | 'no-facts' => false, 99 | ); 100 | $bin = $argv[0]; 101 | $argv = Vec\slice($argv, 1); 102 | foreach ($argv as $arg) { 103 | switch ($arg) { 104 | case '--no-dev': 105 | $options['dev'] = false; 106 | break; 107 | case '--no-facts': 108 | $options['no-facts'] = true; 109 | break; 110 | case '--help': 111 | self::printUsage(\STDOUT, $bin); 112 | exit(0); 113 | default: 114 | \fprintf(\STDERR, "Unrecognized option: '%s'\n", $arg); 115 | self::printUsage(\STDERR, $bin); 116 | exit(1); 117 | } 118 | } 119 | 120 | return $options; 121 | } 122 | 123 | private static function checkRoot(): void { 124 | // NO HSL HERE - autoloader is not yet initialized 125 | if (!\file_exists('hh_autoload.json')) { 126 | if (!\HH\autoload_is_native()) { 127 | \fwrite( 128 | \STDERR, 129 | "This executable must be ran from a directory containing an ". 130 | "hh_autoload.json\n", 131 | ); 132 | exit(1); 133 | } 134 | \fwrite( 135 | \STDERR, 136 | "hh_autoload.json could not be found, but a native autoloader was detected.\n". 137 | "If you intend to use native autoloading, you may ignore this message.\n", 138 | ); 139 | exit(0); 140 | } 141 | } 142 | 143 | private static function generateAutoloader(self::TOptions $options): void { 144 | $importer = new RootImporter( 145 | \getcwd(), 146 | $options['dev'] ? IncludedRoots::DEV_AND_PROD : IncludedRoots::PROD_ONLY, 147 | ); 148 | 149 | $config = $importer->getConfig(); 150 | 151 | $handler = $options['dev'] 152 | ? $config['devFailureHandler'] 153 | : $config['failureHandler']; 154 | 155 | $emit_facts_forwarder_file = 156 | $config['useFactsIfAvailable'] && !$options['no-facts']; 157 | 158 | (new Writer()) 159 | ->setBuilder($importer) 160 | ->setRoot(\getcwd()) 161 | ->setRelativeAutoloadRoot($config['relativeAutoloadRoot']) 162 | ->setFailureHandler(/* HH_IGNORE_ERROR[4110] */ $handler) 163 | ->setIsDev($options['dev']) 164 | ->setEmitFactsForwarderFile($emit_facts_forwarder_file) 165 | ->writeToDirectory(\getcwd().'/vendor/'); 166 | print(\getcwd()."/vendor/autoload.hack\n"); 167 | } 168 | 169 | private static function printUsage(resource $to, string $bin): void { 170 | \fprintf( 171 | $to, 172 | "USAGE: %s [--no-dev] [--no-facts]\n". 173 | "See the README for full information.\n". 174 | "The README can be found at:\n\t- %s\n\t- %s.\n", 175 | $bin, 176 | \getcwd().'/vendor/hhvm/hhvm-autoload/README.md', 177 | // ^^^^^^ Not accurate if hhvm-autoload is the top level project. 178 | 'https://github.com/hhvm/hhvm-autoload/blob/main/README.md', 179 | ); 180 | } 181 | 182 | private static function getFileList(string $root): vec { 183 | // NO HSL HERE - autoloader is not yet initialized 184 | $rdi = new \RecursiveDirectoryIterator($root); 185 | $rii = new \RecursiveIteratorIterator( 186 | $rdi, 187 | \RecursiveIteratorIterator::CHILD_FIRST, 188 | ); 189 | $out = vec[]; 190 | // All we care about is autoloading hhvm-autoload itself and the HSL; 191 | // we don't need to support every valid extension 192 | $extensions = keyset['php', 'hack']; 193 | foreach ($rii as $file_info) { 194 | if (!$file_info->isFile()) { 195 | continue; 196 | } 197 | if (($extensions[$file_info->getExtension()] ?? false) == false) { 198 | continue; 199 | } 200 | $out[] = $file_info->getPathname(); 201 | } 202 | 203 | return $out; 204 | } 205 | } 206 | 207 | <<__EntryPoint>> 208 | function cli_main(): noreturn { 209 | GenerateScript::main(vec(/* HH_FIXME[4110] */ \HH\global_get('argv'))); 210 | exit(0); 211 | } 212 | -------------------------------------------------------------------------------- /src/HHClientFallbackHandler.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use namespace HH\Lib\{C, Str, Vec}; 13 | 14 | /** 15 | * If a class/function/type isn't in the map, ask `hh_client` where it is. 16 | * 17 | * This caches the results in APC (falling back to a file) to provide fast 18 | * workflows. 19 | * 20 | * Does nothing if CI, TRAVIS, or CONTINUOUS_INTEGRATION is true. 21 | */ 22 | class HHClientFallbackHandler extends FailureHandler { 23 | private AutoloadMap $map; 24 | private bool $dirty = false; 25 | const type TCache = shape( 26 | 'build_id' => string, 27 | 'map' => AutoloadMap, 28 | ); 29 | 30 | public function __construct() { 31 | $this->map = Generated\map(); 32 | parent::__construct(); 33 | } 34 | 35 | /** Retrieve the cached autoload map. 36 | * 37 | * This will try to retrieve a cached map from APC, and if that fails, 38 | * a cache file. 39 | */ 40 | protected function getCache(): ?self::TCache { 41 | $key = __CLASS__.'!cache'; 42 | if (\apc_exists($key)) { 43 | $success = false; 44 | $data = \apc_fetch($key, inout $success); 45 | if (!$success) { 46 | return null; 47 | } 48 | return $data; 49 | } 50 | $file = $this->getCacheFilePath(); 51 | if (!\file_exists($file)) { 52 | return null; 53 | } 54 | 55 | $data = \json_decode( 56 | \file_get_contents($file), 57 | /* as array = */ true, 58 | ); 59 | if ($data === null) { 60 | $this->dirty = true; 61 | \unlink($file); 62 | return null; 63 | } 64 | 65 | return $data; 66 | } 67 | 68 | /** Store a cached map in APC and file. 69 | * 70 | * If the file is not writable, it will only be stored in APC. 71 | */ 72 | protected function storeCache(self::TCache $data): void { 73 | \apc_store(__CLASS__.'!cache', $data); 74 | 75 | if (!\is_writable(\dirname($this->getCacheFilePath()))) { 76 | return; 77 | } 78 | 79 | \file_put_contents( 80 | $this->getCacheFilePath(), 81 | \json_encode($data, \JSON_PRETTY_PRINT), 82 | ); 83 | } 84 | 85 | <<__Override>> 86 | public function initialize(): void { 87 | $data = $this->getCache(); 88 | if ($data === null) { 89 | return; 90 | } 91 | if ($data['build_id'] !== Generated\build_id()) { 92 | $this->dirty = true; 93 | return; 94 | } 95 | $map = $data['map']; 96 | $this->map = $map; 97 | $map['failure'] = inst_meth($this, 'handleFailure'); 98 | \HH\autoload_set_paths( 99 | /* HH_IGNORE_ERROR[4110] incorrect hhi */ $map, 100 | Generated\root(), 101 | ); 102 | 103 | \register_shutdown_function(() ==> $this->storeCacheIfDirtyDirty()); 104 | } 105 | 106 | private function storeCacheIfDirtyDirty(): void { 107 | if (!$this->dirty) { 108 | return; 109 | } 110 | $data = shape( 111 | 'build_id' => Generated\build_id(), 112 | 'map' => $this->map, 113 | ); 114 | $this->storeCache($data); 115 | } 116 | 117 | /** Where to store the file cache */ 118 | protected function getCacheFilePath(): string { 119 | return Generated\root().'/vendor/hh_autoload.hh-cache'; 120 | } 121 | 122 | /** Whether or not to use `hh_client`. 123 | * 124 | * Defaults to true, unless we're on a common CI platform. 125 | */ 126 | <<__Override>> 127 | public static function isEnabled(): bool { 128 | $force = \getenv('ENABLE_HH_CLIENT_AUTOLOAD'); 129 | if ($force === 'true' || $force === '1') { 130 | return true; 131 | } 132 | if ($force === 'false' || $force === '0') { 133 | return false; 134 | } 135 | 136 | $killswitches = keyset['CI', 'TRAVIS', 'CONTINUOUS_INTEGRATION']; 137 | foreach ($killswitches as $killswitch) { 138 | $env = \getenv($killswitch); 139 | if ($env === 'true' || $env === '1') { 140 | return false; 141 | } 142 | } 143 | return true; 144 | } 145 | 146 | <<__Override>> 147 | public function handleFailedType(string $name): void { 148 | $file = $this->lookupPath('class', $name); 149 | if ($file === null) { 150 | if (Str\slice($name, 0, 4) === 'xhp_') { 151 | $xhp_name = ':'. 152 | Str\replace_every(Str\slice($name, 4), dict['__' => ':', '_' => '-']); 153 | $file = $this->lookupPath('class', $xhp_name); 154 | } 155 | 156 | if ($file === null) { 157 | $file = $this->lookupPath('typedef', $name); 158 | } 159 | } 160 | 161 | if ($file === null) { 162 | return; 163 | } 164 | 165 | $this->requireFile($file); 166 | } 167 | 168 | <<__Override>> 169 | public function handleFailedFunction(string $name): void { 170 | $file = $this->lookupPath('function', $name); 171 | if ($file === null) { 172 | return; 173 | } 174 | 175 | $this->requireFile($file); 176 | } 177 | 178 | <<__Override>> 179 | public function handleFailedConstant(string $name): void { 180 | $file = $this->lookupPath('constant', $name); 181 | if ($file === null) { 182 | return; 183 | } 184 | 185 | $this->requireFile($file); 186 | } 187 | 188 | static dict $cache = dict[]; 189 | 190 | private function lookupPath(string $kind, string $name): ?string { 191 | $key = $kind.'!'.$name; 192 | if (C\contains_key(static::$cache, $key)) { 193 | return static::$cache[$key]; 194 | } 195 | 196 | $path = $this->lookupPathImpl($kind, $name); 197 | static::$cache[$key] = $path; 198 | 199 | if ($path === null) { 200 | return $path; 201 | } 202 | 203 | switch ($kind) { 204 | case 'class': 205 | $this->map['class'][\strtolower($name)] = $path; 206 | break; 207 | case 'type': 208 | case 'typedef': 209 | $this->map['type'][\strtolower($name)] = $path; 210 | break; 211 | case 'function': 212 | $this->map['function'][\strtolower($name)] = $path; 213 | break; 214 | case 'constant': 215 | $this->map['constant'][$name] = $path; 216 | break; 217 | } 218 | $this->dirty = true; 219 | return $path; 220 | } 221 | 222 | private function lookupPathImpl(string $kind, string $name): ?string { 223 | $cmd = Vec\map( 224 | vec['hh_client', '--json', '--search-'.$kind, $name], 225 | $x ==> \escapeshellarg($x), 226 | ); 227 | $cmd = \implode(' ', $cmd); 228 | 229 | $exit_code = null; 230 | $_output = varray[]; 231 | $last = \exec($cmd, inout $_output, inout $exit_code); 232 | if ($exit_code !== 0) { 233 | return null; 234 | } 235 | 236 | $data = \json_decode($last, /* assoc = */ true); 237 | if (!$data is Traversable<_>) { 238 | return null; 239 | } 240 | foreach ($data as $row) { 241 | $row as KeyedContainer<_, _>; 242 | if ($row['name'] === $name) { 243 | $file = $row['filename'] as ?string; 244 | if ($file is null || \substr($file, -4) === '.hhi') { 245 | return null; 246 | } 247 | return $file; 248 | } 249 | } 250 | return null; 251 | } 252 | 253 | private function requireFile(string $path): void { 254 | require $path; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HHVM-Autoload 2 | ============= 3 | The autoloader for autoloading classes, enums, functions, typedefs, and constants on HHVM. 4 | 5 | [![Continuous Integration](https://github.com/hhvm/hhvm-autoload/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/hhvm/hhvm-autoload/actions/workflows/build-and-test.yml) 6 | 7 | Usage 8 | ===== 9 | 10 | 1. Add an `hh_autoload.json` file (see section below) 11 | 2. `composer require hhvm/hhvm-autoload` 12 | 3. Require the autoload file from your entrypoint functions using `require_once (__DIR__ . '(/..)(repeat as needed)/vendor/autoload.hack');` 13 | 4. Call `Facebook\AutoloadMap\initialize()` to register the autoloader with hhvm. 14 | 5. To re-generate the map, run `vendor/bin/hh-autoload`, `composer dump-autoload`, or any other command that generates the map 15 | 16 | Configuration (`hh_autoload.json`) 17 | ================================== 18 | 19 | A minimal configuration file is: 20 | 21 | ```JSON 22 | { 23 | "roots": [ "src/" ] 24 | } 25 | ``` 26 | 27 | This will look for autoloadable definitions in `src/`, and also look in `vendor/`. 28 | Projects in `vendor/` are only processed if they also contain a `hh_autoload.json` file. 29 | 30 | Previously we also supported projects without `hh_autoload.json` by simulating Composer's autoload behavior, but we no longer do because that mostly applied to PHP files which HHVM can no longer parse. 31 | 32 | The following settings are optional: 33 | 34 | - `"extraFiles": ["file1.hack"]` - files that should not be autoloaded, but should be `require()`ed by `vendor/autoload.hack`. This should be needed much less frequently than under Composer 35 | - `"includeVendor": false` - do not include `vendor/` definitions in `vendor/autoload.hack` 36 | - `"devRoots": [ "path/", ...]` - additional roots to only include in dev mode, not when installed as a dependency. 37 | - `"relativeAutoloadRoot": false` - do not use a path relative to `__DIR__` for autoloading. Instead, use the path to the folder containing `hh_autoload.json` when building the autoload map. 38 | - `"failureHandler:" classname` - use the specified class to handle definitions that aren't the Map. Defaults to none. 39 | - `"devFailureHandler": classname` - use a different handler for development environments. Defaults to the same value as `failureHandler`. 40 | - `"parser:" any of [ext-factparse]"` - select a parser to use, but there is only one valid option. Defaults to a sensible parser. 41 | - `"useFactsIfAvailable": false` - use ext-facts (HH\Facts\...) to back Facebook\AutoloadMap\Generated\map() instead of a codegenned dict. See _Use with HH\Facts_ for more information about this mode. 42 | 43 | Use In Development (Failure Handlers) 44 | ===================================== 45 | 46 | When you add, remove, or move definitions, there are several options available: 47 | 48 | - run `composer dump-autoload` to regenerate the map 49 | - run `vendor/bin/hh-autoload` to regenerate the map faster 50 | - specify `devFailureHandler` as `Facebook\AutoloadMap\HHClientFallbackHandler` 51 | - specify a custom subclass of `Facebook\AutoloadMap\FailureHandler` 52 | - use a filesystem monitor such as 53 | [watchman](https://facebook.github.io/watchman/) to invoke one of the above 54 | commands when necessary 55 | 56 | `Facebook\AutoloadMap\HHClientFallbackHandler` is probably the most 57 | convenient for Hack development. 58 | 59 | HHClientFallbackHandler 60 | ----------------------- 61 | 62 | If you are working in Hack, this handler will remove the need to manually 63 | rebuild the map in almost all circumstances. 64 | 65 | It asks `hh_client` for definitions that aren't in the map, and has the 66 | following additional behaviors: 67 | 68 | - it is disabled if the `CI`, `CONTINUOUS_INTEGRATION`, or `TRAVIS` 69 | environment variables are set to a Truthy value; this is because it 70 | is not a recommended approach for production environments, and you 71 | probably want your automated testing environment to reflect 72 | production 73 | - results are cached in both APC and a file in vendor/, if vendor/ is 74 | writable 75 | 76 | You can override these behaviors in a subclass. 77 | 78 | Custom Handlers 79 | --------------- 80 | 81 | Information you may need is available from: 82 | 83 | - `Facebook\AutoloadMap\Generated\build_id()`: this is unique ID 84 | regenerated every time the map is rebuilt; it includes the date, 85 | time, and a long random hex string. If your failure handler has a 86 | cache, it most likely should be invalidated when this changes, for 87 | example, by including it in the cache key. 88 | - `Facebook\AutoloadMap\Generated\map()`: the autoload map 89 | - `Facebook\AutoloadMap\Generated\root()`: the directory containing the 90 | project root, i.e. the parent directory of `vendor/` 91 | 92 | Use with HH\\Facts 93 | ================= 94 | 95 | HHVM 4.109 introduced ext-facts and ext-watchman. Unlike the static pre-built autoloader which is built into a [repo authoratative](https://docs.hhvm.com/hhvm/advanced-usage/repo-authoritative) build, this native autoloader works incrementally and is suitable for autoloading in your development environment. For more information about setting up this autoloader, see the [blog post](https://hhvm.com/blog/2021/05/11/hhvm-4.109.html) for hhvm 4.109. 96 | 97 | When using a native autoloader (either the repo auth or ext-facts autoloader), you do not need hhvm-autoload to require classes/functions/types/constants at runtime. If you (and your vendor dependencies) do not call any functions in the `Facebook\AutoloadMap` namespace, other than `Facebook\AutoloadMap\initialize()`, you don't need hhvm-autoload anymore. In that case, you could drop this dependency and remove the calls to `initialize()`. If you are using other functions, like `Facebook\AutoloadMap\Generated\map()`, you'd still need the vendor/autoload.hack file that hhvm-autoload generates. 98 | 99 | Hhvm-autoload supports outputting a vendor/autoload.hack file which forwards all queries to ext-facts. `Facebook\AutoloadMap\Generated\map_uncached()` will always be up to date in this mode, since `HH\Facts` is always up to date. `Facebook\AutoloadMap\Generated\map()` is memoized (within a request), since some code may rely on getting the same result from multiple calls. You can enable this mode by adding `"useFactsIfAvailable": true` to the hh_autoload.json config file. Hhvm-autoload will emit a shim file instead of a full map. This option is ignored if `HH\Facts\enabled()` returns false, or when `--no-facts` is passed to `vendor/bin/hh-autoload`. We recommend passing `--no-facts` when building for production (specifically repo auth mode). Returning a hardcoded dict is faster than asking `HH\Facts`. 100 | 101 | Important to note. Autoloading with a native autoloader does not respect hh_autoload.json. The repo auth autoloader allows any code to use any symbol. The facts autoloader honors the configuration in .hhvmconfig.hdf instead. Make sure that the configuration in hh_autoload.json and .hhvmconfig.hdf match. 102 | 103 | How It Works 104 | ============ 105 | 106 | - A parser (FactParse) provides a list of all Hack definitions in the specified locations 107 | - This is used to generate something similar to a classmap, except including other kinds of definitions 108 | - The map is provided to HHVM with [`HH\autoload_set_paths()`](https://docs.hhvm.com/hack/reference/function/HH.autoload_set_paths/) 109 | - If a native autoloader is registered, this autoloader will intentionally not register itself. So calling `Facebook\AutoloadMap\initialize()` in repo auth mode or when the facts based autoloader is registered is a noop. 110 | 111 | Contributing 112 | ============ 113 | 114 | We welcome GitHub issues and pull requests - please see CONTRIBUTING.md for details. 115 | 116 | License 117 | ======= 118 | 119 | hhvm-autoload is [MIT-licensed](LICENSE). 120 | -------------------------------------------------------------------------------- /src/Writer.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\AutoloadMap; 11 | 12 | use namespace HH\Lib\{Str, Vec}; 13 | 14 | /** Class to write `autoload.hack`. 15 | * 16 | * This includes: 17 | * - the autoload map 18 | * - any files to explicitly require 19 | * - several autogenerated convenience functions 20 | * - the failure handler 21 | */ 22 | final class Writer { 23 | private ?vec $files; 24 | private ?AutoloadMap $map; 25 | private ?string $root; 26 | private bool $relativeAutoloadRoot = true; 27 | private ?string $failureHandler; 28 | private bool $isDev = true; 29 | private bool $emitFactsForwarderFile = false; 30 | 31 | /** Mark whether we're running in development mode. 32 | * 33 | * This is only used for `Generated\is_dev()` - the map should already be 34 | * filtered appropriately. 35 | */ 36 | public function setIsDev(bool $is_dev): this { 37 | $this->isDev = $is_dev; 38 | return $this; 39 | } 40 | 41 | /** Class to use to handle lookups for items not in the map */ 42 | public function setFailureHandler(?classname $handler): this { 43 | $this->failureHandler = $handler; 44 | return $this; 45 | } 46 | 47 | /** Files to explicitly include */ 48 | public function setFiles(vec $files): this { 49 | $this->files = $files; 50 | return $this; 51 | } 52 | 53 | /** The actual autoload map */ 54 | public function setAutoloadMap(AutoloadMap $map): this { 55 | $this->map = $map; 56 | return $this; 57 | } 58 | 59 | public function setEmitFactsForwarderFile(bool $should_forward): this { 60 | $this->emitFactsForwarderFile = $should_forward; 61 | return $this; 62 | } 63 | 64 | /** Set the files and maps from a builder. 65 | * 66 | * Convenience function; this is equivalent to calling `setFiles()` and 67 | * `setAutoloadMap()`. 68 | */ 69 | public function setBuilder(Builder $builder): this { 70 | $this->files = $builder->getFiles(); 71 | $this->map = $builder->getAutoloadMap(); 72 | return $this; 73 | } 74 | 75 | /** Set the root directory of the project */ 76 | public function setRoot(string $root): this { 77 | $this->root = \realpath($root); 78 | return $this; 79 | } 80 | 81 | /** Set whether the autoload map should contain relative or absolute paths */ 82 | public function setRelativeAutoloadRoot(bool $relative): this { 83 | $this->relativeAutoloadRoot = $relative; 84 | return $this; 85 | } 86 | 87 | public function writeToDirectory(string $directory): this { 88 | $this->writeToFile($directory.'/autoload.hack'); 89 | 90 | return $this; 91 | } 92 | 93 | /** Write the file to disk. 94 | * 95 | * You will need to call these first: 96 | * - `setFiles()` 97 | * - `setAutoloadMap()` 98 | * - `setIsDev()` 99 | */ 100 | public function writeToFile(string $destination_file): this { 101 | $files = $this->files; 102 | $map = $this->map; 103 | $is_dev = $this->isDev; 104 | 105 | if ($files === null) { 106 | throw new Exception('Call setFiles() before writeToFile()'); 107 | } 108 | if ($map === null) { 109 | throw new Exception('Call setAutoloadMap() before writeToFile()'); 110 | } 111 | if ($is_dev === null) { 112 | throw new Exception('Call setIsDev() before writeToFile()'); 113 | } 114 | $is_dev = $is_dev ? 'true' : 'false'; 115 | 116 | if ($this->relativeAutoloadRoot) { 117 | $root = '__DIR__.\'/../\''; 118 | $requires = Vec\map( 119 | $files, 120 | $file ==> 121 | '__DIR__.'.\var_export('/../'.$this->relativePath($file), true), 122 | ); 123 | } else { 124 | $root_maybe_null = $this->root ?? ''; 125 | $root = \var_export($root_maybe_null.'/', true); 126 | $requires = Vec\map( 127 | $files, 128 | $file ==> 129 | \var_export($root_maybe_null.'/'.$this->relativePath($file), true), 130 | ); 131 | } 132 | 133 | $requires = \implode( 134 | "\n", 135 | Vec\map($requires, $require ==> 'require_once('.$require.');'), 136 | ); 137 | 138 | $map = \array_map( 139 | ($sub_map): mixed ==> { 140 | return \array_map( 141 | $path ==> $this->relativePath($path as string), 142 | $sub_map as KeyedContainer<_, _>, 143 | ); 144 | }, 145 | $map, 146 | ); 147 | 148 | $failure_handler = $this->failureHandler; 149 | if ($failure_handler !== null) { 150 | if (\substr($failure_handler, 0, 1) !== '\\') { 151 | $failure_handler = '\\'.$failure_handler; 152 | } 153 | } 154 | 155 | if ($failure_handler !== null) { 156 | $add_failure_handler = \sprintf( 157 | "if (%s::isEnabled()) {\n". 158 | " \$handler = new %s();\n". 159 | " \$map['failure'] = inst_meth(\$handler, 'handleFailure');\n". 160 | " \HH\autoload_set_paths(/* HH_FIXME[4110] incorrect hhi */ \$map, Generated\\root());\n". 161 | " \$handler->initialize();\n". 162 | "}", 163 | $failure_handler, 164 | $failure_handler, 165 | ); 166 | } else { 167 | $add_failure_handler = ''; 168 | } 169 | 170 | $build_id = 171 | \var_export(\date(\DateTime::ATOM).'!'.\bin2hex(\random_bytes(16)), true); 172 | 173 | if (!$this->emitFactsForwarderFile) { 174 | $memoize = ''; 175 | $map_as_string = \var_export($map, true) 176 | |> \str_replace('array (', 'dict[', $$) 177 | |> \str_replace(')', ']', $$); 178 | } else { 179 | $memoize = "\n<<__Memoize>>"; 180 | $map_as_string = <<<'EOF' 181 | dict[ 182 | 'class' => \HH\Lib\Dict\map_keys(\HH\Facts\all_types(), \HH\Lib\Str\lowercase<>), 183 | 'function' => \HH\Lib\Dict\map_keys(\HH\Facts\all_functions(), \HH\Lib\Str\lowercase<>), 184 | 'type' => \HH\Lib\Dict\map_keys(\HH\Facts\all_type_aliases(), \HH\Lib\Str\lowercase<>), 185 | 'constants' => \HH\Facts\all_constants(), 186 | ] 187 | EOF; 188 | } 189 | 190 | if ($this->relativeAutoloadRoot) { 191 | try { 192 | $autoload_map_typedef = '__DIR__.'. 193 | \var_export( 194 | '/../'.$this->relativePath(__DIR__.'/AutoloadMap.hack'), 195 | true, 196 | ); 197 | } catch (\Exception $_) { 198 | // Our unit tests need to load it, and are rooted in the tests/ subdir 199 | $autoload_map_typedef = \var_export(__DIR__.'/AutoloadMap.hack', true); 200 | } 201 | } else { 202 | $autoload_map_typedef = \var_export(__DIR__.'/AutoloadMap.hack', true); 203 | } 204 | $code = <<> 218 | function is_dev(): bool { 219 | \$override = \getenv('HH_FORCE_IS_DEV'); 220 | if (\$override === false) { 221 | return $is_dev; 222 | } 223 | return (bool) \$override; 224 | } 225 | 226 | function map_uncached(): \Facebook\AutoloadMap\AutoloadMap { 227 | return $map_as_string; 228 | } 229 | $memoize 230 | function map(): \Facebook\AutoloadMap\AutoloadMap { 231 | return map_uncached(); 232 | } 233 | 234 | } // Generated\ 235 | 236 | namespace Facebook\AutoloadMap\_Private { 237 | final class GlobalState { 238 | public static bool \$initialized = false; 239 | } 240 | 241 | function bootstrap(): void { 242 | require_once($autoload_map_typedef); 243 | $requires 244 | } 245 | } 246 | 247 | namespace Facebook\AutoloadMap { 248 | 249 | function initialize(): void { 250 | if (_Private\GlobalState::\$initialized) { 251 | return; 252 | } 253 | if (\\HH\\autoload_is_native()) { 254 | return; 255 | } 256 | _Private\GlobalState::\$initialized = true; 257 | _Private\bootstrap(); 258 | \$map = Generated\\map(); 259 | 260 | \HH\autoload_set_paths(/* HH_FIXME[4110] incorrect hhi */ \$map, Generated\\root()); 261 | 262 | $add_failure_handler 263 | } 264 | 265 | } 266 | EOF; 267 | \file_put_contents($destination_file, $code); 268 | 269 | return $this; 270 | } 271 | 272 | <<__Memoize>> 273 | private function relativePath(string $path): string { 274 | $root = $this->root; 275 | if ($root === null) { 276 | throw new Exception('Call setRoot() before writeToFile()'); 277 | } 278 | $path = \realpath($path); 279 | if (Str\starts_with($path, $root)) { 280 | return Str\slice($path, Str\length($root) + 1); 281 | } 282 | throw new Exception("%s is outside root %s", $path, $root); 283 | } 284 | } 285 | --------------------------------------------------------------------------------