File size: 5,138 Bytes
fc2d789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
Utility helpers for the Code Review NLP Assistant.
"""

from __future__ import annotations
import ast
import re
from typing import Any


# ── Code parsing helpers ──────────────────────────────────────────────────

def extract_functions(code: str) -> list[dict[str, Any]]:
    """
    Parse Python source and return metadata for each function/method.
    Returns a list of dicts with keys:
        name, args, returns, has_docstring, lineno, end_lineno, source
    """
    results = []
    try:
        tree = ast.parse(code)
    except SyntaxError:
        return results

    lines = code.splitlines()

    for node in ast.walk(tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue

        # Argument names
        args = [a.arg for a in node.args.args]

        # Return annotation
        returns = ""
        if node.returns:
            try:
                returns = ast.unparse(node.returns)
            except Exception:
                returns = "?"

        # Docstring check
        has_doc = (
            isinstance(node.body[0], ast.Expr)
            and isinstance(node.body[0].value, ast.Constant)
            and isinstance(node.body[0].value.value, str)
        ) if node.body else False

        end = getattr(node, "end_lineno", node.lineno)
        src_lines = lines[node.lineno - 1 : end]
        source = "\n".join(src_lines)

        results.append({
            "name":          node.name,
            "args":          args,
            "returns":       returns,
            "has_docstring": has_doc,
            "lineno":        node.lineno,
            "end_lineno":    end,
            "source":        source,
        })

    return results


def extract_classes(code: str) -> list[dict[str, Any]]:
    """Return a list of class metadata dicts."""
    results = []
    try:
        tree = ast.parse(code)
    except SyntaxError:
        return results

    for node in ast.walk(tree):
        if not isinstance(node, ast.ClassDef):
            continue
        methods = [
            n.name for n in ast.walk(node)
            if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
        ]
        has_doc = (
            isinstance(node.body[0], ast.Expr)
            and isinstance(node.body[0].value, ast.Constant)
        ) if node.body else False

        results.append({
            "name":          node.name,
            "methods":       methods,
            "has_docstring": has_doc,
            "lineno":        node.lineno,
        })

    return results


def detect_language(code: str) -> str:
    """Very simple language heuristic."""
    if re.search(r'\bdef\b.*:\s*$', code, re.MULTILINE):
        return "python"
    if re.search(r'\bfunction\b|\bconst\b|\blet\b|\bvar\b', code):
        return "javascript"
    if re.search(r'\bpublic\b.*\bclass\b', code):
        return "java"
    return "unknown"


# ── Reporting helpers ─────────────────────────────────────────────────────

GRADE_MAP = [
    (90, "A", "Excellent"),
    (75, "B", "Good"),
    (60, "C", "Needs work"),
    (40, "D", "Poor"),
    (0,  "F", "Critical"),
]


def score_to_grade(score: float) -> tuple[str, str]:
    """Return (letter_grade, label) for a 0-100 score."""
    for threshold, letter, label in GRADE_MAP:
        if score >= threshold:
            return letter, label
    return "F", "Critical"


def score_color(score: float) -> str:
    """Return a hex colour representing the score quality."""
    if score >= 80: return "#22c55e"
    if score >= 60: return "#f59e0b"
    if score >= 40: return "#f97316"
    return "#ef4444"


def build_report(result: Any, filename: str = "code_review") -> str:
    """Generate a Markdown report from a CodeQualityResult."""
    grade, label = score_to_grade(result.overall_score)
    lines = [
        f"# Code Review Report β€” `{filename}`\n",
        f"## Overall Score: {result.overall_score}/100 ({grade} β€” {label})\n",
        "### Sub-scores\n",
        "| Metric | Score |",
        "|--------|-------|",
        f"| Documentation | {result.documentation_score}/100 |",
        f"| Naming Quality | {result.naming_score}/100 |",
        f"| Complexity | {result.complexity_score}/100 |",
        "",
    ]

    if result.issues:
        lines.append("### Issues Found\n")
        for issue in result.issues:
            lines.append(f"- {issue}")
        lines.append("")

    if result.suggestions:
        lines.append("### Suggestions\n")
        for s in result.suggestions:
            lines.append(f"- {s}")
        lines.append("")

    if result.generated_docstring:
        lines.append("### Generated Docstring (CodeT5)\n")
        lines.append("```python")
        lines.append(result.generated_docstring)
        lines.append("```\n")

    lines.append("---")
    lines.append("*Generated by Code Review NLP Assistant using CodeBERT + CodeT5*")

    return "\n".join(lines)