Skip to content

Commit e5446cd

Browse files
authored
Merge pull request #34 from Andrei-Constantin-Programmer/IBM-32-Add-other-metrics-before-after-completion
IBM-32: Add other metrics before after completion
2 parents b5bc86e + 6eaf83a commit e5446cd

3 files changed

Lines changed: 238 additions & 11 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import json, os
2+
from pathlib import Path
3+
import lizard
4+
import re
5+
import javalang
6+
7+
def calculate_nesting_depth(code: str) -> int:
8+
"""
9+
Calculate the maximum nesting depth for a function.
10+
"""
11+
tree = javalang.parse.parse(code)
12+
13+
max_depth = 0
14+
15+
def walk(node, depth=0):
16+
nonlocal max_depth
17+
max_depth = max(max_depth, depth)
18+
if isinstance(node, (javalang.tree.IfStatement,
19+
javalang.tree.ForStatement,
20+
javalang.tree.WhileStatement,
21+
javalang.tree.TryStatement,
22+
javalang.tree.SwitchStatement)):
23+
depth += 1
24+
for child in getattr(node, 'children', []):
25+
if isinstance(child, (list, tuple)):
26+
for c in child:
27+
if isinstance(c, javalang.ast.Node):
28+
walk(c, depth)
29+
elif isinstance(child, javalang.ast.Node):
30+
walk(child, depth)
31+
32+
walk(tree)
33+
return max_depth
34+
35+
36+
def _process_lizard_result(lizard_result, source_code: str, filename: str, source_type: str = "file"):
37+
"""
38+
Helper function to process lizard analysis results and extract metrics.
39+
"""
40+
functions = []
41+
42+
for fn in lizard_result.function_list:
43+
44+
functions.append({
45+
"name": fn.long_name,
46+
"start_line": fn.start_line,
47+
"end_line": fn.end_line,
48+
"nloc": fn.nloc, # SLOC (non-comment LOC) for the function
49+
"cyclomatic_complexity": fn.cyclomatic_complexity
50+
})
51+
52+
# Calculate nesting depth using brace counting approach
53+
nesting_depth = calculate_nesting_depth(source_code)
54+
55+
file_metrics = {
56+
"file": filename,
57+
"file_sloc_nloc": lizard_result.nloc, # file-level SLOC (non-comment LOC)
58+
"total_functions": len(functions),
59+
"avg_cc": round(sum(f["cyclomatic_complexity"] for f in functions)/len(functions), 2) if functions else 0.0,
60+
"max_cc": max((f["cyclomatic_complexity"] for f in functions), default=0),
61+
"max_nd_in_file": nesting_depth,
62+
"functions": functions,
63+
}
64+
65+
# Add source type indicator for string analysis
66+
if source_type == "string":
67+
file_metrics["source_type"] = "string"
68+
69+
return file_metrics
70+
71+
def analyze_file(path: Path):
72+
"""Analyze a Java file from file path."""
73+
# Read the source code for custom nesting analysis
74+
with open(path, 'r', encoding='utf-8') as f:
75+
source_code = f.read()
76+
77+
# Analyze with lizard
78+
lizard_result = lizard.analyze_file(str(path))
79+
80+
# Process the results using shared helper
81+
return _process_lizard_result(lizard_result, source_code, str(path), "file")
82+
83+
def analyze_source_code(source_code: str, filename: str = "AnalyzedCode.java"):
84+
"""Analyze Java source code directly from string."""
85+
# Analyze with lizard
86+
lizard_result = lizard.analyze_file.analyze_source_code(filename, source_code)
87+
88+
# Process the results using shared helper
89+
return _process_lizard_result(lizard_result, source_code, filename, "string")
90+
91+
def compare_code_metrics(original_metrics, refactored_metrics, filename_prefix: str = "Code"):
92+
"""Compare metrics between original and refactored code."""
93+
94+
comparison = {
95+
"improvements": {
96+
"sloc_reduction": original_metrics["file_sloc_nloc"] - refactored_metrics["file_sloc_nloc"],
97+
"function_count_change": refactored_metrics["total_functions"] - original_metrics["total_functions"],
98+
"avg_cc_improvement": original_metrics["avg_cc"] - refactored_metrics["avg_cc"],
99+
"max_cc_reduction": original_metrics["max_cc"] - refactored_metrics["max_cc"],
100+
"max_nesting_reduction": original_metrics["max_nd_in_file"] - refactored_metrics["max_nd_in_file"]
101+
}
102+
}
103+
return comparison
104+
105+
if __name__ == "__main__":
106+
import argparse
107+
p = argparse.ArgumentParser()
108+
p.add_argument("target", help="Path to a .java file or a directory")
109+
p.add_argument("--output", "-o", help="Output JSON file path", default="metrics_results.json")
110+
args = p.parse_args()
111+
112+
results = []
113+
target = Path(args.target)
114+
if target.is_dir():
115+
for f in target.rglob("*.java"):
116+
results.append(analyze_file(f))
117+
else:
118+
results.append(analyze_file(target))
119+
120+
# Save results to JSON file
121+
output_path = Path(args.output)
122+
with open(output_path, 'w', encoding='utf-8') as f:
123+
json.dump(results, f, indent=2)
124+
125+
print(f"Metrics analysis complete. Results saved to: {output_path}")
126+
print(f"Analyzed {len(results)} file(s)")
127+
128+
# Print summary
129+
if results:
130+
total_functions = sum(r.get('total_functions', 0) for r in results)
131+
avg_complexity = sum(r.get('avg_cc', 0) for r in results) / len(results)
132+
print(f"Total functions analyzed: {total_functions}")
133+
print(f"Average complexity across files: {avg_complexity:.2f}")

