Coverage for circular_deps/resolvers/typescript.py: 73%

1 from __future__ import annotations

2

3 import os

4

5 from circular_deps.resolvers.base import PathResolver

6 from circular_deps.resolvers.tsconfig import TsconfigParser

7

8

9 class TypeScriptPathResolver(PathResolver):

10 _INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx"]

11

12 def __init__(self, module_resolution=None, filepath=None, root_dir=None):

13 """Initialize TypeScript path resolver with module resolution mode.

14

15 Priority order for determining mode:

16 1. Explicit module_resolution parameter (CLI override)

17 2. tsconfig.json closest to filepath

18 3. tsconfig.json at root_dir

19 4. Default: "node16"

20

21 Args:

22 module_resolution: Explicit mode ("node", "node16", "nodenext", "bundler")

23 filepath: File path to find closest tsconfig.json

24 root_dir: Root directory to search for tsconfig.json

25 """

26 if module_resolution:

27 self.module_resolution = self._normalize_module_resolution(

28 module_resolution

29 )

30 elif filepath:

31 self.module_resolution = self._detect_from_tsconfig(filepath)

32 elif root_dir:

33 self.module_resolution = self._detect_from_tsconfig(root_dir)

34 else:

35 self.module_resolution = "node16"

36 self._validate_module_resolution()

37

38 def _normalize_module_resolution(self, mode: str) -> str:

39 if mode in ("nodenext", "node16"):

40 return "node16"

41 return mode

42

43 def _detect_from_tsconfig(self, path: str) -> str:

44 tsconfig_path = TsconfigParser.find_tsconfig_for_file(path)

45 if not tsconfig_path:

46 return "node16"

47

48 config = TsconfigParser.parse_tsconfig(tsconfig_path)

49 if not config:

50 return "node16"

51

52 mode = TsconfigParser.get_module_resolution(config)

53 return mode or "node16"

54

55 def _validate_module_resolution(self):

56 valid_modes = {"node", "node16", "bundler"}

57 if self.module_resolution not in valid_modes:

58 self.module_resolution = "node16"

59

60 def resolve(self, module, current_file, root_dir):

61 if "node_modules" in module:

62 return None

63

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

65 return self._resolve_relative(module, current_file, root_dir)

66 else:

67 return self._resolve_absolute(module, current_file, root_dir)

68

69 def _resolve_relative(self, module, current_file, root_dir):

70 current_dir = os.path.dirname(current_file)

71 target_path = os.path.abspath(os.path.join(current_dir, module))

72

73 return self._resolve_module_path(module, target_path, root_dir)

74

75 def _resolve_absolute(self, module, current_file, root_dir):

76 module_path = module.replace("/", os.sep)

77

78 current_dir = os.path.dirname(current_file)

79 target_path = os.path.join(current_dir, module_path)

80 resolved = self._resolve_module_path(module, target_path, root_dir)

81 if resolved:

82 return resolved

83

84 target_path = os.path.join(root_dir, module_path)

85 return self._resolve_module_path(module, target_path, root_dir)

86

87 def _resolve_module_path(

88 self, module: str, target_path: str, root_dir: str

89 ) -> str | None:

90 has_extension = self._has_extension(module)

91

92 if self.module_resolution == "node":

93 if has_extension:

94 return self._resolve_node_with_extension(target_path, root_dir)

95 else:

96 return self._resolve_node_without_extension(

97 module, target_path, root_dir

98 )

99 elif self.module_resolution == "node16":

100 if has_extension:

101 return self._resolve_node16_with_extension(

102 module, target_path, root_dir

103 )

104 else:

105 return None

106 elif self.module_resolution == "bundler":

107 if has_extension:

108 return self._resolve_bundler_with_extension(

109 module, target_path, root_dir

110 )

111 else:

112 return self._resolve_bundler_without_extension(

113 module, target_path, root_dir

114 )

115

116 return None

117

118 def _has_extension(self, module_path: str) -> bool:

119 return os.path.splitext(module_path)[1] != ""

120

121 def _strip_extension(self, module_path: str) -> str:

122 base, ext = os.path.splitext(module_path)

123 if ext.lower() in (".ts", ".tsx", ".js", ".jsx"):

124 return base

125 return module_path

126

127 def _resolve_node_with_extension(

128 self, target_path: str, root_dir: str

129 ) -> str | None:

130 if os.path.isfile(target_path):

