Skip to content
This repository has been archived by the owner on Dec 1, 2024. It is now read-only.

Commit

Permalink
Add migration for top-level requires (#197)
Browse files Browse the repository at this point in the history
* Add migration for top-level requires()

Adds a HH_FIXME[1002] if it can't find an entrypoint function.

* Auto-generate lambdas in TopLevelRequiresMigration

The function that requires a file like:

```
<?hh // partial
$foo = 123;
```

... now has the variable `$foo` set to 123. Limit the scope

* migrate self

* Remove FIXMEs

- if it's strict and has requires, it probably has an entrypoint
- if it's partial, it can have top-level requires

* Don't migrate files that have classes with parents

This kind of file needs manual refactoring:

```
require_once('Bar.php');
class Foo extends Bar {}
```
  • Loading branch information
fredemmott authored Jul 19, 2019
1 parent 201bc7b commit 0ee8177
Show file tree
Hide file tree
Showing 25 changed files with 329 additions and 12 deletions.
6 changes: 4 additions & 2 deletions bin/hhast-inspect
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

namespace Facebook\HHAST\__Private;

require_once(__DIR__.'/hhast-inspect.hack');

// As this file does not have an extension, it is not typechecked. Delegate
// to the typechecked one.
<<__EntryPoint>>
async function hhast_inspect_main_async_UNSAFE(): Awaitable<noreturn> {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once(__DIR__.'/hhast-inspect.hack');
})();
await hhast_inspect_main_async();
}
6 changes: 4 additions & 2 deletions bin/hhast-lint
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

namespace Facebook\HHAST\__Private;

require_once(__DIR__.'/hhast-lint.hack');

// As this file does not have an extension, it is not typechecked. Delegate
// to the typechecked one.
<<__EntryPoint>>
async function hhast_lint_main_async_UNSAFE(): Awaitable<noreturn> {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once(__DIR__.'/hhast-lint.hack');
})();
await hhast_lint_main_async();
}
6 changes: 4 additions & 2 deletions bin/hhast-migrate
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

namespace Facebook\HHAST\__Private;

require_once(__DIR__.'/hhast-migrate.hack');

// As this file does not have an extension, it is not typechecked. Delegate
// to the typechecked one.
<<__EntryPoint>>
async function hhast_migrate_main_async_UNSAFE(): Awaitable<noreturn> {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once(__DIR__.'/hhast-migrate.hack');
})();
await hhast_migrate_main_async();
}
6 changes: 4 additions & 2 deletions bin/update-codegen
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

namespace Facebook\HHAST\__Private;

require_once(__DIR__.'/update-codegen.hack');

// As this file does not have an extension, it is not typechecked. Delegate
// to the typechecked one.
<<__EntryPoint>>
async function update_codegen_async_UNSAFE(): Awaitable<noreturn> {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once(__DIR__.'/update-codegen.hack');
})();
await update_codegen_async();
}
6 changes: 4 additions & 2 deletions bin/update-codegen.hack
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

namespace Facebook\HHAST\__Private;

require_once(__DIR__.'/../vendor/autoload.hack');

<<__EntryPoint>>
async function update_codegen_async(): Awaitable<noreturn> {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once(__DIR__.'/../vendor/autoload.hack');
})();
\Facebook\AutoloadMap\initialize();
$status = await CodegenCLI::runAsync();
exit($status);
Expand Down
24 changes: 24 additions & 0 deletions src/Migrations/BaseMigration.hack
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Facebook\HHAST;

use namespace HH\Lib\C;
use type Facebook\HHAST\Script;

<<__ConsistentConstruct>>
Expand All @@ -21,4 +22,27 @@ abstract class BaseMigration {
}

abstract public function migrateFile(string $path, Script $ast): Script;

protected static async function expressionFromCodeAsync(
string $code,
): Awaitable<IExpression> {
$script = await from_file_async(
File::fromPathAndContents('/dev/null', '$_='.$code.';'),
);
return $script->getDeclarations()
->getChildren()
|> C\firstx($$) as ExpressionStatement
|> $$->getExpression() as BinaryExpression
|> $$->getRightOperand();
}

protected static async function statementFromCodeAsync(
string $code,
): Awaitable<IStatement> {
$script = await from_file_async(
File::fromPathAndContents('/dev/null', $code),
);
return $script->getDeclarations()->getChildren()
|> C\firstx($$) as IStatement;
}
}
3 changes: 1 addition & 2 deletions src/Migrations/HSLMigration.hack
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,6 @@ final class HSLMigration extends BaseMigration {
}

protected function expressionFromCode(string $code): IExpression {
return $this->nodeFromCode('$_ = '.$code.';', BinaryExpression::class)
->getRightOperand();
return \HH\Asio\join(self::expressionFromCodeAsync($code));
}
}
109 changes: 109 additions & 0 deletions src/Migrations/TopLevelRequiresMigration.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2017-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\HHAST;

use namespace HH\Lib\{C, Str, Vec};