AntiPattern_Remediator/workflow/results_manager.py

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,49 @@
88
from pathlib import Path
99
from datetime import datetime
1010
from colorama import Fore, Style
11+
from .compute_metrics import analyze_source_code, compare_code_metrics
12+
13+
14+
def compute_code_metrics(final_state: dict) -> dict:
15+
"""Compute metrics for original and refactored code."""
16+
try:
17+
metrics_data = {}
18+
19+
# Analyze original code
20+
if final_state.get('code'):
21+
original_metrics = analyze_source_code(final_state['code'], "Original.java")
22+
# Remove functions list to keep it simple
23+
original_metrics_simplified = {k: v for k, v in original_metrics.items() if k != 'functions'}
24+
metrics_data['original_metrics'] = original_metrics_simplified
25+
26+
# Analyze refactored code
27+
if final_state.get('refactored_code'):
28+
refactored_metrics = analyze_source_code(final_state['refactored_code'], "Refactored.java")
29+
# Remove functions list to keep it simple
30+
refactored_metrics_simplified = {k: v for k, v in refactored_metrics.items() if k != 'functions'}
31+
metrics_data['refactored_metrics'] = refactored_metrics_simplified
32+
33+
# Compare if both exist
34+
if final_state.get('code'):
35+
comparison = compare_code_metrics(
36+
original_metrics, # Pass the full metrics objects
37+
refactored_metrics # Pass the full metrics objects
38+
)
39+
# Only include the improvements, not the full original/refactored data
40+
metrics_data['improvements'] = comparison['improvements']
41+
42+
return metrics_data
43+
except Exception as e:
44+
print(Fore.YELLOW + f"Warning: Could not compute metrics: {e}" + Style.RESET_ALL)
45+
return {}
1146

1247

