Completed
Push — master ( 24d2a2...041714 )
by Alexander
03:13
created

ClassChecker::processProperty()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 19
cts 19
cp 1
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 14
nc 8
nop 0
crap 6
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\BackwardsCompatibility\Checker;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ClassDataCollector;
16
17
class ClassChecker extends AbstractChecker
18
{
19
20
	const CACHE_DURATION = 3600;
21
22
	const TYPE_CLASS_DELETED = 'class.deleted';
23
	const TYPE_CLASS_MADE_ABSTRACT = 'class.made_abstract';
24
	const TYPE_CLASS_MADE_FINAL = 'class.made_final';
25
	const TYPE_CLASS_CONSTANT_DELETED = 'class.constant.deleted';
26
	const TYPE_PROPERTY_DELETED = 'property.deleted';
27
	const TYPE_PROPERTY_MADE_STATIC = 'property.made_static';
28
	const TYPE_PROPERTY_MADE_NON_STATIC = 'property.made_non_static';
29
	const TYPE_PROPERTY_SCOPE_REDUCED = 'property.scope_reduced';
30
	const TYPE_METHOD_DELETED = 'method.deleted';
31
	const TYPE_METHOD_MADE_ABSTRACT = 'method.made_abstract';
32
	const TYPE_METHOD_MADE_FINAL = 'method.made_final';
33
	const TYPE_METHOD_MADE_STATIC = 'method.made_static';
34
	const TYPE_METHOD_MADE_NON_STATIC = 'method.made_non_static';
35
	const TYPE_METHOD_SCOPE_REDUCED = 'method.scope_reduced';
36
	const TYPE_METHOD_SIGNATURE_CHANGED = 'method.signature_changed';
37
38
	/**
39
	 * Source class data.
40
	 *
41
	 * @var array
42
	 */
43
	protected $sourceClassData = array();
44
45
	/**
46
	 * Target class data.
47
	 *
48
	 * @var array
49
	 */
50
	protected $targetClassData = array();
51
52
	/**
53
	 * Source property data.
54
	 *
55
	 * @var array
56
	 */
57
	protected $sourcePropertyData = array();
58
59
	/**
60
	 * Target property data.
61
	 *
62
	 * @var array
63
	 */
64
	protected $targetPropertyData = array();
65
66
	/**
67
	 * Source method data.
68
	 *
69
	 * @var array
70
	 */
71
	protected $sourceMethodData = array();
72
73
	/**
74
	 * Target method data.
75
	 *
76
	 * @var array
77
	 */
78
	protected $targetMethodData = array();
79
80
	/**
81
	 * Returns backwards compatibility checker name.
82
	 *
83
	 * @return string
84
	 */
85 1
	public function getName()
86
	{
87 1
		return 'class';
88
	}
89
90
	/**
91
	 * Collects backwards compatibility violations.
92
	 *
93
	 * @return void
94
	 */
95 4
	protected function doCheck()
96
	{
97
		$classes_sql = 'SELECT Name, Id, IsAbstract, IsFinal
98 4
						FROM Classes';
99 4
		$source_classes = $this->sourceDatabase->fetchAssoc($classes_sql);
100 4
		$target_classes = $this->targetDatabase->fetchAssoc($classes_sql);
101
102 4
		foreach ( $source_classes as $class_name => $source_class_data ) {
103 4
			if ( !isset($target_classes[$class_name]) ) {
104 2
				$this->addIncident(self::TYPE_CLASS_DELETED, $class_name);
105 2
				continue;
106
			}
107
108 4
			$this->sourceClassData = $source_class_data;
109 4
			$this->targetClassData = $target_classes[$class_name];
110
111 4
			if ( !$this->sourceClassData['IsAbstract'] && $this->targetClassData['IsAbstract'] ) {
112 2
				$this->addIncident(self::TYPE_CLASS_MADE_ABSTRACT, $class_name);
113 2
			}
114
115 4
			if ( !$this->sourceClassData['IsFinal'] && $this->targetClassData['IsFinal'] ) {
116 2
				$this->addIncident(self::TYPE_CLASS_MADE_FINAL, $class_name);
117 2
			}
118
119 4
			$this->processConstants();
120 4
			$this->processProperties();
121 4
			$this->processMethods();
122 4
		}
123 4
	}
124
125
	/**
126
	 * Checks constants.
127
	 *
128
	 * @return void
129
	 */
130 4
	protected function processConstants()
131
	{
132 4
		$class_name = $this->sourceClassData['Name'];
133
134 4
		$source_constants = $this->getConstantsRecursively($this->sourceDatabase, $this->sourceClassData['Id']);
135 4
		$target_constants = $this->getConstantsRecursively($this->targetDatabase, $this->targetClassData['Id']);
136
137 4
		foreach ( $source_constants as $source_constant_name => $source_constant_data ) {
138 4
			$full_constant_name = $class_name . '::' . $source_constant_name;
139
140
			// @codeCoverageIgnoreStart
141
			// Report incidents for processed (not inherited) constants only.
142
			if ( $source_constant_data['ClassId'] !== $this->sourceClassData['Id'] ) {
143
				continue;
144
			}
145
			// @codeCoverageIgnoreEnd
146
147 4
			if ( !isset($target_constants[$source_constant_name]) ) {
148 2
				$this->addIncident(self::TYPE_CLASS_CONSTANT_DELETED, $full_constant_name);
149 2
				continue;
150
			}
151 4
		}
152 4
	}
153
154
	/**
155
	 * Returns class constants.
156
	 *
157
	 * @param ExtendedPdoInterface $db       Database.
158
	 * @param integer              $class_id Class ID.
159
	 *
160
	 * @return array
161
	 */
162 4
	protected function getConstantsRecursively(ExtendedPdoInterface $db, $class_id)
163
	{
164 4
		$cache_key = $this->getCacheKey($db, 'class_constants[' . $class_id . ']');
165 4
		$cached_value = $this->cache->fetch($cache_key);
166
167 4
		if ( $cached_value === false ) {
168
			$sql = 'SELECT Name, ClassId
169
					FROM ClassConstants
170 4
					WHERE ClassId = :class_id';
171 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
172
173 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
174 4
				foreach ( $this->getConstantsRecursively($db, $related_class_id) as $name => $data ) {
175
					// @codeCoverageIgnoreStart
176
					if ( !array_key_exists($name, $cached_value) ) {
177
						$cached_value[$name] = $data;
178
					}
179
					// @codeCoverageIgnoreEnd
180 4
				}
181 4
			}
182
183
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
184 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
185 4
		}
186
187 4
		return $cached_value;
188
	}
