SMWQueryParser   D
last analyzed

Complexity

Total Complexity 112

Size/Duplication

Total Lines 603
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 86.67%

Importance

Changes 0
Metric Value
dl 0
loc 603
ccs 234
cts 270
cp 0.8667
rs 4.5001
c 0
b 0
f 0
wmc 112
lcom 1
cbo 11

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A getQueryToken() 0 3 1
A setContextPage() 0 3 1
A setDefaultNamespaces() 0 12 3
A getQueryDescription() 0 19 3
A getErrors() 0 3 1
A getErrorString() 0 3 1
F getSubqueryDescription() 0 100 25
B getLinkDescription() 0 25 6
C getClassDescription() 0 28 8
F getPropertyDescription() 0 114 26
C getArticleDescription() 0 49 9
C finishLinkDescription() 0 42 11
C readChunk() 0 32 11
A pushDelimiter() 0 3 1
A popDelimiter() 0 4 1
A isPagePropertyType() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like SMWQueryParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SMWQueryParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use SMW\DataTypeRegistry;
4
use SMW\DIWikiPage;
5
use SMW\Query\Language\ClassDescription;
6
use SMW\Query\Language\ConceptDescription;
7
use SMW\Query\Language\Description;
8
use SMW\Query\Language\NamespaceDescription;
9
use SMW\Query\Language\SomeProperty;
10
use SMW\Query\Language\ThingDescription;
11
use SMW\Query\Parser\DescriptionProcessor;
12
use SMW\Query\QueryToken;
13
14
/**
15
 * Objects of this class are in charge of parsing a query string in order
16
 * to create an SMWDescription. The class and methods are not static in order
17
 * to more cleanly store the intermediate state and progress of the parser.
18
 * @ingroup SMWQuery
19
 * @author Markus Krötzsch
20
 */
21
class SMWQueryParser {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
22
23
	private $separatorStack; // list of open blocks ("parentheses") that need closing at current step
24
	private $currentString; // remaining string to be parsed (parsing eats query string from the front)
25
26
	private $defaultNamespace; // description of the default namespace restriction, or NULL if not used
27
28
	private $categoryPrefix; // cache label of category namespace . ':'
29
	private $conceptPrefix; // cache label of concept namespace . ':'
30
	private $categoryPrefixCannonical; // cache canonnical label of category namespace . ':'
31
	private $conceptPrefixCannonical; // cache canonnical label of concept namespace . ':'
32
	private $queryFeatures; // query features to be supported, format similar to $smwgQFeatures
33
34
	/**
35
	 * @var DescriptionProcessor
36
	 */
37
	private $descriptionProcessor;
38 214
39 214
	/**
40
	 * @var QueryToken
41 214
	 */
42 214
	private $queryToken;
43 214
44 214
	public function __construct( $queryFeatures = false ) {
45
		global $wgContLang, $smwgQFeatures;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
46 214
47 214
		$this->categoryPrefix = $wgContLang->getNsText( NS_CATEGORY ) . ':';
48 214
		$this->conceptPrefix = $wgContLang->getNsText( SMW_NS_CONCEPT ) . ':';
49 214
		$this->categoryPrefixCannonical = 'Category:';
50
		$this->conceptPrefixCannonical = 'Concept:';
51
52
		$this->defaultNamespace = null;
53
		$this->queryFeatures = $queryFeatures === false ? $smwgQFeatures : $queryFeatures;
54
		$this->descriptionProcessor = new DescriptionProcessor( $this->queryFeatures );
55
		$this->queryToken = new QueryToken();
56 108
	}
57 108
58 108
	/**
59
	 * @since 2.5
60
	 *
61
	 * @return QueryToken
62
	 */
63
	public function getQueryToken() {
64 109
		return $this->queryToken;
65 109
	}
66
67 109
	/**
68 1
	 * @since 2.4
69 1
	 *
70 1
	 * @param DIWikiPage|null $contextPage
71 1
	 */
72
	public function setContextPage( DIWikiPage $contextPage = null ) {
73
		$this->descriptionProcessor->setContextPage( $contextPage );
74
	}
75 109
76
	/**
77
	 * Provide an array of namespace constants that are used as default restrictions.
78
	 * If NULL is given, no such default restrictions will be added (faster).
79
	 */
80
	public function setDefaultNamespaces( $namespaceArray ) {
81
		$this->defaultNamespace = null;
82
83
		if ( !is_null( $namespaceArray ) ) {
84
			foreach ( $namespaceArray as $ns ) {
85
				$this->defaultNamespace = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
86 170
					$this->defaultNamespace,
87
					new NamespaceDescription( $ns )
88 170
				);
89 170
			}
90 170
		}
91 170
	}
