Coverage for circular_deps/resolvers/tsconfig.py: 89%

1 from __future__ import annotations

2

3 import json

4 import os

5 from pathlib import Path

6

7

8 VALID_MODULE_RESOLUTIONS = {"node", "node16", "nodenext", "bundler"}

9

10

11 class TsconfigParser:

12 """Parse TypeScript tsconfig.json files to extract module resolution settings."""

13

14 @staticmethod

15 def find_tsconfig_for_file(filepath: str | Path) -> str | None:

16 """Find tsconfig.json closest to given file by walking up directory tree.

17

18 Args:

19 filepath: Path to a file (directory will be checked then walked up)

20

21 Returns:

22 Absolute path to closest tsconfig.json, or None if not found

23 """

24 filepath = Path(filepath).resolve()

25

26 # If filepath is a file, start from its directory

27 if filepath.is_file():

28 search_dir = filepath.parent

29 else:

30 search_dir = filepath

31

32 # Walk up the directory tree looking for tsconfig.json

33 current_dir = search_dir

34 while True:

35 tsconfig_path = current_dir / "tsconfig.json"

36 if tsconfig_path.exists() and tsconfig_path.is_file():

37 return str(tsconfig_path.absolute())

38

39 parent = current_dir.parent

40 if parent == current_dir:

41 # Reached root

42 break

43 current_dir = parent

44

45 return None

46

47 @staticmethod

48 def parse_tsconfig(filepath: str | Path) -> dict | None:

49 """Parse tsconfig.json and return dict.

50

51 Silent error handling: returns None for invalid JSON or parse errors.

52

53 Args:

54 filepath: Path to tsconfig.json file

55

56 Returns:

57 Parsed config dict, or None if file cannot be parsed

58 """

59 try:

60 with open(filepath, "r", encoding="utf-8") as f:

61 content = f.read()

62 # Basic JSON parsing (no comments support - standard JSON only)

63 config = json.loads(content)

64 return config

65 except (json.JSONDecodeError, IOError, OSError):

66 return None

67

68 @staticmethod

69 def get_module_resolution(config: dict) -> str | None:

70 """Extract compilerOptions.moduleResolution from tsconfig config.

71

72 Returns only valid values. Returns None for invalid or missing values.

73

74 Args:

75 config: Parsed tsconfig dict

76

77 Returns:

78 One of: "node", "node16", "nodenext", "bundler", or None

79 """

80 if not isinstance(config, dict):

81 return None

82

83 compiler_options = config.get("compilerOptions")

84 if not isinstance(compiler_options, dict):

85 return None

86

87 module_resolution = compiler_options.get("moduleResolution")

88 if not isinstance(module_resolution, str):

89 return None

90

91 # Return only valid values

92 if module_resolution in VALID_MODULE_RESOLUTIONS:

93 return module_resolution

94

95 return None