Repository: hhatto/autopep8

Issue:
W605: identical tokens cause infinite loop 
There is a bug with the way that tokens are handled with W605 that causes an infinite loop to occur when two identical W605 tokens exist on the same line of code.  This infinite loop can be observed both on the command line and by adding a test to the test_autopep8.py unit test file.

**Test Case Code:**
`escape = foo('\.bar', '\.bar')\n`

**Command Line:**
`autopep8.py src/ -i -r --exclude "config_*" -a -j 8`

**Workaround:** 
- To the command line, add `--pep8-passes <n>`.  This will break the infinite loop, but a bad code-fix is put in place
- Bad Code-Fix Example =  `escape = foo(rrrrrrrrr'\.bar', '\.bar')\n`

**Version Info**
`autopep8.py --version  :  autopep8 1.4.1 (pycodestyle: 2.4.0)`
`/usr/bin/env python --version  : Python 2.7.5`

Discussion:

Patch:
diff --git a/autopep8.py b/autopep8.py
index 53a8446..e30b4eb 100755
--- a/autopep8.py
+++ b/autopep8.py
@@ -1363,7 +1363,7 @@ def get_w605_position(tokens):
         'N', 'u', 'U',
     ]
 
-    for token_type, text, start, end, line in tokens:
+    for token_type, text, start_pos, end_pos, line in tokens:
         if token_type == tokenize.STRING:
             quote = text[-3:] if text[-3:] in ('"""', "'''") else text[-1]
             # Extract string modifiers (e.g. u or r)
@@ -1378,7 +1378,8 @@ def get_w605_position(tokens):
                     pos += 1
                     if string[pos] not in valid:
                         yield (
-                            line.find(text),
+                            # No need to search line, token stores position
+                            start_pos[1],
                             "W605 invalid escape sequence '\\%s'" %
                             string[pos],
                         )


Test patch:
diff --git a/test/test_autopep8.py b/test/test_autopep8.py
index 03e3084..cfb80d4 100755
--- a/test/test_autopep8.py
+++ b/test/test_autopep8.py
@@ -4734,6 +4734,25 @@ raise ValueError("error")
         with autopep8_context(line, options=['--aggressive']) as result:
             self.assertEqual(fixed, result)
 
+    def test_w605_identical_token(self):
+        # ***NOTE***: The --pep8-passes option is requred to prevent an infinite loop in
+        # the old, failing code. DO NOT REMOVE.
+        line = "escape = foo('\.bar', '\.kilroy')\n"
+        fixed = "escape = foo(r'\.bar', r'\.kilroy')\n"
+        with autopep8_context(line, options=['--aggressive', '--pep8-passes', '5']) as result:
+            self.assertEqual(fixed, result, "Two tokens get r added")
+
+        line = "escape = foo('\.bar', r'\.kilroy')\n"
+        fixed = "escape = foo(r'\.bar', r'\.kilroy')\n"
+        with autopep8_context(line, options=['--aggressive', '--pep8-passes', '5']) as result:
+            self.assertEqual(fixed, result, "r not added if already there")
+
+        # Test Case to catch bad behavior reported in Issue #449
+        line = "escape = foo('\.bar', '\.bar')\n"
+        fixed = "escape = foo(r'\.bar', r'\.bar')\n"
+        with autopep8_context(line, options=['--aggressive', '--pep8-passes', '5']) as result:
+            self.assertEqual(fixed, result)
+
     def test_trailing_whitespace_in_multiline_string(self):
         line = 'x = """ \nhello"""    \n'
         fixed = 'x = """ \nhello"""\n'