Completed
Push — master ( 54b0e6...78a1c0 )
by
unknown
02:28
created

ContemporaryChecker   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 223
Duplicated Lines 3.14 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 13
dl 7
loc 223
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getSupportedContextTypes() 0 7 1
A getDefaultContextTypes() 0 3 1
C checkConstraint() 7 81 10
C getExtremeValue() 0 32 12
A getViolationMessage() 0 18 2
A checkConstraintParameters() 0 4 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
4
5
use Config;
6
use DataValues\DataValue;
7
use Wikibase\DataModel\Entity\EntityId;
8
use Wikibase\DataModel\Entity\ItemId;
9
use Wikibase\DataModel\Entity\PropertyId;
10
use Wikibase\DataModel\Services\Lookup\EntityLookup;
11
use Wikibase\DataModel\Snak\PropertyValueSnak;
12
use Wikibase\DataModel\Statement\Statement;
13
use Wikibase\DataModel\Statement\StatementList;
14
use WikibaseQuality\ConstraintReport\Constraint;
15
use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
16
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
17
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\RangeCheckerHelper;
18
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
19
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
20
use WikibaseQuality\ConstraintReport\Role;
21
22
/**
23
 * @author David Abián
24
 * @license GNU GPL v2+
25
 */
26
class ContemporaryChecker implements ConstraintChecker {
27
28
	/**
29
	 * @var RangeCheckerHelper
30
	 */
31
	private $rangeCheckerHelper;
32
33
	/**
34
	 * @var Config
35
	 */
36
	private $config;
37
38
	/**
39
	 * @var EntityLookup
40
	 */
41
	private $entityLookup;
42
43
	/**
44
	 * Name of the configuration variable for the array of IDs of the properties that
45
	 * state the start time of the entities.
46
	 */
47
	const CONFIG_VARIABLE_START_PROPERTY_IDS = 'WBQualityConstraintsStartTimePropertyIds';
48
49
	/**
50
	 * Name of the configuration variable for the array of IDs of the properties that
51
	 * state the end time of the entities.
52
	 */
53
	const CONFIG_VARIABLE_END_PROPERTY_IDS = 'WBQualityConstraintsEndTimePropertyIds';
54
55
	public function __construct(
56
		EntityLookup $entityLookup,
57
		RangeCheckerHelper $rangeCheckerHelper,
58
		Config $config
59
	) {
60
		$this->entityLookup = $entityLookup;
61
		$this->rangeCheckerHelper = $rangeCheckerHelper;
62
		$this->config = $config;
63
	}
64
65
	/**
66
	 * @codeCoverageIgnore This method is purely declarative.
67
	 */
68
	public function getSupportedContextTypes() {
69
		return [
70
			Context::TYPE_STATEMENT => CheckResult::STATUS_COMPLIANCE,
71
			Context::TYPE_QUALIFIER => CheckResult::STATUS_NOT_IN_SCOPE,
72
			Context::TYPE_REFERENCE => CheckResult::STATUS_NOT_IN_SCOPE,
73
		];
74
	}
75
76
	/**
77
	 * @codeCoverageIgnore This method is purely declarative.
78
	 */
79
	public function getDefaultContextTypes() {
80
		return [ Context::TYPE_STATEMENT ];
81
	}
82
83
	/**
84
	 * Checks 'Contemporary' constraint.
85
	 *
86
	 * @param Context $context
87
	 * @param Constraint $constraint
88
	 *
89
	 * @return CheckResult
90
	 * @throws \ConfigException
91
	 */
92
	public function checkConstraint( Context $context, Constraint $constraint ) {
93
		if ( $context->getSnakRank() === Statement::RANK_DEPRECATED ) {
94
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_DEPRECATED );
95
		}
96
		$snak = $context->getSnak();
97
		if ( !$snak instanceof PropertyValueSnak ) {
98
			// nothing to check
99
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_COMPLIANCE );
100 View Code Duplication
		} elseif ( $snak->getDataValue()->getType() !== 'wikibase-entityid' ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
			// wrong data type
102
			$message = ( new ViolationMessage( 'wbqc-violation-message-value-needed-of-type' ) )
103
				->withEntityId( new ItemId( $constraint->getConstraintTypeItemId() ), Role::CONSTRAINT_TYPE_ITEM )
104
				->withDataValueType( 'wikibase-entityid' );
105
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_VIOLATION, $message );
106
		}
107
		/** @var EntityId $subjectId */
108
		$subjectId = $context->getEntity()->getId();
109
		/** @var EntityId $objectId */
110
		$objectId = $snak->getDataValue()->getEntityId();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface DataValues\DataValue as the method getEntityId() does only exist in the following implementations of said interface: Wikibase\DataModel\Entity\EntityIdValue.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
111
		/** @var Statement[] $subjectStatements */
112
		$subjectStatements = $context->getEntity()->getStatements()->toArray();
113
		/** @var Statement[] $objectStatements */
114
		$objectStatements = $this->entityLookup->getEntity( $objectId )->getStatements()->toArray();
115
		/** @var String[] $startPropertyIds */
116
		$startPropertyIds = $this->config->get( self::CONFIG_VARIABLE_START_PROPERTY_IDS );
117
		/** @var String[] $endPropertyIds */
118
		$endPropertyIds = $this->config->get( self::CONFIG_VARIABLE_END_PROPERTY_IDS );
119
		$subjectStartValue = $this->getExtremeValue(
120
			$startPropertyIds,
121
			$subjectStatements,
122
			'start'
123
		);
