code2llm 0.5.49__tar.gz → 0.5.50__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {code2llm-0.5.49 → code2llm-0.5.50}/PKG-INFO +3 -3
  2. {code2llm-0.5.49 → code2llm-0.5.50}/README.md +2 -2
  3. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/__init__.py +1 -1
  4. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_analyzer.py +214 -16
  5. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/__init__.py +1 -1
  6. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/PKG-INFO +3 -3
  7. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/SOURCES.txt +1 -0
  8. {code2llm-0.5.49 → code2llm-0.5.50}/pyproject.toml +1 -1
  9. code2llm-0.5.50/tests/test_nonpython_cc_calls.py +312 -0
  10. {code2llm-0.5.49 → code2llm-0.5.50}/LICENSE +0 -0
  11. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/__main__.py +0 -0
  12. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/__init__.py +0 -0
  13. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/call_graph.py +0 -0
  14. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/cfg.py +0 -0
  15. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/coupling.py +0 -0
  16. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/data_analysis.py +0 -0
  17. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/dfg.py +0 -0
  18. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/pipeline_detector.py +0 -0
  19. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/side_effects.py +0 -0
  20. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/smells.py +0 -0
  21. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/type_inference.py +0 -0
  22. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/api.py +0 -0
  23. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli.py +0 -0
  24. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_analysis.py +0 -0
  25. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/__init__.py +0 -0
  26. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/code2logic.py +0 -0
  27. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/formats.py +0 -0
  28. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/orchestrator.py +0 -0
  29. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/prompt.py +0 -0
  30. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/__init__.py +0 -0
  31. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/analyzer.py +0 -0
  32. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/config.py +0 -0
  33. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/__init__.py +0 -0
  34. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_cache.py +0 -0
  35. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_filter.py +0 -0
  36. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/refactoring.py +0 -0
  37. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/large_repo.py +0 -0
  38. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/models.py +0 -0
  39. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/__init__.py +0 -0
  40. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/cache.py +0 -0
  41. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/incremental.py +0 -0
  42. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/prioritizer.py +0 -0
  43. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/scanner.py +0 -0
  44. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/strategies.py +0 -0
  45. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming_analyzer.py +0 -0
  46. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/toon_size_manager.py +0 -0
  47. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/__init__.py +0 -0
  48. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/article_view.py +0 -0
  49. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/base.py +0 -0
  50. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/context_exporter.py +0 -0
  51. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/context_view.py +0 -0
  52. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/evolution_exporter.py +0 -0
  53. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_constants.py +0 -0
  54. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_exporter.py +0 -0
  55. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_renderer.py +0 -0
  56. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/html_dashboard.py +0 -0
  57. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/json_exporter.py +0 -0
  58. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/llm_exporter.py +0 -0
  59. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/map_exporter.py +0 -0
  60. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/mermaid_exporter.py +0 -0
  61. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/project_yaml_exporter.py +0 -0
  62. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/readme_exporter.py +0 -0
  63. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/report_generators.py +0 -0
  64. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/__init__.py +0 -0
  65. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/helpers.py +0 -0
  66. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/metrics.py +0 -0
  67. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/module_detail.py +0 -0
  68. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/renderer.py +0 -0
  69. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon.py +0 -0
  70. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon_view.py +0 -0
  71. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/validate_project.py +0 -0
  72. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/yaml_exporter.py +0 -0
  73. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/__init__.py +0 -0
  74. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/llm_flow.py +0 -0
  75. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/llm_task.py +0 -0
  76. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/mermaid.py +0 -0
  77. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/config.py +0 -0
  78. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/entity_resolution.py +0 -0
  79. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/intent_matching.py +0 -0
  80. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/normalization.py +0 -0
  81. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/pipeline.py +0 -0
  82. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/patterns/__init__.py +0 -0
  83. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/patterns/detector.py +0 -0
  84. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/refactor/__init__.py +0 -0
  85. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/refactor/prompt_engine.py +0 -0
  86. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/dependency_links.txt +0 -0
  87. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/entry_points.txt +0 -0
  88. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/requires.txt +0 -0
  89. {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/top_level.txt +0 -0
  90. {code2llm-0.5.49 → code2llm-0.5.50}/setup.cfg +0 -0
  91. {code2llm-0.5.49 → code2llm-0.5.50}/setup.py +0 -0
  92. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_advanced_analysis.py +0 -0
  93. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_analyzer.py +0 -0
  94. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_deep_analysis.py +0 -0
  95. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_edge_cases.py +0 -0
  96. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_flow_exporter.py +0 -0
  97. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_format_quality.py +0 -0
  98. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_multilanguage_e2e.py +0 -0
  99. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_nlp_pipeline.py +0 -0
  100. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_pipeline_detector.py +0 -0
  101. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_prompt_engine.py +0 -0
  102. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_prompt_txt.py +0 -0
  103. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_refactoring_engine.py +0 -0
  104. {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_toon_v2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.49
3
+ Version: 0.5.50
4
4
  Summary: High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries
5
5
  Home-page: https://github.com/wronai/stts
6
6
  Author: STTS Project
@@ -60,7 +60,7 @@ When you run `code2llm ./ -f all`, the following files are created:
60
60
 
61
61
  | File | Format | Purpose | Key Insights |
62
62
  |------|--------|---------|--------------|
63
- | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 59 critical functions, 0 god modules |
63
+ | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 61 critical functions, 0 god modules |
64
64
  | `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
65
65
 
66
66
  ### 🤖 LLM-Ready Documentation
@@ -375,7 +375,7 @@ code2llm ./ -f yaml --separate-orphans
375
375
 
376
376
  **Generated by**: `code2llm ./ -f all --readme`
377
377
  **Analysis Date**: 2026-03-09
378
- **Total Functions**: 854
378
+ **Total Functions**: 857
379
379
  **Total Classes**: 104
380
380
  **Modules**: 105
381
381
 
@@ -10,7 +10,7 @@ When you run `code2llm ./ -f all`, the following files are created:
10
10
 
11
11
  | File | Format | Purpose | Key Insights |
12
12
  |------|--------|---------|--------------|
13
- | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 59 critical functions, 0 god modules |
13
+ | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 61 critical functions, 0 god modules |
14
14
  | `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
15
15
 
16
16
  ### 🤖 LLM-Ready Documentation
@@ -325,7 +325,7 @@ code2llm ./ -f yaml --separate-orphans
325
325
 
326
326
  **Generated by**: `code2llm ./ -f all --readme`
327
327
  **Analysis Date**: 2026-03-09
328
- **Total Functions**: 854
328
+ **Total Functions**: 857
329
329
  **Total Classes**: 104
330
330
  **Modules**: 105
331
331
 
@@ -8,7 +8,7 @@ Includes NLP Processing Pipeline for query normalization, intent matching,
8
8
  and entity resolution with multilingual support.
9
9
  """
10
10
 
11
- __version__ = "0.5.49"
11
+ __version__ = "0.5.50"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -140,6 +140,120 @@ class FileAnalyzer:
140
140
  if self.config.verbose:
141
141
  print(f"Error calculating complexity for {file_path}: {e}")
142
142
 
143
+ # ------------------------------------------------------------------
144
+ # Universal regex-based complexity & call extraction (non-Python)
145
+ # ------------------------------------------------------------------
146
+
147
+ # Branching keywords per language family
148
+ _CC_PATTERNS = {
149
+ 'c_family': re.compile(
150
+ r'\b(?:if|else\s+if|for|while|do|switch|case|catch|&&|\|\||\?\?|\?\.)\b'
151
+ r'|\?\s*[^:]*\s*:' # ternary
152
+ ),
153
+ 'go': re.compile(
154
+ r'\b(?:if|for|switch|case|select|go|defer)\b'
155
+ r'|&&|\|\|'
156
+ ),
157
+ 'rust': re.compile(
158
+ r'\b(?:if|else\s+if|for|while|loop|match|=>)\b'
159
+ r'|&&|\|\||\?'
160
+ ),
161
+ }
162
+
163
+ @staticmethod
164
+ def _extract_function_body(content: str, start_line: int) -> str:
165
+ """Extract the body of a function between braces from a start line (1-indexed)."""
166
+ lines = content.split('\n')
167
+ if start_line < 1 or start_line > len(lines):
168
+ return ''
169
+ depth = 0
170
+ body_lines = []
171
+ started = False
172
+ for line in lines[start_line - 1:]:
173
+ for ch in line:
174
+ if ch == '{':
175
+ depth += 1
176
+ started = True
177
+ elif ch == '}':
178
+ depth -= 1
179
+ if started:
180
+ body_lines.append(line)
181
+ if started and depth <= 0:
182
+ break
183
+ return '\n'.join(body_lines)
184
+
185
+ def _calculate_complexity_regex(self, content: str, result: Dict,
186
+ lang: str = 'c_family') -> None:
187
+ """Estimate cyclomatic complexity for every function using regex keyword counting."""
188
+ pattern = self._CC_PATTERNS.get(lang, self._CC_PATTERNS['c_family'])
189
+ for func_info in result['functions'].values():
190
+ body = self._extract_function_body(content, func_info.line)
191
+ if not body:
192
+ cc = 1
193
+ else:
194
+ cc = 1 + len(pattern.findall(body))
195
+ rank = 'A' if cc <= 5 else ('B' if cc <= 10 else ('C' if cc <= 20 else 'D'))
196
+ func_info.complexity = {
197
+ 'cyclomatic_complexity': cc,
198
+ 'cc_rank': rank,
199
+ }
200
+
201
+ _CALL_PATTERN_C_FAMILY = re.compile(
202
+ r'(?<!\bfunction\b\s)' # not a function declaration
203
+ r'(?<!\bclass\b\s)' # not a class declaration
204
+ r'\b([a-zA-Z_]\w*)\s*\(' # simple call: foo(
205
+ r'|'
206
+ r'(?:this|self)\s*\.\s*(\w+)\s*\(' # this.method( / self.method(
207
+ r'|'
208
+ r'\b(\w+)\s*\.\s*(\w+)\s*\(' # obj.method(
209
+ )
210
+
211
+ def _extract_calls_regex(self, content: str, module_name: str, result: Dict) -> None:
212
+ """Extract function calls from function bodies using regex."""
213
+ # Build set of known function simple names for resolution
214
+ known_simple: Dict[str, List[str]] = {}
215
+ for qname in result['functions']:
216
+ simple = qname.rsplit('.', 1)[-1]
217
+ known_simple.setdefault(simple, []).append(qname)
218
+
219
+ for func_qname, func_info in result['functions'].items():
220
+ body = self._extract_function_body(content, func_info.line)
221
+ if not body:
222
+ continue
223
+ calls_seen = set()
224
+ for m in self._CALL_PATTERN_C_FAMILY.finditer(body):
225
+ simple_call = m.group(1) or m.group(2) or m.group(4)
226
+ if not simple_call:
227
+ continue
228
+ # Skip language keywords
229
+ if simple_call in ('if', 'for', 'while', 'switch', 'catch',
230
+ 'return', 'throw', 'new', 'typeof', 'instanceof',
231
+ 'import', 'export', 'require', 'console',
232
+ 'super', 'class', 'function', 'async', 'await',
233
+ 'delete', 'void', 'case', 'default'):
234
+ continue
235
+ # Resolve to known qualified name
236
+ if simple_call in known_simple:
237
+ candidates = known_simple[simple_call]
238
+ # Prefer same-module match
239
+ resolved = None
240
+ my_module = func_qname.rsplit('.', 1)[0]
241
+ for cand in candidates:
242
+ if cand.rsplit('.', 1)[0] == my_module:
243
+ resolved = cand
244
+ break
245
+ if resolved is None:
246
+ resolved = candidates[0]
247
+ if resolved != func_qname and resolved not in calls_seen:
248
+ func_info.calls.append(resolved)
249
+ calls_seen.add(resolved)
250
+ else:
251
+ # External call — store as module.name for downstream
252
+ ext_name = f"{module_name}.{simple_call}"
253
+ if ext_name not in calls_seen:
254
+ func_info.calls.append(ext_name)
255
+ calls_seen.add(ext_name)
256
+
143
257
  def _perform_deep_analysis(self, tree: ast.AST, module_name: str, file_path: str, result: Dict) -> None:
144
258
  """Perform deep analysis including DFG and call graph extraction."""
145
259
  try:
@@ -397,22 +511,48 @@ class FileAnalyzer:
397
511
 
398
512
  # Patterns for TypeScript/JavaScript
399
513
  import_pattern = re.compile(r"^\s*import\s+.*?\s+from\s+['\"]([^'\"]+)['\"]")
400
- class_pattern = re.compile(r"^\s*(?:export\s+)?(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?")
514
+ class_pattern = re.compile(r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?")
401
515
  func_pattern = re.compile(r"^\s*(?:export\s+)?(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?[\(\w])")
402
516
  interface_pattern = re.compile(r"^\s*(?:export\s+)?interface\s+(\w+)")
517
+ # Class method patterns: shorthand methods, getters, setters, arrow props
518
+ method_pattern = re.compile(
519
+ r"^\s*(?:(?:public|private|protected|static|readonly|abstract|async|override)\s+)*"
520
+ r"(?:get\s+|set\s+)?"
521
+ r"(\w+)\s*(?:<[^>]*>)?\s*\([^)]*\)"
522
+ )
523
+ arrow_prop_pattern = re.compile(
524
+ r"^\s*(?:(?:public|private|protected|static|readonly)\s+)*"
525
+ r"(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>"
526
+ )
403
527
 
404
528
  current_class = None
405
- current_class_line = 0
529
+ class_brace_depth = 0
530
+ brace_depth = 0
406
531
 
407
532
  for line_no, line in enumerate(lines, 1):
533
+ raw_line = line
408
534
  line = line.strip()
409
535
  if not line or line.startswith('//') or line.startswith('/*') or line.startswith('*'):
536
+ # Still track braces in comments? No — skip them
410
537
  continue
411
538
 
539
+ # Track brace depth for class scope
540
+ for ch in raw_line:
541
+ if ch == '{':
542
+ brace_depth += 1
543
+ elif ch == '}':
544
+ brace_depth -= 1
545
+
546
+ # End of class scope
547
+ if current_class and brace_depth < class_brace_depth:
548
+ current_class = None
549
+ class_brace_depth = 0
550
+
412
551
  # Imports
413
552
  import_match = import_pattern.match(line)
414
553
  if import_match:
415
554
  result['module'].imports.append(import_match.group(1))
555
+ continue
416
556
 
417
557
  # Classes
418
558
  class_match = class_pattern.match(line)
@@ -438,7 +578,8 @@ class FileAnalyzer:
438
578
  result['module'].classes.append(qualified_name)
439
579
  self.stats['classes_found'] += 1
440
580
  current_class = qualified_name
441
- current_class_line = line_no
581
+ class_brace_depth = brace_depth
582
+ continue
442
583
 
443
584
  # Interfaces (treat as classes)
444
585
  interface_match = interface_pattern.match(line)
@@ -457,21 +598,14 @@ class FileAnalyzer:
457
598
  )
458
599
  result['module'].classes.append(qualified_name)
459
600
  self.stats['classes_found'] += 1
601
+ continue
460
602
 
461
- # Functions
603
+ # Standalone functions (function declarations & const/let/var arrows)
462
604
  func_match = func_pattern.match(line)
463
- if func_match:
605
+ if func_match and not current_class:
464
606
  func_name = func_match.group(1) or func_match.group(2)
465
607
  if func_name:
466
- if current_class and line_no > current_class_line:
467
- # Method
468
- method_name = func_name
469
- qualified_name = f"{current_class}.{method_name}"
470
- result['classes'][current_class].methods.append(qualified_name)
471
- else:
472
- # Standalone function
473
- qualified_name = f"{module_name}.{func_name}"
474
-
608
+ qualified_name = f"{module_name}.{func_name}"
475
609
  result['functions'][qualified_name] = FunctionInfo(
476
610
  name=func_name,
477
611
  qualified_name=qualified_name,
@@ -479,8 +613,8 @@ class FileAnalyzer:
479
613
  line=line_no,
480
614
  column=0,
481
615
  module=module_name,
482
- class_name=current_class.split('.')[-1] if current_class else None,
483
- is_method=current_class is not None,
616
+ class_name=None,
617
+ is_method=False,
484
618
  is_private=func_name.startswith('_'),
485
619
  is_property=False,
486
620
  docstring="",
@@ -489,6 +623,58 @@ class FileAnalyzer:
489
623
  )
490
624
  result['module'].functions.append(qualified_name)
491
625
  self.stats['functions_found'] += 1
626
+ continue
627
+
628
+ # Class members: methods (shorthand syntax) & arrow property methods
629
+ if current_class:
630
+ _matched_method = False
631
+ # Arrow property: myMethod = (args) => { ... }
632
+ arrow_match = arrow_prop_pattern.match(line)
633
+ if arrow_match:
634
+ method_name = arrow_match.group(1)
635
+ if method_name not in ('constructor', 'if', 'for', 'while', 'switch', 'return'):
636
+ _matched_method = True
637
+
638
+ # Shorthand method: myMethod(args) { ... }
639
+ if not _matched_method:
640
+ meth_match = method_pattern.match(line)
641
+ if meth_match:
642
+ method_name = meth_match.group(1)
643
+ if method_name not in ('if', 'for', 'while', 'switch', 'return',
644
+ 'catch', 'class', 'import', 'export', 'new'):
645
+ _matched_method = True
646
+
647
+ # Also catch const/let/var arrows inside class (rare but valid)
648
+ if not _matched_method and func_match:
649
+ fname = func_match.group(1) or func_match.group(2)
650
+ if fname:
651
+ method_name = fname
652
+ _matched_method = True
653
+
654
+ if _matched_method:
655
+ qualified_name = f"{current_class}.{method_name}"
656
+ result['classes'][current_class].methods.append(qualified_name)
657
+ result['functions'][qualified_name] = FunctionInfo(
658
+ name=method_name,
659
+ qualified_name=qualified_name,
660
+ file=file_path,
661
+ line=line_no,
662
+ column=0,
663
+ module=module_name,
664
+ class_name=current_class.split('.')[-1],
665
+ is_method=True,
666
+ is_private=method_name.startswith('_') or method_name.startswith('#'),
667
+ is_property=False,
668
+ docstring="",
669
+ args=[],
670
+ decorators=[],
671
+ )
672
+ result['module'].functions.append(qualified_name)
673
+ self.stats['functions_found'] += 1
674
+
675
+ # Regex-based complexity estimation and call extraction
676
+ self._calculate_complexity_regex(content, result, lang='c_family')
677
+ self._extract_calls_regex(content, module_name, result)
492
678
 
493
679
  self.stats['files_processed'] += 1
494
680
  return result
@@ -566,6 +752,10 @@ class FileAnalyzer:
566
752
  result['module'].classes.append(qualified_name)
567
753
  self.stats['classes_found'] += 1
568
754
 
755
+ # Regex-based complexity estimation and call extraction
756
+ self._calculate_complexity_regex(content, result, lang='go')
757
+ self._extract_calls_regex(content, module_name, result)
758
+
569
759
  self.stats['files_processed'] += 1
570
760
  return result
571
761
 
@@ -649,6 +839,10 @@ class FileAnalyzer:
649
839
  result['module'].classes.append(qualified_name)
650
840
  self.stats['classes_found'] += 1
651
841
 
842
+ # Regex-based complexity estimation and call extraction
843
+ self._calculate_complexity_regex(content, result, lang='rust')
844
+ self._extract_calls_regex(content, module_name, result)
845
+
652
846
  self.stats['files_processed'] += 1
653
847
  return result
654
848
 
@@ -734,6 +928,10 @@ class FileAnalyzer:
734
928
  result['classes'][current_class].methods.append(qualified_name)
735
929
  self.stats['functions_found'] += 1
736
930
 
931
+ # Regex-based complexity estimation and call extraction
932
+ self._calculate_complexity_regex(content, result, lang='c_family')
933
+ self._extract_calls_regex(content, module_name, result)
934
+
737
935
  self.stats['files_processed'] += 1
738
936
  return result
739
937
 
@@ -4,7 +4,7 @@ Provides query normalization, intent matching, and entity resolution
4
4
  with multilingual support and fuzzy matching.
5
5
  """
6
6
 
7
- __version__ = "0.5.49"
7
+ __version__ = "0.5.50"
8
8
 
9
9
  from .pipeline import NLPPipeline
10
10
  from .normalization import QueryNormalizer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.49
3
+ Version: 0.5.50
4
4
  Summary: High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries
5
5
  Home-page: https://github.com/wronai/stts
6
6
  Author: STTS Project
@@ -60,7 +60,7 @@ When you run `code2llm ./ -f all`, the following files are created:
60
60
 
61
61
  | File | Format | Purpose | Key Insights |
62
62
  |------|--------|---------|--------------|
63
- | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 59 critical functions, 0 god modules |
63
+ | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 61 critical functions, 0 god modules |
64
64
  | `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
65
65
 
66
66
  ### 🤖 LLM-Ready Documentation
@@ -375,7 +375,7 @@ code2llm ./ -f yaml --separate-orphans
375
375
 
376
376
  **Generated by**: `code2llm ./ -f all --readme`
377
377
  **Analysis Date**: 2026-03-09
378
- **Total Functions**: 854
378
+ **Total Functions**: 857
379
379
  **Total Classes**: 104
380
380
  **Modules**: 105
381
381
 
@@ -94,6 +94,7 @@ tests/test_flow_exporter.py
94
94
  tests/test_format_quality.py
95
95
  tests/test_multilanguage_e2e.py
96
96
  tests/test_nlp_pipeline.py
97
+ tests/test_nonpython_cc_calls.py
97
98
  tests/test_pipeline_detector.py
98
99
  tests/test_prompt_engine.py
99
100
  tests/test_prompt_txt.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2llm"
7
- version = "0.5.49"
7
+ version = "0.5.50"
8
8
  description = "High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,312 @@
1
+ """Regression tests for non-Python cyclomatic complexity and call graph extraction.
2
+
3
+ Verifies that TypeScript, JavaScript, Go, Rust, and Java files get proper
4
+ CC values (not all zeros) and call graph edges extracted.
5
+ """
6
+
7
+ import tempfile
8
+ from pathlib import Path
9
+ from code2llm.core.analyzer import ProjectAnalyzer
10
+ from code2llm.core.config import Config, FAST_CONFIG
11
+
12
+
13
+ class TestTypeScriptComplexityAndCalls:
14
+ """Verify TS/JS files get CC > 0 and call edges."""
15
+
16
+ TS_CODE = """\
17
+ import { Logger } from './logger';
18
+
19
+ class UserService {
20
+ private users: any[] = [];
21
+
22
+ constructor() {
23
+ this.users = [];
24
+ }
25
+
26
+ addUser(user: any): void {
27
+ if (user && user.name) {
28
+ if (user.age > 0) {
29
+ this.users.push(user);
30
+ } else {
31
+ throw new Error('invalid age');
32
+ }
33
+ }
34
+ }
35
+
36
+ findUser(id: number): any {
37
+ for (const u of this.users) {
38
+ if (u.id === id) {
39
+ return u;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ processAll(): void {
46
+ for (const u of this.users) {
47
+ if (u.active || u.admin) {
48
+ this.notify(u);
49
+ }
50
+ }
51
+ }
52
+
53
+ notify(user: any): void {
54
+ console.log(user.name);
55
+ }
56
+ }
57
+
58
+ function initService(): UserService {
59
+ const svc = new UserService();
60
+ return svc;
61
+ }
62
+
63
+ export function main(): void {
64
+ const svc = initService();
65
+ if (svc) {
66
+ svc.processAll();
67
+ }
68
+ }
69
+ """
70
+
71
+ def _analyze(self):
72
+ with tempfile.TemporaryDirectory() as tmpdir:
73
+ ts_file = Path(tmpdir) / "user_service.ts"
74
+ ts_file.write_text(self.TS_CODE)
75
+ analyzer = ProjectAnalyzer(FAST_CONFIG)
76
+ return analyzer.analyze_project(tmpdir)
77
+
78
+ def test_ts_functions_detected(self):
79
+ result = self._analyze()
80
+ func_names = {f.name for f in result.functions.values()}
81
+ # Should detect class methods (shorthand syntax) + standalone functions
82
+ assert 'addUser' in func_names, f"addUser not found in {func_names}"
83
+ assert 'findUser' in func_names, f"findUser not found in {func_names}"
84
+ assert 'processAll' in func_names, f"processAll not found in {func_names}"
85
+ assert 'notify' in func_names, f"notify not found in {func_names}"
86
+ assert 'initService' in func_names, f"initService not found in {func_names}"
87
+ assert 'main' in func_names, f"main not found in {func_names}"
88
+
89
+ def test_ts_complexity_not_zero(self):
90
+ result = self._analyze()
91
+ ccs = {}
92
+ for name, fi in result.functions.items():
93
+ cc = fi.complexity.get('cyclomatic_complexity', 0)
94
+ ccs[fi.name] = cc
95
+
96
+ # addUser has if/if/else → CC should be > 1
97
+ assert ccs.get('addUser', 0) > 1, f"addUser CC should be > 1, got {ccs.get('addUser')}"
98
+ # findUser has for/if → CC > 1
99
+ assert ccs.get('findUser', 0) > 1, f"findUser CC should be > 1, got {ccs.get('findUser')}"
100
+ # processAll has for/if/|| → CC > 1
101
+ assert ccs.get('processAll', 0) > 1, f"processAll CC should be > 1, got {ccs.get('processAll')}"
102
+ # notify is trivial → CC = 1
103
+ assert ccs.get('notify', 0) >= 1, f"notify CC should be >= 1, got {ccs.get('notify')}"
104
+
105
+ def test_ts_calls_extracted(self):
106
+ result = self._analyze()
107
+ all_calls = {}
108
+ for name, fi in result.functions.items():
109
+ simple_calls = [c.rsplit('.', 1)[-1] for c in fi.calls]
110
+ all_calls[fi.name] = simple_calls
111
+
112
+ # processAll calls notify
113
+ assert 'notify' in all_calls.get('processAll', []), \
114
+ f"processAll should call notify, got {all_calls.get('processAll')}"
115
+ # main calls initService
116
+ assert 'initService' in all_calls.get('main', []), \
117
+ f"main should call initService, got {all_calls.get('main')}"
118
+
119
+ def test_ts_cc_avg_not_zero(self):
120
+ result = self._analyze()
121
+ total_cc = sum(
122
+ fi.complexity.get('cyclomatic_complexity', 0)
123
+ for fi in result.functions.values()
124
+ )
125
+ count = len(result.functions)
126
+ assert count > 0
127
+ avg = total_cc / count
128
+ assert avg > 1.0, f"Average CC should be > 1.0 for non-trivial code, got {avg:.2f}"
129
+
130
+
131
+ class TestGoComplexityAndCalls:
132
+ """Verify Go files get CC > 0."""
133
+
134
+ GO_CODE = """\
135
+ package main
136
+
137
+ import "fmt"
138
+
139
+ type Server struct {
140
+ port int
141
+ }
142
+
143
+ func (s *Server) Start() error {
144
+ if s.port <= 0 {
145
+ return fmt.Errorf("invalid port")
146
+ }
147
+ for i := 0; i < 3; i++ {
148
+ if i > 1 {
149
+ fmt.Println("retrying")
150
+ }
151
+ }
152
+ return nil
153
+ }
154
+
155
+ func NewServer(port int) *Server {
156
+ return &Server{port: port}
157
+ }
158
+
159
+ func main() {
160
+ s := NewServer(8080)
161
+ if err := s.Start(); err != nil {
162
+ fmt.Println(err)
163
+ }
164
+ }
165
+ """
166
+
167
+ def _analyze(self):
168
+ with tempfile.TemporaryDirectory() as tmpdir:
169
+ go_file = Path(tmpdir) / "main.go"
170
+ go_file.write_text(self.GO_CODE)
171
+ analyzer = ProjectAnalyzer(FAST_CONFIG)
172
+ return analyzer.analyze_project(tmpdir)
173
+
174
+ def test_go_complexity_not_zero(self):
175
+ result = self._analyze()
176
+ ccs = {fi.name: fi.complexity.get('cyclomatic_complexity', 0)
177
+ for fi in result.functions.values()}
178
+ # Start has if/for/if → CC > 1
179
+ assert ccs.get('Start', 0) > 1, f"Start CC should be > 1, got {ccs.get('Start')}"
180
+ # main has if → CC > 1
181
+ assert ccs.get('main', 0) > 1, f"main CC should be > 1, got {ccs.get('main')}"
182
+
183
+ def test_go_calls_extracted(self):
184
+ result = self._analyze()
185
+ all_calls = {}
186
+ for fi in result.functions.values():
187
+ all_calls[fi.name] = [c.rsplit('.', 1)[-1] for c in fi.calls]
188
+ # main calls NewServer
189
+ assert 'NewServer' in all_calls.get('main', []), \
190
+ f"main should call NewServer, got {all_calls.get('main')}"
191
+
192
+
193
+ class TestRustComplexityAndCalls:
194
+ """Verify Rust files get CC > 0."""
195
+
196
+ RUST_CODE = """\
197
+ use std::collections::HashMap;
198
+
199
+ struct Config {
200
+ values: HashMap<String, String>,
201
+ }
202
+
203
+ impl Config {
204
+ fn get(&self, key: &str) -> Option<&String> {
205
+ if key.is_empty() {
206
+ return None;
207
+ }
208
+ self.values.get(key)
209
+ }
210
+
211
+ fn set(&mut self, key: &str, value: &str) {
212
+ if key.is_empty() || value.is_empty() {
213
+ return;
214
+ }
215
+ self.values.insert(key.to_string(), value.to_string());
216
+ }
217
+ }
218
+
219
+ fn load_config() -> Config {
220
+ Config { values: HashMap::new() }
221
+ }
222
+
223
+ fn main() {
224
+ let mut cfg = load_config();
225
+ cfg.set("key", "value");
226
+ if let Some(v) = cfg.get("key") {
227
+ println!("{}", v);
228
+ }
229
+ }
230
+ """
231
+
232
+ def _analyze(self):
233
+ with tempfile.TemporaryDirectory() as tmpdir:
234
+ rs_file = Path(tmpdir) / "main.rs"
235
+ rs_file.write_text(self.RUST_CODE)
236
+ analyzer = ProjectAnalyzer(FAST_CONFIG)
237
+ return analyzer.analyze_project(tmpdir)
238
+
239
+ def test_rust_complexity_not_zero(self):
240
+ result = self._analyze()
241
+ ccs = {fi.name: fi.complexity.get('cyclomatic_complexity', 0)
242
+ for fi in result.functions.values()}
243
+ # set has if/|| → CC > 1
244
+ assert ccs.get('set', 0) > 1, f"set CC should be > 1, got {ccs.get('set')}"
245
+ # get has if → CC > 1
246
+ assert ccs.get('get', 0) > 1, f"get CC should be > 1, got {ccs.get('get')}"
247
+
248
+
249
+ class TestJavaComplexityAndCalls:
250
+ """Verify Java files get CC > 0."""
251
+
252
+ JAVA_CODE = """\
253
+ import java.util.List;
254
+ import java.util.ArrayList;
255
+
256
+ public class UserService {
257
+ private List<String> users;
258
+
259
+ public UserService() {
260
+ this.users = new ArrayList<>();
261
+ }
262
+
263
+ public void addUser(String name) {
264
+ if (name != null && !name.isEmpty()) {
265
+ users.add(name);
266
+ }
267
+ }
268
+
269
+ public String findUser(String prefix) {
270
+ for (String u : users) {
271
+ if (u.startsWith(prefix)) {
272
+ return u;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ public int countActive(boolean includeAdmin) {
279
+ int count = 0;
280
+ for (String u : users) {
281
+ if (u.startsWith("active") || (includeAdmin && u.startsWith("admin"))) {
282
+ count++;
283
+ }
284
+ }
285
+ return count;
286
+ }
287
+ }
288
+ """
289
+
290
+ def _analyze(self):
291
+ with tempfile.TemporaryDirectory() as tmpdir:
292
+ java_file = Path(tmpdir) / "UserService.java"
293
+ java_file.write_text(self.JAVA_CODE)
294
+ analyzer = ProjectAnalyzer(FAST_CONFIG)
295
+ return analyzer.analyze_project(tmpdir)
296
+
297
+ def test_java_complexity_not_zero(self):
298
+ result = self._analyze()
299
+ ccs = {fi.name: fi.complexity.get('cyclomatic_complexity', 0)
300
+ for fi in result.functions.values()}
301
+ # addUser has if/&& → CC > 1
302
+ assert ccs.get('addUser', 0) > 1, f"addUser CC should be > 1, got {ccs.get('addUser')}"
303
+ # findUser has for/if → CC > 1
304
+ assert ccs.get('findUser', 0) > 1, f"findUser CC should be > 1, got {ccs.get('findUser')}"
305
+ # countActive has for/if/||/&& → CC > 1
306
+ assert ccs.get('countActive', 0) > 1, f"countActive CC should be > 1, got {ccs.get('countActive')}"
307
+
308
+ def test_java_calls_extracted(self):
309
+ result = self._analyze()
310
+ # At least some calls should be detected
311
+ total_calls = sum(len(fi.calls) for fi in result.functions.values())
312
+ assert total_calls > 0, "Java should have some call edges detected"
File without changes
File without changes
File without changes
File without changes
File without changes