/** Move requires to top-level `<<__EntryPoint>>` functions */
final class TopLevelRequiresMigration extends BaseMigration {
<<__Override>>
public function migrateFile(string $_path, Script $script): Script {
return \HH\Asio\join($this->migrateFileAsync($script));
}

private async function migrateFileAsync(Script $script): Awaitable<Script> {
$decls = $script->getDeclarations();
$includes = $decls->getChildrenOfType(InclusionDirective::class);
if (C\is_empty($includes)) {
return $script;
}

$entrypoint = $decls->getChildrenOfType(FunctionDeclaration::class)
|> Vec\filter(
$$,
$f ==> C\any(
$f->getAttributeSpec()?->getAttributes()?->getChildrenOfItems() ??
vec[],
$attr ==>
($attr->getType() as NameToken)->getText() === '__EntryPoint',
),
)
|> C\first($$);

if (!$entrypoint) {
return $script;
}
$classes = $script->getDescendantsOfType(ClassishDeclaration::class);
if (
C\any(
$classes,
$c ==> $c->hasExtendsKeyword() || $c->hasImplementsKeyword(),
)
) {
/* This kind of file needs to be manually refactored:
*
* require_once('Bar.php');
* class Foo extends Bar {}
*/
return $script;
}

// Figure out leading whitespace
$body = $entrypoint->getBody();
if (!$body is CompoundStatement) {
// Invalid, but e.g. `<<__EntryPoint>> function foo(): void;`
return $script;
}

$leading = $body->getStatements()?->toVec() ?? vec[]
|> C\first($$)
|> $$?->getFirstTokenx()?->getLeadingWhitespace()
|> $$?->getText() ?? ' ';

// Generate a lambda so that if the require()'d file sets variables in
// the psuedomain, they don't affect the entrypoint
$includes_text = Vec\map(
$includes,
$incl ==> {
$t = $incl->getFirstTokenx();
return $incl->replace(
$t,
$t->withLeading(new NodeList(vec[new WhiteSpace($leading.$leading)])),
)
->getCode();
},
)
|> Str\join($$, '');
$lambda = await self::statementFromCodeAsync(
$leading.
"(() ==> {\n".
$leading.
$leading.
"// HHAST-generated to avoid pseudomain local leaks\n".
$includes_text.
$leading.
"})();\n\n",
);


$body = $body->withStatements(
new NodeList(
Vec\concat(vec[$lambda], $body->getStatements()?->getChildren() ?? []),
),
);

return $script->withDeclarations(
$script->getDeclarations()
->filterChildren($decl ==> !C\contains($includes, $decl))
->replaceChild($entrypoint, $entrypoint->withBody($body)),
);
}

}
8 changes: 8 additions & 0 deletions src/__Private/MigrationCLI.hack
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use type Facebook\HHAST\{
InstanceofIsMigration,
IsRefinementMigration,
OptionalShapeFieldsMigration,
TopLevelRequiresMigration,
};

use type Facebook\CLILib\{CLIWithRequiredArguments, ExitException};
Expand Down Expand Up @@ -237,6 +238,13 @@ class MigrationCLI extends CLIWithRequiredArguments {
'Replace `$x instanceof Foo` with an `is` expression or `is_a()` call',
'--instanceof-is',
),
CLIOptions\flag(
() ==> {
$this->migrations[] = TopLevelRequiresMigration::class;
},
'Migrate top-level require()s to <<__EntryPoint>> functions',
'--top-level-requires',
),
CLIOptions\flag(
() ==> {
$this->migrations[] = AddFixmesMigration::class;
Expand Down
22 changes: 22 additions & 0 deletions src/nodes/NodeList.hack
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ final class NodeList<+Titem as Node> extends Node {
return new self(Vec\filter_nulls(/* HH_FIXME[4110] */ $children));
}

public function replaceChild<Tchild super Titem as Node>(
Tchild $old,
Tchild $new,
): NodeList<Tchild> {
if ($old === $new) {
return $this;
}
if (!C\contains($this->_children, $old)) {
return $this;
}
return new NodeList(
Vec\map($this->_children, $c ==> $c === $old ? $new : $c),
);
}

public function insertBefore<Tchild super Titem as Node>(
Tchild $before,
Tchild $child,
Expand All @@ -187,6 +202,13 @@ final class NodeList<+Titem as Node> extends Node {
));
}

public function filterChildren((function(Titem): bool) $filter): this {
$new = Vec\filter($this->_children, $filter);
if ($new === $this->_children) {
return $this;
}
return new NodeList($new);
}

public function withoutChild<Tchild super Titem>(Tchild $child): this {
$new = Vec\filter($this->_children, $c ==> $c !== $child);
Expand Down
28 changes: 28 additions & 0 deletions tests/TopLevelRequiresMigrationTest.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2017-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\HHAST;

use namespace HH\Lib\{Str, Vec};

final class TopLevelRequiresMigrationTest extends MigrationTest {
const type TMigration = TopLevelRequiresMigration;

<<__Override>>
public function getExamples(): vec<(string)> {
$prefix = __DIR__.'/examples/';
$dir = $prefix.'migrations/TopLevelRequires/';
return Vec\concat(\glob($dir.'*.php.in'), \glob($dir.'*.hack.in'))
|> Vec\map(
$$,
$file ==>
tuple(Str\strip_suffix(Str\strip_prefix($file, $prefix), '.in')),
);
}
}
13 changes: 13 additions & 0 deletions tests/examples/migrations/TopLevelRequires/all_types.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?hh

<<__EntryPoint>>
function main(): void {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
include('a');
include_once('b');
require('c');
require_once('d');
})();
foo();
}
11 changes: 11 additions & 0 deletions tests/examples/migrations/TopLevelRequires/all_types.php.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?hh

include('a');
include_once('b');
require('c');
require_once('d');

<<__EntryPoint>>
function main(): void {
foo();
}
10 changes: 10 additions & 0 deletions tests/examples/migrations/TopLevelRequires/eight_spaces.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?hh

<<__EntryPoint>>
function main(): void {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once('foo');
})();
bar();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?hh

require_once('foo');

<<__EntryPoint>>
function main(): void {
bar();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?hh

<<__EntryPoint>>
function main(): void {
(() ==> {
// HHAST-generated to avoid pseudomain local leaks
require_once('foo');
})();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?hh

require_once('foo');

<<__EntryPoint>>
function main(): void {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require_once('Foo.hack');

class Bar extends Foo {}

<<__EntryPoint>>
function main(): void {
}
Loading

0 comments on commit 0ee8177

Please sign in to comment.