Repository: ast-grep/ast-grep (rust)

Issue:
[feature] add tracing of resolved configuration file path
# ⭐ Suggestion

`ast-grep scan --tracing summary` should also print which configuration is used

<!-- A summary of what you'd like to see added or changed -->

# 💻 Use Cases

debug the configuration file

Pull request:
feat: add tracing of resolved configuration file path
fix #1755 

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced enhanced tracing capabilities, including the addition of file tracing information.
  - Added project-specific configuration handling across multiple functions.

- **Refactor**
  - Updated method signatures to improve runtime tracing and execution flow management.
  - Enhanced parameter handling for run patterns and specific language execution.

- **Bug Fixes**
  - Resolved issues related to the handling of tracing information within various components.

- **Tests**
  - Added new tests to validate project configuration handling and its impact on command execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs
index 2c03889e..63dbb248 100644
--- a/crates/cli/src/config.rs
+++ b/crates/cli/src/config.rs
@@ -196,6 +196,7 @@ fn read_directory_yaml(
   let collection = RuleCollection::try_new(configs).context(EC::GlobPattern)?;
   let effective_rule_count = collection.total_rule_count();
   let trace = RuleTrace {
+    file_trace: Default::default(),
     effective_rule_count,
     skipped_rule_count: total_rule_count - effective_rule_count,
   };
@@ -209,6 +210,7 @@ pub fn with_rule_stats(
   let collection = RuleCollection::try_new(configs).context(EC::GlobPattern)?;
   let effective_rule_count = collection.total_rule_count();
   let trace = RuleTrace {
+    file_trace: Default::default(),
     effective_rule_count,
     skipped_rule_count: total_rule_count - effective_rule_count,
   };
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
index b125b611..3aa9c10f 100644
--- a/crates/cli/src/lib.rs
+++ b/crates/cli/src/lib.rs
@@ -127,12 +127,12 @@ pub fn main_with_args(args: impl Iterator<Item = String>) -> Result<()> {
   let project = setup_project_is_possible(&args)?;
   // register_custom_language_if_is_run(&args)?;
   if let Some(arg) = try_default_run(&args)? {
-    return run_with_pattern(arg);
+    return run_with_pattern(arg, project);
   }
 
   let app = App::try_parse_from(args)?;
   match app.command {
-    Commands::Run(arg) => run_with_pattern(arg),
+    Commands::Run(arg) => run_with_pattern(arg, project),
     Commands::Scan(arg) => run_with_config(arg, project),
     Commands::Test(arg) => run_test_rule(arg, project),
     Commands::New(arg) => run_create_new(arg, project),
diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs
index b66d90bb..e178a80e 100644
--- a/crates/cli/src/run.rs
+++ b/crates/cli/src/run.rs
@@ -7,6 +7,7 @@ use ast_grep_language::Language;
 use clap::{builder::PossibleValue, Parser, ValueEnum};
 use ignore::WalkParallel;
 
+use crate::config::ProjectConfig;
 use crate::lang::SgLang;
 use crate::print::{ColoredPrinter, Diff, Heading, InteractivePrinter, JSONPrinter, Printer};
 use crate::utils::ErrorContext as EC;
@@ -140,7 +141,9 @@ impl RunArg {
 
 // Every run will include Search or Replace
 // Search or Replace by arguments `pattern` and `rewrite` passed from CLI
-pub fn run_with_pattern(arg: RunArg) -> Result<()> {
+pub fn run_with_pattern(arg: RunArg, project: Result<ProjectConfig>) -> Result<()> {
+  let proj = arg.output.inspect.project_trace();
+  proj.print_project(&project)?;
   let context = arg.context.get();
   if let Some(json) = arg.output.json {
     let printer = JSONPrinter::stdout(json).context(context);
@@ -160,12 +163,12 @@ pub fn run_with_pattern(arg: RunArg) -> Result<()> {
 }
 
 fn run_pattern_with_printer(arg: RunArg, printer: impl Printer + 'static) -> Result<()> {
+  let trace = arg.output.inspect.run_trace();
   if arg.input.stdin {
-    RunWithSpecificLang::new(arg)?.run_std_in(printer)
+    RunWithSpecificLang::new(arg, trace)?.run_std_in(printer)
   } else if arg.lang.is_some() {
-    RunWithSpecificLang::new(arg)?.run_path(printer)
+    RunWithSpecificLang::new(arg, trace)?.run_path(printer)
   } else {
-    let trace = arg.output.inspect.run_trace();
     RunWithInferredLang { arg, trace }.run_path(printer)
   }
 }
@@ -206,7 +209,7 @@ impl PathWorker for RunWithInferredLang {
     self.arg.input.walk()
   }
   fn get_trace(&self) -> &FileTrace {
-    &self.trace.file_trace
+    &self.trace.inner
   }
 
   fn produce_item(&self, path: &Path) -> Option<Vec<Self::Item>> {
@@ -234,7 +237,7 @@ struct RunWithSpecificLang {
 }
 
 impl RunWithSpecificLang {
-  fn new(arg: RunArg) -> Result<Self> {
+  fn new(arg: RunArg, stats: RunTrace) -> Result<Self> {
     let lang = arg.lang.ok_or(anyhow::anyhow!(EC::LanguageNotSpecified))?;
     // do not unwrap result here
     let pattern_ret = arg.build_pattern(lang);
@@ -244,7 +247,6 @@ impl RunWithSpecificLang {
     } else {
       None
     };
-    let stats = arg.output.inspect.run_trace();
     Ok(Self {
       arg,
       pattern: pattern_ret?,
@@ -280,7 +282,7 @@ impl PathWorker for RunWithSpecificLang {
     Ok(self.arg.input.walk_lang(lang))
   }
   fn get_trace(&self) -> &FileTrace {
-    &self.stats.file_trace
+    &self.stats.inner
   }
   fn produce_item(&self, path: &Path) -> Option<Vec<Self::Item>> {
     let arg = &self.arg;
@@ -374,7 +376,8 @@ mod test {
       pattern: "console.log".to_string(),
       ..default_run_arg()
     };
-    assert!(run_with_pattern(arg).is_ok())
+    let proj = Err(anyhow::anyhow!("no project"));
+    assert!(run_with_pattern(arg, proj).is_ok())
   }
 
   #[test]
@@ -384,7 +387,8 @@ mod test {
       strictness: Some(Strictness(MatchStrictness::Ast)),
       ..default_run_arg()
     };
-    assert!(run_with_pattern(arg).is_ok())
+    let proj = Err(anyhow::anyhow!("no project"));
+    assert!(run_with_pattern(arg, proj).is_ok())
   }
 
   #[test]
@@ -394,6 +398,7 @@ mod test {
       lang: Some(SupportLang::Rust.into()),
       ..default_run_arg()
     };
-    assert!(run_with_pattern(arg).is_ok())
+    let proj = Err(anyhow::anyhow!("no project"));
+    assert!(run_with_pattern(arg, proj).is_ok())
   }
 }
diff --git a/crates/cli/src/scan.rs b/crates/cli/src/scan.rs
index 5bc6da3b..07ae2110 100644
--- a/crates/cli/src/scan.rs
+++ b/crates/cli/src/scan.rs
@@ -73,6 +73,8 @@ impl ScanArg {
 }
 
 pub fn run_with_config(arg: ScanArg, project: Result<ProjectConfig>) -> Result<()> {
+  let project_trace = arg.output.inspect.project_trace();
+  project_trace.print_project(&project)?;
   let context = arg.context.get();
   if let Some(_format) = &arg.format {
     let printer = CloudPrinter::stdout();
@@ -198,7 +200,7 @@ fn unused_suppression_rule_config(arg: &ScanArg, overwrite: &RuleOverwrite) -> R
 
 impl PathWorker for ScanWithConfig {
   fn get_trace(&self) -> &FileTrace {
-    &self.trace.file_trace
+    &self.trace.inner.file_trace
   }
   fn build_walk(&self) -> Result<WalkParallel> {
     let mut langs = HashSet::new();
diff --git a/crates/cli/src/utils/inspect.rs b/crates/cli/src/utils/inspect.rs
index 946002e4..5d03eab5 100644
--- a/crates/cli/src/utils/inspect.rs
+++ b/crates/cli/src/utils/inspect.rs
@@ -10,6 +10,7 @@
 //!   * rules skipped (dues to ignore/files)
 //! - Detail level: show how a rule runs on a file
 
+use crate::config::ProjectConfig;
 use crate::lang::SgLang;
 use ast_grep_config::{RuleCollection, RuleConfig};
 
@@ -45,14 +46,24 @@ impl fmt::Debug for Granularity {
 }
 
 impl Granularity {
+  pub fn project_trace(&self) -> ProjectTrace {
+    self.project_trace_impl(std::io::stderr())
+  }
+  fn project_trace_impl<W: Write>(&self, w: W) -> TraceInfo<(), W> {
+    TraceInfo {
+      level: *self,
+      inner: (),
+      output: Mutex::new(w),
+    }
+  }
+
   pub fn run_trace(&self) -> RunTrace {
     self.run_trace_impl(std::io::stderr())
   }
-  fn run_trace_impl<W: Write>(&self, w: W) -> TraceInfo<(), W> {
+  fn run_trace_impl<W: Write>(&self, w: W) -> TraceInfo<FileTrace, W> {
     TraceInfo {
       level: *self,
-      inner: (),
-      file_trace: Default::default(),
+      inner: Default::default(),
       output: Mutex::new(w),
     }
   }
@@ -64,7 +75,6 @@ impl Granularity {
     TraceInfo {
       level: *self,
       inner: rule_stats,
-      file_trace: Default::default(),
       output: Mutex::new(w),
     }
   }
@@ -89,7 +99,6 @@ impl FileTrace {
 
 pub struct TraceInfo<T, W: Write> {
   pub level: Granularity,
-  pub file_trace: FileTrace,
   pub inner: T,
   output: Mutex<W>,
 }
@@ -133,20 +142,33 @@ impl<T, W: Write + Sync> TraceInfo<T, W> {
     })
   }
 
-  fn print_files(&self) -> Result<()> {
+  fn print_files(&self, trace: &FileTrace) -> Result<()> {
     self.print_summary("file", |w| {
-      let scanned = self.file_trace.files_scanned.load(Ordering::Acquire);
-      let skipped = self.file_trace.files_skipped.load(Ordering::Acquire);
+      let scanned = trace.files_scanned.load(Ordering::Acquire);
+      let skipped = trace.files_skipped.load(Ordering::Acquire);
       write!(w, "scannedFileCount={scanned},skippedFileCount={skipped}")?;
       Ok(())
-    })?;
-    Ok(())
+    })
   }
 }
 
 impl<W: Write + Sync> TraceInfo<(), W> {
+  pub fn print_project(&self, project: &Result<ProjectConfig>) -> Result<()> {
+    self.print_summary("project", |w| {
+      if let Ok(project) = project {
+        let dir = project.project_dir.display();
+        write!(w, "isProject=true,projectDir={}", dir)?;
+      } else {
+        write!(w, "isProject=false")?;
+      }
+      Ok(())
+    })
+  }
+}
+
+impl<W: Write + Sync> TraceInfo<FileTrace, W> {
   pub fn print(&self) -> Result<()> {
-    self.print_files()
+    self.print_files(&self.inner)
   }
 
   pub fn print_file(&self, path: &Path, lang: SgLang) -> Result<()> {
@@ -160,7 +182,7 @@ impl<W: Write + Sync> TraceInfo<(), W> {
 impl<W: Write + Sync> TraceInfo<RuleTrace, W> {
   // TODO: support more format?
   pub fn print(&self) -> Result<()> {
-    self.print_files()?;
+    self.print_files(&self.inner.file_trace)?;
     self.print_summary("rule", |w| {
       let (effective, skipped) = (
         self.inner.effective_rule_count,
@@ -174,6 +196,7 @@ impl<W: Write + Sync> TraceInfo<RuleTrace, W> {
     })?;
     Ok(())
   }
+
   pub fn print_file(&self, path: &Path, lang: SgLang, rules: &[&RuleConfig<SgLang>]) -> Result<()> {
     self.print_entity("file", path.display(), |w| {
       let len = rules.len();
@@ -199,11 +222,13 @@ impl<W: Write + Sync> TraceInfo<RuleTrace, W> {
 
 #[derive(Default)]
 pub struct RuleTrace {
+  pub file_trace: FileTrace,
   pub effective_rule_count: usize,
   pub skipped_rule_count: usize,
 }
 
-pub type RunTrace = TraceInfo<(), Stderr>;
+pub type ProjectTrace = TraceInfo<(), Stderr>;
+pub type RunTrace = TraceInfo<FileTrace, Stderr>;
 pub type ScanTrace = TraceInfo<RuleTrace, Stderr>;
 
 #[cfg(test)]
@@ -216,14 +241,8 @@ mod test {
     let mut ret = String::new();
     let run_trace = tracing.run_trace_impl(unsafe { ret.as_mut_vec() });
     assert_eq!(run_trace.level, Granularity::Summary);
-    assert_eq!(
-      run_trace.file_trace.files_scanned.load(Ordering::Relaxed),
-      0
-    );
-    assert_eq!(
-      run_trace.file_trace.files_skipped.load(Ordering::Relaxed),
-      0
-    );
+    assert_eq!(run_trace.inner.files_scanned.load(Ordering::Relaxed), 0);
+    assert_eq!(run_trace.inner.files_skipped.load(Ordering::Relaxed), 0);
     assert!(run_trace.print().is_ok());
     assert_eq!(
       ret,
@@ -234,15 +253,24 @@ mod test {
     let rule_stats = RuleTrace {
       effective_rule_count: 10,
       skipped_rule_count: 2,
+      file_trace: Default::default(),
     };
     let scan_trace = tracing.scan_trace_impl(rule_stats, unsafe { ret.as_mut_vec() });
     assert_eq!(scan_trace.level, Granularity::Summary);
     assert_eq!(
-      scan_trace.file_trace.files_scanned.load(Ordering::Relaxed),
+      scan_trace
+        .inner
+        .file_trace
+        .files_scanned
+        .load(Ordering::Relaxed),
       0
     );
     assert_eq!(
-      scan_trace.file_trace.files_skipped.load(Ordering::Relaxed),
+      scan_trace
+        .inner
+        .file_trace
+        .files_skipped
+        .load(Ordering::Relaxed),
       0
     );
     assert_eq!(scan_trace.inner.effective_rule_count, 10);


Test patch:
diff --git a/crates/cli/tests/run_test.rs b/crates/cli/tests/run_test.rs
index d4fe3e55..e3c09c25 100644
--- a/crates/cli/tests/run_test.rs
+++ b/crates/cli/tests/run_test.rs
@@ -105,3 +105,33 @@ fn test_unfound_sg_config() -> Result<()> {
     .stderr(contains("Cannot read configuration"));
   Ok(())
 }
+
+#[test]
+fn test_trace_default_project() -> Result<()> {
+  let dir = create_test_files([("sgconfig.yml", "ruleDirs: []")])?;
+  Command::cargo_bin("ast-grep")?
+    .current_dir(dir.path())
+    .args(["-p", "alert($A)", "--inspect=summary"])
+    .assert()
+    .success()
+    .stderr(contains("isProject=true,projectDir"));
+  Ok(())
+}
+
+#[test]
+fn test_trace_project() -> Result<()> {
+  let dir = create_test_files([("not.yml", "ruleDirs: []")])?;
+  Command::cargo_bin("ast-grep")?
+    .current_dir(dir.path())
+    .args(["-p", "alert($A)", "--inspect=summary"])
+    .assert()
+    .success()
+    .stderr(contains("isProject=false"));
+  Command::cargo_bin("ast-grep")?
+    .current_dir(dir.path())
+    .args(["run", "-c=not.yml", "-p", "alert($A)", "--inspect=summary"])
+    .assert()
+    .success()
+    .stderr(contains("isProject=true,projectDir"));
+  Ok(())
+}