@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhorgePHPParserExtractor extends PhpParser\NodeVisitorAbstract {
4 private static $knownTypes = array(
5 'PhabricatorEdgeType' => array(
6 'getTransactionAddString' => array(null, 'phutilnumber', null),
7 'getTransactionRemoveString' => array(null, 'phutilnumber', null),
8 'getTransactionEditString' => array(
9 null,
10 'phutilnumber',
11 'phutilnumber',
12 null,
13 'phutilnumber',
14 null,
15 ),
16 'getFeedAddString' => array(null, null, 'phutilnumber', null),
17 'getFeedRemoveString' => array(null, null, 'phutilnumber', null),
18 'getFeedEditString' => array(
19 null,
20 null,
21 'phutilnumber',
22 'phutilnumber',
23 null,
24 'phutilnumber',
25 null,
26 ),
27 ),
28 );
29 private $classDecl = null;
30 private $classExtends = null;
31 private $methodDecl = null;
32 private $paramVars = array();
33 private $filename = null;
34 private $results = array();
35 private $warnings = array();
36 private $variableAssignments = array();
37
38 public function getWarnings() {
39 return $this->warnings;
40 }
41
42 public function getResults() {
43 return $this->results;
44 }
45
46 public function __construct($filename) {
47 $this->filename = $filename;
48 }
49
50 private function getName($node) {
51 $name = $node->name;
52 if (is_string($name)) {
53 return $name;
54 }
55 if ($name instanceof PhpParser\Node\Name ||
56 $name instanceof PhpParser\Node\Identifier
57 ) {
58 return $name->name;
59 }
60 return null;
61 }
62
63 private function typeNode(PhpParser\Node $node, array $seen = array()) {
64 if ($node instanceof PhpParser\Node\Scalar\Int_) {
65 return 'number';
66 } else if ($node instanceof PhpParser\Node\Expr\BinaryOp) {
67 // i.e count($foos) + count($bars)
68 $type1 = $this->typeNode($node->left, $seen);
69 $type2 = $this->typeNode($node->right, $seen);
70 if ($type1 === $type2) {
71 return $type1;
72 }
73 } else if ($node instanceof PhpParser\Node\Expr\FuncCall) {
74 switch ($this->getName($node)) {
75 case 'phutil_count':
76 return 'phutilnumber';
77 case 'count':
78 return 'number';
79 case 'phutil_person':
80 return 'person';
81 }
82 } else if ($node instanceof PhpParser\Node\Expr\New_ ) {
83 if ($this->getName($node->class) == 'PhutilNumber') {
84 return 'phutilnumber';
85 }
86 } else if ($node instanceof PhpParser\Node\Expr\Variable) {
87 $name = $this->getName($node);
88 if (isset($seen[$name])) {
89 return null;
90 }
91 if (isset($this->variableAssignments[$name])) {
92 return $this->typeNode(
93 $this->variableAssignments[$name],
94 $seen + array($name => true));
95 }
96 $known = static::$knownTypes;
97 if (isset($known[$this->classExtends])) {
98 $class_data = $known[$this->classExtends];
99 } else if (isset($known[$this->classDecl])) {
100 $class_data = $known[$this->classDecl];
101 }
102 if (isset($class_data[$this->methodDecl])) {
103 $types = $class_data[$this->methodDecl];
104 if ($name && isset($this->paramVars[$name])) {
105 if (isset($types[$this->paramVars[$name]])) {
106 return $types[$this->paramVars[$name]];
107 }
108 }
109 }
110 }
111 return null;
112 }
113
114 public function enterNode(PhpParser\Node $node) {
115 if ($node instanceof PhpParser\Node\Stmt\Class_) {
116 $this->classDecl = $this->getName($node);
117 if ($node->extends) {
118 $this->classExtends = $this->getName($node->extends);
119 }
120 } else if ($node instanceof PhpParser\Node\Stmt\ClassMethod) {
121 $this->methodDecl = $this->getName($node);
122 $this->paramVars = array();
123 $i = 0;
124 foreach ($node->params as $param) {
125 $name = $this->getName($param->var);
126 if ($name) {
127 $this->paramVars[$name] = $i;
128 }
129 $i++;
130 }
131 } else if ($node instanceof PhpParser\Node\Expr\Assign) {
132 if ($node->var instanceof PhpParser\Node\Expr\Variable) {
133 $name = $this->getName($node->var);
134 // $name could be null if this is a "variable variable"
135 // (like $$foo = bar)
136 if ($name) {
137 $this->variableAssignments[$name] = $node->expr;
138 }
139 }
140 } else if ($node instanceof PhpParser\Node\Expr\FuncCall) {
141 if ($this->getName($node) == 'pht') {
142 $types = array();
143 $args = $node->args;
144 $first = array_shift($args);
145 try {
146 $key = id(new PhpParser\ConstExprEvaluator())
147 ->evaluateSilently($first->value);
148 } catch (PhpParser\ConstExprEvaluationException $ex) {
149 $this->warnings[] = pht(
150 'Failed to evaluate pht() call on line %d in "%s": %s',
151 $first->getStartLine(),
152 $this->filename,
153 $ex->getMessage());
154 return;
155 }
156 foreach ($args as $child) {
157 $types[] = $this->typeNode($child->value);
158 }
159 $this->results[] = array(
160 'string' => $key,
161 'line' => $first->getStartLine(),
162 'types' => $types,
163 'file' => $this->filename,
164 );
165 }
166 }
167 }
168
169 public function leaveNode(PhpParser\Node $node) {
170 if ($node instanceof PhpParser\Node\Stmt\Class_) {
171 $this->classDecl = null;
172 $this->classExtends = null;
173 } else if ($node instanceof PhpParser\Node\Stmt\ClassMethod) {
174 $this->methodDecl = null;
175 $this->paramVars = array();
176 $this->variableAssignments = array();
177 }
178 }
179}