92 170
93
	/**
94 170
	 * Compute an SMWDescription from a query string. Returns whatever descriptions could be
95 169
	 * wrestled from the given string (the most general result being SMWThingDescription if
96
	 * no meaningful condition was extracted).
97
	 *
98 170
	 * @param string $queryString
99 1
	 *
100
	 * @return Description
101
	 */
102
	public function getQueryDescription( $queryString ) {
103 170
104
		$this->descriptionProcessor->clear();
105
		$this->currentString = $queryString;
106
		$this->separatorStack = array();
107
		$setNS = false;
108
		$result = $this->getSubqueryDescription( $setNS );
109
110
		if ( !$setNS ) { // add default namespaces if applicable
111 109
			$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $this->defaultNamespace, $result );
112 109
		}
113
114
		if ( is_null( $result ) ) { // parsing went wrong, no default namespaces
115
			$result = new ThingDescription();
116
		}
117
118
119
		return $result;
120
	}
121
122
	/**
123
	 * Return array of error messages (possibly empty).
124
	 *
125
	 * @return array
126
	 */
127
	public function getErrors() {
128
		return $this->descriptionProcessor->getErrors();
129
	}
130
131
	/**
132
	 * Return error message or empty string if no error occurred.
133
	 *
134
	 * @return string
135
	 */
136
	public function getErrorString() {
137
		return smwfEncodeMessages( $this->getErrors() );
138
	}
139
140
	/**
141
	 * Compute an SMWDescription for current part of a query, which should
142
	 * be a standalone query (the main query or a subquery enclosed within
143
	 * "\<q\>...\</q\>". Recursively calls similar methods and returns NULL upon error.
144
	 *
145
	 * The call-by-ref parameter $setNS is a boolean. Its input specifies whether
146
	 * the query should set the current default namespace if no namespace restrictions
147 170
	 * were given. If false, the calling super-query is happy to set the required
148 170
	 * NS-restrictions by itself if needed. Otherwise the subquery has to impose the defaults.
149 170
	 * This is so, since outermost queries and subqueries of disjunctions will have to set
150 170
	 * their own default restrictions.
151 170
	 *
152
	 * The return value of $setNS specifies whether or not the subquery has a namespace
153 170
	 * specification in place. This might happen automatically if the query string imposes
154
	 * such restrictions. The return value is important for those callers that otherwise
155 170
	 * set up their own restrictions.
156 170
	 *
157
	 * Note that $setNS is no means to switch on or off default namespaces in general,
158
	 * but just controls query generation. For general effect, the default namespaces
159 170
	 * should be set to NULL.
160 170
	 *
161
	 * @return Description|null
162 170
	 */
