tokens = $tokens; $this->major = $major; $this->minor = $minor; $this->lo = $target . '.0'; $this->hi = $target . '.99'; $this->id = array( $major * 10000 + $minor * 100, $major * 10000 + $minor * 100 + 99 ); $keys = function ( array $codes ) { return array_fill_keys( $codes, true ); }; $this->levels = array( array( 'seq', $keys( array( T_COMMA, T_DOUBLE_ARROW, T_FN_ARROW ) ) ), array( 'or', $keys( array( T_LOGICAL_OR ) ) ), array( 'seq', $keys( array( T_LOGICAL_XOR ) ) ), array( 'and', $keys( array( T_LOGICAL_AND ) ) ), array( 'seq', Tokens::ASSIGNMENT_TOKENS ), array( 'ternary', $keys( array( T_INLINE_THEN, T_INLINE_ELSE ) ) ), array( 'seq', $keys( array( T_COALESCE ) ) ), array( 'or', $keys( array( T_BOOLEAN_OR ) ) ), array( 'and', $keys( array( T_BOOLEAN_AND ) ) ), ); } /** True when the finding PHP_CodeSniffer reported at this line and column cannot run on the target PHP. */ public function skips( $line, $column, $type, $code ) { if ( null === $this->at ) { $this->at = array(); foreach ( $this->tokens as $ptr => $t ) { if ( ! isset( $this->at[ $t['line'] ][ $t['column'] ] ) ) { $this->at[ $t['line'] ][ $t['column'] ] = $ptr; } } } if ( ! isset( $this->at[ $line ][ $column ] ) ) { return false; } $ptr = $this->at[ $line ][ $column ]; // function_exists( 'x' ) and defined( 'X' ) are false on the target only for what the finding says is removed there. $this->removed = 'error' === $type && preg_match( '/\.RemovedFunctions\.(\w+?)(?:Deprecated)?Removed$/', $code, $m ) ? strtolower( $m[1] ) : ''; $this->constant = 'error' === $type && preg_match( '/\.RemovedConstants\.(\w+?)(?:Deprecated)?Removed$/', $code, $m ) ? strtolower( $m[1] ) : ''; foreach ( $this->tokens[ $ptr ]['conditions'] as $owner => $kind ) { if ( in_array( $kind, array( T_IF, T_ELSEIF, T_ELSE ), true ) && $this->dead( $owner ) ) { return true; } } $start = $this->statement_start( $ptr ); if ( $this->dead_inline( $start ) ) { return true; } $list = $this->significant( $start, $this->statement_end( $start ) ); return isset( $list['i'][ $ptr ] ) && $this->blocked( $list, 0, count( $list['p'] ) - 1, $list['i'][ $ptr ], 0 ); } /** An if, elseif or else branch that never runs: its condition is false, or an earlier branch of the chain is true. */ private function dead( $owner ) { if ( T_ELSE !== $this->tokens[ $owner ]['code'] && false === $this->condition( $owner ) ) { return true; } for ( $prev = $this->previous_branch( $owner ); null !== $prev; $prev = $this->previous_branch( $prev ) ) { if ( true === $this->condition( $prev ) ) { return true; } } return false; } private function condition( $owner ) { $t = $this->tokens[ $owner ]; if ( ! isset( $t['parenthesis_opener'], $t['parenthesis_closer'] ) ) { return null; } $list = $this->significant( $t['parenthesis_opener'] + 1, $t['parenthesis_closer'] - 1 ); return $list['p'] ? $this->value( $list, 0, count( $list['p'] ) - 1, 0 ) : null; } private function previous_branch( $owner ) { if ( T_IF === $this->tokens[ $owner ]['code'] ) { // "else if" in two words continues the chain of that else. $owner = $this->prev_sig( $owner - 1 ); if ( null === $owner || T_ELSE !== $this->tokens[ $owner ]['code'] ) { return null; } } $end = $this->prev_sig( $owner - 1 ); if ( null === $end ) { return null; } $prev = null; if ( T_CLOSE_CURLY_BRACKET === $this->tokens[ $end ]['code'] && isset( $this->tokens[ $end ]['scope_condition'] ) ) { $prev = $this->tokens[ $end ]['scope_condition']; } elseif ( T_SEMICOLON === $this->tokens[ $end ]['code'] ) { $prev = $this->inline_owner( $this->statement_start( $end ) ); } return null !== $prev && in_array( $this->tokens[ $prev ]['code'], array( T_IF, T_ELSEIF ), true ) ? $prev : null; } /** The if, elseif or else without braces whose single statement starts here, or null. */ private function inline_owner( $start ) { $before = $this->prev_sig( $start - 1 ); if ( null === $before ) { return null; } $t = $this->tokens[ $before ]; if ( T_CLOSE_PARENTHESIS === $t['code'] && isset( $t['parenthesis_owner'] ) ) { $owner = $t['parenthesis_owner']; return in_array( $this->tokens[ $owner ]['code'], array( T_IF, T_ELSEIF ), true ) && ! isset( $this->tokens[ $owner ]['scope_opener'] ) ? $owner : null; } return T_ELSE === $t['code'] && ! isset( $t['scope_opener'] ) ? $before : null; } private function dead_inline( $start ) { $owner = $this->inline_owner( $start ); return null !== $owner && $this->dead( $owner ); } private function statement_start( $ptr ) { $stops = array( T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG, T_COLON, T_ELSE, T_RETURN, T_ECHO, T_PRINT, T_THROW, T_YIELD, T_YIELD_FROM ); for ( $i = $ptr - 1; $i >= 0; $i-- ) { $t = $this->tokens[ $i ]; if ( T_CLOSE_PARENTHESIS === $t['code'] && isset( $t['parenthesis_opener'] ) ) { if ( isset( $t['parenthesis_owner'] ) && $this->controls( $t['parenthesis_owner'] ) ) { return $i + 1; } $i = $t['parenthesis_opener']; } elseif ( isset( $t['bracket_opener'] ) && $t['bracket_opener'] < $i ) { if ( T_CLOSE_CURLY_BRACKET === $t['code'] && ! $this->expression_body( $t ) ) { return $i + 1; } $i = isset( $t['scope_condition'] ) && $this->expression_body( $t ) ? $t['scope_condition'] : $t['bracket_opener']; } elseif ( T_OPEN_PARENTHESIS === $t['code'] && isset( $t['parenthesis_owner'] ) && $this->controls( $t['parenthesis_owner'] ) ) { return $i + 1; } elseif ( in_array( $t['code'], $stops, true ) ) { return $i + 1; } } return 0; } private function statement_end( $start ) { $last = count( $this->tokens ) - 1; for ( $i = $start; $i <= $last; $i++ ) { $t = $this->tokens[ $i ]; if ( T_OPEN_PARENTHESIS === $t['code'] && isset( $t['parenthesis_closer'] ) && $t['parenthesis_closer'] > $i ) { $i = $t['parenthesis_closer']; } elseif ( isset( $t['bracket_closer'] ) && $t['bracket_closer'] > $i ) { $i = $t['bracket_closer']; } elseif ( in_array( $t['code'], array( T_SEMICOLON, T_CLOSE_TAG, T_CLOSE_PARENTHESIS, T_CLOSE_SQUARE_BRACKET, T_CLOSE_SHORT_ARRAY, T_CLOSE_CURLY_BRACKET, T_COLON ), true ) ) { return $i - 1; } } return $last; } private function controls( $owner ) { return ! in_array( $this->tokens[ $owner ]['code'], array( T_ARRAY, T_LIST, T_ISSET, T_EMPTY, T_UNSET, T_EVAL, T_EXIT ), true ); } private function expression_body( array $t ) { return isset( $t['scope_condition'] ) && in_array( $this->tokens[ $t['scope_condition'] ]['code'], array( T_CLOSURE, T_MATCH, T_ANON_CLASS ), true ); } private function prev_sig( $from ) { for ( $i = $from; $i >= 0; $i-- ) { if ( ! isset( Tokens::EMPTY_TOKENS[ $this->tokens[ $i ]['code'] ] ) ) { return $i; } } return null; } private function significant( $from, $to ) { $p = array(); for ( $i = max( 0, $from ); $i <= $to; $i++ ) { if ( ! isset( Tokens::EMPTY_TOKENS[ $this->tokens[ $i ]['code'] ] ) ) { $p[] = $i; } } return array( 'p' => $p, 'i' => array_flip( $p ), ); } private function code( array $l, $k ) { return $this->tokens[ $l['p'][ $k ] ]['code']; } private function closer( array $l, $k ) { $ptr = $l['p'][ $k ]; $t = $this->tokens[ $ptr ]; if ( T_OPEN_PARENTHESIS === $t['code'] && isset( $t['parenthesis_closer'] ) ) { $end = $t['parenthesis_closer']; } elseif ( isset( $t['bracket_closer'] ) && $t['bracket_closer'] > $ptr ) { $end = $t['bracket_closer']; } else { return null; } return isset( $l['i'][ $end ] ) ? $l['i'][ $end ] : null; } /** Positions of the given operators outside any brackets within [$i, $j]. */ private function split( array $l, $i, $j, array $ops ) { $at = array(); for ( $k = $i; $k <= $j; $k++ ) { if ( isset( $ops[ $this->code( $l, $k ) ] ) ) { $at[] = $k; continue; } $end = $this->closer( $l, $k ); if ( null !== $end && $end <= $j ) { $k = $end; } } return $at; } private function ternary( array $l, array $at ) { return 2 === count( $at ) && T_INLINE_THEN === $this->code( $l, $at[0] ) && T_INLINE_ELSE === $this->code( $l, $at[1] ); } /** True when reaching position $k inside [$i, $j] needs a condition that is false on the target. */ private function blocked( array $l, $i, $j, $k, $level ) { for ( $count = count( $this->levels ); $level < $count; $level++ ) { list( $kind, $ops ) = $this->levels[ $level ]; $at = $this->split( $l, $i, $j, $ops ); if ( ! $at ) { continue; } if ( 'ternary' === $kind && $this->ternary( $l, $at ) ) { list( $q, $r ) = $at; if ( $k < $q ) { return $this->blocked( $l, $i, $q - 1, $k, $level + 1 ); } $cond = $this->value( $l, $i, $q - 1, $level + 1 ); if ( $k > $q && $k < $r ) { return false === $cond || $this->blocked( $l, $q + 1, $r - 1, $k, 0 ); } return $k > $r && ( true === $cond || $this->blocked( $l, $r + 1, $j, $k, $level + 1 ) ); } $bounds = array_merge( array( $i - 1 ), $at, array( $j + 1 ) ); for ( $p = 0, $parts = count( $bounds ) - 1; $p < $parts; $p++ ) { if ( $k <= $bounds[ $p ] || $k >= $bounds[ $p + 1 ] ) { continue; } for ( $q = 0; $q < $p && ( 'and' === $kind || 'or' === $kind ); $q++ ) { if ( ( 'and' === $kind ? false : true ) === $this->value( $l, $bounds[ $q ] + 1, $bounds[ $q + 1 ] - 1, $level + 1 ) ) { return true; } } return $this->blocked( $l, $bounds[ $p ] + 1, $bounds[ $p + 1 ] - 1, $k, $level + 1 ); } return false; } if ( $k > $i && in_array( $this->code( $l, $i ), array( T_BOOLEAN_NOT, T_ASPERAND ), true ) ) { return $this->blocked( $l, $i + 1, $j, $k, count( $this->levels ) ); } for ( $m = $i; $m <= $j; $m++ ) { $end = $this->closer( $l, $m ); if ( null === $end ) { continue; } if ( $m < $k && $k < $end ) { return T_OPEN_CURLY_BRACKET !== $this->code( $l, $m ) && $this->blocked( $l, $m + 1, $end - 1, $k, 0 ); } $m = $end; } return false; } /** The value of [$i, $j] on every release of the target PHP: true, false, or null when it depends on more than the PHP version. */ private function value( array $l, $i, $j, $level ) { if ( $i > $j ) { return null; } for ( $count = count( $this->levels ); $level < $count; $level++ ) { list( $kind, $ops ) = $this->levels[ $level ]; $at = $this->split( $l, $i, $j, $ops ); if ( ! $at ) { continue; } if ( 'ternary' === $kind && $this->ternary( $l, $at ) ) { $cond = $this->value( $l, $i, $at[0] - 1, $level + 1 ); $then = $at[1] === $at[0] + 1 ? $cond : $this->value( $l, $at[0] + 1, $at[1] - 1, 0 ); $else = $this->value( $l, $at[1] + 1, $j, $level + 1 ); if ( null !== $cond ) { return $cond ? $then : $else; } return $then === $else ? $then : null; } if ( 'and' !== $kind && 'or' !== $kind ) { return null; } $known = true; $bounds = array_merge( array( $i - 1 ), $at, array( $j + 1 ) ); for ( $p = 0, $parts = count( $bounds ) - 1; $p < $parts; $p++ ) { $v = $this->value( $l, $bounds[ $p ] + 1, $bounds[ $p + 1 ] - 1, $level + 1 ); if ( ( 'or' === $kind ) === $v ) { return $v; } $known = $known && null !== $v; } return $known ? 'and' === $kind : null; } return $this->atom( $l, $i, $j ); } private function atom( array $l, $i, $j ) { $code = $this->code( $l, $i ); if ( T_BOOLEAN_NOT === $code ) { $v = $i < $j ? $this->atom( $l, $i + 1, $j ) : null; return null === $v ? null : ! $v; } if ( $this->closer( $l, $i ) === $j && T_OPEN_PARENTHESIS === $code ) { return $this->value( $l, $i + 1, $j - 1, 0 ); } if ( $i === $j && ( T_TRUE === $code || T_FALSE === $code ) ) { return T_TRUE === $code; } $cmp = $this->split( $l, $i, $j, array_fill_keys( array( T_IS_EQUAL, T_IS_NOT_EQUAL, T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_LESS_THAN, T_GREATER_THAN, T_IS_SMALLER_OR_EQUAL, T_IS_GREATER_OR_EQUAL ), true ) ); if ( 1 === count( $cmp ) ) { return $this->comparison( $l, $i, $cmp[0], $j ); } $args = $this->call( $l, $i, $j ); if ( null === $args ) { return null; } $name = $this->name( $l, $i ); $missing = array( 'function_exists' => $this->removed, 'is_callable' => $this->removed, 'defined' => $this->constant, ); if ( isset( $missing[ $name ] ) && 1 === count( $args ) ) { $arg = $this->literal( $l, $args[0] ); return '' !== $missing[ $name ] && null !== $arg && strtolower( ltrim( $arg, '\\' ) ) === $missing[ $name ] ? false : null; } if ( 'version_compare' === $name && 3 === count( $args ) ) { $op = $this->literal( $l, $args[2] ); if ( ! in_array( $op, array( '<', 'lt', '<=', 'le', '>', 'gt', '>=', 'ge', '==', 'eq', '!=', '<>', 'ne' ), true ) ) { return null; } $sides = $this->versions( $l, $args[0], $args[1] ); if ( null === $sides ) { return null; } $a = version_compare( $sides[0][0], $sides[1][0], $op ); $b = version_compare( $sides[0][1], $sides[1][1], $op ); return $a === $b ? $a : null; } return null; } /** PHP_VERSION or phpversion() against a version string, as [[left at .0, left at .99], [right at .0, right at .99]]. */ private function versions( array $l, array $a, array $b ) { $lit = $this->literal( $l, $b ); if ( $this->php_version( $l, $a ) && null !== $lit ) { return array( array( $this->lo, $this->hi ), array( $lit, $lit ) ); } $lit = $this->literal( $l, $a ); if ( $this->php_version( $l, $b ) && null !== $lit ) { return array( array( $lit, $lit ), array( $this->lo, $this->hi ) ); } return null; } private function comparison( array $l, $i, $c, $j ) { $op = $this->code( $l, $c ); $range = $this->range( $l, $i, $c - 1 ); $num = $this->number( $l, $c + 1, $j ); if ( null === $range || null === $num ) { $range = $this->range( $l, $c + 1, $j ); $num = $this->number( $l, $i, $c - 1 ); $flip = array( T_LESS_THAN => T_GREATER_THAN, T_GREATER_THAN => T_LESS_THAN, T_IS_SMALLER_OR_EQUAL => T_IS_GREATER_OR_EQUAL, T_IS_GREATER_OR_EQUAL => T_IS_SMALLER_OR_EQUAL, ); $op = isset( $flip[ $op ] ) ? $flip[ $op ] : $op; } if ( null === $range || null === $num ) { return null; } list( $lo, $hi ) = $range; // [always true, always false] for each ordering operator over [lo, hi]. $order = array( T_LESS_THAN => array( $hi < $num, $lo >= $num ), T_IS_SMALLER_OR_EQUAL => array( $hi <= $num, $lo > $num ), T_GREATER_THAN => array( $lo > $num, $hi <= $num ), T_IS_GREATER_OR_EQUAL => array( $lo >= $num, $hi < $num ), ); if ( isset( $order[ $op ] ) ) { return $order[ $op ][0] ? true : ( $order[ $op ][1] ? false : null ); } $equal = $num < $lo || $num > $hi ? false : ( $lo === $hi ? true : null ); return null === $equal || in_array( $op, array( T_IS_EQUAL, T_IS_IDENTICAL ), true ) ? $equal : ! $equal; } /** PHP_VERSION_ID, PHP_MAJOR_VERSION, PHP_MINOR_VERSION or version_compare( PHP_VERSION, 'x' ) as [min, max] over the target's releases. */ private function range( array $l, $i, $j ) { if ( $i === $j ) { $ranges = array( 'PHP_VERSION_ID' => $this->id, 'PHP_MAJOR_VERSION' => array( $this->major, $this->major ), 'PHP_MINOR_VERSION' => array( $this->minor, $this->minor ), ); $name = ltrim( $this->tokens[ $l['p'][ $i ] ]['content'], '\\' ); return isset( Tokens::NAME_TOKENS[ $this->code( $l, $i ) ], $ranges[ $name ] ) ? $ranges[ $name ] : null; } $args = $this->call( $l, $i, $j ); if ( null === $args || 'version_compare' !== $this->name( $l, $i ) || 2 !== count( $args ) ) { return null; } $sides = $this->versions( $l, $args[0], $args[1] ); if ( null === $sides ) { return null; } $a = version_compare( $sides[0][0], $sides[1][0] ); $b = version_compare( $sides[0][1], $sides[1][1] ); return array( min( $a, $b ), max( $a, $b ) ); } private function number( array $l, $i, $j ) { $minus = $j === $i + 1 && T_MINUS === $this->code( $l, $i ); $k = $minus ? $j : $i; if ( $k !== $j || T_LNUMBER !== $this->code( $l, $k ) || ! preg_match( '/^[1-9][\d_]*$|^0$/', $this->tokens[ $l['p'][ $k ] ]['content'] ) ) { return null; } $n = (int) str_replace( '_', '', $this->tokens[ $l['p'][ $k ] ]['content'] ); return $minus ? -$n : $n; } private function php_version( array $l, array $arg ) { list( $i, $j ) = $arg; if ( $i === $j ) { return isset( Tokens::NAME_TOKENS[ $this->code( $l, $i ) ] ) && 'PHP_VERSION' === ltrim( $this->tokens[ $l['p'][ $i ] ]['content'], '\\' ); } return array() === $this->call( $l, $i, $j ) && 'phpversion' === $this->name( $l, $i ); } /** Arguments of a plain function call spanning [$i, $j] as [start, end] pairs, or null when it is not one. */ private function call( array $l, $i, $j ) { if ( $j < $i + 2 || ! isset( Tokens::NAME_TOKENS[ $this->code( $l, $i ) ] ) || $this->closer( $l, $i + 1 ) !== $j || T_OPEN_PARENTHESIS !== $this->code( $l, $i + 1 ) ) { return null; } if ( $j === $i + 2 ) { return array(); } $bounds = array_merge( array( $i + 1 ), $this->split( $l, $i + 2, $j - 1, array( T_COMMA => true ) ), array( $j ) ); $args = array(); for ( $p = 0, $parts = count( $bounds ) - 1; $p < $parts; $p++ ) { $args[] = array( $bounds[ $p ] + 1, $bounds[ $p + 1 ] - 1 ); } return $args; } private function name( array $l, $i ) { return strtolower( ltrim( $this->tokens[ $l['p'][ $i ] ]['content'], '\\' ) ); } private function literal( array $l, array $arg ) { list( $i, $j ) = $arg; if ( $i !== $j || T_CONSTANT_ENCAPSED_STRING !== $this->code( $l, $i ) ) { return null; } return substr( $this->tokens[ $l['p'][ $i ] ]['content'], 1, -1 ); } }