Skip to content

Commit 4fbe1ab

Browse files
authored
feat(graders/code): add CodeSecurityGrader, CodeBugDetectionGrader, C… (#147)
* 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 * fix(graders/code): wrap long lines in prompt templates to satisfy pylint line-length Made-with: Cursor
1 parent fe69438 commit 4fbe1ab

4 files changed

Lines changed: 1159 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: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
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,
105+
describe: what the bug is, which input or condition triggers it,
106+
and its likely impact. If no bugs are found, confirm correctness.>",
107+
"score": <integer between 1 and 5, where 5 means no bugs detected and 1 means critical bugs>
108+
}}
109+
</Output Schema>
110+
111+
JSON:
112+
"""
113+
).strip()
114+
115+
# Chinese Prompt
116+
CODE_BUG_DETECTION_PROMPT_ZH = textwrap.dedent(
117+
"""
118+
你是一名专业的软件工程师和代码审查员,负责识别AI生成代码中的潜在Bug。你的任务是分析代码的正确性问题,并根据发现的Bug的可能性和严重性进行评分。
119+
120+
<评分标准>
121+
无Bug的代码应该:
122+
- 处理所有边界和边缘情况(空输入、零值、负数、None/null值、空集合、最大值、差一错误场景)。
123+
- 正确实现任务中描述的算法,不存在逻辑错误。
124+
- 正确管理资源(文件句柄、连接、锁)——打开的要关闭,获取的要释放。
125+
- 使用正确的数据类型,避免意外的类型强制转换或精度损失。
126+
- 避免循环边界、切片索引和范围计算中的差一错误。
127+
- 处理异常和错误条件,不静默吞噬错误或崩溃。
128+
- 对基本情况、典型情况和极端情况产生正确的输出。
129+
- 不依赖未定义行为、未初始化变量或在运行时可能不成立的状态隐式假设。
130+
- 在适用时正确处理并发问题(竞态条件、死锁、TOCTOU)。
131+
- 在所有代码路径上正确返回或传播结果(无缺失的返回语句)。
132+
133+
以下情况应扣分:
134+
- 对有效输入产生错误结果的逻辑错误。
135+
- 缺失或错误的边界/边缘情况处理。
136+
- 循环、索引或范围计算中的差一错误。
137+
- 未处理的异常或导致崩溃的错误路径。
138+
- 资源泄漏(未关闭的文件、连接或未释放的锁)。
139+
- 对输入类型、可空性或状态的错误假设。
140+
- 无限循环或无基本情况保护的意外递归。
141+
- 并发代码中的竞态条件或共享状态变更。
142+
- 某些代码路径缺少返回值。
143+
- 可变默认参数的不正确使用(Python特定:`def f(x=[]):`)。
144+
</评分标准>
145+
146+
<评估步骤>
147+
- 仔细阅读任务描述,了解预期行为和期望的输入/输出。
148+
- 在脑中追踪典型输入、边缘情况(空、None、零、负值、最大值)和错误条件下的代码逻辑。
149+
- 检查循环边界、索引访问和差一错误模式。
150+
- 寻找未处理的异常路径、缺失的错误检查和资源清理。
151+
- 识别代码在运行时可能不总是成立的假设。
152+
- 根据发现结果评估整体Bug可能性。
153+
</评估步骤>
154+
155+
<注意事项>
156+
仅关注正确性Bug,不考虑风格、性能或安全性(这些是独立的关注点)。编写精美但逻辑错误的函数应获得低分。简单但正确的代码应获得高分。只针对真实输入可能触发的Bug扣分,不针对纯假设场景。
157+
</注意事项>
158+
159+
<评分量表>
160+
- 5: 未检测到Bug。代码正确处理所有典型情况和可见的边缘情况。
161+
- 4: 存在轻微的潜在问题,在实践中不太可能出现(例如,在预期使用场景中几乎不会发生的边缘情况,或更多是风格而非实质的防御性缺失检查)。
162+
- 3: 存在明显的Bug,会导致某些有效输入出现错误行为(例如,循环中的差一错误,可空字段缺少null检查)。
163+
- 2: 存在重大Bug,会导致常见输入的失败或错误结果(例如,不正确的算法逻辑、正常使用时未处理的异常、频繁调用路径中的资源泄漏)。
164+
- 1: 存在关键Bug,导致代码基本无法运行。主要用例失败,或存在多个严重问题,共同使代码不可靠。
165+
</评分量表>
166+
167+
<任务描述>
168+
{query}
169+
</任务描述>
170+
171+
<代码>
172+
{response}
173+
</代码>
174+
175+
<输出格式>
176+
请按以下结构化 JSON 格式提供你的评估:
177+
{{
178+
"reason": "<发现结果的简要说明。对于发现的每个Bug,描述:Bug是什么,哪种输入或条件触发它,以及其可能的影响。如果没有发现Bug,确认代码的正确性。>",
179+
"score": <1到5之间的整数,其中5表示未检测到Bug,1表示存在关键Bug>
180+
}}
181+
</输出格式>
182+
183+
JSON:
184+
"""
185+
).strip()
186+
187+
# Build default template from prompts
188+
DEFAULT_CODE_BUG_DETECTION_TEMPLATE = PromptTemplate(
189+
messages={
190+
LanguageEnum.EN: [
191+
ChatMessage(
192+
role="user",
193+
content=CODE_BUG_DETECTION_PROMPT_EN,
194+
),
195+
],
196+
LanguageEnum.ZH: [
197+
ChatMessage(
198+
role="user",
199+
content=CODE_BUG_DETECTION_PROMPT_ZH,
200+
),
201+
],
202+
},
203+
)
204+
205+
206+
class CodeBugDetectionGrader(LLMGrader):
207+
"""
208+
Code Bug Detection Grader
209+
210+
Purpose:
211+
Detects potential bugs in AI-generated code through LLM-based reasoning, inspired by
212+
pr-agent's `key_issues_to_review` dimension. Unlike `CodeExecutionGrader`, this grader
213+
requires no pre-written test cases — it reasons about correctness from the code itself,
214+
covering bugs that unit tests often miss (race conditions, resource leaks, edge cases).
215+
216+
What it evaluates:
217+
- Logic Errors: Incorrect algorithm implementation, wrong conditionals, bad state transitions
218+
- Boundary / Edge Cases: Empty inputs, null/None, zero, negative, max values, off-by-one
219+
- Resource Management: Unclosed files/connections, unreleased locks, memory leaks
220+
- Exception Handling: Swallowed errors, missing error propagation, crash-prone paths
221+
- Type Safety: Wrong type assumptions, implicit coercions, precision loss
222+
- Concurrency: Race conditions, deadlocks, shared mutable state issues
223+
- Return Value Correctness: Missing returns on some paths, incorrect propagation
224+
225+
When to use:
226+
- Evaluating LLM code generation quality without a test suite
227+
- Benchmarking model bug-proneness across different tasks
228+
- Early-stage code review before execution testing
229+
- Complementing `CodeExecutionGrader` with reasoning-based bug detection
230+
- Identifying systematic failure patterns in a model's code output
231+
232+
Scoring (higher = fewer bugs):
233+
- 5: No bugs detected; code handles typical and edge cases correctly
234+
- 4: Minor potential issues unlikely to manifest in normal usage
235+
- 3: Noticeable bugs for some valid inputs (off-by-one, missing null check)
236+
- 2: Significant bugs causing failures on common inputs
237+
- 1: Critical bugs; primary use case fails or multiple severe issues exist
238+
239+
Args:
240+
model: BaseChatModel instance or dict config for OpenAIChatModel
241+
threshold: Minimum score [1, 5] to pass (default: 3)
242+
template: Custom evaluation template (default: DEFAULT_CODE_BUG_DETECTION_TEMPLATE)
243+
language: Prompt language - EN or ZH (default: LanguageEnum.EN)
244+
strategy: Evaluation strategy (default: DirectEvaluationStrategy)
245+
246+
Returns:
247+
GraderScore with:
248+
- score: [1, 5] where 5 = no bugs, 1 = critical bugs
249+
- reason: Description of each bug found (trigger condition + impact)
250+
- metadata: Threshold and evaluation details
251+
252+
Example:
253+
>>> import asyncio
254+
>>> from openjudge.models.openai_chat_model import OpenAIChatModel
255+
>>> from openjudge.graders.code.code_bug_detection import CodeBugDetectionGrader
256+
>>>
257+
>>> model = OpenAIChatModel(api_key="sk-...", model="qwen3-32b")
258+
>>> grader = CodeBugDetectionGrader(model=model, threshold=3)
259+
>>>
260+
>>> # Buggy code: off-by-one + missing empty list check
261+
>>> result = asyncio.run(grader.aevaluate(
262+
... query="Return the second largest element in a list.",
263+
... response='''
264+
... def second_largest(nums):
265+
... nums.sort()
266+
... return nums[-2]
267+
... ''',
268+
... ))
269+
>>> print(result.score) # 2 - crashes on empty list, returns wrong value for duplicates
270+
>>> print(result.reason) # "Off-by-one on empty list: IndexError when len < 2. ..."
271+
>>>
272+
>>> # Correct code with edge case handling
273+
>>> result = asyncio.run(grader.aevaluate(
274+
... query="Return the second largest element in a list.",
275+
... response='''
276+
... def second_largest(nums):
277+
... if len(nums) < 2:
278+
... raise ValueError("Need at least 2 elements")
279+
... unique = sorted(set(nums), reverse=True)
280+
... if len(unique) < 2:
281+
... raise ValueError("Need at least 2 distinct elements")
282+
... return unique[1]
283+
... ''',
284+
... ))
285+
>>> print(result.score) # 5 - handles edge cases correctly
286+
"""
287+
288+
DEFAULT_TEMPLATE = DEFAULT_CODE_BUG_DETECTION_TEMPLATE
289+
290+
def __init__(
291+
self,
292+
model: BaseChatModel | dict,
293+
threshold: float = 3,
294+
template: Optional[PromptTemplate] = None,
295+
language: LanguageEnum = LanguageEnum.EN,
296+
strategy: BaseEvaluationStrategy | None = None,
297+
):
298+
"""
299+
Initialize CodeBugDetectionGrader.
300+
301+
Args:
302+
model: BaseChatModel instance or dict config for OpenAIChatModel
303+
threshold: Success threshold [1, 5] (default: 3)
304+
template: PromptTemplate for evaluation prompts (default: DEFAULT_CODE_BUG_DETECTION_TEMPLATE)
305+
language: Language for prompts (default: LanguageEnum.EN)
306+
strategy: The evaluation strategy to use. Defaults to DirectEvaluationStrategy.
307+
308+
Raises:
309+
ValueError: If threshold is not in range [1, 5]
310+
"""
311+
if not 1 <= threshold <= 5:
312+
raise ValueError(f"threshold must be in range [1, 5], got {threshold}")
313+
314+
super().__init__(
315+
name="code_bug_detection",
316+
mode=GraderMode.POINTWISE,
317+
description="Detect potential bugs in AI-generated code without requiring test cases",
318+
model=model,
319+
template=template or self.DEFAULT_TEMPLATE,
320+
language=language,
321+
strategy=strategy,
322+
)
323+
self.threshold = threshold
324+
325+
async def _aevaluate(
326+
self,
327+
query: str,
328+
response: str,
329+
**kwargs,
330+
) -> GraderScore:
331+
"""
332+
Evaluate code for potential bugs.
333+
334+
Args:
335+
query: Task description or prompt that produced the code
336+
response: AI-generated code to evaluate
337+
**kwargs: Additional keyword arguments passed to the model
338+
339+
Returns:
340+
GraderScore: Score [1, 5] where 5 = no bugs detected,
341+
1 = critical bugs that break primary functionality
342+
343+
Example:
344+
>>> result = await grader.aevaluate(
345+
... query="Implement a stack with push, pop, and peek.",
346+
... response="class Stack:\\n def pop(self): return self.data.pop()",
347+
... )
348+
>>> # score=2: pop() crashes on empty stack (no guard), missing push/peek
349+
"""
350+
try:
351+
result = await super()._aevaluate(
352+
query=query,
353+
response=response,
354+
)
355+
return GraderScore(
356+
name=self.name,
357+
score=result.score,
358+
reason=result.reason,
359+
metadata={**result.metadata, "threshold": self.threshold},
360+
)
361+
except Exception as e:
362+
logger.exception(f"Error evaluating code bugs: {e}")
363+
return GraderError(
364+
name=self.name,
365+
error=f"Evaluation error: {str(e)}",
366+
)
367+
368+
369+
__all__ = ["CodeBugDetectionGrader", "DEFAULT_CODE_BUG_DETECTION_TEMPLATE"]

0 commit comments

Comments
 (0)