1
|
|
|
<?php |
2
|
|
|
/****************************************************************************** |
3
|
|
|
* An implementation of dicto (scg.unibe.ch/dicto) in and for PHP. |
4
|
|
|
* |
5
|
|
|
* Copyright (c) 2016, 2015 Richard Klees <[email protected]> |
6
|
|
|
* |
7
|
|
|
* This software is licensed under The MIT License. You should have received |
8
|
|
|
* a copy of the licence along with the code. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Lechimp\Dicto\Indexer\PhpParser; |
12
|
|
|
|
13
|
|
|
use Lechimp\Dicto\Indexer as I; |
14
|
|
|
use Lechimp\Dicto\Analysis\Consts; |
15
|
|
|
use PhpParser\Node as N; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Implementation of Indexer with PhpParser. |
19
|
|
|
*/ |
20
|
|
|
class Indexer implements I\Indexer, \PhpParser\NodeVisitor { |
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var string |
23
|
|
|
*/ |
24
|
|
|
protected $project_root_path; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var I\Insert|null |
28
|
|
|
*/ |
29
|
|
|
protected $insert; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var \PhpParser\Parser |
33
|
|
|
*/ |
34
|
|
|
protected $parser; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var Listener[]; |
38
|
|
|
*/ |
39
|
|
|
protected $listeners; |
40
|
|
|
|
41
|
|
|
// state for parsing a file |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var string|null |
45
|
|
|
*/ |
46
|
|
|
protected $file_path = null; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var string[]|null |
50
|
|
|
*/ |
51
|
|
|
protected $file_content = null; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* This contains the stack of ids were currently in, i.e. the nesting of |
55
|
|
|
* known code blocks we are in. |
56
|
|
|
* |
57
|
|
|
* @var int[]|null |
58
|
|
|
*/ |
59
|
|
|
protected $entity_id_stack = null; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* This contains cached reference ids. |
63
|
|
|
* |
64
|
|
|
* @var array|null string => int |
65
|
|
|
*/ |
66
|
|
|
protected $reference_cache = null; |
67
|
|
|
|
68
|
|
|
|
69
|
17 |
|
public function __construct(\PhpParser\Parser $parser) { |
70
|
17 |
|
$this->project_root_path = ""; |
71
|
17 |
|
$this->parser = $parser; |
72
|
17 |
|
$this->listeners = array(); |
73
|
17 |
|
} |
74
|
|
|
|
75
|
17 |
|
protected function build_listeners() { |
76
|
|
|
return array |
77
|
17 |
|
( new DependenciesListener($this->insert, $this) |
|
|
|
|
78
|
17 |
|
, new InvocationsListener($this->insert, $this) |
|
|
|
|
79
|
17 |
|
); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @inheritdoc |
84
|
|
|
*/ |
85
|
16 |
|
public function index_file($path) { |
86
|
16 |
|
if ($this->insert === null) { |
87
|
|
|
throw new \RuntimeException( |
88
|
|
|
"Set an inserter to be used before starting to index files."); |
89
|
|
|
} |
90
|
|
|
|
91
|
16 |
|
$content = file_get_contents($this->project_root_path."/$path"); |
92
|
16 |
|
if ($content === false) { |
93
|
|
|
throw \InvalidArgumentException("Can't read file $path."); |
94
|
|
|
} |
95
|
|
|
|
96
|
16 |
|
$stmts = $this->parser->parse($content); |
97
|
|
|
|
98
|
16 |
|
if ($stmts === null) { |
99
|
|
|
throw new \RuntimeException("Could not parse file '$path'."); |
100
|
|
|
} |
101
|
|
|
|
102
|
16 |
|
$traverser = new \PhpParser\NodeTraverser; |
103
|
16 |
|
$traverser->addVisitor($this); |
104
|
|
|
|
105
|
16 |
|
$this->entity_id_stack = array(); |
106
|
16 |
|
$this->file_path = $path; |
107
|
16 |
|
$this->file_content = explode("\n", $content); |
108
|
16 |
|
$this->reference_cache = array(); |
109
|
16 |
|
$traverser->traverse($stmts); |
110
|
16 |
|
$this->entity_id_stack = null; |
111
|
16 |
|
$this->file_path = null; |
112
|
16 |
|
$this->file_content = null; |
113
|
16 |
|
$this->reference_cache = null; |
114
|
16 |
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @inheritdoc |
118
|
|
|
*/ |
119
|
17 |
|
public function use_insert(I\Insert $insert) { |
120
|
17 |
|
$this->insert = $insert; |
121
|
17 |
|
$this->listeners = $this->build_listeners(); |
122
|
17 |
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @inheritdoc |
126
|
|
|
*/ |
127
|
17 |
|
public function set_project_root_to($path) { |
128
|
17 |
|
assert('is_string($path)'); |
129
|
17 |
|
$this->project_root_path = $path; |
130
|
17 |
|
} |
131
|
|
|
|
132
|
|
|
// helper |
133
|
|
|
|
134
|
16 |
|
private function lines_from_to($start, $end) { |
135
|
16 |
|
assert('is_int($start)'); |
136
|
16 |
|
assert('is_int($end)'); |
137
|
16 |
|
return implode("\n", array_slice($this->file_content, $start-1, $end-$start+1)); |
138
|
|
|
} |
139
|
|
|
|
140
|
14 |
|
public function get_reference($entity_type, $name, $start_line) { |
141
|
14 |
|
assert('in_array($entity_type, \\Lechimp\\Dicto\\Analysis\\Consts::$ENTITY_TYPES)'); |
142
|
14 |
|
assert('is_string($name)'); |
143
|
14 |
|
assert('is_int($start_line)'); |
144
|
|
|
|
145
|
|
|
// caching |
146
|
14 |
|
$key = $entity_type.":".$name.":".$this->file_path.":".$start_line; |
147
|
14 |
|
if (array_key_exists($key, $this->reference_cache)) { |
148
|
12 |
|
return $this->reference_cache[$key]; |
149
|
|
|
} |
150
|
|
|
|
151
|
14 |
|
$ref_id = $this->insert->reference |
152
|
14 |
|
( $entity_type |
153
|
14 |
|
, $name |
154
|
14 |
|
, $this->file_path |
155
|
14 |
|
, $start_line |
156
|
14 |
|
); |
157
|
|
|
|
158
|
14 |
|
$this->reference_cache[$key] = $ref_id; |
159
|
14 |
|
return $ref_id; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
// from \PhpParser\NodeVisitor |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* @inheritdoc |
166
|
|
|
*/ |
167
|
16 |
|
public function beforeTraverse(array $nodes) { |
168
|
|
|
// for sure found a file |
169
|
16 |
|
$id = $this->insert->entity |
170
|
16 |
|
( Consts::FILE_ENTITY |
171
|
16 |
|
, $this->file_path |
172
|
16 |
|
, $this->file_path |
173
|
16 |
|
, 1 |
174
|
16 |
|
, count($this->file_content) |
175
|
16 |
|
, implode("\n", $this->file_content) |
176
|
16 |
|
); |
177
|
|
|
|
178
|
16 |
|
$this->entity_id_stack[] = $id; |
179
|
|
|
|
180
|
16 |
|
foreach ($this->listeners as $listener) { |
181
|
16 |
|
$listener->on_enter_file($id, $this->file_path, implode("\n", $this->file_content)); |
182
|
16 |
|
} |
183
|
|
|
|
184
|
16 |
|
return null; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* @inheritdoc |
189
|
|
|
*/ |
190
|
16 |
|
public function afterTraverse(array $nodes) { |
191
|
16 |
|
$id = array_pop($this->entity_id_stack); |
192
|
|
|
|
193
|
16 |
|
foreach ($this->listeners as $listener) { |
194
|
16 |
|
$listener->on_leave_file($id); |
195
|
16 |
|
} |
196
|
|
|
|
197
|
16 |
|
return null; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* @inheritdoc |
202
|
|
|
*/ |
203
|
16 |
|
public function enterNode(\PhpParser\Node $node) { |
204
|
16 |
|
$start_line = $node->getAttribute("startLine"); |
205
|
16 |
|
$end_line = $node->getAttribute("endLine"); |
206
|
16 |
|
$source = $this->lines_from_to($start_line, $end_line); |
207
|
|
|
|
208
|
16 |
|
$id = null; |
209
|
|
|
|
210
|
|
|
// Class |
211
|
16 |
|
if ($node instanceof N\Stmt\Class_) { |
212
|
15 |
|
$id = $this->insert->entity |
213
|
15 |
|
( Consts::CLASS_ENTITY |
214
|
15 |
|
, $node->name |
215
|
15 |
|
, $this->file_path |
216
|
15 |
|
, $start_line |
217
|
15 |
|
, $end_line |
218
|
15 |
|
, $source |
219
|
15 |
|
); |
220
|
|
|
|
221
|
15 |
|
foreach ($this->listeners as $listener) { |
222
|
15 |
|
$listener->on_enter_class($id, $node); |
223
|
15 |
|
} |
224
|
15 |
|
} |
225
|
|
|
// Method or Function |
226
|
16 |
View Code Duplication |
elseif ($node instanceof N\Stmt\ClassMethod) { |
|
|
|
|
227
|
15 |
|
$id = $this->insert->entity |
228
|
15 |
|
( Consts::METHOD_ENTITY |
229
|
15 |
|
, $node->name |
230
|
15 |
|
, $this->file_path |
231
|
15 |
|
, $start_line |
232
|
15 |
|
, $end_line |
233
|
15 |
|
, $source |
234
|
15 |
|
); |
235
|
|
|
|
236
|
15 |
|
foreach ($this->listeners as $listener) { |
237
|
15 |
|
$listener->on_enter_method($id, $node); |
238
|
15 |
|
} |
239
|
15 |
|
} |
240
|
16 |
View Code Duplication |
elseif ($node instanceof N\Stmt\Function_) { |
|
|
|
|
241
|
|
|
$id = $this->insert->entity |
242
|
|
|
( Consts::FUNCTION_ENTITY |
243
|
|
|
, $node->name |
244
|
|
|
, $this->file_path |
245
|
|
|
, $start_line |
246
|
|
|
, $end_line |
247
|
|
|
, $source |
248
|
|
|
); |
249
|
|
|
|
250
|
|
|
foreach ($this->listeners as $listener) { |
251
|
|
|
$listener->on_enter_function($id, $node); |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
else { |
255
|
16 |
|
foreach ($this->listeners as $listener) { |
256
|
16 |
|
$listener->on_enter_misc($node); |
257
|
16 |
|
} |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
// Call to method or function |
261
|
16 |
|
if ($id !== null) { |
262
|
15 |
|
$this->entity_id_stack[] = $id; |
263
|
15 |
|
} |
264
|
16 |
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @inheritdoc |
268
|
|
|
*/ |
269
|
16 |
|
public function leaveNode(\PhpParser\Node $node) { |
270
|
|
|
// Class |
271
|
16 |
|
if ($node instanceof N\Stmt\Class_) { |
272
|
|
|
// We pushed it, we need to pop it now, as we are leaving the class. |
273
|
15 |
|
$id = array_pop($this->entity_id_stack); |
274
|
|
|
|
275
|
15 |
|
foreach ($this->listeners as $listener) { |
276
|
15 |
|
$listener->on_leave_class($id); |
277
|
15 |
|
} |
278
|
15 |
|
} |
279
|
|
|
// Method or Function |
280
|
16 |
|
elseif ($node instanceof N\Stmt\ClassMethod) { |
281
|
|
|
// We pushed it, we need to pop it now, as we are leaving the method |
282
|
|
|
// or function. |
283
|
15 |
|
$id = array_pop($this->entity_id_stack); |
284
|
|
|
|
285
|
15 |
|
foreach ($this->listeners as $listener) { |
286
|
15 |
|
$listener->on_leave_method($id); |
287
|
15 |
|
} |
288
|
15 |
|
} |
289
|
16 |
|
elseif ($node instanceof N\Stmt\Function_) { |
290
|
|
|
// We pushed it, we need to pop it now, as we are leaving the method |
291
|
|
|
// or function. |
292
|
|
|
$id = array_pop($this->entity_id_stack); |
293
|
|
|
|
294
|
|
|
foreach ($this->listeners as $listener) { |
295
|
|
|
$listener->on_leave_function($id); |
296
|
|
|
} |
297
|
|
|
} |
298
|
16 |
|
} |
299
|
|
|
} |
300
|
|
|
|