use crate::model::{Edge, EdgeType, Language, Node as ModelNode, NodeLabel};
use crate::resolve::FqnGenerator;
use super::dedupe_qn;
use super::error::{ParseError, Result};
use super::extractor::{ExtractResult, Extractor};
use super::parser_factory::ParserFactory;
const PATTERN_NAME: &str = "regex_pattern";
pub struct RegexExtractor {
_priv: (),
}
impl RegexExtractor {
#[must_use]
pub const fn new() -> Self {
Self { _priv: () }
}
}
impl Default for RegexExtractor {
fn default() -> Self {
Self::new()
}
}
impl Extractor for RegexExtractor {
fn language(&self) -> Language {
Language::Regex
}
fn extract(&self, source: &str, file_path: &str, project: &str) -> Result<ExtractResult> {
let mut result = ExtractResult::new(file_path, Language::Regex);
let mut parser = ParserFactory::create_parser(Language::Regex)?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ParseError::ParseFailed {
file_path: file_path.to_string(),
})?;
let root = tree.root_node();
if root.kind() == "pattern" {
let qn = dedupe_qn(
make_qn(file_path, PATTERN_NAME, project),
root.start_position().row as u32 + 1,
&result,
);
let model_node = ModelNode::builder(NodeLabel::Const, PATTERN_NAME, qn)
.file_path(file_path)
.start_line(root.start_position().row as u32 + 1)
.end_line(root.end_position().row as u32 + 1)
.language(Language::Regex)
.project(project)
.is_global(true)
.build();
add_definition_edges(file_path, project, &model_node, &mut result);
result.push_node(model_node);
}
Ok(result)
}
}
fn make_qn(file_path: &str, name: &str, project: &str) -> String {
FqnGenerator::generate(project, file_path, name, Language::Regex, None)
}
fn add_definition_edges(
file_path: &str,
project: &str,
node: &ModelNode,
result: &mut ExtractResult,
) {
result.edges.push(Edge::new(
file_path.to_string(),
node.id.clone(),
EdgeType::Defines,
project,
));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::NodeLabel;
fn extract(source: &str) -> ExtractResult {
let ext = RegexExtractor::new();
ext.extract(source, "test.regex", "proj")
.expect("extraction should succeed")
}
#[test]
fn language_returns_regex() {
assert_eq!(RegexExtractor::new().language(), Language::Regex);
}
#[test]
fn default_creates_extractor() {
let ext = RegexExtractor::default();
assert_eq!(ext.language(), Language::Regex);
}
#[test]
fn extracts_simple_pattern() {
let result = extract("abc\n");
let nodes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Const)
.collect();
assert_eq!(
nodes.len(),
1,
"should extract 1 pattern node: {:?}",
result.nodes
);
assert_eq!(nodes[0].name, "regex_pattern");
assert_eq!(nodes[0].language, Some(Language::Regex));
assert_eq!(nodes[0].project, "proj");
assert!(nodes[0].is_global);
}
#[test]
fn extracts_complex_pattern() {
let result = extract("^[a-zA-Z0-9]+@example\\.com$\n");
assert_eq!(
result.nodes.len(),
1,
"should extract exactly 1 node for any pattern"
);
assert_eq!(result.nodes[0].name, "regex_pattern");
}
#[test]
fn result_language_is_regex() {
let result = extract("abc\n");
assert_eq!(result.language, Language::Regex);
assert_eq!(result.file_path, "test.regex");
}
#[test]
fn creates_defines_edge() {
let result = extract("abc\n");
let defines_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Defines)
.count();
assert_eq!(defines_count, 1, "should create one DEFINES edge");
}
#[test]
fn qualified_name_uses_file_path_and_pattern_name() {
let result = extract("abc\n");
let node = &result.nodes[0];
assert_eq!(
node.qualified_name, "proj.test.regex.regex_pattern",
"FQN should be project.file_path.regex_pattern"
);
}
#[test]
fn pattern_has_correct_line_range() {
let result = extract("abc");
let node = &result.nodes[0];
assert_eq!(node.start_line, Some(1), "start line should be 1");
assert_eq!(node.end_line, Some(1), "end line should be 1");
}
#[test]
fn empty_source_produces_no_node_or_handles_gracefully() {
let result = extract("");
assert_eq!(result.language, Language::Regex);
for node in &result.nodes {
assert_eq!(node.name, "regex_pattern");
}
}
}