Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/DrevOps/Sniffs/NamingConventions/LocalVariableNamingSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
*/
final class LocalVariableNamingSniff extends AbstractVariableNamingSniff {

/**
* The comment types to fix when auto-fixing variable names.
*
* Comma-separated list of comment types:
* - 'doc': Fix variable references in doc comments (/** ... *\/)
* - 'inline': Fix variable references in inline comments (// and /* *\/)
*
* @var string
*/
public $fixCommentTypes = 'doc';

/**
* Error code for non-snake_case variables.
*/
Expand Down Expand Up @@ -81,9 +92,149 @@ public function process(File $phpcsFile, $stackPtr): void {
// and Fixer). Unit tests only check for error detection, not fixing.
if ($fix === TRUE) {
$phpcsFile->fixer->replaceToken($stackPtr, '$' . $suggestion);

// Fix variable references in preceding comments.
$comment_tokens = $this->findPrecedingComment($phpcsFile, $stackPtr);
$this->fixCommentVariable($phpcsFile, $comment_tokens, $var_name, $suggestion);
}
// @codeCoverageIgnoreEnd
}
}

/**
* Find preceding comment tokens for a variable.
*
* Searches backwards from the variable token, skipping whitespace, and
* collects all comment tokens until a non-whitespace/non-comment token
* is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* The file being scanned.
* @param int $stackPtr
* The position of the variable token.
*
* @return array<int>
* Array of comment token positions, or empty array if none found.
*/
private function findPrecedingComment(File $phpcsFile, int $stackPtr): array {
$tokens = $phpcsFile->getTokens();
$comment_tokens = [];

$comment_types = [
T_COMMENT,
T_DOC_COMMENT_OPEN_TAG,
T_DOC_COMMENT_CLOSE_TAG,
T_DOC_COMMENT_STAR,
T_DOC_COMMENT_WHITESPACE,
T_DOC_COMMENT_TAG,
T_DOC_COMMENT_STRING,
];

for ($i = $stackPtr - 1; $i >= 0; $i--) {
$code = $tokens[$i]['code'];

if ($code === T_WHITESPACE) {
continue;
}

// Skip trailing comments on previous statements (e.g.,
// `$other = 1; // comment`). A T_COMMENT whose preceding
// non-whitespace token is on the same line is trailing, not preceding.
if ($code === T_COMMENT) {
$prev_non_ws = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, NULL, TRUE);
if ($prev_non_ws !== FALSE && $tokens[$prev_non_ws]['line'] === $tokens[$i]['line']) {
break;
}
}

if (in_array($code, $comment_types, TRUE)) {
$comment_tokens[] = $i;
continue;
}

// Hit a non-whitespace, non-comment token — stop.
break;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return $comment_tokens;
}

/**
* Fix variable references in preceding comment tokens.
*
* Replaces occurrences of the old variable name with the new one in
* comment tokens, respecting the configured comment types. Skips comments
* that contain references to multiple distinct variables.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* The file being scanned.
* @param array<int> $commentTokens
* Array of comment token positions.
* @param string $oldName
* The old variable name (without $).
* @param string $newName
* The new variable name (without $).
*
* @codeCoverageIgnore
*/
private function fixCommentVariable(File $phpcsFile, array $commentTokens, string $oldName, string $newName): void {
if ($commentTokens === []) {
return;
}

$tokens = $phpcsFile->getTokens();
$fix_types = array_map('trim', explode(',', $this->fixCommentTypes));

// Collect text only from eligible comment token types to check for
// multiple variables. This prevents unrelated neighboring comments
// from blocking a valid fix.
$all_text = '';
foreach ($commentTokens as $ptr) {
$code = $tokens[$ptr]['code'];
$is_doc = ($code === T_DOC_COMMENT_STRING);
$is_inline = ($code === T_COMMENT);
if (($is_doc && in_array('doc', $fix_types, TRUE)) || ($is_inline && in_array('inline', $fix_types, TRUE))) {
$all_text .= $tokens[$ptr]['content'];
}
}

// Count distinct $variable references in the eligible comment text.
preg_match_all('/\$[a-zA-Z_]\w*/', $all_text, $matches);
$unique_vars = array_unique($matches[0]);
if (count($unique_vars) > 1) {
// Multiple variables in comment — skip (requires manual fix).
return;
}

$pattern = '/\$' . preg_quote($oldName, '/') . '(?![a-zA-Z0-9_])/';

foreach ($commentTokens as $ptr) {
$token = $tokens[$ptr];
$code = $token['code'];
$content = $token['content'];

// Check if this token type matches the configured fix types.
$is_doc = ($code === T_DOC_COMMENT_STRING);
$is_inline = ($code === T_COMMENT);

if ($is_doc && !in_array('doc', $fix_types, TRUE)) {
continue;
}

if ($is_inline && !in_array('inline', $fix_types, TRUE)) {
continue;
}

// Only process tokens that can contain variable references.
if (!$is_doc && !$is_inline) {
continue;
}

if (preg_match($pattern, $content) === 1) {
$new_content = (string) preg_replace($pattern, '$' . $newName, $content);
$phpcsFile->fixer->replaceToken($ptr, $new_content);
}
}
}

}
184 changes: 184 additions & 0 deletions tests/Fixtures/CommentVariableNaming.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

