use regex::Regex;
use std::sync::LazyLock;
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)#.*$").unwrap());
static COMMENT_MULTI: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?ms)^=begin.*?^=end").unwrap());
static RUBY_CLASS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]*class\s+([A-Z]\w*)").unwrap());
static RUBY_MODULE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]*module\s+([A-Z]\w*)").unwrap());
static RUBY_DEF: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^def\s+(?:self\.)?(\w+)").unwrap());
static RUBY_METHOD: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]+def\s+(?:self\.)?(\w+)").unwrap());
static RUBY_CONST: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]*([A-Z][A-Z0-9_]+)\s*=").unwrap());
static RUBY_ATTR: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]*attr_(?:accessor|reader|writer)\s+(.+)$").unwrap());
static RUBY_SYMBOL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":(\w+)").unwrap());
static VISIBILITY_PRIVATE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^\s*(?:private|protected)\s*$").unwrap());
static VISIBILITY_PUBLIC: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^\s*public\s*$").unwrap());
pub fn extract_exports(content: &str) -> Vec<String> {
let stripped = COMMENT_MULTI.replace_all(content, "");
let stripped = COMMENT_SINGLE.replace_all(&stripped, "");
let mut symbols = Vec::new();
for caps in RUBY_CLASS.captures_iter(&stripped) {
if let Some(name) = caps.get(1) {
symbols.push(name.as_str().to_string());
}
}
for caps in RUBY_MODULE.captures_iter(&stripped) {
if let Some(name) = caps.get(1) {
let n = name.as_str().to_string();
if !symbols.contains(&n) {
symbols.push(n);
}
}
}
for caps in RUBY_DEF.captures_iter(&stripped) {
if let Some(name) = caps.get(1) {
let n = name.as_str();
if !n.starts_with('_') && !symbols.contains(&n.to_string()) {
symbols.push(n.to_string());
}
}
}
let mut public = true;
for line in stripped.lines() {
if VISIBILITY_PRIVATE.is_match(line) {
public = false;
continue;
}
if VISIBILITY_PUBLIC.is_match(line) {
public = true;
continue;
}
if public
&& let Some(caps) = RUBY_METHOD.captures(line)
&& let Some(name) = caps.get(1)
{
let n = name.as_str();
if !n.starts_with('_') && n != "initialize" && !symbols.contains(&n.to_string()) {
symbols.push(n.to_string());
}
}
}
for caps in RUBY_CONST.captures_iter(&stripped) {
if let Some(name) = caps.get(1) {
let n = name.as_str().to_string();
if !symbols.contains(&n) {
symbols.push(n);
}
}
}
for caps in RUBY_ATTR.captures_iter(&stripped) {
if let Some(attrs) = caps.get(1) {
for sym in RUBY_SYMBOL.captures_iter(attrs.as_str()) {
if let Some(name) = sym.get(1) {
let n = name.as_str().to_string();
if !symbols.contains(&n) {
symbols.push(n);
}
}
}
}
}
symbols
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ruby_class_and_methods() {
let src = r#"
module Authentication
class AuthService
DEFAULT_TTL = 3600
attr_reader :token, :expires_at
def validate(token)
# ...
end
def self.create(config)
# ...
end
private
def internal_check
# ...
end
end
end
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"Authentication".to_string()));
assert!(symbols.contains(&"AuthService".to_string()));
assert!(symbols.contains(&"validate".to_string()));
assert!(symbols.contains(&"create".to_string()));
assert!(symbols.contains(&"DEFAULT_TTL".to_string()));
assert!(symbols.contains(&"token".to_string()));
assert!(symbols.contains(&"expires_at".to_string()));
assert!(!symbols.contains(&"internal_check".to_string()));
}
#[test]
fn test_ruby_top_level_functions() {
let src = r#"
def process_data(input)
# ...
end
def _private_helper
# ...
end
class DataProcessor
end
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"process_data".to_string()));
assert!(symbols.contains(&"DataProcessor".to_string()));
assert!(!symbols.contains(&"_private_helper".to_string()));
}
#[test]
fn test_ruby_visibility_toggle() {
let src = r#"
class Foo
def public_one
end
def public_two
end
private
def secret_one
end
public
def public_again
end
protected
def also_hidden
end
end
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"Foo".to_string()));
assert!(symbols.contains(&"public_one".to_string()));
assert!(symbols.contains(&"public_two".to_string()));
assert!(symbols.contains(&"public_again".to_string()));
assert!(!symbols.contains(&"secret_one".to_string()));
assert!(!symbols.contains(&"also_hidden".to_string()));
}
#[test]
fn test_ruby_skips_initialize() {
let src = r#"
class Bar
def initialize(name)
@name = name
end
def name
@name
end
end
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"Bar".to_string()));
assert!(symbols.contains(&"name".to_string()));
assert!(!symbols.contains(&"initialize".to_string()));
}
}