Skip to content

Commit 54e31ea

Browse files
committed
feat(graders/code): add CodeSecurityGrader, CodeBugDetectionGrader, CodeComplexityGrader
Introduce three new LLM-based code graders to the `openjudge/graders/code/` module, inspired by pr-agent's review dimensions (security_concerns, key_issues_to_review, estimated_effort_to_review, and maintainability label). - CodeSecurityGrader: detects security vulnerabilities (SQL injection, XSS, hardcoded credentials, SSRF, path traversal, weak crypto, etc.) without requiring a test suite. Default threshold set to 4 reflecting a high security bar. - CodeBugDetectionGrader: identifies potential bugs through LLM reasoning (off-by-one, missing edge-case handling, resource leaks, race conditions) complementing the existing CodeExecutionGrader when no pre-written test cases are available. - CodeComplexityGrader: evaluates whether AI-generated code is unnecessarily complex or over-engineered relative to the task (excessive abstraction, deep nesting, redundant variables, reimplemented builtins), targeting a known LLM failure mode. All three graders support EN/ZH bilingual prompts and follow the established LLMGrader pattern with PromptTemplate, GraderMode.POINTWISE, and configurable threshold. Made-with: Cursor
1 parent fe69438 commit 54e31ea

4 files changed

Lines changed: 1144 additions & 0 deletions

File tree

