1use crate::attributes::{collect_attrs_for_path, AttrValue, MacroTable};
13use crate::config::ConfigSet;
14use regex::{Regex, RegexBuilder};
15use std::collections::BTreeMap;
16use std::sync::{Arc, OnceLock};
17
18const BUILTIN_PATTERN_DEFS: &[(&str, &str, bool)] = &[
20 (
21 "ada",
22 r"!^(.*[ ])?(is[ ]+new|renames|is[ ]+separate)([ ].*)?$
23!^[ ]*with[ ].*$
24^[ ]*((procedure|function)[ ]+.*)$
25^[ ]*((package|protected|task)[ ]+.*)$",
26 true,
27 ),
28 (
29 "bash",
30 r"^[ ]*((([a-zA-Z_][a-zA-Z0-9_]*[ ]*\([ ]*\))|(function[ ]+[a-zA-Z_][a-zA-Z0-9_]*(([ ]*\([ ]*\))|([ ]+)))).*$)",
31 false,
32 ),
33 (
34 "bibtex",
35 r#"(@[a-zA-Z]{1,}[ ]*\{{0,1}[ ]*[^ "@',\#}{~%]*).*$"#,
36 false,
37 ),
38 (
39 "cpp",
40 r"!^[ ]*[A-Za-z_][A-Za-z_0-9]*:[[:space:]]*($|/[/*])
41^((::[[:space:]]*)?[A-Za-z_].*)$",
42 false,
43 ),
44 (
45 "csharp",
46 r"!(^|[ ]+)(do|while|for|foreach|if|else|new|default|return|switch|case|throw|catch|using|lock|fixed)([ (]+|$)
47^[ ]*(([][[:alnum:]@_.](<[][[:alnum:]@_, <>]+>)?)+([ ]+([][[:alnum:]@_.](<[][[:alnum:]@_, <>]+>)?)+)+[ ]*\([^;]*)$
48^[ ]*(([][[:alnum:]@_.](<[][[:alnum:]@_, <>]+>)?)+([ ]+([][[:alnum:]@_.](<[][[:alnum:]@_, <>]+>)?)+)+[^;=:,()]*)$
49^[ ]*(((static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)[ ]+)*(class|enum|interface|struct|record)[ ]+.*)$
50^[ ]*(namespace[ ]+.*)$",
51 false,
52 ),
53 (
54 "css",
55 r"![:;][[:space:]]*$
56^[:[@.#]?[_a-z0-9].*$",
57 true,
58 ),
59 (
60 "dts",
61 r"!;
62!=
63^[ ]*((/[ ]*\{|&?[a-zA-Z_]).*)",
64 false,
65 ),
66 (
67 "elixir",
68 r"^[ ]*((def(macro|module|impl|protocol|p)?|test)[ ].*)$",
69 false,
70 ),
71 (
72 "fortran",
73 r#"!^([C*]|[ ]*!)
74!^[ ]*MODULE[ ]+PROCEDURE[ ]
75^[ ]*((END[ ]+)?(PROGRAM|MODULE|BLOCK[ ]+DATA|([^!'" ]+[ ]+)*(SUBROUTINE|FUNCTION))[ ]+[A-Z].*)$"#,
76 true,
77 ),
78 (
79 "fountain",
80 r"^((\.[^.]|(int|ext|est|int\.?/ext|i/e)[. ]).*)$",
81 true,
82 ),
83 (
84 "golang",
85 r"^[ ]*(func[ ]*.*(\{[ ]*)?)
86^[ ]*(type[ ].*(struct|interface)[ ]*(\{[ ]*)?)",
87 false,
88 ),
89 ("html", r"^[ ]*(<[Hh][1-6]([ ].*)?>.*)$", false),
90 ("ini", r"^[ ]*\[[^]]+\]", false),
91 (
92 "java",
93 r"!^[ ]*(catch|do|for|if|instanceof|new|return|switch|throw|while)
94^[ ]*(([a-z-]+[ ]+)*(class|enum|interface|record)[ ]+.*)$
95^[ ]*(([A-Za-z_<>&][][?&<>.,A-Za-z_0-9]*[ ]+)+[A-Za-z_][A-Za-z_0-9]*[ ]*\([^;]*)$",
96 false,
97 ),
98 (
99 "kotlin",
100 r"^[ ]*(([a-z]+[ ]+)*(fun|class|interface)[ ]+.*)$",
101 false,
102 ),
103 ("markdown", r"^ {0,3}#{1,6}[ ].*", false),
104 (
105 "matlab",
106 r"^[[:space:]]*((classdef|function)[[:space:]].*)$|^(%%%?|##)[[:space:]].*$",
107 false,
108 ),
109 (
110 "objc",
111 r"!^[ ]*(do|for|if|else|return|switch|while)
112^[ ]*([-+][ ]*\([ ]*[A-Za-z_][A-Za-z_0-9* ]*\)[ ]*[A-Za-z_].*)$
113^[ ]*(([A-Za-z_][A-Za-z_0-9]*[ ]+)+[A-Za-z_][A-Za-z_0-9]*[ ]*\([^;]*)$
114^(@(implementation|interface|protocol)[ ].*)$",
115 false,
116 ),
117 (
118 "pascal",
119 r"^(((class[ ]+)?(procedure|function)|constructor|destructor|interface|implementation|initialization|finalization)[ ]*.*)$
120^(.*=[ ]*(class|record).*)$",
121 false,
122 ),
123 (
124 "perl",
125 r"^package .*
126^sub [[:alnum:]_':]+[ ]*(\([^)]*\)[ ]*)?(:[^;#]*)?(\{[ ]*)?(#.*)?$
127^(BEGIN|END|INIT|CHECK|UNITCHECK|AUTOLOAD|DESTROY)[ ]*(\{[ ]*)?(#.*)?$
128^=head[0-9] .*",
129 false,
130 ),
131 (
132 "php",
133 r"^[ ]*(((public|protected|private|static|abstract|final)[ ]+)*function.*)$
134^[ ]*((((final|abstract)[ ]+)?class|enum|interface|trait).*)$",
135 false,
136 ),
137 ("python", r"^[ ]*((class|(async[ ]+)?def)[ ].*)$", false),
138 (
139 "r",
140 r"^[ ]*([a-zA-z][a-zA-Z0-9_.]*[ ]*(<-|=)[ ]*function.*)$",
141 false,
142 ),
143 ("ruby", r"^[ ]*((class|module|def)[ ].*)$", false),
144 (
145 "rust",
146 r#"^[ ]*((pub(\([^\)]+\))?[ ]+)?((async|const|unsafe|extern([ ]+"[^"]+"))[ ]+)?(struct|enum|union|mod|trait|fn|impl|macro_rules!)[< ]+[^;]*)$"#,
147 false,
148 ),
149 (
150 "scheme",
151 r"^[ ]*(\(((define|def(struct|syntax|class|method|rules|record|proto|alias)?)[-*/ ]|(library|module|struct|class)[*+ ]).*)$",
152 false,
153 ),
154 (
155 "tex",
156 r"^(\\((sub)*section|chapter|part)\*{0,1}\{.*)$",
157 false,
158 ),
159];
160
161#[derive(Debug, Clone)]
162struct FuncRule {
163 matcher: RuleMatcher,
164 negate: bool,
165}
166
167#[derive(Debug, Clone)]
168enum RuleMatcher {
169 Rust(Regex),
170}
171
172#[derive(Debug, Clone)]
173struct BuiltinPattern {
174 pattern: String,
175 ignore_case: bool,
176}
177
178static BUILTIN_FUNCNAME_MATCHERS: OnceLock<BTreeMap<String, Arc<FuncnameMatcher>>> =
180 OnceLock::new();
181
182#[derive(Debug, Clone)]
184pub struct FuncnameMatcher {
185 rules: Vec<FuncRule>,
186}
187
188impl FuncnameMatcher {
189 #[must_use]
193 pub fn match_line(&self, line: &str) -> Option<String> {
194 let mut text = line;
195 if let Some(stripped) = text.strip_suffix('\n') {
196 text = stripped;
197 if let Some(stripped_cr) = text.strip_suffix('\r') {
198 text = stripped_cr;
199 }
200 }
201
202 for rule in &self.rules {
203 let matched_text = match &rule.matcher {
204 RuleMatcher::Rust(regex) => {
205 if let Some(caps) = regex.captures(text) {
206 caps.get(1)
207 .or_else(|| caps.get(0))
208 .map(|m| m.as_str())
209 .unwrap_or_default()
210 .trim_end_matches(char::is_whitespace)
211 .to_owned()
212 } else if regex.is_match(text) {
213 text.trim_end_matches(char::is_whitespace).to_owned()
214 } else {
215 continue;
216 }
217 }
218 };
219 if rule.negate {
220 return None;
221 }
222 return Some(matched_text.trim_end().to_owned());
223 }
224 None
225 }
226}
227
228pub fn matcher_for_path_parsed(
232 config: &ConfigSet,
233 rules: &[crate::attributes::AttrRule],
234 macros: &MacroTable,
235 rel_path: &str,
236 ignore_case: bool,
237) -> Result<Option<FuncnameMatcher>, String> {
238 let map = collect_attrs_for_path(rules, macros, rel_path, ignore_case);
239 let Some(AttrValue::Value(driver)) = map.get("diff") else {
240 return Ok(None);
241 };
242 matcher_for_driver(config, driver.as_str())
243}
244
245pub fn matcher_for_driver(
250 config: &ConfigSet,
251 driver: &str,
252) -> Result<Option<FuncnameMatcher>, String> {
253 if let Some(pattern) = config.get(&format!("diff.{driver}.xfuncname")) {
254 return compile_matcher(&pattern, true, false).map(Some);
255 }
256 if let Some(pattern) = config.get(&format!("diff.{driver}.funcname")) {
257 return compile_matcher(&pattern, false, false).map(Some);
258 }
259 if let Some(matcher) = cached_builtin_matcher(driver) {
260 return Ok(Some((*matcher).clone()));
261 }
262 Ok(None)
263}
264
265fn cached_builtin_matcher(driver: &str) -> Option<Arc<FuncnameMatcher>> {
266 let map = BUILTIN_FUNCNAME_MATCHERS.get_or_init(|| {
267 builtin_patterns()
268 .clone()
269 .into_iter()
270 .map(|(name, builtin)| {
271 let matcher = compile_matcher(&builtin.pattern, true, builtin.ignore_case)
272 .unwrap_or_else(|e| panic!("built-in userdiff driver {name}: {e}"));
273 (name, Arc::new(matcher))
274 })
275 .collect()
276 });
277 map.get(driver).cloned()
278}
279
280fn compile_matcher(
281 pattern: &str,
282 extended: bool,
283 ignore_case: bool,
284) -> Result<FuncnameMatcher, String> {
285 let lines: Vec<&str> = pattern.split('\n').collect();
286 if lines.is_empty() {
287 return Ok(FuncnameMatcher { rules: Vec::new() });
288 }
289
290 let mut rules = Vec::with_capacity(lines.len());
291 for (idx, raw) in lines.iter().enumerate() {
292 let mut line = *raw;
293 let negate = line.starts_with('!');
294 if negate {
295 if idx == lines.len() - 1 {
296 return Err(format!("Last expression must not be negated: {line}"));
297 }
298 line = &line[1..];
299 }
300
301 let posix_pattern = if extended {
302 line.to_owned()
303 } else {
304 bre_to_ere(line)
305 };
306
307 let matcher = compile_posix_ere_rule(&posix_pattern, ignore_case, line)?;
308 rules.push(FuncRule { matcher, negate });
309 }
310
311 Ok(FuncnameMatcher { rules })
312}
313
314fn builtin_patterns() -> &'static BTreeMap<String, BuiltinPattern> {
315 static BUILTIN_PATTERNS: OnceLock<BTreeMap<String, BuiltinPattern>> = OnceLock::new();
317 BUILTIN_PATTERNS.get_or_init(parse_builtin_patterns)
318}
319
320fn parse_builtin_patterns() -> BTreeMap<String, BuiltinPattern> {
321 BUILTIN_PATTERN_DEFS
322 .iter()
323 .filter(|(name, _, _)| !name.is_empty() && *name != "default")
324 .map(|(name, pattern, ignore_case)| {
325 (
326 (*name).to_owned(),
327 BuiltinPattern {
328 pattern: (*pattern).to_owned(),
329 ignore_case: *ignore_case,
330 },
331 )
332 })
333 .collect()
334}
335
336fn bre_to_ere(pattern: &str) -> String {
337 let mut result = String::with_capacity(pattern.len());
338 let chars: Vec<char> = pattern.chars().collect();
339 let mut i = 0usize;
340 let mut in_bracket = false;
341
342 while i < chars.len() {
343 if in_bracket {
344 if chars[i] == ']' && i > 0 {
345 result.push(']');
346 in_bracket = false;
347 i += 1;
348 } else if chars[i] == '[' {
349 result.push('[');
350 i += 1;
351 } else if chars[i] == '\\' {
352 if i + 1 < chars.len() {
356 let next = chars[i + 1];
357 if next.is_ascii_alphabetic() {
358 result.push('\\');
359 result.push('\\');
360 result.push(next);
361 i += 2;
362 } else {
363 result.push('\\');
364 result.push(next);
365 i += 2;
366 }
367 } else {
368 result.push('\\');
369 i += 1;
370 }
371 } else {
372 result.push(chars[i]);
373 i += 1;
374 }
375 } else if chars[i] == '[' {
376 result.push('[');
377 in_bracket = true;
378 i += 1;
379 if i < chars.len() && (chars[i] == '^' || chars[i] == '!') {
380 result.push(chars[i]);
381 i += 1;
382 }
383 if i < chars.len() && chars[i] == ']' {
384 result.push(']');
385 i += 1;
386 }
387 } else if chars[i] == '\\' && i + 1 < chars.len() {
388 match chars[i + 1] {
389 '+' | '?' | '{' | '}' | '(' | ')' | '|' => {
390 result.push(chars[i + 1]);
391 i += 2;
392 }
393 _ => {
394 result.push(chars[i]);
395 result.push(chars[i + 1]);
396 i += 2;
397 }
398 }
399 } else if matches!(chars[i], '+' | '?' | '{' | '}' | '(' | ')' | '|') {
400 result.push('\\');
401 result.push(chars[i]);
402 i += 1;
403 } else {
404 result.push(chars[i]);
405 i += 1;
406 }
407 }
408
409 result
410}
411
412fn posix_ere_to_rust_regex(pattern: &str) -> Option<String> {
416 let mut out = String::with_capacity(pattern.len() + 16);
417 let chars: Vec<char> = pattern.chars().collect();
418 let mut i = 0usize;
419 while i < chars.len() {
420 if chars[i] == '[' {
421 let (translated, consumed) = translate_posix_bracket(&chars[i..])?;
422 out.push_str(&translated);
423 i += consumed;
424 } else if chars[i] == '\\' && i + 1 < chars.len() {
425 out.push(chars[i]);
426 out.push(chars[i + 1]);
427 i += 2;
428 } else {
429 out.push(chars[i]);
430 i += 1;
431 }
432 }
433 Some(out)
434}
435
436fn translate_posix_bracket(src: &[char]) -> Option<(String, usize)> {
438 debug_assert!(src.first() == Some(&'['));
439 let mut out = String::from("[");
440 let mut i = 1usize;
441 if i < src.len() && (src[i] == '^' || src[i] == '!') {
442 out.push(src[i]);
443 i += 1;
444 }
445 if i < src.len() && src[i] == ']' {
446 out.push(']');
447 i += 1;
448 }
449 while i < src.len() {
450 if src[i] == ']' {
451 out.push(']');
452 return Some((out, i + 1));
453 }
454 if src[i] == '[' && i + 2 < src.len() && src[i + 1] == ':' {
455 let start = i;
456 i += 2;
457 while i + 1 < src.len() {
458 if src[i] == ':' && src[i + 1] == ']' {
459 i += 2;
460 break;
461 }
462 i += 1;
463 }
464 for ch in &src[start..i] {
465 out.push(*ch);
466 }
467 continue;
468 }
469 if src[i] == '\\' && i + 1 < src.len() {
470 out.push('\\');
471 out.push(src[i + 1]);
472 i += 2;
473 continue;
474 }
475 let ch = src[i];
476 if matches!(ch, '[' | ']' | '\\') {
477 out.push('\\');
478 }
479 out.push(ch);
480 i += 1;
481 }
482 None
483}
484
485fn validate_posix_ere(pattern: &str, ignore_case: bool) -> Result<(), regex::Error> {
487 let Some(rust_pattern) = posix_ere_to_rust_regex(pattern) else {
488 return Err(regex::Error::Syntax(
489 "unterminated bracket expression".into(),
490 ));
491 };
492 RegexBuilder::new(&rust_pattern)
493 .case_insensitive(ignore_case)
494 .build()
495 .map(|_| ())
496}
497
498fn compile_posix_ere_rule(
500 pattern: &str,
501 ignore_case: bool,
502 line: &str,
503) -> Result<RuleMatcher, String> {
504 validate_posix_ere(pattern, ignore_case)
505 .map_err(|_| format!("Invalid regexp to look for hunk header: {line}"))?;
506 let rust_pattern = posix_ere_to_rust_regex(pattern)
507 .ok_or_else(|| format!("Invalid regexp to look for hunk header: {line}"))?;
508 RegexBuilder::new(&rust_pattern)
509 .case_insensitive(ignore_case)
510 .build()
511 .map_err(|_| format!("Invalid regexp to look for hunk header: {line}"))
512 .map(RuleMatcher::Rust)
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use std::path::Path;
519
520 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
521
522 fn config_with_xfuncname(driver: &str, pattern: &str) -> ConfigSet {
523 let snippet = format!("[diff \"{driver}\"]\n\txfuncname = {pattern}\n");
524 let file = ConfigFile::parse(Path::new(".git/config"), &snippet, ConfigScope::Local)
525 .expect("parse config snippet");
526 let mut set = ConfigSet::new();
527 set.merge(&file);
528 set
529 }
530
531 #[test]
532 fn unterminated_posix_bracket_class_is_rejected() {
533 let err = compile_matcher("[a", true, false).unwrap_err();
534 assert!(err.contains("Invalid regexp"), "unexpected error: {err}");
535 let cfg = config_with_xfuncname("probe", "[a");
536 let err = matcher_for_driver(&cfg, "probe").unwrap_err();
537 assert!(err.contains("Invalid regexp"), "unexpected error: {err}");
538 }
539
540 #[test]
541 fn all_builtin_userdiff_drivers_compile_and_cache() {
542 let config = ConfigSet::default();
543 for (name, _, _) in BUILTIN_PATTERN_DEFS {
544 if name.is_empty() || *name == "default" {
545 continue;
546 }
547 let matcher = matcher_for_driver(&config, name)
548 .unwrap_or_else(|e| panic!("driver {name}: {e}"))
549 .unwrap_or_else(|| panic!("driver {name} missing builtin matcher"));
550 assert!(
551 cached_builtin_matcher(name).is_some(),
552 "driver {name} must be cached"
553 );
554 let _ = matcher.match_line(" \n");
555 }
556 }
557}