Coverage for src\llm_code_lens\formatters\llm.py: 92%

178 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-12 10:23 +0200

1from typing import Dict, List 

2from ..analyzer.base import AnalysisResult 

3 

4def format_analysis(result: AnalysisResult) -> str: 

5 """Format analysis results in an LLM-friendly text format.""" 

6 sections = [] 

7 

8 # Project Overview 

9 sections.extend([ 

10 "CODEBASE SUMMARY:", 

11 f"This project contains {result.summary['project_stats']['total_files']} files:", 

12 "File types: " + ", ".join( 

13 f"{ext}: {count}" 

14 for ext, count in result.summary['project_stats']['by_type'].items() 

15 ), 

16 f"Total lines of code: {result.summary['project_stats']['lines_of_code']}", 

17 f"Average file size: {result.summary['project_stats']['avg_file_size']:.1f} lines", 

18 f"Overall complexity: {sum(f.get('metrics', {}).get('complexity', 0) for f in result.files.values())}", 

19 "", 

20 ]) 

21 

22 # Key Insights 

23 if result.insights: 

24 sections.extend([ 

25 "KEY INSIGHTS:", 

26 *[f"- {insight}" for insight in result.insights], 

27 "", 

28 ]) 

29 

30 # Code Metrics 

31 sections.extend([ 

32 "CODE METRICS:", 

33 f"Functions: {result.summary['code_metrics']['functions']['count']} " 

34 f"({result.summary['code_metrics']['functions']['with_docs']} documented, " 

35 f"{result.summary['code_metrics']['functions']['complex']} complex)", 

36 f"Classes: {result.summary['code_metrics']['classes']['count']} " 

37 f"({result.summary['code_metrics']['classes']['with_docs']} documented)", 

38 f"Documentation coverage: {result.summary['maintenance']['doc_coverage']:.1f}%", 

39 f"Total imports: {result.summary['code_metrics']['imports']['count']} " 

40 f"({len(result.summary['code_metrics']['imports']['unique'])} unique)", 

41 "", 

42 ]) 

43 

44 # Maintenance Info 

45 if result.summary['maintenance']['todos']: 

46 sections.extend([ 

47 "TODOS:", 

48 *[_format_todo(todo) for todo in result.summary['maintenance']['todos']], 

49 "", 

50 ]) 

51 

52 # Structure Info 

53 if result.summary['structure']['entry_points']: 

54 sections.extend([ 

55 "ENTRY POINTS:", 

56 *[f"- {entry}" for entry in result.summary['structure']['entry_points']], 

57 "", 

58 ]) 

59 

60 if result.summary['structure']['core_files']: 

61 sections.extend([ 

62 "CORE FILES:", 

63 *[f"- {file}" for file in result.summary['structure']['core_files']], 

64 "", 

65 ]) 

66 

67 # File Analysis 

68 sections.append("PROJECT STRUCTURE AND CODE INSIGHTS:") 

69 

70 # Group files by directory 

71 by_directory = {} 

72 total_by_dir = {} 

73 for file_path, analysis in result.files.items(): 

74 dir_path = '/'.join(file_path.split('\\')[:-1]) or '.' 

75 if dir_path not in by_directory: 

76 by_directory[dir_path] = {} 

77 total_by_dir[dir_path] = 0 

78 by_directory[dir_path][file_path.split('\\')[-1]] = analysis 

79 total_by_dir[dir_path] += analysis.get('metrics', {}).get('loc', 0) 

80 

81 # Format each directory 

82 for dir_path, files in sorted(by_directory.items()): 

83 sections.extend([ 

84 "", # Empty line before directory 

85 "=" * 80, # Separator line 

86 f"{dir_path}/ ({total_by_dir[dir_path]} lines)", 

87 "=" * 80, 

88 ]) 

89 

90 # Sort files by importance (non-empty before empty) 

91 sorted_files = sorted( 

92 files.items(), 

93 key=lambda x: ( 

94 x[1].get('metrics', {}).get('loc', 0) == 0, 

95 x[0] 

96 ) 

97 ) 

98 

99 for filename, analysis in sorted_files: 

100 # Skip empty files or show them in compact form 

101 if analysis.get('metrics', {}).get('loc', 0) == 0: 

102 sections.append(f" {filename} (empty)") 

103 continue 

104 