189
190
	/**
191
	 * Checks properties.
192
	 *
193
	 * @return void
194
	 */
195 4
	protected function processProperties()
196
	{
197 4
		$class_name = $this->sourceClassData['Name'];
198 4
		$source_properties = $this->getPropertiesRecursively(
199 4
			$this->sourceDatabase,
200 4
			$this->sourceClassData['Id'],
201 4
			$this->coveredScopes()
202 4
		);
203 4
		$target_properties = $this->getPropertiesRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
204
205 4
		foreach ( $source_properties as $source_property_name => $source_property_data ) {
206 4
			$full_property_name = $class_name . '::$' . $source_property_name;
207
208
			// Report incidents for processed (not inherited) properties only.
209 4
			if ( $source_property_data['ClassId'] !== $this->sourceClassData['Id'] ) {
210 4
				continue;
211
			}
212
213 4
			if ( !isset($target_properties[$source_property_name]) ) {
214 2
				$this->addIncident(self::TYPE_PROPERTY_DELETED, $full_property_name);
215 2
				continue;
216
			}
217
218 4
			$this->sourcePropertyData = $source_property_data;
219 4
			$this->targetPropertyData = $target_properties[$source_property_name];
220
221 4
			$this->processProperty();
222 4
		}
223 4
	}
224
225
	/**
226
	 * Returns class properties.
227
	 *
228
	 * @param ExtendedPdoInterface $db       Database.
229
	 * @param integer              $class_id Class ID.
230
	 * @param string               $scopes   Scopes.
231
	 *
232
	 * @return array
233
	 */
234 4
	protected function getPropertiesRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
235
	{
236 4
		$cache_key = $this->getCacheKey($db, 'class_properties[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
237 4
		$cached_value = $this->cache->fetch($cache_key);
238
239 4
		if ( $cached_value === false ) {
240
			$sql = 'SELECT Name, Scope, IsStatic, ClassId
241
					FROM ClassProperties
242 4
					WHERE ClassId = :class_id';
243
244 4
			if ( $scopes ) {
245 4
				$sql .= ' AND Scope IN (' . $scopes . ')';
246 4
			}
247
248 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
249
250 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
251 4
				foreach ( $this->getPropertiesRecursively($db, $related_class_id, $scopes) as $name => $data ) {
252 4
					if ( !array_key_exists($name, $cached_value) ) {
253 4
						$cached_value[$name] = $data;
254 4
					}
255 4
				}
256 4
			}
257
258
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
259 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
260 4
		}
261
262 4
		return $cached_value;
263
	}
264
265
	/**
266
	 * Processes property.
267
	 *
268
	 * @return void
269
	 */
270 4
	protected function processProperty()
