src/Writer.hack (206 lines of code) (raw):
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
namespace Facebook\AutoloadMap;
use namespace HH\Lib\{Str, Vec};
/** Class to write `autoload.hack`.
*
* This includes:
* - the autoload map
* - any files to explicitly require
* - several autogenerated convenience functions
* - the failure handler
*/
final class Writer {
private ?vec<string> $files;
private ?AutoloadMap $map;
private ?string $root;
private bool $relativeAutoloadRoot = true;
private ?string $failureHandler;
private bool $isDev = true;
private bool $emitFactsForwarderFile = false;
/** Mark whether we're running in development mode.
*
* This is only used for `Generated\is_dev()` - the map should already be
* filtered appropriately.
*/
public function setIsDev(bool $is_dev): this {
$this->isDev = $is_dev;
return $this;
}
/** Class to use to handle lookups for items not in the map */
public function setFailureHandler(?classname<FailureHandler> $handler): this {
$this->failureHandler = $handler;
return $this;
}
/** Files to explicitly include */
public function setFiles(vec<string> $files): this {
$this->files = $files;
return $this;
}
/** The actual autoload map */
public function setAutoloadMap(AutoloadMap $map): this {
$this->map = $map;
return $this;
}
public function setEmitFactsForwarderFile(bool $should_forward): this {
$this->emitFactsForwarderFile = $should_forward;
return $this;
}
/** Set the files and maps from a builder.
*
* Convenience function; this is equivalent to calling `setFiles()` and
* `setAutoloadMap()`.
*/
public function setBuilder(Builder $builder): this {
$this->files = $builder->getFiles();
$this->map = $builder->getAutoloadMap();
return $this;
}
/** Set the root directory of the project */
public function setRoot(string $root): this {
$this->root = \realpath($root);
return $this;
}
/** Set whether the autoload map should contain relative or absolute paths */
public function setRelativeAutoloadRoot(bool $relative): this {
$this->relativeAutoloadRoot = $relative;
return $this;
}
public function writeToDirectory(string $directory): this {
$this->writeToFile($directory.'/autoload.hack');
return $this;
}
/** Write the file to disk.
*
* You will need to call these first:
* - `setFiles()`
* - `setAutoloadMap()`
* - `setIsDev()`
*/
public function writeToFile(string $destination_file): this {
$files = $this->files;
$map = $this->map;
$is_dev = $this->isDev;
if ($files === null) {
throw new Exception('Call setFiles() before writeToFile()');
}
if ($map === null) {
throw new Exception('Call setAutoloadMap() before writeToFile()');
}
if ($is_dev === null) {
throw new Exception('Call setIsDev() before writeToFile()');
}
$is_dev = $is_dev ? 'true' : 'false';
if ($this->relativeAutoloadRoot) {
$root = '__DIR__.\'/../\'';
$requires = Vec\map(
$files,
$file ==>
'__DIR__.'.\var_export('/../'.$this->relativePath($file), true),
);
} else {
$root_maybe_null = $this->root ?? '';
$root = \var_export($root_maybe_null.'/', true);
$requires = Vec\map(
$files,
$file ==>
\var_export($root_maybe_null.'/'.$this->relativePath($file), true),
);
}
$requires = \implode(
"\n",
Vec\map($requires, $require ==> 'require_once('.$require.');'),
);
$map = \array_map(
($sub_map): mixed ==> {
return \array_map(
$path ==> $this->relativePath($path as string),
$sub_map as KeyedContainer<_, _>,
);
},
$map,
);
$failure_handler = $this->failureHandler;
if ($failure_handler !== null) {
if (\substr($failure_handler, 0, 1) !== '\\') {
$failure_handler = '\\'.$failure_handler;
}
}
if ($failure_handler !== null) {
$add_failure_handler = \sprintf(
"if (%s::isEnabled()) {\n".
" \$handler = new %s();\n".
" \$map['failure'] = inst_meth(\$handler, 'handleFailure');\n".
" \HH\autoload_set_paths(/* HH_FIXME[4110] incorrect hhi */ \$map, Generated\\root());\n".
" \$handler->initialize();\n".
"}",
$failure_handler,
$failure_handler,
);
} else {
$add_failure_handler = '';
}
$build_id =
\var_export(\date(\DateTime::ATOM).'!'.\bin2hex(\random_bytes(16)), true);
if (!$this->emitFactsForwarderFile) {
$memoize = '';
$map_as_string = \var_export($map, true)
|> \str_replace('array (', 'dict[', $$)
|> \str_replace(')', ']', $$);
} else {
$memoize = "\n<<__Memoize>>";
$map_as_string = <<<'EOF'
dict[
'class' => \HH\Lib\Dict\map_keys(\HH\Facts\all_types(), \HH\Lib\Str\lowercase<>),
'function' => \HH\Lib\Dict\map_keys(\HH\Facts\all_functions(), \HH\Lib\Str\lowercase<>),
'type' => \HH\Lib\Dict\map_keys(\HH\Facts\all_type_aliases(), \HH\Lib\Str\lowercase<>),
'constants' => \HH\Facts\all_constants(),
]
EOF;
}
if ($this->relativeAutoloadRoot) {
try {
$autoload_map_typedef = '__DIR__.'.
\var_export(
'/../'.$this->relativePath(__DIR__.'/AutoloadMap.hack'),
true,
);
} catch (\Exception $_) {
// Our unit tests need to load it, and are rooted in the tests/ subdir
$autoload_map_typedef = \var_export(__DIR__.'/AutoloadMap.hack', true);
}
} else {
$autoload_map_typedef = \var_export(__DIR__.'/AutoloadMap.hack', true);
}
$code = <<<EOF
/// Generated file, do not edit by hand ///
namespace Facebook\AutoloadMap\Generated {
function build_id(): string {
return $build_id;
}
function root(): string {
return $root;
}
<<__Memoize>>
function is_dev(): bool {
\$override = \getenv('HH_FORCE_IS_DEV');
if (\$override === false) {
return $is_dev;
}
return (bool) \$override;
}
function map_uncached(): \Facebook\AutoloadMap\AutoloadMap {
return $map_as_string;
}
$memoize
function map(): \Facebook\AutoloadMap\AutoloadMap {
return map_uncached();
}
} // Generated\
namespace Facebook\AutoloadMap\_Private {
final class GlobalState {
public static bool \$initialized = false;
}
function bootstrap(): void {
require_once($autoload_map_typedef);
$requires
}
}
namespace Facebook\AutoloadMap {
function initialize(): void {
if (_Private\GlobalState::\$initialized) {
return;
}
if (\\HH\\autoload_is_native()) {
return;
}
_Private\GlobalState::\$initialized = true;
_Private\bootstrap();
\$map = Generated\\map();
\HH\autoload_set_paths(/* HH_FIXME[4110] incorrect hhi */ \$map, Generated\\root());
$add_failure_handler
}
}
EOF;
\file_put_contents($destination_file, $code);
return $this;
}
<<__Memoize>>
private function relativePath(string $path): string {
$root = $this->root;
if ($root === null) {
throw new Exception('Call setRoot() before writeToFile()');
}
$path = \realpath($path);
if (Str\starts_with($path, $root)) {
return Str\slice($path, Str\length($root) + 1);
}
throw new Exception("%s is outside root %s", $path, $root);
}
}