Skip to content

Commit 3d4f607

Browse files
committed
Initial implementation
1 parent 0f8085d commit 3d4f607

16 files changed

+436
-0
lines changed

Diff for: .coveralls.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
service_name: travis-ci
2+
coverage_clover: tests/tmp/clover.xml
3+
json_path: tests/tmp/coveralls.json

Diff for: .editorconfig

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
9+
[*.{php,phpt}]
10+
indent_style = tab
11+
indent_size = 4
12+
13+
[*.xml]
14+
indent_style = tab
15+
indent_size = 4
16+
17+
[*.neon]
18+
indent_style = tab
19+
indent_size = 4
20+
21+
[*.{yaml,yml}]
22+
indent_style = space
23+
indent_size = 2
24+
25+
[composer.json]
26+
indent_style = tab
27+
indent_size = 4

Diff for: .gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/tests export-ignore

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/composer.lock
2+
/vendor

Diff for: .travis.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: php
2+
php:
3+
- 7.0
4+
- 7.1
5+
- 7.2
6+
before_script:
7+
- composer self-update
8+
- composer install
9+
script:
10+
- vendor/bin/phing
11+
after_script:
12+
- php vendor/bin/coveralls -v

Diff for: README.md

+38
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
11
# PHPStan PHPUnit extensions and rules
2+
3+
[![Build Status](https://travis-ci.org/phpstan/phpstan-phpunit.svg)](https://travis-ci.org/phpstan/phpstan-phpunit)
4+
[![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-phpunit/v/stable)](https://packagist.org/packages/phpstan/phpstan-phpunit)
5+
[![License](https://poser.pugx.org/phpstan/phpstan-phpunit/license)](https://packagist.org/packages/phpstan/phpstan-phpunit)
6+
7+
* [PHPStan](https://github.com/phpstan/phpstan)
8+
* [PHPUnit](https://phpunit.de)
9+
10+
This extension provides following features:
11+
12+
* `createMock()` method returns an intersection type of the mock object and the mocked class so that both methods from the mock object (like `expects`) and from the mocked class are available on the object.
13+
* Interprets `Foo|PHPUnit_Framework_MockObject_MockObject` in phpDoc so that it results in an intersection type instead of a union type.
14+
* Defines early terminating method calls for the `PHPUnit\Framework\TestCase` class to prevent undefined variable errors.
15+
16+
It also contains this framework-specific rule (can be enabled separately):
17+
18+
* Check that both values passed to `assertSame()` method are of the same type.
19+
20+
## Usage
21+
22+
To use this extension, require it in [Composer](https://getcomposer.org/):
23+
24+
```bash
25+
composer require --dev phpstan/phpstan-phpunit
26+
```
27+
28+
And include extension.neon in your project's PHPStan config:
29+
30+
```
31+
includes:
32+
- vendor/phpstan/phpstan-phpunit/extension.neon
33+
```
34+
35+
To perform framework-specific checks, include also this file:
36+
37+
```
38+
- vendor/phpstan/phpstan-phpunit/rules.neon
39+
```

Diff for: build.xml

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<project name="PHPStan PHPUnit extensions and rules" default="check">
3+
4+
<target name="check" depends="
5+
composer,
6+
lint,
7+
cs,
8+
tests,
9+
phpstan
10+
"/>
11+
12+
<target name="composer">
13+
<exec
14+
executable="composer"
15+
logoutput="true"
16+
passthru="true"
17+
checkreturn="true"
18+
>
19+
<arg value="install"/>
20+
</exec>
21+
</target>
22+
23+
<target name="lint">
24+
<exec
25+
executable="vendor/bin/parallel-lint"
26+
logoutput="true"
27+
passthru="true"
28+
checkreturn="true"
29+
>
30+
<arg path="src" />
31+
<arg path="tests" />
32+
</exec>
33+
</target>
34+
35+
<target name="cs">
36+
<exec
37+
executable="vendor/bin/phpcs"
38+
logoutput="true"
39+
passthru="true"
40+
checkreturn="true"
41+
>
42+
<arg value="--extensions=php"/>
43+
<arg value="--encoding=utf-8"/>
44+
<arg value="--tab-width=4"/>
45+
<arg value="--ignore=tests/*/data"/>
46+
<arg value="-sp"/>
47+
<arg path="src"/>
48+
<arg path="tests"/>
49+
</exec>
50+
</target>
51+
52+
<target name="cs-fix">
53+
<exec
54+
executable="vendor/bin/phpcbf"
55+
logoutput="true"
56+
passthru="true"
57+
checkreturn="true"
58+
>
59+
<arg value="--extensions=php"/>
60+
<arg value="--encoding=utf-8"/>
61+
<arg value="--tab-width=4"/>
62+
<arg value="--ignore=tests/*/data"/>
63+
<arg value="-sp"/>
64+
<arg path="src"/>
65+
<arg path="tests"/>
66+
</exec>
67+
</target>
68+
69+
<target name="tests">
70+
<exec
71+
executable="vendor/bin/phpunit"
72+
logoutput="true"
73+
passthru="true"
74+
checkreturn="true"
75+
>
76+
<arg value="-c"/>
77+
<arg value="tests/phpunit.xml"/>
78+
<arg path="tests"/>
79+
</exec>
80+
</target>
81+
82+
<target name="phpstan">
83+
<exec
84+
executable="vendor/bin/phpstan"
85+
logoutput="true"
86+
passthru="true"
87+
checkreturn="true"
88+
>
89+
<arg value="analyse"/>
90+
<arg value="-l"/>
91+
<arg value="7"/>
92+
<arg value="-c"/>
93+
<arg path="phpstan.neon"/>
94+
<arg path="src"/>
95+
<arg path="tests"/>
96+
</exec>
97+
</target>
98+
99+
</project>

Diff for: composer.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "phpstan/phpstan-phpunit",
3+
"description": "PHPUnit extensions and rules for PHPStan",
4+
"license": ["MIT"],
5+
"minimum-stability": "dev",
6+
"prefer-stable": true,
7+
"extra": {
8+
"branch-alias": {
9+
"dev-master": "0.9-dev"
10+
}
11+
},
12+
"require": {
13+
"php": "~7.0",
14+
"phpstan/phpstan": "^0.9",
15+
"phpunit/phpunit": "^6.3"
16+
},
17+
"require-dev": {
18+
"consistence/coding-standard": "^2.0",
19+
"jakub-onderka/php-parallel-lint": "^0.9.2",
20+
"phing/phing": "^2.16.0",
21+
"satooshi/php-coveralls": "^1.0",
22+
"slevomat/coding-standard": "^3.3.0"
23+
},
24+
"autoload": {
25+
"psr-4": {
26+
"PHPStan\\": "src/"
27+
}
28+
},
29+
"autoload-dev": {
30+
"classmap": ["tests/"]
31+
}
32+
}

Diff for: extension.neon

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
parameters:
2+
earlyTerminatingMethodCalls:
3+
PHPUnit\Framework\TestCase:
4+
- fail
5+
- markTestIncomplete
6+
- markTestSkipped
7+
8+
services:
9+
-
10+
class: PHPStan\Type\PHPUnit\CreateMockDynamicReturnTypeExtension
11+
tags:
12+
- phpstan.broker.dynamicMethodReturnTypeExtension

Diff for: phpcs.xml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="PHPStan PHPUnit extensions and rules">
3+
<rule ref="vendor/consistence/coding-standard/Consistence/ruleset.xml">
4+
<exclude name="Squiz.Functions.GlobalFunction.Found"/>
5+
</rule>
6+
<rule ref="vendor/slevomat/coding-standard/SlevomatCodingStandard/ruleset.xml">
7+
<exclude name="SlevomatCodingStandard.Classes.ClassConstantVisibility.MissingConstantVisibility"/>
8+
<exclude name="SlevomatCodingStandard.Files.TypeNameMatchesFileName"/>
9+
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameAfterKeyword"/>
10+
<exclude name="SlevomatCodingStandard.Namespaces.UseOnlyWhitelistedNamespaces"/>
11+
<exclude name="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly"/>
12+
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingTraversableParameterTypeHintSpecification"/>
13+
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingTraversableReturnTypeHintSpecification"/>
14+
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation.NonFullyQualifiedClassName"/>
15+
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants"/>
16+
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions"/>
17+
<exclude name="SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue"/>
18+
</rule>
19+
<rule ref="SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses">
20+
<properties>
21+
<property name="caseSensitive" value="false"/>
22+
</properties>
23+
</rule>
24+
<rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
25+
<properties>
26+
<property name="newlinesCountBetweenOpenTagAndDeclare" value="0"/>
27+
</properties>
28+
</rule>
29+
<rule ref="SlevomatCodingStandard.TypeHints.TypeHintDeclaration">
30+
<properties>
31+
<property name="usefulAnnotations" type="array" value="
32+
@dataProvider,
33+
@requires
34+
"/>
35+
<property name="enableNullableTypeHints" type="false" />
36+
<property name="enableVoidTypeHint" type="false" />
37+
</properties>
38+
</rule>
39+
</ruleset>

Diff for: phpstan.neon

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
includes:
2+
- extension.neon
3+
- rules.neon

Diff for: rules.neon

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
-
3+
class: PHPStan\Rules\PHPUnit\AssertSameDifferentTypesRule
4+
tags:
5+
- phpstan.rules.rule

Diff for: src/Rules/PHPUnit/AssertSameDifferentTypesRule.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\ObjectType;
8+
9+
class AssertSameDifferentTypesRule implements \PHPStan\Rules\Rule
10+
{
11+
12+
public function getNodeType(): string
13+
{
14+
return \PhpParser\NodeAbstract::class;
15+
}
16+
17+
/**
18+
* @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node
19+
* @param \PHPStan\Analyser\Scope $scope
20+
* @return string[] errors
21+
*/
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
$testCaseType = new ObjectType(\PHPUnit\Framework\TestCase::class);
25+
if ($node instanceof Node\Expr\MethodCall) {
26+
$calledOnType = $scope->getType($node->var);
27+
} elseif ($node instanceof Node\Expr\StaticCall) {
28+
if ($node->class instanceof Node\Name) {
29+
$class = (string) $node->class;
30+
if (in_array(
31+
strtolower($class),
32+
[
33+
'self',
34+
'static',
35+
'parent',
36+
],
37+
true
38+
)) {
39+
$calledOnType = new ObjectType($scope->getClassReflection()->getName());
40+
} else {
41+
$calledOnType = new ObjectType($class);
42+
}
43+
} else {
44+
$calledOnType = $scope->getType($node->class);
45+
}
46+
} else {
47+
return [];
48+
}
49+
50+
if (!$testCaseType->isSupersetOf($calledOnType)->yes()) {
51+
return [];
52+
}
53+
54+
if (count($node->args) < 2) {
55+
return [];
56+
}
57+
if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') {
58+
return [];
59+
}
60+
61+
$leftType = $scope->getType($node->args[0]->value);
62+
$rightType = $scope->getType($node->args[1]->value);
63+
64+
if ($leftType->isSupersetOf($rightType)->no()) {
65+
return [
66+
sprintf(
67+
'Call to assertSame() with different types %s and %s will always result in test failure.',
68+
$leftType->describe(),
69+
$rightType->describe()
70+
),
71+
];
72+
}
73+
74+
return [];
75+
}
76+
77+
}

0 commit comments

Comments
 (0)