271
	{
272 4
		$class_name = $this->sourceClassData['Name'];
273 4
		$property_name = $this->sourcePropertyData['Name'];
274
275 4
		$full_property_name = $class_name . '::$' . $property_name;
276
277 4
		if ( !$this->sourcePropertyData['IsStatic'] && $this->targetPropertyData['IsStatic'] ) {
278 2
			$this->addIncident(self::TYPE_PROPERTY_MADE_STATIC, $full_property_name);
279 2
		}
280
281 4
		if ( $this->sourcePropertyData['IsStatic'] && !$this->targetPropertyData['IsStatic'] ) {
282 2
			$this->addIncident(self::TYPE_PROPERTY_MADE_NON_STATIC, $full_property_name);
283 2
		}
284
285 4
		if ( $this->sourcePropertyData['Scope'] > $this->targetPropertyData['Scope'] ) {
286 2
			$this->addIncident(
287 2
				self::TYPE_PROPERTY_SCOPE_REDUCED,
288 2
				$full_property_name,
289 2
				$this->getScopeName($this->sourcePropertyData['Scope']),
290 2
				$this->getScopeName($this->targetPropertyData['Scope'])
291 2
			);
292 2
		}
293 4
	}
294
295
	/**
296
	 * Checks methods.
297
	 *
298
	 * @return void
299
	 */
300 4
	protected function processMethods()
301
	{
302 4
		$class_name = $this->sourceClassData['Name'];
303 4
		$source_methods = $this->getMethodsRecursively(
304 4
			$this->sourceDatabase,
305 4
			$this->sourceClassData['Id'],
306 4
			$this->coveredScopes()
307 4
		);
308 4
		$target_methods = $this->getMethodsRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
309
310 4
		foreach ( $source_methods as $source_method_name => $source_method_data ) {
311 4
			$target_method_name = $source_method_name;
312 4
			$full_method_name = $class_name . '::' . $source_method_name;
313
314
			// Ignore PHP4 constructor rename into PHP5 constructor.
315 4
			if ( !isset($target_methods[$target_method_name]) && $target_method_name === $class_name ) {
316 2
				$target_method_name = '__construct';
317 2
			}
318
319
			// Ignore PHP5 constructor rename into PHP4 constructor.
320 4
			if ( !isset($target_methods[$target_method_name]) && $target_method_name === '__construct' ) {
321 2
				$target_method_name = $class_name;
322 2
			}
323
324
			// Report incidents for processed (not inherited) methods only.
325 4
			if ( $source_method_data['ClassId'] !== $this->sourceClassData['Id'] ) {
326 4
				continue;
327
			}
328
329 4
			if ( !isset($target_methods[$target_method_name]) ) {
330 2
				$this->addIncident(self::TYPE_METHOD_DELETED, $full_method_name);
331 2
				continue;
332
			}
333
334 4
			$this->sourceMethodData = $source_method_data;
335 4
			$this->sourceMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
336 4
				$this->sourceDatabase,
337 4
				$this->sourceMethodData['Id']
338 4
			);
339
340 4
			$this->targetMethodData = $target_methods[$target_method_name];
341 4
			$this->targetMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
342 4
				$this->targetDatabase,
343 4
				$this->targetMethodData['Id']
344 4
			);
345
346 4
			$this->processMethod();
347 4
		}
348 4
	}
349
350
	/**
351
	 * Returns class methods.
352
	 *
353
	 * @param ExtendedPdoInterface $db       Database.
354
	 * @param integer              $class_id Class ID.
355
	 * @param string               $scopes   Scopes.
356
	 *
357
	 * @return array
358
	 */
359 4
	protected function getMethodsRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
360
	{
361 4
		$cache_key = $this->getCacheKey($db, 'class_methods[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
362 4
		$cached_value = $this->cache->fetch($cache_key);
363
364 4
		if ( $cached_value === false ) {
365
			$sql = 'SELECT Name, Id, Scope, IsAbstract, IsFinal, IsStatic, ClassId
366
					FROM ClassMethods
367 4
					WHERE ClassId = :class_id';
368
369 4
			if ( $scopes ) {
370 4
				$sql .= ' AND Scope IN (' . $scopes . ')';
371 4
			}
372
373 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
374
375 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
376 4
				foreach ( $this->getMethodsRecursively($db, $related_class_id, $scopes) as $name => $data ) {
377 4
					if ( !array_key_exists($name, $cached_value) ) {
378 4
						$cached_value[$name] = $data;
379 4
					}
380 4
				}
381 4
			}
382
383
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
384 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
385 4
		}
386
387 4
		return $cached_value;
388
	}
389
390
	/**
391
	 * Calculates method parameter signature.
392
	 *
393
	 * @param ExtendedPdoInterface $db        Database.
394
	 * @param integer              $method_id Method ID.
395
	 *
396
	 * @return integer
397
	 */
398 4
	protected function getMethodParameterSignature(ExtendedPdoInterface $db, $method_id)
