Skip to content

Commit 41b60a4

Browse files
committed
Refactored language heuristics
1 parent e2be981 commit 41b60a4

File tree

17 files changed

+1190
-200
lines changed

17 files changed

+1190
-200
lines changed

docs/src/content/docs/features/ambiguity-resolution.mdx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,31 @@ Renamify uses a sophisticated multi-level approach to resolve ambiguities, tryin
1919

2020
### 1. Language-Specific Conventions
2121

22-
For known programming languages, Renamify applies language-specific heuristics based on syntactic context.
22+
For known programming languages, Renamify applies language-specific heuristics based on syntactic context. The system supports over 30 file extensions with intelligent context-aware suggestions.
23+
24+
**Supported Languages:**
25+
- **Ruby** (.rb, .rake, .gemspec): Classes/modules → PascalCase, methods → snake_case, constants → SCREAMING_SNAKE
26+
- **Python** (.py, .pyw, .pyi): Classes → PascalCase, functions/decorators → snake_case, constants → SCREAMING_SNAKE
27+
- **JavaScript/TypeScript** (.js, .jsx, .ts, .tsx, .mjs, .cjs): Classes/interfaces → PascalCase, variables/functions → camelCase, constants → SCREAMING_SNAKE
28+
- **Go** (.go): Exported types/functions → PascalCase, unexported → camelCase, packages → lowercase
29+
- **Rust** (.rs): Types → PascalCase, functions/modules → snake_case, constants → SCREAMING_SNAKE, macros → snake_case
30+
- **Java/Kotlin** (.java, .kt, .kts): Classes → PascalCase, methods/fields → camelCase, constants → SCREAMING_SNAKE
31+
- **C/C++** (.c, .cpp, .cc, .h, .hpp): Classes/structs → PascalCase or snake_case, macros → SCREAMING_SNAKE
32+
- **Shell Scripts** (.sh, .bash, .zsh): Environment variables → SCREAMING_SNAKE, functions/locals → snake_case
33+
- **Web Technologies** (HTML, CSS, YAML, JSON): Attributes/classes → kebab-case, config keys → snake_case or camelCase
2334

2435
**Ruby Example:**
2536
```ruby
2637
# "User" after "class" -> PascalCase expected
2738
class User
2839
end
2940

30-
# "API" after "module" -> PascalCase expected
31-
# (using Active Support Inflections for acronyms)
32-
module API
41+
# "process_data" after "def" -> snake_case expected
42+
def process_data
3343
end
44+
45+
# "MAX_SIZE" in constant context -> SCREAMING_SNAKE expected
46+
MAX_SIZE = 100
3447
```
3548

3649
**Python Example:**
@@ -40,6 +53,18 @@ def get_user(): # snake_case expected after "def"
4053