105 sections.extend(_format_file_analysis(filename, analysis)) 

106 sections.append("") # Empty line between files 

107 

108 return '\n'.join(sections) 

109 

110def _format_file_analysis(filename: str, analysis: dict) -> List[str]: 

111 """Format file analysis with improved error handling.""" 

112 sections = [f" {filename}"] 

113 metrics = analysis.get('metrics', {}) 

114 

115 # Basic metrics 

116 sections.append(f" Lines: {metrics.get('loc', 0)}") 

117 if 'complexity' in metrics: 

118 sections.append(f" Complexity: {metrics['complexity']}") 

119 

120 # Handle errors with standardized format 

121 if analysis.get('errors'): 

122 sections.append("\n ERRORS:") 

123 for error in analysis['errors']: 

124 if 'line' in error: 

125 sections.append(f" Line {error['line']}: {error['text']}") 

126 else: 

127 sections.append(f" {error['type'].replace('_', ' ').title()}: {error['text']}") 

128 

129 # Type-specific information 

130 if analysis['type'] == 'python': 

131 sections.extend(_format_python_file(analysis)) 

132 elif analysis['type'] == 'sql': 

133 sections.extend(_format_sql_file(analysis)) 

134 elif analysis['type'] == 'javascript': 

135 sections.extend(_format_js_file(analysis)) 

136 

137 # Format imports 

138 if analysis.get('imports'): 

139 sections.append("\n IMPORTS:") 

140 sections.extend(f" {imp}" for imp in sorted(analysis['imports'])) 

141 

142 # Format TODOs 

143 if analysis.get('todos'): 

144 sections.append("\n TODOS:") 

145 for todo in sorted(analysis['todos'], key=lambda x: x['line']): 

146 sections.append(f" Line {todo['line']}: {todo['text']}") 

147 

148 return sections 

149 

150def _format_python_file(analysis: dict) -> List[str]: 

151 """Format Python-specific file information with better method grouping.""" 

152 sections = [] 

153 

154 if analysis.get('classes'): 

155 sections.append('\nCLASSES:') 

156 for cls in sorted(analysis['classes'], key=lambda x: x.get('line_number', 0)): 

157 sections.append(f" {cls['name']}:") 

158 if 'line_number' in cls: 

159 sections.append(f" Line: {cls['line_number']}") 

160 if cls.get('bases'): 

161 sections.append(f" Inherits: {', '.join(cls['bases'])}") 

162 if cls.get('docstring'): 

163 sections.append(f" Doc: {cls['docstring'].split(chr(10))[0]}") 

164 

165 # Handle different method types 

166 if cls.get('methods'): 

167 methods = cls['methods'] 

168 if isinstance(methods[0], dict): 

169 # Group methods by type 

170 instance_methods = [] 

171 class_methods = [] 

172 static_methods = [] 

173 property_methods = [] 

174 

175 for method in methods: 

176 if method.get('type') == 'class' or method.get('is_classmethod'): 

177 class_methods.append(method['name']) 

178 elif method.get('type') == 'static' or method.get('is_staticmethod'): 

179 static_methods.append(method['name']) 

180 elif method.get('type') == 'property' or method.get('is_property'): 

181 property_methods.append(method['name']) 

182 else: 

183 instance_methods.append(method['name']) 

184 

185 # Add each method type if present 

186 if instance_methods: 

187 sections.append(f" Instance methods: {', '.join(instance_methods)}") 

188 if class_methods: 

189 sections.append(f" Class methods: {', '.join(class_methods)}") 

190 if static_methods: 

191 sections.append(f" Static methods: {', '.join(static_methods)}") 

192 if property_methods: 

193 sections.append(f" Properties: {', '.join(property_methods)}") 

194 else: 

195 # Handle simple string method list 

196 sections.append(f" Methods: {', '.join(methods)}") 

197 

198 if analysis.get('functions'): 

199 sections.append('\nFUNCTIONS:') 

200 for func in sorted(analysis['functions'], key=lambda x: x.get('line_number', 0)): 

201 sections.append(f" {func['name']}:") 

202 if 'line_number' in func: 

203 sections.append(f" Line: {func['line_number']}") 

204 if func.get('args'): 

205 # Handle both string and dict arguments 

206 args_list = [] 

207 for arg in func['args']: 

208 if isinstance(arg, dict): 