163 170
	private function getSubqueryDescription( &$setNS ) {
164
		$conjunction = null;      // used for the current inner conjunction
165 170
		$disjuncts = array();     // (disjunctive) array of subquery conjunctions
166 170
		$hasNamespaces = false;   // does the current $conjnuction have its own namespace restrictions?
167 170
		$mustSetNS = $setNS;      // must NS restrictions be set? (may become true even if $setNS is false)
168 14
169 14
		$continue = ( $chunk = $this->readChunk() ) !== ''; // skip empty subquery completely, thorwing an error
170 14
171 170
		while ( $continue ) {
172 170
			$setsubNS = false;
173 170
174 26
			switch ( $chunk ) {
175 170
				case '[[': // start new link block
176 1
					$ld = $this->getLinkDescription( $setsubNS );
177
178 1
					if ( !is_null( $ld ) ) {
179 1
						$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $ld );
180
					}
181 1
				break;
182
				case 'AND':
183
				case '<q>': // enter new subquery, currently irrelevant but possible
184
					$this->pushDelimiter( '</q>' );
185 1
					$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->getSubqueryDescription( $setsubNS ) );
186 1
				break;
187
				case 'OR':
188
				case '||':
189
				case '':
190
				case '</q>': // finish disjunction and maybe subquery
191
					if ( !is_null( $this->defaultNamespace ) ) { // possibly add namespace restrictions
192 170
						if ( $hasNamespaces && !$mustSetNS ) {
193
							// add NS restrictions to all earlier conjunctions (all of which did not have them yet)
194 170
							$mustSetNS = true; // enforce NS restrictions from now on
195 170
							$newdisjuncts = array();
196
197
							foreach ( $disjuncts as $conj ) {
198 170
								$newdisjuncts[] = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conj, $this->defaultNamespace );
199 24
							}
200 24
201
							$disjuncts = $newdisjuncts;
202
						} elseif ( !$hasNamespaces && $mustSetNS ) {
203 24
							// add ns restriction to current result
204
							$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->defaultNamespace );
205 170
						}
206 170
					}
207
208 170
					$disjuncts[] = $conjunction;
209 2
					// start anew
210
					$conjunction = null;
211
					$hasNamespaces = false;
212 2
213
					// finish subquery?
214
					if ( $chunk == '</q>' ) {
215
						if ( $this->popDelimiter( '</q>' ) ) {
216 170
							$continue = false; // leave the loop
217 48
						} else {
218
							$this->descriptionProcessor->addErrorWithMsgKey( 'smw_toomanyclosing', $chunk );
219
							return null;
220 170
						}
221 170
					} elseif ( $chunk === '' ) {
222
						$continue = false;
223
					}
224
				break;
225 170
				case '+': // "... AND true" (ignore)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
226 170
				break;
227
				default: // error: unexpected $chunk
228 170
					$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk );
229 170
					// return null; // Try to go on, it can only get better ...
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
230 1
			}
231 1
232 1
			if ( $setsubNS ) { // namespace restrictions encountered in current conjunct
233
				$hasNamespaces = true;
234 170
			}
235
236
			if ( $continue ) { // read on only if $continue remained true
237
				$chunk = $this->readChunk();
238
			}
239
		}
240
241
		if ( count( $disjuncts ) > 0 ) { // make disjunctive result
242
			$result = null;
243 170
244
			foreach ( $disjuncts as $d ) {
245 170
				if ( is_null( $d ) ) {
246
					$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
247
					$setNS = false;
248
					return null;
249
				} else {
250
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $d );
251
				}
252
			}
253
		} else {
254 170
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
255
			$setNS = false;
256
			return null;
257 170
		}
258
259 170
		$setNS = $mustSetNS; // NOTE: also false if namespaces were given but no default NS descs are available
260 170
261 66
		return $result;
262 66
	}
263
264
	/**
265 155
	 * Compute an SMWDescription for current part of a query, which should
266
	 * be the content of "[[ ... ]]". Returns NULL upon error.
267 155
	 *
268 125
	 * Parameters $setNS has the same use as in getSubqueryDescription().
269 125
	 */
