Completed
Push — add/cs-package ( 64f35d...56eded )
by
unknown
21:11 queued 13:50
created

HooksInlineDocsSniff::verify_valid_match()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
/**
3
 * This sniff verifies a WordPress hook function has a preceding docblock.
4
 *
5
 * @package   automattic/jetpack-coding-standards
6
 */
7
8
// I admit this is weird. This allows the sniff to be named "Jetpack.Commenting.HooksInlineDocs".
9
namespace Automattic\Jetpack\CodingStandards\Jetpack\Sniffs\Commenting;
10
11
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
12
use PHP_CodeSniffer\Util\Tokens;
13
14
/**
15
 * Class HooksInlineDocsSniff
16
 *
17
 * @package WPCS\WordPressCodingStandards
18
 */
19
class HooksInlineDocsSniff extends AbstractFunctionRestrictionsSniff {
20
21
	/**
22
	 * Array of WordPress hook execution functions.
23
	 *
24
	 * @var array WordPress hook execution function name => filter or action.
25
	 */
26
	protected $hook_functions = array(
27
		'apply_filters'            => 'filter',
28
		'apply_filters_ref_array'  => 'filter',
29
		'apply_filters_deprecated' => 'filter',
30
		'do_action'                => 'action',
31
		'do_action_ref_array'      => 'action',
32
		'do_action_deprecated'     => 'action',
33
	);
34
35
	/**
36
	 * Array of allowed exceptional version numbers.
37
	 *
38
	 * By default, X.Y.Z version numbers are required. If there are any exceptions,
39
	 * they can be passed in the ruleset XML file via:
40
	 * <rule ref="Jetpack.Commenting.HooksInlineDocs">
41
	 *  <properties>
42
	 *   <property name="allowed_extra_versions" type="array">
43
	 *    <element key="0.71" value="0.71"/>
44
	 *    <element key="MU (3.0.0)" value="MU (3.0.0)"/>
45
	 *   </property>
46
	 * </rule>
47
	 *
48
	 * The key values are used to determine if a version is allowed outside of the X.Y.Z scheme. The value is not considered.
49
	 */
50
	public $allowed_extra_versions = array();
51
52
	/**
53
	 * Groups of functions to restrict.
54
	 *
55
	 * Example: groups => array(
56
	 *  'lambda' => array(
57
	 *      'type'      => 'error' | 'warning',
58
	 *      'message'   => 'Use anonymous functions instead please!',
59
	 *      'functions' => array( 'file_get_contents', 'create_function' ),
60
	 *  )
61
	 * )
62
	 *
63
	 * @return array
64
	 */
65
	public function getGroups() {
66
		return array(
67
			'hooks' => array(
68
				'functions' => array_keys( $this->hook_functions ),
69
			),
70
		);
71
	}
72
73
	/**
74
	 * Process a matched token.
75
	 *
76
	 * @since 1.0.0 Logic split off from the `process_token()` method.
77
	 *
78
	 * @param int    $stack_ptr       The position of the current token in the stack.
79
	 * @param string $group_name      The name of the group which was matched.
80
	 * @param string $matched_content The token content (function name) which was matched.
81
	 *
82
	 * @return int|void Integer stack pointer to skip forward or void to continue
83
	 *                  normal file processing.
84
	 */
85
	public function process_matched_token( $stack_ptr, $group_name, $matched_content ) {
86
87
		if ( ! $this->verify_valid_match( $stack_ptr ) ) {
88
			return;
89
		}
90
91
		$previous_comment = $this->return_previous_comment( $stack_ptr );
92
93
		if ( false !== $previous_comment ) {
94
			/*
95
			 * Check to determine if there is a comment immediately preceding the function call.
96
			 */
97
			if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) !== $this->tokens[ $stack_ptr ]['line'] ) {
98
				$this->phpcsFile->addError(
99
					'The inline documentation for a hook must be on the line immediately before the function call.',
100
					$stack_ptr,
101
					'DocMustBePreceding'
102
				);
103
			}
104
105
			/*
106
			 * Check that the comment starts is a docblock.
107
			 */
108
			if ( \T_DOC_COMMENT_CLOSE_TAG !== $this->tokens[ $previous_comment ]['code'] ) {
109
				$this->phpcsFile->addError(
110
					'Hooks must include a docblock with /** formatting */',
111
					$stack_ptr,
112
					'NoDocblockFound'
113
				);
114
			}
115
116
			/*
117
			 * Process docblock tags.
118
			 */
119
			$comment_end   = $previous_comment;
120
			$comment_start = $this->return_comment_start( $comment_end );
121
			$has           = array(
122
				'since' => false,
123
			);
124
125
			// The comment isn't a docblock or is documented elsewhere, so we're going to stop here.
126
			if ( ! $comment_start || $this->is_previously_documented( $comment_start, $comment_end ) ) {
127
				return;
128
			}
129
130
			foreach ( $this->tokens[ $comment_start ]['comment_tags'] as $tag ) {
131
				// Is the next tag of the docblock the "@since" tag?
132
				if ( '@since' === $this->tokens[ $tag ]['content'] ) {
133
					$has['since'] = true;
134
					// Find the next string, which will be the text after the @since.
135
					$string = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, $tag, $comment_end );
136
					// If it is false, there is no text or if the text is on the another line, error.
137
					if ( false === $string || $this->tokens[ $string ]['line'] !== $this->tokens[ $tag ]['line'] ) {
138
						$this->phpcsFile->addError( 'Since tag must have a value.', $tag, 'EmptySince' );
139
					} elseif ( ! preg_match('/^\d+\.\d+\.\d+/', $this->tokens[ $string ]['content'], $matches ) ) { // Requires X.Y.Z. Trailing 0 is needed for a major release.
140
						if ( empty( $this->allowed_extra_versions ) || ! $this->array_begins_with( $this->tokens[ $string ]['content'], $this->allowed_extra_versions ) ) {
141
							$this->phpcsFile->addError( 'Since tag must have a X.Y.Z version number.' . $this->tokens[ $string ]['content'], $tag, 'InvalidSince' );
142
						}
143
					}
144
				}
145
			}
