Repository: PHP-CS-Fixer/PHP-CS-Fixer (php)

Issue:
TokensAnalyzer::isConstantInvocation() false-negative in foreach statement
### Problem description

`\PhpCsFixer\Fixer\Import\NoUnusedImportsFixer` Incorrectly removed a `use const` statement because the constant was used in a `foreach` statement. Tracked down the issue to `TokensAnalyzer::isConstantInvocation()`.

I wasn't able to submit a PR because I Couldn't get the testsuite to pass on a clean checkout
```
1 files with FAILURES:
 PhpCsFixer\Tests\Fixer\PhpUnit\PhpUnitAttributesFixerTest
```

Here is a patch with test & fix code
```patch
diff --git a/src/Tokenizer/TokensAnalyzer.php b/src/Tokenizer/TokensAnalyzer.php
index e367abc8a..d0193540c 100644
--- a/src/Tokenizer/TokensAnalyzer.php
+++ b/src/Tokenizer/TokensAnalyzer.php
@@ -368,11 +368,20 @@ public function isConstantInvocation(int $index): bool
 
         if (
             $this->tokens[$nextIndex]->equalsAny(['(', '{'])
-            || $this->tokens[$nextIndex]->isGivenKind([T_AS, T_DOUBLE_COLON, T_ELLIPSIS, T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_VARIABLE])
+            || $this->tokens[$nextIndex]->isGivenKind([T_DOUBLE_COLON, T_ELLIPSIS, T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_VARIABLE])
         ) {
             return false;
         }
 
+        // handle foreach( FOO as $_ ) {}
+        if ($this->tokens[$nextIndex]->isGivenKind(T_AS)) {
+            $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
+
+            if (!$this->tokens[$prevIndex]->equals('(')) {
+                return false;
+            }
+        }
+
         $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
 
         if ($this->tokens[$prevIndex]->isGivenKind(Token::getClassyTokenKinds())) {
diff --git a/tests/Tokenizer/TokensAnalyzerTest.php b/tests/Tokenizer/TokensAnalyzerTest.php
index 33ee5ac80..456628116 100644
--- a/tests/Tokenizer/TokensAnalyzerTest.php
+++ b/tests/Tokenizer/TokensAnalyzerTest.php
@@ -1238,6 +1238,11 @@ public static function provideIsConstantInvocationCases(): iterable
             '<?php use X\Y\{FOO, BAR as BAR2, BAZ};',
         ];
 
+        yield [
+            [4 => true],
+            '<?php foreach(FOO as $foo) {}',
+        ];
+
         yield [
             [6 => false, 16 => false, 21 => false],
             '<?php
```

### Minimal reproducer

```php
<?php
use const Foo\BAR;

foreach(BAR as $bar) {}
```

### Fixer version

3.64.1

### PHP version

8.2.23

### How do you run Fixer?

Composer package

### Contribution Checks

- [X] I have verified if this problem was already reported
- [X] I am familiar with ["Feature or bug?"](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/feature-or-bug.rst)

Pull request:
fix: Properly recognise constants in foreach loops
Closes #8202

Patch:
diff --git a/src/Tokenizer/TokensAnalyzer.php b/src/Tokenizer/TokensAnalyzer.php
index e367abc8a..d0193540c 100644
--- a/src/Tokenizer/TokensAnalyzer.php
+++ b/src/Tokenizer/TokensAnalyzer.php
@@ -368,11 +368,20 @@ public function isConstantInvocation(int $index): bool
 
         if (
             $this->tokens[$nextIndex]->equalsAny(['(', '{'])
-            || $this->tokens[$nextIndex]->isGivenKind([T_AS, T_DOUBLE_COLON, T_ELLIPSIS, T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_VARIABLE])
+            || $this->tokens[$nextIndex]->isGivenKind([T_DOUBLE_COLON, T_ELLIPSIS, T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_VARIABLE])
         ) {
             return false;
         }
 
+        // handle foreach( FOO as $_ ) {}
+        if ($this->tokens[$nextIndex]->isGivenKind(T_AS)) {
+            $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
+
+            if (!$this->tokens[$prevIndex]->equals('(')) {
+                return false;
+            }
+        }
+
         $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
 
         if ($this->tokens[$prevIndex]->isGivenKind(Token::getClassyTokenKinds())) {


Test patch:
diff --git a/tests/Tokenizer/TokensAnalyzerTest.php b/tests/Tokenizer/TokensAnalyzerTest.php
index 33ee5ac80..fe90f70da 100644
--- a/tests/Tokenizer/TokensAnalyzerTest.php
+++ b/tests/Tokenizer/TokensAnalyzerTest.php
@@ -1283,6 +1283,20 @@ abstract public function test(): Foo;
             [3 => false, 11 => false, 18 => false],
             '<?php interface Foo { public function bar(): ?\Baz; }',
         ];
+
+        yield [
+            [3 => true],
+            '<?php foreach(FOO as $foo) {}',
+        ];
+
+        yield [
+            [
+                3 => false,
+                5 => false,
+                9 => false,
+            ],
+            '<?php use Foo\Bir as FBB;',
+        ];
     }
 
     /**