use std::path::Path;
use super::types::{
capture_name_to_chunk_type, Chunk, ChunkType, Language, ParserError, SignatureStyle,
};
use super::Parser;
impl Parser {
pub(crate) fn extract_chunk(
&self,
source: &str,
m: &tree_sitter::QueryMatch<'_, '_>,
query: &tree_sitter::Query,
language: Language,
path: &Path,
) -> Result<Chunk, ParserError> {
let (node, base_chunk_type) = ChunkType::CAPTURE_NAMES
.iter()
.find_map(|name| {
let chunk_type = capture_name_to_chunk_type(name)?;
query
.capture_index_for_name(name)
.and_then(|idx| m.captures.iter().find(|c| c.index == idx))
.map(|c| (c.node, chunk_type))
})
.ok_or_else(|| {
ParserError::ParseFailed("No definition capture found in match".into())
})?;
let name_idx = query.capture_index_for_name("name");
let name_capture = name_idx.and_then(|idx| m.captures.iter().find(|c| c.index == idx));
let mut name = name_capture
.map(|c| {
let raw = source[c.node.byte_range()].to_string();
raw.lines().next().unwrap_or(&raw).trim().to_string()
})
.unwrap_or_else(|| "<anonymous>".to_string());
let content = source[node.byte_range()].to_string();
if let Some(nc) = name_capture {
let name_line = nc.node.start_position().row;
let def_line = node.start_position().row;
if name_line.saturating_sub(def_line) > 5 {
if let Some(extracted) = extract_name_fallback(&content) {
name = extracted;
}
}
}
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let signature = extract_signature(&content, language);
let doc = extract_doc_comment(node, source, language);
let (chunk_type, parent_type_name) = if base_chunk_type == ChunkType::Function {
infer_chunk_type(node, language, source)
} else {
(base_chunk_type, None)
};
if let Some(ref ptn) = parent_type_name {
tracing::debug!(parent_type = %ptn, method = %name, "Extracted parent type for method");
}
let content_hash = blake3::hash(content.as_bytes()).to_hex().to_string();
let hash_prefix = content_hash.get(..8).unwrap_or(&content_hash);
let id = format!("{}:{}:{}", path.display(), line_start, hash_prefix);
Ok(Chunk {
id,
file: path.to_path_buf(),
language,
chunk_type,
name,
signature,
content,
doc,
line_start,
line_end,
content_hash,
parent_id: None,
window_idx: None,
parent_type_name,
})
}
}
pub(crate) fn extract_signature(content: &str, language: Language) -> String {
let sig_end = match language.def().signature_style {
SignatureStyle::UntilBrace => content.find('{').unwrap_or(content.len()),
SignatureStyle::UntilColon => content.find(':').unwrap_or(content.len()),
SignatureStyle::FirstLine => content.find('\n').unwrap_or(content.len()),
SignatureStyle::UntilAs => {
let bytes = content.as_bytes();
let is_boundary = |b: u8| b == b' ' || b == b'\n' || b == b'\t' || b == b'\r';
let mut pos = None;
for i in 0..bytes.len().saturating_sub(2) {
if bytes[i].eq_ignore_ascii_case(&b'A') && bytes[i + 1].eq_ignore_ascii_case(&b'S')
{
let left_ok = i == 0 || is_boundary(bytes[i - 1]);
let right_ok = i + 2 >= bytes.len() || is_boundary(bytes[i + 2]);
if left_ok && right_ok {
pos = Some(if i > 0 { i - 1 } else { 0 });
break;
}
}
}
pos.unwrap_or(content.len())
}
SignatureStyle::Breadcrumb => content.len(),
};
let sig = &content[..sig_end];
sig.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn extract_doc_comment(
node: tree_sitter::Node,
source: &str,
language: Language,
) -> Option<String> {
let doc_nodes = language.def().doc_nodes;
let mut comments = Vec::new();
let mut current = node.prev_sibling();
while let Some(sibling) = current {
let kind = sibling.kind();
if doc_nodes.contains(&kind) {
let text = &source[sibling.byte_range()];
comments.push(text.to_string());
current = sibling.prev_sibling();
} else if kind.contains("comment") {
current = sibling.prev_sibling();
} else {
break;
}
}
if comments.is_empty() {
if language == Language::Python {
if let Some(body) = node.child_by_field_name("body") {
if let Some(first) = body.named_child(0) {
if first.kind() == "expression_statement" {
if let Some(string) = first.named_child(0) {
if string.kind() == "string" {
return Some(source[string.byte_range()].to_string());
}
}
}
}
}
}
return None;
}
comments.reverse();
Some(comments.join("\n"))
}
fn extract_name_fallback(content: &str) -> Option<String> {
let bytes = content.as_bytes();
for keyword in &[b"PROCEDURE" as &[u8], b"FUNCTION", b"VIEW", b"TRIGGER"] {
if let Some(pos) = bytes.windows(keyword.len()).position(|w| {
w.iter()
.zip(keyword.iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}) {
let after_keyword = pos + keyword.len();
if after_keyword >= content.len() {
continue;
}
let rest = content[after_keyword..].trim_start();
let name_end = rest
.find(|c: char| c.is_whitespace() || c == '(' || c == '@')
.unwrap_or(rest.len());
let name = rest[..name_end].trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
None
}
fn infer_chunk_type(
node: tree_sitter::Node,
language: Language,
source: &str,
) -> (ChunkType, Option<String>) {
let def = language.def();
if def.method_node_kinds.contains(&node.kind()) {
let parent_type = extract_method_receiver_type(node, language, source);
return (ChunkType::Method, parent_type);
}
if let Some(extractor) = def.extract_qualified_method {
if let Some(class_name) = extractor(node, source) {
tracing::debug!(class = %class_name, "Qualified method inference");
return (ChunkType::Method, Some(class_name));
}
}
let mut current = node.parent();
while let Some(parent) = current {
if def.method_containers.contains(&parent.kind()) {
let parent_type = extract_container_type_name(parent, language, source);
return (ChunkType::Method, parent_type);
}
current = parent.parent();
}
(ChunkType::Function, None)
}
fn extract_container_type_name(
container: tree_sitter::Node,
language: Language,
source: &str,
) -> Option<String> {
let def = language.def();
if let Some(extractor) = def.extract_container_name {
return extractor(container, source);
}
let type_node = if def.container_body_kinds.contains(&container.kind()) {
container.parent()
} else {
Some(container)
};
type_node.and_then(|n| {
n.child_by_field_name("name")
.map(|name| source[name.byte_range()].to_string())
})
}
fn extract_method_receiver_type(
node: tree_sitter::Node,
language: Language,
source: &str,
) -> Option<String> {
if language != Language::Go {
return None;
}
let receiver = node.child_by_field_name("receiver")?;
let first_param = receiver.named_child(0)?;
find_type_identifier_recursive(first_param, source)
}
fn find_type_identifier_recursive(node: tree_sitter::Node, source: &str) -> Option<String> {
if node.kind() == "type_identifier" {
return Some(source[node.byte_range()].to_string());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(name) = find_type_identifier_recursive(child, source) {
return Some(name);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
mod signature_tests {
use super::*;
#[test]
fn test_rust_signature_stops_at_brace() {
let content = "fn process(x: i32) -> Result<(), Error> {\n body\n}";
let sig = extract_signature(content, Language::Rust);
assert_eq!(sig, "fn process(x: i32) -> Result<(), Error>");
}
#[test]
fn test_rust_signature_normalizes_whitespace() {
let content = "fn process( x: i32 ) -> i32 {";
let sig = extract_signature(content, Language::Rust);
assert_eq!(sig, "fn process( x: i32 ) -> i32");
}
#[test]
fn test_python_signature_stops_at_colon() {
let content = "def calculate(x, y):\n return x + y";
let sig = extract_signature(content, Language::Python);
assert_eq!(sig, "def calculate(x, y)");
}
#[test]
fn test_go_signature_stops_at_brace() {
let content = "func (s *Server) Handle(r Request) error {\n\treturn nil\n}";
let sig = extract_signature(content, Language::Go);
assert_eq!(sig, "func (s *Server) Handle(r Request) error");
}
#[test]
fn test_typescript_signature_stops_at_brace() {
let content = "function processData(input: string): Promise<Result> {\n return ok;\n}";
let sig = extract_signature(content, Language::TypeScript);
assert_eq!(sig, "function processData(input: string): Promise<Result>");
}
#[test]
fn test_ruby_signature_stops_at_newline() {
let content = "def calculate(x, y)\n x + y\nend";
let sig = extract_signature(content, Language::Ruby);
assert_eq!(sig, "def calculate(x, y)");
}
#[test]
fn test_signature_without_terminator() {
let content = "fn abstract_decl()";
let sig = extract_signature(content, Language::Rust);
assert_eq!(sig, "fn abstract_decl()");
}
#[test]
fn extract_signature_until_as_preserves_unicode() {
let content = "CREATE VIEW stra\u{00DF}e AS SELECT 1";
let sig = extract_signature(content, Language::Sql);
assert!(sig.contains("stra\u{00DF}e"));
assert!(!sig.contains("AS"));
}
}
fn write_temp_file(content: &str, ext: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(&format!(".{}", ext))
.tempfile()
.unwrap();
file.write_all(content.as_bytes()).unwrap();
file.flush().unwrap();
file
}
mod parse_tests {
use super::*;
#[test]
fn test_parse_rust_function() {
let content = r#"
/// Adds two numbers
fn add(a: i32, b: i32) -> i32 {
a + b
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].name, "add");
assert_eq!(chunks[0].chunk_type, ChunkType::Function);
assert!(chunks[0].doc.as_ref().unwrap().contains("Adds two numbers"));
}
#[test]
fn test_parse_rust_method_in_impl() {
let content = r#"
struct Counter { value: i32 }
impl Counter {
fn increment(&mut self) {
self.value += 1;
}
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "increment").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
}
#[test]
fn test_parse_python_class_method() {
let content = r#"
class Calculator:
"""A simple calculator."""
def add(self, a, b):
"""Add two numbers."""
return a + b
"#;
let file = write_temp_file(content, "py");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let class = chunks.iter().find(|c| c.name == "Calculator").unwrap();
assert_eq!(class.chunk_type, ChunkType::Class);
let method = chunks.iter().find(|c| c.name == "add").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
}
#[test]
fn test_parse_go_method_vs_function() {
let content = r#"
package main
func standalone() {
println("standalone")
}
func (s *Server) method() {
println("method")
}
"#;
let file = write_temp_file(content, "go");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let standalone = chunks.iter().find(|c| c.name == "standalone").unwrap();
assert_eq!(standalone.chunk_type, ChunkType::Function);
let method = chunks.iter().find(|c| c.name == "method").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
}
#[test]
fn test_parse_typescript_interface() {
let content = r#"
interface User {
name: string;
age: number;
}
"#;
let file = write_temp_file(content, "ts");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].name, "User");
assert_eq!(chunks[0].chunk_type, ChunkType::Interface);
}
#[test]
fn test_parse_c_function() {
let content = r#"
/* Adds two integers */
int add(int a, int b) {
return a + b;
}
"#;
let file = write_temp_file(content, "c");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].name, "add");
assert_eq!(chunks[0].chunk_type, ChunkType::Function);
assert!(chunks[0]
.doc
.as_ref()
.unwrap()
.contains("Adds two integers"));
}
#[test]
fn test_parse_c_struct_and_enum() {
let content = r#"
struct Point {
int x;
int y;
};
enum Color {
RED,
GREEN,
BLUE
};
"#;
let file = write_temp_file(content, "c");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let point = chunks.iter().find(|c| c.name == "Point").unwrap();
assert_eq!(point.chunk_type, ChunkType::Struct);
let color = chunks.iter().find(|c| c.name == "Color").unwrap();
assert_eq!(color.chunk_type, ChunkType::Enum);
}
#[test]
fn test_parse_java_class_with_method() {
let content = r#"
public class Calculator {
/**
* Adds two numbers
*/
public int add(int a, int b) {
return a + b;
}
}
"#;
let file = write_temp_file(content, "java");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let class = chunks.iter().find(|c| c.name == "Calculator").unwrap();
assert_eq!(class.chunk_type, ChunkType::Class);
let method = chunks.iter().find(|c| c.name == "add").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert!(method.doc.as_ref().unwrap().contains("Adds two numbers"));
}
#[test]
fn test_parse_java_interface_and_enum() {
let content = r#"
interface Printable {
void print();
}
enum Direction {
NORTH,
SOUTH,
EAST,
WEST
}
"#;
let file = write_temp_file(content, "java");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let iface = chunks.iter().find(|c| c.name == "Printable").unwrap();
assert_eq!(iface.chunk_type, ChunkType::Interface);
let dir = chunks.iter().find(|c| c.name == "Direction").unwrap();
assert_eq!(dir.chunk_type, ChunkType::Enum);
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_class_and_method() {
let content = r#"
public class Calculator {
public int Add(int a, int b) {
return a + b;
}
}
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let class = chunks.iter().find(|c| c.name == "Calculator").unwrap();
assert_eq!(class.chunk_type, ChunkType::Class);
let method = chunks.iter().find(|c| c.name == "Add").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_property() {
let content = r#"
public class Foo {
public int Value { get; set; }
}
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks
.iter()
.any(|c| c.name == "Value" && c.chunk_type == ChunkType::Property));
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_delegate() {
let content = "public delegate void OnComplete(int result);";
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks
.iter()
.any(|c| c.name == "OnComplete" && c.chunk_type == ChunkType::Delegate));
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_event() {
let content = r#"
public class Foo {
public event EventHandler Changed;
}
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks
.iter()
.any(|c| c.name == "Changed" && c.chunk_type == ChunkType::Event));
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_interface_and_enum() {
let content = r#"
public interface ICalculator {
int Add(int a, int b);
}
public enum Color { Red, Green, Blue }
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks
.iter()
.any(|c| c.name == "ICalculator" && c.chunk_type == ChunkType::Interface));
assert!(chunks
.iter()
.any(|c| c.name == "Color" && c.chunk_type == ChunkType::Enum));
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_record_maps_to_struct() {
let content = "public record Person(string Name, int Age);";
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks
.iter()
.any(|c| c.name == "Person" && c.chunk_type == ChunkType::Struct));
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_constructor_inferred_constructor() {
let content = r#"
public class Foo {
public Foo(int x) { }
}
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let ctors: Vec<_> = chunks
.iter()
.filter(|c| c.name == "Foo" && c.chunk_type == ChunkType::Constructor)
.collect();
assert!(
!ctors.is_empty(),
"Constructor should be classified as Constructor"
);
}
#[test]
#[cfg(feature = "lang-csharp")]
fn test_parse_csharp_local_function() {
let content = r#"
public class Foo {
public void Bar() {
int Helper(int x) { return x + 1; }
Helper(5);
}
}
"#;
let file = write_temp_file(content, "cs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert!(chunks.iter().any(|c| c.name == "Helper"));
}
#[test]
fn test_parse_rust_macro() {
let content = r#"
macro_rules! my_macro {
($x:expr) => {
println!("{}", $x);
};
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].name, "my_macro");
assert_eq!(chunks[0].chunk_type, ChunkType::Macro);
assert!(chunks[0].signature.contains("macro_rules! my_macro"));
}
}
mod parent_type_tests {
use super::*;
#[test]
fn test_rust_method_has_parent_type_name() {
let content = r#"
struct Counter { value: i32 }
impl Counter {
fn increment(&mut self) { self.value += 1; }
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "increment").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Counter"));
}
#[test]
fn test_rust_trait_method_has_parent_type_name() {
let content = r#"
trait Drawable {
fn draw(&self) {}
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "draw").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Drawable"));
}
#[test]
fn test_rust_impl_trait_for_type() {
let content = r#"
struct Foo;
trait Display { fn fmt(&self) -> String; }
impl Display for Foo {
fn fmt(&self) -> String { String::new() }
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks
.iter()
.find(|c| c.name == "fmt" && c.chunk_type == ChunkType::Method)
.unwrap();
assert_eq!(method.parent_type_name.as_deref(), Some("Foo"));
}
#[test]
fn test_rust_generic_impl() {
let content = r#"
struct Container<T> { items: Vec<T> }
impl<T> Container<T> {
fn push(&mut self, item: T) {}
}
"#;
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "push").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Container"));
}
#[test]
fn test_python_method_has_parent_type_name() {
let content = r#"
class Calculator:
def add(self, a, b):
return a + b
"#;
let file = write_temp_file(content, "py");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "add").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Calculator"));
}
#[test]
fn test_go_method_pointer_receiver() {
let content = r#"
package main
type Server struct{}
func (s *Server) Handle() {}
"#;
let file = write_temp_file(content, "go");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "Handle").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Server"));
}
#[test]
fn test_go_method_value_receiver() {
let content = r#"
package main
type Point struct{ x, y int }
func (p Point) Distance() float64 { return 0.0 }
"#;
let file = write_temp_file(content, "go");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "Distance").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Point"));
}
#[test]
fn test_js_method_has_parent_type_name() {
let content = r#"
class Cache {
get(key) { return this.data[key]; }
}
"#;
let file = write_temp_file(content, "js");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "get").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Cache"));
}
#[test]
fn test_ts_method_has_parent_type_name() {
let content = r#"
class TypedCache {
get(key: string): string { return ""; }
}
"#;
let file = write_temp_file(content, "ts");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "get").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("TypedCache"));
}
#[test]
fn test_java_method_has_parent_type_name() {
let content = r#"
public class Calculator {
public int add(int a, int b) { return a + b; }
}
"#;
let file = write_temp_file(content, "java");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
let method = chunks.iter().find(|c| c.name == "add").unwrap();
assert_eq!(method.chunk_type, ChunkType::Method);
assert_eq!(method.parent_type_name.as_deref(), Some("Calculator"));
}
#[test]
fn test_standalone_function_no_parent() {
let content = "fn standalone() {}";
let file = write_temp_file(content, "rs");
let parser = Parser::new().unwrap();
let chunks = parser.parse_file(file.path()).unwrap();
assert_eq!(chunks[0].chunk_type, ChunkType::Function);
assert!(chunks[0].parent_type_name.is_none());
}
}
#[test]
fn extract_name_fallback_with_unicode_before_keyword() {
let content = "CREATE FUNCTION caf\u{00e9}_func() RETURNS void";
let name = extract_name_fallback(content);
assert_eq!(name, Some("caf\u{00e9}_func".to_string()));
}
}