lintro 0.13.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 (72) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +226 -16
  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/init.py +361 -0
  7. lintro/cli_utils/commands/list_tools.py +180 -42
  8. lintro/cli_utils/commands/test.py +316 -0
  9. lintro/cli_utils/commands/versions.py +81 -0
  10. lintro/config/__init__.py +62 -0
  11. lintro/config/config_loader.py +420 -0
  12. lintro/config/lintro_config.py +189 -0
  13. lintro/config/tool_config_generator.py +403 -0
  14. lintro/enums/tool_name.py +2 -0
  15. lintro/enums/tool_type.py +2 -0
  16. lintro/formatters/tools/__init__.py +12 -0
  17. lintro/formatters/tools/eslint_formatter.py +108 -0
  18. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  19. lintro/formatters/tools/pytest_formatter.py +201 -0
  20. lintro/parsers/__init__.py +69 -9
  21. lintro/parsers/bandit/__init__.py +6 -0
  22. lintro/parsers/bandit/bandit_issue.py +49 -0
  23. lintro/parsers/bandit/bandit_parser.py +99 -0
  24. lintro/parsers/black/black_issue.py +4 -0
  25. lintro/parsers/eslint/__init__.py +6 -0
  26. lintro/parsers/eslint/eslint_issue.py +26 -0
  27. lintro/parsers/eslint/eslint_parser.py +63 -0
  28. lintro/parsers/markdownlint/__init__.py +6 -0
  29. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  30. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  31. lintro/parsers/pytest/__init__.py +21 -0
  32. lintro/parsers/pytest/pytest_issue.py +28 -0
  33. lintro/parsers/pytest/pytest_parser.py +483 -0
  34. lintro/tools/__init__.py +2 -0
  35. lintro/tools/core/timeout_utils.py +112 -0
  36. lintro/tools/core/tool_base.py +255 -45
  37. lintro/tools/core/tool_manager.py +77 -24
  38. lintro/tools/core/version_requirements.py +482 -0
  39. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  40. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  41. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  42. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  43. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  44. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  45. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  46. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  47. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  48. lintro/tools/implementations/tool_actionlint.py +106 -16
  49. lintro/tools/implementations/tool_bandit.py +23 -7
  50. lintro/tools/implementations/tool_black.py +236 -29
  51. lintro/tools/implementations/tool_darglint.py +180 -21
  52. lintro/tools/implementations/tool_eslint.py +374 -0
  53. lintro/tools/implementations/tool_hadolint.py +94 -25
  54. lintro/tools/implementations/tool_markdownlint.py +354 -0
  55. lintro/tools/implementations/tool_prettier.py +313 -26
  56. lintro/tools/implementations/tool_pytest.py +327 -0
  57. lintro/tools/implementations/tool_ruff.py +247 -70
  58. lintro/tools/implementations/tool_yamllint.py +448 -34
  59. lintro/tools/tool_enum.py +6 -0
  60. lintro/utils/config.py +41 -18
  61. lintro/utils/console_logger.py +211 -25
  62. lintro/utils/path_utils.py +42 -0
  63. lintro/utils/tool_executor.py +336 -39
  64. lintro/utils/tool_utils.py +38 -2
  65. lintro/utils/unified_config.py +926 -0
  66. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
  67. lintro-0.17.2.dist-info/RECORD +134 -0
  68. lintro-0.13.2.dist-info/RECORD +0 -96
  69. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  70. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  71. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  72. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,403 @@
