use super::class_edit_texts;
use super::*;
use crate::types::ClassLikeKind;
#[test]
fn test_detect_class_in_single_class_file() {
let source = "<?php\nclass DateTime {\n}\n";
let result = detect_stub_class_kind("DateTime", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, false, false)),
"should detect a plain class"
);
}
#[test]
fn test_detect_interface_in_single_file() {
let source = "<?php\ninterface JsonSerializable\n{\n}\n";
let result = detect_stub_class_kind("JsonSerializable", source);
assert_eq!(
result,
Some((ClassLikeKind::Interface, false, false)),
"should detect an interface"
);
}
#[test]
fn test_detect_abstract_class() {
let source = "<?php\nabstract class SplHeap implements Iterator, Countable\n{\n}\n";
let result = detect_stub_class_kind("SplHeap", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, true, false)),
"should detect an abstract class"
);
}
#[test]
fn test_detect_final_class() {
let source = "<?php\nfinal class Closure {\n}\n";
let result = detect_stub_class_kind("Closure", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, false, true)),
"should detect a final class"
);
}
#[test]
fn test_detect_readonly_class() {
let source = "<?php\nreadonly class Value {\n}\n";
let result = detect_stub_class_kind("Value", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, false, false)),
"readonly class is neither abstract nor final"
);
}
#[test]
fn test_detect_final_readonly_class() {
let source = "<?php\nfinal readonly class Immutable {\n}\n";
let result = detect_stub_class_kind("Immutable", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, false, true)),
"should detect final through readonly"
);
}
#[test]
fn test_detect_abstract_readonly_class() {
let source = "<?php\nabstract readonly class Base {\n}\n";
let result = detect_stub_class_kind("Base", source);
assert_eq!(
result,
Some((ClassLikeKind::Class, true, false)),
"should detect abstract through readonly"
);
}
#[test]
fn test_detect_trait() {
let source = "<?php\ntrait Stringable {\n}\n";
let result = detect_stub_class_kind("Stringable", source);
assert_eq!(
result,
Some((ClassLikeKind::Trait, false, false)),
"should detect a trait"
);
}
#[test]
fn test_detect_enum() {
let source = "<?php\nenum Suit {\n}\n";
let result = detect_stub_class_kind("Suit", source);
assert_eq!(
result,
Some((ClassLikeKind::Enum, false, false)),
"should detect an enum"
);
}
#[test]
fn test_detect_class_in_multi_class_file() {
let source = concat!(
"<?php\n",
"class SplFileInfo implements Stringable\n{\n}\n",
"class DirectoryIterator extends SplFileInfo implements SeekableIterator\n{\n}\n",
"class FilesystemIterator extends DirectoryIterator\n{\n}\n",
"abstract class SplHeap implements Iterator, Countable\n{\n}\n",
"interface SplObserver\n{\n}\n",
"interface SplSubject\n{\n}\n",
"class SplObjectStorage implements Countable\n{\n}\n",
);
assert_eq!(
detect_stub_class_kind("DirectoryIterator", source),
Some((ClassLikeKind::Class, false, false)),
"should find DirectoryIterator as a class in a multi-class file"
);
assert_eq!(
detect_stub_class_kind("SplHeap", source),
Some((ClassLikeKind::Class, true, false)),
"should find SplHeap as an abstract class"
);
assert_eq!(
detect_stub_class_kind("SplObserver", source),
Some((ClassLikeKind::Interface, false, false)),
"should find SplObserver as an interface"
);
assert_eq!(
detect_stub_class_kind("SplObjectStorage", source),
Some((ClassLikeKind::Class, false, false)),
"should find SplObjectStorage as a class"
);
}
#[test]
fn test_detect_does_not_match_substring() {
let source = concat!(
"<?php\n",
"interface Iterator\n{\n}\n",
"class DirectoryIterator extends SplFileInfo\n{\n}\n",
);
assert_eq!(
detect_stub_class_kind("Iterator", source),
Some((ClassLikeKind::Interface, false, false)),
"should match the standalone 'Iterator' interface, not the substring in DirectoryIterator"
);
}
#[test]
fn test_detect_does_not_match_superstring() {
let source = "<?php\nclass DirectoryIterator extends SplFileInfo\n{\n}\n";
assert_eq!(
detect_stub_class_kind("Directory", source),
None,
"should not match 'Directory' inside 'DirectoryIterator'"
);
}
#[test]
fn test_detect_skips_name_in_comments() {
let source = concat!(
"<?php\n",
"/**\n",
" * @see DirectoryIterator\n",
" */\n",
"class DirectoryIterator extends SplFileInfo\n{\n}\n",
);
assert_eq!(
detect_stub_class_kind("DirectoryIterator", source),
Some((ClassLikeKind::Class, false, false)),
"should skip the comment mention and find the actual class declaration"
);
}
#[test]
fn test_detect_skips_extends_mention() {
let source = concat!(
"<?php\n",
"class DirectoryIterator extends SplFileInfo\n{\n}\n",
);
assert_eq!(
detect_stub_class_kind("SplFileInfo", source),
None,
"should not match SplFileInfo in 'extends SplFileInfo' (no declaration keyword before it)"
);
}
#[test]
fn test_detect_with_fqn_key() {
let source = concat!(
"<?php\n",
"namespace Ds;\n",
"class Set implements Collection\n{\n}\n",
);
assert_eq!(
detect_stub_class_kind("Ds\\Set", source),
Some((ClassLikeKind::Class, false, false)),
"should handle FQN keys by extracting the short name"
);
}
#[test]
fn test_detect_not_found() {
let source = "<?php\nclass Foo {\n}\n";
assert_eq!(
detect_stub_class_kind("Bar", source),
None,
"should return None when the class is not in the source"
);
}
#[test]
fn test_detect_class_with_extends_and_implements() {
let source = "<?php\nclass SplFixedArray implements Iterator, ArrayAccess, Countable, IteratorAggregate, JsonSerializable\n{\n}\n";
assert_eq!(
detect_stub_class_kind("SplFixedArray", source),
Some((ClassLikeKind::Class, false, false)),
"should detect a class with multiple implements"
);
}
#[test]
fn test_extends_class_rejects_interface() {
assert!(
!ClassNameContext::ExtendsClass.matches_kind_flags(ClassLikeKind::Interface, false, false),
"ExtendsClass should reject interfaces"
);
}
#[test]
fn test_extends_class_rejects_final() {
assert!(
!ClassNameContext::ExtendsClass.matches_kind_flags(ClassLikeKind::Class, false, true),
"ExtendsClass should reject final classes"
);
}
#[test]
fn test_extends_class_accepts_abstract() {
assert!(
ClassNameContext::ExtendsClass.matches_kind_flags(ClassLikeKind::Class, true, false),
"ExtendsClass should accept abstract classes"
);
}
#[test]
fn test_implements_accepts_interface() {
assert!(
ClassNameContext::Implements.matches_kind_flags(ClassLikeKind::Interface, false, false),
"Implements should accept interfaces"
);
}
#[test]
fn test_implements_rejects_class() {
assert!(
!ClassNameContext::Implements.matches_kind_flags(ClassLikeKind::Class, false, false),
"Implements should reject classes"
);
}
#[test]
fn test_trait_use_accepts_trait() {
assert!(
ClassNameContext::TraitUse.matches_kind_flags(ClassLikeKind::Trait, false, false),
"TraitUse should accept traits"
);
}
#[test]
fn test_trait_use_rejects_class() {
assert!(
!ClassNameContext::TraitUse.matches_kind_flags(ClassLikeKind::Class, false, false),
"TraitUse should reject classes"
);
}
#[test]
fn test_instanceof_rejects_trait() {
assert!(
!ClassNameContext::Instanceof.matches_kind_flags(ClassLikeKind::Trait, false, false),
"Instanceof should reject traits"
);
}
#[test]
fn test_instanceof_accepts_enum() {
assert!(
ClassNameContext::Instanceof.matches_kind_flags(ClassLikeKind::Enum, false, false),
"Instanceof should accept enums"
);
}
#[test]
fn test_new_rejects_abstract() {
assert!(
!ClassNameContext::New.matches_kind_flags(ClassLikeKind::Class, true, false),
"New should reject abstract classes"
);
}
#[test]
fn test_new_rejects_interface() {
assert!(
!ClassNameContext::New.matches_kind_flags(ClassLikeKind::Interface, false, false),
"New should reject interfaces"
);
}
#[test]
fn test_type_hint_accepts_class() {
assert!(
ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Class, false, false),
"TypeHint should accept classes"
);
}
#[test]
fn test_type_hint_accepts_interface() {
assert!(
ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Interface, false, false),
"TypeHint should accept interfaces"
);
}
#[test]
fn test_type_hint_accepts_enum() {
assert!(
ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Enum, false, false),
"TypeHint should accept enums"
);
}
#[test]
fn test_type_hint_rejects_trait() {
assert!(
!ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Trait, false, false),
"TypeHint should reject traits"
);
}
#[test]
fn test_type_hint_accepts_abstract_class() {
assert!(
ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Class, true, false),
"TypeHint should accept abstract classes (they are valid type hints)"
);
}
#[test]
fn test_type_hint_accepts_final_class() {
assert!(
ClassNameContext::TypeHint.matches_kind_flags(ClassLikeKind::Class, false, true),
"TypeHint should accept final classes"
);
}
#[test]
fn test_type_hint_is_class_only() {
assert!(
ClassNameContext::TypeHint.is_class_only(),
"TypeHint should be class-only (no constants or functions)"
);
}
#[test]
fn test_type_hint_matches_class_info() {
let cls = ClassInfo {
kind: ClassLikeKind::Class,
name: "Foo".to_string(),
..Default::default()
};
assert!(ClassNameContext::TypeHint.matches(&cls));
let iface = ClassInfo {
kind: ClassLikeKind::Interface,
name: "Bar".to_string(),
..Default::default()
};
assert!(ClassNameContext::TypeHint.matches(&iface));
let enm = ClassInfo {
kind: ClassLikeKind::Enum,
name: "Baz".to_string(),
..Default::default()
};
assert!(ClassNameContext::TypeHint.matches(&enm));
let trait_info = ClassInfo {
kind: ClassLikeKind::Trait,
name: "Qux".to_string(),
..Default::default()
};
assert!(!ClassNameContext::TypeHint.matches(&trait_info));
}
#[test]
fn test_detect_use_import_context() {
let content = "<?php\nuse App";
let pos = Position {
line: 1,
character: 7,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::UseImport,
"Top-level `use` should produce UseImport"
);
}
#[test]
fn test_detect_use_function_context() {
let content = "<?php\nuse function array";
let pos = Position {
line: 1,
character: 19,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::UseFunction,
"`use function` should produce UseFunction"
);
}
#[test]
fn test_detect_use_const_context() {
let content = "<?php\nuse const PHP";
let pos = Position {
line: 1,
character: 14,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::UseConst,
"`use const` should produce UseConst"
);
}
#[test]
fn test_detect_use_inside_class_body_is_trait_use() {
let content = "<?php\nclass Foo {\n use Some";
let pos = Position {
line: 2,
character: 12,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::TraitUse,
"`use` inside class body should remain TraitUse"
);
}
#[test]
fn test_use_import_is_class_only() {
assert!(
ClassNameContext::UseImport.is_class_only(),
"UseImport should be class-only (no constants or functions)"
);
}
#[test]
fn test_use_function_is_not_class_only() {
assert!(
!ClassNameContext::UseFunction.is_class_only(),
"UseFunction should NOT be class-only (handler shows functions)"
);
}
#[test]
fn test_use_const_is_not_class_only() {
assert!(
!ClassNameContext::UseConst.is_class_only(),
"UseConst should NOT be class-only (handler shows constants)"
);
}
#[test]
fn test_use_import_accepts_all_kinds() {
assert!(ClassNameContext::UseImport.matches_kind_flags(ClassLikeKind::Class, false, false));
assert!(ClassNameContext::UseImport.matches_kind_flags(ClassLikeKind::Interface, false, false));
assert!(ClassNameContext::UseImport.matches_kind_flags(ClassLikeKind::Trait, false, false));
assert!(ClassNameContext::UseImport.matches_kind_flags(ClassLikeKind::Enum, false, false));
}
#[test]
fn test_detect_use_function_with_fqn_partial() {
let content = "<?php\nuse function App\\Helpers\\format";
let pos = Position {
line: 1,
character: 35,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::UseFunction,
"`use function` with namespace-qualified partial should produce UseFunction"
);
}
#[test]
fn test_detect_use_const_with_fqn_partial() {
let content = "<?php\nuse const App\\Config\\DB";
let pos = Position {
line: 1,
character: 26,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::UseConst,
"`use const` with namespace-qualified partial should produce UseConst"
);
}
#[test]
fn test_detect_namespace_declaration_context() {
let content = "<?php\nnamespace App";
let pos = Position {
line: 1,
character: 13,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::NamespaceDeclaration,
"Top-level `namespace` should produce NamespaceDeclaration"
);
}
#[test]
fn test_detect_namespace_declaration_with_partial_fqn() {
let content = "<?php\nnamespace App\\Models";
let pos = Position {
line: 1,
character: 22,
};
assert_eq!(
detect_class_name_context(content, pos),
ClassNameContext::NamespaceDeclaration,
"`namespace App\\Models` should produce NamespaceDeclaration"
);
}
#[test]
fn test_namespace_inside_class_body_is_not_declaration() {
let content = "<?php\nclass Foo {\n public function bar() {\n namespace\n";
let pos = Position {
line: 3,
character: 17,
};
assert_ne!(
detect_class_name_context(content, pos),
ClassNameContext::NamespaceDeclaration,
"`namespace` inside class body (brace depth >= 1) should not be NamespaceDeclaration"
);
}
#[test]
fn test_class_edit_texts_fqn_same_namespace_simplifies() {
let ns = Some("Demo".to_string());
let (insert, _filter, use_import) = class_edit_texts("Box", "Demo\\Box", true, true, &ns);
assert_eq!(insert, "Box", "Insert text should be the relative name");
assert!(
use_import.is_none(),
"No use import needed for same namespace"
);
}
#[test]
fn test_class_edit_texts_fqn_different_namespace_keeps_fqn() {
let ns = Some("Demo".to_string());
let (insert, _filter, use_import) = class_edit_texts("Foo", "Other\\Foo", true, true, &ns);
assert_eq!(
insert, "\\Other\\Foo",
"Insert should have leading backslash"
);
assert!(use_import.is_none(), "FQN mode never produces a use import");
}
#[test]
fn test_class_edit_texts_non_fqn_always_short_name() {
let ns: Option<String> = None;
let (insert, _filter, use_import) =
class_edit_texts("Dechunk", "http\\Encoding\\Dechunk", false, false, &ns);
assert_eq!(
insert, "Dechunk",
"Non-FQN mode should insert the short name"
);
assert_eq!(
use_import.as_deref(),
Some("http\\Encoding\\Dechunk"),
"Non-FQN mode should import the full FQN"
);
}
#[test]
fn test_class_edit_texts_fqn_nested_same_namespace() {
let ns = Some("Demo".to_string());
let (insert, _filter, use_import) =
class_edit_texts("Thing", "Demo\\Sub\\Thing", true, true, &ns);
assert_eq!(
insert, "Sub\\Thing",
"Nested same-namespace class should use relative path"
);
assert!(use_import.is_none(), "No use import for same namespace");
}
#[test]
fn test_class_edit_texts_leading_backslash_single_segment_same_ns() {
let ns = Some("Demo".to_string());
let (insert, _filter, use_import) = class_edit_texts("Box", "Demo\\Box", true, true, &ns);
assert_eq!(
insert, "Box",
"Insert text should be 'Box', not '\\Box' or '\\Demo\\Box'"
);
assert!(
use_import.is_none(),
"No use import needed for same namespace"
);
}
#[test]
fn test_class_edit_texts_leading_backslash_single_segment_diff_ns() {
let ns = Some("Demo".to_string());
let (insert, _filter, use_import) = class_edit_texts("Foo", "Other\\Foo", true, true, &ns);
assert_eq!(
insert, "\\Other\\Foo",
"Insert should have leading backslash for different namespace"
);
assert!(use_import.is_none(), "FQN mode never produces a use import");
}
#[test]
fn test_affinity_table_empty_use_map() {
let use_map = HashMap::new();
let ns: Option<String> = None;
let table = build_affinity_table(&use_map, &ns);
assert!(
table.is_empty(),
"Empty use-map + no namespace → empty table"
);
}
#[test]
fn test_affinity_table_single_import() {
let mut use_map = HashMap::new();
use_map.insert(
"Brand".to_string(),
"Luxplus\\Database\\Model\\Brands\\Brand".to_string(),
);
let ns: Option<String> = None;
let table = build_affinity_table(&use_map, &ns);
assert_eq!(table.get("Luxplus"), Some(&1));
assert_eq!(table.get("Luxplus\\Database"), Some(&1));
assert_eq!(table.get("Luxplus\\Database\\Model"), Some(&1));
assert_eq!(table.get("Luxplus\\Database\\Model\\Brands"), Some(&1));
assert_eq!(
table.get("Luxplus\\Database\\Model\\Brands\\Brand"),
None,
"Class name itself is not a prefix"
);
}
#[test]
fn test_affinity_table_file_namespace_only() {
let use_map = HashMap::new();
let ns = Some("App\\Http\\Controllers".to_string());
let table = build_affinity_table(&use_map, &ns);
assert_eq!(table.get("App"), Some(&1));
assert_eq!(table.get("App\\Http"), Some(&1));
assert_eq!(table.get("App\\Http\\Controllers"), Some(&1));
}
#[test]
fn test_affinity_table_file_namespace_plus_imports() {
let mut use_map = HashMap::new();
use_map.insert(
"Request".to_string(),
"App\\Http\\Requests\\SupplierRequest".to_string(),
);
use_map.insert(
"Brand".to_string(),
"Luxplus\\Database\\Model\\Brands\\Brand".to_string(),
);
let ns = Some("App\\Http\\Controllers".to_string());
let table = build_affinity_table(&use_map, &ns);
assert_eq!(table.get("App"), Some(&2));
assert_eq!(table.get("App\\Http"), Some(&2));
assert_eq!(table.get("App\\Http\\Controllers"), Some(&1));
assert_eq!(table.get("App\\Http\\Requests"), Some(&1));
assert_eq!(table.get("Luxplus"), Some(&1));
}
#[test]
fn test_affinity_table_global_namespace_import() {
let mut use_map = HashMap::new();
use_map.insert(
"RuntimeException".to_string(),
"RuntimeException".to_string(),
);
let ns: Option<String> = None;
let table = build_affinity_table(&use_map, &ns);
assert!(
table.is_empty(),
"Global-namespace imports contribute nothing to the table"
);
}
#[test]
fn test_affinity_score_known_prefix() {
let mut table = HashMap::new();
table.insert("Luxplus".to_string(), 11);
table.insert("Luxplus\\Database".to_string(), 6);
table.insert("Luxplus\\Database\\Model".to_string(), 6);
let score = affinity_score("Luxplus\\Database\\Model\\Orders\\Order", &table);
assert_eq!(score, 23);
}
#[test]
fn test_affinity_score_no_matching_prefix() {
let mut table = HashMap::new();
table.insert("App".to_string(), 4);
let score = affinity_score("Some\\Random\\Vendor\\Order", &table);
assert_eq!(score, 0);
}
#[test]
fn test_affinity_score_global_namespace_candidate() {
let mut table = HashMap::new();
table.insert("App".to_string(), 4);
let score = affinity_score("RuntimeException", &table);
assert_eq!(score, 0);
}
#[test]
fn test_affinity_score_empty_table() {
let table = HashMap::new();
let score = affinity_score("Luxplus\\Database\\Model\\Orders\\Order", &table);
assert_eq!(score, 0);
}
#[test]
fn test_match_quality_exact() {
assert_eq!(match_quality("Order", "Order"), 'a');
}
#[test]
fn test_match_quality_exact_case_insensitive() {
assert_eq!(match_quality("Order", "order"), 'a');
assert_eq!(match_quality("ORDER", "order"), 'a');
}
#[test]
fn test_match_quality_starts_with() {
assert_eq!(match_quality("OrderLine", "Order"), 'b');
assert_eq!(match_quality("OrderService", "ord"), 'b');
}
#[test]
fn test_match_quality_contains() {
assert_eq!(match_quality("CheckOrderFlowJob", "Order"), 'c');
assert_eq!(match_quality("MyOrderService", "order"), 'c');
}
#[test]
fn test_match_quality_empty_prefix_returns_b() {
assert_eq!(match_quality("Order", ""), 'b');
assert_eq!(match_quality("AnythingAtAll", ""), 'b');
}
#[test]
fn test_class_sort_text_format() {
let mut table = HashMap::new();
table.insert("App".to_string(), 4);
let result = class_sort_text("Order", "App\\Models\\Order", "Order", '2', false, &table);
assert_eq!(result, "a299950000_order");
}
#[test]
fn test_class_sort_text_demoted() {
let table = HashMap::new();
let normal = class_sort_text("Handler", "Vendor\\Handler", "Handler", '2', false, &table);
let demoted = class_sort_text("Handler", "Vendor\\Handler", "Handler", '2', true, &table);
assert!(
normal < demoted,
"Demoted should sort after normal: normal={normal}, demoted={demoted}"
);
}
#[test]
fn test_class_sort_text_quality_beats_tier() {
let table = HashMap::new();
let exact_tier2 = class_sort_text("Order", "Vendor\\Order", "Order", '2', false, &table);
let prefix_tier0 = class_sort_text(
"OrderLine",
"Vendor\\OrderLine",
"Order",
'0',
false,
&table,
);
assert!(
exact_tier2 < prefix_tier0,
"Exact match (tier 2) should sort before prefix match (tier 0): exact={exact_tier2}, prefix={prefix_tier0}"
);
}
#[test]
fn test_class_sort_text_tier_beats_affinity() {
let mut table = HashMap::new();
table.insert("Luxplus".to_string(), 50);
let tier1_low = class_sort_text("Order", "App\\Order", "Order", '1', false, &table);
let tier2_high = class_sort_text("Order", "Luxplus\\Order", "Order", '2', false, &table);
assert!(
tier1_low < tier2_high,
"Tier 1 should sort before tier 2 regardless of affinity: tier1={tier1_low}, tier2={tier2_high}"
);
}
#[test]
fn test_class_sort_text_affinity_within_same_tier() {
let mut table = HashMap::new();
table.insert("Luxplus".to_string(), 11);
table.insert("Luxplus\\Database".to_string(), 6);
table.insert("Luxplus\\Database\\Model".to_string(), 6);
table.insert("App".to_string(), 4);
let high = class_sort_text(
"Order",
"Luxplus\\Database\\Model\\Orders\\Order",
"Order",
'2',
false,
&table,
);
let low = class_sort_text("Order", "App\\Models\\Order", "Order", '2', false, &table);
assert!(
high < low,
"Higher affinity should sort first: high={high}, low={low}"
);
}
#[test]
fn test_class_sort_text_demote_after_quality() {
let table = HashMap::new();
let demoted_exact = class_sort_text("Order", "Vendor\\Order", "Order", '2', true, &table);
let normal_prefix = class_sort_text(
"OrderLine",
"Vendor\\OrderLine",
"Order",
'2',
false,
&table,
);
assert!(
demoted_exact < normal_prefix,
"Demoted exact match should sort before non-demoted prefix match: demoted_exact={demoted_exact}, normal_prefix={normal_prefix}"
);
}
#[test]
fn test_class_sort_text_alphabetical_tiebreak() {
let table = HashMap::new();
let alpha = class_sort_text("Alpha", "Vendor\\Alpha", "Al", '2', false, &table);
let beta = class_sort_text("Beta", "Vendor\\Beta", "B", '2', false, &table);
assert!(
alpha < beta,
"Alphabetical tiebreak: alpha={alpha}, beta={beta}"
);
}
#[test]
fn test_class_sort_text_gap_within_same_affinity() {
let mut table = HashMap::new();
table.insert("Luxplus".to_string(), 11);
table.insert("Luxplus\\Core".to_string(), 6);
table.insert("Luxplus\\Core\\Database".to_string(), 6);
table.insert("Luxplus\\Core\\Database\\Model".to_string(), 6);
table.insert("Luxplus\\Core\\Database\\Model\\Products".to_string(), 1);
let short = class_sort_text(
"Product",
"Luxplus\\Core\\Database\\Model\\Products\\Product",
"Pro",
'2',
false,
&table,
);
let long = class_sort_text(
"ProductFilterTerm",
"Luxplus\\Core\\Database\\Model\\Products\\Filters\\ProductFilterTerm",
"Pro",
'2',
false,
&table,
);
assert!(
short < long,
"Shorter name (smaller gap) should sort first within same affinity: short={short}, long={long}"
);
}
#[test]
fn test_class_sort_text_affinity_beats_gap() {
let mut table = HashMap::new();
table.insert("Luxplus".to_string(), 11);
table.insert("Luxplus\\Database".to_string(), 6);
let high_affinity = class_sort_text(
"Product",
"Luxplus\\Database\\Product",
"Pro",
'2',
false,
&table,
);
let low_affinity = class_sort_text("Proxy", "Mockery\\Proxy", "Pro", '2', false, &table);
assert!(
high_affinity < low_affinity,
"Higher affinity should beat smaller gap: high_affinity={high_affinity}, low_affinity={low_affinity}"
);
}
#[test]
fn test_is_class_declaration_class() {
let content = "<?php\n\nclass T";
let pos = Position {
line: 2,
character: 7,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_no_partial() {
let content = "<?php\n\nclass ";
let pos = Position {
line: 2,
character: 6,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_interface() {
let content = "<?php\n\ninterface P";
let pos = Position {
line: 2,
character: 11,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_trait() {
let content = "<?php\n\ntrait H";
let pos = Position {
line: 2,
character: 7,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_enum() {
let content = "<?php\n\nenum S";
let pos = Position {
line: 2,
character: 6,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_abstract_class() {
let content = "<?php\n\nabstract class B";
let pos = Position {
line: 2,
character: 16,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_final_class() {
let content = "<?php\n\nfinal class S";
let pos = Position {
line: 2,
character: 13,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_readonly_class() {
let content = "<?php\n\nreadonly class V";
let pos = Position {
line: 2,
character: 16,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_final_readonly_class() {
let content = "<?php\n\nfinal readonly class M";
let pos = Position {
line: 2,
character: 22,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_anonymous_class_rejected() {
let content = "<?php\n\n$x = new class ";
let pos = Position {
line: 2,
character: 16,
};
assert!(!is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_extends_rejected() {
let content = "<?php\n\nclass Foo extends B";
let pos = Position {
line: 2,
character: 21,
};
assert!(!is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_implements_rejected() {
let content = "<?php\n\nclass Foo implements S";
let pos = Position {
line: 2,
character: 25,
};
assert!(!is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_new_keyword_rejected() {
let content = "<?php\n\n$x = new Foo";
let pos = Position {
line: 2,
character: 12,
};
assert!(!is_class_declaration_name(content, pos));
}
#[test]
fn test_is_class_declaration_with_namespace() {
let content = "<?php\n\nnamespace App\\Services;\n\nclass O";
let pos = Position {
line: 4,
character: 7,
};
assert!(is_class_declaration_name(content, pos));
}
#[test]
fn test_extract_partial_rejects_open_tag() {
let content = "<?php";
let pos = Position {
line: 0,
character: 5,
};
assert_eq!(Backend::extract_partial_class_name(content, pos), None);
}
#[test]
fn test_extract_partial_rejects_open_tag_with_trailing_newline() {
let content = "<?php\n";
let pos = Position {
line: 0,
character: 5,
};
assert_eq!(Backend::extract_partial_class_name(content, pos), None);
}
#[test]
fn test_extract_partial_allows_php_prefixed_identifier() {
let content = "<?php\nphp_ini";
let pos = Position {
line: 1,
character: 7,
};
assert_eq!(
Backend::extract_partial_class_name(content, pos),
Some("php_ini".to_string())
);
}