openjudge/graders/code/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
extensible evaluation mechanisms for AI-generated content.
99
"""
1010

11+
from .code_bug_detection import CodeBugDetectionGrader
12+
from .code_complexity import CodeComplexityGrader
1113
from .code_execution import CodeExecutionGrader
14+
from .code_security import CodeSecurityGrader
1215
from .code_style import CodeStyleGrader
1316
from .patch_similarity import PatchSimilarityGrader
1417
from .syntax_checker import SyntaxCheckGrader
1518

1619
__all__ = [
20+
"CodeBugDetectionGrader",
21+
"CodeComplexityGrader",
1722
"CodeExecutionGrader",
23+
"CodeSecurityGrader",
1824
"CodeStyleGrader",
1925
"PatchSimilarityGrader",
2026
"SyntaxCheckGrader",
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Code Bug Detection Grader
4+
5+
Evaluates whether AI-generated code contains potential bugs — including logic errors,
6+
boundary condition failures, resource leaks, race conditions, and incorrect assumptions —
7+
without requiring pre-written test cases.
8+
9+
Inspired by pr-agent's `key_issues_to_review` dimension, which surfaces high-priority bugs
10+
and correctness concerns that a human reviewer should focus on, covering issues that static
11+
analysis and unit tests may miss.
12+
"""
13+
14+
import textwrap
15+
from typing import Optional
16+
17+
from loguru import logger
18+
19+
from openjudge.evaluation_strategy import BaseEvaluationStrategy
20+
from openjudge.graders.base_grader import GraderError, GraderMode, GraderScore
21+
from openjudge.graders.llm_grader import LLMGrader
22+
from openjudge.models.base_chat_model import BaseChatModel
23+
from openjudge.models.schema.oai.message import ChatMessage
24+
from openjudge.models.schema.prompt_template import LanguageEnum, PromptTemplate
25+
26+
# English Prompt
27+
CODE_BUG_DETECTION_PROMPT_EN = textwrap.dedent(
28+
"""
29+
You are an expert software engineer and code reviewer responsible for identifying potential
30+
bugs in AI-generated code. Your task is to analyze the code for correctness issues and
31+
assign a score based on the likelihood and severity of bugs found.
32+
33+
<Rubrics>
34+
Bug-free code should:
35+
- Handle all boundary and edge cases (empty inputs, zero, negative numbers, None/null values,
36+
empty collections, maximum values, off-by-one scenarios).
37+
- Correctly implement the algorithm described in the task without logic errors.
38+
- Properly manage resources (file handles, connections, locks) — open what you close,
39+
acquire what you release.
40+
- Use correct data types and avoid unintended type coercions or precision loss.
41+
- Avoid off-by-one errors in loop bounds, slice indices, and range calculations.
42+
- Handle exceptions and error conditions without silently swallowing errors or crashing.
43+
- Produce correct output for the base case, typical case, and extreme cases.
44+
- Not rely on undefined behavior, uninitialized variables, or implicit assumptions about
45+
state that may not hold at runtime.
46+
- Correctly handle concurrency concerns when applicable (race conditions, deadlocks, TOCTOU).
47+
- Return or propagate results correctly through all code paths (no missing return statements).
48+
49+
Points should be deducted for:
50+
- Logic errors that cause incorrect results on valid inputs.
51+
- Missing or incorrect boundary/edge case handling.
52+
- Off-by-one errors in loops, indices, or range computations.
53+
- Unhandled exceptions or error paths that cause crashes.
54+
- Resource leaks (unclosed files, connections, or unreleased locks).
55+
- Incorrect assumptions about input types, nullability, or state.
56+
- Infinite loops or unintended recursion without base case protection.
57+
- Race conditions or shared-state mutation in concurrent code.
58+
- Missing return values on some code paths.
59+
- Incorrect use of mutable default arguments (Python-specific: `def f(x=[]):`).
60+
</Rubrics>
61+
62+
<Steps>
63+
- Carefully read the task description to understand the intended behavior and expected inputs/outputs.
64+
- Trace through the code logic mentally for typical inputs, edge cases (empty, None, zero,
65+
negative, maximum), and error conditions.
66+
- Check loop bounds, index access, and off-by-one patterns.
67+
- Look for unhandled exception paths, missing error checks, and resource cleanup.
68+
- Identify any assumptions the code makes that may not always hold at runtime.
69+
- Assess the overall bug likelihood based on findings.
70+
</Steps>
71+
72+
<Constraints>
73+
Focus on correctness bugs only — not style, performance, or security (those are separate
74+
concerns). A beautifully written but logically incorrect function should score low. Simple,
75+
correct code should score high. Only penalize for bugs that are plausibly triggered by real
76+
inputs, not purely hypothetical scenarios.
77+
</Constraints>
78+
79+
<Scale>
80+
- 5: No bugs detected. The code correctly handles all typical cases and visible edge cases.
81+
- 4: Minor potential issues that are unlikely to manifest in practice (e.g., an edge case
82+
that almost never occurs in the expected usage context, or a very defensive missing check
83+
that is more style than substance).
84+
- 3: Noticeable bugs present that would cause incorrect behavior for some valid inputs
85+
(e.g., an off-by-one error in a loop, missing null check for a nullable field).
86+
- 2: Significant bugs that would cause failures or wrong results for common inputs
87+
(e.g., incorrect algorithm logic, unhandled exception on normal usage, resource leak
88+
in a frequently-called path).
89+
- 1: Critical bugs rendering the code largely non-functional. The primary use case fails,
90+
or multiple severe issues exist that together make the code unreliable.
91+
</Scale>
92+
93+
<Task Description>
94+
{query}
95+
</Task Description>
96+
97+
<Code>
98+
{response}
99+
</Code>
100+
101+
<Output Schema>
102+
Provide your evaluation in the following structured JSON format:
103+
{{
104+
"reason": "<concise explanation of findings. For each bug found, describe: what the bug is, which input or condition triggers it, and its likely impact. If no bugs are found, confirm correctness.>",
105+
"score": <integer between 1 and 5, where 5 means no bugs detected and 1 means critical bugs>
106+
}}
107+
</Output Schema>
108+
109+
JSON:
110+
"""
111+
).strip()
112+
113+
# Chinese Prompt
114+
CODE_BUG_DETECTION_PROMPT_ZH = textwrap.dedent(
115+
"""
116+
你是一名专业的软件工程师和代码审查员,负责识别AI生成代码中的潜在Bug。你的任务是分析代码的正确性问题,并根据发现的Bug的可能性和严重性进行评分。
117+
118+
<评分标准>
119+
无Bug的代码应该:
120+
- 处理所有边界和边缘情况(空输入、零值、负数、None/null值、空集合、最大值、差一错误场景)。
121+
- 正确实现任务中描述的算法,不存在逻辑错误。
122+
- 正确管理资源(文件句柄、连接、锁)——打开的要关闭,获取的要释放。
123+
- 使用正确的数据类型,避免意外的类型强制转换或精度损失。
124+
- 避免循环边界、切片索引和范围计算中的差一错误。
125+
- 处理异常和错误条件,不静默吞噬错误或崩溃。
126+
- 对基本情况、典型情况和极端情况产生正确的输出。
127+
- 不依赖未定义行为、未初始化变量或在运行时可能不成立的状态隐式假设。
128+
- 在适用时正确处理并发问题(竞态条件、死锁、TOCTOU)。
129+
- 在所有代码路径上正确返回或传播结果(无缺失的返回语句)。
130+
131+
以下情况应扣分:
132+
- 对有效输入产生错误结果的逻辑错误。
133+
- 缺失或错误的边界/边缘情况处理。
134+
- 循环、索引或范围计算中的差一错误。
135+
- 未处理的异常或导致崩溃的错误路径。
136+
- 资源泄漏(未关闭的文件、连接或未释放的锁)。
137+
- 对输入类型、可空性或状态的错误假设。
138+
- 无限循环或无基本情况保护的意外递归。
139+
- 并发代码中的竞态条件或共享状态变更。
140+
- 某些代码路径缺少返回值。
141+
- 可变默认参数的不正确使用(Python特定:`def f(x=[]):`)。
142+
</评分标准>
143+
144+
<评估步骤>
145+
- 仔细阅读任务描述,了解预期行为和期望的输入/输出。
146+
- 在脑中追踪典型输入、边缘情况(空、None、零、负值、最大值)和错误条件下的代码逻辑。
147+
- 检查循环边界、索引访问和差一错误模式。
148+
- 寻找未处理的异常路径、缺失的错误检查和资源清理。
149+
- 识别代码在运行时可能不总是成立的假设。
150+
- 根据发现结果评估整体Bug可能性。
151+
</评估步骤>
152+
153+
<注意事项>
154+
仅关注正确性Bug,不考虑风格、性能或安全性(这些是独立的关注点)。编写精美但逻辑错误的函数应获得低分。简单但正确的代码应获得高分。只针对真实输入可能触发的Bug扣分,不针对纯假设场景。
155+
</注意事项>
156+
157+
<评分量表>
158+
- 5: 未检测到Bug。代码正确处理所有典型情况和可见的边缘情况。
159+
- 4: 存在轻微的潜在问题,在实践中不太可能出现(例如,在预期使用场景中几乎不会发生的边缘情况,或更多是风格而非实质的防御性缺失检查)。
160+
- 3: 存在明显的Bug,会导致某些有效输入出现错误行为(例如,循环中的差一错误,可空字段缺少null检查)。
161+
- 2: 存在重大Bug,会导致常见输入的失败或错误结果(例如,不正确的算法逻辑、正常使用时未处理的异常、频繁调用路径中的资源泄漏)。
162+
- 1: 存在关键Bug,导致代码基本无法运行。主要用例失败,或存在多个严重问题,共同使代码不可靠。
163+
</评分量表>
164+
165+
<任务描述>
166+
{query}
167+
</任务描述>
168+
169+
<代码>
170+
{response}
171+
</代码>
172+
173+
<输出格式>
174+
请按以下结构化 JSON 格式提供你的评估:
175+
{{
176+
"reason": "<发现结果的简要说明。对于发现的每个Bug,描述:Bug是什么,哪种输入或条件触发它,以及其可能的影响。如果没有发现Bug,确认代码的正确性。>",
177+
"score": <1到5之间的整数,其中5表示未检测到Bug,1表示存在关键Bug>
178+
}}
179+
</输出格式>
180+
181+
JSON:
182+
"""
183+
).strip()
184+
185+
# Build default template from prompts
186+
DEFAULT_CODE_BUG_DETECTION_TEMPLATE = PromptTemplate(
187+
messages={
188+
LanguageEnum.EN: [
189+
ChatMessage(
190+
role="user",
191+
content=CODE_BUG_DETECTION_PROMPT_EN,
192+
),
193+
],
194+
LanguageEnum.ZH: [
195+
ChatMessage(
196+
role="user",
197+
content=CODE_BUG_DETECTION_PROMPT_ZH,
198+
),
199+
],
200+
},
201+
)
202+
203+
204+
class CodeBugDetectionGrader(LLMGrader):
205+
"""
206+
Code Bug Detection Grader
207+
208+
Purpose:
209+
Detects potential bugs in AI-generated code through LLM-based reasoning, inspired by
210+
pr-agent's `key_issues_to_review` dimension. Unlike `CodeExecutionGrader`, this grader
211+
requires no pre-written test cases — it reasons about correctness from the code itself,
212+
covering bugs that unit tests often miss (race conditions, resource leaks, edge cases).
213+
214+
What it evaluates:
215+
- Logic Errors: Incorrect algorithm implementation, wrong conditionals, bad state transitions
216+
- Boundary / Edge Cases: Empty inputs, null/None, zero, negative, max values, off-by-one
217+
- Resource Management: Unclosed files/connections, unreleased locks, memory leaks
218+
- Exception Handling: Swallowed errors, missing error propagation, crash-prone paths
219+
- Type Safety: Wrong type assumptions, implicit coercions, precision loss
220+
- Concurrency: Race conditions, deadlocks, shared mutable state issues
221+
- Return Value Correctness: Missing returns on some paths, incorrect propagation
222+
223+
When to use:
224+
- Evaluating LLM code generation quality without a test suite
225+
- Benchmarking model bug-proneness across different tasks
226+
- Early-stage code review before execution testing
227+
- Complementing `CodeExecutionGrader` with reasoning-based bug detection
228+
- Identifying systematic failure patterns in a model's code output
229+
230+
Scoring (higher = fewer bugs):
231+
- 5: No bugs detected; code handles typical and edge cases correctly
232+
- 4: Minor potential issues unlikely to manifest in normal usage
233+
- 3: Noticeable bugs for some valid inputs (off-by-one, missing null check)
234+
- 2: Significant bugs causing failures on common inputs
235+
- 1: Critical bugs; primary use case fails or multiple severe issues exist
236+
237+
Args:
238+
model: BaseChatModel instance or dict config for OpenAIChatModel
239+
threshold: Minimum score [1, 5] to pass (default: 3)
240+
template: Custom evaluation template (default: DEFAULT_CODE_BUG_DETECTION_TEMPLATE)
241+
language: Prompt language - EN or ZH (default: LanguageEnum.EN)
242+
strategy: Evaluation strategy (default: DirectEvaluationStrategy)
243+
244+
Returns:
245+
GraderScore with:
246+
- score: [1, 5] where 5 = no bugs, 1 = critical bugs
247+
- reason: Description of each bug found (trigger condition + impact)
248+
- metadata: Threshold and evaluation details
249+
250+
Example:
251+
>>> import asyncio
252+
>>> from openjudge.models.openai_chat_model import OpenAIChatModel
253+
>>> from openjudge.graders.code.code_bug_detection import CodeBugDetectionGrader
254+
>>>
255+
>>> model = OpenAIChatModel(api_key="sk-...", model="qwen3-32b")
256+
>>> grader = CodeBugDetectionGrader(model=model, threshold=3)
257+
>>>
258+
>>> # Buggy code: off-by-one + missing empty list check
259+
>>> result = asyncio.run(grader.aevaluate(
260+
... query="Return the second largest element in a list.",
261+
... response='''
262+
... def second_largest(nums):
263+
... nums.sort()
264+
... return nums[-2]
265+
... ''',
266+
... ))
267+
>>> print(result.score) # 2 - crashes on empty list, returns wrong value for duplicates
268+
>>> print(result.reason) # "Off-by-one on empty list: IndexError when len < 2. ..."
269+
>>>
270+
>>> # Correct code with edge case handling
271+
>>> result = asyncio.run(grader.aevaluate(
272+
... query="Return the second largest element in a list.",
273+
... response='''
274+
... def second_largest(nums):
275+
... if len(nums) < 2:
276+
... raise ValueError("Need at least 2 elements")
277+
... unique = sorted(set(nums), reverse=True)
278+
... if len(unique) < 2:
279+
... raise ValueError("Need at least 2 distinct elements")
280+
... return unique[1]
281+
... ''',
282+
... ))
283+
>>> print(result.score) # 5 - handles edge cases correctly
284+
"""
285+
286+
DEFAULT_TEMPLATE = DEFAULT_CODE_BUG_DETECTION_TEMPLATE
287+
288+
def __init__(
289+
self,
290+
model: BaseChatModel | dict,
291+
threshold: float = 3,
292+
template: Optional[PromptTemplate] = None,
293+
language: LanguageEnum = LanguageEnum.EN,
294+
strategy: BaseEvaluationStrategy | None = None,
295+
):
296+
"""
297+
Initialize CodeBugDetectionGrader.
298+
299+
Args:
300+
model: BaseChatModel instance or dict config for OpenAIChatModel
301+
threshold: Success threshold [1, 5] (default: 3)
302+
template: PromptTemplate for evaluation prompts (default: DEFAULT_CODE_BUG_DETECTION_TEMPLATE)
303+
language: Language for prompts (default: LanguageEnum.EN)
304+
strategy: The evaluation strategy to use. Defaults to DirectEvaluationStrategy.
305+
306+
Raises:
307+
ValueError: If threshold is not in range [1, 5]
308+
"""
309+
if not 1 <= threshold <= 5:
310+
raise ValueError(f"threshold must be in range [1, 5], got {threshold}")
311+
312+
super().__init__(
313+
name="code_bug_detection",
314+
mode=GraderMode.POINTWISE,
315+
description="Detect potential bugs in AI-generated code without requiring test cases",
316+
model=model,
317+
template=template or self.DEFAULT_TEMPLATE,
318+
language=language,
319+
strategy=strategy,
320+
)
321+
self.threshold = threshold
322+
323+
async def _aevaluate(
324+
self,
325+
query: str,
326+
response: str,
327+
**kwargs,
328+
) -> GraderScore:
329+
"""
330+
Evaluate code for potential bugs.
331+
332+
Args:
333+
query: Task description or prompt that produced the code
334+
response: AI-generated code to evaluate
335+
**kwargs: Additional keyword arguments passed to the model
336+
337+
Returns:
338+
GraderScore: Score [1, 5] where 5 = no bugs detected,
339+
1 = critical bugs that break primary functionality
340+
341+
Example:
342+
>>> result = await grader.aevaluate(
343+
... query="Implement a stack with push, pop, and peek.",
344+
... response="class Stack:\\n def pop(self): return self.data.pop()",
345+
... )
346+
>>> # score=2: pop() crashes on empty stack (no guard), missing push/peek
347+
"""
348+
try:
349+
result = await super()._aevaluate(
350+
query=query,
351+
response=response,
352+
)
353+
return GraderScore(
354+
name=self.name,
355+
score=result.score,
356+
reason=result.reason,
357+
metadata={**result.metadata, "threshold": self.threshold},
358+
)
359+
except Exception as e:
360+
logger.exception(f"Error evaluating code bugs: {e}")
361+
return GraderError(
362+
name=self.name,
363+
error=f"Evaluation error: {str(e)}",
364+
)
365+
366+
367+
__all__ = ["CodeBugDetectionGrader", "DEFAULT_CODE_BUG_DETECTION_TEMPLATE"]

0 commit comments

Comments
 (0)