1
+ """Tool configuration generator for Lintro.
2
+
3
+ This module provides CLI argument injection for enforced settings and
4
+ default config generation for tools without native configs.
5
+
6
+ The tiered configuration model:
7
+ 1. EXECUTION: What tools run and how
8
+ 2. ENFORCE: Cross-cutting settings injected via CLI flags
9
+ 3. DEFAULTS: Fallback config when no native config exists
10
+ 4. TOOLS: Per-tool enable/disable and config source
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import atexit
16
+ import json
17
+ import os
18
+ import tempfile
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from loguru import logger
23
+
24
+ from lintro.config.lintro_config import LintroConfig
25
+
26
+ try:
27
+ import yaml
28
+ except ImportError:
29
+ yaml = None # type: ignore[assignment]
30
+
31
+
32
+ # CLI flags for enforced settings: setting -> {tool: flag}
33
+ ENFORCE_CLI_FLAGS: dict[str, dict[str, str]] = {
34
+ "line_length": {
35
+ "ruff": "--line-length",
36
+ "black": "--line-length",
37
+ "prettier": "--print-width",
38
+ },
39
+ "target_python": {
40
+ "ruff": "--target-version",
41
+ "black": "--target-version",
42
+ },
43
+ }
44
+
45
+ # Tool config format for defaults generation
46
+ TOOL_CONFIG_FORMATS: dict[str, str] = {
47
+ "prettier": "json",
48
+ "yamllint": "yaml",
49
+ "markdownlint": "json",
50
+ "hadolint": "yaml",
51
+ "bandit": "yaml",
52
+ }
53
+
54
+ # Native config file patterns for checking if tool has native config
55
+ NATIVE_CONFIG_PATTERNS: dict[str, list[str]] = {
56
+ "prettier": [
57
+ ".prettierrc",
58
+ ".prettierrc.json",
59
+ ".prettierrc.yaml",
60
+ ".prettierrc.yml",
61
+ ".prettierrc.js",
62
+ ".prettierrc.cjs",
63
+ ".prettierrc.toml",
64
+ "prettier.config.js",
65
+ "prettier.config.cjs",
66
+ ],
67
+ "markdownlint": [
68
+ ".markdownlint-cli2.jsonc",
69
+ ".markdownlint-cli2.yaml",
70
+ ".markdownlint-cli2.cjs",
71
+ ".markdownlint.jsonc",
72
+ ".markdownlint.json",
73
+ ".markdownlint.yaml",
74
+ ".markdownlint.yml",
75
+ ".markdownlint.cjs",
76
+ ],
77
+ "yamllint": [
78
+ ".yamllint",
79
+ ".yamllint.yaml",
80
+ ".yamllint.yml",
81
+ ],
82
+ "hadolint": [
83
+ ".hadolint.yaml",
84
+ ".hadolint.yml",
85
+ ],
86
+ "bandit": [
87
+ ".bandit",
88
+ ".bandit.yaml",
89
+ ".bandit.yml",
90
+ "bandit.yaml",
91
+ "bandit.yml",
92
+ ],
93
+ }
94
+
95
+ # Track temporary files for cleanup
96
+ _temp_files: list[Path] = []
97
+
98
+
99
+ def _cleanup_temp_files() -> None:
100
+ """Clean up temporary config files on exit."""
101
+ for temp_file in _temp_files:
102
+ try:
103
+ if temp_file.exists():
104
+ temp_file.unlink()
105
+ logger.debug(f"Cleaned up temp config: {temp_file}")
106
+ except Exception as e:
107
+ logger.debug(f"Failed to clean up {temp_file}: {e}")
108
+
109
+
110
+ # Register cleanup on exit
111
+ atexit.register(_cleanup_temp_files)
112
+
113
+
114
+ def get_enforce_cli_args(
115
+ tool_name: str,
116
+ lintro_config: LintroConfig,
117
+ ) -> list[str]:
118
+ """Get CLI arguments for enforced settings.
119
+
120
+ These settings override native tool configs to ensure consistency
121
+ across different tools for shared concerns like line length.
122
+
123
+ Args:
124
+ tool_name: Name of the tool (e.g., "ruff", "prettier").
125
+ lintro_config: Lintro configuration.
126
+
127
+ Returns:
128
+ list[str]: CLI arguments to inject (e.g., ["--line-length", "88"]).
129
+ """
130
+ args: list[str] = []
131
+ tool_lower = tool_name.lower()
132
+ enforce = lintro_config.enforce
133
+
134
+ # Inject line_length if set
135
+ if enforce.line_length is not None:
136
+ flag = ENFORCE_CLI_FLAGS.get("line_length", {}).get(tool_lower)
137
+ if flag:
138
+ args.extend([flag, str(enforce.line_length)])
139
+ logger.debug(
140
+ f"Injecting enforce.line_length={enforce.line_length} "
141
+ f"to {tool_name} as {flag}",
142
+ )
143
+
144
+ # Inject target_python if set
145
+ if enforce.target_python is not None:
146
+ flag = ENFORCE_CLI_FLAGS.get("target_python", {}).get(tool_lower)
147
+ if flag:
148
+ args.extend([flag, enforce.target_python])
149
+ logger.debug(
150
+ f"Injecting enforce.target_python={enforce.target_python} "
151
+ f"to {tool_name} as {flag}",
152
+ )
153
+
154
+ return args
155
+
156
+
157
+ def has_native_config(tool_name: str) -> bool:
158
+ """Check if a tool has a native config file in the project.
159
+
160
+ Searches for known native config file patterns starting from the
161
+ current working directory and moving upward to find the project root.
162
+
163
+ Args:
164
+ tool_name: Name of the tool (e.g., "prettier", "markdownlint").
165
+
166
+ Returns:
167
+ bool: True if a native config file exists.
168
+ """
169
+ tool_lower = tool_name.lower()
170
+ patterns = NATIVE_CONFIG_PATTERNS.get(tool_lower, [])
171
+
172
+ if not patterns:
173
+ return False
174
+
175
+ # Search from current directory upward
176
+ current = Path.cwd().resolve()
177
+
178
+ while True:
179
+ for pattern in patterns:
180
+ config_path = current / pattern
181
+ if config_path.exists():
182
+ logger.debug(
183
+ f"Found native config for {tool_name}: {config_path}",
184
+ )
185
+ return True
186
+
187
+ # Move up one directory
188
+ parent = current.parent
189
+ if parent == current:
190
+ # Reached filesystem root
191
+ break
192
+ current = parent
193
+
194
+ return False
195
+
196
+
197
+ def generate_defaults_config(
198
+ tool_name: str,
199
+ lintro_config: LintroConfig,
200
+ ) -> Path | None:
201
+ """Generate a temporary config file from defaults.
202
+
203
+ Only used when a tool has no native config file and defaults
204
+ are specified in the Lintro config.
205
+
206
+ Args:
207
+ tool_name: Name of the tool.
208
+ lintro_config: Lintro configuration.
209
+
210
+ Returns:
211
+ Path | None: Path to generated config file, or None if not needed.
212
+ """
213
+ tool_lower = tool_name.lower()
214
+
215
+ # Check if tool has native config - if so, don't generate defaults
216
+ if has_native_config(tool_lower):
217
+ logger.debug(
218
+ f"Tool {tool_name} has native config, skipping defaults generation",
219
+ )
220
+ return None
221
+
222
+ # Get defaults for this tool
223
+ defaults = lintro_config.get_tool_defaults(tool_lower)
224
+ if not defaults:
225
+ return None
226
+
227
+ # Get config format for this tool
228
+ config_format = TOOL_CONFIG_FORMATS.get(tool_lower, "json")
229
+
230
+ try:
231
+ return _write_defaults_config(
232
+ defaults=defaults,
233
+ tool_name=tool_lower,
234
+ config_format=config_format,
235
+ )
236
+ except Exception as e:
237
+ logger.error(f"Failed to generate defaults config for {tool_name}: {e}")
238
+ return None
239
+
240
+
241
+ def _write_defaults_config(
242
+ defaults: dict[str, Any],
243
+ tool_name: str,
244
+ config_format: str,
245
+ ) -> Path:
246
+ """Write defaults configuration to a temporary file.
247
+
248
+ Args:
249
+ defaults: Default configuration dictionary.
250
+ tool_name: Name of the tool.
251
+ config_format: Output format (json, yaml).
252
+
253
+ Returns:
254
+ Path: Path to temporary config file.
255
+
256
+ Raises:
257
+ ImportError: If PyYAML is not installed and YAML format is requested.
258
+ """
259
+ suffix_map = {"json": ".json", "yaml": ".yaml"}
260
+ suffix = suffix_map.get(config_format, ".json")
261
+
262
+ temp_fd, temp_path_str = tempfile.mkstemp(
263
+ prefix=f"lintro-{tool_name}-defaults-",
264
+ suffix=suffix,
265
+ )
266
+ os.close(temp_fd)
267
+ temp_path = Path(temp_path_str)
268
+ _temp_files.append(temp_path)
269
+
270
+ if config_format == "yaml":
271
+ if yaml is None:
272
+ raise ImportError("PyYAML required for YAML output")
273
+ content = yaml.dump(defaults, default_flow_style=False)
274
+ else:
275
+ content = json.dumps(defaults, indent=2)
276
+
277
+ temp_path.write_text(content, encoding="utf-8")
278
+ logger.debug(f"Generated defaults config for {tool_name}: {temp_path}")
279
+
280
+ return temp_path
281
+
282
+
283
+ def get_defaults_injection_args(
284
+ tool_name: str,
285
+ config_path: Path | None,
286
+ ) -> list[str]:
287
+ """Get CLI arguments to inject defaults config file into a tool.
288
+
289
+ Args:
290
+ tool_name: Name of the tool.
291
+ config_path: Path to defaults config file (or None).
292
+
293
+ Returns:
294
+ list[str]: CLI arguments to pass to the tool.
295
+ """
296
+ if config_path is None:
297
+ return []
298
+
299
+ tool_lower = tool_name.lower()
300
+ config_str = str(config_path)
301
+
302
+ # Tool-specific config flags
303
+ config_flags: dict[str, list[str]] = {
304
+ "prettier": ["--config", config_str],
305
+ "yamllint": ["-c", config_str],
306
+ "markdownlint": ["--config", config_str],
307
+ "hadolint": ["--config", config_str],
308
+ "bandit": ["-c", config_str],
309
+ }
310
+
311
+ return config_flags.get(tool_lower, [])
312
+
313
+
314
+ def cleanup_temp_config(config_path: Path) -> None:
315
+ """Explicitly clean up a temporary config file.
316
+
317
+ Args:
318
+ config_path: Path to temporary config file.
319
+ """
320
+ try:
321
+ if config_path in _temp_files:
322
+ _temp_files.remove(config_path)
323
+ if config_path.exists():
324
+ config_path.unlink()
325
+ logger.debug(f"Cleaned up temp config: {config_path}")
326
+ except Exception as e:
327
+ logger.debug(f"Failed to clean up {config_path}: {e}")
328
+
329
+
330
+ # =============================================================================
331
+ # DEPRECATED: Legacy functions for backward compatibility
332
+ # These will be removed in a future version.
333
+ # =============================================================================
334
+
335
+
336
+ def generate_tool_config(
337
+ tool_name: str,
338
+ lintro_config: LintroConfig,
339
+ ) -> Path | None:
340
+ """Generate a temporary configuration file for a tool.
341
+
342
+ DEPRECATED: This function is deprecated. Use get_enforce_cli_args() for
343
+ CLI flag injection and generate_defaults_config() for defaults.
344
+
345
+ Args:
346
+ tool_name: Name of the tool.
347
+ lintro_config: Lintro configuration.
348
+
349
+ Returns:
350
+ Path | None: Path to generated config file, or None.
351
+ """
352
+ logger.warning(
353
+ f"generate_tool_config() is deprecated for {tool_name}. "
354
+ "Use get_enforce_cli_args() instead.",
355
+ )
356
+ return generate_defaults_config(
357
+ tool_name=tool_name,
358
+ lintro_config=lintro_config,
359
+ )
360
+
361
+
362
+ def get_config_injection_args(
363
+ tool_name: str,
364
+ config_path: Path | None,
365
+ ) -> list[str]:
366
+ """Get CLI arguments to inject config file into a tool.
367
+
368
+ DEPRECATED: Use get_defaults_injection_args() instead.
369
+
370
+ Args:
371
+ tool_name: Name of the tool.
372
+ config_path: Path to config file (or None).
373
+
374
+ Returns:
375
+ list[str]: CLI arguments to pass to the tool.
376
+ """
377
+ logger.warning(
378
+ f"get_config_injection_args() is deprecated for {tool_name}. "
379
+ "Use get_defaults_injection_args() instead.",
380
+ )
381
+ return get_defaults_injection_args(
382
+ tool_name=tool_name,
383
+ config_path=config_path,
384
+ )
385
+
386
+
387
+ def get_no_auto_config_args(tool_name: str) -> list[str]:
388
+ """Get CLI arguments to disable auto-config discovery.
389
+
390
+ DEPRECATED: No longer needed with the tiered model.
391
+ Tools use their native configs by default.
392
+
393
+ Args:
394
+ tool_name: Name of the tool.
395
+
396
+ Returns:
397
+ list[str]: Empty list (no longer used).
398
+ """
399
+ logger.warning(
400
+ f"get_no_auto_config_args() is deprecated for {tool_name}. "
401
+ "No longer needed with the tiered config model.",
402
+ )
403
+ return []
lintro/enums/tool_name.py CHANGED
@@ -12,7 +12,9 @@ class ToolName(StrEnum):
12
12
  """Supported tool identifiers in lower-case values."""
13
13
 
14
14
  DARGLINT = auto()
15
+ ESLINT = auto()
15
16
  HADOLINT = auto()
17
+ MARKDOWNLINT = auto()
16
18
  PRETTIER = auto()
17
19
  RUFF = auto()
18
20
  YAMLLINT = auto()
lintro/enums/tool_type.py CHANGED
@@ -17,6 +17,7 @@ class ToolType(Flag):
17
17
  DOCUMENTATION = Tool that checks documentation
18
18
  SECURITY = Tool that checks for security issues
19
19
  INFRASTRUCTURE = Tool that checks infrastructure code
20
+ TEST_RUNNER = Tool that runs tests
20
21
  """