declare(strict_types=1);

namespace DrevOps\PhpcsStandard\Tests\Fixtures;

/**
* Fixture for testing variable references in comments during auto-fix.
*
* Tests various comment styles, positions, and edge cases to verify that
* phpcbf correctly updates (or skips) variable references in comments.
*
* @phpcs:disable Drupal.Commenting.FunctionComment
* @phpcs:disable Drupal.Commenting.DocComment
*/
class CommentVariableNaming {

// =========================================================================
// Variables with doc comments — violations expected.
// =========================================================================

/**
* Single-line doc comment with simple type.
*/
public function singleLineDocComment(): void {
/** @var \SomeClass $moduleHandler */
$moduleHandler = get_service('module_handler');
}

/**
* Single-line doc comment with FQN type (leading backslash).
*/
public function fqnTypeLeadingBackslash(): void {
/** @var \Drupal\Core\Extension\ModuleHandler $moduleHandler */
$moduleHandler = get_service('module_handler');
}

/**
* Single-line doc comment with FQN type (no leading backslash).
*/
public function fqnTypeNoLeadingBackslash(): void {
/** @var Drupal\Core\Extension\ModuleHandler $moduleHandler */
$moduleHandler = get_service('module_handler');
}

/**
* Single-line doc comment with very long FQN type.
*/
public function longFqnType(): void {
/** @var \Very\Long\Namespace\Path\To\SomeClass $moduleHandler */
$moduleHandler = get_service('module_handler');
}

/**
* Multi-line doc comment above variable.
*/
public function multiLineDocComment(): void {
/**
* @var \Drupal\Core\Extension\ModuleHandler $moduleHandler
* The module handler service.
*/
$moduleHandler = get_service('module_handler');
}

/**
* Doc comment with a blank line between comment and variable.
*/
public function docCommentWithBlankLine(): void {
/** @var \SomeClass $moduleHandler */

$moduleHandler = get_service('module_handler');
}

// =========================================================================
// Variables with inline comments — violations expected.
// =========================================================================

/**
* Inline comment with variable reference.
*/
public function inlineComment(): void {
// $moduleHandler holds the module handler.
$moduleHandler = get_service('module_handler');
}

/**
* C-style inline comment with variable reference.
*/
public function cStyleInlineComment(): void {
/* $moduleHandler holds the module handler. */
$moduleHandler = get_service('module_handler');
}

// =========================================================================
// Mix of comment types — violations expected.
// =========================================================================

/**
* Both doc and inline comments in the same method.
*/
public function mixedCommentTypes(): void {
/** @var \SomeClass $firstHandler */
$firstHandler = get_service('first');

// $secondHandler holds the second handler.
$secondHandler = get_service('second');
}

// =========================================================================
// Comments with multiple variables — violation on variable, but comment
// fix should be skipped (requires manual fix).
// =========================================================================

/**
* Doc comment referencing multiple variables.
*/
public function multipleVarsInComment(): void {
/** @var \SomeClass $moduleHandler See also $otherService */
$moduleHandler = get_service('module_handler');
}

// =========================================================================
// Code between comment and variable — violation on variable, but comment
// fix should not apply (search stops at code).
// =========================================================================

/**
* Code statement between comment and variable.
*/
public function codeBetweenCommentAndVariable(): void {
/** @var \SomeClass $moduleHandler */
$valid_other = 1;
$moduleHandler = get_service('module_handler');
}

// =========================================================================
// Trailing comments on previous statements — violation on variable, but
// comment fix should not apply (trailing comment is not a preceding comment).
// =========================================================================

/**
* Trailing inline comment on previous statement.
*/
public function trailingCommentOnPreviousStatement(): void {
$valid_other = get_service('other'); // $moduleHandler trailing.
$moduleHandler = get_service('module_handler');
}

// =========================================================================
// Valid variables — no violations expected.
// =========================================================================

/**
* Valid snake_case variable with doc comment.
*/
public function validDocComment(): void {
/** @var \SomeClass $module_handler */
$module_handler = get_service('module_handler');
}

/**
* Valid snake_case variable with inline comment.
*/
public function validInlineComment(): void {
// $module_handler holds the module handler.
$module_handler = get_service('module_handler');
}

/**
* Valid snake_case variable with no comment.
*/
public function validNoComment(): void {
$module_handler = get_service('module_handler');
}

/**
* Doc comment with variable but no violation on the actual variable.
*/
public function docCommentOnValidVariable(): void {
/** @var \Drupal\Core\Extension\ModuleHandler $module_handler */
$module_handler = get_service('module_handler');
}

}
11 changes: 11 additions & 0 deletions tests/Fixtures/VariableNaming.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,14 @@ function functionWithMixedParams(string $valid_param, int $invalidParam): void {

$invalidVar = 'invalid';
}

/**
* Function with variables that have @var comments.
*/
function functionWithVarComments(): void {
/** @var \Drupal\Core\Extension\ModuleHandler $moduleHandler */
$moduleHandler = get_service('module_handler');

/** @var SomeClass $validVar */
$valid_var = get_service('valid');
}
Loading
Loading