|  | // Copyright 2023 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package python | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "testing" | 
|  |  | 
|  | "github.com/stretchr/testify/assert" | 
|  | ) | 
|  |  | 
|  | func TestParseImportStatements(t *testing.T) { | 
|  | t.Parallel() | 
|  | units := []struct { | 
|  | name     string | 
|  | code     string | 
|  | filepath string | 
|  | result   []Module | 
|  | }{ | 
|  | { | 
|  | name:     "not has import", | 
|  | code:     "a = 1\nb = 2", | 
|  | filepath: "", | 
|  | result:   nil, | 
|  | }, | 
|  | { | 
|  | name:     "has import", | 
|  | code:     "import unittest\nimport os.path\nfrom foo.bar import abc.xyz", | 
|  | filepath: "abc.py", | 
|  | result: []Module{ | 
|  | { | 
|  | Name:       "unittest", | 
|  | LineNumber: 1, | 
|  | Filepath:   "abc.py", | 
|  | From:       "", | 
|  | }, | 
|  | { | 
|  | Name:       "os.path", | 
|  | LineNumber: 2, | 
|  | Filepath:   "abc.py", | 
|  | From:       "", | 
|  | }, | 
|  | { | 
|  | Name:       "foo.bar.abc.xyz", | 
|  | LineNumber: 3, | 
|  | Filepath:   "abc.py", | 
|  | From:       "foo.bar", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name: "has import in def", | 
|  | code: `def foo(): | 
|  | import unittest | 
|  | `, | 
|  | filepath: "abc.py", | 
|  | result: []Module{ | 
|  | { | 
|  | Name:       "unittest", | 
|  | LineNumber: 2, | 
|  | Filepath:   "abc.py", | 
|  | From:       "", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name:     "invalid syntax", | 
|  | code:     "import os\nimport", | 
|  | filepath: "abc.py", | 
|  | result: []Module{ | 
|  | { | 
|  | Name:       "os", | 
|  | LineNumber: 1, | 
|  | Filepath:   "abc.py", | 
|  | From:       "", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name:     "import as", | 
|  | code:     "import os as b\nfrom foo import bar as c# 123", | 
|  | filepath: "abc.py", | 
|  | result: []Module{ | 
|  | { | 
|  | Name:       "os", | 
|  | LineNumber: 1, | 
|  | Filepath:   "abc.py", | 
|  | From:       "", | 
|  | }, | 
|  | { | 
|  | Name:       "foo.bar", | 
|  | LineNumber: 2, | 
|  | Filepath:   "abc.py", | 
|  | From:       "foo", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | // align to https://docs.python.org/3/reference/simple_stmts.html#index-34 | 
|  | { | 
|  | name: "complex import", | 
|  | code: "from unittest import *\nfrom foo import (bar as c, baz, qux as d)\nfrom . import abc", | 
|  | result: []Module{ | 
|  | { | 
|  | Name:       "unittest.*", | 
|  | LineNumber: 1, | 
|  | From:       "unittest", | 
|  | }, | 
|  | { | 
|  | Name:       "foo.bar", | 
|  | LineNumber: 2, | 
|  | From:       "foo", | 
|  | }, | 
|  | { | 
|  | Name:       "foo.baz", | 
|  | LineNumber: 2, | 
|  | From:       "foo", | 
|  | }, | 
|  | { | 
|  | Name:       "foo.qux", | 
|  | LineNumber: 2, | 
|  | From:       "foo", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | } | 
|  | for _, u := range units { | 
|  | t.Run(u.name, func(t *testing.T) { | 
|  | p := NewFileParser() | 
|  | code := []byte(u.code) | 
|  | p.SetCodeAndFile(code, "", u.filepath) | 
|  | output, err := p.Parse(context.Background()) | 
|  | assert.NoError(t, err) | 
|  | assert.Equal(t, u.result, output.Modules) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestParseComments(t *testing.T) { | 
|  | t.Parallel() | 
|  | units := []struct { | 
|  | name   string | 
|  | code   string | 
|  | result []Comment | 
|  | }{ | 
|  | { | 
|  | name:   "not has comment", | 
|  | code:   "a = 1\nb = 2", | 
|  | result: nil, | 
|  | }, | 
|  | { | 
|  | name:   "has comment", | 
|  | code:   "# a = 1\n# b = 2", | 
|  | result: []Comment{"# a = 1", "# b = 2"}, | 
|  | }, | 
|  | { | 
|  | name:   "has comment in if", | 
|  | code:   "if True:\n  # a = 1\n  # b = 2", | 
|  | result: []Comment{"# a = 1", "# b = 2"}, | 
|  | }, | 
|  | { | 
|  | name:   "has comment inline", | 
|  | code:   "import os# 123\nfrom pathlib import Path as b#456", | 
|  | result: []Comment{"# 123", "#456"}, | 
|  | }, | 
|  | } | 
|  | for _, u := range units { | 
|  | t.Run(u.name, func(t *testing.T) { | 
|  | p := NewFileParser() | 
|  | code := []byte(u.code) | 
|  | p.SetCodeAndFile(code, "", "") | 
|  | output, err := p.Parse(context.Background()) | 
|  | assert.NoError(t, err) | 
|  | assert.Equal(t, u.result, output.Comments) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestParseMain(t *testing.T) { | 
|  | t.Parallel() | 
|  | units := []struct { | 
|  | name   string | 
|  | code   string | 
|  | result bool | 
|  | }{ | 
|  | { | 
|  | name:   "not has main", | 
|  | code:   "a = 1\nb = 2", | 
|  | result: false, | 
|  | }, | 
|  | { | 
|  | name: "has main in function", | 
|  | code: `def foo(): | 
|  | if __name__ == "__main__": | 
|  | a = 3 | 
|  | `, | 
|  | result: false, | 
|  | }, | 
|  | { | 
|  | name: "has main", | 
|  | code: ` | 
|  | import unittest | 
|  |  | 
|  | from lib import main | 
|  |  | 
|  |  | 
|  | class ExampleTest(unittest.TestCase): | 
|  | def test_main(self): | 
|  | self.assertEqual( | 
|  | "", | 
|  | main([["A", 1], ["B", 2]]), | 
|  | ) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | unittest.main() | 
|  | `, | 
|  | result: true, | 
|  | }, | 
|  | } | 
|  | for _, u := range units { | 
|  | t.Run(u.name, func(t *testing.T) { | 
|  | p := NewFileParser() | 
|  | code := []byte(u.code) | 
|  | p.SetCodeAndFile(code, "", "") | 
|  | output, err := p.Parse(context.Background()) | 
|  | assert.NoError(t, err) | 
|  | assert.Equal(t, u.result, output.HasMain) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestParseFull(t *testing.T) { | 
|  | p := NewFileParser() | 
|  | code := []byte(`from bar import abc`) | 
|  | p.SetCodeAndFile(code, "foo", "a.py") | 
|  | output, err := p.Parse(context.Background()) | 
|  | assert.NoError(t, err) | 
|  | assert.Equal(t, ParserOutput{ | 
|  | Modules:  []Module{{Name: "bar.abc", LineNumber: 1, Filepath: "foo/a.py", From: "bar"}}, | 
|  | Comments: nil, | 
|  | HasMain:  false, | 
|  | FileName: "a.py", | 
|  | }, *output) | 
|  | } | 
|  |  | 
|  | func TestTypeCheckingImports(t *testing.T) { | 
|  | code := ` | 
|  | import sys | 
|  | from typing import TYPE_CHECKING | 
|  |  | 
|  | if TYPE_CHECKING: | 
|  | import boto3 | 
|  | from rest_framework import serializers | 
|  |  | 
|  | def example_function(): | 
|  | _ = sys.version_info | 
|  | ` | 
|  | p := NewFileParser() | 
|  | p.SetCodeAndFile([]byte(code), "", "test.py") | 
|  |  | 
|  | result, err := p.Parse(context.Background()) | 
|  | if err != nil { | 
|  | t.Fatalf("Failed to parse: %v", err) | 
|  | } | 
|  |  | 
|  | // Check that we found the expected modules | 
|  | expectedModules := map[string]bool{ | 
|  | "sys": false, | 
|  | "typing.TYPE_CHECKING": false, | 
|  | "boto3": true, | 
|  | "rest_framework.serializers": true, | 
|  | } | 
|  |  | 
|  | for _, mod := range result.Modules { | 
|  | if expected, exists := expectedModules[mod.Name]; exists { | 
|  | if mod.TypeCheckingOnly != expected { | 
|  | t.Errorf("Module %s: expected TypeCheckingOnly=%v, got %v", mod.Name, expected, mod.TypeCheckingOnly) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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) | 
|  | }) | 
|  | } |