146
147 View Code Duplication
			foreach ( $has as $name => $present ) {
148
				if ( ! $present ) {
149
					$this->phpcsFile->addError( 'Hook documentation is missing a tag: ' . $name, $comment_start, 'No' . ucfirst( $name ) );
150
				}
151
			}
152
		}
153
	}
154
155
	/**
156
	 * Helper function to identify the comment previous to a pointer reference.
157
	 *
158
	 * @param int $stack_ptr       The position of the token in the stack.
159
	 */
160
	protected function return_previous_comment( $stack_ptr ) {
161
		return $this->phpcsFile->findPrevious( Tokens::$commentTokens, ( $stack_ptr - 1 ) );
162
	}
163
164
	/**
165
	 * Returns the starting comment reference when passed an end reference.
166
	 *
167
	 * Used to help set bounds for searching through a docblock.
168
	 *
169
	 * @param int $end       The position of the ending token in the stack.
170
	 */
171
	protected function return_comment_start( $end ) {
172
		return ( isset( $this->tokens[ $end ]['comment_opener'] ) ) ? $this->tokens[ $end ]['comment_opener'] : false;
173
	}
174
175
	/**
176
	 * Determines if a filter docblock is referencing a complete docblock elsewhere.
177
	 *
178
	 * @param int $start       The position of the starting token in the stack.
179
	 * @param int $end       The position of the ending token in the stack.
180
	 *
181
	 * @return bool If docblock matches the previously documented convention.
182
	 */
183
	protected function is_previously_documented( $start, $end ) {
184
		$string = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, $start, $end );
185
186
		$content = $this->tokens[ $string ]['content'];
187
		// If the call is documented elsewhere, stop here.
188
		if ( 0 === strpos( $content, 'This filter is documented' ) ) {
189
			return true;
190
		}
191
192
		if ( 0 === strpos( $content, 'This action is documented' ) ) {
193
			return true;
194
		}
195
196
		return false;
197
	}
198
199
	/**
200
	 * Verifies the match is valid and worthy of continued processing.
201
	 *
202
	 * @param int $stack_ptr       The position of the token in the stack.
203
	 *
204
	 * @return bool True for valid.
205
	 */
206
	protected function verify_valid_match( $stack_ptr ) {
207
		$func_open_paren_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true );
208
		if ( false === $func_open_paren_token
209
		     || \T_OPEN_PARENTHESIS !== $this->tokens[ $func_open_paren_token ]['code']
210
		     || ! isset( $this->tokens[ $func_open_paren_token ]['parenthesis_closer'] )
211
		) {
212
			// Live coding, parse error or not a function call.
213
			return false;
214
		}
215
216
		return true;
217
	}
218
219
	protected function array_begins_with( $string, $array ){
220
		foreach ( $array as $value ) {
221
			if ( 0 === strpos( $string, $value ) ) {
222
				return true;
223
			}
224
		}
225
		return false;
226
	}
227
}
228