209 arg_str = f"{arg['name']}: {arg['type']}" if 'type' in arg else arg['name'] 

210 args_list.append(arg_str) 

211 else: 

212 args_list.append(arg) 

213 sections.append(f" Args: {', '.join(args_list)}") 

214 if func.get('return_type'): 

215 sections.append(f" Returns: {func['return_type']}") 

216 if func.get('docstring'): 

217 sections.append(f" Doc: {func['docstring'].split(chr(10))[0]}") 

218 if func.get('decorators'): 

219 sections.append(f" Decorators: {', '.join(func['decorators'])}") 

220 if func.get('complexity'): 

221 sections.append(f" Complexity: {func['complexity']}") 

222 if func.get('is_async'): 

223 sections.append(" Async: Yes") 

224 

225 return sections 

226 

227 

228def _format_sql_file(analysis: dict) -> List[str]: 

229 """Format SQL-specific file information with enhanced object handling.""" 

230 sections = [] 

231 

232 def format_metrics(obj: Dict) -> List[str]: 

233 """Helper to format metrics consistently.""" 

234 result = [] 

235 if 'lines' in obj.get('metrics', {}): 

236 result.append(f" Lines: {obj['metrics']['lines']}") 

237 if 'complexity' in obj.get('metrics', {}): 

238 result.append(f" Complexity: {obj['metrics']['complexity']}") 

239 return result 

240 

241 # Format SQL objects 

242 for obj in sorted(analysis.get('objects', []), key=lambda x: x['name']): 

243 sections.extend([ 

244 f"\n {obj['type'].upper()}:", 

245 f" Name: {obj['name']}" 

246 ]) 

247 sections.extend(format_metrics(obj)) 

248 

249 # Format parameters with improved handling 

250 if analysis.get('parameters'): 

251 sections.append("\n PARAMETERS:") 

252 for param in sorted(analysis['parameters'], key=lambda x: x['name']): 

253 param_text = f" @{param['name']} ({param['data_type']}" 

254 if 'default' in param: 

255 param_text += f", default={param['default']}" 

256 param_text += ")" 

257 if 'description' in param: 

258 param_text += f" -- {param['description']}" 

259 sections.append(param_text) 

260 

261 # Format dependencies 

262 if analysis.get('dependencies'): 

263 sections.append("\n DEPENDENCIES:") 

264 sections.extend(f" {dep}" for dep in sorted(analysis['dependencies'])) 

265 

266 # Format comments 

267 if analysis.get('comments'): 

268 sections.append("\n COMMENTS:") 

269 for comment in sorted(analysis['comments'], key=lambda x: x['line']): 

270 sections.append(f" Line {comment['line']}: {comment['text']}") 

271 

272 return sections 

273 

274 

275 

276def _format_js_file(analysis: dict) -> List[str]: 

277 """Format JavaScript-specific file information.""" 

278 sections = [] 

279 

280 if analysis.get('imports'): 

281 sections.append('\n IMPORTS:') 

282 sections.extend(f' {imp}' for imp in sorted(analysis['imports'])) 

283 

284 if analysis.get('exports'): 

285 sections.append('\n EXPORTS:') 

286 sections.extend(f' {exp}' for exp in sorted(analysis['exports'])) 

287 

288 if analysis.get('classes'): 

289 sections.append('\n CLASSES:') 

290 for cls in analysis['classes']: 

291 sections.extend([ 

292 f' {cls["name"]}:' 

293 ]) 

294 if 'line_number' in cls: 

295 sections.extend([f' Line: {cls["line_number"]}']) 

296 if cls.get('extends'): 

297 sections.extend([f' Extends: {cls["extends"]}']) 

298 if cls.get('methods'): 

299 sections.extend([f' Methods: {", ".join(cls["methods"])}']) 

300 

301 if analysis.get('functions'): 

302 sections.append("\n FUNCTIONS:") 

303 for func in analysis['functions']: 

304 sections.extend([ 

305 f" {func['name']}:", 

306 f" Line: {func['line_number']}" 

307 ]) 

308 if func.get('params'): 

309 sections.append(f" Parameters: {', '.join(func['params'])}") 

310 

311 return sections 

312 

313def _format_todo(todo: dict) -> str: 

314 """Format a TODO entry.""" 

315 return f"- [{todo['priority']}] {todo['file']}: {todo['text']}"