1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of PHP Mess Detector. |
4
|
|
|
* |
5
|
|
|
* Copyright (c) Manuel Pichler <[email protected]>. |
6
|
|
|
* All rights reserved. |
7
|
|
|
* |
8
|
|
|
* Licensed under BSD License |
9
|
|
|
* For full copyright and license information, please see the LICENSE file. |
10
|
|
|
* Redistributions of files must retain the above copyright notice. |
11
|
|
|
* |
12
|
|
|
* @author Manuel Pichler <[email protected]> |
13
|
|
|
* @copyright Manuel Pichler. All rights reserved. |
14
|
|
|
* @license https://opensource.org/licenses/bsd-license.php BSD License |
15
|
|
|
* @link http://phpmd.org/ |
16
|
|
|
*/ |
17
|
|
|
|
18
|
|
|
namespace PHPMD; |
19
|
|
|
|
20
|
|
|
use ErrorException; |
21
|
|
|
use Iterator; |
22
|
|
|
use PDepend\Source\AST\ASTClass; |
23
|
|
|
use PDepend\Source\AST\ASTFunction; |
24
|
|
|
use PDepend\Source\AST\ASTMethod; |
25
|
|
|
use PDepend\Source\AST\ASTNamespace; |
26
|
|
|
use PDepend\Source\Language\PHP\PHPBuilder; |
27
|
|
|
use PDepend\Source\Language\PHP\PHPParserGeneric; |
28
|
|
|
use PDepend\Source\Language\PHP\PHPTokenizerInternal; |
29
|
|
|
use PDepend\Util\Cache\Driver\MemoryCacheDriver; |
30
|
|
|
use PHPMD\Node\ClassNode; |
31
|
|
|
use PHPMD\Node\FunctionNode; |
32
|
|
|
use PHPMD\Node\InterfaceNode; |
33
|
|
|
use PHPMD\Node\MethodNode; |
34
|
|
|
use PHPMD\Node\TraitNode; |
35
|
|
|
use PHPMD\Rule\Design\TooManyFields; |
36
|
|
|
use PHPMD\Stubs\RuleStub; |
37
|
|
|
use PHPUnit_Framework_ExpectationFailedException; |
38
|
|
|
use PHPUnit_Framework_MockObject_MockBuilder; |
39
|
|
|
use PHPUnit_Framework_MockObject_MockObject; |
40
|
|
|
use ReflectionProperty; |
41
|
|
|
use Traversable; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Abstract base class for PHPMD test cases. |
45
|
|
|
*/ |
46
|
|
|
abstract class AbstractTest extends AbstractStaticTest |
47
|
|
|
{ |
48
|
|
|
/** @var int At least one violation is expected */ |
49
|
|
|
const AL_LEAST_ONE_VIOLATION = -1; |
50
|
|
|
|
51
|
|
|
/** @var int No violation is expected */ |
52
|
|
|
const NO_VIOLATION = 0; |
53
|
|
|
|
54
|
|
|
/** @var int One violation is expected */ |
55
|
|
|
const ONE_VIOLATION = 1; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Get a list of files that should trigger a rule violation. |
59
|
|
|
* |
60
|
|
|
* By default, files named like "testRuleAppliesTo*", but it can be overridden in sub-classes. |
61
|
|
|
* |
62
|
|
|
* @return string[] |
63
|
|
|
*/ |
64
|
|
|
public function getApplyingFiles() |
65
|
|
|
{ |
66
|
|
|
return $this->getFilesForCalledClass('testRuleAppliesTo*'); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Get a list of files that should not trigger a rule violation. |
71
|
|
|
* |
72
|
|
|
* By default, files named like "testRuleDoesNotApplyTo*", but it can be overridden in sub-classes. |
73
|
|
|
* |
74
|
|
|
* @return string[] |
75
|
|
|
*/ |
76
|
|
|
public function getNotApplyingFiles() |
77
|
|
|
{ |
78
|
|
|
return $this->getFilesForCalledClass('testRuleDoesNotApplyTo*'); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Get a list of test files specified by getApplyingFiles() as an array of 1-length arguments lists. |
83
|
|
|
* |
84
|
|
|
* @return string[][] |
85
|
|
|
*/ |
86
|
|
|
public function getApplyingCases() |
87
|
|
|
{ |
88
|
|
|
return static::getValuesAsArrays($this->getApplyingFiles()); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Get a list of test files specified by getNotApplyingFiles() as an array of 1-length arguments lists. |
93
|
|
|
* |
94
|
|
|
* @return string[][] |
95
|
|
|
*/ |
96
|
|
|
public function getNotApplyingCases() |
97
|
|
|
{ |
98
|
|
|
return static::getValuesAsArrays($this->getNotApplyingFiles()); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Resets a changed working directory. |
103
|
|
|
* |
104
|
|
|
* @return void |
105
|
|
|
*/ |
106
|
|
|
protected function tearDown() |
107
|
|
|
{ |
108
|
|
|
static::returnToOriginalWorkingDirectory(); |
109
|
|
|
static::cleanupTempFiles(); |
110
|
|
|
|
111
|
|
|
parent::tearDown(); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Returns the first class found in a source file related to the calling |
116
|
|
|
* test method. |
117
|
|
|
* |
118
|
|
|
* @return ClassNode |
119
|
|
|
*/ |
120
|
|
|
protected function getClass() |
121
|
|
|
{ |
122
|
|
|
return new ClassNode( |
123
|
|
|
$this->getNodeForCallingTestCase( |
124
|
|
|
$this->parseTestCaseSource()->getClasses() |
125
|
|
|
) |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Returns the first interface found in a source file related to the calling |
131
|
|
|
* test method. |
132
|
|
|
* |
133
|
|
|
* @return InterfaceNode |
134
|
|
|
*/ |
135
|
|
|
protected function getInterface() |
136
|
|
|
{ |
137
|
|
|
return new InterfaceNode( |
138
|
|
|
$this->getNodeForCallingTestCase( |
139
|
|
|
$this->parseTestCaseSource()->getInterfaces() |
140
|
|
|
) |
141
|
|
|
); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @return TraitNode |
146
|
|
|
*/ |
147
|
|
|
protected function getTrait() |
148
|
|
|
{ |
149
|
|
|
return new TraitNode( |
150
|
|
|
$this->getNodeForCallingTestCase( |
151
|
|
|
$this->parseTestCaseSource()->getTraits() |
152
|
|
|
) |
153
|
|
|
); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Returns the first method found in a source file related to the calling |
158
|
|
|
* test method. |
159
|
|
|
* |
160
|
|
|
* @return MethodNode |
161
|
|
|
*/ |
162
|
|
|
protected function getMethod() |
163
|
|
|
{ |
164
|
|
|
return new MethodNode( |
165
|
|
|
$this->getNodeForCallingTestCase( |
166
|
|
|
$this->parseTestCaseSource() |
167
|
|
|
->getTypes() |
168
|
|
|
->current() |
169
|
|
|
->getMethods() |
170
|
|
|
) |
171
|
|
|
); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Returns the first function found in a source files related to the calling |
176
|
|
|
* test method. |
177
|
|
|
* |
178
|
|
|
* @return FunctionNode |
179
|
|
|
*/ |
180
|
|
|
protected function getFunction() |
181
|
|
|
{ |
182
|
|
|
return new FunctionNode( |
183
|
|
|
$this->getNodeForCallingTestCase( |
184
|
|
|
$this->parseTestCaseSource()->getFunctions() |
185
|
|
|
) |
186
|
|
|
); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Returns the first class found for a given test file. |
191
|
|
|
* |
192
|
|
|
* @return ClassNode |
193
|
|
|
*/ |
194
|
|
|
protected function getClassNodeForTestFile($file) |
195
|
|
|
{ |
196
|
|
|
return new ClassNode( |
197
|
|
|
$this->parseSource($file) |
198
|
|
|
->getTypes() |
199
|
|
|
->current() |
200
|
|
|
); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Returns the first method or function node for a given test file. |
205
|
|
|
* |
206
|
|
|
* @param string $file |
207
|
|
|
* @return MethodNode|FunctionNode |
208
|
|
|
* @since 2.8.3 |
209
|
|
|
*/ |
210
|
|
|
protected function getNodeForTestFile($file) |
211
|
|
|
{ |
212
|
|
|
$source = $this->parseSource($file); |
213
|
|
|
$class = $source |
214
|
|
|
->getTypes() |
215
|
|
|
->current(); |
216
|
|
|
$nodeClassName = 'PHPMD\\Node\\FunctionNode'; |
217
|
|
|
$getter = 'getFunctions'; |
218
|
|
|
|
219
|
|
|
if ($class) { |
220
|
|
|
$source = $class; |
221
|
|
|
$nodeClassName = 'PHPMD\\Node\\MethodNode'; |
222
|
|
|
$getter = 'getMethods'; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
return new $nodeClassName( |
226
|
|
|
$this->getNodeByName( |
227
|
|
|
$source->$getter(), |
228
|
|
|
pathinfo($file, PATHINFO_FILENAME) |
229
|
|
|
) |
230
|
|
|
); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Assert that a given file trigger N times the given rule. |
235
|
|
|
* |
236
|
|
|
* Rethrows the PHPUnit ExpectationFailedException with the base name |
237
|
|
|
* of the file for better readability. |
238
|
|
|
* |
239
|
|
|
* @param Rule $rule Rule to test. |
240
|
|
|
* @param int $expectedInvokes Count of expected invocations. |
241
|
|
|
* @param string $file Test file containing a method with the same name to be tested. |
242
|
|
|
*/ |
243
|
|
|
protected function expectRuleHasViolationsForFile(Rule $rule, $expectedInvokes, $file) |
244
|
|
|
{ |
245
|
|
|
$report = new Report(); |
246
|
|
|
$rule->setReport($report); |
247
|
|
|
$rule->apply($this->getNodeForTestFile($file)); |
248
|
|
|
$violations = $report->getRuleViolations(); |
249
|
|
|
$actualInvokes = count($violations); |
250
|
|
|
$assertion = $expectedInvokes === self::AL_LEAST_ONE_VIOLATION |
251
|
|
|
? $actualInvokes > 0 |
252
|
|
|
: $actualInvokes === $expectedInvokes; |
253
|
|
|
|
254
|
|
|
if (!$assertion) { |
255
|
|
|
throw new PHPUnit_Framework_ExpectationFailedException( |
256
|
|
|
$this->getViolationFailureMessage($file, $actualInvokes, $expectedInvokes, $violations) |
257
|
|
|
); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
$this->assertTrue($assertion); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Return a human-friendly failure message for a given list of violations and the actual/expected counts. |
265
|
|
|
* |
266
|
|
|
* @param string $file |
267
|
|
|
* @param int $expectedInvokes |
268
|
|
|
* @param int $actualInvokes |
269
|
|
|
* @param array|iterable|Traversable $violations |
270
|
|
|
* |
271
|
|
|
* @return string |
272
|
|
|
*/ |
273
|
|
|
protected function getViolationFailureMessage($file, $expectedInvokes, $actualInvokes, $violations) |
274
|
|
|
{ |
275
|
|
|
return basename($file)." failed:\n". |
276
|
|
|
"Expected $expectedInvokes violation".($expectedInvokes !== 1 ? 's' : '')."\n". |
277
|
|
|
"But $actualInvokes violation".($actualInvokes !== 1 ? 's' : '')." raised". |
278
|
|
|
($actualInvokes > 0 |
279
|
|
|
? ":\n".$this->getViolationsSummary($violations) |
280
|
|
|
: '.' |
281
|
|
|
); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Return a human-friendly summary for a list of violations. |
286
|
|
|
* |
287
|
|
|
* @param array|iterable|Traversable $violations |
288
|
|
|
* @return string |
289
|
|
|
*/ |
290
|
|
|
protected function getViolationsSummary($violations) |
291
|
|
|
{ |
292
|
|
|
if (!is_array($violations)) { |
293
|
|
|
$violations = iterator_to_array($violations); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
return implode("\n", array_map(function (RuleViolation $violation) { |
297
|
|
|
$nodeExtractor = new ReflectionProperty('PHPMD\\RuleViolation', 'node'); |
298
|
|
|
$nodeExtractor->setAccessible(true); |
299
|
|
|
$node = $nodeExtractor->getValue($violation); |
300
|
|
|
$node = $node ? $node->getNode() : null; |
301
|
|
|
$message = ' - line '.$violation->getBeginLine(); |
302
|
|
|
|
303
|
|
|
if ($node) { |
304
|
|
|
$type = preg_replace('/^PDepend\\\\Source\\\\AST\\\\AST/', '', get_class($node)); |
305
|
|
|
$message .= ' on '.$type.' '.$node->getImage(); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $message; |
309
|
|
|
}, $violations)); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Returns the absolute path for a test resource for the current test. |
314
|
|
|
* |
315
|
|
|
* @return string |
316
|
|
|
* @since 1.1.0 |
317
|
|
|
*/ |
318
|
|
|
protected static function createCodeResourceUriForTest() |
319
|
|
|
{ |
320
|
|
|
$frame = static::getCallingTestCase(); |
321
|
|
|
|
322
|
|
|
return self::createResourceUriForTest($frame['function'] . '.php'); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Returns the absolute path for a test resource for the current test. |
327
|
|
|
* |
328
|
|
|
* @param string $localPath The local/relative file location |
329
|
|
|
* @return string |
330
|
|
|
* @since 1.1.0 |
331
|
|
|
*/ |
332
|
|
|
protected static function createResourceUriForTest($localPath) |
333
|
|
|
{ |
334
|
|
|
$frame = static::getCallingTestCase(); |
335
|
|
|
|
336
|
|
|
return static::getResourceFilePathFromClassName($frame['class'], $localPath); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Return URI for a given pattern with directory based on the current called class name. |
341
|
|
|
* |
342
|
|
|
* @param string $pattern |
343
|
|
|
* @return string |
344
|
|
|
*/ |
345
|
|
|
protected function createResourceUriForCalledClass($pattern) |
346
|
|
|
{ |
347
|
|
|
return $this->getResourceFilePathFromClassName(get_class($this), $pattern); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Return list of files matching a given pattern with directory based on the current called class name. |
352
|
|
|
* |
353
|
|
|
* @param string $pattern |
354
|
|
|
* @return string[] |
355
|
|
|
*/ |
356
|
|
|
protected function getFilesForCalledClass($pattern = '*') |
357
|
|
|
{ |
358
|
|
|
return glob($this->createResourceUriForCalledClass($pattern)); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Creates a mocked class node instance. |
363
|
|
|
* |
364
|
|
|
* @param string $metric |
365
|
|
|
* @param integer $value |
366
|
|
|
* @return ClassNode |
367
|
|
|
*/ |
368
|
|
|
protected function getClassMock($metric = null, $value = null) |
369
|
|
|
{ |
370
|
|
|
$class = $this->getMockFromBuilder( |
371
|
|
|
$this->getMockBuilder('PHPMD\\Node\\ClassNode') |
372
|
|
|
->setConstructorArgs(array(new ASTClass('FooBar'))) |
373
|
|
|
); |
374
|
|
|
|
375
|
|
|
if ($metric !== null) { |
376
|
|
|
$class->expects($this->atLeastOnce()) |
377
|
|
|
->method('getMetric') |
378
|
|
|
->with($this->equalTo($metric)) |
379
|
|
|
->willReturn($value); |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
return $class; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Creates a mocked method node instance. |
387
|
|
|
* |
388
|
|
|
* @param string $metric |
389
|
|
|
* @param integer $value |
390
|
|
|
* @return MethodNode |
391
|
|
|
*/ |
392
|
|
|
protected function getMethodMock($metric = null, $value = null) |
393
|
|
|
{ |
394
|
|
|
return $this->createFunctionOrMethodMock('PHPMD\\Node\\MethodNode', new ASTMethod('fooBar'), $metric, $value); |
|
|
|
|
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Creates a mocked function node instance. |
399
|
|
|
* |
400
|
|
|
* @param string $metric The metric acronym used by PHP_Depend. |
401
|
|
|
* @param mixed $value The expected metric return value. |
402
|
|
|
* @return FunctionNode |
403
|
|
|
*/ |
404
|
|
|
protected function createFunctionMock($metric = null, $value = null) |
405
|
|
|
{ |
406
|
|
|
return $this->createFunctionOrMethodMock( |
|
|
|
|
407
|
|
|
'PHPMD\\Node\\FunctionNode', |
408
|
|
|
new ASTFunction('fooBar'), |
409
|
|
|
$metric, |
410
|
|
|
$value |
411
|
|
|
); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* Initializes the getMetric() method of the given function or method node. |
416
|
|
|
* |
417
|
|
|
* @param FunctionNode|MethodNode|PHPUnit_Framework_MockObject_MockObject $mock |
418
|
|
|
* @param string $metric |
419
|
|
|
* @param mixed $value |
420
|
|
|
* @return FunctionNode|MethodNode |
421
|
|
|
*/ |
422
|
|
|
protected function initFunctionOrMethod($mock, $metric, $value) |
423
|
|
|
{ |
424
|
|
|
if ($metric === null) { |
425
|
|
|
return $mock; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
$mock->expects($this->atLeastOnce()) |
|
|
|
|
429
|
|
|
->method('getMetric') |
430
|
|
|
->with($this->equalTo($metric)) |
431
|
|
|
->willReturn($value); |
432
|
|
|
|
433
|
|
|
return $mock; |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Creates a mocked report instance. |
438
|
|
|
* |
439
|
|
|
* @param integer $expectedInvokes Number of expected invokes. |
440
|
|
|
* @return Report|PHPUnit_Framework_MockObject_MockObject |
441
|
|
|
*/ |
442
|
|
|
protected function getReportMock($expectedInvokes = -1) |
443
|
|
|
{ |
444
|
|
|
if ($expectedInvokes === self::AL_LEAST_ONE_VIOLATION) { |
445
|
|
|
$expects = $this->atLeastOnce(); |
446
|
|
|
} elseif ($expectedInvokes === self::NO_VIOLATION) { |
447
|
|
|
$expects = $this->never(); |
448
|
|
|
} elseif ($expectedInvokes === self::ONE_VIOLATION) { |
449
|
|
|
$expects = $this->once(); |
450
|
|
|
} else { |
451
|
|
|
$expects = $this->exactly($expectedInvokes); |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
$report = $this->getMockFromBuilder($this->getMockBuilder('PHPMD\\Report')); |
455
|
|
|
$report->expects($expects) |
456
|
|
|
->method('addRuleViolation'); |
457
|
|
|
|
458
|
|
|
return $report; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Get a mocked report with one violation |
463
|
|
|
* |
464
|
|
|
* @return Report |
465
|
|
|
*/ |
466
|
|
|
public function getReportWithOneViolation() |
467
|
|
|
{ |
468
|
|
|
return $this->getReportMock(self::ONE_VIOLATION); |
|
|
|
|
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* Get a mocked report with no violation |
473
|
|
|
* |
474
|
|
|
* @return Report |
475
|
|
|
*/ |
476
|
|
|
public function getReportWithNoViolation() |
477
|
|
|
{ |
478
|
|
|
return $this->getReportMock(self::NO_VIOLATION); |
|
|
|
|
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* Get a mocked report with at least one violation |
483
|
|
|
* |
484
|
|
|
* @return Report |
485
|
|
|
*/ |
486
|
|
|
public function getReportWithAtLeastOneViolation() |
487
|
|
|
{ |
488
|
|
|
return $this->getReportMock(self::AL_LEAST_ONE_VIOLATION); |
|
|
|
|
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
protected function getMockFromBuilder(PHPUnit_Framework_MockObject_MockBuilder $builder) |
492
|
|
|
{ |
493
|
|
|
if (version_compare(PHP_VERSION, '7.4.0-dev', '<')) { |
494
|
|
|
return $builder->getMock(); |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
return @$builder->getMock(); |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* Creates a mocked {@link \PHPMD\AbstractRule} instance. |
502
|
|
|
* |
503
|
|
|
* @return AbstractRule|PHPUnit_Framework_MockObject_MockObject |
504
|
|
|
*/ |
505
|
|
|
protected function getRuleMock() |
506
|
|
|
{ |
507
|
|
|
if (version_compare(PHP_VERSION, '7.4.0-dev', '<')) { |
508
|
|
|
return $this->getMockForAbstractClass('PHPMD\\AbstractRule'); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
return @$this->getMockForAbstractClass('PHPMD\\AbstractRule'); |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* Creates a mocked rule-set instance. |
516
|
|
|
* |
517
|
|
|
* @param string $expectedClass Optional class name for apply() expected at least once. |
518
|
|
|
* @param int|string $count How often should apply() be called? |
519
|
|
|
* @return RuleSet|PHPUnit_Framework_MockObject_MockObject |
520
|
|
|
*/ |
521
|
|
|
protected function getRuleSetMock($expectedClass = null, $count = '*') |
522
|
|
|
{ |
523
|
|
|
$ruleSet = $this->getMockFromBuilder($this->getMockBuilder('PHPMD\RuleSet')); |
524
|
|
|
if ($expectedClass === null) { |
525
|
|
|
$ruleSet->expects($this->never())->method('apply'); |
526
|
|
|
|
527
|
|
|
return $ruleSet; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
if ($count === '*') { |
531
|
|
|
$count = $this->atLeastOnce(); |
532
|
|
|
} else { |
533
|
|
|
$count = $this->exactly($count); |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
$ruleSet->expects($count) |
537
|
|
|
->method('apply') |
538
|
|
|
->with($this->isInstanceOf($expectedClass)); |
539
|
|
|
|
540
|
|
|
return $ruleSet; |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
/** |
544
|
|
|
* Creates a mocked rule violation instance. |
545
|
|
|
* |
546
|
|
|
* @param string $fileName The filename to use. |
547
|
|
|
* @param integer $beginLine The begin of violation line number to use. |
548
|
|
|
* @param integer $endLine The end of violation line number to use. |
549
|
|
|
* @param null|object $rule The rule object to use. |
550
|
|
|
* @param null|string $description The violation description to use. |
551
|
|
|
* @return PHPUnit_Framework_MockObject_MockObject |
552
|
|
|
*/ |
553
|
|
|
protected function getRuleViolationMock( |
554
|
|
|
$fileName = '/foo/bar.php', |
555
|
|
|
$beginLine = 23, |
556
|
|
|
$endLine = 42, |
557
|
|
|
$rule = null, |
558
|
|
|
$description = null |
559
|
|
|
) { |
560
|
|
|
$ruleViolation = $this->getMockFromBuilder( |
561
|
|
|
$this->getMockBuilder('PHPMD\\RuleViolation') |
562
|
|
|
->setConstructorArgs(array(new TooManyFields(), new FunctionNode(new ASTFunction('fooBar')), 'Hello')) |
563
|
|
|
); |
564
|
|
|
|
565
|
|
|
if ($rule === null) { |
566
|
|
|
$rule = new RuleStub(); |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
if ($description === null) { |
570
|
|
|
$description = 'Test description'; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
$ruleViolation |
574
|
|
|
->method('getRule') |
575
|
|
|
->willReturn($rule); |
576
|
|
|
$ruleViolation |
577
|
|
|
->method('getFileName') |
578
|
|
|
->willReturn($fileName); |
579
|
|
|
$ruleViolation |
580
|
|
|
->method('getBeginLine') |
581
|
|
|
->willReturn($beginLine); |
582
|
|
|
$ruleViolation |
583
|
|
|
->method('getEndLine') |
584
|
|
|
->willReturn($endLine); |
585
|
|
|
$ruleViolation |
586
|
|
|
->method('getNamespaceName') |
587
|
|
|
->willReturn('TestStubPackage'); |
588
|
|
|
$ruleViolation |
589
|
|
|
->method('getDescription') |
590
|
|
|
->willReturn($description); |
591
|
|
|
|
592
|
|
|
return $ruleViolation; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Creates a mocked rul violation instance. |
597
|
|
|
* |
598
|
|
|
* @param string $file |
599
|
|
|
* @param string $message |
600
|
|
|
* @return ProcessingError|PHPUnit_Framework_MockObject_MockObject |
601
|
|
|
*/ |
602
|
|
|
protected function getErrorMock( |
603
|
|
|
$file = '/foo/baz.php', |
604
|
|
|
$message = 'Error in file "/foo/baz.php"' |
605
|
|
|
) { |
606
|
|
|
|
607
|
|
|
$processingError = $this->getMockFromBuilder( |
608
|
|
|
$this->getMockBuilder('PHPMD\\ProcessingError') |
609
|
|
|
->setConstructorArgs(array(null)) |
610
|
|
|
->setMethods(array('getFile', 'getMessage')) |
611
|
|
|
); |
612
|
|
|
|
613
|
|
|
$processingError |
614
|
|
|
->method('getFile') |
615
|
|
|
->willReturn($file); |
616
|
|
|
$processingError |
617
|
|
|
->method('getMessage') |
618
|
|
|
->willReturn($message); |
619
|
|
|
|
620
|
|
|
return $processingError; |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
/** |
624
|
|
|
* Parses the source code for the calling test method and returns the first |
625
|
|
|
* package node found in the parsed file. |
626
|
|
|
* |
627
|
|
|
* @return ASTNamespace |
628
|
|
|
*/ |
629
|
|
|
private function parseTestCaseSource() |
630
|
|
|
{ |
631
|
|
|
return $this->parseSource($this->createCodeResourceUriForTest()); |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
/** |
635
|
|
|
* @param string $mockBuilder |
636
|
|
|
* @param ASTFunction|ASTMethod $mock |
637
|
|
|
* @param string $metric The metric acronym used by PHP_Depend. |
638
|
|
|
* @param mixed $value The expected metric return value. |
639
|
|
|
* @return FunctionNode|MethodNode |
640
|
|
|
*/ |
641
|
|
|
private function createFunctionOrMethodMock($mockBuilder, $mock, $metric = null, $value = null) |
642
|
|
|
{ |
643
|
|
|
return $this->initFunctionOrMethod( |
644
|
|
|
$this->getMockFromBuilder( |
645
|
|
|
$this->getMockBuilder($mockBuilder) |
646
|
|
|
->setConstructorArgs(array($mock)) |
647
|
|
|
), |
648
|
|
|
$metric, |
649
|
|
|
$value |
650
|
|
|
); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
/** |
654
|
|
|
* Returns the PHP_Depend node having the given name. |
655
|
|
|
* |
656
|
|
|
* @param Iterator $nodes |
657
|
|
|
* @return PHP_Depend_Code_AbstractItem |
658
|
|
|
* @throws ErrorException |
659
|
|
|
*/ |
660
|
|
|
private function getNodeByName(Iterator $nodes, $name) |
661
|
|
|
{ |
662
|
|
|
foreach ($nodes as $node) { |
663
|
|
|
if ($node->getName() === $name) { |
664
|
|
|
return $node; |
665
|
|
|
} |
666
|
|
|
} |
667
|
|
|
throw new ErrorException("Cannot locate node named $name."); |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
/** |
671
|
|
|
* Returns the PHP_Depend node for the calling test case. |
672
|
|
|
* |
673
|
|
|
* @param Iterator $nodes |
674
|
|
|
* @return PHP_Depend_Code_AbstractItem |
675
|
|
|
* @throws ErrorException |
676
|
|
|
*/ |
677
|
|
|
private function getNodeForCallingTestCase(Iterator $nodes) |
678
|
|
|
{ |
679
|
|
|
$frame = $this->getCallingTestCase(); |
680
|
|
|
|
681
|
|
|
return $this->getNodeByName($nodes, $frame['function']); |
682
|
|
|
} |
683
|
|
|
|
684
|
|
|
/** |
685
|
|
|
* Parses the source of the given file and returns the first package found |
686
|
|
|
* in that file. |
687
|
|
|
* |
688
|
|
|
* @param string $sourceFile |
689
|
|
|
* @return ASTNamespace |
690
|
|
|
* @throws ErrorException |
691
|
|
|
*/ |
692
|
|
|
private function parseSource($sourceFile) |
693
|
|
|
{ |
694
|
|
|
if (file_exists($sourceFile) === false) { |
695
|
|
|
throw new ErrorException('Cannot locate source file: ' . $sourceFile); |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
$tokenizer = new PHPTokenizerInternal(); |
699
|
|
|
$tokenizer->setSourceFile($sourceFile); |
700
|
|
|
|
701
|
|
|
$builder = new PHPBuilder(); |
702
|
|
|
|
703
|
|
|
$parser = new PHPParserGeneric( |
704
|
|
|
$tokenizer, |
705
|
|
|
$builder, |
706
|
|
|
new MemoryCacheDriver() |
707
|
|
|
); |
708
|
|
|
$parser->parse(); |
709
|
|
|
|
710
|
|
|
return $builder->getNamespaces()->current(); |
711
|
|
|
} |
712
|
|
|
} |
713
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.