Skip to content
3 changes: 1 addition & 2 deletions .github/workflows/run-dev-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,8 @@ jobs:
- name: Generate Coverage lcov report
run: nur test lcov

- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{secrets.CODECOV_TOKEN}}
files: lcov.info
fail_ci_if_error: true # optional (default = false)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
use_pypi: true # workaround unexpected GPG key changes (temporarily)
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cpp-linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

[dependencies.git-bot-feedback]
version = "0.5.5"
version = "0.5.6"
features = ["file-changes"]
# path = "../../git-bot-feedback"
# git = "https://github.com/2bndy5/git-bot-feedback"
Expand Down
17 changes: 10 additions & 7 deletions cpp-linter/src/clang_tools/clang_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ use gix_imara_diff::{Diff, InternedInput};
use log::Level;

// project-specific crates/modules
use super::MakeSuggestions;
use super::{CACHE_DIR, MakeSuggestions};
use crate::{cli::ClangParams, common_fs::FileObj, error::ClangCaptureError};

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FormatAdvice {
/// A list of [`Replacement`]s that clang-tidy wants to make.
/// A list of line ranges that clang-format wants to replace.
pub replacements: Vec<RangeInclusive<u32>>,

pub patched: PathBuf,
Expand Down Expand Up @@ -73,13 +73,14 @@ pub fn run_clang_format(
.as_ref()
.ok_or(ClangCaptureError::ToolPathUnknown("clang-format"))?;
let mut cmd = Command::new(cmd_path);
cmd.current_dir(&clang_params.repo_root);
let mut logs = vec![];
cmd.args(["--style", &clang_params.style]);
let ranges = file.get_ranges(&clang_params.lines_changed_only);
for range in &ranges {
cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
}
let cache_path = clang_params.project_cache_dir.join("patches");
let cache_path = clang_params.repo_root.join(CACHE_DIR).join("patches");
let cache_format_fixes = cache_path.join(file.name.with_added_extension("format"));
fs::create_dir_all(
cache_format_fixes
Expand Down Expand Up @@ -125,9 +126,11 @@ pub fn run_clang_format(

// use a diff between patched and original contents to get format results
let original_contents =
fs::read_to_string(&file.name).map_err(|e| ClangCaptureError::ReadFileFailed {
file_name: file_name.clone(),
source: e,
fs::read_to_string(clang_params.repo_root.join(&file.name)).map_err(|e| {
ClangCaptureError::ReadFileFailed {
file_name: file_name.clone(),
source: e,
}
})?;
let patched_contents = String::from_utf8(output.stdout.to_vec()).map_err(|e| {
ClangCaptureError::NonUtf8Output {
Expand All @@ -143,7 +146,7 @@ pub fn run_clang_format(
.hunks()
.filter_map(|hunk| {
let replacement = if hunk.is_pure_insertion() {
RangeInclusive::new(hunk.after.start, hunk.after.end.saturating_sub(1))
RangeInclusive::new(hunk.after.start, hunk.after.start)
} else {
RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
};
Expand Down
33 changes: 18 additions & 15 deletions cpp-linter/src/clang_tools/clang_tidy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//! output.

use std::{
env::{consts::OS, current_dir},
env::consts::OS,
fs,
path::PathBuf,
path::{Path, PathBuf},
process::Command,
sync::{Arc, Mutex, MutexGuard},
};
Expand Down Expand Up @@ -144,13 +144,13 @@ const NOTE_HEADER: &str = r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+),?[
fn parse_tidy_output(
tidy_stdout: &[u8],
database_json: &Option<Vec<CompilationUnit>>,
repo_root: &Path,
) -> Result<TidyAdvice, ClangCaptureError> {
let note_header = Regex::new(NOTE_HEADER)?;
let fixed_note = Regex::new(r"^.+:(\d+):\d+:\snote: FIX-IT applied suggested code changes$")?;
let mut found_fix = false;
let mut notification = None;
let mut result = Vec::new();
let cur_dir = current_dir().map_err(ClangCaptureError::UnknownWorkingDirectory)?;
for line in String::from_utf8(tidy_stdout.to_vec())
.map_err(|e| ClangCaptureError::NonUtf8Output {
task: "convert clang-tidy stdout".to_string(),
Expand Down Expand Up @@ -191,16 +191,16 @@ fn parse_tidy_output(
// file was not a named unit in the database;
// try to normalize path as if relative to working directory.
// NOTE: This shouldn't happen with a properly formed JSON database
filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
filename = normalize_path(&PathBuf::from_iter([repo_root, &filename]));
}
} else {
// still need to normalize the relative path despite missing database info.
// let's assume the file is relative to current working directory.
filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
filename = normalize_path(&PathBuf::from_iter([repo_root, &filename]));
}
debug_assert!(filename.is_absolute());

if filename.is_absolute()
&& let Ok(file_n) = filename.strip_prefix(&cur_dir)
&& let Ok(file_n) = filename.strip_prefix(repo_root)
{
// if this filename can't be made into a relative path, then it is
// likely not a member of the project's sources (ie /usr/include/stdio.h)
Expand Down Expand Up @@ -273,6 +273,7 @@ pub fn run_clang_tidy(
.as_ref()
.ok_or(ClangCaptureError::ToolPathUnknown("clang-tidy"))?;
let mut cmd = Command::new(cmd_path);
cmd.current_dir(&clang_params.repo_root);
let mut logs = vec![];
if !clang_params.tidy_checks.is_empty() {
cmd.args(["-checks", &clang_params.tidy_checks]);
Expand All @@ -296,16 +297,17 @@ pub fn run_clang_tidy(
);
cmd.args(["--line-filter", filter.as_str()]);
}
let repo_file_path = clang_params.repo_root.join(&file.name);
let original_content = if !clang_params.tidy_review {
None
} else {
cmd.arg("--fix-errors");
Some(
fs::read_to_string(&file.name).map_err(|e| ClangCaptureError::ReadFileFailed {
Some(fs::read_to_string(&repo_file_path).map_err(|e| {
ClangCaptureError::ReadFileFailed {
file_name: file_name.clone(),
source: e,
})?,
)
}
})?)
};
if !clang_params.style.is_empty() {
cmd.args(["--format-style", clang_params.style.as_str()]);
Expand Down Expand Up @@ -347,6 +349,7 @@ pub fn run_clang_tidy(
file.tidy_advice = Some(parse_tidy_output(
&output.stdout,
&clang_params.database_json,
&clang_params.repo_root,
)?);
if clang_params.tidy_review
&& let Some(original_content) = &original_content
Expand All @@ -355,14 +358,14 @@ pub fn run_clang_tidy(
// cache file changes in a buffer and restore the original contents for further analysis
tidy_advice.patched =
Some(
fs::read(&file_name).map_err(|e| ClangCaptureError::ReadFileFailed {
fs::read(&repo_file_path).map_err(|e| ClangCaptureError::ReadFileFailed {
file_name: file_name.clone(),
source: e,
})?,
);
}
// original_content is guaranteed to be Some() value at this point
fs::write(&file_name, original_content).map_err(|e| {
fs::write(&repo_file_path, original_content).map_err(|e| {
ClangCaptureError::WriteFileFailed {
file_name,
source: e,
Expand Down Expand Up @@ -496,7 +499,7 @@ mod test {
format_review: false,
clang_tidy_command: Some(exe_path),
clang_format_command: None,
project_cache_dir: PathBuf::from(".").join(".cpp-linter-cache"),
repo_root: PathBuf::from("."),
};
let mut file_lock = arc_file.lock().unwrap();
let logs = run_clang_tidy(&mut file_lock, &clang_params)
Expand Down Expand Up @@ -564,7 +567,7 @@ TrenchBroom/TrenchBroom/common/test/src/mdl/tst_ReadFreeImageTexture.cpp:44:48:
44 | return diskFS.openFile(name) | kdl::and_then([](const auto& file) {
| ^
"#;
let advice = parse_tidy_output(tidy_out.as_bytes(), &None).unwrap();
let advice = parse_tidy_output(tidy_out.as_bytes(), &None, &PathBuf::from(".")).unwrap();
assert_eq!(advice.notes.len(), 4);
for note in advice.notes {
assert!(note.diagnostic.contains('-'));
Expand Down
78 changes: 67 additions & 11 deletions cpp-linter/src/clang_tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ use tokio::task::JoinSet;

// project-specific modules/crates
use super::common_fs::FileObj;
use crate::error::{ClangCaptureError, ClangTaskError};
use crate::{
clang_tools::clang_tidy::CompilationUnit,
cli::ClangParams,
error::{ClangCaptureError, ClangTaskError},
rest_client::{RestClient, USER_OUTREACH},
};
pub mod clang_format;
use clang_format::run_clang_format;
pub mod clang_tidy;
use clang_tidy::{CompilationUnit, run_clang_tidy};
use clang_tidy::run_clang_tidy;

pub const CACHE_DIR: &str = ".cpp-linter-cache";

/// This creates a task to run clang-tidy and clang-format on a single file.
///
Expand Down Expand Up @@ -135,15 +138,27 @@ pub async fn capture_clang_tools_output(
clang_versions.format_version = Some(tool_info.version);
clang_params.clang_format_command = Some(tool_info.path);
}

// parse database (if provided) to match filenames when parsing clang-tidy's stdout
if let Some(db_path) = &clang_params.database
&& let Ok(db_str) = fs::read(db_path.join("compile_commands.json"))
{
clang_params.database_json = Some(
// A compilation database should be UTF-8 encoded, but file paths are not; use lossy conversion.
serde_json::from_str::<Vec<CompilationUnit>>(&String::from_utf8_lossy(&db_str))?,
)
if let Some(db_path) = &clang_params.database {
let db_path = db_path.join("compile_commands.json");
match fs::read_to_string(&db_path) {
Ok(db_str) => match serde_json::from_str::<Vec<CompilationUnit>>(&db_str) {
Ok(db_json) => {
clang_params.database_json = Some(db_json);
}
Err(e) => {
log::warn!(
"Failed to parse compilation database JSON at {}: {e:?}",
db_path.to_string_lossy()
);
}
},
Err(e) => {
log::warn!(
"Failed to read compilation database file at {}: {e:?}",
db_path.to_string_lossy()
);
}
}
};

let mut executors = JoinSet::new();
Expand Down Expand Up @@ -382,3 +397,44 @@ pub trait MakeSuggestions {
*tool_total += hunks_in_patch;
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]

use std::{env, fs, path::Path};

use super::*;
#[cfg(feature = "bin")]
use crate::logger::try_init;

async fn test_db_parse<P: AsRef<Path>>(path: P) -> Result<ClangVersions, ClangTaskError> {
let clang_params = ClangParams {
database: Some(path.as_ref().to_path_buf()),
repo_root: PathBuf::from("."),
..Default::default()
};
let version = RequestedVersion::default();
// We don't need to use any specific git REST API client for this.
unsafe {
env::remove_var("GITHUB_ACTIONS");
}
let rest_client = RestClient::new().unwrap();
#[cfg(feature = "bin")]
try_init();
capture_clang_tools_output(&[], &version, clang_params, &rest_client).await
}

#[tokio::test]
async fn bad_db_path() {
test_db_parse("nonexistent/path").await.unwrap();
}

#[tokio::test]
async fn bad_db_json() {
let tmp_dir = tempfile::tempdir().unwrap();
let db_path = tmp_dir.path().join("compile_commands.json");
fs::write(&db_path, "not a valid json").unwrap();
test_db_parse(tmp_dir.path()).await.unwrap();
}
}
9 changes: 4 additions & 5 deletions cpp-linter/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,15 @@ pub struct SourceOptions {
)]
pub extensions: Vec<String>,

/// The relative path to the repository root directory.
/// The path to the repository root directory.
///
/// This path is relative to the runner's `GITHUB_WORKSPACE`
/// environment variable (or the current working directory if
/// not using a CI runner).
/// This path should be relative to the current
/// working directory. It can also be an absolute path.
#[cfg_attr(
feature = "bin",
arg(short, long, default_value = ".", help_heading = "Source options")
)]
pub repo_root: String,
pub repo_root: PathBuf,

/// This controls what part of the files are analyzed.
#[cfg_attr(
Expand Down
Loading
Loading