Coverage for lintro / ai / fix_parsing.py: 100%
62 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Response parsing and diff generation for AI fix suggestions.
3Parses single and batch AI responses into AIFixSuggestion objects
4and generates unified diffs between original and suggested code.
5"""
7from __future__ import annotations
9import difflib
10import json
11from typing import TYPE_CHECKING
13from loguru import logger
15from lintro.ai.paths import relative_path
17if TYPE_CHECKING:
18 from lintro.ai.models import AIFixSuggestion
21def generate_diff(
22 file_path: str,
23 original: str,
24 suggested: str,
25) -> str:
26 """Generate a unified diff between original and suggested code.
28 Args:
29 file_path: Path for the diff header.
30 original: Original code snippet.
31 suggested: Suggested replacement.
33 Returns:
34 Unified diff string.
35 """
36 original_lines = original.splitlines()
37 suggested_lines = suggested.splitlines()
39 rel = relative_path(file_path)
40 diff = difflib.unified_diff(
41 original_lines,
42 suggested_lines,
43 fromfile=f"a/{rel}",
44 tofile=f"b/{rel}",
45 )
46 return "\n".join(diff)
49def parse_fix_response(
50 content: str,
51 file_path: str,
52 line: int,
53 code: str,
54) -> AIFixSuggestion | None:
55 """Parse an AI response into an AIFixSuggestion.
57 Args:
58 content: Raw AI response content.
59 file_path: Path to the file.
60 line: Line number of the issue.
61 code: Error code of the issue.
63 Returns:
64 Parsed AIFixSuggestion, or None if parsing fails.
65 """
66 from lintro.ai.models import AIFixSuggestion
68 try:
69 data = json.loads(content)
70 except json.JSONDecodeError:
71 logger.debug(f"Failed to parse AI fix response for {file_path}:{line}")
72 return None
74 if not isinstance(data, dict):
75 logger.debug(f"AI fix response is not a JSON object for {file_path}:{line}")
76 return None
78 original = data.get("original_code", "")
79 suggested = data.get("suggested_code", "")
81 if not isinstance(original, str) or not isinstance(suggested, str):
82 logger.debug(f"AI fix code fields are not strings for {file_path}:{line}")
83 return None
85 if not original or not suggested:
86 return None
87 if original == suggested:
88 logger.debug(f"AI suggested no-op fix for {file_path}:{line}")
89 return None
91 diff = generate_diff(file_path, original, suggested)
93 return AIFixSuggestion(
94 file=file_path,
95 line=line,
96 code=code,
97 original_code=original,
98 suggested_code=suggested,
99 diff=diff,
100 explanation=data.get("explanation", ""),
101 confidence=data.get("confidence", "medium"),
102 risk_level=data.get("risk_level", ""),
103 )
106def parse_batch_response(
107 content: str,
108 file_path: str,
109) -> list[AIFixSuggestion]:
110 """Parse a batch AI response into a list of AIFixSuggestions.
112 Args:
113 content: Raw AI response content (expected JSON array).
114 file_path: Path to the file.
116 Returns:
117 List of parsed AIFixSuggestions (may be empty on parse failure).
118 """
119 from lintro.ai.models import AIFixSuggestion
121 try:
122 data = json.loads(content)
123 except json.JSONDecodeError:
124 logger.debug(f"Failed to parse batch AI response for {file_path}")
125 return []
127 if not isinstance(data, list):
128 logger.debug(f"Batch response is not an array for {file_path}")
129 return []
131 results: list[AIFixSuggestion] = []
132 for item in data:
133 if not isinstance(item, dict):
134 continue
135 original = item.get("original_code", "")
136 suggested = item.get("suggested_code", "")
137 if (
138 not isinstance(original, str)
139 or not isinstance(suggested, str)
140 or not original
141 or not suggested
142 or original == suggested
143 ):
144 continue
145 raw_line = item.get("line", 0)
146 try:
147 line = int(raw_line) if not isinstance(raw_line, int) else raw_line
148 except (TypeError, ValueError):
149 line = 0
150 raw_code = item.get("code", "")
151 code = str(raw_code) if not isinstance(raw_code, str) else raw_code
152 diff = generate_diff(file_path, original, suggested)
153 results.append(
154 AIFixSuggestion(
155 file=file_path,
156 line=line,
157 code=code,
158 original_code=original,
159 suggested_code=suggested,
160 diff=diff,
161 explanation=item.get("explanation", ""),
162 confidence=item.get("confidence", "medium"),
163 risk_level=item.get("risk_level", ""),
164 ),
165 )
166 return results