Repository: getgrit/gritql (rust)

Issue:
Error message when pattern name contains invalid characters is confusing
The error message that comes out when a pattern name contains a `-` is confusing and not helpful

Reproducer:
`grit.yaml`
```yaml
patterns:
  - name: use-winston
    level: error
    body: |
      `console.log($msg)` => `winston.log($msg)` where {
        $msg <: not within or { `it($_, $_)`, `test($_, $_)`, `describe($_, $_)` }
      }
```

Run:
```
$ grit apply use-winston
Your working tree currently has untracked changes and Grit will rewrite files in place. Do you want to proceed? yes
ERROR (code: 299) - Pattern syntax error at 1:1. If you hit this error while running grit apply on a pattern from the Grit standard library, try running grit init. If you are running a custom pattern, check out the docs at https://docs.grit.io/ for help with writing patterns.
```

Pull request description:
fix: improve error message from invalid pattern name
fixes #134 

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

## Summary by CodeRabbit

- **Refactor**
	- Enhanced module functionality by optimizing imports and removing duplicate code.
- **New Features**
	- Introduced a new `grit.yaml` file for tagging errors and providing code transformations.
	- Added a new `Button` class in test files to extend component functionality.
- **Bug Fixes**
	- Improved pattern name validation to ensure robust pattern handling.
- **Tests**
	- Added a test to validate the behavior of commands with invalid names.
- **Documentation**
	- Not explicitly mentioned, but changes imply an improvement in code documentation and debugging capabilities through the addition of debug attributes and error tagging.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/crates/cli/src/commands/apply_pattern.rs b/crates/cli/src/commands/apply_pattern.rs
index 10f181a0..fb710df6 100644
--- a/crates/cli/src/commands/apply_pattern.rs
+++ b/crates/cli/src/commands/apply_pattern.rs
@@ -18,6 +18,7 @@ use marzano_core::pattern::{
 use marzano_gritmodule::fetcher::KeepFetcherKind;
 use marzano_gritmodule::markdown::get_body_from_md_content;
 use marzano_gritmodule::searcher::find_grit_modules_dir;
+use marzano_gritmodule::utils::is_pattern_name;
 use marzano_language::target_language::{expand_paths, PatternLanguage};
 
 use marzano_util::position::Position;
@@ -47,7 +48,7 @@ use marzano_messenger::{
 };
 
 use crate::resolver::{get_grit_files_from_cwd, GritModuleResolver};
-use crate::utils::{has_uncommitted_changes, is_pattern_name};
+use crate::utils::has_uncommitted_changes;
 
 use super::init::init_config_from_cwd;
 
diff --git a/crates/cli/src/commands/plumbing.rs b/crates/cli/src/commands/plumbing.rs
index 3b475837..7d9e033c 100644
--- a/crates/cli/src/commands/plumbing.rs
+++ b/crates/cli/src/commands/plumbing.rs
@@ -5,6 +5,7 @@ use marzano_gritmodule::config::GritPatternTestInfo;
 use marzano_gritmodule::fetcher::KeepFetcherKind;
 use marzano_gritmodule::patterns_directory::PatternsDirectory;
 use marzano_gritmodule::searcher::find_grit_modules_dir;
+use marzano_gritmodule::utils::is_pattern_name;
 use marzano_messenger::emit::ApplyDetails;
 use serde::{Deserialize, Serialize};
 use std::io::{stdin, Read};
@@ -15,7 +16,6 @@ use crate::analytics::track_event_line;
 use crate::flags::GlobalFormatFlags;
 use crate::lister::list_applyables;
 use crate::resolver::{get_grit_files_from, resolve_from, Source};
-use crate::utils::is_pattern_name;
 
 use super::super::analytics::AnalyticsArgs;
 use super::apply_pattern::{run_apply_pattern, ApplyPatternArgs};
diff --git a/crates/cli/src/resolver.rs b/crates/cli/src/resolver.rs
index 1080027a..0b5412a9 100644
--- a/crates/cli/src/resolver.rs
+++ b/crates/cli/src/resolver.rs
@@ -32,6 +32,7 @@ pub struct GritModuleResolver<'a> {
     _root_directory: &'a str,
 }
 
+#[derive(Debug)]
 pub struct RichPattern<'b> {
     pub body: &'b str,
     pub name: Option<String>,
diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs
index 62841993..0b0e6e1c 100644
--- a/crates/cli/src/utils.rs
+++ b/crates/cli/src/utils.rs
@@ -1,7 +1,6 @@
 use clap::ValueEnum;
 use git2::{Repository, StatusOptions};
 use marzano_gritmodule::searcher::find_git_dir_from;
