diff --git a/CHANGELOG.md b/CHANGELOG.md index ad68669df2..834a2c1a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ END_UNRELEASED_TEMPLATE ({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 31fce02712..e129337e11 100644 --- a/gazelle/python/file_parser.go +++ b/gazelle/python/file_parser.go @@ -144,6 +144,16 @@ func parseImportStatement(node *sitter.Node, code []byte) (Module, bool) { 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 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { 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 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { } } 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 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { } 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 f4db1a316b..0a6fd1b4ab 100644 --- a/gazelle/python/file_parser_test.go +++ b/gazelle/python/file_parser_test.go @@ -291,3 +291,95 @@ def example_function(): } } } + +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 d0f51c443c..20eda530e5 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 + )