forked from hhvm/hhast
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DontAwaitInALoopLinter.hack
125 lines (108 loc) · 3.58 KB
/
DontAwaitInALoopLinter.hack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*
* 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};
use function Facebook\HHAST\find_position;
final class DontAwaitInALoopLinter extends ASTLinter {
const type TNode = PrefixUnaryExpression;
const type TContext = Script;
<<__Override>>
public function getLintErrorForNode(
Script $script,
PrefixUnaryExpression $node,
): ?ASTLintError {
if (!$node->getOperator() is AwaitToken) {
return null;
}
$boundary = $script->getClosestAncestorOfDescendantOfType<IHasFunctionBody>(
$node,
) ??
$script;
$outermost_loop = $boundary->getFirstAncestorOfDescendantWhere(
$node,
$a ==> $a is ILoopStatement,
) as ?ILoopStatement;
if (
$outermost_loop is null ||
self::isInTerminalStatement($node, $boundary) ||
!self::isInLoop($outermost_loop, $node)
) {
return null;
}
return new ASTLintError($this, "Don't use await in a loop", $node);
}
<<__Override>>
public function getPrettyTextForNode(PrefixUnaryExpression $blame): string {
$boundary = $this->getAST()
->getClosestAncestorOfDescendantOfType<IHasFunctionBody>($blame) ??
$this->getAST();
$loops = $boundary->getAncestorsOfDescendant($blame)
|> Vec\map(
$$,
$x ==> $x is ILoopStatement && self::isInLoop($x, $blame) ? $x : null,
)
|> Vec\filter_nulls($$);
$lines = $this->getFile()->getContents() |> Str\split($$, "\n");
list($blame_line, $_col) = find_position($this->getAST(), $blame);
if (C\count($loops) === 1) {
list($line, $_col) = find_position($this->getAST(), C\onlyx($loops));
if ($line === $blame_line) {
return $lines[$line - 1];
}
}
$output = vec[];
foreach ($loops as $loop) {
list($line, $_col) = find_position($this->getAST(), $loop);
$output[] = 'Line '.$line.': '.$lines[$line - 1];
}
$output[] = 'Line '.$blame_line.': '.$lines[$blame_line - 1];
return Str\join($output, "\n");
}
/**
* The following statements, although `await`ing in a loop,
* will never execute more than once.
* We can detect these cases and not emit a lint error for them.
* ```
* return await Asio\curl_exec($url);
* // or
* throw new BadRequestException(await $repo->getUserAsync());
* ```
*
* Cases like these are not covered and they should be suppressed.
* ```
* // @see https://docs.hhvm.com/hack/asynchronous-operations/await-as-an-expression#limitations
* $some_value = await $some_awaitable;
* return await something_else_async($some_value);
* ```
*/
private static function isInTerminalStatement(
PrefixUnaryExpression $node,
Node $boundary,
): bool {
return C\any(
$boundary->getAncestorsOfDescendant($node),
$a ==> $a is ReturnStatement || $a is ThrowStatement,
);
}
/**
* Checks that the $node is in the part of the $loop that actually loops. This
* includes the body of the loop as well as e.g. the condition (since it's
* evaluated at every iteration), but excludes e.g. the initializer in for
* loops since it's only evaluated once.
*/
private static function isInLoop(
ILoopStatement $loop,
PrefixUnaryExpression $node,
): bool {
return !(
$loop is ForStatement && $loop->getInitializer()?->isAncestorOf($node) ||
$loop is ForeachStatement && $loop->getCollection()->isAncestorOf($node)
);
}
}