1348
def save_intermediate_results(file_path: str, final_state: dict, settings, results_dir: str = "../processing_results") -> bool:
1449
"""Save intermediate results from the agentic workflow for analysis in markdown format."""
1550
try:
51+
# Compute code metrics
52+
metrics_data = compute_code_metrics(final_state)
53+
1654
if file_path != 'java_code_snippet' and not None:
1755
# Create results directory if it doesn't exist
1856
results_path = Path(results_dir)
@@ -170,25 +208,79 @@ def save_intermediate_results(file_path: str, final_state: dict, settings, resul
170208

171209
markdown_content += '</div>\n\n'
172210

173-
# Add code comparison summary
174-
if original_code and refactored_code:
175-
original_lines = len(original_code.splitlines())
176-
refactored_lines = len(refactored_code.splitlines())
177-
line_change = refactored_lines - original_lines
178-
line_change_str = f"+{line_change}" if line_change > 0 else str(line_change)
179-
180-
markdown_content += f"**Code Metrics:**\n"
181-
markdown_content += f"- Original Loc: {original_lines}\n"
182-
markdown_content += f"- Refactored Loc: {refactored_lines}\n"
183-
markdown_content += f"- LoC change: {line_change_str}\n\n"
184211
else:
185212
markdown_content += "No code available for comparison.\n\n"
186213

214+
# Add code metrics section
215+
if metrics_data:
216+
markdown_content += "---\n\n## Code Metrics\n\n"
217+
218+
# Display Original and Refactored metrics side by side
219+
if 'original_metrics' in metrics_data and 'refactored_metrics' in metrics_data:
220+
orig = metrics_data['original_metrics']
221+
refac = metrics_data['refactored_metrics']
222+
223+
markdown_content += "### Original vs Refactored Code \n\n"
224+
markdown_content += "| Metric | Original | Refactored |\n"
225+
markdown_content += "|--------|----------|------------|\n"
226+
markdown_content += f"| **Source Lines of Code (SLOC)** | {orig.get('file_sloc_nloc', 'N/A')} | {refac.get('file_sloc_nloc', 'N/A')} |\n"
227+
markdown_content += f"| **Total Functions** | {orig.get('total_functions', 'N/A')} | {refac.get('total_functions', 'N/A')} |\n"
228+
markdown_content += f"| **Average Cyclomatic Complexity** | {orig.get('avg_cc', 'N/A')} | {refac.get('avg_cc', 'N/A')} |\n"
229+
markdown_content += f"| **Max Cyclomatic Complexity** | {orig.get('max_cc', 'N/A')} | {refac.get('max_cc', 'N/A')} |\n"
230+
markdown_content += f"| **Max Nesting Depth** | {orig.get('max_nd_in_file', 'N/A')} | {refac.get('max_nd_in_file', 'N/A')} |\n\n"
231+
232+
elif 'original_metrics' in metrics_data:
233+
orig = metrics_data['original_metrics']
234+
markdown_content += "### Original Code Metrics\n\n"
235+
markdown_content += f"- **Source Lines of Code (SLOC):** {orig.get('file_sloc_nloc', 'N/A')}\n"
236+
markdown_content += f"- **Total Functions:** {orig.get('total_functions', 'N/A')}\n"
237+
markdown_content += f"- **Average Cyclomatic Complexity:** {orig.get('avg_cc', 'N/A')}\n"
238+
markdown_content += f"- **Max Cyclomatic Complexity:** {orig.get('max_cc', 'N/A')}\n"
239+
markdown_content += f"- **Max Nesting Depth:** {orig.get('max_nd_in_file', 'N/A')}\n\n"
240+
241+
# Display Improvements
242+
if 'improvements' in metrics_data:
243+
imp = metrics_data['improvements']
244+
markdown_content += "### Comparison\n\n"
245+
246+
sloc_change = imp.get('sloc_reduction', 0)
247+
func_change = imp.get('function_count_change', 0)
248+
avg_cc_imp = imp.get('avg_cc_improvement', 0)
249+
max_cc_red = imp.get('max_cc_reduction', 0)
250+
nest_red = imp.get('max_nesting_reduction', 0)
251+
252+
markdown_content += f"- **Lines of Code Change:** {sloc_change:+d} lines\n"
253+
markdown_content += f"- **Function Count Change:** {func_change:+d} functions\n"
254+
markdown_content += f"- **Average Complexity Improvement:** {avg_cc_imp:+.2f}\n"
255+
markdown_content += f"- **Max Complexity Reduction:** {max_cc_red:+d}\n"
256+
markdown_content += f"- **Max Nesting Reduction:** {nest_red:+d}\n\n"
257+
187258
markdown_content += f"---\n\n*Generated by AntiPattern Remediator Tool using {settings.LLM_MODEL}*\n"
188259
# Save to markdown file
189260
with open(results_file_path, 'w', encoding='utf-8') as f:
190261
f.write(markdown_content)
191262

263+
# Save metrics to JSON file if available
264+
if metrics_data:
265+
if file_path != 'java_code_snippet':
266+
json_filename = f"{safe_filename}_metrics.json"
267+
json_file_path = results_path / json_filename
268+
else:
269+
json_filename = "java_code_snippet_metrics.json"
270+
json_file_path = json_filename
271+
272+
# Include file path and timestamp in metrics JSON
273+
metrics_with_metadata = {
274+
"file_path": file_path,
275+
"timestamp": timestamp,
276+
"metrics": metrics_data
277+
}
278+
279+
with open(json_file_path, 'w', encoding='utf-8') as f:
280+
json.dump(metrics_with_metadata, f, indent=2)
281+
282+
print(Fore.CYAN + f"Code metrics saved: {json_file_path}" + Style.RESET_ALL)
283+
192284
print(Fore.CYAN + f"Intermediate results saved: {results_file_path}" + Style.RESET_ALL)
193285
return True
194286

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ idna==3.10
4848
importlib_metadata==8.7.0
4949
importlib_resources==6.5.2
5050
iniconfig==2.1.0
51+
javalang==0.13.0
5152
jaraco.classes==3.4.0
5253
jaraco.context==6.0.1
5354
jaraco.functools==4.2.1
@@ -70,6 +71,7 @@ langgraph-checkpoint==2.1.0
7071
langgraph-prebuilt==0.5.2
7172
langgraph-sdk==0.1.72
7273
langsmith==0.4.4
74+
lizard==1.17.31
7375
lomond==0.3.3
7476
markdown-it-py==3.0.0
7577
marshmallow==3.26.1

0 commit comments

Comments
 (0)