270
	private function getLinkDescription( &$setNS ) {
271
		// This method is called when we encountered an opening '[['. The following
272
		// block could be a Category-statement, fixed object, or property statement.
273
		$chunk = $this->readChunk( '', true, false ); // NOTE: untrimmed, initial " " escapes prop. chains
274
275 48
		if ( in_array( smwfNormalTitleText( $chunk ),
276
			array( $this->categoryPrefix, $this->conceptPrefix, $this->categoryPrefixCannonical, $this->conceptPrefixCannonical ) ) ) {
277
			return $this->getClassDescription( $setNS, (
278
				smwfNormalTitleText( $chunk ) == $this->categoryPrefix || smwfNormalTitleText( $chunk ) == $this->categoryPrefixCannonical
279
			) );
280
		} else { // fixed subject, namespace restriction, property query, or subquery
281
			$sep = $this->readChunk( '', false ); // do not consume hit, "look ahead"
282
283
			if ( ( $sep == '::' ) || ( $sep == ':=' ) ) {
284
				if ( $chunk{0} != ':' ) { // property statement
285 66
					return $this->getPropertyDescription( $chunk, $setNS );
286
				} else { // escaped article description, read part after :: to get full contents
287 66
					$chunk .= $this->readChunk( '\[\[|\]\]|\|\||\|' );
288 66
					return $this->getArticleDescription( trim( $chunk ), $setNS );
289
				}
290 66
			} else { // Fixed article/namespace restriction. $sep should be ]] or ||
291 66
				return $this->getArticleDescription( trim( $chunk ), $setNS );
292
			}
293 66
		}
294
	}
295
296
	/**
297
	 * Parse a category description (the part of an inline query that
298 66
	 * is in between "[[Category:" and the closing "]]" and create a
299
	 * suitable description.
300 66
	 */
301 66
	private function getClassDescription( &$setNS, $category = true ) {
302 66
		// note: no subqueries allowed here, inline disjunction allowed, wildcards allowed
303 66
		$result = null;
304
		$continue = true;
305
306
		while ( $continue ) {
307 66
			$chunk = $this->readChunk();
308 66
309
			if ( $chunk == '+' ) {
310
				$description = new NamespaceDescription( $category ? NS_CATEGORY : SMW_NS_CONCEPT );
311 66
				$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $description );
312
			} else { // assume category/concept title
313
				/// NOTE: we add m_c...prefix to prevent problems with, e.g., [[Category:Template:Test]]
314
				$title = Title::newFromText( ( $category ? $this->categoryPrefix : $this->conceptPrefix ) . $chunk );
315
316
				if ( !is_null( $title ) ) {
317
					$diWikiPage = new SMWDIWikiPage( $title->getDBkey(), $title->getNameSpace(), '' );
318
					$desc = $category ? new ClassDescription( $diWikiPage ) : new ConceptDescription( $diWikiPage );
319
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $desc );
320 125
				}
321 125
			}
322
323
			$chunk = $this->readChunk();
324 125
			$continue = ( $chunk == '||' ) && $category; // disjunctions only for cateories
325 125
		}
326 125
327 125
		return $this->finishLinkDescription( $chunk, false, $result, $setNS );
0 ignored issues
show
Bug introduced by
The variable $chunk does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
328
	}
329 125
330 125
	/**
331
	 * Parse a property description (the part of an inline query that
332
	 * is in between "[[Some property::" and the closing "]]" and create a
333
	 * suitable description. The "::" is the first chunk on the current
334
	 * string.
335 125
	 */
