1use crate::analysis::class_model::{
25 AccessorType, ClassAccessorMode, ClassModel, ClassModelBuilder, Framework,
26};
27use crate::ast::Node;
28use perl_semantic_facts::{
29 AnchorId, Confidence, EntityId, FileId, GeneratedMember, GeneratedMemberKind, Provenance,
30};
31
32pub struct GeneratedMemberExtractor;
35
36impl GeneratedMemberExtractor {
37 pub fn extract(ast: &Node, package: &str, _file_id: FileId) -> Vec<GeneratedMember> {
45 let models = ClassModelBuilder::new().build(ast);
46 Self::extract_from_models(&models, package)
47 }
48
49 pub fn extract_from_models(models: &[ClassModel], package: &str) -> Vec<GeneratedMember> {
54 let mut members = Vec::new();
55
56 for model in models {
57 let pkg = if model.name.is_empty() { package } else { &model.name };
58
59 if is_accessor_framework(model.framework) {
60 collect_has_members(model, pkg, &mut members);
61 }
62
63 if model.framework == Framework::ClassAccessor {
64 collect_class_accessor_members(model, pkg, &mut members);
65 }
66 }
67
68 members
69 }
70}
71
72fn is_accessor_framework(framework: Framework) -> bool {
74 matches!(framework, Framework::Moo | Framework::Moose | Framework::Mouse)
75}
76
77fn collect_has_members(model: &ClassModel, package: &str, members: &mut Vec<GeneratedMember>) {
78 for attr in &model.attributes {
79 let anchor_id = AnchorId(attr.location.start as u64);
80
81 match attr.is {
83 None => {
84 members.push(make_member(
87 &attr.accessor_name,
88 GeneratedMemberKind::Accessor,
89 anchor_id,
90 package,
91 ));
92 }
93 Some(AccessorType::Rw) => {
94 members.push(make_member(
96 &attr.accessor_name,
97 GeneratedMemberKind::Accessor,
98 anchor_id,
99 package,
100 ));
101 }
102 Some(AccessorType::Ro | AccessorType::Lazy) => {
103 members.push(make_member(
105 &attr.accessor_name,
106 GeneratedMemberKind::Getter,
107 anchor_id,
108 package,
109 ));
110 }
111 Some(AccessorType::Bare) => {
112 }
114 }
115
116 if let Some(pred_name) = &attr.predicate {
118 members.push(make_member(
119 pred_name,
120 GeneratedMemberKind::Predicate,
121 anchor_id,
122 package,
123 ));
124 }
125
126 if let Some(clear_name) = &attr.clearer {
128 members.push(make_member(clear_name, GeneratedMemberKind::Clearer, anchor_id, package));
129 }
130
131 if let Some(builder_name) = &attr.builder {
133 members.push(make_member(
134 builder_name,
135 GeneratedMemberKind::Builder,
136 anchor_id,
137 package,
138 ));
139 }
140 }
141}
142
143fn collect_class_accessor_members(
144 model: &ClassModel,
145 package: &str,
146 members: &mut Vec<GeneratedMember>,
147) {
148 for method in &model.methods {
149 let Some(accessor_mode) = method.accessor_mode else {
150 continue;
151 };
152 if !method.synthetic {
153 continue;
154 }
155
156 members.push(make_member(
157 &method.name,
158 class_accessor_kind(accessor_mode),
159 AnchorId(method.location.start as u64),
160 package,
161 ));
162 }
163}
164
165fn class_accessor_kind(mode: ClassAccessorMode) -> GeneratedMemberKind {
166 match mode {
167 ClassAccessorMode::Rw => GeneratedMemberKind::Accessor,
168 ClassAccessorMode::Ro => GeneratedMemberKind::Getter,
169 ClassAccessorMode::Wo => GeneratedMemberKind::Setter,
170 }
171}
172
173fn make_member(
176 name: &str,
177 kind: GeneratedMemberKind,
178 source_anchor_id: AnchorId,
179 package: &str,
180) -> GeneratedMember {
181 let entity_id = deterministic_entity_id(package, name, source_anchor_id);
183 GeneratedMember::new(
184 entity_id,
185 name.to_string(),
186 kind,
187 source_anchor_id,
188 package.to_string(),
189 Provenance::FrameworkSynthesis,
190 Confidence::Medium,
191 )
192}
193
194fn deterministic_entity_id(package: &str, name: &str, anchor_id: AnchorId) -> EntityId {
198 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
200 const FNV_PRIME: u64 = 0x0100_0000_01b3;
201
202 let mut hash = FNV_OFFSET;
203 for byte in package.as_bytes() {
204 hash ^= u64::from(*byte);
205 hash = hash.wrapping_mul(FNV_PRIME);
206 }
207 hash ^= 0xFF;
209 hash = hash.wrapping_mul(FNV_PRIME);
210 for byte in name.as_bytes() {
211 hash ^= u64::from(*byte);
212 hash = hash.wrapping_mul(FNV_PRIME);
213 }
214 hash ^= anchor_id.0;
215 hash = hash.wrapping_mul(FNV_PRIME);
216
217 EntityId(hash)
218}
219
220#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::Parser;
226
227 fn parse_and_extract(code: &str) -> Vec<GeneratedMember> {
229 let mut parser = Parser::new(code);
230 let ast = match parser.parse() {
231 Ok(ast) => ast,
232 Err(_) => return Vec::new(),
233 };
234 GeneratedMemberExtractor::extract(&ast, "main", FileId(1))
235 }
236
237 fn members_named<'a>(members: &'a [GeneratedMember], name: &str) -> Vec<&'a GeneratedMember> {
239 members.iter().filter(|m| m.name == name).collect()
240 }
241
242 #[test]
245 fn bare_has_generates_accessor() -> Result<(), String> {
246 let code = "package MyApp::User;\nuse Moo;\nhas 'username';\n1;";
247 let members = parse_and_extract(code);
248 let matched = members_named(&members, "username");
249 let member = matched.first().ok_or("expected a GeneratedMember for 'username'")?;
250
251 assert_eq!(member.kind, GeneratedMemberKind::Accessor);
252 assert_eq!(member.package, "MyApp::User");
253 assert_eq!(member.provenance, Provenance::FrameworkSynthesis);
254 assert_eq!(member.confidence, Confidence::Medium);
255 Ok(())
256 }
257
258 #[test]
261 fn rw_has_generates_accessor() -> Result<(), String> {
262 let code = "package MyApp::User;\nuse Moose;\nhas 'email' => (is => 'rw');\n1;";
263 let members = parse_and_extract(code);
264 let matched = members_named(&members, "email");
265 assert_eq!(matched.len(), 1, "expected one accessor, got {}", matched.len());
266 assert_eq!(matched[0].kind, GeneratedMemberKind::Accessor);
267 assert_eq!(matched[0].provenance, Provenance::FrameworkSynthesis);
268 assert_eq!(matched[0].confidence, Confidence::Medium);
269 Ok(())
270 }
271
272 #[test]
275 fn ro_has_generates_getter_only() -> Result<(), String> {
276 let code = "package MyApp::User;\nuse Moo;\nhas 'name' => (is => 'ro');\n1;";
277 let members = parse_and_extract(code);
278 let matched = members_named(&members, "name");
279 assert_eq!(matched.len(), 1, "expected one member, got {}", matched.len());
280
281 let member = matched[0];
282 assert_eq!(member.kind, GeneratedMemberKind::Getter);
283 assert_eq!(member.provenance, Provenance::FrameworkSynthesis);
284 assert_eq!(member.confidence, Confidence::Medium);
285 Ok(())
286 }
287
288 #[test]
291 fn lazy_has_generates_getter_only() -> Result<(), String> {
292 let code = "package MyApp::Config;\nuse Moo;\nhas 'settings' => (is => 'lazy');\n1;";
293 let members = parse_and_extract(code);
294 let matched = members_named(&members, "settings");
295 assert_eq!(matched.len(), 1, "expected one member, got {}", matched.len());
296 assert_eq!(matched[0].kind, GeneratedMemberKind::Getter);
297 Ok(())
298 }
299
300 #[test]
303 fn bare_is_generates_no_accessor() -> Result<(), String> {
304 let code = "package MyApp::Internal;\nuse Moose;\nhas '_data' => (is => 'bare');\n1;";
305 let members = parse_and_extract(code);
306 let matched = members_named(&members, "_data");
307 assert!(matched.is_empty(), "expected no members for bare accessor, got {matched:?}");
308 Ok(())
309 }
310
311 #[test]
314 fn predicate_clearer_builder_generated() -> Result<(), String> {
315 let code = r#"
316package MyApp::User;
317use Moose;
318has 'nickname' => (
319 is => 'rw',
320 predicate => 1,
321 clearer => 1,
322 builder => 1,
323);
3241;
325"#;
326 let members = parse_and_extract(code);
327
328 let pred = members.iter().find(|m| m.name == "has_nickname");
329 let pred = pred.ok_or("expected predicate 'has_nickname'")?;
330 assert_eq!(pred.kind, GeneratedMemberKind::Predicate);
331
332 let clear = members.iter().find(|m| m.name == "clear_nickname");
333 let clear = clear.ok_or("expected clearer 'clear_nickname'")?;
334 assert_eq!(clear.kind, GeneratedMemberKind::Clearer);
335
336 let builder = members.iter().find(|m| m.name == "_build_nickname");
337 let builder = builder.ok_or("expected builder '_build_nickname'")?;
338 assert_eq!(builder.kind, GeneratedMemberKind::Builder);
339 Ok(())
340 }
341
342 #[test]
345 fn custom_predicate_clearer_builder_names() -> Result<(), String> {
346 let code = r#"
347package MyApp::User;
348use Moo;
349has 'age' => (
350 is => 'ro',
351 predicate => 'has_user_age',
352 clearer => 'reset_age',
353 builder => 'compute_age',
354);
3551;
356"#;
357 let members = parse_and_extract(code);
358
359 let pred = members.iter().find(|m| m.name == "has_user_age");
360 assert!(pred.is_some(), "expected custom predicate 'has_user_age'");
361
362 let clear = members.iter().find(|m| m.name == "reset_age");
363 assert!(clear.is_some(), "expected custom clearer 'reset_age'");
364
365 let builder = members.iter().find(|m| m.name == "compute_age");
366 assert!(builder.is_some(), "expected custom builder 'compute_age'");
367 Ok(())
368 }
369
370 #[test]
373 fn multiple_attributes_in_one_has() -> Result<(), String> {
374 let code = r#"
375package MyApp::Point;
376use Moo;
377has ['x', 'y'] => (is => 'ro');
3781;
379"#;
380 let members = parse_and_extract(code);
381 let x_members = members_named(&members, "x");
382 let y_members = members_named(&members, "y");
383
384 assert_eq!(x_members.len(), 1, "expected one member for 'x'");
385 assert_eq!(y_members.len(), 1, "expected one member for 'y'");
386 assert_eq!(x_members[0].kind, GeneratedMemberKind::Getter);
387 assert_eq!(y_members[0].kind, GeneratedMemberKind::Getter);
388 Ok(())
389 }
390
391 #[test]
392 fn bare_identifier_has_generates_getter() -> Result<(), String> {
393 let code = "package MyApp::User;\nuse Moo;\nhas name => (is => 'ro');\n1;";
394 let members = parse_and_extract(code);
395 let matched = members_named(&members, "name");
396 assert_eq!(matched.len(), 1, "expected one generated member for bare identifier attr");
397 assert_eq!(matched[0].kind, GeneratedMemberKind::Getter);
398 assert_eq!(matched[0].provenance, Provenance::FrameworkSynthesis);
399 assert_eq!(matched[0].confidence, Confidence::Medium);
400 Ok(())
401 }
402
403 #[test]
404 fn augmented_attribute_strips_plus_prefix() -> Result<(), String> {
405 let code = r#"
406package MyApp::User;
407use Moo;
408has '+name' => (is => 'ro', builder => 1, predicate => 1, clearer => 1);
4091;
410"#;
411 let members = parse_and_extract(code);
412
413 let getter = members.iter().find(|m| m.name == "name");
414 assert!(getter.is_some(), "expected getter named `name`, got {members:?}");
415 assert!(
416 members.iter().any(|m| m.name == "_build_name"),
417 "expected builder `_build_name`, got {members:?}"
418 );
419 assert!(
420 members.iter().any(|m| m.name == "has_name"),
421 "expected predicate `has_name`, got {members:?}"
422 );
423 assert!(
424 members.iter().any(|m| m.name == "clear_name"),
425 "expected clearer `clear_name`, got {members:?}"
426 );
427 assert!(
428 members.iter().all(|m| !m.name.starts_with('+')),
429 "generated member names should not retain `+`: {members:?}"
430 );
431 Ok(())
432 }
433
434 #[test]
435 fn class_accessor_methods_emit_generated_members() -> Result<(), String> {
436 let code = r#"
437package MyApp::Accessor;
438use parent 'Class::Accessor';
439__PACKAGE__->mk_accessors(qw(foo bar));
440__PACKAGE__->mk_rw_accessors(qw(read_write));
441__PACKAGE__->mk_ro_accessors(qw(read_only));
442__PACKAGE__->mk_wo_accessors(qw(write_only));
4431;
444"#;
445 let members = parse_and_extract(code);
446
447 let foo = members_named(&members, "foo");
448 let read_write = members_named(&members, "read_write");
449 let read_only = members_named(&members, "read_only");
450 let write_only = members_named(&members, "write_only");
451
452 assert_eq!(foo.len(), 1, "expected Class::Accessor `foo` member");
453 assert_eq!(foo[0].kind, GeneratedMemberKind::Accessor);
454 assert_eq!(read_write.len(), 1, "expected Class::Accessor rw member");
455 assert_eq!(read_write[0].kind, GeneratedMemberKind::Accessor);
456 assert_eq!(read_only.len(), 1, "expected Class::Accessor ro member");
457 assert_eq!(read_only[0].kind, GeneratedMemberKind::Getter);
458 assert_eq!(write_only.len(), 1, "expected Class::Accessor wo member");
459 assert_eq!(write_only[0].kind, GeneratedMemberKind::Setter);
460
461 for member in &members {
462 assert_eq!(member.package, "MyApp::Accessor");
463 assert_eq!(member.provenance, Provenance::FrameworkSynthesis);
464 assert_eq!(member.confidence, Confidence::Medium);
465 }
466 Ok(())
467 }
468
469 #[test]
472 fn plain_oo_has_is_ignored() -> Result<(), String> {
473 let code = "package PlainPkg;\nuse parent 'Base';\nhas 'x' => (is => 'rw');\n1;";
474 let members = parse_and_extract(code);
475 let matched = members_named(&members, "x");
479 assert!(
480 matched.is_empty(),
481 "expected no generated members for plain OO package, got {matched:?}"
482 );
483 Ok(())
484 }
485
486 #[test]
489 fn mouse_has_generates_members() -> Result<(), String> {
490 let code = "package MyApp::Tiny;\nuse Mouse;\nhas 'value' => (is => 'rw');\n1;";
491 let members = parse_and_extract(code);
492 let matched = members_named(&members, "value");
493 assert_eq!(matched.len(), 1, "expected accessor for Mouse class");
494 assert_eq!(matched[0].kind, GeneratedMemberKind::Accessor);
495 Ok(())
496 }
497
498 #[test]
501 fn all_members_have_framework_synthesis_provenance() -> Result<(), String> {
502 let code = r#"
503package MyApp::Full;
504use Moose;
505has 'attr1' => (is => 'rw', predicate => 1, clearer => 1, builder => 1);
506has 'attr2' => (is => 'ro');
5071;
508"#;
509 let members = parse_and_extract(code);
510 assert!(!members.is_empty(), "expected at least one generated member");
511
512 for member in &members {
513 assert_eq!(
514 member.provenance,
515 Provenance::FrameworkSynthesis,
516 "member '{}' has wrong provenance: {:?}",
517 member.name,
518 member.provenance
519 );
520 assert_eq!(
521 member.confidence,
522 Confidence::Medium,
523 "member '{}' has wrong confidence: {:?}",
524 member.name,
525 member.confidence
526 );
527 }
528 Ok(())
529 }
530
531 #[test]
534 fn entity_ids_are_deterministic() -> Result<(), String> {
535 let code = "package MyApp::User;\nuse Moo;\nhas 'name' => (is => 'ro');\n1;";
536 let members1 = parse_and_extract(code);
537 let members2 = parse_and_extract(code);
538
539 assert_eq!(members1.len(), members2.len(), "member count differs across runs");
540 for (a, b) in members1.iter().zip(members2.iter()) {
541 assert_eq!(a.entity_id, b.entity_id, "entity_id differs for '{}'", a.name);
542 }
543 Ok(())
544 }
545
546 #[test]
549 fn multiple_packages_have_correct_package_names() -> Result<(), String> {
550 let code = r#"
551package Foo;
552use Moo;
553has 'a' => (is => 'ro');
554
555package Bar;
556use Moose;
557has 'b' => (is => 'rw');
5581;
559"#;
560 let members = parse_and_extract(code);
561
562 let a_members = members_named(&members, "a");
563 let a = a_members.first().ok_or("expected member 'a'")?;
564 assert_eq!(a.package, "Foo");
565
566 let b_members = members_named(&members, "b");
567 for m in &b_members {
569 assert_eq!(m.package, "Bar");
570 }
571 Ok(())
572 }
573}