lintro 0.6.2__py3-none-any.whl → 0.17.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +230 -14
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/format.py +2 -2
  7. lintro/cli_utils/commands/init.py +361 -0
  8. lintro/cli_utils/commands/list_tools.py +180 -42
  9. lintro/cli_utils/commands/test.py +316 -0
  10. lintro/cli_utils/commands/versions.py +81 -0
  11. lintro/config/__init__.py +62 -0
  12. lintro/config/config_loader.py +420 -0
  13. lintro/config/lintro_config.py +189 -0
  14. lintro/config/tool_config_generator.py +403 -0
  15. lintro/enums/__init__.py +1 -0
  16. lintro/enums/darglint_strictness.py +10 -0
  17. lintro/enums/hadolint_enums.py +22 -0
  18. lintro/enums/tool_name.py +2 -0
  19. lintro/enums/tool_type.py +2 -0
  20. lintro/enums/yamllint_format.py +11 -0
  21. lintro/exceptions/__init__.py +1 -0
  22. lintro/formatters/__init__.py +1 -0
  23. lintro/formatters/core/__init__.py +1 -0
  24. lintro/formatters/core/output_style.py +11 -0
  25. lintro/formatters/core/table_descriptor.py +8 -0
  26. lintro/formatters/styles/csv.py +2 -0
  27. lintro/formatters/styles/grid.py +2 -0
  28. lintro/formatters/styles/html.py +2 -0
  29. lintro/formatters/styles/json.py +2 -0
  30. lintro/formatters/styles/markdown.py +2 -0
  31. lintro/formatters/styles/plain.py +2 -0
  32. lintro/formatters/tools/__init__.py +12 -0
  33. lintro/formatters/tools/black_formatter.py +27 -5
  34. lintro/formatters/tools/darglint_formatter.py +16 -1
  35. lintro/formatters/tools/eslint_formatter.py +108 -0
  36. lintro/formatters/tools/hadolint_formatter.py +13 -0
  37. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  38. lintro/formatters/tools/prettier_formatter.py +15 -0
  39. lintro/formatters/tools/pytest_formatter.py +201 -0
  40. lintro/formatters/tools/ruff_formatter.py +26 -5
  41. lintro/formatters/tools/yamllint_formatter.py +14 -1
  42. lintro/models/__init__.py +1 -0
  43. lintro/models/core/__init__.py +1 -0
  44. lintro/models/core/tool_config.py +11 -7
  45. lintro/parsers/__init__.py +69 -9
  46. lintro/parsers/actionlint/actionlint_parser.py +1 -1
  47. lintro/parsers/bandit/__init__.py +6 -0
  48. lintro/parsers/bandit/bandit_issue.py +49 -0
  49. lintro/parsers/bandit/bandit_parser.py +99 -0
  50. lintro/parsers/black/black_issue.py +4 -0
  51. lintro/parsers/darglint/__init__.py +1 -0
  52. lintro/parsers/darglint/darglint_issue.py +11 -0
  53. lintro/parsers/eslint/__init__.py +6 -0
  54. lintro/parsers/eslint/eslint_issue.py +26 -0
  55. lintro/parsers/eslint/eslint_parser.py +63 -0
  56. lintro/parsers/markdownlint/__init__.py +6 -0
  57. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  58. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  59. lintro/parsers/prettier/__init__.py +1 -0
  60. lintro/parsers/prettier/prettier_issue.py +12 -0
  61. lintro/parsers/prettier/prettier_parser.py +1 -1
  62. lintro/parsers/pytest/__init__.py +21 -0
  63. lintro/parsers/pytest/pytest_issue.py +28 -0
  64. lintro/parsers/pytest/pytest_parser.py +483 -0
  65. lintro/parsers/ruff/ruff_parser.py +6 -2
  66. lintro/parsers/yamllint/__init__.py +1 -0
  67. lintro/tools/__init__.py +3 -1
  68. lintro/tools/core/__init__.py +1 -0
  69. lintro/tools/core/timeout_utils.py +112 -0
  70. lintro/tools/core/tool_base.py +286 -50
  71. lintro/tools/core/tool_manager.py +77 -24
  72. lintro/tools/core/version_requirements.py +482 -0
  73. lintro/tools/implementations/__init__.py +1 -0
  74. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  75. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  76. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  77. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  78. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  79. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  80. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  81. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  82. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  83. lintro/tools/implementations/tool_actionlint.py +106 -16
  84. lintro/tools/implementations/tool_bandit.py +34 -29
  85. lintro/tools/implementations/tool_black.py +236 -29
  86. lintro/tools/implementations/tool_darglint.py +183 -22
  87. lintro/tools/implementations/tool_eslint.py +374 -0
  88. lintro/tools/implementations/tool_hadolint.py +94 -25
  89. lintro/tools/implementations/tool_markdownlint.py +354 -0
  90. lintro/tools/implementations/tool_prettier.py +317 -24
  91. lintro/tools/implementations/tool_pytest.py +327 -0
  92. lintro/tools/implementations/tool_ruff.py +278 -84
  93. lintro/tools/implementations/tool_yamllint.py +448 -34
  94. lintro/tools/tool_enum.py +8 -0
  95. lintro/utils/__init__.py +1 -0
  96. lintro/utils/ascii_normalize_cli.py +5 -0
  97. lintro/utils/config.py +41 -18
  98. lintro/utils/console_logger.py +211 -25
  99. lintro/utils/path_utils.py +42 -0
  100. lintro/utils/tool_executor.py +339 -45
  101. lintro/utils/tool_utils.py +51 -24
  102. lintro/utils/unified_config.py +926 -0
  103. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
  104. lintro-0.17.2.dist-info/RECORD +134 -0
  105. lintro-0.6.2.dist-info/RECORD +0 -96
  106. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  107. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  108. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  109. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,14 @@