336
	private function getPropertyDescription( $propertyName, &$setNS ) {
337 125
		$this->readChunk(); // consume separator ":=" or "::"
338
339
		// first process property chain syntax (e.g. "property1.property2::value"), escaped by initial " ":
340
		$propertynames = ( $propertyName{0} == ' ' ) ? array( $propertyName ) : explode( '.', $propertyName );
341
		$properties = array();
342 125
		$typeid = '_wpg';
343 125
		$inverse = false;
344 125
345
		foreach ( $propertynames as $name ) {
346
			if ( !$this->isPagePropertyType( $typeid ) ) { // non-final property in chain was no wikipage: not allowed
347 125
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', $name );
348 125
				return null; ///TODO: read some more chunks and try to finish [[ ]]
349
			}
350 125
351 125
			$property = SMWPropertyValue::makeUserProperty( $name );
352
353
			if ( !$property->isValid() ) { // illegal property identifier
354 125
				$this->descriptionProcessor->addError( $property->getErrors() );
355 42
				return null; ///TODO: read some more chunks and try to finish [[ ]]
356
			}
357
358 42
			$typeid = $property->getDataItem()->findPropertyTypeID();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMWDataItem as the method findPropertyTypeID() does only exist in the following sub-classes of SMWDataItem: SMWDIProperty, SMW\DIProperty. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
359
			$inverse = $property->isInverse();
0 ignored issues
show
Deprecated Code introduced by
The method SMWPropertyValue::isInverse() has been deprecated with message: since 1.6

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
360 42
			$properties[] = $property;
361 42
		} ///NOTE: after iteration, $property and $typeid correspond to last value
362 98
363 12
		$innerdesc = null;
364 12
		$continue = true;
365 12
366 12
		while ( $continue ) {
367
			$chunk = $this->readChunk();
368
369
			switch ( $chunk ) {
370
				case '+': // wildcard, add namespaces for page-type properties
371 12
					if ( !is_null( $this->defaultNamespace ) && ( $this->isPagePropertyType( $typeid ) || $inverse ) ) {
372 12
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace );
373
					} else {
374
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
375 98
					}
376 98
					$chunk = $this->readChunk();
377 98
				break;
378
				case '<q>': // subquery, set default namespaces
379 98
					if ( $this->isPagePropertyType( $typeid ) || $inverse ) {
380 98
						$this->pushDelimiter( '</q>' );
381
						$setsubNS = true;
382 98
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->getSubqueryDescription( $setsubNS ) );
383
					} else { // no subqueries allowed for non-pages
384
						$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', end( $propertynames ) );
385 98
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
386 98
					}
387 98
					$chunk = $this->readChunk();
388 11
				break;
389 10
				default: // normal object value
390 10
					// read value(s), possibly with inner [[...]]
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% 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...
391 10
					$open = 1;
392
					$value = $chunk;
393 10
					$continue2 = true;
394 1
					// read value with inner [[, ]], ||
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% 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...
395
					while ( ( $open > 0 ) && ( $continue2 ) ) {
396
						$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
397
						switch ( $chunk ) {
398 98
							case '[[': // open new [[ ]]
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
399 1
								$open++;
400
							break;
401
							case ']]': // close [[ ]]
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
402
								$open--;
403 98
							break;
404
							case '|':
405 98
							case '||': // terminates only outermost [[ ]]
406
								if ( $open == 1 ) {
407
									$open = 0;
408
								}
409 125
							break;
410
							case '': ///TODO: report error; this is not good right now
411
								$continue2 = false;
412 125
							break;
413
						}
414
						if ( $open != 0 ) {
415
							$value .= $chunk;
416
						}
417
					} ///NOTE: at this point, we normally already read one more chunk behind the value
418
					$outerDesription = $this->descriptionProcessor->constructDescriptionForPropertyObjectValue(
419 125
						$property->getDataItem(),
0 ignored issues
show
Bug introduced by
The variable $property does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Compatibility introduced by
$property->getDataItem() of type object<SMWDataItem> is not a sub-type of object<SMW\DIProperty>. It seems like you assume a child class of the class SMWDataItem to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
420
						$value
421 125
					);
422 125
423
					$this->queryToken->addFromDesciption( $outerDesription );
424
					$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
425 125
						$innerdesc,
426
						$outerDesription
427 125
					);
428
429
			}
430
			$continue = ( $chunk == '||' );
431
		}
432
433
		if ( is_null( $innerdesc ) ) { // make a wildcard search
434
			$innerdesc = ( !is_null( $this->defaultNamespace ) && $this->isPagePropertyType( $typeid ) ) ?
435
							$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace ) :
436
							$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
437 48
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_propvalueproblem', $property->getWikiValue() );
438 48
		}