21
22
 
22
23
  LINTER = auto()
@@ -25,3 +26,4 @@ class ToolType(Flag):
25
26
  DOCUMENTATION = auto()
26
27
  SECURITY = auto()
27
28
  INFRASTRUCTURE = auto()
29
+ TEST_RUNNER = auto()
@@ -12,10 +12,18 @@ from lintro.formatters.tools.darglint_formatter import (
12
12
  DarglintTableDescriptor,
13
13
  format_darglint_issues,
14
14
  )
15
+ from lintro.formatters.tools.eslint_formatter import (
16
+ EslintTableDescriptor,
17
+ format_eslint_issues,
18
+ )
15
19
  from lintro.formatters.tools.hadolint_formatter import (
16
20
  HadolintTableDescriptor,
17
21
  format_hadolint_issues,
18
22
  )
23
+ from lintro.formatters.tools.markdownlint_formatter import (
24
+ MarkdownlintTableDescriptor,
25
+ format_markdownlint_issues,
26
+ )
19
27
  from lintro.formatters.tools.prettier_formatter import (
20
28
  PrettierTableDescriptor,
21
29
  format_prettier_issues,
@@ -36,8 +44,12 @@ __all__ = [
36
44
  "format_bandit_issues",
37
45
  "DarglintTableDescriptor",
38
46
  "format_darglint_issues",
47
+ "EslintTableDescriptor",
48
+ "format_eslint_issues",
39
49
  "HadolintTableDescriptor",
40
50
  "format_hadolint_issues",
51
+ "MarkdownlintTableDescriptor",
52
+ "format_markdownlint_issues",
41
53
  "PrettierTableDescriptor",
42
54
  "format_prettier_issues",
43
55
  "RuffTableDescriptor",
@@ -0,0 +1,108 @@
1
+ """Formatter for ESLint issues."""
2
+
3
+ from lintro.formatters.core.table_descriptor import TableDescriptor
4
+ from lintro.formatters.styles.csv import CsvStyle
5
+ from lintro.formatters.styles.grid import GridStyle
6
+ from lintro.formatters.styles.html import HtmlStyle
7
+ from lintro.formatters.styles.json import JsonStyle
8
+ from lintro.formatters.styles.markdown import MarkdownStyle
9
+ from lintro.formatters.styles.plain import PlainStyle
10
+ from lintro.parsers.eslint.eslint_issue import EslintIssue
11
+ from lintro.utils.path_utils import normalize_file_path_for_display
12
+
13
+ FORMAT_MAP = {
14
+ "plain": PlainStyle(),
15
+ "grid": GridStyle(),
16
+ "markdown": MarkdownStyle(),
17
+ "html": HtmlStyle(),
18
+ "json": JsonStyle(),
19
+ "csv": CsvStyle(),
20
+ }
21
+
22
+
23
+ class EslintTableDescriptor(TableDescriptor):
24
+ """Describe columns and rows for ESLint issues."""
25
+
26
+ def get_columns(self) -> list[str]:
27
+ """Return ordered column headers for the ESLint table.
28
+
29
+ Returns:
30
+ list[str]: Column names for the formatted table.
31
+ """
32
+ return ["File", "Line", "Column", "Code", "Severity", "Message"]
33
+
34
+ def get_rows(
35
+ self,
36
+ issues: list[EslintIssue],
37
+ ) -> list[list[str]]:
38
+ """Return rows for the ESLint issues table.
39
+
40
+ Args:
41
+ issues: Parsed ESLint issues to render.
42
+
43
+ Returns:
44
+ list[list[str]]: Table rows with normalized file path and fields.
45
+ """
46
+ rows = []
47
+ for issue in issues:
48
+ severity_str = "error" if issue.severity == 2 else "warning"
49
+ rows.append(
50
+ [
51
+ normalize_file_path_for_display(issue.file),
52
+ str(issue.line),
53
+ str(issue.column),
54
+ issue.code,
55
+ severity_str,
56
+ issue.message,
57
+ ],
58
+ )
59
+ return rows
60
+
61
+
62
+ def format_eslint_issues(
63
+ issues: list[EslintIssue],
64
+ format: str = "grid",
65
+ ) -> str:
66
+ """Format ESLint issues with auto-fixable labeling.
67
+
68
+ Args:
69
+ issues: List of EslintIssue objects.
70
+ format: Output format identifier (e.g., "grid", "json").
71
+
72
+ Returns:
73
+ str: Formatted output string.
74
+
75
+ Notes:
76
+ ESLint issues can be auto-fixable if the fixable flag is True.
77
+ For non-JSON formats, issues are split into auto-fixable and
78
+ not auto-fixable sections.
79
+ JSON returns the combined table for compatibility.
80
+ """
81
+ descriptor = EslintTableDescriptor()
82
+ formatter = FORMAT_MAP.get(format, GridStyle())
83
+
84
+ if format == "json":
85
+ columns = descriptor.get_columns()
86
+ rows = descriptor.get_rows(issues)
87
+ return formatter.format(columns=columns, rows=rows, tool_name="eslint")
88
+
89
+ # Split issues by fixability
90
+ fixable_issues = [i for i in issues if i.fixable]
91
+ non_fixable_issues = [i for i in issues if not i.fixable]
92
+
93
+ sections: list[str] = []
94
+ if fixable_issues:
95
+ columns = descriptor.get_columns()
96
+ rows = descriptor.get_rows(fixable_issues)
97
+ table = formatter.format(columns=columns, rows=rows)
98
+ sections.append("Auto-fixable issues\n" + table)
99
+ if non_fixable_issues:
100
+ columns = descriptor.get_columns()
101
+ rows = descriptor.get_rows(non_fixable_issues)
102
+ table = formatter.format(columns=columns, rows=rows)
103
+ sections.append("Not auto-fixable issues\n" + table)
104
+
105
+ if not sections:
106
+ return "No issues found."
107
+
108
+ return "\n\n".join(sections)
@@ -0,0 +1,88 @@
1
+ """Formatter for Markdownlint issues."""
2
+
3
+ from lintro.formatters.core.table_descriptor import TableDescriptor
4
+ from lintro.formatters.styles.csv import CsvStyle
5
+ from lintro.formatters.styles.grid import GridStyle
6
+ from lintro.formatters.styles.html import HtmlStyle
7
+ from lintro.formatters.styles.json import JsonStyle
8
+ from lintro.formatters.styles.markdown import MarkdownStyle
9
+ from lintro.formatters.styles.plain import PlainStyle
10
+ from lintro.parsers.markdownlint.markdownlint_issue import MarkdownlintIssue
11
+ from lintro.utils.path_utils import normalize_file_path_for_display
12
+
13
+
14
+ class MarkdownlintTableDescriptor(TableDescriptor):
15
+ """Describe columns and rows for Markdownlint issues."""
16
+
17
+ def get_columns(self) -> list[str]:
18
+ """Return ordered column headers for the Markdownlint table.
19
+
20
+ Returns:
21
+ list[str]: Column names for the formatted table.
22
+ """
23
+ return ["File", "Line", "Column", "Code", "Message"]
24
+
25
+ def get_rows(
26
+ self,
27
+ issues: list[MarkdownlintIssue],
28
+ ) -> list[list[str]]:
29
+ """Return rows for the Markdownlint issues table.
30
+
31
+ Args:
32
+ issues: Parsed Markdownlint issues to render.
33
+
34
+ Returns:
35
+ list[list[str]]: Table rows with normalized file path and fields.
36
+ """
37
+ rows = []
38
+ for issue in issues:
39
+ rows.append(
40
+ [
41
+ normalize_file_path_for_display(issue.file),
42
+ str(issue.line),
43
+ str(issue.column) if issue.column is not None else "-",
44
+ issue.code,
45
+ issue.message,
46
+ ],
47
+ )
48
+ return rows
49
+
50
+
51
+ def format_markdownlint_issues(
52
+ issues: list[MarkdownlintIssue],
53
+ format: str = "grid",
54
+ *,
55
+ tool_name: str = "markdownlint",
56
+ ) -> str:
57
+ """Format Markdownlint issues to the given style.
58
+
59
+ Args:
60
+ issues: List of MarkdownlintIssue instances.
61
+ format: Output style identifier.
62
+ tool_name: Tool name for JSON metadata.
63
+
64
+ Returns:
65
+ str: Rendered string for the issues table.
66
+ """
67
+ descriptor = MarkdownlintTableDescriptor()
68
+ columns = descriptor.get_columns()
69
+ rows = descriptor.get_rows(issues)
70
+
71
+ if not rows:
72
+ return "No issues found."
73
+
74
+ style = (format or "grid").lower()
75
+ if style == "grid":
76
+ return GridStyle().format(columns=columns, rows=rows)
77
+ if style == "plain":
78
+ return PlainStyle().format(columns=columns, rows=rows)
79
+ if style == "markdown":
80
+ return MarkdownStyle().format(columns=columns, rows=rows)
81
+ if style == "html":
82
+ return HtmlStyle().format(columns=columns, rows=rows)
83
+ if style == "json":
84
+ return JsonStyle().format(columns=columns, rows=rows, tool_name=tool_name)
85
+ if style == "csv":
86
+ return CsvStyle().format(columns=columns, rows=rows)
87
+
88
+ return GridStyle().format(columns=columns, rows=rows)