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

Issue:
[bug] sg run -c worng.yml should report error
### Please read the FAQ for the bug you encountered.

- [x] I have read the existing FAQ

### ⏯ Playground Link

N/A

### 💻 Code

```bash
sg run -c invalid.yml -p test
```

This command should report the yaml 

### 🙁 Actual behavior

not reporting error. ast-grep just silently ignored the wrong configuration.

### 🙂 Expected behavior

reporting error

### Additional information about the issue

_No response_

Pull request:
fix: report error in run with wrong yaml
fix #1768

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

- **New Features**
  - Enhanced configuration error handling with more explicit result types.
  
- **Tests**
  - Added tests for handling invalid and non-existent configuration files.

- **Bug Fixes**
  - Improved error reporting for the configuration setup process.

These changes improve clarity in error handling and expand test coverage to ensure robust management of configuration-related issues.
<!-- 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 f52bd50f..2c03889e 100644
--- a/crates/cli/src/config.rs
+++ b/crates/cli/src/config.rs
@@ -67,12 +67,11 @@ pub struct ProjectConfig {
 impl ProjectConfig {
   // return None if config file does not exist
   fn discover_project(config_path: Option<PathBuf>) -> Result<Option<(PathBuf, AstGrepConfig)>> {
-    let config_path = find_config_path_with_default(config_path).context(EC::ReadConfiguration)?;
+    let config_path = find_config_path_with_default(config_path).context(EC::ProjectNotExist)?;
     // NOTE: if config file does not exist, return None
-    // this is not 100% correct because of racing condition
-    if !config_path.is_file() {
+    let Some(config_path) = config_path else {
       return Ok(None);
-    }
+    };
     let config_str = read_to_string(&config_path).context(EC::ReadConfiguration)?;
     let sg_config: AstGrepConfig = from_str(&config_str).context(EC::ParseConfiguration)?;
     let project_dir = config_path
@@ -89,7 +88,9 @@ impl ProjectConfig {
     let global_rules = find_util_rules(self)?;
     read_directory_yaml(self, global_rules, rule_overwrite)
   }
-  // do not report error if no sgconfig.yml is found
+  /// returns a Result of Result.
+  /// The inner Result is for configuration not found, or ProjectNotExist
+  /// The outer Result is for definitely wrong config.
   pub fn setup(config_path: Option<PathBuf>) -> Result<Result<Self>> {
     let Some((project_dir, mut sg_config)) = Self::discover_project(config_path)? else {
       return Ok(Err(anyhow::anyhow!(EC::ProjectNotExist)));
@@ -229,20 +230,21 @@ pub fn read_rule_file(
 
 const CONFIG_FILE: &str = "sgconfig.yml";
 
-fn find_config_path_with_default(config_path: Option<PathBuf>) -> Result<PathBuf> {
-  if let Some(config) = config_path {
-    return Ok(config);
+/// return None if config file does not exist
+fn find_config_path_with_default(config_path: Option<PathBuf>) -> Result<Option<PathBuf>> {
+  if config_path.is_some() {
+    return Ok(config_path);
   }
   let mut path = std::env::current_dir()?;
   loop {
     let maybe_config = path.join(CONFIG_FILE);
     if maybe_config.exists() {
-      break Ok(maybe_config);
+      break Ok(Some(maybe_config));
     }
     if let Some(parent) = path.parent() {
       path = parent.to_path_buf();
     } else {
-      break Ok(PathBuf::from(CONFIG_FILE));
+      break Ok(None);
     }
   }
 }
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
index ea9be868..b125b611 100644
--- a/crates/cli/src/lib.rs
+++ b/crates/cli/src/lib.rs
@@ -98,7 +98,7 @@ fn try_default_run(args: &[String]) -> Result<Option<RunArg>> {
 }
 
 /// finding project and setup custom language configuration
-fn setup_project_is_possible(args: &[String]) -> Result<ProjectConfig> {
+fn setup_project_is_possible(args: &[String]) -> Result<Result<ProjectConfig>> {
   let mut config = None;
   for i in 0..args.len() {
     let arg = &args[i];
@@ -118,13 +118,13 @@ fn setup_project_is_possible(args: &[String]) -> Result<ProjectConfig> {
     let config_file = (&args[i + 1]).into();
     config = Some(config_file);
   }
-  ProjectConfig::setup(config)?
+  ProjectConfig::setup(config)
 }
 
 // this wrapper function is for testing
 pub fn main_with_args(args: impl Iterator<Item = String>) -> Result<()> {
   let args: Vec<_> = args.collect();
-  let project = setup_project_is_possible(&args);
+  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);


Test patch:
diff --git a/crates/cli/tests/run_test.rs b/crates/cli/tests/run_test.rs
index a7726557..d4fe3e55 100644
--- a/crates/cli/tests/run_test.rs
+++ b/crates/cli/tests/run_test.rs
@@ -81,3 +81,27 @@ fn test_debug_query() -> Result<()> {
 
   Ok(())
 }
+
+#[test]
+fn test_invalid_sg_config() -> Result<()> {
+  let dir = create_test_files([("invalid.yml", "invalid")])?;
+  Command::cargo_bin("ast-grep")?
+    .current_dir(dir.path())
+    .args(["-p", "alert($A)", "-c", "invalid.yml"])
+    .assert()
+    .failure()
+    .stderr(contains("Cannot parse configuration"));
+  Ok(())
+}
+
+#[test]
+fn test_unfound_sg_config() -> Result<()> {
+  let dir = create_test_files([])?;
+  Command::cargo_bin("ast-grep")?
+    .current_dir(dir.path())
+    .args(["-p", "alert($A)", "-c", "not-found.yml"])
+    .assert()
+    .failure()
+    .stderr(contains("Cannot read configuration"));
+  Ok(())
+}