-use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{
     fmt::{Display, Formatter},
@@ -73,11 +72,6 @@ pub fn get_random_port() -> Option<u16> {
     Some(port)
 }
 
-pub fn is_pattern_name(pattern: &str) -> bool {
-    let regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*(\(\))?$").unwrap();
-    regex.is_match(pattern)
-}
-
 pub async fn has_uncommitted_changes(dir: PathBuf) -> bool {
     let git_dir = match find_git_dir_from(dir).await {
         Some(git_dir) => git_dir,
diff --git a/crates/cli_bin/fixtures/config_invalid_name/.grit/grit.yaml b/crates/cli_bin/fixtures/config_invalid_name/.grit/grit.yaml
new file mode 100644
index 00000000..7cf2dcd6
--- /dev/null
+++ b/crates/cli_bin/fixtures/config_invalid_name/.grit/grit.yaml
@@ -0,0 +1,9 @@
+version: 0.0.1
+patterns:
+  - name: test-bad
+    tags:
+      - style
+      - debugging
+    level: error
+    body: |
+      `$a = $b` => `console.log($b)`
diff --git a/crates/gritmodule/src/config.rs b/crates/gritmodule/src/config.rs
index 87e5c4ea..9dc2a81a 100644
--- a/crates/gritmodule/src/config.rs
+++ b/crates/gritmodule/src/config.rs
@@ -9,8 +9,8 @@ use std::{
 };
 use tree_sitter::Parser;
 
-use crate::{fetcher::ModuleRepo, parser::PatternFileExt};
-use anyhow::Result;
+use crate::{fetcher::ModuleRepo, parser::PatternFileExt, utils::is_pattern_name};
+use anyhow::{bail, Result};
 
 #[derive(Debug, Deserialize)]
 pub struct GritGitHubConfig {
@@ -273,6 +273,10 @@ pub fn pattern_config_to_model(
     let defined_local_name = split_name.next();
     let local_name = defined_local_name.unwrap_or(&pattern.name).to_string();
 
+    if !is_pattern_name(&local_name) && local_name != NAMESPACE_IMPORT_INDICATOR {
+        bail!("Invalid pattern name: {}. Grit patterns must match the regex /[\\^#A-Za-z_][A-Za-z0-9_]*/. For more info, consult the docs at https://docs.grit.io/guides/patterns#pattern-definitions.", local_name);
+    }
+
     let module: Option<ModuleRepo> = match repo {
         None => None,
         Some(_) => {
diff --git a/crates/gritmodule/src/utils.rs b/crates/gritmodule/src/utils.rs
index 09ce27f3..a15d5a63 100644
--- a/crates/gritmodule/src/utils.rs
+++ b/crates/gritmodule/src/utils.rs
@@ -2,6 +2,7 @@ use std::{env::current_exe, fs::canonicalize, path::Path};
 
 use anyhow::{bail, Result};
 use marzano_core::pattern::api::MatchResult;
+use regex::Regex;
 
 /// Extracts the *rewritten* (after applying a pattern) path from a `MatchResult`.
 pub fn extract_path(result: &MatchResult) -> Option<&String> {
@@ -25,3 +26,8 @@ pub fn remove_dir_all_safe(dir: &Path) -> Result<()> {
     std::fs::remove_dir_all(dir)?;
     Ok(())
 }
+
+pub fn is_pattern_name(pattern: &str) -> bool {
+    let regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*(\(\))?$").unwrap();
+    regex.is_match(pattern)
+}


Test patch:
diff --git a/crates/cli_bin/fixtures/config_invalid_name/test.js b/crates/cli_bin/fixtures/config_invalid_name/test.js
new file mode 100644
index 00000000..9c52261f
--- /dev/null
+++ b/crates/cli_bin/fixtures/config_invalid_name/test.js
@@ -0,0 +1,3 @@
+class Button extends Component {
+  a = 1;
+}
diff --git a/crates/cli_bin/tests/apply.rs b/crates/cli_bin/tests/apply.rs
index 7ab16a04..656aa38c 100644
--- a/crates/cli_bin/tests/apply.rs
+++ b/crates/cli_bin/tests/apply.rs
@@ -2263,3 +2263,23 @@ fn apply_only_in_diff() -> Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn config_pattern_with_invalid_name() -> Result<()> {
+    let (_temp_dir, dir) = get_fixture("config_invalid_name", true)?;
+
+    let mut cmd = get_test_cmd()?;
+
+    cmd.arg("apply").arg("test-bad").current_dir(dir.clone());
+
+    let output = cmd.output()?;
+
+    assert!(
+        !output.status.success(),
+        "Command should have failed"
+    );
+
+    assert_snapshot!(String::from_utf8(output.stderr)?);
+
+    Ok(())
+}
diff --git a/crates/cli_bin/tests/snapshots/apply__config_pattern_with_invalid_name.snap b/crates/cli_bin/tests/snapshots/apply__config_pattern_with_invalid_name.snap
new file mode 100644
index 00000000..f3d05129
--- /dev/null
+++ b/crates/cli_bin/tests/snapshots/apply__config_pattern_with_invalid_name.snap
@@ -0,0 +1,5 @@
+---
+source: crates/cli_bin/tests/apply.rs
+expression: "String::from_utf8(output.stderr)?"
+---
+Error: Invalid pattern name: test-bad. Grit patterns must match the regex /[\^#A-Za-z_][A-Za-z0-9_]*/. For more info, consult the docs at https://docs.grit.io/guides/patterns#pattern-definitions.