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

Commit

Permalink
add --preg-with-matches migration (#210)
Browse files Browse the repository at this point in the history
Migrates `preg_match()` and `preg_match_all()` calls with a by-ref `$matches` argument to `preg_match_with_matches()` and `preg_match_all_with_matches()`.
  • Loading branch information
jjergus authored Aug 30, 2019
1 parent bf9ab30 commit de5cffc
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 59 deletions.
107 changes: 107 additions & 0 deletions src/Migrations/PregWithMatchesMigration.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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;
use function Facebook\HHAST\resolve_function;

/**
* Migrates preg_match() and preg_match_all() calls with a by-ref $matches
* argument to preg_match_with_matches() and preg_match_all_with_matches().
*/
final class PregWithMatchesMigration extends BaseMigration {

const dict<string, string> REPLACEMENTS = dict[
'preg_match' => 'preg_match_with_matches',
'preg_match_all' => 'preg_match_all_with_matches',
];

<<__Override>>
public function migrateFile(string $_path, Script $root): Script {
foreach (
$root->getDescendantsOfType(FunctionCallExpression::class) as $node
) {
// Only replace calls to functions from the root namespace.
$receiver = $node->getReceiver();
if ($receiver is NameToken) {
$fn_name = $receiver->getText();
} else if ($receiver is QualifiedName) {
$fn_name = '';
foreach ($receiver->getParts()->getChildren() as $part) {
invariant(
$part->getSeparator() is null ||
$part->getSeparator() is BackslashToken,
'Unexpected separator inside qualified function name: "%s"',
($part->getSeparator() as nonnull)->getText(),
);
$fn_name .= $part->getItem()?->getText() ?? '';
$fn_name .= $part->getSeparator()?->getText() ?? '';
}
} else {
invariant_violation(
'Unsupported function call receiver type %s.',
\get_class($receiver),
);
}

$resolved_name = resolve_function($fn_name, $root, $node);

if (!C\contains_key(self::REPLACEMENTS, $resolved_name)) {
continue;
}

// We only need to migrate calls that have a by-ref/inout 3rd argument.
$args = ($node->getArgumentList() as nonnull)->getChildren();
if (C\count($args) < 3) {
continue;
}

$arg3 = $args[2]->getItemx();
if (
$arg3 is DecoratedExpression && $arg3->getDecorator() is InoutToken ||
$arg3 is PrefixUnaryExpression && $arg3->getOperator() is AmpersandToken
) {
$leading = $receiver->getFirstTokenx()->getLeading();
$trailing = $receiver->getLastTokenx()->getTrailing();
$new_name = self::REPLACEMENTS[$resolved_name];

$root = $root->replace(
$receiver,
new QualifiedName(
NodeList::createMaybeEmptyList(vec[
new ListItem(null, new BackslashToken($leading, null)),
new ListItem(new NameToken(null, $trailing, $new_name), null),
]),
),
);

// If it's by-ref, migrate to inout.
if ($arg3 is PrefixUnaryExpression) {
// If there is nothing between "&" and the next token, insert a space.
$trailing = $arg3->getOperator()->getTrailing();
if (
$trailing->isEmpty() &&
$arg3->getOperand()->getFirstTokenx()->getLeading()->isEmpty()
) {
$trailing =
NodeList::createMaybeEmptyList(vec[new WhiteSpace(' ')]);
}

$root = $root->replace(
$arg3->getOperator(),
new InoutToken($arg3->getOperator()->getLeading(), $trailing),
);
}
}
}

return $root;
}
}
7 changes: 6 additions & 1 deletion src/__Private/Resolution/get_current_uses.hack
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function get_current_uses(
): shape(
'namespaces' => dict<string, string>,
'types' => dict<string, string>,
'functions' => dict<string, string>,
) {
$namespaces = $root->getNamespaces();
if (!$namespaces) {
Expand All @@ -32,5 +33,9 @@ function get_current_uses(
}
}

return shape('namespaces' => dict[], 'types' => dict[]);
return shape(
'namespaces' => dict[],
'types' => dict[],
'functions' => dict[],
);
}
15 changes: 14 additions & 1 deletion src/__Private/Resolution/get_uses_directly_in_scope.hack
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Facebook\HHAST\__Private\Resolution;

use type Facebook\HHAST\{
FunctionToken,
NamespaceGroupUseDeclaration,
NamespaceToken,
NamespaceUseClause,
Expand All @@ -25,9 +26,14 @@ function get_uses_directly_in_scope(
): shape(
'namespaces' => dict<string, string>,
'types' => dict<string, string>,
'functions' => dict<string, string>,
) {
if ($scope === null) {
return shape('namespaces' => dict[], 'types' => dict[]);
return shape(
'namespaces' => dict[],
'types' => dict[],
'functions' => dict[],
);
}
$uses = vec[];

Expand Down Expand Up @@ -66,8 +72,11 @@ function get_uses_directly_in_scope(

$namespaces = dict[];
$types = dict[];
$functions = dict[];
foreach ($uses as $use) {
list($kind, $name, $alias) = $use;
// Leading "\" in "use" declarations does nothing.
$name = Str\strip_prefix($name, '\\');
$alias = $alias === null
? $name
|> \explode('\\', $$)
Expand All @@ -78,15 +87,19 @@ function get_uses_directly_in_scope(
if ($kind === null) {
$namespaces[$alias] = $name;
$types[$alias] = $name;
$functions[$alias] = $name;
} else if ($kind is NamespaceToken) {
$namespaces[$alias] = $name;
} else if ($kind is TypeToken) {
$types[$alias] = $name;
} else if ($kind is FunctionToken) {
$functions[$alias] = $name;
}
}

return shape(
'namespaces' => $namespaces,
'types' => $types,
'functions' => $functions,
);
}
75 changes: 75 additions & 0 deletions src/__Private/Resolution/resolve_name.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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\__Private\Resolution;

use namespace HH\Lib\{C, Str, Vec};
use type Facebook\HHAST\{Node, Script};

function resolve_name(
string $name,
Script $root,
Node $node,
dict<string, string> $used_namespaces,
): string {
if (Str\starts_with($name, '\\')) {
return Str\strip_prefix($name, '\\');
}
invariant(
!Str\contains($name, '<'),
'Call on the class name without generics',
);

$autoimports = keyset[
'Awaitable',
'ConstMap',
'ConstSet',
'ConstVector',
'Container',
'ImmMap',
'ImmSet',
'ImmVector',
'KeyedContainer',
'KeyedTraversable',
'Map',
'Set',
'Stringish',
'Traversable',
'Vector',
];
if (C\contains_key($autoimports, $name)) {
return 'HH\\'.$name;
}

$ns = get_current_namespace($root, $node);

if (Str\contains($name, '\\')) {
$maybe_aliased = $name
|> Str\split($$, '\\')
|> C\firstx($$);
if (C\contains_key($used_namespaces, $maybe_aliased)) {
return $name
|> Str\split($$, '\\')
|> Vec\drop($$, 1)
|> Str\join($$, '\\')
|> $used_namespaces[$maybe_aliased].'\\'.$$;
}

if ($ns !== null) {
return $ns.'\\'.$name;
}
return $name;
}

if ($ns !== null) {
return $ns.'\\'.$name;
}

return $name;
}
2 changes: 2 additions & 0 deletions src/nodes/Script.hack
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class Script extends ScriptGeneratedBase {
'uses' => shape(
'namespaces' => dict<string, string>,
'types' => dict<string, string>,
'functions' => dict<string, string>,
),
);

Expand Down Expand Up @@ -79,6 +80,7 @@ final class Script extends ScriptGeneratedBase {
'namespaces' =>
Dict\merge($outer['namespaces'], $inner['namespaces']),
'types' => Dict\merge($outer['types'], $inner['types']),
'functions' => Dict\merge($outer['functions'], $inner['functions']),
),
);
},
Expand Down
23 changes: 23 additions & 0 deletions src/resolve_function.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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;
use namespace Facebook\HHAST\__Private\Resolution;

function resolve_function(string $name, Script $root, Node $node): string {
$uses = Resolution\get_current_uses($root, $node);

if (C\contains_key($uses['functions'], $name)) {
return $uses['functions'][$name];
}

return Resolution\resolve_name($name, $root, $node, $uses['namespaces']);
}
56 changes: 2 additions & 54 deletions src/resolve_type.hack
Original file line number Diff line number Diff line change
Expand Up @@ -9,67 +9,15 @@

namespace Facebook\HHAST;

use namespace HH\Lib\{C, Str, Vec};
use namespace HH\Lib\C;
use namespace Facebook\HHAST\__Private\Resolution;

function resolve_type(string $type, Script $root, Node $node): string {
if (Str\starts_with($type, '\\')) {
return Str\strip_prefix($type, '\\');
}
invariant(
!Str\contains($type, '<'),
'Call on the class name without generics',
);

$autoimports = keyset[
'Awaitable',
'ConstMap',
'ConstSet',
'ConstVector',
'Container',
'ImmMap',
'ImmSet',
'ImmVector',
'KeyedContainer',
'KeyedTraversable',
'Map',
'Set',
'Stringish',
'Traversable',
'Vector',
];
if (C\contains_key($autoimports, $type)) {
return 'HH\\'.$type;
}

$ns = Resolution\get_current_namespace($root, $node);
$uses = Resolution\get_current_uses($root, $node);

if (Str\contains($type, '\\')) {
$maybe_aliased = $type
|> \explode("\\", $$)
|> C\firstx($$);
if (C\contains_key($uses['namespaces'], $maybe_aliased)) {
return $type
|> \explode('\\', $$)
|> Vec\drop($$, 1)
|> Str\join($$, '\\')
|> $uses['namespaces'][$maybe_aliased].'\\'.$$;
}

if ($ns !== null) {
return $ns.'\\'.$type;
}
return $type;
}

if (C\contains_key($uses['types'], $type)) {
return $uses['types'][$type];
}

if ($ns !== null) {
return $ns.'\\'.$type;
}

return $type;
return Resolution\resolve_name($type, $root, $node, $uses['namespaces']);
}
14 changes: 14 additions & 0 deletions tests/PregWithMatchesMigrationTest.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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;

final class PregWithMatchesMigrationTest extends MigrationTest {
const type TMigration = PregWithMatchesMigration;
}
Loading

0 comments on commit de5cffc

Please sign in to comment.