Skip to main content

linthis/interactive/
quickfix.rs

1// Copyright 2024 zhlinh and linthis Project Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found at
4//
5// https://opensource.org/license/MIT
6//
7// The above copyright notice and this permission
8// notice shall be included in all copies or
9// substantial portions of the Software.
10
11//! Vim quickfix format output generation.
12//!
13//! Generates output in the standard vim quickfix format:
14//! ```text
15//! file:line:column:message
16//! ```
17//!
18//! This can be used with:
19//! - `vim -q quickfix.txt` to open vim with the quickfix list
20//! - `:cfile quickfix.txt` in vim to load the quickfix list
21//! - Other editors that support the quickfix format
22
23use crate::utils::types::{LintIssue, RunResult, Severity};
24use std::fs::File;
25use std::io::Write;
26use std::path::Path;
27
28/// Generate quickfix format string from a list of issues
29pub fn generate_quickfix(issues: &[LintIssue]) -> String {
30    issues
31        .iter()
32        .map(format_issue_quickfix)
33        .collect::<Vec<_>>()
34        .join("\n")
35}
36
37/// Generate quickfix format string from a RunResult
38pub fn generate_quickfix_from_result(result: &RunResult) -> String {
39    generate_quickfix(&result.issues)
40}
41
42/// Format a single issue in quickfix format
43///
44/// Format: `file:line:column:severity:message (code)`
45fn format_issue_quickfix(issue: &LintIssue) -> String {
46    let file = issue.file_path.display();
47    let line = issue.line;
48    let col = issue.column.unwrap_or(1);
49
50    // Severity prefix for vim
51    let severity = match issue.severity {
52        Severity::Error => "error",
53        Severity::Warning => "warning",
54        Severity::Info => "info",
55    };
56
57    // Build message with optional code
58    let message = if let Some(ref code) = issue.code {
59        format!("{} ({}) [{}]", issue.message, code, severity)
60    } else {
61        format!("{} [{}]", issue.message, severity)
62    };
63
64    // Escape special characters in message
65    let message = message.replace('\n', " ").replace('\r', "");
66
67    format!("{}:{}:{}:{}", file, line, col, message)
68}
69
70/// Write quickfix format to a file
71///
72/// # Arguments
73/// * `issues` - List of lint issues
74/// * `path` - Path to write the quickfix file
75///
76/// # Returns
77/// * `Ok(())` on success
78/// * `Err(String)` on failure
79pub fn write_quickfix_file(issues: &[LintIssue], path: &Path) -> super::InteractiveResult<()> {
80    use super::InteractiveError;
81
82    let content = generate_quickfix(issues);
83
84    let mut file = File::create(path).map_err(|e| {
85        InteractiveError::QuickfixWrite(format!("Failed to create file: {}", e))
86    })?;
87
88    file.write_all(content.as_bytes()).map_err(|e| {
89        InteractiveError::QuickfixWrite(format!("Failed to write content: {}", e))
90    })?;
91
92    // Ensure trailing newline
93    if !content.is_empty() && !content.ends_with('\n') {
94        file.write_all(b"\n").map_err(|e| {
95            InteractiveError::QuickfixWrite(format!("Failed to write newline: {}", e))
96        })?;
97    }
98
99    Ok(())
100}
101
102/// Get the default quickfix file path
103pub fn default_quickfix_path() -> std::path::PathBuf {
104    std::path::PathBuf::from(".linthis").join("quickfix.txt")
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::path::PathBuf;
111
112    fn make_issue(file: &str, line: usize, col: Option<usize>, severity: Severity, msg: &str, code: Option<&str>) -> LintIssue {
113        let mut issue = LintIssue::new(
114            PathBuf::from(file),
115            line,
116            msg.to_string(),
117            severity,
118        );
119        if let Some(c) = col {
120            issue = issue.with_column(c);
121        }
122        if let Some(code) = code {
123            issue = issue.with_code(code.to_string());
124        }
125        issue
126    }
127
128    #[test]
129    fn test_format_issue_quickfix_basic() {
130        let issue = make_issue("src/main.rs", 42, Some(10), Severity::Error, "unused variable", Some("W0612"));
131        let formatted = format_issue_quickfix(&issue);
132        assert_eq!(formatted, "src/main.rs:42:10:unused variable (W0612) [error]");
133    }
134
135    #[test]
136    fn test_format_issue_quickfix_no_column() {
137        let issue = make_issue("test.py", 100, None, Severity::Warning, "line too long", Some("E501"));
138        let formatted = format_issue_quickfix(&issue);
139        assert_eq!(formatted, "test.py:100:1:line too long (E501) [warning]");
140    }
141
142    #[test]
143    fn test_format_issue_quickfix_no_code() {
144        let issue = make_issue("file.cpp", 5, Some(1), Severity::Info, "consider using const", None);
145        let formatted = format_issue_quickfix(&issue);
146        assert_eq!(formatted, "file.cpp:5:1:consider using const [info]");
147    }
148
149    #[test]
150    fn test_generate_quickfix_multiple() {
151        let issues = vec![
152            make_issue("a.rs", 1, Some(1), Severity::Error, "error 1", Some("E001")),
153            make_issue("b.rs", 2, Some(5), Severity::Warning, "warning 1", Some("W001")),
154        ];
155        let output = generate_quickfix(&issues);
156        let lines: Vec<&str> = output.lines().collect();
157        assert_eq!(lines.len(), 2);
158        assert!(lines[0].starts_with("a.rs:1:1:"));
159        assert!(lines[1].starts_with("b.rs:2:5:"));
160    }
161
162    #[test]
163    fn test_generate_quickfix_empty() {
164        let issues: Vec<LintIssue> = vec![];
165        let output = generate_quickfix(&issues);
166        assert!(output.is_empty());
167    }
168
169    #[test]
170    fn test_format_issue_quickfix_escapes_newlines() {
171        let issue = make_issue("test.rs", 1, Some(1), Severity::Error, "line1\nline2\rline3", None);
172        let formatted = format_issue_quickfix(&issue);
173        assert!(!formatted.contains('\n'));
174        assert!(!formatted.contains('\r'));
175    }
176}