Repository: pylint-dev/pylint

Issue:
Rule E0643 behaves inconsistently when indexing tuples
### Bug description

In the following example file ex.py I'm indexing a tuple built from another iterable. The potential-index-error rule (E0643) behaves differently depending on the syntax choices: 
- indexing a tuple from the `tuple` -constructor produces no error
- indexing a tuple made using the star syntax triggers E0643 (if using index >= 1).
- however, saving the starred tuple to a variable first and indexing that does not produce an error

ex.py

```python
"""Example"""

from typing import reveal_type

my_list = ["foo", "bar"]

assert tuple(my_list)[0] == "foo"  # Ok
assert tuple(my_list)[1] == "bar"  # Ok

assert (*my_list,)[0] == "foo"  # Ok
assert (*my_list,)[1] == "bar"  # Pylint(E0643:potential-index-error)

my_tuple = (*my_list,)
reveal_type(my_tuple)  # tuple[str, ...]

assert my_tuple[1] == "bar"  # Now ok, no error
```

### Command used

```shell
pylint ex.py
```

### Pylint output

```python
************* Module ex
ex.py:11:7: E0643: Invalid index for iterable length (potential-index-error)

------------------------------------------------------------------
Your code has been rated at 4.44/10
```

### Expected behavior

No errors in the file, or the E0643 error occurs multiple times. Whichever is intended for indexing tuples.

### Pylint version

```shell
pylint 3.3.1
astroid 3.3.5
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)]
```

### OS / Environment

Win11



Discussion:
Thank you for highlighting this issue. In the given example, E0643 was raised because the star/unpacking operator was not considered. As a result, the checker counted the literal number of tuple elements - `(["foo", "bar"])` instead of `("foo", "bar")`. This error should not be raised.
Thank you for the answers!



Patch:
diff --git a/doc/whatsnew/fragments/10076.false_positive b/doc/whatsnew/fragments/10076.false_positive
new file mode 100644
index 0000000000..298f448873
--- /dev/null
+++ b/doc/whatsnew/fragments/10076.false_positive
@@ -0,0 +1,4 @@
+Fix a false positive for `potential-index-error` when an indexed iterable
+contains a starred element that evaluates to more than one item.
+
+Closes #10076
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index 7a63798d91..c68ed7985b 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -3366,6 +3366,19 @@ def visit_subscript(self, node: nodes.Subscript) -> None:
 
         self._check_potential_index_error(node, inferred_slice)
 
+    def _inferred_iterable_length(self, iterable: nodes.Tuple | nodes.List) -> int:
+        length = 0
+        for elt in iterable.elts:
+            if not isinstance(elt, nodes.Starred):
+                length += 1
+                continue
+            unpacked = utils.safe_infer(elt.value)
+            if isinstance(unpacked, nodes.BaseContainer):
+                length += len(unpacked.elts)
+            else:
+                length += 1
+        return length
+
     def _check_potential_index_error(
         self,
         node: nodes.Subscript,
@@ -3381,7 +3394,7 @@ def _check_potential_index_error(
         # If the node.value is a Tuple or List without inference it is defined in place
         if isinstance(node.value, (nodes.Tuple, nodes.List)):
             # Add 1 because iterables are 0-indexed
-            if len(node.value.elts) < inferred_slice.value + 1:
+            if self._inferred_iterable_length(node.value) < inferred_slice.value + 1:
                 self.add_message(
                     "potential-index-error", node=node, confidence=INFERENCE
                 )


Test patch:
diff --git a/tests/functional/p/potential_index_error.py b/tests/functional/p/potential_index_error.py
index 4d3c48d75d..71c025a767 100644
--- a/tests/functional/p/potential_index_error.py
+++ b/tests/functional/p/potential_index_error.py
@@ -23,3 +23,26 @@ def my_func():
 # Test that we don't crash on more complicated indices/slices
 # We do not raise here (currently)
 print([1, 2, 3][2:3])
+
+
+# Test for cases with unpacking operation
+my_list = ["foo", "bar"]
+my_set = {"foo", "bar"}
+my_tuple = ("foo", "bar")
+my_iterable = (*my_list, *my_set, *my_tuple, *("foo", "bar"))
+my_non_iterable = None
+
+print([*my_list][1])
+print([*my_list][2])  # [potential-index-error]
+
+print([*my_set][1])
+print([*my_set][2])  # [potential-index-error]
+
+print((*my_tuple,)[1])
+print((*my_tuple,)[2])  # [potential-index-error]
+
+print((*my_iterable,)[7])
+print((*my_iterable,)[8])  # [potential-index-error]
+
+print((*my_non_iterable,)[0])
+print((*my_non_iterable,)[1])  # [potential-index-error]
diff --git a/tests/functional/p/potential_index_error.txt b/tests/functional/p/potential_index_error.txt
index 2340f81737..fc99227ec9 100644
--- a/tests/functional/p/potential_index_error.txt
+++ b/tests/functional/p/potential_index_error.txt
@@ -1,3 +1,8 @@
 potential-index-error:6:6:6:18::Invalid index for iterable length:INFERENCE
 potential-index-error:7:6:7:18::Invalid index for iterable length:INFERENCE
 potential-index-error:8:6:8:22::Invalid index for iterable length:INFERENCE
+potential-index-error:36:6:36:19::Invalid index for iterable length:INFERENCE
+potential-index-error:39:6:39:18::Invalid index for iterable length:INFERENCE
+potential-index-error:42:6:42:21::Invalid index for iterable length:INFERENCE
+potential-index-error:45:6:45:24::Invalid index for iterable length:INFERENCE
+potential-index-error:48:6:48:28::Invalid index for iterable length:INFERENCE