439 48
440 48
		$properties = array_reverse( $properties );
441
442 48
		foreach ( $properties as $property ) {
443 48
			$innerdesc = new SomeProperty( $property->getDataItem(), $innerdesc );
0 ignored issues
show
Bug introduced by
It seems like $innerdesc can be null; however, __construct() 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...
444
		}
445
446
		$result = $innerdesc;
447
448
		return $this->finishLinkDescription( $chunk, false, $result, $setNS );
0 ignored issues
show
Bug introduced by
The variable $chunk does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
449 48
	}
450
451 48
	/**
452 2
	 * Parse an article description (the part of an inline query that
453
	 * is in between "[[" and the closing "]]" assuming it is not specifying
454 48
	 * a category or property) and create a suitable description.
455
	 * The first chunk behind the "[[" has already been read and is
456 4
	 * passed as a parameter.
457
	 */
458 4
	private function getArticleDescription( $firstChunk, &$setNS ) {
459 4
		$chunk = $firstChunk;
460
		$result = null;
461
		$continue = true;
462 45
463
		while ( $continue ) {
464 45
			if ( $chunk == '<q>' ) { // no subqueries of the form [[<q>...</q>]] (not needed)
465
466
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsubquery' );
467
				return null;
468 48
			}
469
470 48
			$list = preg_split( '/:/', $chunk, 3 ); // ":Category:Foo" "User:bar"  ":baz" ":+"
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
471
472
			if ( ( $list[0] === '' ) && ( count( $list ) == 3 ) ) {
473
				$list = array_slice( $list, 1 );
474 48
			}
475
			if ( ( count( $list ) == 2 ) && ( $list[1] == '+' ) ) { // try namespace restriction
476
477
				$idx = \SMW\Localizer::getInstance()->getNamespaceIndexByName( $list[0] );
478 48
479
				if ( $idx !== false ) {
480
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, new NamespaceDescription( $idx ) );
0 ignored issues
show
Bug introduced by
It seems like $idx defined by \SMW\Localizer::getInsta...ceIndexByName($list[0]) on line 477 can also be of type boolean; however, SMW\Query\Language\Names...cription::__construct() does only seem to accept integer, 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...
481 170
				}
482 170
			} else {
483
				$outerDesription = $this->descriptionProcessor->constructDescriptionForWikiPageValueChunk(
484 170
					$chunk
485
				);
486
487
				$this->queryToken->addFromDesciption( $outerDesription );
488
489 170
				$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
490
					$result,
491 170
					$outerDesription
492
				);
493 1
			}
494 1
495 1
			$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
496 1
497 1
			if ( $chunk == '||' ) {
498
				$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
499 1
				$continue = true;
500
			} else {
501
				$continue = false;
502 170
			}
503
		}
504
505
		return $this->finishLinkDescription( $chunk, true, $result, $setNS );
506 2
	}
507 1
508
	private function finishLinkDescription( $chunk, $hasNamespaces, $result, &$setNS ) {
509
		if ( is_null( $result ) ) { // no useful information or concrete error found
510 1
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk ); // was smw_badqueryatom
511
		} elseif ( !$hasNamespaces && $setNS && !is_null( $this->defaultNamespace  ) ) {
512 1
			$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $result, $this->defaultNamespace );
513 1
			$hasNamespaces = true;
514
		}
515
516 2
		$setNS = $hasNamespaces;
517 1
518
		if ( $chunk == '|' ) { // skip content after single |, but report a warning
519
			// Note: Using "|label" in query atoms used to be a way to set the mainlabel in SMW <1.0; no longer supported now
520
			$chunk = $this->readChunk( '\]\]' );
521 170
			$labelpart = '|';
522
			if ( $chunk != ']]' ) {
523
				$labelpart .= $chunk;
524
				$chunk = $this->readChunk( '\]\]' );
525
			}
526
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $labelpart );
527
		}