1
1
  """Prettier code formatter integration."""
2
2
 
3
3
  import os
4
+ import subprocess # nosec B404 - used safely with shell disabled
4
5
  from dataclasses import dataclass, field
5
6
 
6
7
  from loguru import logger
7
8
 
8
9
  from lintro.enums.tool_type import ToolType
9
10
  from lintro.models.core.tool import Tool, ToolConfig, ToolResult
11
+ from lintro.parsers.prettier.prettier_issue import PrettierIssue
10
12
  from lintro.parsers.prettier.prettier_parser import parse_prettier_output
11
13
  from lintro.tools.core.tool_base import BaseTool
12
14
  from lintro.utils.tool_utils import walk_files_with_excludes
@@ -55,12 +57,21 @@ class PrettierTool(BaseTool):
55
57
  ),
56
58
  )
57
59
 
60
+ def __post_init__(self) -> None:
61
+ """Initialize prettier tool."""
62
+ super().__post_init__()
63
+ # Note: .prettierignore is handled by passing --ignore-path to prettier
64
+ # rather than loading into lintro's exclude patterns, to ensure prettier's
65
+ # native ignore logic is used consistently
66
+
58
67
  def set_options(
59
68
  self,
60
69
  exclude_patterns: list[str] | None = None,
61
70
  include_venv: bool = False,
62
71
  timeout: int | None = None,
63
72
  verbose_fix_output: bool | None = None,
73
+ line_length: int | None = None,
74
+ **kwargs,
64
75
  ) -> None:
65
76
  """Set options for the core.
66
77
 
@@ -69,13 +80,26 @@ class PrettierTool(BaseTool):
69
80
  include_venv: Whether to include virtual environment directories
70
81
  timeout: Timeout in seconds per file (default: 30)
71
82
  verbose_fix_output: If True, include raw Prettier output in fix()
83
+ line_length: Print width for prettier (maps to --print-width).
84
+ If provided, this will be stored and used in CLI args.
85
+ **kwargs: Additional options (ignored for compatibility)
86
+
87
+ Raises:
88
+ ValueError: If line_length is not a positive integer.
72
89
  """
73
- self.exclude_patterns = exclude_patterns or []
90
+ if exclude_patterns is not None:
91
+ self.exclude_patterns = exclude_patterns.copy()
74
92
  self.include_venv = include_venv
75
93
  if timeout is not None:
76
94
  self.timeout = timeout
77
95
  if verbose_fix_output is not None:
78
96
  self.options["verbose_fix_output"] = verbose_fix_output
