Coverage for circular_deps / parsers / typescript.py: 72%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 15:04 -0800

1from __future__ import annotations 

2 

3import tree_sitter_typescript as tstypescript 

4from tree_sitter import Language 

5from tree_sitter import Parser 

6 

7from circular_deps.models import ImportInfo 

8from circular_deps.parsers.base import ImportParser 

9 

10TS_LANGUAGE = Language(tstypescript.language_typescript()) 

11TSX_LANGUAGE = Language(tstypescript.language_tsx()) 

12 

13_IMPORT_NODE_TYPES = {"import_statement", "export_statement"} 

14 

15 

16def parse(code, tsx=False): 

17 lang = TSX_LANGUAGE if tsx else TS_LANGUAGE 

18 parser_obj = Parser(lang) 

19 tree = parser_obj.parse(code.encode()) 

20 return tree.root_node 

21 

22 

23class TypeScriptImportParser(ImportParser): 

24 def extract_imports(self, source, filepath): 

25 tsx = filepath.endswith(".tsx") or filepath.endswith(".jsx") 

26 root = parse(source, tsx=tsx) 

27 imports = [] 

28 

29 for node in root.children: 

30 if node.type in _IMPORT_NODE_TYPES: 

31 imports.extend(self._process_import_export_statement(node)) 

32 

33 return self._filter_dynamic(imports, root) 

34 

35 def _process_import_export_statement(self, node): 

36 imports = [] 

37 

38 for child in node.children: 

39 if child.type == "string": 

40 module = child.text.decode().strip("\"'") 

41 if module: 

42 imports.append( 

43 ImportInfo( 

44 raw_module=module, 

45 resolved_path=None, 

46 import_type=self._detect_import_type(module), 

47 line=node.start_point[0] + 1, 

48 is_dynamic=False, 

49 node_type=node.type, 

50 ) 

51 ) 

52 

53 return imports 

54 

55 def _detect_import_type(self, module): 

56 if module.startswith(".") or module.startswith(".."): 

57 return "relative" 

58 return "absolute" 

59 

60 def _filter_dynamic(self, imports, root): 

61 for node in root.children: 

62 if node.type == "expression_statement": 

63 for expr in node.children: 

64 if expr.type == "call": 

65 if self._is_dynamic_import_call(expr): 

66 line = node.start_point[0] + 1 

67 for imp in imports: 

68 if imp.line == line: 

69 imp.is_dynamic = True 

70 

71 return [imp for imp in imports if not imp.is_dynamic] 

72 

73 def _is_dynamic_import_call(self, call_node): 

74 for child in call_node.children: 

75 if child.type == "import": 

76 return True 

77 for gc in child.children: 

78 if gc.type == "import": 

79 return True 

80 return False