1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the Code-Insight library. |
4
|
|
|
* For the full copyright and license information, please view |
5
|
|
|
* the LICENSE file that was distributed with this source code. |
6
|
|
|
* |
7
|
|
|
* @copyright Alexander Obuhovich <[email protected]> |
8
|
|
|
* @link https://github.com/console-helpers/code-insight |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace ConsoleHelpers\CodeInsight\KnowledgeBase; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
use Aura\Sql\ExtendedPdoInterface; |
15
|
|
|
use Composer\Autoload\ClassLoader; |
16
|
|
|
use ConsoleHelpers\CodeInsight\BackwardsCompatibility\Checker\AbstractChecker; |
17
|
|
|
use ConsoleHelpers\CodeInsight\BackwardsCompatibility\Checker\CheckerFactory; |
18
|
|
|
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\AbstractDataCollector; |
19
|
|
|
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ClassDataCollector; |
20
|
|
|
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ConstantDataCollector; |
21
|
|
|
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\FunctionDataCollector; |
22
|
|
|
use ConsoleHelpers\ConsoleKit\ConsoleIO; |
23
|
|
|
use Go\ParserReflection\Locator\CallableLocator; |
24
|
|
|
use Go\ParserReflection\Locator\ComposerLocator; |
25
|
|
|
use Go\ParserReflection\LocatorInterface; |
26
|
|
|
use Go\ParserReflection\ReflectionEngine; |
27
|
|
|
use Go\ParserReflection\ReflectionFile; |
28
|
|
|
use Symfony\Component\Finder\Finder; |
29
|
|
|
|
30
|
|
|
class KnowledgeBase |
31
|
|
|
{ |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Project path. |
35
|
|
|
* |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
protected $projectPath = ''; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Regular expression for removing project path. |
42
|
|
|
* |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
protected $projectPathRegExp = ''; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Database. |
49
|
|
|
* |
50
|
|
|
* @var ExtendedPdoInterface |
51
|
|
|
*/ |
52
|
|
|
protected $db; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Config |
56
|
|
|
* |
57
|
|
|
* @var array |
58
|
|
|
*/ |
59
|
|
|
protected $config = array(); |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Data collectors. |
63
|
|
|
* |
64
|
|
|
* @var AbstractDataCollector[] |
65
|
|
|
*/ |
66
|
|
|
protected $dataCollectors = array(); |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Console IO. |
70
|
|
|
* |
71
|
|
|
* @var ConsoleIO |
72
|
|
|
*/ |
73
|
|
|
protected $io; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Creates knowledge base instance. |
77
|
|
|
* |
78
|
|
|
* @param string $project_path Project path. |
79
|
|
|
* @param ExtendedPdoInterface $db Database. |
80
|
|
|
* @param ConsoleIO $io Console IO. |
81
|
|
|
* |
82
|
|
|
* @throws \InvalidArgumentException When project path doesn't exist. |
83
|
|
|
*/ |
84
|
|
|
public function __construct($project_path, ExtendedPdoInterface $db, ConsoleIO $io = null) |
85
|
|
|
{ |
86
|
|
|
if ( !file_exists($project_path) || !is_dir($project_path) ) { |
87
|
|
|
throw new \InvalidArgumentException('The project path doesn\'t exist.'); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
$this->projectPath = $project_path; |
91
|
|
|
$this->projectPathRegExp = '#^' . preg_quote($project_path, '#') . '/#'; |
92
|
|
|
|
93
|
|
|
$this->db = $db; |
94
|
|
|
$this->config = $this->getConfiguration(); |
95
|
|
|
$this->io = $io; |
96
|
|
|
|
97
|
|
|
$this->dataCollectors[] = new ClassDataCollector($db); |
98
|
|
|
$this->dataCollectors[] = new ConstantDataCollector($db); |
99
|
|
|
$this->dataCollectors[] = new FunctionDataCollector($db); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Returns database. |
104
|
|
|
* |
105
|
|
|
* @return ExtendedPdoInterface |
106
|
|
|
*/ |
107
|
8 |
|
public function getDatabase() |
108
|
|
|
{ |
109
|
8 |
|
return $this->db; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Returns project configuration. |
114
|
|
|
* |
115
|
|
|
* @return array |
116
|
|
|
* @throws \LogicException When configuration file is not found. |
117
|
|
|
* @throws \LogicException When configuration file isn't in JSON format. |
118
|
|
|
*/ |
119
|
|
|
protected function getConfiguration() |
120
|
|
|
{ |
121
|
|
|
$config_file = $this->projectPath . '/.code-insight.json'; |
122
|
|
|
|
123
|
|
|
if ( !file_exists($config_file) ) { |
124
|
|
|
throw new \LogicException( |
125
|
|
|
'Configuration file ".code-insight.json" not found at "' . $this->projectPath . '".' |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$config = json_decode(file_get_contents($config_file), true); |
130
|
|
|
|
131
|
|
|
if ( $config === null ) { |
132
|
|
|
throw new \LogicException('Configuration file ".code-insight.json" is not in JSON format.'); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
return $config; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Refreshes database. |
140
|
|
|
* |
141
|
|
|
* @return void |
142
|
|
|
* @throws \LogicException When "$this->io" wasn't set upfront. |
143
|
|
|
*/ |
144
|
|
|
public function refresh() |
145
|
|
|
{ |
146
|
|
|
if ( !isset($this->io) ) { |
147
|
|
|
throw new \LogicException('The "$this->io" must be set prior to calling "$this->refresh()".'); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
ReflectionEngine::setMaximumCachedFiles(20); |
151
|
|
|
ReflectionEngine::init($this->detectClassLocator()); |
152
|
|
|
|
153
|
|
|
$sql = 'UPDATE Files |
154
|
|
|
SET Found = 0'; |
155
|
|
|
$this->db->perform($sql); |
156
|
|
|
|
157
|
|
|
$files = array(); |
158
|
|
|
$this->io->write('Searching for files ... '); |
159
|
|
|
|
160
|
|
|
foreach ( $this->getFinders() as $finder ) { |
161
|
|
|
$files = array_merge($files, array_keys(iterator_to_array($finder))); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
$file_count = count($files); |
165
|
|
|
$this->io->writeln(array('<info>' . $file_count . ' found</info>', '')); |
166
|
|
|
|
167
|
|
|
$progress_bar = $this->io->createProgressBar($file_count + 2); |
168
|
|
|
$progress_bar->setMessage(''); |
169
|
|
|
$progress_bar->setFormat( |
170
|
|
|
'%message%' . PHP_EOL . '%current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>' |
171
|
|
|
); |
172
|
|
|
$progress_bar->start(); |
173
|
|
|
|
174
|
|
|
foreach ( $files as $file ) { |
175
|
|
|
$progress_bar->setMessage('Processing file: <info>' . $this->removeProjectPath($file) . '</info>'); |
176
|
|
|
$progress_bar->display(); |
177
|
|
|
|
178
|
|
|
$this->processFile($file); |
179
|
|
|
|
180
|
|
|
$progress_bar->advance(); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
$sql = 'SELECT Id |
184
|
|
|
FROM Files |
185
|
|
|
WHERE Found = 0'; |
186
|
|
|
$deleted_files = $this->db->fetchCol($sql); |
187
|
|
|
|
188
|
|
|
if ( $deleted_files ) { |
|
|
|
|
189
|
|
|
$progress_bar->setMessage('Erasing information about deleted files ...'); |
190
|
|
|
$progress_bar->display(); |
191
|
|
|
|
192
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
193
|
|
|
$data_collector->deleteData($deleted_files); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$progress_bar->advance(); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$progress_bar->setMessage('Aggregating processed data ...'); |
200
|
|
|
$progress_bar->display(); |
201
|
|
|
|
202
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
203
|
|
|
$data_collector->aggregateData($this); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
$progress_bar->advance(); |
207
|
|
|
|
208
|
|
|
$progress_bar->finish(); |
209
|
|
|
$progress_bar->clear(); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Refreshes database silently. |
214
|
|
|
* |
215
|
|
|
* @return void |
216
|
|
|
*/ |
217
|
|
|
public function silentRefresh() |
218
|
|
|
{ |
219
|
|
|
ReflectionEngine::setMaximumCachedFiles(20); |
220
|
|
|
ReflectionEngine::init($this->detectClassLocator()); |
221
|
|
|
|
222
|
|
|
$sql = 'UPDATE Files |
223
|
|
|
SET Found = 0'; |
224
|
|
|
$this->db->perform($sql); |
225
|
|
|
|
226
|
|
|
$files = array(); |
227
|
|
|
|
228
|
|
|
foreach ( $this->getFinders() as $finder ) { |
229
|
|
|
$files = array_merge($files, array_keys(iterator_to_array($finder))); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
foreach ( $files as $file ) { |
233
|
|
|
$this->processFile($file); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$sql = 'SELECT Id |
237
|
|
|
FROM Files |
238
|
|
|
WHERE Found = 0'; |
239
|
|
|
$deleted_files = $this->db->fetchCol($sql); |
240
|
|
|
|
241
|
|
|
if ( $deleted_files ) { |
|
|
|
|
242
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
243
|
|
|
$data_collector->deleteData($deleted_files); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
248
|
|
|
$data_collector->aggregateData($this); |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Prints statistics about the code. |
254
|
|
|
* |
255
|
|
|
* @return array |
256
|
|
|
*/ |
257
|
|
|
public function getStatistics() |
258
|
|
|
{ |
259
|
|
|
$ret = array(); |
260
|
|
|
|
261
|
|
|
$sql = 'SELECT COUNT(*) |
262
|
|
|
FROM Files'; |
263
|
|
|
$ret['Files'] = $this->db->fetchValue($sql); |
264
|
|
|
|
265
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
266
|
|
|
$ret = array_merge($ret, $data_collector->getStatistics()); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return $ret; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Processes file. |
274
|
|
|
* |
275
|
|
|
* @param string $file File. |
276
|
|
|
* |
277
|
|
|
* @return integer |
278
|
|
|
*/ |
279
|
|
|
public function processFile($file) |
280
|
|
|
{ |
281
|
|
|
$size = filesize($file); |
282
|
|
|
$relative_file = $this->removeProjectPath($file); |
283
|
|
|
|
284
|
|
|
$sql = 'SELECT Id, Size |
285
|
|
|
FROM Files |
286
|
|
|
WHERE Name = :name'; |
287
|
|
|
$file_data = $this->db->fetchOne($sql, array( |
288
|
|
|
'name' => $relative_file, |
289
|
|
|
)); |
290
|
|
|
|
291
|
|
|
$this->db->beginTransaction(); |
292
|
|
|
|
293
|
|
|
if ( $file_data === false ) { |
294
|
|
|
$sql = 'INSERT INTO Files (Name, Size) VALUES (:name, :size)'; |
295
|
|
|
$this->db->perform($sql, array( |
296
|
|
|
'name' => $relative_file, |
297
|
|
|
'size' => $size, |
298
|
|
|
)); |
299
|
|
|
|
300
|
|
|
$file_id = $this->db->lastInsertId(); |
301
|
|
|
} |
302
|
|
|
else { |
303
|
|
|
$file_id = $file_data['Id']; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
// File is not changed since last time it was indexed. |
307
|
|
|
if ( $file_data !== false && (int)$file_data['Size'] === $size ) { |
308
|
|
|
$sql = 'UPDATE Files |
309
|
|
|
SET Found = 1 |
310
|
|
|
WHERE Id = :file_id'; |
311
|
|
|
$this->db->perform($sql, array( |
312
|
|
|
'file_id' => $file_data['Id'], |
313
|
|
|
)); |
314
|
|
|
|
315
|
|
|
$this->db->commit(); |
316
|
|
|
|
317
|
|
|
return $file_data['Id']; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
$sql = 'UPDATE Files |
321
|
|
|
SET Found = 1, Size = :size |
322
|
|
|
WHERE Id = :file_id'; |
323
|
|
|
$this->db->perform($sql, array( |
324
|
|
|
'file_id' => $file_id, |
325
|
|
|
'size' => $size, |
326
|
|
|
)); |
327
|
|
|
|
328
|
|
|
$parsed_file = new ReflectionFile($file); |
329
|
|
|
|
330
|
|
|
foreach ( $parsed_file->getFileNamespaces() as $namespace ) { |
331
|
|
|
foreach ( $this->dataCollectors as $data_collector ) { |
332
|
|
|
$data_collector->collectData($file_id, $namespace); |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$this->db->commit(); |
337
|
|
|
|
338
|
|
|
return $file_id; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Determines class locator. |
343
|
|
|
* |
344
|
|
|
* @return LocatorInterface |
345
|
|
|
* @throws \LogicException When class locator from "class_locator" setting doesn't exist. |
346
|
|
|
* @throws \LogicException When class locator from "class_locator" setting has non supported type. |
347
|
|
|
*/ |
348
|
|
|
protected function detectClassLocator() |
349
|
|
|
{ |
350
|
|
|
$class_locator = null; |
351
|
|
|
$raw_class_locator_file = $this->getConfigSetting('class_locator'); |
352
|
|
|
|
353
|
|
|
if ( $raw_class_locator_file !== null ) { |
354
|
|
|
$class_locator_file = $this->resolveProjectPath($raw_class_locator_file); |
355
|
|
|
|
356
|
|
|
if ( !file_exists($class_locator_file) || !is_file($class_locator_file) ) { |
357
|
|
|
throw new \LogicException( |
358
|
|
|
'The "' . $raw_class_locator_file . '" class locator doesn\'t exist.' |
359
|
|
|
); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
$class_locator = require $class_locator_file; |
363
|
|
|
} |
364
|
|
|
else { |
365
|
|
|
$class_locator_file = $this->resolveProjectPath('vendor/autoload.php'); |
366
|
|
|
|
367
|
|
|
if ( file_exists($class_locator_file) && is_file($class_locator_file) ) { |
368
|
|
|
$class_locator = require $class_locator_file; |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
// Make sure memory limit isn't changed by class locator. |
373
|
|
|
ini_restore('memory_limit'); |
374
|
|
|
|
375
|
|
|
if ( is_callable($class_locator) ) { |
376
|
|
|
return new CallableLocator($class_locator); |
377
|
|
|
} |
378
|
|
|
elseif ( $class_locator instanceof ClassLoader ) { |
379
|
|
|
return new ComposerLocator($class_locator); |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
throw new \LogicException( |
383
|
|
|
'The "class_loader" setting must point to "vendor/autoload.php" or a file, that would return the closure.' |
384
|
|
|
); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Processes the Finders configuration list. |
389
|
|
|
* |
390
|
|
|
* @return Finder[] |
391
|
|
|
* @throws \LogicException If "finder" setting doesn't exist. |
392
|
|
|
* @throws \LogicException If the configured method does not exist. |
393
|
|
|
*/ |
394
|
|
|
protected function getFinders() |
395
|
|
|
{ |
396
|
|
|
$finder_config = $this->getConfigSetting('finder'); |
397
|
|
|
|
398
|
|
|
// Process "finder" config setting. |
399
|
|
|
if ( $finder_config === null ) { |
400
|
|
|
throw new \LogicException('The "finder" setting must be present in config file.'); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
$finders = array(); |
404
|
|
|
|
405
|
|
|
foreach ( $finder_config as $methods ) { |
406
|
|
|
$finder = Finder::create()->files(); |
407
|
|
|
|
408
|
|
|
if ( isset($methods['in']) ) { |
409
|
|
|
$methods['in'] = (array)$methods['in']; |
410
|
|
|
|
411
|
|
|
foreach ( $methods['in'] as $folder_index => $in_folder ) { |
412
|
|
|
$methods['in'][$folder_index] = $this->resolveProjectPath($in_folder); |
413
|
|
|
} |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
foreach ( $methods as $method => $arguments ) { |
417
|
|
|
if ( !method_exists($finder, $method) ) { |
418
|
|
|
throw new \LogicException(sprintf( |
419
|
|
|
'The method "Finder::%s" does not exist.', |
420
|
|
|
$method |
421
|
|
|
)); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
$arguments = (array)$arguments; |
425
|
|
|
|
426
|
|
|
foreach ( $arguments as $argument ) { |
427
|
|
|
$finder->$method($argument); |
428
|
|
|
} |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
$finders[] = $finder; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
return $finders; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* Resolves path within project. |
439
|
|
|
* |
440
|
|
|
* @param string $relative_path Relative path. |
441
|
|
|
* |
442
|
|
|
* @return string |
443
|
|
|
*/ |
444
|
|
|
protected function resolveProjectPath($relative_path) |
445
|
|
|
{ |
446
|
|
|
return realpath($this->projectPath . DIRECTORY_SEPARATOR . $relative_path); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* Removes project path from file path. |
451
|
|
|
* |
452
|
|
|
* @param string $absolute_path Absolute path. |
453
|
|
|
* |
454
|
|
|
* @return string |
455
|
|
|
*/ |
456
|
|
|
protected function removeProjectPath($absolute_path) |
457
|
|
|
{ |
458
|
|
|
return preg_replace($this->projectPathRegExp, '', $absolute_path, 1); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Returns backwards compatibility checkers. |
463
|
|
|
* |
464
|
|
|
* @param CheckerFactory $factory Factory. |
465
|
|
|
* |
466
|
|
|
* @return AbstractChecker[] |
467
|
|
|
*/ |
468
|
|
|
public function getBackwardsCompatibilityCheckers(CheckerFactory $factory) |
469
|
|
|
{ |
470
|
|
|
$ret = array(); |
471
|
|
|
$default_names = array('class', 'function', 'constant'); |
472
|
|
|
|
473
|
|
|
foreach ( $this->getConfigSetting('bc_checkers', $default_names) as $name ) { |
474
|
|
|
$ret[] = $factory->get($name); |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
return $ret; |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* Returns backwards compatibility ignore rules. |
482
|
|
|
* |
483
|
|
|
* @return array |
484
|
|
|
*/ |
485
|
|
|
public function getBackwardsCompatibilityIgnoreRules() |
486
|
|
|
{ |
487
|
|
|
return $this->getConfigSetting('bc_ignore', array()); |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
/** |
491
|
|
|
* Returns value of configuration setting. |
492
|
|
|
* |
493
|
|
|
* @param string $name Name. |
494
|
|
|
* @param mixed|null $default Default value. |
495
|
|
|
* |
496
|
|
|
* @return mixed |
497
|
|
|
*/ |
498
|
|
|
protected function getConfigSetting($name, $default = null) |
499
|
|
|
{ |
500
|
|
|
return array_key_exists($name, $this->config) ? $this->config[$name] : $default; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
} |
504
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.