cpp_linter/
run.rs

1//! This module is the native backend of the cpp-linter package written in Rust.
2//!
3//! In python, this module is exposed as `cpp_linter.run` that has 1 function exposed:
4//! `main()`.
5
6use std::env;
7use std::path::Path;
8use std::sync::{Arc, Mutex};
9
10// non-std crates
11use anyhow::{anyhow, Result};
12use log::{set_max_level, LevelFilter};
13#[cfg(feature = "openssl-vendored")]
14use openssl_probe;
15
16// project specific modules/crates
17use crate::clang_tools::capture_clang_tools_output;
18use crate::cli::{get_arg_parser, ClangParams, Cli, FeedbackInput, LinesChangedOnly};
19use crate::common_fs::FileFilter;
20use crate::logger;
21use crate::rest_api::{github::GithubApiClient, RestApiClient};
22
23const VERSION: &str = env!("CARGO_PKG_VERSION");
24
25fn probe_ssl_certs() {
26    #[cfg(feature = "openssl-vendored")]
27    openssl_probe::init_ssl_cert_env_vars();
28}
29
30/// This is the backend entry point for console applications.
31///
32/// The idea here is that all functionality is implemented in Rust. However, passing
33/// command line arguments is done differently in Python, node.js, or Rust.
34///
35/// - In python, the CLI arguments list is optionally passed to the binding's
36///   `cpp_linter.main()` function (which wraps [`run_main()`]). If no args are passed,
37///   then `cpp_linter.main()` uses [`std::env::args`] without the leading path to the
38///   python interpreter removed.
39/// - In node.js, the `process.argv` array (without the leading path to the node
40///   interpreter removed) is passed from `cli.js` module to rust via `index.node`
41///   module's `main()` (which wraps([`run_main()`])).
42/// - In rust, the [`std::env::args`] is passed to [`run_main()`] in the binary
43///   source `main.rs`.
44///
45/// This is done because of the way the python entry point is invoked. If [`std::env::args`]
46/// is used instead of python's `sys.argv`, then the list of strings includes the entry point
47/// alias ("path/to/cpp-linter.exe"). Thus, the parser in [`crate::cli`] will halt on an error
48/// because it is not configured to handle positional arguments.
49pub async fn run_main(args: Vec<String>) -> Result<()> {
50    probe_ssl_certs();
51
52    let arg_parser = get_arg_parser();
53    let args = arg_parser.get_matches_from(args);
54    let cli = Cli::from(&args);
55
56    if args.subcommand_matches("version").is_some() {
57        println!("cpp-linter v{}", VERSION);
58        return Ok(());
59    }
60
61    logger::init().unwrap();
62
63    if cli.version == "NO-VERSION" {
64        log::error!("The `--version` arg is used to specify which version of clang to use.");
65        log::error!("To get the cpp-linter version, use `cpp-linter version` sub-command.");
66        return Err(anyhow!("Clang version not specified."));
67    }
68
69    if cli.repo_root != "." {
70        env::set_current_dir(Path::new(&cli.repo_root))
71            .unwrap_or_else(|_| panic!("'{}' is inaccessible or does not exist", cli.repo_root));
72    }
73
74    let rest_api_client = GithubApiClient::new()?;
75    set_max_level(if cli.verbosity || rest_api_client.debug_enabled {
76        LevelFilter::Debug
77    } else {
78        LevelFilter::Info
79    });
80    log::info!("Processing event {}", rest_api_client.event_name);
81
82    let mut file_filter = FileFilter::new(&cli.ignore, cli.extensions.clone());
83    file_filter.parse_submodules();
84    if let Some(files) = &cli.not_ignored {
85        file_filter.not_ignored.extend(files.clone());
86    }
87
88    if !file_filter.ignored.is_empty() {
89        log::info!("Ignored:");
90        for pattern in &file_filter.ignored {
91            log::info!("  {pattern}");
92        }
93    }
94    if !file_filter.not_ignored.is_empty() {
95        log::info!("Not Ignored:");
96        for pattern in &file_filter.not_ignored {
97            log::info!("  {pattern}");
98        }
99    }
100
101    rest_api_client.start_log_group(String::from("Get list of specified source files"));
102    let files = if cli.lines_changed_only != LinesChangedOnly::Off || cli.files_changed_only {
103        // parse_diff(github_rest_api_payload)
104        rest_api_client
105            .get_list_of_changed_files(&file_filter)
106            .await?
107    } else {
108        // walk the folder and look for files with specified extensions according to ignore values.
109        let mut all_files = file_filter.list_source_files(".")?;
110        if rest_api_client.event_name == "pull_request" && (cli.tidy_review || cli.format_review) {
111            let changed_files = rest_api_client
112                .get_list_of_changed_files(&file_filter)
113                .await?;
114            for changed_file in changed_files {
115                for file in &mut all_files {
116                    if changed_file.name == file.name {
117                        file.diff_chunks = changed_file.diff_chunks.clone();
118                        file.added_lines = changed_file.added_lines.clone();
119                        file.added_ranges = changed_file.added_ranges.clone();
120                    }
121                }
122            }
123        }
124        all_files
125    };
126    let mut arc_files = vec![];
127    log::info!("Giving attention to the following files:");
128    for file in files {
129        log::info!("  ./{}", file.name.to_string_lossy().replace('\\', "/"));
130        arc_files.push(Arc::new(Mutex::new(file)));
131    }
132    rest_api_client.end_log_group();
133
134    let mut clang_params = ClangParams::from(&cli);
135    let user_inputs = FeedbackInput::from(&cli);
136    let clang_versions = capture_clang_tools_output(
137        &mut arc_files,
138        cli.version.as_str(),
139        &mut clang_params,
140        &rest_api_client,
141    )
142    .await?;
143    rest_api_client.start_log_group(String::from("Posting feedback"));
144    let checks_failed = rest_api_client
145        .post_feedback(&arc_files, user_inputs, clang_versions)
146        .await?;
147    rest_api_client.end_log_group();
148    if env::var("PRE_COMMIT").is_ok_and(|v| v == "1") {
149        if checks_failed > 1 {
150            return Err(anyhow!("Some checks did not pass"));
151        } else {
152            return Ok(());
153        }
154    }
155    Ok(())
156}
157
158#[cfg(test)]
159mod test {
160    use super::run_main;
161    use std::env;
162
163    #[tokio::test]
164    async fn normal() {
165        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
166        let result = run_main(vec![
167            "cpp-linter".to_string(),
168            "-l".to_string(),
169            "false".to_string(),
170            "--repo-root".to_string(),
171            "tests".to_string(),
172            "demo/demo.cpp".to_string(),
173        ])
174        .await;
175        assert!(result.is_ok());
176    }
177
178    #[tokio::test]
179    async fn version_command() {
180        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
181        let result = run_main(vec!["cpp-linter".to_string(), "version".to_string()]).await;
182        assert!(result.is_ok());
183    }
184
185    #[tokio::test]
186    async fn force_debug_output() {
187        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
188        let result = run_main(vec![
189            "cpp-linter".to_string(),
190            "-l".to_string(),
191            "false".to_string(),
192            "-v".to_string(),
193            "debug".to_string(),
194        ])
195        .await;
196        assert!(result.is_ok());
197    }
198
199    #[tokio::test]
200    async fn bad_version_input() {
201        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
202        let result = run_main(vec![
203            "cpp-linter".to_string(),
204            "-l".to_string(),
205            "false".to_string(),
206            "-V".to_string(),
207        ])
208        .await;
209        assert!(result.is_err());
210    }
211
212    #[tokio::test]
213    async fn pre_commit_env() {
214        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
215        env::set_var("PRE_COMMIT", "1");
216        let result = run_main(vec![
217            "cpp-linter".to_string(),
218            "-l".to_string(),
219            "false".to_string(),
220        ])
221        .await;
222        assert!(result.is_err());
223    }
224
225    // Verifies that the system gracefully handles cases where all analysis is disabled.
226    // This ensures no diagnostic comments are generated when analysis is explicitly skipped.
227    #[tokio::test]
228    async fn no_analysis() {
229        env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests
230        let result = run_main(vec![
231            "cpp-linter".to_string(),
232            "-l".to_string(),
233            "false".to_string(),
234            "--style".to_string(),
235            String::new(),
236            "--tidy-checks=-*".to_string(),
237        ])
238        .await;
239        assert!(result.is_ok());
240    }
241}