Completed
Pull Request — master (#99)
by
unknown
01:49
created

Signature::analyze()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 15
nc 1
nop 2
crap 1
1
<?php
2
3
namespace PHPSemVerChecker\Comparator;
4
5
use PhpParser\Node\FunctionLike;
6
7
class Signature
8
{
9
10 78
	public static function analyze(FunctionLike $functionA, FunctionLike $functionB)
11
	{
12
		$changes = [
13 78
			'function_renamed' => false,
14
			'function_renamed_case_only' => false,
15
			'parameter_added' => false,
16
			'parameter_removed' => false,
17
			'parameter_renamed' => false,
18
			'parameter_typing_added' => false,
19
			'parameter_typing_removed' => false,
20
			'parameter_default_added' => false,
21
			'parameter_default_removed' => false,
22
			'parameter_default_value_changed' => false,
23
		];
24
25 78
		$changes = self::detectFunctionNameChanges($changes, $functionA, $functionB);
26 78
		$changes = self::detectParameterChanges($changes, $functionA, $functionB);
27
28 78
		return $changes;
29
	}
30
31 78
	private static function detectFunctionNameChanges($changes, FunctionLike $functionA, FunctionLike $functionB)
32
	{
33 78
		if ($functionA->name != $functionB->name) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node\FunctionLike suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
34
35 3
			if (strtolower($functionA->name) == strtolower($functionB->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node\FunctionLike suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
36 2
				$changes['function_renamed_case_only'] = true;
37
			} else {
38 1
				$changes['function_renamed'] = true;
39
			}
40
41 3
			return $changes;
42
		}
43
44 75
		return $changes;
45
	}
46
47 78
	private static function detectParameterChanges($changes, FunctionLike $functionA, FunctionLike $functionB)
48
	{
49 78
		$parametersA = $functionA->getParams();
50 78
		$parametersB = $functionB->getParams();
51
52 78
		$lengthA = count($parametersA);
53 78
		$lengthB = count($parametersB);
54
55
		// TODO([email protected]): This is only true if newer params do not have defaults
56 78
		if ($lengthA < $lengthB) {
57 10
			$changes['parameter_added'] = true;
58 68
		} elseif ($lengthA > $lengthB) {
59 8
			$changes['parameter_removed'] = true;
60
		}
61
62 78
		$iterations = min($lengthA, $lengthB);
63 78
		for ($i = 0; $i < $iterations; ++$i) {
64
			// Name checking
65 69
			if ($parametersA[$i]->name !== $parametersB[$i]->name) {
66 11
				$changes['parameter_renamed'] = true;
67
			}
68
69
			// Type checking
70 69
			if (Type::get($parametersA[$i]->type) !== Type::get($parametersB[$i]->type)) {
0 ignored issues
show
Bug introduced by
It seems like $parametersA[$i]->type can also be of type object<PhpParser\Node\NullableType>; however, PHPSemVerChecker\Comparator\Type::get() does only seem to accept object<PhpParser\Node\Name>|string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $parametersB[$i]->type can also be of type object<PhpParser\Node\NullableType>; however, PHPSemVerChecker\Comparator\Type::get() does only seem to accept object<PhpParser\Node\Name>|string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
71
//				if ($paramsA[$i]->default !== null && $paramsB[$i]->default !== null) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
72
//					$changes['parameter_default_value_changed'] = true;
73 16
				if ($parametersA[$i]->type !== null) {
74 8
					$changes['parameter_typing_removed'] = true;
75
				}
76 16
				if ($parametersB[$i]->type !== null) {
77 8
					$changes['parameter_typing_added'] = true;
78
				}
79
			}
80
81
			// Default checking
82 69
			if ($parametersA[$i]->default === null && $parametersB[$i]->default === null) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
83
				// Do nothing
84 24
			} elseif ($parametersA[$i]->default !== null && $parametersB[$i]->default === null) {
85 8
				$changes['parameter_default_removed'] = true;
86 16
			} elseif ($parametersA[$i]->default === null && $parametersB[$i]->default !== null) {
87 8
				$changes['parameter_default_added'] = true;
88
				// TODO([email protected]): Not all nodes have a value property
89 8
			} elseif (!Node::isEqual($parametersA[$i]->default, $parametersB[$i]->default)) {
0 ignored issues
show
Bug introduced by
It seems like $parametersA[$i]->default can be null; however, isEqual() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like $parametersB[$i]->default can be null; however, isEqual() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
90 8
				$changes['parameter_default_value_changed'] = true;
91
			}
92
		}
93
94 78
		return $changes;
95
	}
96
}
97