Initial commit
This commit is contained in:
18
.claude-plugin/marketplace.json
Normal file
18
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||||
|
"name": "cargo-log-parser-marketplace",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Cargo build log parser skill for Rust debugging",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mathieu Gosbee"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "cargo-log-parser",
|
||||||
|
"description": "Parse and filter cargo build logs with regex-based filtering for errors, warnings, and diagnostics",
|
||||||
|
"source": "./",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "development"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "cargo-log-parser",
|
||||||
|
"description": "Parse and filter cargo build logs to extract errors, warnings, and diagnostics with regex-based filtering. Use for debugging Rust projects, analyzing cargo build output, or filtering errors by file/message/code patterns.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Mathieu"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# cargo-log-parser
|
||||||
|
|
||||||
|
Parse and filter cargo build logs to extract errors, warnings, and diagnostics with regex-based filtering. Use for debugging Rust projects, analyzing cargo build output, or filtering errors by file/message/code patterns.
|
||||||
94
SKILL.md
Normal file
94
SKILL.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
name: cargo-log-parser
|
||||||
|
description: Parse and filter cargo build logs to extract errors, warnings, and diagnostics with regex-based filtering. Use when debugging Rust projects, analyzing cargo build output, filtering errors by file path or message pattern, or helping users understand compilation failures. Triggers on cargo build errors, rustc diagnostics, Rust compilation issues, or requests to parse .log files from cargo.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cargo Log Parser
|
||||||
|
|
||||||
|
Parse cargo build logs to extract and filter errors/warnings with precise log boundaries.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From file
|
||||||
|
python scripts/cargo_log_parser.py build.log --errors
|
||||||
|
|
||||||
|
# From stdin (pipe from cargo)
|
||||||
|
cargo build 2>&1 | python scripts/cargo_log_parser.py --errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Filters
|
||||||
|
|
||||||
|
| Flag | Purpose | Example |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `-e, --errors` | Errors only | `--errors` |
|
||||||
|
| `-w, --warnings` | Warnings only | `--warnings` |
|
||||||
|
| `-f, --file PATTERN` | Filter by file path regex | `--file "tests/.*"` |
|
||||||
|
| `-m, --message PATTERN` | Filter by message regex | `--message "cannot find"` |
|
||||||
|
| `-c, --code PATTERN` | Filter by error code | `--code "E0425"` |
|
||||||
|
|
||||||
|
## Output Modes
|
||||||
|
|
||||||
|
| Flag | Output Style |
|
||||||
|
|------|--------------|
|
||||||
|
| (default) | Detailed LLM-friendly format with context |
|
||||||
|
| `--stream` | Compact one-line-per-diagnostic |
|
||||||
|
| `--raw` | Original log text for matches only |
|
||||||
|
| `--json` | Structured JSON output |
|
||||||
|
| `-v, --verbose` | Include raw log text in output |
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Errors in test files only
|
||||||
|
python scripts/cargo_log_parser.py build.log --errors --file "tests/.*"
|
||||||
|
|
||||||
|
# Find "not found" errors
|
||||||
|
python scripts/cargo_log_parser.py build.log --errors --message "cannot find|not found"
|
||||||
|
|
||||||
|
# All E04xx errors (name resolution)
|
||||||
|
python scripts/cargo_log_parser.py build.log --code "E04\d\d"
|
||||||
|
|
||||||
|
# Group errors by file
|
||||||
|
python scripts/cargo_log_parser.py build.log --errors --group-by-file
|
||||||
|
|
||||||
|
# Group by error code
|
||||||
|
python scripts/cargo_log_parser.py build.log --errors --group-by-code
|
||||||
|
|
||||||
|
# Quick summary
|
||||||
|
python scripts/cargo_log_parser.py build.log --summary
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python API
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cargo_log_parser import CargoLogParser, LogQuery, DiagnosticLevel
|
||||||
|
|
||||||
|
parser = CargoLogParser()
|
||||||
|
parsed = parser.parse_file("build.log")
|
||||||
|
query = LogQuery(parsed)
|
||||||
|
|
||||||
|
# Filter errors
|
||||||
|
errors = query.find_errors(file_pattern=r"tests/.*", message_pattern=r"cannot find")
|
||||||
|
|
||||||
|
# Get exact log boundaries
|
||||||
|
for error in errors:
|
||||||
|
bounds = query.get_error_boundaries(error)
|
||||||
|
print(f"Lines {bounds['line_start']}-{bounds['line_end']}")
|
||||||
|
print(bounds['raw_text'])
|
||||||
|
|
||||||
|
# Group and analyze
|
||||||
|
by_file = query.group_by_file(level=DiagnosticLevel.ERROR)
|
||||||
|
by_code = query.group_by_code()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regex Reference
|
||||||
|
|
||||||
|
| Pattern | Matches |
|
||||||
|
|---------|---------|
|
||||||
|
| `tests/.*` | Test files |
|
||||||
|
| `src/.*` | Source files |
|
||||||
|
| `.*/mod\.rs` | Any mod.rs |
|
||||||
|
| `E03\d\d` | Borrow checker errors |
|
||||||
|
| `E04\d\d` | Name resolution errors |
|
||||||
|
| `unused_.*` | Unused warnings |
|
||||||
57
plugin.lock.json
Normal file
57
plugin.lock.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:matbeedotcom/cargo-log-parser:",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "7c2e7bee82ffa818331eb3c2f035a7842e0381ac",
|
||||||
|
"treeHash": "afc2245d81d33e54d51aeefb22dc75e58906271a6ab8c1492ba81d7f42f65da7",
|
||||||
|
"generatedAt": "2025-11-28T10:27:02.507346Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "cargo-log-parser",
|
||||||
|
"description": "Parse and filter cargo build logs to extract errors, warnings, and diagnostics with regex-based filtering. Use for debugging Rust projects, analyzing cargo build output, or filtering errors by file/message/code patterns.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "LICENSE",
|
||||||
|
"sha256": "06dcddbb6908a0c6dd4a9e8ec822eea41d5a460a53089fecccc8a68049e99241"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "6b5376639cfb0991444aa691e46e7f333bdb2eedcefa51224edc5bf593fcdda3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "299603c375b9ce614884b93494ddfa7e42aab5f233cb512724dd6d2ac87bb22a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/cargo_log_parser.py",
|
||||||
|
"sha256": "14606977afeb0b2ddb06343e5067a769734a9e1f2259b2c3530bf42afaaf1345"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/marketplace.json",
|
||||||
|
"sha256": "6204b8a95a533d8a9e8cf214d240991b4ebba9e3091f2f44912440581e31cf8e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "69e5d0432c7601487dd87a5204eb07c2ba9095dd6d9f9b022e66d9ed3fbeed44"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "afc2245d81d33e54d51aeefb22dc75e58906271a6ab8c1492ba81d7f42f65da7"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
875
scripts/cargo_log_parser.py
Normal file
875
scripts/cargo_log_parser.py
Normal file
@@ -0,0 +1,875 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cargo Build Log Parser
|
||||||
|
|
||||||
|
A comprehensive parser for cargo build logs that extracts errors, warnings,
|
||||||
|
and notes with full context. Provides regex-based filtering by file path,
|
||||||
|
error message, error code, and more.
|
||||||
|
|
||||||
|
Supports stdin for piping directly from cargo build:
|
||||||
|
cargo build 2>&1 | cargo_log_parser.py --errors --file "tests/.*"
|
||||||
|
|
||||||
|
Designed for LLM consumption with structured output options.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO, Callable
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticLevel(Enum):
|
||||||
|
ERROR = "error"
|
||||||
|
WARNING = "warning"
|
||||||
|
NOTE = "note"
|
||||||
|
HELP = "help"
|
||||||
|
INFO = "info"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SourceLocation:
|
||||||
|
"""Represents a location in source code."""
|
||||||
|
file_path: str
|
||||||
|
line: Optional[int] = None
|
||||||
|
column: Optional[int] = None
|
||||||
|
end_line: Optional[int] = None
|
||||||
|
end_column: Optional[int] = None
|
||||||
|
|
||||||
|
def matches_file_regex(self, pattern: str) -> bool:
|
||||||
|
"""Check if file path matches regex pattern."""
|
||||||
|
return bool(re.search(pattern, self.file_path))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.line is not None:
|
||||||
|
if self.column is not None:
|
||||||
|
return f"{self.file_path}:{self.line}:{self.column}"
|
||||||
|
return f"{self.file_path}:{self.line}"
|
||||||
|
return self.file_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Diagnostic:
|
||||||
|
"""Represents a single diagnostic (error/warning/note) from cargo."""
|
||||||
|
level: DiagnosticLevel
|
||||||
|
message: str
|
||||||
|
code: Optional[str] = None # e.g., E0425, E0308
|
||||||
|
location: Optional[SourceLocation] = None
|
||||||
|
raw_text: str = "" # The complete raw text block
|
||||||
|
line_start: int = 0 # Line number in log file where this starts
|
||||||
|
line_end: int = 0 # Line number in log file where this ends
|
||||||
|
children: list = field(default_factory=list) # Sub-diagnostics (notes, help)
|
||||||
|
context_lines: list = field(default_factory=list) # Source code context
|
||||||
|
|
||||||
|
def matches_message_regex(self, pattern: str) -> bool:
|
||||||
|
"""Check if message matches regex pattern."""
|
||||||
|
return bool(re.search(pattern, self.message))
|
||||||
|
|
||||||
|
def matches_code(self, code: str) -> bool:
|
||||||
|
"""Check if error code matches (exact or pattern)."""
|
||||||
|
if self.code is None:
|
||||||
|
return False
|
||||||
|
return bool(re.search(code, self.code))
|
||||||
|
|
||||||
|
def matches_file_regex(self, pattern: str) -> bool:
|
||||||
|
"""Check if any location matches the file pattern."""
|
||||||
|
if self.location and self.location.matches_file_regex(pattern):
|
||||||
|
return True
|
||||||
|
for child in self.children:
|
||||||
|
if child.location and child.location.matches_file_regex(pattern):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert to dictionary for JSON serialization."""
|
||||||
|
result = {
|
||||||
|
"level": self.level.value,
|
||||||
|
"message": self.message,
|
||||||
|
"code": self.code,
|
||||||
|
"location": str(self.location) if self.location else None,
|
||||||
|
"log_lines": f"{self.line_start}-{self.line_end}",
|
||||||
|
"raw_text": self.raw_text,
|
||||||
|
}
|
||||||
|
if self.children:
|
||||||
|
result["children"] = [c.to_dict() for c in self.children]
|
||||||
|
if self.context_lines:
|
||||||
|
result["context"] = self.context_lines
|
||||||
|
return result
|
||||||
|
|
||||||
|
def summary(self) -> str:
|
||||||
|
"""Return a brief summary for listing."""
|
||||||
|
loc = f" at {self.location}" if self.location else ""
|
||||||
|
code = f"[{self.code}]" if self.code else ""
|
||||||
|
return f"{self.level.value}{code}: {self.message}{loc}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParsedLog:
|
||||||
|
"""Container for all parsed diagnostics from a cargo log."""
|
||||||
|
file_path: str
|
||||||
|
diagnostics: list = field(default_factory=list)
|
||||||
|
raw_lines: list = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self) -> list:
|
||||||
|
return [d for d in self.diagnostics if d.level == DiagnosticLevel.ERROR]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def warnings(self) -> list:
|
||||||
|
return [d for d in self.diagnostics if d.level == DiagnosticLevel.WARNING]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notes(self) -> list:
|
||||||
|
return [d for d in self.diagnostics if d.level == DiagnosticLevel.NOTE]
|
||||||
|
|
||||||
|
def filter(
|
||||||
|
self,
|
||||||
|
level: Optional[DiagnosticLevel] = None,
|
||||||
|
file_pattern: Optional[str] = None,
|
||||||
|
message_pattern: Optional[str] = None,
|
||||||
|
code_pattern: Optional[str] = None,
|
||||||
|
) -> list:
|
||||||
|
"""Filter diagnostics by various criteria."""
|
||||||
|
results = self.diagnostics
|
||||||
|
|
||||||
|
if level is not None:
|
||||||
|
results = [d for d in results if d.level == level]
|
||||||
|
|
||||||
|
if file_pattern is not None:
|
||||||
|
results = [d for d in results if d.matches_file_regex(file_pattern)]
|
||||||
|
|
||||||
|
if message_pattern is not None:
|
||||||
|
results = [d for d in results if d.matches_message_regex(message_pattern)]
|
||||||
|
|
||||||
|
if code_pattern is not None:
|
||||||
|
results = [d for d in results if d.matches_code(code_pattern)]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_log_slice(self, line_start: int, line_end: int) -> str:
|
||||||
|
"""Get a slice of the raw log by line numbers."""
|
||||||
|
return "\n".join(self.raw_lines[line_start:line_end + 1])
|
||||||
|
|
||||||
|
def summary(self) -> dict:
|
||||||
|
"""Return a summary of the parsed log."""
|
||||||
|
return {
|
||||||
|
"file": self.file_path,
|
||||||
|
"total_diagnostics": len(self.diagnostics),
|
||||||
|
"errors": len(self.errors),
|
||||||
|
"warnings": len(self.warnings),
|
||||||
|
"notes": len(self.notes),
|
||||||
|
"error_codes": list(set(d.code for d in self.errors if d.code)),
|
||||||
|
"warning_codes": list(set(d.code for d in self.warnings if d.code)),
|
||||||
|
"affected_files": list(set(
|
||||||
|
d.location.file_path for d in self.diagnostics
|
||||||
|
if d.location
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CargoLogParser:
|
||||||
|
"""
|
||||||
|
Parser for cargo build log files.
|
||||||
|
|
||||||
|
Handles the standard cargo output format including:
|
||||||
|
- error[E0XXX]: message
|
||||||
|
- warning[lint_name]: message
|
||||||
|
- note: message
|
||||||
|
- help: message
|
||||||
|
- Source code snippets with line numbers
|
||||||
|
- Multi-line diagnostics with proper boundaries
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pattern for the start of a diagnostic
|
||||||
|
DIAGNOSTIC_HEADER = re.compile(
|
||||||
|
r'^(error|warning|note|help|info)(\[(?P<code>[^\]]+)\])?:\s*(?P<message>.*)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern for source location: --> file:line:col
|
||||||
|
LOCATION_PATTERN = re.compile(
|
||||||
|
r'^\s*-->\s*(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Alternative location pattern: ::: file:line:col (for macro expansions)
|
||||||
|
ALT_LOCATION_PATTERN = re.compile(
|
||||||
|
r'^\s*:::\s*(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Source code line with line number
|
||||||
|
SOURCE_LINE_PATTERN = re.compile(
|
||||||
|
r'^\s*(?P<line_num>\d+)\s*\|(?P<code>.*)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Continuation/annotation line (with | but no line number)
|
||||||
|
ANNOTATION_PATTERN = re.compile(
|
||||||
|
r'^\s*\|(?P<content>.*)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# For aborting due to errors
|
||||||
|
ABORT_PATTERN = re.compile(
|
||||||
|
r'^(error|warning): (aborting due to|could not compile|build failed)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compilation stats
|
||||||
|
STATS_PATTERN = re.compile(
|
||||||
|
r'^(error|warning): `[^`]+` \(.*\) generated \d+ (error|warning)'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.diagnostics = []
|
||||||
|
self.raw_lines = []
|
||||||
|
|
||||||
|
def parse_file(self, file_path: str) -> ParsedLog:
|
||||||
|
"""Parse a cargo log file."""
|
||||||
|
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
content = f.read()
|
||||||
|
return self.parse_string(content, file_path)
|
||||||
|
|
||||||
|
def parse_stream(
|
||||||
|
self,
|
||||||
|
stream: TextIO,
|
||||||
|
source_name: str = "<stdin>",
|
||||||
|
on_diagnostic: Optional[Callable[[Diagnostic], None]] = None,
|
||||||
|
) -> ParsedLog:
|
||||||
|
"""
|
||||||
|
Parse cargo log from a stream (e.g., stdin).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stream: Input stream to read from
|
||||||
|
source_name: Name to use for the source
|
||||||
|
on_diagnostic: Optional callback called for each diagnostic as it's parsed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedLog with all parsed diagnostics
|
||||||
|
"""
|
||||||
|
content = stream.read()
|
||||||
|
parsed = self.parse_string(content, source_name)
|
||||||
|
|
||||||
|
if on_diagnostic:
|
||||||
|
for diag in parsed.diagnostics:
|
||||||
|
on_diagnostic(diag)
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
def parse_string(self, content: str, source_name: str = "<string>") -> ParsedLog:
|
||||||
|
"""Parse cargo log from a string."""
|
||||||
|
self.raw_lines = content.splitlines()
|
||||||
|
self.diagnostics = []
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(self.raw_lines):
|
||||||
|
line = self.raw_lines[i]
|
||||||
|
|
||||||
|
# Skip abort/stats messages
|
||||||
|
if self.ABORT_PATTERN.match(line) or self.STATS_PATTERN.match(line):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for diagnostic header
|
||||||
|
match = self.DIAGNOSTIC_HEADER.match(line)
|
||||||
|
if match:
|
||||||
|
diagnostic, end_line = self._parse_diagnostic(i)
|
||||||
|
if diagnostic:
|
||||||
|
self.diagnostics.append(diagnostic)
|
||||||
|
i = end_line + 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return ParsedLog(
|
||||||
|
file_path=source_name,
|
||||||
|
diagnostics=self.diagnostics,
|
||||||
|
raw_lines=self.raw_lines
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_diagnostic(self, start_line: int) -> tuple:
|
||||||
|
"""
|
||||||
|
Parse a complete diagnostic block starting at start_line.
|
||||||
|
Returns (Diagnostic, end_line_index).
|
||||||
|
"""
|
||||||
|
line = self.raw_lines[start_line]
|
||||||
|
match = self.DIAGNOSTIC_HEADER.match(line)
|
||||||
|
if not match:
|
||||||
|
return None, start_line
|
||||||
|
|
||||||
|
level_str = match.group(1)
|
||||||
|
level = DiagnosticLevel(level_str)
|
||||||
|
code = match.group('code')
|
||||||
|
message = match.group('message').strip()
|
||||||
|
|
||||||
|
diagnostic = Diagnostic(
|
||||||
|
level=level,
|
||||||
|
message=message,
|
||||||
|
code=code,
|
||||||
|
line_start=start_line,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_lines = [line]
|
||||||
|
context_lines = []
|
||||||
|
children = []
|
||||||
|
current_line = start_line + 1
|
||||||
|
|
||||||
|
# Parse the body of the diagnostic
|
||||||
|
while current_line < len(self.raw_lines):
|
||||||
|
line = self.raw_lines[current_line]
|
||||||
|
|
||||||
|
# Check if this is a new top-level diagnostic
|
||||||
|
if self.DIAGNOSTIC_HEADER.match(line):
|
||||||
|
# But first check if it's a child note/help (indented context)
|
||||||
|
if not line.startswith(' ') and not self._is_child_diagnostic(line, diagnostic):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for location
|
||||||
|
loc_match = self.LOCATION_PATTERN.match(line) or self.ALT_LOCATION_PATTERN.match(line)
|
||||||
|
if loc_match:
|
||||||
|
if diagnostic.location is None:
|
||||||
|
diagnostic.location = SourceLocation(
|
||||||
|
file_path=loc_match.group('file'),
|
||||||
|
line=int(loc_match.group('line')),
|
||||||
|
column=int(loc_match.group('col'))
|
||||||
|
)
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for source code line
|
||||||
|
src_match = self.SOURCE_LINE_PATTERN.match(line)
|
||||||
|
if src_match:
|
||||||
|
context_lines.append(line)
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for annotation line
|
||||||
|
if self.ANNOTATION_PATTERN.match(line):
|
||||||
|
context_lines.append(line)
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for child diagnostic (note: or help: within context)
|
||||||
|
child_match = self.DIAGNOSTIC_HEADER.match(line.strip())
|
||||||
|
if child_match and line.startswith(' '):
|
||||||
|
child_level = DiagnosticLevel(child_match.group(1))
|
||||||
|
child = Diagnostic(
|
||||||
|
level=child_level,
|
||||||
|
message=child_match.group('message').strip(),
|
||||||
|
code=child_match.group('code'),
|
||||||
|
line_start=current_line,
|
||||||
|
line_end=current_line,
|
||||||
|
)
|
||||||
|
children.append(child)
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for = note: or = help: style
|
||||||
|
eq_match = re.match(r'^\s*=\s*(note|help):\s*(.*)$', line)
|
||||||
|
if eq_match:
|
||||||
|
child = Diagnostic(
|
||||||
|
level=DiagnosticLevel(eq_match.group(1)),
|
||||||
|
message=eq_match.group(2).strip(),
|
||||||
|
line_start=current_line,
|
||||||
|
line_end=current_line,
|
||||||
|
)
|
||||||
|
children.append(child)
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Empty line might be separator or part of message
|
||||||
|
if line.strip() == '':
|
||||||
|
# Look ahead to see if diagnostic continues
|
||||||
|
if current_line + 1 < len(self.raw_lines):
|
||||||
|
next_line = self.raw_lines[current_line + 1]
|
||||||
|
if (self.LOCATION_PATTERN.match(next_line) or
|
||||||
|
self.SOURCE_LINE_PATTERN.match(next_line) or
|
||||||
|
self.ANNOTATION_PATTERN.match(next_line) or
|
||||||
|
next_line.strip().startswith('=')):
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
# Other content - might be continuation of message or end
|
||||||
|
if line.startswith(' '): # Indented content
|
||||||
|
raw_lines.append(line)
|
||||||
|
current_line += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Unknown line type - end the diagnostic
|
||||||
|
break
|
||||||
|
|
||||||
|
diagnostic.line_end = current_line - 1
|
||||||
|
diagnostic.raw_text = '\n'.join(raw_lines)
|
||||||
|
diagnostic.children = children
|
||||||
|
diagnostic.context_lines = context_lines
|
||||||
|
|
||||||
|
return diagnostic, current_line - 1
|
||||||
|
|
||||||
|
def _is_child_diagnostic(self, line: str, parent: Diagnostic) -> bool:
|
||||||
|
"""Check if a diagnostic line is a child of the parent."""
|
||||||
|
# Standalone note/help that follows immediately might be related
|
||||||
|
if line.startswith('note:') or line.startswith('help:'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class LogQuery:
|
||||||
|
"""
|
||||||
|
High-level query interface for cargo logs.
|
||||||
|
Designed for LLM consumption with clear, structured responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parsed_log: ParsedLog):
|
||||||
|
self.log = parsed_log
|
||||||
|
|
||||||
|
def find_errors(
|
||||||
|
self,
|
||||||
|
file_pattern: Optional[str] = None,
|
||||||
|
message_pattern: Optional[str] = None,
|
||||||
|
code_pattern: Optional[str] = None,
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Find all errors matching the given criteria.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_pattern: Regex to match file paths (e.g., "tests/.*" for test files)
|
||||||
|
message_pattern: Regex to match error messages (e.g., "not found in")
|
||||||
|
code_pattern: Regex to match error codes (e.g., "E0425" or "E04.*")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching Diagnostic objects
|
||||||
|
"""
|
||||||
|
return self.log.filter(
|
||||||
|
level=DiagnosticLevel.ERROR,
|
||||||
|
file_pattern=file_pattern,
|
||||||
|
message_pattern=message_pattern,
|
||||||
|
code_pattern=code_pattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_warnings(
|
||||||
|
self,
|
||||||
|
file_pattern: Optional[str] = None,
|
||||||
|
message_pattern: Optional[str] = None,
|
||||||
|
code_pattern: Optional[str] = None,
|
||||||
|
) -> list:
|
||||||
|
"""Find all warnings matching the given criteria."""
|
||||||
|
return self.log.filter(
|
||||||
|
level=DiagnosticLevel.WARNING,
|
||||||
|
file_pattern=file_pattern,
|
||||||
|
message_pattern=message_pattern,
|
||||||
|
code_pattern=code_pattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_by_file(self, file_pattern: str) -> list:
|
||||||
|
"""
|
||||||
|
Find all diagnostics for files matching the pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_pattern: Regex pattern for file paths
|
||||||
|
Examples: "src/main.rs", "tests/.*", ".*/mod.rs"
|
||||||
|
"""
|
||||||
|
return self.log.filter(file_pattern=file_pattern)
|
||||||
|
|
||||||
|
def find_by_message(self, message_pattern: str) -> list:
|
||||||
|
"""
|
||||||
|
Find all diagnostics with messages matching the pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_pattern: Regex pattern for messages
|
||||||
|
Examples: "not found", "unused.*variable", "lifetime"
|
||||||
|
"""
|
||||||
|
return self.log.filter(message_pattern=message_pattern)
|
||||||
|
|
||||||
|
def find_by_code(self, code_pattern: str) -> list:
|
||||||
|
"""
|
||||||
|
Find all diagnostics with matching error/warning codes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code_pattern: Regex for codes (e.g., "E0425", "E04.*", "unused_.*")
|
||||||
|
"""
|
||||||
|
return self.log.filter(code_pattern=code_pattern)
|
||||||
|
|
||||||
|
def get_error_boundaries(self, diagnostic: Diagnostic) -> dict:
|
||||||
|
"""
|
||||||
|
Get the exact boundaries of an error in the log file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"line_start": int, # Starting line in log file (0-indexed)
|
||||||
|
"line_end": int, # Ending line in log file (0-indexed)
|
||||||
|
"raw_text": str, # The complete raw error block
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"line_start": diagnostic.line_start,
|
||||||
|
"line_end": diagnostic.line_end,
|
||||||
|
"raw_text": diagnostic.raw_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_unique_error_codes(self) -> list:
|
||||||
|
"""Get all unique error codes in the log."""
|
||||||
|
codes = set()
|
||||||
|
for d in self.log.diagnostics:
|
||||||
|
if d.code:
|
||||||
|
codes.add(d.code)
|
||||||
|
return sorted(codes)
|
||||||
|
|
||||||
|
def get_affected_files(self, level: Optional[DiagnosticLevel] = None) -> list:
|
||||||
|
"""Get all files that have diagnostics."""
|
||||||
|
diagnostics = self.log.diagnostics
|
||||||
|
if level:
|
||||||
|
diagnostics = [d for d in diagnostics if d.level == level]
|
||||||
|
|
||||||
|
files = set()
|
||||||
|
for d in diagnostics:
|
||||||
|
if d.location:
|
||||||
|
files.add(d.location.file_path)
|
||||||
|
return sorted(files)
|
||||||
|
|
||||||
|
def group_by_file(
|
||||||
|
self,
|
||||||
|
level: Optional[DiagnosticLevel] = None,
|
||||||
|
file_pattern: Optional[str] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Group diagnostics by file path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"src/main.rs": [Diagnostic, ...],
|
||||||
|
"src/lib.rs": [Diagnostic, ...],
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
diagnostics = self.log.diagnostics
|
||||||
|
if level:
|
||||||
|
diagnostics = [d for d in diagnostics if d.level == level]
|
||||||
|
if file_pattern:
|
||||||
|
diagnostics = [d for d in diagnostics if d.matches_file_regex(file_pattern)]
|
||||||
|
|
||||||
|
grouped = {}
|
||||||
|
for d in diagnostics:
|
||||||
|
if d.location:
|
||||||
|
file_path = d.location.file_path
|
||||||
|
if file_path not in grouped:
|
||||||
|
grouped[file_path] = []
|
||||||
|
grouped[file_path].append(d)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
def group_by_code(
|
||||||
|
self,
|
||||||
|
level: Optional[DiagnosticLevel] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Group diagnostics by error/warning code.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"E0425": [Diagnostic, ...],
|
||||||
|
"E0308": [Diagnostic, ...],
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
diagnostics = self.log.diagnostics
|
||||||
|
if level:
|
||||||
|
diagnostics = [d for d in diagnostics if d.level == level]
|
||||||
|
|
||||||
|
grouped = {}
|
||||||
|
for d in diagnostics:
|
||||||
|
code = d.code or "no_code"
|
||||||
|
if code not in grouped:
|
||||||
|
grouped[code] = []
|
||||||
|
grouped[code].append(d)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
def to_json(
|
||||||
|
self,
|
||||||
|
diagnostics: Optional[list] = None,
|
||||||
|
include_raw: bool = True,
|
||||||
|
indent: int = 2,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Convert diagnostics to JSON for LLM consumption.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diagnostics: List of diagnostics (defaults to all)
|
||||||
|
include_raw: Include raw log text in output
|
||||||
|
indent: JSON indentation
|
||||||
|
"""
|
||||||
|
if diagnostics is None:
|
||||||
|
diagnostics = self.log.diagnostics
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"summary": self.log.summary(),
|
||||||
|
"diagnostics": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for d in diagnostics:
|
||||||
|
entry = d.to_dict()
|
||||||
|
if not include_raw:
|
||||||
|
del entry["raw_text"]
|
||||||
|
output["diagnostics"].append(entry)
|
||||||
|
|
||||||
|
return json.dumps(output, indent=indent)
|
||||||
|
|
||||||
|
def format_for_llm(
|
||||||
|
self,
|
||||||
|
diagnostics: Optional[list] = None,
|
||||||
|
verbose: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format diagnostics in a readable format optimized for LLM analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diagnostics: List of diagnostics (defaults to all)
|
||||||
|
verbose: Include full raw text blocks
|
||||||
|
"""
|
||||||
|
if diagnostics is None:
|
||||||
|
diagnostics = self.log.diagnostics
|
||||||
|
|
||||||
|
if not diagnostics:
|
||||||
|
return "No diagnostics found matching the criteria."
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f"Found {len(diagnostics)} diagnostic(s):\n")
|
||||||
|
|
||||||
|
for i, d in enumerate(diagnostics, 1):
|
||||||
|
lines.append(f"{'='*60}")
|
||||||
|
lines.append(f"[{i}] {d.level.value.upper()}", )
|
||||||
|
if d.code:
|
||||||
|
lines.append(f" Code: {d.code}")
|
||||||
|
lines.append(f" Message: {d.message}")
|
||||||
|
if d.location:
|
||||||
|
lines.append(f" Location: {d.location}")
|
||||||
|
lines.append(f" Log lines: {d.line_start}-{d.line_end}")
|
||||||
|
|
||||||
|
if d.children:
|
||||||
|
for child in d.children:
|
||||||
|
lines.append(f" └─ {child.level.value}: {child.message}")
|
||||||
|
|
||||||
|
if verbose and d.raw_text:
|
||||||
|
lines.append("\n Raw output:")
|
||||||
|
for raw_line in d.raw_text.split('\n'):
|
||||||
|
lines.append(f" │ {raw_line}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""CLI interface for the cargo log parser."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Parse cargo build logs and filter errors/warnings",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Show all errors
|
||||||
|
%(prog)s build.log --errors
|
||||||
|
|
||||||
|
# Pipe from cargo build
|
||||||
|
cargo build 2>&1 | %(prog)s --errors
|
||||||
|
cargo build 2>&1 | %(prog)s --file "tests/.*"
|
||||||
|
|
||||||
|
# Use - for stdin explicitly
|
||||||
|
cargo build 2>&1 | %(prog)s - --errors --file "src/.*"
|
||||||
|
|
||||||
|
# Find errors in test files
|
||||||
|
%(prog)s build.log --errors --file "tests/.*"
|
||||||
|
|
||||||
|
# Find "not found" errors
|
||||||
|
%(prog)s build.log --errors --message "not found"
|
||||||
|
|
||||||
|
# Find specific error code
|
||||||
|
%(prog)s build.log --code "E0425"
|
||||||
|
|
||||||
|
# Output as JSON
|
||||||
|
%(prog)s build.log --json
|
||||||
|
|
||||||
|
# Group by file
|
||||||
|
%(prog)s build.log --group-by-file
|
||||||
|
|
||||||
|
# Stream mode - output each match immediately (useful with stdin)
|
||||||
|
cargo build 2>&1 | %(prog)s --errors --stream
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"log_file",
|
||||||
|
nargs="?",
|
||||||
|
default="-",
|
||||||
|
help="Path to cargo build log file (use - or omit for stdin)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--errors", "-e", action="store_true", help="Show only errors")
|
||||||
|
parser.add_argument("--warnings", "-w", action="store_true", help="Show only warnings")
|
||||||
|
parser.add_argument("--file", "-f", metavar="PATTERN", help="Filter by file path regex")
|
||||||
|
parser.add_argument("--message", "-m", metavar="PATTERN", help="Filter by message regex")
|
||||||
|
parser.add_argument("--code", "-c", metavar="PATTERN", help="Filter by error code regex")
|
||||||
|
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||||
|
parser.add_argument("--verbose", "-v", action="store_true", help="Include raw log text")
|
||||||
|
parser.add_argument("--summary", "-s", action="store_true", help="Show summary only")
|
||||||
|
parser.add_argument("--group-by-file", action="store_true", help="Group diagnostics by file")
|
||||||
|
parser.add_argument("--group-by-code", action="store_true", help="Group diagnostics by error code")
|
||||||
|
parser.add_argument("--list-codes", action="store_true", help="List all unique error codes")
|
||||||
|
parser.add_argument("--list-files", action="store_true", help="List all affected files")
|
||||||
|
parser.add_argument(
|
||||||
|
"--stream",
|
||||||
|
action="store_true",
|
||||||
|
help="Stream mode: output each matching diagnostic immediately"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--raw",
|
||||||
|
action="store_true",
|
||||||
|
help="Output raw log text only (useful for piping)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--passthrough",
|
||||||
|
action="store_true",
|
||||||
|
help="Pass through all input while also outputting matches"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Determine level filter
|
||||||
|
level = None
|
||||||
|
if args.errors:
|
||||||
|
level = DiagnosticLevel.ERROR
|
||||||
|
elif args.warnings:
|
||||||
|
level = DiagnosticLevel.WARNING
|
||||||
|
|
||||||
|
# Create filter function
|
||||||
|
def matches_filter(diag: Diagnostic) -> bool:
|
||||||
|
if level is not None and diag.level != level:
|
||||||
|
return False
|
||||||
|
if args.file and not diag.matches_file_regex(args.file):
|
||||||
|
return False
|
||||||
|
if args.message and not diag.matches_message_regex(args.message):
|
||||||
|
return False
|
||||||
|
if args.code and not diag.matches_code(args.code):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Parse input (file or stdin)
|
||||||
|
parser_instance = CargoLogParser()
|
||||||
|
|
||||||
|
if args.log_file == "-" or (args.log_file == "-" and not sys.stdin.isatty()):
|
||||||
|
# Reading from stdin
|
||||||
|
if sys.stdin.isatty() and args.log_file == "-":
|
||||||
|
print("Reading from stdin... (Ctrl+D to end, or pipe input)", file=sys.stderr)
|
||||||
|
|
||||||
|
if args.passthrough:
|
||||||
|
# Read all input, print it, then parse
|
||||||
|
content = sys.stdin.read()
|
||||||
|
print(content, end='')
|
||||||
|
parsed = parser_instance.parse_string(content, "<stdin>")
|
||||||
|
else:
|
||||||
|
parsed = parser_instance.parse_stream(sys.stdin, "<stdin>")
|
||||||
|
else:
|
||||||
|
# Reading from file
|
||||||
|
parsed = parser_instance.parse_file(args.log_file)
|
||||||
|
|
||||||
|
query = LogQuery(parsed)
|
||||||
|
|
||||||
|
# Handle special list commands
|
||||||
|
if args.list_codes:
|
||||||
|
codes = query.get_unique_error_codes()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(codes))
|
||||||
|
else:
|
||||||
|
print("Unique error/warning codes:")
|
||||||
|
for code in codes:
|
||||||
|
print(f" {code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.list_files:
|
||||||
|
files = query.get_affected_files(level)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(files))
|
||||||
|
else:
|
||||||
|
print("Affected files:")
|
||||||
|
for f in files:
|
||||||
|
print(f" {f}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.summary:
|
||||||
|
summary = parsed.summary()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(summary, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"Log file: {summary['file']}")
|
||||||
|
print(f"Total diagnostics: {summary['total_diagnostics']}")
|
||||||
|
print(f" Errors: {summary['errors']}")
|
||||||
|
print(f" Warnings: {summary['warnings']}")
|
||||||
|
print(f" Notes: {summary['notes']}")
|
||||||
|
if summary['error_codes']:
|
||||||
|
print(f"Error codes: {', '.join(summary['error_codes'])}")
|
||||||
|
if summary['affected_files']:
|
||||||
|
print(f"Affected files: {len(summary['affected_files'])}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Filter diagnostics
|
||||||
|
diagnostics = [d for d in parsed.diagnostics if matches_filter(d)]
|
||||||
|
|
||||||
|
# Handle grouping
|
||||||
|
if args.group_by_file:
|
||||||
|
grouped = {}
|
||||||
|
for d in diagnostics:
|
||||||
|
if d.location:
|
||||||
|
fp = d.location.file_path
|
||||||
|
if fp not in grouped:
|
||||||
|
grouped[fp] = []
|
||||||
|
grouped[fp].append(d)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {f: [d.to_dict() for d in diags] for f, diags in grouped.items()}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
for file_path, diags in grouped.items():
|
||||||
|
print(f"\n{file_path} ({len(diags)} issues):")
|
||||||
|
for d in diags:
|
||||||
|
print(f" - {d.summary()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.group_by_code:
|
||||||
|
grouped = {}
|
||||||
|
for d in diagnostics:
|
||||||
|
code = d.code or "no_code"
|
||||||
|
if code not in grouped:
|
||||||
|
grouped[code] = []
|
||||||
|
grouped[code].append(d)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {c: [d.to_dict() for d in diags] for c, diags in grouped.items()}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
for code, diags in grouped.items():
|
||||||
|
print(f"\n{code} ({len(diags)} occurrences):")
|
||||||
|
for d in diags:
|
||||||
|
loc = f" at {d.location}" if d.location else ""
|
||||||
|
print(f" - {d.message}{loc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if args.raw:
|
||||||
|
# Output only raw log text for matching diagnostics
|
||||||
|
for d in diagnostics:
|
||||||
|
print(d.raw_text)
|
||||||
|
print() # Blank line between
|
||||||
|
elif args.stream or args.json:
|
||||||
|
if args.json:
|
||||||
|
print(query.to_json(diagnostics, include_raw=args.verbose))
|
||||||
|
else:
|
||||||
|
# Stream mode - one diagnostic at a time
|
||||||
|
for d in diagnostics:
|
||||||
|
if args.verbose:
|
||||||
|
print(d.raw_text)
|
||||||
|
else:
|
||||||
|
print(d.summary())
|
||||||
|
else:
|
||||||
|
print(query.format_for_llm(diagnostics, verbose=args.verbose))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user