528
529
		if ( $chunk != ']]' ) {
530
			// What happended? We found some chunk that could not be processed as
531
			// link content (as in [[Category:Test<q>]]), or the closing ]] are
532
			// just missing entirely.
533
			if ( $chunk !== '' ) {
534
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsymbol', $chunk );
535
536
				// try to find a later closing ]] to finish this misshaped subpart
537
				$chunk = $this->readChunk( '\]\]' );
538
539
				if ( $chunk != ']]' ) {
540
					$chunk = $this->readChunk( '\]\]' );
541 170
				}
542 170
			}
543
			if ( $chunk === '' ) {
544 170
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_noclosingbrackets' );
545 170
			}
546 170
		}
547
548 170
		return $result;
549 170
	}
550 170
551 170
	/**
552
	 * Get the next unstructured string chunk from the query string.
553
	 * Chunks are delimited by any of the special strings used in inline queries
554 170
	 * (such as [[, ]], <q>, ...). If the string starts with such a delimiter,
555 170
	 * this delimiter is returned. Otherwise the first string in front of such a
556 170
	 * delimiter is returned.
557 170
	 * Trailing and initial spaces are ignored if $trim is true, and chunks
558 170
	 * consisting only of spaces are not returned.
559
	 * If there is no more qurey string left to process, the empty string is
560
	 * returned (and in no other case).
561 170
	 *
562
	 * The stoppattern can be used to customise the matching, especially in order to
563 169
	 * overread certain special symbols.
564 169
	 *
565
	 * $consume specifies whether the returned chunk should be removed from the
566
	 * query string.
567 169
	 */
568
	private function readChunk( $stoppattern = '', $consume = true, $trim = true ) {
569
		if ( $stoppattern === '' ) {
570
			$stoppattern = '\[\[|\]\]|::|:=|<q>|<\/q>' .
571
				'|^' . $this->categoryPrefix . '|^' . $this->categoryPrefixCannonical .
572
				'|^' . $this->conceptPrefix . '|^' . $this->conceptPrefixCannonical .
573
				'|\|\||\|';
574
		}
575
		$chunks = preg_split( '/[\s]*(' . $stoppattern . ')/iu', $this->currentString, 2, PREG_SPLIT_DELIM_CAPTURE );
576
		if ( count( $chunks ) == 1 ) { // no matches anymore, strip spaces and finish
577
			if ( $consume ) {
578 24
				$this->currentString = '';
579 24
			}
580 24
581
			return $trim ? trim( $chunks[0] ) : $chunks[0];
582
		} elseif ( count( $chunks ) == 3 ) { // this should generally happen if count is not 1
583
			if ( $chunks[0] === '' ) { // string started with delimiter
584
				if ( $consume ) {
585
					$this->currentString = $chunks[2];
586
				}
587 24
588 24
				return $trim ? trim( $chunks[1] ) : $chunks[1];
589 24
			} else {
590
				if ( $consume ) {
591
					$this->currentString = $chunks[1] . $chunks[2];
592 125
				}
593 125
594
				return $trim ? trim( $chunks[0] ) : $chunks[0];
595
			}
596
		} else {
597
			return false;
598
		} // should never happen
599
	}
600
601
	/**
602
	 * Enter a new subblock in the query, which must at some time be terminated by the
603
	 * given $endstring delimiter calling popDelimiter();
604
	 */
605
	private function pushDelimiter( $endstring ) {
606
		array_push( $this->separatorStack, $endstring );
607
	}
608
609
	/**
610
	 * Exit a subblock in the query ending with the given delimiter.
611
	 * If the delimiter does not match the top-most open block, false
612
	 * will be returned. Otherwise return true.
613
	 */
614
	private function popDelimiter( $endstring ) {
615
		$topdelim = array_pop( $this->separatorStack );
616
		return ( $topdelim == $endstring );
617
	}
618
619
	private function isPagePropertyType( $typeid ) {
620
		return $typeid == '_wpg' || DataTypeRegistry::getInstance()->isSubDataType( $typeid );
621
	}
622
623
}
624