@@ -1428,9 +1428,11 @@ protected function has_nonce_check( $stackPtr ) {
14281428 }
14291429
14301430 /**
1431- * Check if a token is inside of an isset() or empty () statement.
1431+ * Check if a token is inside of an isset(), empty() or array_key_exists () statement.
14321432 *
14331433 * @since 0.5.0
1434+ * @since 2.0.1 Now checks for the token being used as the array parameter
1435+ * in function calls to array_key_exists() as well.
14341436 *
14351437 * @param int $stackPtr The index of the token in the stack.
14361438 *
@@ -1448,7 +1450,42 @@ protected function is_in_isset_or_empty( $stackPtr ) {
14481450 $ open_parenthesis = key ( $ nested_parenthesis );
14491451
14501452 $ previous_non_empty = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ open_parenthesis - 1 ), null , true , null , true );
1451- return in_array ( $ this ->tokens [ $ previous_non_empty ]['code ' ], array ( \T_ISSET , \T_EMPTY ), true );
1453+ if ( false === $ previous_non_empty ) {
1454+ return false ;
1455+ }
1456+
1457+ $ previous_code = $ this ->tokens [ $ previous_non_empty ]['code ' ];
1458+ if ( \T_ISSET === $ previous_code || \T_EMPTY === $ previous_code ) {
1459+ return true ;
1460+ }
1461+
1462+ if ( \T_STRING === $ previous_code && 'array_key_exists ' === $ this ->tokens [ $ previous_non_empty ]['content ' ] ) {
1463+ $ before_function = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ previous_non_empty - 1 ), null , true , null , true );
1464+
1465+ if ( false !== $ before_function ) {
1466+ if ( \T_OBJECT_OPERATOR === $ this ->tokens [ $ before_function ]['code ' ]
1467+ || \T_DOUBLE_COLON === $ this ->tokens [ $ before_function ]['code ' ]
1468+ ) {
1469+ // Method call.
1470+ return false ;
1471+ }
1472+
1473+ if ( \T_NS_SEPARATOR === $ this ->tokens [ $ before_function ]['code ' ] ) {
1474+ $ before_before = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ before_function - 1 ), null , true , null , true );
1475+ if ( false !== $ before_before && \T_STRING === $ this ->tokens [ $ before_before ]['code ' ] ) {
1476+ // Namespaced function call.
1477+ return false ;
1478+ }
1479+ }
1480+ }
1481+
1482+ $ second_param = $ this ->get_function_call_parameter ( $ previous_non_empty , 2 );
1483+ if ( $ stackPtr >= $ second_param ['start ' ] && $ stackPtr <= $ second_param ['end ' ] ) {
1484+ return true ;
1485+ }
1486+ }
1487+
1488+ return false ;
14521489 }
14531490
14541491 /**
@@ -1666,7 +1703,7 @@ protected function get_array_access_key( $stackPtr ) {
16661703 }
16671704
16681705 /**
1669- * Check if the existence of a variable is validated with isset() or empty ().
1706+ * Check if the existence of a variable is validated with isset(), empty() or array_key_exists ().
16701707 *
16711708 * When $in_condition_only is false, (which is the default), this is considered
16721709 * valid:
@@ -1689,6 +1726,7 @@ protected function get_array_access_key( $stackPtr ) {
16891726 * ```
16901727 *
16911728 * @since 0.5.0
1729+ * @since 2.0.1 Now recognizes array_key_exists() as a validation function.
16921730 *
16931731 * @param int $stackPtr The index of this token in the stack.
16941732 * @param string $array_key An array key to check for ("bar" in $foo['bar']).
@@ -1754,36 +1792,94 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
17541792 }
17551793
17561794 $ bare_array_key = $ this ->strip_quotes ( $ array_key );
1795+ $ targets = array (
1796+ \T_ISSET => 'construct ' ,
1797+ \T_EMPTY => 'construct ' ,
1798+ \T_UNSET => 'construct ' ,
1799+ \T_STRING => 'function_call ' ,
1800+ );
17571801
17581802 // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below.
17591803 for ( $ i = ( $ scope_start + 1 ); $ i < $ scope_end ; $ i ++ ) {
17601804
1761- if ( ! \in_array ( $ this ->tokens [ $ i ]['code ' ], array ( \ T_ISSET , \ T_EMPTY , \ T_UNSET ), true ) ) {
1805+ if ( isset ( $ targets [ $ this ->tokens [ $ i ]['code ' ] ] ) === false ) {
17621806 continue ;
17631807 }
17641808
1765- $ issetOpener = $ this ->phpcsFile ->findNext ( \T_OPEN_PARENTHESIS , $ i );
1766- $ issetCloser = $ this ->tokens [ $ issetOpener ]['parenthesis_closer ' ];
1809+ switch ( $ targets [ $ this ->tokens [ $ i ]['code ' ] ] ) {
1810+ case 'construct ' :
1811+ $ issetOpener = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ i + 1 ), null , true , null , true );
1812+ if ( false === $ issetOpener || \T_OPEN_PARENTHESIS !== $ this ->tokens [ $ issetOpener ]['code ' ] ) {
1813+ // Parse error or live coding.
1814+ continue 2 ;
1815+ }
17671816
1768- // Look for this variable. We purposely stomp $i from the parent loop.
1769- for ( $ i = ( $ issetOpener + 1 ); $ i < $ issetCloser ; $ i ++ ) {
1817+ $ issetCloser = $ this ->tokens [ $ issetOpener ]['parenthesis_closer ' ];
17701818
1771- if ( \T_VARIABLE !== $ this ->tokens [ $ i ]['code ' ] ) {
1772- continue ;
1773- }
1819+ // Look for this variable. We purposely stomp $i from the parent loop.
1820+ for ( $ i = ( $ issetOpener + 1 ); $ i < $ issetCloser ; $ i ++ ) {
17741821
1775- if ( $ this -> tokens [ $ stackPtr ][ ' content ' ] !== $ this ->tokens [ $ i ]['content ' ] ) {
1776- continue ;
1777- }
1822+ if ( \ T_VARIABLE !== $ this ->tokens [ $ i ]['code ' ] ) {
1823+ continue ;
1824+ }
17781825
1779- // If we're checking for a specific array key (ex: 'hello' in
1780- // $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1781- if ( isset ( $ array_key )
1782- && $ this ->strip_quotes ( $ this ->get_array_access_key ( $ i ) ) !== $ bare_array_key ) {
1783- continue ;
1784- }
1826+ if ( $ this ->tokens [ $ stackPtr ]['content ' ] !== $ this ->tokens [ $ i ]['content ' ] ) {
1827+ continue ;
1828+ }
17851829
1786- return true ;
1830+ // If we're checking for a specific array key (ex: 'hello' in
1831+ // $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1832+ if ( isset ( $ array_key )
1833+ && $ this ->strip_quotes ( $ this ->get_array_access_key ( $ i ) ) !== $ bare_array_key ) {
1834+ continue ;
1835+ }
1836+
1837+ return true ;
1838+ }
1839+
1840+ break ;
1841+
1842+ case 'function_call ' :
1843+ // Only check calls to array_key_exists().
1844+ if ( 'array_key_exists ' !== $ this ->tokens [ $ i ]['content ' ] ) {
1845+ continue 2 ;
1846+ }
1847+
1848+ $ next_non_empty = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ i + 1 ), null , true , null , true );
1849+ if ( false === $ next_non_empty || \T_OPEN_PARENTHESIS !== $ this ->tokens [ $ next_non_empty ]['code ' ] ) {
1850+ // Not a function call.
1851+ continue 2 ;
1852+ }
1853+
1854+ $ previous_non_empty = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ i - 1 ), null , true , null , true );
1855+ if ( false !== $ previous_non_empty ) {
1856+ if ( \T_OBJECT_OPERATOR === $ this ->tokens [ $ previous_non_empty ]['code ' ]
1857+ || \T_DOUBLE_COLON === $ this ->tokens [ $ previous_non_empty ]['code ' ]
1858+ ) {
1859+ // Method call.
1860+ continue 2 ;
1861+ }
1862+
1863+ if ( \T_NS_SEPARATOR === $ this ->tokens [ $ previous_non_empty ]['code ' ] ) {
1864+ $ pprev = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ previous_non_empty - 1 ), null , true , null , true );
1865+ if ( false !== $ pprev && \T_STRING === $ this ->tokens [ $ pprev ]['code ' ] ) {
1866+ // Namespaced function call.
1867+ continue 2 ;
1868+ }
1869+ }
1870+ }
1871+
1872+ $ params = $ this ->get_function_call_parameters ( $ i );
1873+ if ( $ params [2 ]['raw ' ] !== $ this ->tokens [ $ stackPtr ]['content ' ] ) {
1874+ continue 2 ;
1875+ }
1876+
1877+ if ( isset ( $ array_key )
1878+ && $ this ->strip_quotes ( $ params [1 ]['raw ' ] ) !== $ bare_array_key ) {
1879+ continue 2 ;
1880+ }
1881+
1882+ return true ;
17871883 }
17881884 }
17891885
0 commit comments