Repository: asottile/pyupgrade

Issue:
six.reraise: IndexError: list index out of range
pyupgrade 1.26.0

Minimal test case. Requires using the `--py3-plus` argument.

```
def foo(func, arg):
    et, ev, _ = sys.exc_info()
    six.reraise(et, (ev[0], ev[1] + (" %s %s" % (func, arg))))
```

```
Traceback (most recent call last):
  File ".../venv/bin/pyupgrade", line 10, in <module>
    sys.exit(main())
  File ".../venv/lib64/python3.8/site-packages/pyupgrade.py", line 2318, in main
    ret |= _fix_file(filename, args)
  File ".../venv/lib64/python3.8/site-packages/pyupgrade.py", line 2280, in _fix_file
    contents_text = _fix_py3_plus(contents_text)
  File ".../venv/lib64/python3.8/site-packages/pyupgrade.py", line 1984, in _fix_py3_plus
    _replace_call(tokens, i, end, func_args, template)
  File ".../venv/lib64/python3.8/site-packages/pyupgrade.py", line 1849, in _replace_call
    src = tmpl.format(args=arg_strs, rest=rest)
IndexError: list index out of range
```

Discovered by running on setuptools:

https://github.com/pypa/setuptools/blob/682b6511ac67e021b542e74ce30e13fe52bc2da9/setuptools/command/easy_install.py#L1730-L1735

Discussion:

Patch:
diff --git a/pyupgrade.py b/pyupgrade.py
index 2f9170e..4a0f9c1 100644
--- a/pyupgrade.py
+++ b/pyupgrade.py
@@ -1137,10 +1137,9 @@ SIX_CALLS = {
 SIX_B_TMPL = 'b{args[0]}'
 WITH_METACLASS_NO_BASES_TMPL = 'metaclass={args[0]}'
 WITH_METACLASS_BASES_TMPL = '{rest}, metaclass={args[0]}'
-SIX_RAISES = {
-    'raise_from': 'raise {args[0]} from {rest}',
-    'reraise': 'raise {args[1]}.with_traceback({args[2]})',
-}
+RAISE_FROM_TMPL = 'raise {args[0]} from {rest}'
+RERAISE_2_TMPL = 'raise {args[1]}.with_traceback(None)'
+RERAISE_3_TMPL = 'raise {args[1]}.with_traceback({args[2]})'
 
 
 def _all_isinstance(vals, tp):
@@ -1234,7 +1233,8 @@ class FindPy3Plus(ast.NodeVisitor):
         self.six_calls = {}  # type: Dict[Offset, ast.Call]
         self.six_iter = {}  # type: Dict[Offset, ast.Call]
         self._previous_node = None  # type: Optional[ast.AST]
-        self.six_raises = {}  # type: Dict[Offset, ast.Call]
+        self.six_raise_from = set()  # type: Set[Offset]
+        self.six_reraise = set()  # type: Set[Offset]
         self.six_remove_decorators = set()  # type: Set[Offset]
         self.six_simple = {}  # type: Dict[Offset, NameOrAttr]
         self.six_type_ctx = {}  # type: Dict[Offset, NameOrAttr]
@@ -1448,10 +1448,16 @@ class FindPy3Plus(ast.NodeVisitor):
             self.six_iter[_ast_to_offset(node.args[0])] = node.args[0]
         elif (
                 isinstance(self._previous_node, ast.Expr) and
-                self._is_six(node.func, SIX_RAISES) and
+                self._is_six(node.func, ('raise_from',)) and
+                not _starargs(node)
+        ):
+            self.six_raise_from.add(_ast_to_offset(node))
+        elif (
+                isinstance(self._previous_node, ast.Expr) and
+                self._is_six(node.func, ('reraise',)) and
                 not _starargs(node)
         ):
-            self.six_raises[_ast_to_offset(node)] = node
+            self.six_reraise.add(_ast_to_offset(node))
         elif (
                 not self._in_comp and
                 self._class_info_stack and
@@ -1882,7 +1888,8 @@ def _fix_py3_plus(contents_text):  # type: (str) -> str
             visitor.six_b,
             visitor.six_calls,
             visitor.six_iter,
-            visitor.six_raises,
+            visitor.six_raise_from,
+            visitor.six_reraise,
             visitor.six_remove_decorators,
             visitor.six_simple,
             visitor.six_type_ctx,
@@ -1975,13 +1982,17 @@ def _fix_py3_plus(contents_text):  # type: (str) -> str
             assert isinstance(call.func, (ast.Name, ast.Attribute))
             template = _get_tmpl(SIX_CALLS, call.func)
             _replace_call(tokens, i, end, func_args, template)
-        elif token.offset in visitor.six_raises:
+        elif token.offset in visitor.six_raise_from:
             j = _find_open_paren(tokens, i)
             func_args, end = _parse_call_args(tokens, j)
-            call = visitor.six_raises[token.offset]
-            assert isinstance(call.func, (ast.Name, ast.Attribute))
-            template = _get_tmpl(SIX_RAISES, call.func)
-            _replace_call(tokens, i, end, func_args, template)
+            _replace_call(tokens, i, end, func_args, RAISE_FROM_TMPL)
+        elif token.offset in visitor.six_reraise:
+            j = _find_open_paren(tokens, i)
+            func_args, end = _parse_call_args(tokens, j)
+            if len(func_args) == 2:
+                _replace_call(tokens, i, end, func_args, RERAISE_2_TMPL)
+            else:
+                _replace_call(tokens, i, end, func_args, RERAISE_3_TMPL)
         elif token.offset in visitor.six_add_metaclass:
             j = _find_open_paren(tokens, i)
             func_args, end = _parse_call_args(tokens, j)


Test patch:
diff --git a/tests/six_test.py b/tests/six_test.py
index 83ce558..c58f5fc 100644
--- a/tests/six_test.py
+++ b/tests/six_test.py
@@ -221,6 +221,10 @@ def test_fix_six_noop(s):
             'six.reraise(tp, exc, tb)\n',
             'raise exc.with_traceback(tb)\n',
         ),
+        (
+            'six.reraise(tp, exc)\n',
+            'raise exc.with_traceback(None)\n',
+        ),
         (
             'from six import raise_from\n'
             'raise_from(exc, exc_from)\n',