diff --git a/docs/docs/tools/review.md b/docs/docs/tools/review.md index fad93d95..a741cac9 100644 --- a/docs/docs/tools/review.md +++ b/docs/docs/tools/review.md @@ -98,6 +98,11 @@ extra_instructions = "..." require_security_review If set to true, the tool will add a section that checks if the PR contains a possible security or vulnerability issue. Default is true. + + + require_todo_scan + If set to true, the tool will add a section that lists TODO comments found in the PR code changes. Default is false. + require_ticket_analysis_review diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 3e3103ad..2251ac08 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ast import copy import difflib import hashlib @@ -14,7 +15,7 @@ import traceback from datetime import datetime from enum import Enum from importlib.metadata import PackageNotFoundError, version -from typing import Any, List, Tuple +from typing import Any, List, Tuple, TypedDict import html2text import requests @@ -37,21 +38,31 @@ def get_model(model_type: str = "model_weak") -> str: return get_settings().config.model_reasoning return get_settings().config.model + class Range(BaseModel): line_start: int # should be 0-indexed line_end: int column_start: int = -1 column_end: int = -1 + class ModelType(str, Enum): REGULAR = "regular" WEAK = "weak" REASONING = "reasoning" + +class TodoItem(TypedDict): + relevant_file: str + line_range: Tuple[int, int] + content: str + + class PRReviewHeader(str, Enum): REGULAR = "## PR Reviewer Guide" INCREMENTAL = "## Incremental PR Reviewer Guide" + class ReasoningEffort(str, Enum): HIGH = "high" MEDIUM = "medium" @@ -109,6 +120,7 @@ def unique_strings(input_list: List[str]) -> List[str]: seen.add(item) return unique_list + def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, incremental_review=None, @@ -131,6 +143,7 @@ def convert_to_markdown_v2(output_data: dict, "Focused PR": "✨", "Relevant ticket": "🎫", "Security concerns": "🔒", + "Todo sections": "📝", "Insights from user's answers": "📝", "Code feedback": "🤖", "Estimated effort to review [1-5]": "⏱️", @@ -151,6 +164,7 @@ def convert_to_markdown_v2(output_data: dict, if gfm_supported: markdown_text += "\n" + todo_summary = output_data['review'].pop('todo_summary', '') for key, value in output_data['review'].items(): if value is None or value == '' or value == {} or value == []: if key.lower() not in ['can_be_split', 'key_issues_to_review']: @@ -209,6 +223,82 @@ def convert_to_markdown_v2(output_data: dict, markdown_text += f"### {emoji} Security concerns\n\n" value = emphasize_header(value.strip(), only_markdown=True) markdown_text += f"{value}\n\n" + elif 'todo sections' in key_nice.lower(): + def format_todo_item(todo_item: TodoItem) -> str: + relevant_file = todo_item.get('relevant_file', '').strip() + line_range = todo_item.get('line_range', []) + content = todo_item.get('content', '') + reference_link = None + + if isinstance(line_range, str): + line_range = ast.literal_eval(line_range.strip()) + try: + if git_provider and relevant_file and line_range: + reference_link = git_provider.get_line_link(relevant_file, line_range[0], line_range[1]) + except Exception as e: + get_logger().exception(f"Error generating link: {e}") + line_str = f"[{line_range[0]}]" if line_range[0] == line_range[1] else f"[{line_range[0]}-{line_range[1]}]" + return f"{relevant_file} {line_str}: {content}" + + line_str = f"[{line_range[0]}]" if line_range[0] == line_range[1] else f"[{line_range[0]}-{line_range[1]}]" + file_ref = f"{relevant_file} {line_str}" + if reference_link: + if gfm_supported: + file_ref = f"{file_ref}" + else: + file_ref = f"[{file_ref}]({reference_link})" + + content_lines = content.strip().split("\n") + # if TODO content is single-line : + if len(content_lines) == 1: + return f"{file_ref}: {content_lines[0]}" + # else if TODO content is multi-line: + elif len(content_lines) > 1: + content_lines = "
".join(content_lines) + return f"{file_ref}:
\n{content_lines}\n
" + # else if TODO content is empty: + else: + return file_ref + + def format_todo_items(value: list[TodoItem] | TodoItem) -> str: + markdown_text = "" + if gfm_supported: + if isinstance(value, list): + markdown_text += "\n" + else: + markdown_text += f"

{format_todo_item(value)}

\n" + else: + if isinstance(value, list): + for todo_item in value: + markdown_text += f"- {format_todo_item(todo_item)}\n" + else: + markdown_text += f"- {format_todo_item(value)}\n" + return markdown_text + + if gfm_supported: + markdown_text += "\n" + else: + if is_value_no(value): + markdown_text += f"### {emoji} No TODO sections\n\n" + else: + markdown_todo_items = format_todo_items(value) + + markdown_text += f"### {emoji} TODO sections\n
{todo_summary}\n\n" + markdown_text += markdown_todo_items + markdown_text += "\n
\n\n" elif 'can be split' in key_nice.lower(): if gfm_supported: markdown_text += f"
" + if is_value_no(value): + markdown_text += f"{emoji} No TODO sections" + else: + markdown_todo_items = format_todo_items(value) + + markdown_text += f"{emoji} TODO sections\n

\n" + markdown_text += f"
{todo_summary}\n\n" + markdown_text += markdown_todo_items + markdown_text += "\n
\n" + markdown_text += "
" diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index ea651731..f028c9f0 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -78,6 +78,7 @@ require_tests_review=true require_estimate_effort_to_review=true require_can_be_split_review=false require_security_review=true +require_todo_scan=false require_ticket_analysis_review=true # general options publish_output_no_suggestions=true # Set to "false" if you only need the reviewer's remarks (not labels, not "security audit", etc.) and want to avoid noisy "No major issues detected" comments. diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index d4c0a523..6a55c978 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -72,6 +72,13 @@ class KeyIssuesComponentLink(BaseModel): start_line: int = Field(description="The start line that corresponds to this issue in the relevant file") end_line: int = Field(description="The end line that corresponds to this issue in the relevant file") +{%- if require_todo_scan %} +class TodoSection(BaseModel): + relevant_file: str = Field(description="The file containing the TODO comment") + line_range: Tuple[int, int] = Field(description="Start and end line numbers of the TODO comment (inclusive). Must be a tuple of two integers, e.g., (7, 7) for a single line or (7, 10) for a range. Do not use list format [7, 7].") + content: str = Field(description="The content of the TODO comment. Only include actual TODO comments within code comments (e.g., lines starting with '#', '//', '/*', '