4154
class UserModel: # PascalCase expected after "class"
4255
pass
56+
57+
@decorator_name # snake_case expected after "@"
58+
def method():
59+
pass
60+
```
61+
62+
**JavaScript Example:**
63+
```javascript
64+
class UserController { } // PascalCase after "class"
65+
const userName = ""; // camelCase after "const"
66+
const MAX_RETRIES = 3; // SCREAMING_SNAKE in all-caps context
67+
process.env.NODE_ENV // SCREAMING_SNAKE for env vars
4368
```
4469

4570
:::note
Lines changed: 67 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::case_model::Style;
22
use std::path::Path;
33

4+
use super::languages;
5+
46
/// Language-specific heuristics for resolving case style ambiguity
57
pub struct LanguageHeuristics;
68

@@ -19,198 +21,40 @@ impl LanguageHeuristics {
1921
let context = preceding_context.trim();
2022

2123
match extension {
22-
"rb" => Self::ruby_heuristics(context, possible_styles),
23-
"py" => Self::python_heuristics(context, possible_styles),
24-
"js" | "jsx" | "ts" | "tsx" => Self::javascript_heuristics(context, possible_styles),
25-
"go" => Self::go_heuristics(context, possible_styles),
26-
"rs" => Self::rust_heuristics(context, possible_styles),
27-
"java" => Self::java_heuristics(context, possible_styles),
28-
"c" | "cpp" | "cc" | "h" | "hpp" => Self::c_cpp_heuristics(context, possible_styles),
29-
"css" | "scss" | "sass" | "less" => Self::css_heuristics(context, possible_styles),
30-
"html" | "htm" | "xml" => Self::html_heuristics(context, possible_styles),
31-
_ => None,
32-
}
33-
}
34-
35-
fn ruby_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
36-
if context.ends_with("class") || context.ends_with("module") {
37-
// Classes and modules should be PascalCase
38-
if possible_styles.contains(&Style::Pascal) {
39-
return Some(Style::Pascal);
40-
}
41-
} else if context.ends_with("def") {
42-
// Methods should be snake_case
43-
if possible_styles.contains(&Style::Snake) {
44-
return Some(Style::Snake);
45-
}
46-
} else if context.contains("CONSTANT") || context.contains("VERSION") {
47-
// Constants are SCREAMING_SNAKE
48-
if possible_styles.contains(&Style::ScreamingSnake) {
49-
return Some(Style::ScreamingSnake);
50-
}
51-
}
52-
None
53-
}
54-
55-
fn python_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
56-
if context.ends_with("class") {
57-
// Classes should be PascalCase
58-
if possible_styles.contains(&Style::Pascal) {
59-
return Some(Style::Pascal);
60-
}
61-
} else if context.ends_with("def") {
62-
// Functions should be snake_case
63-
if possible_styles.contains(&Style::Snake) {
64-
return Some(Style::Snake);
65-
}
66-
} else if context
67-
.chars()
68-
.all(|c| c.is_uppercase() || c == '_' || c == '=')
69-
{
70-
// Constants (all caps context)
71-
if possible_styles.contains(&Style::ScreamingSnake) {
72-
return Some(Style::ScreamingSnake);
73-
}
74-
}
75-
None
76-
}
77-
78-
fn javascript_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
79-
if context.ends_with("class") || context.ends_with("interface") || context.ends_with("type")
80-
{
81-
// Classes, interfaces, and types should be PascalCase
82-
if possible_styles.contains(&Style::Pascal) {
83-
return Some(Style::Pascal);
84-
}
85-
} else if context.ends_with("function")
86-
|| context.ends_with("const")
87-
|| context.ends_with("let")
88-
|| context.ends_with("var")
89-
{
90-
// Functions and variables typically camelCase
91-
if possible_styles.contains(&Style::Camel) {
92-
return Some(Style::Camel);
93-
}
94-
} else if context.contains("export const")
95-
&& possible_styles.contains(&Style::ScreamingSnake)
96-
{
97-
// Exported constants might be SCREAMING_SNAKE
98-
return Some(Style::ScreamingSnake);
99-
}
100-
None
101-
}
102-
103-
fn go_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
104-
if context.ends_with("type")
105-
|| context.ends_with("struct")
106-
|| context.ends_with("interface")
107-
{
108-
// Types: PascalCase for exported, camelCase for private
109-
// Since we don't know if it's exported, prefer PascalCase if available
110-
if possible_styles.contains(&Style::Pascal) {
111-
return Some(Style::Pascal);
112-
} else if possible_styles.contains(&Style::Camel) {
113-
return Some(Style::Camel);
114-
}
115-
} else if context.ends_with("func") {
116-
// Functions: same as types
117-
if possible_styles.contains(&Style::Pascal) {
118-
return Some(Style::Pascal);
119-
} else if possible_styles.contains(&Style::Camel) {
120-
return Some(Style::Camel);
121-
}
122-
}
123-
None
124-
}
125-
126-
fn rust_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
127-
if context.ends_with("struct")
128-
|| context.ends_with("enum")
129-
|| context.ends_with("trait")
130-
|| context.ends_with("impl")
131-
{
132-
// Types should be PascalCase
133-
if possible_styles.contains(&Style::Pascal) {
134-
return Some(Style::Pascal);
135-
}
136-
} else if context.ends_with("fn") || context.ends_with("let") || context.ends_with("mut") {
137-
// Functions and variables should be snake_case
138-
if possible_styles.contains(&Style::Snake) {
139-
return Some(Style::Snake);
140-
}
141-
} else if context.ends_with("const") || context.ends_with("static") {
142-
// Constants are SCREAMING_SNAKE
143-
if possible_styles.contains(&Style::ScreamingSnake) {
144-
return Some(Style::ScreamingSnake);
145-
}
146-
}
147-
None
148-
}
149-
150-
fn java_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
151-
if context.ends_with("class") || context.ends_with("interface") || context.ends_with("enum")
152-
{
153-
// Classes should be PascalCase
154-
if possible_styles.contains(&Style::Pascal) {
155-
return Some(Style::Pascal);
156-
}
157-
} else if context.contains("private")
158-
|| context.contains("public")
159-
|| context.contains("protected")
160-
{
161-
// Methods and fields typically camelCase
162-
if possible_styles.contains(&Style::Camel) {
163-
return Some(Style::Camel);
164-
}
165-
} else if context.ends_with("final") && context.contains("static") {
166-
// Constants are SCREAMING_SNAKE
167-
if possible_styles.contains(&Style::ScreamingSnake) {
168-
return Some(Style::ScreamingSnake);
169-
}
170-
}
171-
None
172-
}
24+
// Programming languages
25+
"rb" | "rake" | "gemspec" => languages::ruby::suggest_style(context, possible_styles),
26+
"py" | "pyw" | "pyi" => languages::python::suggest_style(context, possible_styles),
27+
"js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" => {
28+
languages::javascript::suggest_style(context, possible_styles)
29+
},
30+
"go" => languages::go::suggest_style(context, possible_styles),
31+
"rs" => languages::rust::suggest_style(context, possible_styles),
32+
"java" | "kt" | "kts" => languages::java::suggest_style(context, possible_styles),
33+
"c" | "cpp" | "cc" | "cxx" | "h" | "hpp" | "hxx" => {
34+
languages::c_cpp::suggest_style(context, possible_styles)
35+
},
36+
37+
// Web technologies
38+
"css" | "scss" | "sass" | "less" | "styl" => {
39+
languages::css::suggest_style(context, possible_styles)
40+
},
41+
"html" | "htm" | "xml" | "svg" | "vue" => {
42+
languages::html::suggest_style(context, possible_styles)
43+
},
44+
45+
// Shell and scripting
46+
"sh" | "bash" | "zsh" | "fish" | "ksh" => {
47+
languages::shell::suggest_style(context, possible_styles)
48+
},
49+
50+
// Configuration files
51+
"yml" | "yaml" => languages::yaml::suggest_style(context, possible_styles),
52+
"json" | "jsonc" | "json5" | "toml" | "ini" | "cfg" | "conf" | "env" => {
53+
languages::config::suggest_style(context, possible_styles)
54+
},
17355

174-
fn c_cpp_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
175-
if context.ends_with("class") || context.ends_with("struct") {
176-
// Classes and structs often PascalCase
177-
if possible_styles.contains(&Style::Pascal) {
178-
return Some(Style::Pascal);
179-
}
180-
} else if context.ends_with("#define") {
181-
// Macros are SCREAMING_SNAKE
182-
if possible_styles.contains(&Style::ScreamingSnake) {
183-
return Some(Style::ScreamingSnake);
184-
}
185-
} else if context.contains("typedef") {
186-
// Typedefs often use snake_case or PascalCase
187-
if possible_styles.contains(&Style::Pascal) {
188-
return Some(Style::Pascal);
189-
} else if possible_styles.contains(&Style::Snake) {
190-
return Some(Style::Snake);
191-
}
192-
}
193-
None
194-
}
195-
196-
fn css_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
197-
// CSS classes and IDs typically use kebab-case
198-
if (context.ends_with('.') || context.ends_with('#') || context.contains("class="))
199-
&& possible_styles.contains(&Style::Kebab)
200-
{
201-
return Some(Style::Kebab);
202-
}
203-
None
204-
}
205-
206-
fn html_heuristics(context: &str, possible_styles: &[Style]) -> Option<Style> {
207-
// HTML attributes and data attributes use kebab-case
208-
if (context.contains("data-") || context.contains("class=") || context.contains("id="))
209-
&& possible_styles.contains(&Style::Kebab)
210-
{
211-
return Some(Style::Kebab);
56+
_ => None,
21257
}
213-
None
21458
}
21559
}
21660

@@ -249,18 +93,46 @@ mod tests {
24993
#[test]
25094
fn test_javascript_function_heuristic() {
25195
let path = PathBuf::from("test.js");
252-
let possible_styles = vec![Style::Camel, Style::Snake, Style::Kebab];
96+
let possible_styles = vec![Style::Camel, Style::Snake];
25397

25498
let result = LanguageHeuristics::suggest_style(&path, "function ", &possible_styles);
25599
assert_eq!(result, Some(Style::Camel));
256100
}
257101

258102
#[test]
259103
fn test_no_matching_style() {
260-
let path = PathBuf::from("test.rb");
261-
let possible_styles = vec![Style::Camel, Style::Snake]; // No PascalCase
104+
let path = PathBuf::from("test.py");
105+
let possible_styles = vec![Style::Kebab]; // Python doesn't use kebab
262106

263-
let result = LanguageHeuristics::suggest_style(&path, "class ", &possible_styles);
264-
assert_eq!(result, None); // Can't suggest PascalCase if it's not possible
107+
let result = LanguageHeuristics::suggest_style(&path, "def ", &possible_styles);
108+
assert_eq!(result, None);
109+
}
110+
111+
#[test]
112+
fn test_unsupported_extension() {
113+
let path = PathBuf::from("test.xyz");
114+
let possible_styles = vec![Style::Snake, Style::Camel];
115+
116+
let result = LanguageHeuristics::suggest_style(&path, "function ", &possible_styles);
117+
assert_eq!(result, None);
118+
}
119+
120+
#[test]
121+
fn test_shell_export_heuristic() {
122+
let path = PathBuf::from("script.sh");
123+
let possible_styles = vec![Style::ScreamingSnake, Style::Snake];
124+
125+
// Note: "export " gets trimmed to "export" in suggest_style
126+
let result = LanguageHeuristics::suggest_style(&path, "export ", &possible_styles);
127+
assert_eq!(result, Some(Style::ScreamingSnake));
128+
}
129+
130+
#[test]
131+
fn test_yaml_key_heuristic() {
132+
let path = PathBuf::from("config.yml");
133+
let possible_styles = vec![Style::Snake, Style::Camel];
134+
135+
let result = LanguageHeuristics::suggest_style(&path, "key:", &possible_styles);
136+
assert_eq!(result, Some(Style::Snake));
265137
}
266138
}

0 commit comments

Comments
 (0)