97
+ if line_length is not None:
98
+ if not isinstance(line_length, int):
99
+ raise ValueError("line_length must be an integer")
100
+ if line_length <= 0:
101
+ raise ValueError("line_length must be positive")
102
+ self.options["line_length"] = line_length
79
103
 
80
104
  def _find_config(self) -> str | None:
81
105
  """Locate a Prettier config if none is found by native discovery.
@@ -90,6 +114,150 @@ class PrettierTool(BaseTool):
90
114
  """
91
115
  return None
92
116
 
117
+ def _find_prettier_config(self, search_dir: str | None = None) -> str | None:
118
+ """Locate prettier config file by walking up the directory tree.
119
+
120
+ Prettier searches upward from the file's directory to find config files,
121
+ so we do the same to match native behavior and ensure config is found
122
+ even when cwd is a subdirectory.
123
+
124
+ Args:
125
+ search_dir: Directory to start searching from. If None, searches from
126
+ current working directory.
127
+
128
+ Returns:
129
+ str | None: Path to config file if found, None otherwise.
130
+ """
131
+ config_paths = [
132
+ ".prettierrc",
133
+ ".prettierrc.json",
134
+ ".prettierrc.js",
135
+ ".prettierrc.yaml",
136
+ ".prettierrc.yml",
137
+ "prettier.config.js",
138
+ "package.json",
139
+ ]
140
+ # Search upward from search_dir (or cwd) to find config, just like prettier does
141
+ start_dir = os.path.abspath(search_dir) if search_dir else os.getcwd()
142
+ current_dir = start_dir
143
+
144
+ # Walk upward from the directory to find config
145
+ # Stop at filesystem root to avoid infinite loop
146
+ while True:
147
+ for config_name in config_paths:
148
+ config_path = os.path.join(current_dir, config_name)
149
+ if os.path.exists(config_path):
150
+ # For package.json, check if it contains prettier config
151
+ if config_name == "package.json":
152
+ try:
153
+ import json
154
+
155
+ with open(config_path, encoding="utf-8") as f:
156
+ pkg_data = json.load(f)
157
+ if "prettier" not in pkg_data:
158
+ continue
159
+ except (
160
+ json.JSONDecodeError,
161
+ FileNotFoundError,
162
+ PermissionError,
163
+ ):
164
+ # Skip invalid or unreadable package.json files
165
+ continue
166
+ logger.debug(
167
+ f"[PrettierTool] Found config file: {config_path} "
168
+ f"(searched from {start_dir})",
169
+ )
170
+ return config_path
171
+
172
+ # Move up one directory
173
+ parent_dir = os.path.dirname(current_dir)
174
+ # Stop if we've reached the filesystem root (parent == current)
175
+ if parent_dir == current_dir:
176
+ break
177
+ current_dir = parent_dir
178
+
179
+ return None
180
+
181
+ def _find_prettierignore(self, search_dir: str | None = None) -> str | None:
182
+ """Locate .prettierignore file by walking up the directory tree.
183
+
184
+ Prettier searches upward from the file's directory to find .prettierignore,
185
+ so we do the same to match native behavior and ensure ignore file is found
186
+ even when cwd is a subdirectory.
187
+
188
+ Args:
189
+ search_dir: Directory to start searching from. If None, searches from
190
+ current working directory.
191
+
192
+ Returns:
193
+ str | None: Path to .prettierignore file if found, None otherwise.
194
+ """
195
+ ignore_filename = ".prettierignore"
196
+ # Search upward from search_dir (or cwd) to find ignore file
197
+ start_dir = os.path.abspath(search_dir) if search_dir else os.getcwd()
198
+ current_dir = start_dir
199
+
200
+ # Walk upward from the directory to find ignore file
201
+ # Stop at filesystem root to avoid infinite loop
202
+ while True:
203
+ ignore_path = os.path.join(current_dir, ignore_filename)
204
+ if os.path.exists(ignore_path):
205
+ logger.debug(
206
+ f"[PrettierTool] Found .prettierignore: {ignore_path} "
207
+ f"(searched from {start_dir})",
208
+ )
209
+ return ignore_path
210
+
211
+ # Move up one directory
212
+ parent_dir = os.path.dirname(current_dir)
213
+ # Stop if we've reached the filesystem root (parent == current)
214
+ if parent_dir == current_dir:
215
+ break
216
+ current_dir = parent_dir
217
+
218
+ return None
219
+
220
+ def _create_timeout_result(
221
+ self,
222
+ timeout_val: int,
223
+ initial_issues: list | None = None,
224
+ initial_count: int = 0,
225
+ ) -> ToolResult:
226
+ """Create a ToolResult for timeout scenarios.
227
+
228
+ Args:
229
+ timeout_val: The timeout value that was exceeded.
230
+ initial_issues: Optional list of issues found before timeout.
231
+ initial_count: Optional count of initial issues.
232
+
233
+ Returns:
234
+ ToolResult: ToolResult instance representing timeout failure.
235
+ """
236
+ timeout_msg = (
237
+ f"Prettier execution timed out ({timeout_val}s limit exceeded).\n\n"
238
+ "This may indicate:\n"
239
+ " - Large codebase taking too long to process\n"
240
+ " - Need to increase timeout via --tool-options prettier:timeout=N"
241
+ )
242
+ timeout_issue = PrettierIssue(
243
+ file="execution",
244
+ line=None,
245
+ code="TIMEOUT",
246
+ message=timeout_msg,
247
+ column=None,
248
+ )
249
+ combined_issues = (initial_issues or []) + [timeout_issue]
250
+ return ToolResult(
251
+ name=self.name,
252
+ success=False,
253
+ output=timeout_msg,
254
+ issues_count=len(combined_issues),
255
+ issues=combined_issues,
256
+ initial_issues_count=initial_count,
257
+ fixed_issues_count=0,
258
+ remaining_issues_count=len(combined_issues),
259
+ )
260
+
93
261
  def check(
94
262
  self,
95
263
  paths: list[str],
@@ -102,6 +270,11 @@ class PrettierTool(BaseTool):
102
270
  Returns:
103
271
  ToolResult instance
104
272
  """
273
+ # Check version requirements
274
+ version_result = self._verify_tool_version()
275
+ if version_result is not None:
276
+ return version_result
277
+
105
278
  self._validate_paths(paths=paths)
106
279
  prettier_files: list[str] = walk_files_with_excludes(
107
280
  paths=paths,
@@ -109,6 +282,17 @@ class PrettierTool(BaseTool):
109
282
  exclude_patterns=self.exclude_patterns,
110
283
  include_venv=self.include_venv,
111
284
  )
285
+ logger.debug(
286
+ f"[PrettierTool] Discovered {len(prettier_files)} files matching patterns: "
287
+ f"{self.config.file_patterns}",
288
+ )
289
+ logger.debug(
290
+ f"[PrettierTool] Exclude patterns applied: {self.exclude_patterns}",
291
+ )
292
+ if prettier_files:
293
+ logger.debug(
294
+ f"[PrettierTool] Files to check (first 10): " f"{prettier_files[:10]}",
295
+ )
112
296
  if not prettier_files:
113
297
  return Tool.to_result(
114
298
  name=self.name,
@@ -118,6 +302,7 @@ class PrettierTool(BaseTool):
118
302
  )
119
303
  # Use relative paths and set cwd to the common parent
120
304
  cwd: str = self.get_cwd(paths=prettier_files)
305
+ logger.debug(f"[PrettierTool] Working directory: {cwd}")
121
306
  rel_files: list[str] = [
122
307
  os.path.relpath(f, cwd) if cwd else f for f in prettier_files
123
308
  ]
@@ -125,14 +310,53 @@ class PrettierTool(BaseTool):
125
310
  cmd: list[str] = self._get_executable_command(tool_name="prettier") + [
126
311
  "--check",
127
312
  ]
128
- # Do not force config; rely on native discovery via cwd
313
+
314
+ # Add Lintro config injection args (--no-config, --config)
315
+ # This takes precedence over native config auto-discovery
316
+ config_args = self._build_config_args()
317
+ if config_args:
318
+ cmd.extend(config_args)
319
+ logger.debug(
320
+ "[PrettierTool] Using Lintro config injection",
321
+ )
322
+ else:
323
+ # Fallback: Find config and ignore files by walking up from cwd
324
+ found_config = self._find_prettier_config(search_dir=cwd)
325
+ if found_config:
326
+ logger.debug(
327
+ f"[PrettierTool] Found config: {found_config} (auto-detecting)",
328
+ )
329
+ else:
330
+ logger.debug(
331
+ "[PrettierTool] No prettier config file found (using defaults)",
332
+ )
333
+ # Apply line_length as --print-width if set and no config found
334
+ line_length = self.options.get("line_length")
335
+ if line_length:
336
+ cmd.extend(["--print-width", str(line_length)])
337
+ logger.debug(
338
+ "[PrettierTool] Using --print-width=%s from options",
339
+ line_length,
340
+ )
341
+ # Find .prettierignore by walking up from cwd
342
+ prettierignore_path = self._find_prettierignore(search_dir=cwd)
343
+ if prettierignore_path:
344
+ logger.debug(
345
+ f"[PrettierTool] Found .prettierignore: {prettierignore_path} "
346
+ "(auto-detecting)",
347
+ )
348
+
129
349
  cmd.extend(rel_files)
130
350
  logger.debug(f"[PrettierTool] Running: {' '.join(cmd)} (cwd={cwd})")
131
- result = self._run_subprocess(
132
- cmd=cmd,
133
- timeout=self.options.get("timeout", self._default_timeout),
134
- cwd=cwd,
135
- )
351
+ timeout_val: int = self.options.get("timeout", self._default_timeout)
352
+ try:
353
+ result = self._run_subprocess(
354
+ cmd=cmd,
355
+ timeout=timeout_val,
356
+ cwd=cwd,
357
+ )
358
+ except subprocess.TimeoutExpired:
359
+ return self._create_timeout_result(timeout_val=timeout_val)
136
360
  output: str = result[1]
137
361
  # Do not filter lines post-hoc; rely on discovery and ignore files
138
362
  issues: list = parse_prettier_output(output=output)
@@ -142,11 +366,14 @@ class PrettierTool(BaseTool):
142
366
  # so the unified logger prints a single, consistent success line.
143
367
  if success:
144
368
  output = None
145
- return Tool.to_result(
369
+
370
+ # Return full ToolResult so table rendering can use parsed issues
371
+ return ToolResult(
146
372
  name=self.name,
147
373
  success=success,
148
374
  output=output,
149
375
  issues_count=issues_count,
376
+ issues=issues,
150
377
  )
151
378
 
152
379
  def fix(
@@ -161,6 +388,11 @@ class PrettierTool(BaseTool):
161
388
  Returns:
162
389
  ToolResult: Result object with counts and messages.
163
390
  """
391
+ # Check version requirements
392
+ version_result = self._verify_tool_version()
393
+ if version_result is not None:
394
+ return version_result
395
+
164
396
  self._validate_paths(paths=paths)
165
397
  prettier_files: list[str] = walk_files_with_excludes(
166
398
  paths=paths,
@@ -182,18 +414,55 @@ class PrettierTool(BaseTool):
182
414
  os.path.relpath(f, cwd) if cwd else f for f in prettier_files
183
415
  ]
184
416
 
417
+ # Get Lintro config injection args (--no-config, --config)
418
+ config_args = self._build_config_args()
419
+ fallback_args: list[str] = []
420
+ if not config_args:
421
+ # Fallback: Find config and ignore files by walking up from cwd
422
+ found_config = self._find_prettier_config(search_dir=cwd)
423
+ if found_config:
424
+ logger.debug(
425
+ f"[PrettierTool] Found config: {found_config} (auto-detecting)",
426
+ )
427
+ else:
428
+ logger.debug(
429
+ "[PrettierTool] No prettier config file found (using defaults)",
430
+ )
431
+ # Apply line_length as --print-width if set and no config found
432
+ line_length = self.options.get("line_length")
433
+ if line_length:
434
+ fallback_args.extend(["--print-width", str(line_length)])
435
+ logger.debug(
436
+ "[PrettierTool] Using --print-width=%s from options",
437
+ line_length,
438
+ )
439
+ prettierignore_path = self._find_prettierignore(search_dir=cwd)
440
+ if prettierignore_path:
441
+ logger.debug(
442
+ f"[PrettierTool] Found .prettierignore: {prettierignore_path} "
443
+ "(auto-detecting)",
444
+ )
445
+
185
446
  # Check for issues first
186
447
  check_cmd: list[str] = self._get_executable_command(tool_name="prettier") + [
187
448
  "--check",
188
449
  ]
189
- # Do not force config; rely on native discovery via cwd
450
+ # Add Lintro config injection if available, otherwise use fallback args
451
+ if config_args:
452
+ check_cmd.extend(config_args)
453
+ elif fallback_args:
454
+ check_cmd.extend(fallback_args)
190
455
  check_cmd.extend(rel_files)
191
456
  logger.debug(f"[PrettierTool] Checking: {' '.join(check_cmd)} (cwd={cwd})")
192
- check_result = self._run_subprocess(
193
- cmd=check_cmd,
194
- timeout=self.options.get("timeout", self._default_timeout),
195
- cwd=cwd,
196
- )
457
+ timeout_val: int = self.options.get("timeout", self._default_timeout)
458
+ try:
459
+ check_result = self._run_subprocess(
460
+ cmd=check_cmd,
461
+ timeout=timeout_val,
462
+ cwd=cwd,
463
+ )
464
+ except subprocess.TimeoutExpired:
465
+ return self._create_timeout_result(timeout_val=timeout_val)
197
466
  check_output: str = check_result[1]
198
467
 
199
468
  # Parse initial issues
@@ -204,21 +473,40 @@ class PrettierTool(BaseTool):
204
473
  fix_cmd: list[str] = self._get_executable_command(tool_name="prettier") + [
205
474
  "--write",
206
475
  ]
476
+ # Add Lintro config injection if available, otherwise use fallback args
477
+ if config_args:
478
+ fix_cmd.extend(config_args)
479
+ elif fallback_args:
480
+ fix_cmd.extend(fallback_args)
207
481
  fix_cmd.extend(rel_files)
208
482
  logger.debug(f"[PrettierTool] Fixing: {' '.join(fix_cmd)} (cwd={cwd})")
209
- fix_result = self._run_subprocess(
210
- cmd=fix_cmd,
211
- timeout=self.options.get("timeout", self._default_timeout),
212
- cwd=cwd,
213
- )
483
+ try:
484
+ fix_result = self._run_subprocess(
485
+ cmd=fix_cmd,
486
+ timeout=timeout_val,
487
+ cwd=cwd,
488
+ )
489
+ except subprocess.TimeoutExpired:
490
+ return self._create_timeout_result(
491
+ timeout_val=timeout_val,
492
+ initial_issues=initial_issues,
493
+ initial_count=initial_count,
494
+ )
214
495
  fix_output: str = fix_result[1]
215
496
 
216
497
  # Check for remaining issues after fixing
217
- final_check_result = self._run_subprocess(
218
- cmd=check_cmd,
219
- timeout=self.options.get("timeout", self._default_timeout),
220
- cwd=cwd,
221
- )
498
+ try:
499
+ final_check_result = self._run_subprocess(
500
+ cmd=check_cmd,
501
+ timeout=timeout_val,
502
+ cwd=cwd,
503
+ )
504
+ except subprocess.TimeoutExpired:
505
+ return self._create_timeout_result(
506
+ timeout_val=timeout_val,
507
+ initial_issues=initial_issues,
508
+ initial_count=initial_count,
509
+ )
222
510
  final_check_output: str = final_check_result[1]
223
511
  remaining_issues: list = parse_prettier_output(output=final_check_output)
224
512
  remaining_count: int = len(remaining_issues)
@@ -258,12 +546,17 @@ class PrettierTool(BaseTool):
258
546
  # Success means no remaining issues
259
547
  success: bool = remaining_count == 0
260
548
 
549
+ # Combine initial and remaining issues so formatter can split them by fixability
550
+ all_issues = (initial_issues or []) + (remaining_issues or [])
551
+
261
552
  return ToolResult(
262
553
  name=self.name,
263
554
  success=success,
264
555
  output=final_output,
265
556
  # For fix operations, issues_count represents remaining for summaries
266
557
  issues_count=remaining_count,
558
+ # Provide both initial (fixed) and remaining issues for display
559
+ issues=all_issues,
267
560
  initial_issues_count=initial_count,
268
561
  fixed_issues_count=fixed_count,
269
562
  remaining_issues_count=remaining_count,