131 ext = os.path.splitext(target_path)[1]

132 if ext in (".ts", ".tsx", ".js", ".jsx"):

133 return self._check_under_root(target_path, root_dir)

134 return None

135

136 if os.path.isdir(target_path):

137 for index_file in self._INDEX_FILES:

138 index_path = os.path.join(target_path, index_file)

139 if os.path.isfile(index_path):

140 return self._check_under_root(index_path, root_dir)

141

142 dirname = os.path.basename(target_path)

143 for ext in (".ts", ".tsx", ".js", ".jsx"):

144 file_path = os.path.join(target_path, dirname + ext)

145 if os.path.isfile(file_path):

146 return self._check_under_root(file_path, root_dir)

147

148 return None

149

150 def _resolve_node_without_extension(

151 self, module: str, target_path: str, root_dir: str

152 ) -> str | None:

153 if os.path.isdir(target_path) and not module.endswith("/"):

154 dirname = os.path.basename(target_path)

155 for ext in (".ts", ".tsx"):

156 file_path = os.path.join(target_path, dirname + ext)

157 if os.path.isfile(file_path):

158 return self._check_under_root(file_path, root_dir)

159

160 for index_file in self._INDEX_FILES:

161 index_path = os.path.join(target_path, index_file)

162 if os.path.isfile(index_path):

163 return self._check_under_root(index_path, root_dir)

164

165 for ext in (".d.ts", ".ts", ".tsx", ".js", ".jsx"):

166 file_path = target_path + ext

167 if os.path.isfile(file_path):

168 return self._check_under_root(file_path, root_dir)

169

170 return None

171

172 def _resolve_node16_with_extension(

173 self, module: str, target_path: str, root_dir: str

174 ) -> str | None:

175 ext = os.path.splitext(module)[1]

176

177 if os.path.isfile(target_path):

178 target_ext = os.path.splitext(target_path)[1]

179 if target_ext == ext:

180 return self._check_under_root(target_path, root_dir)

181

182 base = self._strip_extension(target_path)

183 for search_ext in (".ts", ".tsx"):

184 file_path = base + search_ext

185 if os.path.isfile(file_path):

186 return self._check_under_root(file_path, root_dir)

187

188 if os.path.isdir(target_path):

189 for index_file in self._INDEX_FILES:

190 index_path = os.path.join(target_path, index_file)

191 if os.path.isfile(index_path):

192 return self._check_under_root(index_path, root_dir)

193

194 return None

195

196 def _resolve_bundler_with_extension(

197 self, module: str, target_path: str, root_dir: str

198 ) -> str | None:

199 ext = os.path.splitext(module)[1]

200

201 if os.path.isfile(target_path):

202 target_ext = os.path.splitext(target_path)[1]

203 if target_ext == ext:

204 return self._check_under_root(target_path, root_dir)

205

206 base = self._strip_extension(target_path)

207 for search_ext in (".ts", ".tsx"):

208 file_path = base + search_ext

209 if os.path.isfile(file_path):

210 return self._check_under_root(file_path, root_dir)

211

212 if os.path.isdir(target_path):

213 for index_file in self._INDEX_FILES:

214 index_path = os.path.join(target_path, index_file)

215 if os.path.isfile(index_path):

216 return self._check_under_root(index_path, root_dir)

217

218 return None

219

220 def _resolve_bundler_without_extension(

221 self, module: str, target_path: str, root_dir: str

222 ) -> str | None:

223 if os.path.isdir(target_path) and not module.endswith("/"):

224 dirname = os.path.basename(target_path)

225 for ext in (".ts", ".tsx", ".js", ".jsx"):

226 file_path = os.path.join(target_path, dirname + ext)

227 if os.path.isfile(file_path):

228 return self._check_under_root(file_path, root_dir)

229

230 for index_file in self._INDEX_FILES:

231 index_path = target_path + "/" + index_file

232 if os.path.isfile(index_path):

233 return self._check_under_root(index_path, root_dir)

234

235 for ext in (".ts", ".tsx", ".js", ".jsx"):

236 file_path = target_path + ext

237 if os.path.isfile(file_path):

238 return self._check_under_root(file_path, root_dir)

239

240 return None

241

242 def _check_under_root(self, path, root_dir):

243 abs_path = os.path.abspath(path)

244 abs_root = os.path.abspath(root_dir)

245 if abs_path.startswith(abs_root):

246 return abs_path

247 return None