diff --git a/phpcs-sniffs/Formidable/Sniffs/Commenting/AlignFilterParamTypesWithFunctionSniff.php b/phpcs-sniffs/Formidable/Sniffs/Commenting/AlignFilterParamTypesWithFunctionSniff.php
new file mode 100644
index 0000000000..38e426cd3f
--- /dev/null
+++ b/phpcs-sniffs/Formidable/Sniffs/Commenting/AlignFilterParamTypesWithFunctionSniff.php
@@ -0,0 +1,405 @@
+getTokens();
+
+ if ( ! isset( $tokens[ $stackPtr ]['scope_opener'], $tokens[ $stackPtr ]['scope_closer'] ) ) {
+ return;
+ }
+
+ $functionDocblock = $this->findFunctionDocblock( $phpcsFile, $stackPtr );
+
+ if ( false === $functionDocblock ) {
+ return;
+ }
+
+ $functionParams = $this->getDocblockParamTags( $phpcsFile, $functionDocblock );
+
+ if ( empty( $functionParams ) ) {
+ return;
+ }
+
+ $scopeStart = $tokens[ $stackPtr ]['scope_opener'];
+ $scopeEnd = $tokens[ $stackPtr ]['scope_closer'];
+
+ for ( $i = $scopeStart + 1; $i < $scopeEnd; $i++ ) {
+ if ( $tokens[ $i ]['code'] !== T_DOC_COMMENT_OPEN_TAG ) {
+ continue;
+ }
+
+ if ( ! isset( $tokens[ $i ]['comment_closer'] ) ) {
+ continue;
+ }
+
+ $docblockClose = $tokens[ $i ]['comment_closer'];
+ $nextToken = $phpcsFile->findNext( T_WHITESPACE, $docblockClose + 1, $scopeEnd, true );
+
+ if ( false === $nextToken || $tokens[ $nextToken ]['code'] !== T_RETURN ) {
+ $i = $docblockClose;
+ continue;
+ }
+
+ $filterCall = $this->findFilterCallAfterReturn( $phpcsFile, $nextToken, $scopeEnd );
+
+ if ( false === $filterCall ) {
+ $i = $docblockClose;
+ continue;
+ }
+
+ $inlineParams = $this->getDocblockParamTags( $phpcsFile, $i );
+
+ if ( empty( $inlineParams ) ) {
+ $i = $docblockClose;
+ continue;
+ }
+
+ foreach ( $inlineParams as $paramName => $paramInfo ) {
+ if ( ! isset( $functionParams[ $paramName ] ) ) {
+ continue;
+ }
+
+ if ( empty( $functionParams[ $paramName ]['normalized'] ) || empty( $paramInfo['normalized'] ) ) {
+ continue;
+ }
+
+ if ( $this->variableModifiedBeforeDocblock( $phpcsFile, $paramName, $scopeStart, $i ) ) {
+ continue;
+ }
+
+ if ( $this->typesMatch( $functionParams[ $paramName ]['normalized'], $paramInfo['normalized'] ) ) {
+ continue;
+ }
+
+ $fix = $phpcsFile->addFixableError(
+ 'Filter docblock type for %s should match function @param type "%s" since the variable is unmodified before the filter.',
+ $paramInfo['token'],
+ 'FilterParamTypeMismatch',
+ array(
+ $paramName,
+ $functionParams[ $paramName ]['type'],
+ )
+ );
+
+ if ( true === $fix ) {
+ $this->replaceParamType( $phpcsFile, $paramInfo['token'], $functionParams[ $paramName ]['type'] );
+ }
+ }
+
+ $i = $docblockClose;
+ }
+ }
+
+ /**
+ * Find the docblock that documents the function itself.
+ *
+ * @param File $phpcsFile File reference.
+ * @param int $stackPtr Function pointer.
+ *
+ * @return false|int
+ */
+ private function findFunctionDocblock( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+ $ignore = array(
+ T_WHITESPACE,
+ T_STATIC,
+ T_PUBLIC,
+ T_PRIVATE,
+ T_PROTECTED,
+ T_ABSTRACT,
+ T_FINAL,
+ T_ATTRIBUTE,
+ );
+
+ $prev = $phpcsFile->findPrevious( $ignore, $stackPtr - 1, null, true );
+
+ if ( false !== $prev && $tokens[ $prev ]['code'] === T_DOC_COMMENT_CLOSE_TAG ) {
+ return $tokens[ $prev ]['comment_opener'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse @param tags within a docblock.
+ *
+ * @param File $phpcsFile File reference.
+ * @param int $docblockPt Docblock opener pointer.
+ *
+ * @return array
+ */
+ private function getDocblockParamTags( File $phpcsFile, $docblockPt ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( ! isset( $tokens[ $docblockPt ]['comment_closer'] ) ) {
+ return array();
+ }
+
+ $closer = $tokens[ $docblockPt ]['comment_closer'];
+ $params = array();
+
+ for ( $i = $docblockPt; $i <= $closer; $i++ ) {
+ if ( $tokens[ $i ]['code'] !== T_DOC_COMMENT_TAG || '@param' !== $tokens[ $i ]['content'] ) {
+ continue;
+ }
+
+ $stringPtr = $phpcsFile->findNext( T_DOC_COMMENT_STRING, $i + 1, $closer );
+
+ if ( false === $stringPtr ) {
+ continue;
+ }
+
+ $content = trim( $tokens[ $stringPtr ]['content'] );
+
+ if ( ! preg_match( '/^([^\s]+)\s+(\$\w+)/', $content, $matches ) ) {
+ continue;
+ }
+
+ $type = trim( $matches[1] );
+ $paramName = $matches[2];
+
+ $params[ $paramName ] = array(
+ 'type' => $type,
+ 'normalized' => $this->normalizeTypeList( $type ),
+ 'token' => $stringPtr,
+ );
+ }
+
+ return $params;
+ }
+
+ /**
+ * Find the filter call after a return statement.
+ *
+ * @param File $phpcsFile File reference.
+ * @param int $returnPtr Return token pointer.
+ * @param int $scopeEnd Function scope end.
+ *
+ * @return false|int
+ */
+ private function findFilterCallAfterReturn( File $phpcsFile, $returnPtr, $scopeEnd ) {
+ $tokens = $phpcsFile->getTokens();
+
+ for ( $i = $returnPtr + 1; $i < $scopeEnd; $i++ ) {
+ $code = $tokens[ $i ]['code'];
+
+ if ( $code === T_SEMICOLON ) {
+ break;
+ }
+
+ if ( $code === T_STRING && in_array( $tokens[ $i ]['content'], $this->filterFunctions, true ) ) {
+ return $i;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if a variable is modified before a certain docblock.
+ *
+ * @param File $phpcsFile File reference.
+ * @param string $paramName Parameter name (with $).
+ * @param int $scopeStart Function scope start brace.
+ * @param int $docblockPt Docblock pointer.
+ *
+ * @return bool
+ */
+ private function variableModifiedBeforeDocblock( File $phpcsFile, $paramName, $scopeStart, $docblockPt ) {
+ $tokens = $phpcsFile->getTokens();
+
+ for ( $i = $scopeStart + 1; $i < $docblockPt; $i++ ) {
+ if ( $tokens[ $i ]['code'] !== T_VARIABLE || $tokens[ $i ]['content'] !== $paramName ) {
+ continue;
+ }
+
+ $prev = $phpcsFile->findPrevious( T_WHITESPACE, $i - 1, null, true );
+
+ if ( false !== $prev && in_array( $tokens[ $prev ]['code'], array( T_INC, T_DEC ), true ) ) {
+ return true;
+ }
+
+ $next = $phpcsFile->findNext( T_WHITESPACE, $i + 1, null, true );
+
+ if ( false === $next ) {
+ continue;
+ }
+
+ if ( in_array( $tokens[ $next ]['code'], $this->assignmentTokens, true ) ) {
+ return true;
+ }
+
+ if ( in_array( $tokens[ $next ]['code'], array( T_INC, T_DEC ), true ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Normalize a set of type strings.
+ *
+ * @param string $typeString Type string from a docblock.
+ *
+ * @return array
+ */
+ private function normalizeTypeList( $typeString ) {
+ $typeString = trim( $typeString );
+
+ if ( '' === $typeString ) {
+ return array();
+ }
+
+ if ( strpos( $typeString, '?' ) === 0 ) {
+ $typeString = 'null|' . substr( $typeString, 1 );
+ }
+
+ $parts = explode( '|', $typeString );
+ $normalized = array();
+
+ foreach ( $parts as $part ) {
+ $single = $this->normalizeType( $part );
+
+ if ( '' !== $single ) {
+ $normalized[] = $single;
+ }
+ }
+
+ $normalized = array_values( array_unique( $normalized ) );
+ sort( $normalized );
+
+ return $normalized;
+ }
+
+ /**
+ * Normalize a single type entry for comparison.
+ *
+ * @param string $type Type fragment.
+ *
+ * @return string
+ */
+ private function normalizeType( $type ) {
+ $type = trim( $type );
+
+ if ( '' === $type ) {
+ return '';
+ }
+
+ if ( substr( $type, -2 ) === '[]' ) {
+ return 'array';
+ }
+
+ if ( preg_match( '/^array\s*[<{]/i', $type ) ) {
+ return 'array';
+ }
+
+ $lower = strtolower( ltrim( $type, '\\' ) );
+
+ $aliases = array(
+ 'integer' => 'int',
+ 'boolean' => 'bool',
+ 'double' => 'float',
+ 'real' => 'float',
+ 'number' => 'float',
+ 'true' => 'bool',
+ );
+
+ if ( isset( $aliases[ $lower ] ) ) {
+ return $aliases[ $lower ];
+ }
+
+ return $lower;
+ }
+
+ /**
+ * Compare two normalized type lists.
+ *
+ * @param array $functionTypes Function doc types.
+ * @param array $inlineTypes Inline doc types.
+ *
+ * @return bool
+ */
+ private function typesMatch( array $functionTypes, array $inlineTypes ) {
+ return $functionTypes === $inlineTypes;
+ }
+
+ /**
+ * Replace the @param type in an inline docblock.
+ *
+ * @param File $phpcsFile File reference.
+ * @param int $tokenPtr Doc comment string pointer.
+ * @param string $newType Replacement type string.
+ *
+ * @return void
+ */
+ private function replaceParamType( File $phpcsFile, $tokenPtr, $newType ) {
+ $tokens = $phpcsFile->getTokens();
+ $content = $tokens[ $tokenPtr ]['content'];
+ $newValue = preg_replace( '/^[^\s]+/', $newType, $content, 1 );
+
+ if ( null === $newValue ) {
+ return;
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->replaceToken( $tokenPtr, $newValue );
+ $phpcsFile->fixer->endChangeset();
+ }
+}
diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml
index bae2d5326e..9965585dd3 100644
--- a/phpcs-sniffs/Formidable/ruleset.xml
+++ b/phpcs-sniffs/Formidable/ruleset.xml
@@ -48,6 +48,7 @@
+