399
	{
400
		$sql = 'SELECT *
401
				FROM MethodParameters
402
				WHERE MethodId = :method_id
403 4
				ORDER BY Position ASC';
404 4
		$method_parameters = $db->fetchAll($sql, array('method_id' => $method_id));
405
406 4
		$hash_parts = array();
407
408 4
		foreach ( $method_parameters as $method_parameter_data ) {
409 4
			$hash_parts[] = $this->paramToString($method_parameter_data);
410 4
		}
411
412 4
		return implode(', ', $hash_parts);
413
	}
414
415
	/**
416
	 * Processes method.
417
	 *
418
	 * @return void
419
	 */
420 4
	protected function processMethod()
421
	{
422 4
		$class_name = $this->sourceClassData['Name'];
423 4
		$method_name = $this->sourceMethodData['Name'];
424
425 4
		$full_method_name = $class_name . '::' . $method_name;
426
427 4
		if ( !$this->sourceMethodData['IsAbstract'] && $this->targetMethodData['IsAbstract'] ) {
428 2
			$this->addIncident(self::TYPE_METHOD_MADE_ABSTRACT, $full_method_name);
429 2
		}
430
431 4
		if ( !$this->sourceMethodData['IsFinal'] && $this->targetMethodData['IsFinal'] ) {
432 2
			$this->addIncident(self::TYPE_METHOD_MADE_FINAL, $full_method_name);
433 2
		}
434
435 4
		if ( !$this->sourceMethodData['IsStatic'] && $this->targetMethodData['IsStatic'] ) {
436 2
			$this->addIncident(self::TYPE_METHOD_MADE_STATIC, $full_method_name);
437 2
		}
438
439 4
		if ( $this->sourceMethodData['IsStatic'] && !$this->targetMethodData['IsStatic'] ) {
440 2
			$this->addIncident(self::TYPE_METHOD_MADE_NON_STATIC, $full_method_name);
441 2
		}
442
443 4
		if ( $this->sourceMethodData['ParameterSignature'] !== $this->targetMethodData['ParameterSignature'] ) {
444 2
			$this->addIncident(
445 2
				self::TYPE_METHOD_SIGNATURE_CHANGED,
446 2
				$full_method_name,
447 2
				$this->sourceMethodData['ParameterSignature'],
448 2
				$this->targetMethodData['ParameterSignature']
449 2
			);
450 2
		}
451
452 4
		if ( $this->sourceMethodData['Scope'] > $this->targetMethodData['Scope'] ) {
453 2
			$this->addIncident(
454 2
				self::TYPE_METHOD_SCOPE_REDUCED,
455 2
				$full_method_name,
456 2
				$this->getScopeName($this->sourceMethodData['Scope']),
457 2
				$this->getScopeName($this->targetMethodData['Scope'])
458 2
			);
459 2
		}
460 4
	}
461
462
	/**
463
	 * Returns scope name.
464
	 *
465
	 * @param integer $scope Scope.
466
	 *
467
	 * @return string
468
	 */
469 2
	protected function getScopeName($scope)
470
	{
471
		$mapping = array(
472 2
			ClassDataCollector::SCOPE_PRIVATE => 'private',
473 2
			ClassDataCollector::SCOPE_PROTECTED => 'protected',
474 2
			ClassDataCollector::SCOPE_PUBLIC => 'public',
475 2
		);
476
477 2
		return $mapping[$scope];
478
	}
479
480
	/**
481
	 * Scopes covered by backwards compatibility checks.
482
	 *
483
	 * @return string
484
	 */
485 4
	protected function coveredScopes()
486
	{
487
		// Ignore changes in protected class members for "final" classes.
488 4
		if ( $this->sourceClassData['IsFinal'] && $this->targetClassData['IsFinal'] ) {
489 4
			return ClassDataCollector::SCOPE_PUBLIC;
490
		}
491
492 4
		return ClassDataCollector::SCOPE_PUBLIC . ',' . ClassDataCollector::SCOPE_PROTECTED;
493
	}
494
495
	/**
496
	 * Returns class constants.
497
	 *
498
	 * @param ExtendedPdoInterface $db       Database.
499
	 * @param integer              $class_id Class ID.
500
	 *
501
	 * @return array
502
	 */
503 4
	protected function getClassRelations(ExtendedPdoInterface $db, $class_id)
504
	{
505 4
		$cache_key = $this->getCacheKey($db, 'class_relations[' . $class_id . ']');
506 4
		$cached_value = $this->cache->fetch($cache_key);
507
508 4
		if ( $cached_value === false ) {
509
			$sql = 'SELECT RelatedClassId, RelatedClass
510
					FROM ClassRelations
511 4
					WHERE ClassId = :class_id AND RelatedClassId <> 0';
512 4
			$cached_value = $db->fetchPairs($sql, array('class_id' => $class_id));
513
514
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
515 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
516 4
		}
517
518 4
		return $cached_value;
519
	}
520
521
}
522