fix(gazelle) Update gazelle to properly process multi-line python imports (#3077)

A python import may be imported as:
```
from foo.bar.application.\
    pipeline.model import (
    Baz
)
```
However, gazelle fails to resolve this import with the error:
`line 30: "foo.bar.application.pipeline.model\\\n pipeline.mode.Baz" is
an invalid dependency:`

Clean up the imports such that whitespace and \n are removed from the
import path.

---------

Co-authored-by: yushan <yushan@uber.com>
Co-authored-by: Douglas Thor <dougthor42@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad68669..834a2c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,6 +87,7 @@
   ({gh-issue}`3043`).
 * (pypi) The pipstar `defaults` configuration now supports any custom platform
   name.
+* Multi-line python imports (e.g. with escaped newlines) are now correctly processed by Gazelle.
 
 {#v0-0-0-added}
 ### Added
diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go
index 31fce02..e129337 100644
--- a/gazelle/python/file_parser.go
+++ b/gazelle/python/file_parser.go
@@ -144,6 +144,16 @@
 	return Module{}, false
 }
 
+// cleanImportString removes backslashes and all whitespace from the string.
+func cleanImportString(s string) string {
+	s = strings.ReplaceAll(s, "\r\n", "")
+	s = strings.ReplaceAll(s, "\\", "")
+	s = strings.ReplaceAll(s, " ", "")
+	s = strings.ReplaceAll(s, "\n", "")
+	s = strings.ReplaceAll(s, "\t", "")
+	return s
+}
+
 // parseImportStatements parses a node for import statements, returning true if the node is
 // an import statement. It updates FileParser.output.Modules with the `module` that the
 // import represents.
@@ -154,6 +164,8 @@
 			if !ok {
 				continue
 			}
+			m.From = cleanImportString(m.From)
+			m.Name = cleanImportString(m.Name)
 			m.Filepath = p.relFilepath
 			m.TypeCheckingOnly = p.inTypeCheckingBlock
 			if strings.HasPrefix(m.Name, ".") {
@@ -163,6 +175,7 @@
 		}
 	} else if node.Type() == sitterNodeTypeImportFromStatement {
 		from := node.Child(1).Content(p.code)
+		from = cleanImportString(from)
 		// If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1.
 		// If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules.
 		if from == "." {
@@ -175,6 +188,7 @@
 			}
 			m.Filepath = p.relFilepath
 			m.From = from
+			m.Name = cleanImportString(m.Name)
 			m.Name = fmt.Sprintf("%s.%s", from, m.Name)
 			m.TypeCheckingOnly = p.inTypeCheckingBlock
 			p.output.Modules = append(p.output.Modules, m)
diff --git a/gazelle/python/file_parser_test.go b/gazelle/python/file_parser_test.go
index f4db1a3..0a6fd1b 100644
--- a/gazelle/python/file_parser_test.go
+++ b/gazelle/python/file_parser_test.go
@@ -291,3 +291,95 @@
 		}
 	}
 }
+
+func TestParseImportStatements_MultilineWithBackslashAndWhitespace(t *testing.T) {
+	t.Parallel()
+	t.Run("multiline from import", func(t *testing.T) {
+		p := NewFileParser()
+		code := []byte(`from foo.bar.\
+    baz import (
+    Something,
+    AnotherThing
+)
+
+from foo\
+	.test import (
+    Foo,
+    Bar
+)
+`)
+		p.SetCodeAndFile(code, "", "test.py")
+		output, err := p.Parse(context.Background())
+		assert.NoError(t, err)
+		// Updated expected to match parser output
+		expected := []Module{
+			{
+				Name:       "foo.bar.baz.Something",
+				LineNumber: 3,
+				Filepath:   "test.py",
+				From:       "foo.bar.baz",
+			},
+			{
+				Name:       "foo.bar.baz.AnotherThing",
+				LineNumber: 4,
+				Filepath:   "test.py",
+				From:       "foo.bar.baz",
+			},
+			{
+				Name:       "foo.test.Foo",
+				LineNumber: 9,
+				Filepath:   "test.py",
+				From:       "foo.test",
+			},
+			{
+				Name:       "foo.test.Bar",
+				LineNumber: 10,
+				Filepath:   "test.py",
+				From:       "foo.test",
+			},
+		}
+		assert.ElementsMatch(t, expected, output.Modules)
+	})
+	t.Run("multiline import", func(t *testing.T) {
+		p := NewFileParser()
+		code := []byte(`import foo.bar.\
+    baz
+`)
+		p.SetCodeAndFile(code, "", "test.py")
+		output, err := p.Parse(context.Background())
+		assert.NoError(t, err)
+		// Updated expected to match parser output
+		expected := []Module{
+			{
+				Name:       "foo.bar.baz",
+				LineNumber: 1,
+				Filepath:   "test.py",
+				From:       "",
+			},
+		}
+		assert.ElementsMatch(t, expected, output.Modules)
+	})
+	t.Run("windows line endings", func(t *testing.T) {
+		p := NewFileParser()
+		code := []byte("from foo.bar.\r\n baz import (\r\n    Something,\r\n    AnotherThing\r\n)\r\n")
+		p.SetCodeAndFile(code, "", "test.py")
+		output, err := p.Parse(context.Background())
+		assert.NoError(t, err)
+		// Updated expected to match parser output
+		expected := []Module{
+			{
+				Name:       "foo.bar.baz.Something",
+				LineNumber: 3,
+				Filepath:   "test.py",
+				From:       "foo.bar.baz",
+			},
+			{
+				Name:       "foo.bar.baz.AnotherThing",
+				LineNumber: 4,
+				Filepath:   "test.py",
+				From:       "foo.bar.baz",
+			},
+		}
+		assert.ElementsMatch(t, expected, output.Modules)
+	})
+}
diff --git a/gazelle/python/testdata/from_imports/import_nested_var/__init__.py b/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
index d0f51c4..20eda53 100644
--- a/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
+++ b/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
@@ -13,4 +13,8 @@
 # limitations under the License.
 
 # baz is a variable in foo/bar/baz.py
-from foo.bar.baz import baz
+from foo\
+    .bar.\
+        baz import (
+            baz
+        )