Repository: ipython/ipython

Issue:
Bug: extra closing ) does not trigger SyntaxError
Consider the following:
```
> ipython
IPython 7.21.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print('hello'))
   ...:
   ...:
```
The extra `)` should have resulted in a `SyntaxError`; instead, more input is apparently expected. This problem is present in earlier versions of IPython as well. 



Discussion:

Resolving patch:
diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py
--- a/IPython/core/inputtransformer2.py
+++ b/IPython/core/inputtransformer2.py
@@ -508,6 +508,20 @@ def make_tokens_by_line(lines:List[str]):
 
     return tokens_by_line
 
+
+def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
+    """Check if the depth of brackets in the list of tokens drops below 0"""
+    parenlev = 0
+    for token in tokens:
+        if token.string in {"(", "[", "{"}:
+            parenlev += 1
+        elif token.string in {")", "]", "}"}:
+            parenlev -= 1
+            if parenlev < 0:
+                return True
+    return False
+
+
 def show_linewise_tokens(s: str):
     """For investigation and debugging"""
     if not s.endswith('\n'):
@@ -662,6 +676,15 @@ def check_complete(self, cell: str):
 
         tokens_by_line = make_tokens_by_line(lines)
 
+        # Bail if we got one line and there are more closing parentheses than
+        # the opening ones
+        if (
+            len(lines) == 1
+            and tokens_by_line
+            and has_sunken_brackets(tokens_by_line[0])
+        ):
+            return "invalid", None
+
         if not tokens_by_line:
             return 'incomplete', find_last_indent(lines)
 


Test patch:
diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py
--- a/IPython/core/tests/test_inputtransformer2.py
+++ b/IPython/core/tests/test_inputtransformer2.py
@@ -255,18 +255,18 @@ def __init__(self, s):
 
 def test_check_complete():
     cc = ipt2.TransformerManager().check_complete
-    nt.assert_equal(cc("a = 1"), ('complete', None))
-    nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4))
-    nt.assert_equal(cc("for a in range(5):\n    if a > 0:"), ('incomplete', 8))
-    nt.assert_equal(cc("raise = 2"), ('invalid', None))
-    nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0))
-    nt.assert_equal(cc(")"), ('incomplete', 0))
-    nt.assert_equal(cc("\\\r\n"), ('incomplete', 0))
-    nt.assert_equal(cc("a = '''\n   hi"), ('incomplete', 3))
-    nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
-    nt.assert_equal(cc("a \\ "), ('invalid', None))  # Nothing allowed after backslash
-    nt.assert_equal(cc("1\\\n+2"), ('complete', None))
-    nt.assert_equal(cc("exit"), ('complete', None))
+    nt.assert_equal(cc("a = 1"), ("complete", None))
+    nt.assert_equal(cc("for a in range(5):"), ("incomplete", 4))
+    nt.assert_equal(cc("for a in range(5):\n    if a > 0:"), ("incomplete", 8))
+    nt.assert_equal(cc("raise = 2"), ("invalid", None))
+    nt.assert_equal(cc("a = [1,\n2,"), ("incomplete", 0))
+    nt.assert_equal(cc("(\n))"), ("incomplete", 0))
+    nt.assert_equal(cc("\\\r\n"), ("incomplete", 0))
+    nt.assert_equal(cc("a = '''\n   hi"), ("incomplete", 3))
+    nt.assert_equal(cc("def a():\n x=1\n global x"), ("invalid", None))
+    nt.assert_equal(cc("a \\ "), ("invalid", None))  # Nothing allowed after backslash
+    nt.assert_equal(cc("1\\\n+2"), ("complete", None))
+    nt.assert_equal(cc("exit"), ("complete", None))
 
     example = dedent("""
         if True:
@@ -297,6 +297,24 @@ def test_check_complete_II():
     nt.assert_equal(cc('''def foo():\n    """'''), ('incomplete', 4))
 
 
+def test_check_complete_invalidates_sunken_brackets():
+    """
+    Test that a single line with more closing brackets than the opening ones is
+    interpretted as invalid
+    """
+    cc = ipt2.TransformerManager().check_complete
+    nt.assert_equal(cc(")"), ("invalid", None))
+    nt.assert_equal(cc("]"), ("invalid", None))
+    nt.assert_equal(cc("}"), ("invalid", None))
+    nt.assert_equal(cc(")("), ("invalid", None))
+    nt.assert_equal(cc("]["), ("invalid", None))
+    nt.assert_equal(cc("}{"), ("invalid", None))
+    nt.assert_equal(cc("]()("), ("invalid", None))
+    nt.assert_equal(cc("())("), ("invalid", None))
+    nt.assert_equal(cc(")[]("), ("invalid", None))
+    nt.assert_equal(cc("()]("), ("invalid", None))
+
+
 def test_null_cleanup_transformer():
     manager = ipt2.TransformerManager()
     manager.cleanup_transforms.insert(0, null_cleanup_transformer)