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.
- {code2llm-0.5.49 → code2llm-0.5.50}/PKG-INFO +3 -3
- {code2llm-0.5.49 → code2llm-0.5.50}/README.md +2 -2
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/__init__.py +1 -1
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_analyzer.py +214 -16
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/__init__.py +1 -1
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/PKG-INFO +3 -3
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/SOURCES.txt +1 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/pyproject.toml +1 -1
- code2llm-0.5.50/tests/test_nonpython_cc_calls.py +312 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/LICENSE +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/__main__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/call_graph.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/cfg.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/coupling.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/data_analysis.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/dfg.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/pipeline_detector.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/side_effects.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/smells.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/analysis/type_inference.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/api.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_analysis.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/code2logic.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/formats.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/orchestrator.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/cli_exports/prompt.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/analyzer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/config.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_cache.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/file_filter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/core/refactoring.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/large_repo.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/models.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/cache.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/incremental.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/prioritizer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/scanner.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming/strategies.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/streaming_analyzer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/core/toon_size_manager.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/article_view.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/base.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/context_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/context_view.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/evolution_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_constants.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/flow_renderer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/html_dashboard.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/json_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/llm_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/map_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/mermaid_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/project_yaml_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/readme_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/report_generators.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/helpers.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/metrics.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/module_detail.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon/renderer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/toon_view.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/validate_project.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/exporters/yaml_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/llm_flow.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/llm_task.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/generators/mermaid.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/config.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/entity_resolution.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/intent_matching.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/normalization.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/nlp/pipeline.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/patterns/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/patterns/detector.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/refactor/__init__.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm/refactor/prompt_engine.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/dependency_links.txt +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/entry_points.txt +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/requires.txt +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/code2llm.egg-info/top_level.txt +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/setup.cfg +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/setup.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_advanced_analysis.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_analyzer.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_deep_analysis.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_edge_cases.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_flow_exporter.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_format_quality.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_multilanguage_e2e.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_nlp_pipeline.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_pipeline_detector.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_prompt_engine.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_prompt_txt.py +0 -0
- {code2llm-0.5.49 → code2llm-0.5.50}/tests/test_refactoring_engine.py +0 -0
- {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.
|
|
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 |
|
|
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**:
|
|
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 |
|
|
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**:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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=
|
|
483
|
-
is_method=
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2llm
|
|
3
|
-
Version: 0.5.
|
|
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 |
|
|
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**:
|
|
378
|
+
**Total Functions**: 857
|
|
379
379
|
**Total Classes**: 104
|
|
380
380
|
**Modules**: 105
|
|
381
381
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code2llm"
|
|
7
|
-
version = "0.5.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|