Skip to content

Commit 6f27511

Browse files
yushan26yushan8dougthor42
authored
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 <[email protected]> Co-authored-by: Douglas Thor <[email protected]>
1 parent c30980a commit 6f27511

File tree

4 files changed

+112
-1
lines changed

4 files changed

+112
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ END_UNRELEASED_TEMPLATE
8787
({gh-issue}`3043`).
8888
* (pypi) The pipstar `defaults` configuration now supports any custom platform
8989
name.
90+
* Multi-line python imports (e.g. with escaped newlines) are now correctly processed by Gazelle.
9091

9192
{#v0-0-0-added}
9293
### Added

gazelle/python/file_parser.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ func parseImportStatement(node *sitter.Node, code []byte) (Module, bool) {
144144
return Module{}, false
145145
}
146146

147+
// cleanImportString removes backslashes and all whitespace from the string.
148+
func cleanImportString(s string) string {
149+
s = strings.ReplaceAll(s, "\r\n", "")
150+
s = strings.ReplaceAll(s, "\\", "")
151+
s = strings.ReplaceAll(s, " ", "")
152+
s = strings.ReplaceAll(s, "\n", "")
153+
s = strings.ReplaceAll(s, "\t", "")
154+
return s
155+
}
156+
147157
// parseImportStatements parses a node for import statements, returning true if the node is
148158
// an import statement. It updates FileParser.output.Modules with the `module` that the
149159
// import represents.
@@ -154,6 +164,8 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
154164
if !ok {
155165
continue
156166
}
167+
m.From = cleanImportString(m.From)
168+
m.Name = cleanImportString(m.Name)
157169
m.Filepath = p.relFilepath
158170
m.TypeCheckingOnly = p.inTypeCheckingBlock
159171
if strings.HasPrefix(m.Name, ".") {
@@ -163,6 +175,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
163175
}
164176
} else if node.Type() == sitterNodeTypeImportFromStatement {
165177
from := node.Child(1).Content(p.code)
178+
from = cleanImportString(from)
166179
// If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1.
167180
// If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules.
168181
if from == "." {
@@ -175,6 +188,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
175188
}
176189
m.Filepath = p.relFilepath
177190
m.From = from
191+
m.Name = cleanImportString(m.Name)
178192
m.Name = fmt.Sprintf("%s.%s", from, m.Name)
179193
m.TypeCheckingOnly = p.inTypeCheckingBlock
180194
p.output.Modules = append(p.output.Modules, m)

gazelle/python/file_parser_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,95 @@ def example_function():
291291
}
292292
}
293293
}
294+
295+
func TestParseImportStatements_MultilineWithBackslashAndWhitespace(t *testing.T) {
296+
t.Parallel()
297+
t.Run("multiline from import", func(t *testing.T) {
298+
p := NewFileParser()
299+
code := []byte(`from foo.bar.\
300+
baz import (
301+
Something,
302+
AnotherThing
303+
)
304+
305+
from foo\
306+
.test import (
307+
Foo,
308+
Bar
309+
)
310+
`)
311+
p.SetCodeAndFile(code, "", "test.py")
312+
output, err := p.Parse(context.Background())
313+
assert.NoError(t, err)
314+
// Updated expected to match parser output
315+
expected := []Module{
316+
{
317+
Name: "foo.bar.baz.Something",
318+
LineNumber: 3,
319+
Filepath: "test.py",
320+
From: "foo.bar.baz",
321+
},
322+
{
323+
Name: "foo.bar.baz.AnotherThing",
324+
LineNumber: 4,
325+
Filepath: "test.py",
326+
From: "foo.bar.baz",
327+
},
328+
{
329+
Name: "foo.test.Foo",
330+
LineNumber: 9,
331+
Filepath: "test.py",
332+
From: "foo.test",
333+
},
334+
{
335+
Name: "foo.test.Bar",
336+
LineNumber: 10,
337+
Filepath: "test.py",
338+
From: "foo.test",
339+
},
340+
}
341+
assert.ElementsMatch(t, expected, output.Modules)
342+
})
343+
t.Run("multiline import", func(t *testing.T) {
344+
p := NewFileParser()
345+
code := []byte(`import foo.bar.\
346+
baz
347+
`)
348+
p.SetCodeAndFile(code, "", "test.py")
349+
output, err := p.Parse(context.Background())
350+
assert.NoError(t, err)
351+
// Updated expected to match parser output
352+
expected := []Module{
353+
{
354+
Name: "foo.bar.baz",
355+
LineNumber: 1,
356+
Filepath: "test.py",
357+
From: "",
358+
},
359+
}
360+
assert.ElementsMatch(t, expected, output.Modules)
361+
})
362+
t.Run("windows line endings", func(t *testing.T) {
363+
p := NewFileParser()
364+
code := []byte("from foo.bar.\r\n baz import (\r\n Something,\r\n AnotherThing\r\n)\r\n")
365+
p.SetCodeAndFile(code, "", "test.py")
366+
output, err := p.Parse(context.Background())
367+
assert.NoError(t, err)
368+
// Updated expected to match parser output
369+
expected := []Module{
370+
{
371+
Name: "foo.bar.baz.Something",
372+
LineNumber: 3,
373+
Filepath: "test.py",
374+
From: "foo.bar.baz",
375+
},
376+
{
377+
Name: "foo.bar.baz.AnotherThing",
378+
LineNumber: 4,
379+
Filepath: "test.py",
380+
From: "foo.bar.baz",
381+
},
382+
}
383+
assert.ElementsMatch(t, expected, output.Modules)
384+
})
385+
}

gazelle/python/testdata/from_imports/import_nested_var/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
# limitations under the License.
1414

1515
# baz is a variable in foo/bar/baz.py
16-
from foo.bar.baz import baz
16+
from foo\
17+
.bar.\
18+
baz import (
19+
baz
20+
)

0 commit comments

Comments
 (0)