<?php
/**
 * PHPCompatibility, an external standard for PHP_CodeSniffer.
 *
 * @package   PHPCompatibility
 * @copyright 2012-2019 PHPCompatibility Contributors
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
 * @link      https://github.com/PHPCompatibility/PHPCompatibility
 */

namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;

use Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff as PHPCS_CamelCapsFunctionNameSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Standards_AbstractScopeSniff as PHPCS_AbstractScopeSniff;
use PHP_CodeSniffer_Tokens as Tokens;

/**
 * All function and method names starting with double underscore are reserved by PHP.
 *
 * PHP version All
 *
 * {@internal Extends an upstream sniff to benefit from the properties contained therein.
 *            The properties are lists of valid PHP magic function and method names, which
 *            should be ignored for the purposes of this sniff.
 *            As this sniff is not PHP version specific, we don't need access to the utility
 *            methods in the PHPCompatibility\Sniff, so extending the upstream sniff is fine.
 *            As the upstream sniff checks the same (and more, but we don't need the rest),
 *            the logic in this sniff is largely the same as used upstream.
 *            Extending the upstream sniff instead of including it via the ruleset, however,
 *            prevents hard to debug issues of errors not being reported from the upstream sniff
 *            if this library is used in combination with other rulesets.}
 *
 * @link https://www.php.net/manual/en/language.oop5.magic.php
 *
 * @since 8.2.0 This was previously, since 7.0.3, checked by the upstream sniff.
 * @since 9.3.2 The sniff will now ignore functions marked as `@deprecated` by design.
 */
class ReservedFunctionNamesSniff extends PHPCS_CamelCapsFunctionNameSniff {


	/**
	 * Overload the constructor to work round various PHPCS cross-version compatibility issues.
	 *
	 * @since 8.2.0
	 */
	public function __construct() {
		$scopeTokens = array( \T_CLASS, \T_INTERFACE, \T_TRAIT );
		if ( \defined( 'T_ANON_CLASS' ) ) {
			$scopeTokens[] = \T_ANON_CLASS;
		}

		// Call the grand-parent constructor directly.
		PHPCS_AbstractScopeSniff::__construct( $scopeTokens, array( \T_FUNCTION ), true );

		// Make sure debuginfo is included in the array. Upstream only includes it since 2.5.1.
		$this->magicMethods['debuginfo'] = true;
	}


	/**
	 * Processes the tokens within the scope.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
	 * @param int                   $stackPtr  The position where this token was
	 *                                         found.
	 * @param int                   $currScope The position of the current scope.
	 *
	 * @return void
	 */
	protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) {
		$tokens = $phpcsFile->getTokens();

		/*
		 * Determine if this is a function which needs to be examined.
		 * The `processTokenWithinScope()` is called for each valid scope a method is in,
		 * so for nested classes, we need to make sure we only examine the token for
		 * the lowest level valid scope.
		 */
		$conditions = $tokens[ $stackPtr ]['conditions'];
		end( $conditions );
		$deepestScope = key( $conditions );
		if ( $deepestScope !== $currScope ) {
			return;
		}

		if ( $this->isFunctionDeprecated( $phpcsFile, $stackPtr ) === true ) {
			/*
			 * Deprecated functions don't have to comply with the naming conventions,
			 * otherwise functions deprecated in favour of a function with a compliant
			 * name would still trigger an error.
			 */
			return;
		}

		$methodName = $phpcsFile->getDeclarationName( $stackPtr );
		if ( $methodName === null ) {
			// Ignore closures.
			return;
		}

		// Is this a magic method. i.e., is prefixed with "__" ?
		if ( preg_match( '|^__[^_]|', $methodName ) > 0 ) {
			$magicPart = strtolower( substr( $methodName, 2 ) );
			if ( isset( $this->magicMethods[ $magicPart ] ) === false
				&& isset( $this->methodsDoubleUnderscore[ $magicPart ] ) === false
			) {
				$className         = '[anonymous class]';
				$scopeNextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $currScope + 1 ), null, true );
				if ( $scopeNextNonEmpty !== false && $tokens[ $scopeNextNonEmpty ]['code'] === \T_STRING ) {
					$className = $tokens[ $scopeNextNonEmpty ]['content'];
				}

				$phpcsFile->addWarning(
					'Method name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
					$stackPtr,
					'MethodDoubleUnderscore',
					array( $className . '::' . $methodName )
				);
			}
		}
	}


	/**
	 * Processes the tokens outside the scope.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
	 * @param int                   $stackPtr  The position where this token was
	 *                                         found.
	 *
	 * @return void
	 */
	protected function processTokenOutsideScope( File $phpcsFile, $stackPtr ) {
		if ( $this->isFunctionDeprecated( $phpcsFile, $stackPtr ) === true ) {
			/*
			 * Deprecated functions don't have to comply with the naming conventions,
			 * otherwise functions deprecated in favour of a function with a compliant
			 * name would still trigger an error.
			 */
			return;
		}

		$functionName = $phpcsFile->getDeclarationName( $stackPtr );
		if ( $functionName === null ) {
			// Ignore closures.
			return;
		}

		// Is this a magic function. i.e., it is prefixed with "__".
		if ( preg_match( '|^__[^_]|', $functionName ) > 0 ) {
			$magicPart = strtolower( substr( $functionName, 2 ) );
			if ( isset( $this->magicFunctions[ $magicPart ] ) === false ) {
				$phpcsFile->addWarning(
					'Function name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
					$stackPtr,
					'FunctionDoubleUnderscore',
					array( $functionName )
				);
			}
		}
	}


	/**
	 * Check whether a function has been marked as deprecated via a @deprecated tag
	 * in the function docblock.
	 *
	 * @since 9.3.2
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
	 * @param int                         $stackPtr  The position of a T_FUNCTION
	 *                                               token in the stack.
	 *
	 * @return bool
	 */
	private function isFunctionDeprecated( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();
		$find   = Tokens::$methodPrefixes;
		$find[] = \T_WHITESPACE;

		$commentEnd = $phpcsFile->findPrevious( $find, ( $stackPtr - 1 ), null, true );
		if ( $tokens[ $commentEnd ]['code'] !== \T_DOC_COMMENT_CLOSE_TAG ) {
			// Function doesn't have a doc comment or is using the wrong type of comment.
			return false;
		}

		$commentStart = $tokens[ $commentEnd ]['comment_opener'];
		foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
			if ( $tokens[ $tag ]['content'] === '@deprecated' ) {
				return true;
			}
		}

		return false;
	}
}
