- RoutingDecision の missing_caps を Vec<CapabilityTag> に変更(型安全化) - error_tags は to_tag() メソッドで自動生成 - 全 callsite を enum variant に修正 - capability_tags モジュール(文字列定数群)を完全削除 - 全テスト PASS(型安全性向上を確認) - フォーマット適用
459 lines
14 KiB
Rust
459 lines
14 KiB
Rust
//! TrimDetector - Pure detection logic for trim pattern
|
|
//!
|
|
//! Extracted from LoopBodyCarrierPromoter to enable:
|
|
//! - Single responsibility (detection only)
|
|
//! - Independent unit testing
|
|
//! - Reusable pattern for future analyzers
|
|
//!
|
|
//! # Design Philosophy
|
|
//!
|
|
//! This detector follows the **Detector/Recorder separation** principle:
|
|
//! - **Detector**: Pure detection logic (this module)
|
|
//! - **Recorder**: PromotedBindingRecorder (records BindingId mappings)
|
|
//! - **Promoter**: Orchestrates detection + recording + carrier building
|
|
//!
|
|
//! # Pattern: A-3 Trim (Substring + Equality OR Chain)
|
|
//!
|
|
//! ```nyash
|
|
//! loop(start < end) {
|
|
//! local ch = s.substring(start, start+1)
|
|
//! if ch == " " || ch == "\t" || ch == "\n" {
|
|
//! start = start + 1
|
|
//! } else {
|
|
//! break
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
|
|
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
|
|
|
|
/// Detection result for trim pattern.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct TrimDetectionResult {
|
|
/// Variable name (e.g., "ch")
|
|
pub match_var: String,
|
|
|
|
/// Carrier name (e.g., "is_ch_match")
|
|
pub carrier_name: String,
|
|
|
|
/// Comparison literals (e.g., [" ", "\t", "\n", "\r"])
|
|
pub comparison_literals: Vec<String>,
|
|
}
|
|
|
|
/// Pure detection logic for A-3 Trim pattern
|
|
pub struct TrimDetector;
|
|
|
|
impl TrimDetector {
|
|
/// Detect trim pattern in condition and body.
|
|
///
|
|
/// Returns None if pattern not found, Some(result) if detected.
|
|
///
|
|
/// # Algorithm
|
|
///
|
|
/// 1. Extract equality literals from condition (e.g., [" ", "\t"])
|
|
/// 2. Find substring() definition in loop body
|
|
/// 3. Generate carrier name (e.g., "is_ch_match")
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `condition` - Break or continue condition AST node
|
|
/// * `body` - Loop body statements
|
|
/// * `var_name` - Variable name to check (from LoopBodyLocal analysis)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Some(TrimDetectionResult)` if pattern detected
|
|
/// * `None` if pattern not found
|
|
pub fn detect(
|
|
condition: &ASTNode,
|
|
body: &[ASTNode],
|
|
var_name: &str,
|
|
) -> Option<TrimDetectionResult> {
|
|
// Step 1: Find substring() definition for the variable
|
|
let definition = Self::find_definition_in_body(body, var_name)?;
|
|
|
|
// Step 2: Verify it's a substring() method call
|
|
if !Self::is_substring_method_call(definition) {
|
|
return None;
|
|
}
|
|
|
|
// Step 3: Extract equality literals from condition
|
|
let literals = Self::extract_equality_literals(condition, var_name);
|
|
|
|
if literals.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Step 4: Generate carrier name
|
|
let carrier_name = format!("is_{}_match", var_name);
|
|
|
|
Some(TrimDetectionResult {
|
|
match_var: var_name.to_string(),
|
|
carrier_name,
|
|
comparison_literals: literals,
|
|
})
|
|
}
|
|
|
|
/// Find definition in loop body
|
|
///
|
|
/// Searches for assignment: `local var = ...` or `var = ...`
|
|
fn find_definition_in_body<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> {
|
|
let mut worklist: Vec<&'a ASTNode> = body.iter().collect();
|
|
|
|
while let Some(node) = worklist.pop() {
|
|
match node {
|
|
// Assignment: target = value
|
|
ASTNode::Assignment { target, value, .. } => {
|
|
if let ASTNode::Variable { name, .. } = target.as_ref() {
|
|
if name == var_name {
|
|
return Some(value.as_ref());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Local: local var = value
|
|
ASTNode::Local {
|
|
variables,
|
|
initial_values,
|
|
..
|
|
} if initial_values.len() == variables.len() => {
|
|
for (i, var) in variables.iter().enumerate() {
|
|
if var == var_name {
|
|
if let Some(Some(init_expr)) = initial_values.get(i) {
|
|
return Some(init_expr.as_ref());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nested structures
|
|
ASTNode::If {
|
|
then_body,
|
|
else_body,
|
|
..
|
|
} => {
|
|
for stmt in then_body {
|
|
worklist.push(stmt);
|
|
}
|
|
if let Some(else_stmts) = else_body {
|
|
for stmt in else_stmts {
|
|
worklist.push(stmt);
|
|
}
|
|
}
|
|
}
|
|
|
|
ASTNode::Loop {
|
|
body: loop_body, ..
|
|
} => {
|
|
for stmt in loop_body {
|
|
worklist.push(stmt);
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Check if node is a substring() method call
|
|
fn is_substring_method_call(node: &ASTNode) -> bool {
|
|
matches!(
|
|
node,
|
|
ASTNode::MethodCall { method, .. } if method == "substring"
|
|
)
|
|
}
|
|
|
|
/// Extract equality literals from condition
|
|
///
|
|
/// Handles: `ch == " " || ch == "\t" || ch == "\n"`
|
|
///
|
|
/// Returns: `[" ", "\t", "\n"]`
|
|
fn extract_equality_literals(cond: &ASTNode, var_name: &str) -> Vec<String> {
|
|
let mut result = Vec::new();
|
|
let mut worklist = vec![cond];
|
|
|
|
while let Some(node) = worklist.pop() {
|
|
match node {
|
|
// BinaryOp: Or splits, Equal compares
|
|
ASTNode::BinaryOp {
|
|
operator,
|
|
left,
|
|
right,
|
|
..
|
|
} => match operator {
|
|
BinaryOperator::Or => {
|
|
// OR chain: traverse both sides
|
|
worklist.push(left.as_ref());
|
|
worklist.push(right.as_ref());
|
|
}
|
|
BinaryOperator::Equal => {
|
|
// Equality comparison: extract literal
|
|
if let Some(literal) = Self::extract_literal_from_equality(
|
|
left.as_ref(),
|
|
right.as_ref(),
|
|
var_name,
|
|
) {
|
|
result.push(literal);
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
|
|
// UnaryOp: not (...)
|
|
ASTNode::UnaryOp { operand, .. } => {
|
|
worklist.push(operand.as_ref());
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Extract string literal from equality comparison
|
|
///
|
|
/// Handles: `ch == " "` or `" " == ch`
|
|
fn extract_literal_from_equality(
|
|
left: &ASTNode,
|
|
right: &ASTNode,
|
|
var_name: &str,
|
|
) -> Option<String> {
|
|
// Pattern 1: var == literal
|
|
if let ASTNode::Variable { name, .. } = left {
|
|
if name == var_name {
|
|
if let ASTNode::Literal {
|
|
value: LiteralValue::String(s),
|
|
..
|
|
} = right
|
|
{
|
|
return Some(s.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pattern 2: literal == var
|
|
if let ASTNode::Literal {
|
|
value: LiteralValue::String(s),
|
|
..
|
|
} = left
|
|
{
|
|
if let ASTNode::Variable { name, .. } = right {
|
|
if name == var_name {
|
|
return Some(s.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::ast::Span;
|
|
|
|
fn var_node(name: &str) -> ASTNode {
|
|
ASTNode::Variable {
|
|
name: name.to_string(),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn string_literal(value: &str) -> ASTNode {
|
|
ASTNode::Literal {
|
|
value: LiteralValue::String(value.to_string()),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn method_call(object: &str, method: &str, args: Vec<ASTNode>) -> ASTNode {
|
|
ASTNode::MethodCall {
|
|
object: Box::new(var_node(object)),
|
|
method: method.to_string(),
|
|
arguments: args,
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn assignment(target: &str, value: ASTNode) -> ASTNode {
|
|
ASTNode::Assignment {
|
|
target: Box::new(var_node(target)),
|
|
value: Box::new(value),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn equality(var: &str, literal: &str) -> ASTNode {
|
|
ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(var_node(var)),
|
|
right: Box::new(string_literal(literal)),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn or_op(left: ASTNode, right: ASTNode) -> ASTNode {
|
|
ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Or,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_basic_trim_pattern() {
|
|
// local ch = s.substring(...)
|
|
// if ch == " " || ch == "\t" { ... }
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = or_op(equality("ch", " "), equality("ch", "\t"));
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_some());
|
|
let result = result.unwrap();
|
|
assert_eq!(result.match_var, "ch");
|
|
assert_eq!(result.carrier_name, "is_ch_match");
|
|
assert_eq!(result.comparison_literals.len(), 2);
|
|
assert!(result.comparison_literals.contains(&" ".to_string()));
|
|
assert!(result.comparison_literals.contains(&"\t".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_no_match_not_substring() {
|
|
// ch = s.length() // Not substring!
|
|
// if ch == " " { ... }
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "length", vec![]))];
|
|
|
|
let condition = equality("ch", " ");
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_no_match_no_equality() {
|
|
// local ch = s.substring(...)
|
|
// if ch < "x" { ... } // Not equality!
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(var_node("ch")),
|
|
right: Box::new(string_literal("x")),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_single_equality() {
|
|
// local ch = s.substring(...)
|
|
// if ch == " " { ... }
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = equality("ch", " ");
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_some());
|
|
let result = result.unwrap();
|
|
assert_eq!(result.comparison_literals.len(), 1);
|
|
assert_eq!(result.comparison_literals[0], " ");
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_multiple_whitespace() {
|
|
// local ch = s.substring(...)
|
|
// if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { ... }
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = or_op(
|
|
or_op(equality("ch", " "), equality("ch", "\t")),
|
|
or_op(equality("ch", "\n"), equality("ch", "\r")),
|
|
);
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_some());
|
|
let result = result.unwrap();
|
|
assert_eq!(result.comparison_literals.len(), 4);
|
|
assert!(result.comparison_literals.contains(&" ".to_string()));
|
|
assert!(result.comparison_literals.contains(&"\t".to_string()));
|
|
assert!(result.comparison_literals.contains(&"\n".to_string()));
|
|
assert!(result.comparison_literals.contains(&"\r".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_carrier_name_generation() {
|
|
// Test carrier name generation: "ch" → "is_ch_match"
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = equality("ch", " ");
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch").unwrap();
|
|
|
|
assert_eq!(result.carrier_name, "is_ch_match");
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_different_variable_names() {
|
|
// Test with different variable names
|
|
|
|
let test_cases = vec!["ch", "c", "char", "character"];
|
|
|
|
for var_name in test_cases {
|
|
let loop_body = vec![assignment(var_name, method_call("s", "substring", vec![]))];
|
|
|
|
let condition = equality(var_name, " ");
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, var_name);
|
|
|
|
assert!(
|
|
result.is_some(),
|
|
"Expected detection for var '{}'",
|
|
var_name
|
|
);
|
|
let result = result.unwrap();
|
|
assert_eq!(result.match_var, var_name);
|
|
assert_eq!(result.carrier_name, format!("is_{}_match", var_name));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_literal_reversed_equality() {
|
|
// Test: " " == ch (literal on left side)
|
|
|
|
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
|
|
|
|
let condition = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(string_literal(" ")),
|
|
right: Box::new(var_node("ch")),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = TrimDetector::detect(&condition, &loop_body, "ch");
|
|
|
|
assert!(result.is_some());
|
|
let result = result.unwrap();
|
|
assert_eq!(result.comparison_literals.len(), 1);
|
|
assert_eq!(result.comparison_literals[0], " ");
|
|
}
|
|
}
|