124
		$objectStartValue = $this->getExtremeValue(
125
			$startPropertyIds,
126
			$objectStatements,
127
			'start'
128
		);
129
		$subjectEndValue = $this->getExtremeValue(
130
			$endPropertyIds,
131
			$subjectStatements,
132
			'end'
133
		);
134
		$objectEndValue = $this->getExtremeValue(
135
			$endPropertyIds,
136
			$objectStatements,
137
			'end'
138
		);
139
		if (
140
			$this->rangeCheckerHelper->getComparison( $subjectStartValue, $subjectEndValue ) <= 0 &&
141
			$this->rangeCheckerHelper->getComparison( $objectStartValue, $objectEndValue ) <= 0 && (
142
				$this->rangeCheckerHelper->getComparison( $subjectEndValue, $objectStartValue ) < 0 ||
143
				$this->rangeCheckerHelper->getComparison( $objectEndValue, $subjectStartValue ) < 0
144
			)
145
		) {
146
			if (
147
				$subjectEndValue == null ||
148
				$this->rangeCheckerHelper->getComparison( $objectEndValue, $subjectEndValue ) < 0
149
			) {
150
				$earlierEntityId = $objectId;
151
				$minEndValue = $objectEndValue;
152
				$maxStartValue = $subjectStartValue;
153
			} else {
154
				$earlierEntityId = $subjectId;
155
				$minEndValue = $subjectEndValue;
156
				$maxStartValue = $objectStartValue;
157
			}
158
			$message = $this->getViolationMessage(
159
				$earlierEntityId,
160
				$subjectId,
161
				$context->getSnak()->getPropertyId(),
162
				$objectId,
163
				$minEndValue,
0 ignored issues
show
Bug introduced by
It seems like $minEndValue defined by $objectEndValue on line 151 can be null; however, WikibaseQuality\Constrai...::getViolationMessage() 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...
164
				$maxStartValue
0 ignored issues
show
Bug introduced by
It seems like $maxStartValue can be null; however, getViolationMessage() 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...
165
			);
166
			$status = CheckResult::STATUS_VIOLATION;
167
		} else {
168
			$message = null;
169
			$status = CheckResult::STATUS_COMPLIANCE;
170
		}
171
		return new CheckResult( $context, $constraint, [], $status, $message );
172
	}
173
174
	/**
175
	 * @param String[] $extremePropertyIds
176
	 * @param Statement[] $statements
177
	 * @param string $startOrEnd 'start' or 'end'
178
	 *
179
	 * @return DataValue|null
180
	 */
181
	private function getExtremeValue( $extremePropertyIds, $statements, $startOrEnd ) {
182
		if ( $startOrEnd !== 'start' && $startOrEnd !== 'end' ) {
183
			throw new \InvalidArgumentException( '$startOrEnd must be \'start\' or \'end\'.' );
184
		}
185
		$extremeValue = null;
186
		foreach ( $extremePropertyIds as $extremePropertyId ) {
187
			$statementList = new StatementList( $statements );
188
			$extremeStatements = $statementList->getByPropertyId( new PropertyId( $extremePropertyId ) );
189
			/** @var Statement $extremeStatement */
190
			foreach ( $extremeStatements as $extremeStatement ) {
191
				if ( $extremeStatement->getRank() !== Statement::RANK_DEPRECATED ) {
192
					$snak = $extremeStatement->getMainSnak();
193
					if ( !$snak instanceof PropertyValueSnak ) {
194
						return null;
195
					} else {
196
						$comparison = $this->rangeCheckerHelper->getComparison(
197
							$snak->getDataValue(),
198
							$extremeValue
199
						);
200
						if (
201
							$extremeValue === null ||
202
							( $startOrEnd === 'start' && $comparison < 0 ) ||
203
							( $startOrEnd === 'end' && $comparison > 0 )
204
						) {
205
							$extremeValue = $snak->getDataValue();
206
						}
207
					}
208
				}
209
			}
210
		}
211
		return $extremeValue;
212
	}
213
214
	/**
215
	 * @param EntityId $earlierEntityId
216
	 * @param EntityId $subjectId
217
	 * @param EntityId $propertyId
218
	 * @param EntityId $objectId
219
	 * @param DataValue $minEndValue
220
	 * @param DataValue $maxStartValue
221
	 *
222
	 * @return ViolationMessage
223
	 */
224
	private function getViolationMessage(
225
		EntityId $earlierEntityId,
226
		EntityId $subjectId,
227
		EntityId $propertyId,
228
		EntityId $objectId,
229
		DataValue $minEndValue,
230
		DataValue $maxStartValue
231
	) {
232
		$messageKey = $earlierEntityId === $subjectId ?
233
			'wbqc-violation-message-contemporary-subject-earlier' :
234
			'wbqc-violation-message-contemporary-value-earlier';
235
		return ( new ViolationMessage( $messageKey ) )
236
			->withEntityId( $subjectId, Role::SUBJECT )
237
			->withEntityId( $propertyId, Role::PREDICATE )
238
			->withEntityId( $objectId, Role::OBJECT )
239
			->withDataValue( $minEndValue, Role::OBJECT )
240
			->withDataValue( $maxStartValue, Role::OBJECT );
241
	}
242
243
	public function checkConstraintParameters( Constraint $constraint ) {
244
		// no parameters
245
		return [];
246
	}
247
248
}
249