1use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5use std::sync::OnceLock;
6
7use crate::ast::{
8 Block, DerefKind, Expr, ExprKind, MatchArrayElem, Program, Sigil, Statement, StmtKind,
9 StringPart, SubSigParam,
10};
11use crate::error::{ErrorKind, StrykeError, StrykeResult};
12
13static BUILTINS: OnceLock<HashSet<&'static str>> = OnceLock::new();
14
15fn builtins() -> &'static HashSet<&'static str> {
16 BUILTINS.get_or_init(|| {
17 include_str!("lsp_completion_words.txt")
18 .lines()
19 .map(str::trim)
20 .filter(|l| !l.is_empty() && !l.starts_with('#'))
21 .collect()
22 })
23}
24
25#[derive(Default)]
26struct Scope {
27 scalars: HashSet<String>,
28 arrays: HashSet<String>,
29 hashes: HashSet<String>,
30 subs: HashSet<String>,
31 scalar_types: HashMap<String, String>,
35}
36
37impl Scope {
38 fn declare_scalar(&mut self, name: &str) {
39 self.scalars.insert(name.to_string());
40 }
41 fn declare_array(&mut self, name: &str) {
42 self.arrays.insert(name.to_string());
43 }
44 fn declare_hash(&mut self, name: &str) {
45 self.hashes.insert(name.to_string());
46 }
47 fn declare_sub(&mut self, name: &str) {
48 self.subs.insert(name.to_string());
49 }
50}
51
52pub struct StaticAnalyzer {
53 scopes: Vec<Scope>,
54 errors: Vec<StrykeError>,
55 file: String,
56 current_package: String,
57 strict_vars: bool,
66 seen_required_files: HashSet<PathBuf>,
70 type_fields: HashMap<String, HashSet<String>>,
75 type_methods: HashMap<String, HashSet<String>>,
80 type_parents: HashMap<String, Vec<String>>,
85 current_class: Option<String>,
89}
90
91impl StaticAnalyzer {
92 pub fn new(file: &str) -> Self {
93 Self::with_strict_vars(file, false)
94 }
95
96 pub fn with_strict_vars(file: &str, strict_vars: bool) -> Self {
97 let mut global = Scope::default();
98 for name in ["_", "a", "b", "ARGV", "ENV", "SIG", "INC"] {
99 global.declare_array(name);
100 }
101 for name in ["ENV", "SIG", "INC"] {
102 global.declare_hash(name);
103 }
104 for name in [
105 "_", "a", "b", "!", "$", "@", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "&",
106 "`", "'", "+", ".", "/", "\\", "|", "%", "=", "-", "~", "^", "*", "?", "\"",
107 ] {
108 global.declare_scalar(name);
109 }
110 Self {
111 scopes: vec![global],
112 errors: Vec::new(),
113 file: file.to_string(),
114 current_package: "main".to_string(),
115 strict_vars,
116 seen_required_files: HashSet::new(),
117 type_fields: HashMap::new(),
118 type_methods: HashMap::new(),
119 type_parents: HashMap::new(),
120 current_class: None,
121 }
122 }
123
124 fn push_scope(&mut self) {
125 self.scopes.push(Scope::default());
126 }
127
128 fn pop_scope(&mut self) {
129 if self.scopes.len() > 1 {
130 self.scopes.pop();
131 }
132 }
133
134 fn declare_scalar(&mut self, name: &str) {
135 if let Some(scope) = self.scopes.last_mut() {
136 scope.declare_scalar(name);
137 }
138 }
139
140 fn declare_array(&mut self, name: &str) {
141 if let Some(scope) = self.scopes.last_mut() {
142 scope.declare_array(name);
143 }
144 }
145
146 fn declare_hash(&mut self, name: &str) {
147 if let Some(scope) = self.scopes.last_mut() {
148 scope.declare_hash(name);
149 }
150 }
151
152 fn declare_sub(&mut self, name: &str) {
153 if let Some(scope) = self.scopes.first_mut() {
154 scope.declare_sub(name);
155 }
156 }
157
158 fn declare_scalar_type(&mut self, name: &str, ty: &str) {
162 if let Some(scope) = self.scopes.last_mut() {
163 scope.scalar_types.insert(name.to_string(), ty.to_string());
164 }
165 }
166
167 fn resolve_scalar_type(&self, name: &str) -> Option<&str> {
170 for s in self.scopes.iter().rev() {
171 if let Some(t) = s.scalar_types.get(name) {
172 return Some(t.as_str());
173 }
174 }
175 None
176 }
177
178 fn infer_constructor_type(&self, init: &Expr) -> Option<String> {
183 match &init.kind {
184 ExprKind::FuncCall { name, .. } => {
185 let bare = name.rsplit("::").next().unwrap_or(name);
186 if self.type_fields.contains_key(name) {
187 return Some(name.clone());
188 }
189 if self.type_fields.contains_key(bare) {
190 return Some(bare.to_string());
191 }
192 None
193 }
194 ExprKind::MethodCall { object, method, .. } if method == "new" => {
195 if let ExprKind::Bareword(n) = &object.kind {
196 let bare = n.rsplit("::").next().unwrap_or(n);
197 if self.type_fields.contains_key(n) {
198 return Some(n.clone());
199 }
200 if self.type_fields.contains_key(bare) {
201 return Some(bare.to_string());
202 }
203 }
204 None
205 }
206 _ => None,
207 }
208 }
209
210 fn is_scalar_defined(&self, name: &str) -> bool {
211 if is_special_var(name) || is_topic_var(name) {
212 return true;
213 }
214 self.scopes.iter().rev().any(|s| s.scalars.contains(name))
215 }
216
217 fn is_array_defined(&self, name: &str) -> bool {
218 if is_special_var(name) || is_topic_var(name) {
219 return true;
220 }
221 self.scopes.iter().rev().any(|s| s.arrays.contains(name))
222 }
223
224 fn is_hash_defined(&self, name: &str) -> bool {
225 if is_special_var(name) || is_topic_var(name) {
226 return true;
227 }
228 self.scopes.iter().rev().any(|s| s.hashes.contains(name))
229 }
230
231 fn is_sub_defined(&self, name: &str) -> bool {
232 if name.starts_with("static::") {
234 return true;
235 }
236 if matches!(
241 name,
242 "_thread_par_run"
243 | "__stryke_rust_compile"
244 | "defer__internal"
245 | "deque",
249 ) {
250 return true;
251 }
252 let base = name.rsplit("::").next().unwrap_or(name);
253 if builtins().contains(base) {
254 return true;
255 }
256 self.scopes
257 .iter()
258 .rev()
259 .any(|s| s.subs.contains(name) || s.subs.contains(base))
260 }
261
262 fn error(&mut self, kind: ErrorKind, msg: String, line: usize) {
263 self.errors
264 .push(StrykeError::new(kind, msg, line, &self.file));
265 }
266
267 pub fn analyze(mut self, program: &Program) -> StrykeResult<()> {
268 for stmt in &program.statements {
269 self.collect_declarations_stmt(stmt);
270 }
271 for stmt in &program.statements {
272 self.analyze_stmt(stmt);
273 }
274 if let Some(e) = self.errors.into_iter().next() {
275 Err(e)
276 } else {
277 Ok(())
278 }
279 }
280
281 fn collect_declarations_stmt(&mut self, stmt: &Statement) {
282 match &stmt.kind {
283 StmtKind::Package { name } => {
284 self.current_package = name.clone();
285 }
286 StmtKind::SubDecl { name, .. } => {
287 let fqn = if name.contains("::") {
288 name.clone()
289 } else {
290 format!("{}::{}", self.current_package, name)
291 };
292 self.declare_sub(name);
293 self.declare_sub(&fqn);
294 }
295 StmtKind::Use { module, imports } => {
296 self.declare_sub(module);
297 if module == "constant" {
302 self.collect_use_constant_names(imports);
303 }
304 }
305 StmtKind::Expression(e) => self.collect_required_subs_from_expr(e),
312 StmtKind::Block(b)
313 | StmtKind::StmtGroup(b)
314 | StmtKind::Begin(b)
315 | StmtKind::End(b)
316 | StmtKind::UnitCheck(b)
317 | StmtKind::Check(b)
318 | StmtKind::Init(b) => {
319 for s in b {
320 self.collect_declarations_stmt(s);
321 }
322 }
323 StmtKind::If {
324 body,
325 elsifs,
326 else_block,
327 ..
328 } => {
329 for s in body {
330 self.collect_declarations_stmt(s);
331 }
332 for (_, b) in elsifs {
333 for s in b {
334 self.collect_declarations_stmt(s);
335 }
336 }
337 if let Some(b) = else_block {
338 for s in b {
339 self.collect_declarations_stmt(s);
340 }
341 }
342 }
343 StmtKind::ClassDecl { def } => {
344 self.declare_sub(&def.name);
345 for m in &def.methods {
346 if m.is_static {
347 self.declare_sub(&format!("{}::{}", def.name, m.name));
348 }
349 }
350 for sf in &def.static_fields {
351 self.declare_sub(&format!("{}::{}", def.name, sf.name));
352 }
353 let mut fields: HashSet<String> = HashSet::new();
354 for f in &def.fields {
355 fields.insert(f.name.clone());
356 }
357 self.type_fields.insert(def.name.clone(), fields);
358 let mut methods: HashSet<String> = HashSet::new();
360 for m in &def.methods {
361 methods.insert(m.name.clone());
362 }
363 self.type_methods.insert(def.name.clone(), methods);
364 let mut parents = def.extends.clone();
370 parents.extend(def.implements.iter().cloned());
371 if !parents.is_empty() {
372 self.type_parents.insert(def.name.clone(), parents);
373 }
374 }
375 StmtKind::StructDecl { def } => {
376 self.declare_sub(&def.name);
377 let mut fields: HashSet<String> = HashSet::new();
378 for f in &def.fields {
379 fields.insert(f.name.clone());
380 }
381 self.type_fields.insert(def.name.clone(), fields);
382 let mut methods: HashSet<String> = HashSet::new();
383 for m in &def.methods {
384 methods.insert(m.name.clone());
385 }
386 self.type_methods.insert(def.name.clone(), methods);
387 }
388 StmtKind::EnumDecl { def } => {
389 self.declare_sub(&def.name);
390 for v in &def.variants {
391 self.declare_sub(&format!("{}::{}", def.name, v.name));
392 }
393 let mut fields: HashSet<String> = HashSet::new();
397 for v in &def.variants {
398 fields.insert(v.name.clone());
399 }
400 self.type_fields.insert(def.name.clone(), fields);
401 }
402 StmtKind::TraitDecl { def } => {
408 self.declare_sub(&def.name);
409 let mut methods: HashSet<String> = HashSet::new();
410 for m in &def.methods {
411 methods.insert(m.name.clone());
412 }
413 self.type_methods.insert(def.name.clone(), methods);
414 self.type_fields.entry(def.name.clone()).or_default();
417 }
418 _ => {}
419 }
420 }
421
422 fn collect_use_constant_names(&mut self, imports: &[Expr]) {
429 for imp in imports {
430 match &imp.kind {
431 ExprKind::List(items) => {
432 let mut i = 0;
433 while i + 1 < items.len() {
434 if let Some(name) = static_string_value(&items[i]) {
435 let fqn = format!("{}::{}", self.current_package, name);
436 self.declare_sub(&name);
437 self.declare_sub(&fqn);
438 }
439 i += 2;
440 }
441 }
442 ExprKind::HashRef(pairs) => {
443 for (k, _) in pairs {
444 if let Some(name) = static_string_value(k) {
445 let fqn = format!("{}::{}", self.current_package, name);
446 self.declare_sub(&name);
447 self.declare_sub(&fqn);
448 }
449 }
450 }
451 _ => {}
452 }
453 }
454 }
455
456 fn collect_required_subs_from_expr(&mut self, expr: &Expr) {
464 match &expr.kind {
465 ExprKind::Require(inner) => {
466 let Some(spec) = static_string_value(inner) else {
467 return;
468 };
469 self.follow_require(&spec);
470 }
471 ExprKind::PostfixIf { expr: inner, .. }
472 | ExprKind::PostfixUnless { expr: inner, .. } => {
473 self.collect_required_subs_from_expr(inner);
474 }
475 _ => {}
476 }
477 }
478
479 fn follow_require(&mut self, spec: &str) {
486 let spec = spec.trim();
487 if spec.is_empty() {
488 return;
489 }
490 if matches!(
493 spec,
494 "strict"
495 | "warnings"
496 | "utf8"
497 | "feature"
498 | "v5"
499 | "threads"
500 | "Thread::Pool"
501 | "Parallel::ForkManager"
502 ) {
503 return;
504 }
505 let Some(target) = self.resolve_require_path(spec) else {
506 return;
507 };
508 let canon = target.canonicalize().unwrap_or(target.clone());
509 if !self.seen_required_files.insert(canon.clone()) {
510 return; }
512 let Ok(src) = std::fs::read_to_string(&target) else {
513 return;
514 };
515 let file_str = target.to_string_lossy().into_owned();
516 let Ok(program) = crate::parse_module_with_file(&src, &file_str) else {
517 return;
518 };
519 let saved_pkg = std::mem::replace(&mut self.current_package, "main".to_string());
522 for stmt in &program.statements {
523 self.collect_declarations_stmt(stmt);
524 }
525 self.current_package = saved_pkg;
526 }
527
528 fn resolve_require_path(&self, spec: &str) -> Option<PathBuf> {
529 resolve_require_path_from_file(&self.file, spec)
530 }
531
532 fn analyze_stmt(&mut self, stmt: &Statement) {
533 match &stmt.kind {
534 StmtKind::Package { name } => {
535 self.current_package = name.clone();
536 }
537 StmtKind::My(decls)
538 | StmtKind::Our(decls)
539 | StmtKind::Local(decls)
540 | StmtKind::State(decls)
541 | StmtKind::MySync(decls)
542 | StmtKind::OurSync(decls) => {
543 let is_package_global =
548 matches!(stmt.kind, StmtKind::Our(_) | StmtKind::OurSync(_));
549 for d in decls {
550 match d.sigil {
551 Sigil::Scalar => {
552 self.declare_scalar(&d.name);
553 if is_package_global {
554 let q = format!("{}::{}", self.current_package, d.name);
555 self.declare_scalar(&q);
556 }
557 }
558 Sigil::Array => {
559 self.declare_array(&d.name);
560 if is_package_global {
561 let q = format!("{}::{}", self.current_package, d.name);
562 self.declare_array(&q);
563 }
564 }
565 Sigil::Hash => {
566 self.declare_hash(&d.name);
567 if is_package_global {
568 let q = format!("{}::{}", self.current_package, d.name);
569 self.declare_hash(&q);
570 }
571 }
572 Sigil::Typeglob => {}
573 }
574 if let Some(init) = &d.initializer {
575 if matches!(d.sigil, Sigil::Scalar) {
576 if let Some(ty) = self.infer_constructor_type(init) {
577 self.declare_scalar_type(&d.name, &ty);
578 }
579 }
580 self.analyze_expr(init);
581 }
582 }
583 }
584 StmtKind::Expression(e) => self.analyze_expr(e),
585 StmtKind::Return(Some(e)) => self.analyze_expr(e),
586 StmtKind::Return(None) => {}
587 StmtKind::If {
588 condition,
589 body,
590 elsifs,
591 else_block,
592 } => {
593 self.analyze_expr(condition);
594 self.push_scope();
595 self.analyze_block(body);
596 self.pop_scope();
597 for (cond, b) in elsifs {
598 self.analyze_expr(cond);
599 self.push_scope();
600 self.analyze_block(b);
601 self.pop_scope();
602 }
603 if let Some(b) = else_block {
604 self.push_scope();
605 self.analyze_block(b);
606 self.pop_scope();
607 }
608 }
609 StmtKind::Unless {
610 condition,
611 body,
612 else_block,
613 } => {
614 self.analyze_expr(condition);
615 self.push_scope();
616 self.analyze_block(body);
617 self.pop_scope();
618 if let Some(b) = else_block {
619 self.push_scope();
620 self.analyze_block(b);
621 self.pop_scope();
622 }
623 }
624 StmtKind::While {
625 condition,
626 body,
627 continue_block,
628 ..
629 }
630 | StmtKind::Until {
631 condition,
632 body,
633 continue_block,
634 ..
635 } => {
636 self.analyze_expr(condition);
637 self.push_scope();
638 self.analyze_block(body);
639 if let Some(cb) = continue_block {
640 self.analyze_block(cb);
641 }
642 self.pop_scope();
643 }
644 StmtKind::DoWhile { body, condition } => {
645 self.push_scope();
646 self.analyze_block(body);
647 self.pop_scope();
648 self.analyze_expr(condition);
649 }
650 StmtKind::For {
651 init,
652 condition,
653 step,
654 body,
655 continue_block,
656 ..
657 } => {
658 self.push_scope();
659 if let Some(i) = init {
660 self.analyze_stmt(i);
661 }
662 if let Some(c) = condition {
663 self.analyze_expr(c);
664 }
665 if let Some(s) = step {
666 self.analyze_expr(s);
667 }
668 self.analyze_block(body);
669 if let Some(cb) = continue_block {
670 self.analyze_block(cb);
671 }
672 self.pop_scope();
673 }
674 StmtKind::Foreach {
675 var,
676 list,
677 body,
678 continue_block,
679 ..
680 } => {
681 self.analyze_expr(list);
682 self.push_scope();
683 self.declare_scalar(var);
684 self.analyze_block(body);
685 if let Some(cb) = continue_block {
686 self.analyze_block(cb);
687 }
688 self.pop_scope();
689 }
690 StmtKind::SubDecl {
691 name, params, body, ..
692 } => {
693 let fqn = if name.contains("::") {
694 name.clone()
695 } else {
696 format!("{}::{}", self.current_package, name)
697 };
698 self.declare_sub(name);
699 self.declare_sub(&fqn);
700 self.push_scope();
701 for p in params {
702 self.declare_param(p);
703 }
704 self.analyze_block(body);
705 self.pop_scope();
706 }
707 StmtKind::Block(b)
708 | StmtKind::StmtGroup(b)
709 | StmtKind::Begin(b)
710 | StmtKind::End(b)
711 | StmtKind::UnitCheck(b)
712 | StmtKind::Check(b)
713 | StmtKind::Init(b)
714 | StmtKind::Continue(b) => {
715 self.push_scope();
716 self.analyze_block(b);
717 self.pop_scope();
718 }
719 StmtKind::TryCatch {
720 try_block,
721 catch_var,
722 catch_block,
723 finally_block,
724 } => {
725 self.push_scope();
726 self.analyze_block(try_block);
727 self.pop_scope();
728 self.push_scope();
729 self.declare_scalar(catch_var);
730 self.analyze_block(catch_block);
731 self.pop_scope();
732 if let Some(fb) = finally_block {
733 self.push_scope();
734 self.analyze_block(fb);
735 self.pop_scope();
736 }
737 }
738 StmtKind::EvalTimeout { body, .. } => {
739 self.push_scope();
740 self.analyze_block(body);
741 self.pop_scope();
742 }
743 StmtKind::Given { topic, body } => {
744 self.analyze_expr(topic);
745 self.push_scope();
746 self.analyze_block(body);
747 self.pop_scope();
748 }
749 StmtKind::When { cond, body } => {
750 self.analyze_expr(cond);
751 self.push_scope();
752 self.analyze_block(body);
753 self.pop_scope();
754 }
755 StmtKind::DefaultCase { body } => {
756 self.push_scope();
757 self.analyze_block(body);
758 self.pop_scope();
759 }
760 StmtKind::LocalExpr {
761 target,
762 initializer,
763 } => {
764 self.analyze_expr(target);
765 if let Some(init) = initializer {
766 self.analyze_expr(init);
767 }
768 }
769 StmtKind::Goto { target } => {
770 self.analyze_expr(target);
771 }
772 StmtKind::Tie { class, args, .. } => {
773 self.analyze_expr(class);
774 for a in args {
775 self.analyze_expr(a);
776 }
777 }
778 StmtKind::Use { imports, .. } | StmtKind::No { imports, .. } => {
779 for e in imports {
780 self.analyze_expr(e);
781 }
782 }
783 StmtKind::StructDecl { def } => {
784 let prev = self.current_class.take();
788 self.current_class = Some(def.name.clone());
789 for m in &def.methods {
790 self.push_scope();
791 self.declare_scalar("self");
792 for p in &m.params {
793 self.declare_param(p);
794 }
795 self.analyze_block(&m.body);
796 self.pop_scope();
797 }
798 self.current_class = prev;
799 }
800 StmtKind::ClassDecl { def } => {
801 let prev = self.current_class.take();
802 self.current_class = Some(def.name.clone());
803 for m in &def.methods {
804 if let Some(body) = &m.body {
805 self.push_scope();
806 self.declare_scalar("self");
807 for p in &m.params {
808 self.declare_param(p);
809 }
810 self.analyze_block(body);
811 self.pop_scope();
812 }
813 }
814 self.current_class = prev;
815 }
816 StmtKind::EnumDecl { .. }
817 | StmtKind::TraitDecl { .. }
818 | StmtKind::FormatDecl { .. }
819 | StmtKind::AdviceDecl { .. }
820 | StmtKind::UsePerlVersion { .. }
821 | StmtKind::UseOverload { .. }
822 | StmtKind::Last(_)
823 | StmtKind::Next(_)
824 | StmtKind::Redo(_)
825 | StmtKind::Empty => {}
826 }
827 }
828
829 fn declare_param(&mut self, param: &SubSigParam) {
830 match param {
831 SubSigParam::Scalar(name, _, _) => self.declare_scalar(name),
832 SubSigParam::Array(name, _) => self.declare_array(name),
833 SubSigParam::Hash(name, _) => self.declare_hash(name),
834 SubSigParam::ArrayDestruct(elems) => {
835 for e in elems {
836 match e {
837 MatchArrayElem::CaptureScalar(n) => self.declare_scalar(n),
838 MatchArrayElem::RestBind(n) => self.declare_array(n),
839 _ => {}
840 }
841 }
842 }
843 SubSigParam::HashDestruct(pairs) => {
844 for (_, name) in pairs {
845 self.declare_scalar(name);
846 }
847 }
848 }
849 }
850
851 fn analyze_block(&mut self, block: &Block) {
852 for stmt in block {
853 self.analyze_stmt(stmt);
854 }
855 }
856
857 fn check_constructor_keys(&mut self, type_name: &str, args: &[Expr], call_line: usize) {
863 let bare_tail = type_name.rsplit("::").next().unwrap_or(type_name);
864 let resolved = if self.type_fields.contains_key(type_name) {
867 type_name.to_string()
868 } else if self.type_fields.contains_key(bare_tail) {
869 bare_tail.to_string()
870 } else {
871 return;
872 };
873 let mut all_fields: HashSet<String> = HashSet::new();
877 let mut seen: HashSet<String> = HashSet::new();
878 let mut queue: Vec<String> = vec![resolved.clone()];
879 while let Some(c) = queue.pop() {
880 if !seen.insert(c.clone()) {
881 continue;
882 }
883 if let Some(fs) = self.type_fields.get(&c) {
884 all_fields.extend(fs.iter().cloned());
885 }
886 if let Some(parents) = self.type_parents.get(&c) {
887 queue.extend(parents.iter().cloned());
888 }
889 }
890 let first_arg_is_field = args.first().is_some_and(|a| match &a.kind {
897 ExprKind::String(s) | ExprKind::Bareword(s) => all_fields.contains(s),
898 _ => false,
899 });
900 let looks_keyed = args.len().is_multiple_of(2)
901 && first_arg_is_field
902 && (0..args.len()).step_by(2).all(|i| {
903 matches!(&args[i].kind, ExprKind::String(_))
904 || matches!(&args[i].kind, ExprKind::Bareword(s) if !s.contains("::"))
905 });
906 if !looks_keyed {
907 return;
908 }
909 let mut i = 0;
910 while i < args.len() {
911 let key_name = match &args[i].kind {
912 ExprKind::String(s) => Some(s.clone()),
913 ExprKind::Bareword(s) => Some(s.clone()),
914 _ => None,
915 };
916 if let Some(name) = key_name {
917 if !all_fields.contains(&name) {
918 let line = if args[i].line > 0 {
919 args[i].line
920 } else {
921 call_line
922 };
923 let bare_for_msg = type_name.rsplit("::").next().unwrap_or(type_name);
924 self.error(
925 ErrorKind::UndefinedSubroutine,
926 format!(
927 "Unknown field `{name}` in constructor call to `{bare_for_msg}` — \
928 declared fields: {}",
929 {
930 let mut v: Vec<&String> = all_fields.iter().collect();
931 v.sort();
932 v.into_iter()
933 .map(String::as_str)
934 .collect::<Vec<_>>()
935 .join(", ")
936 }
937 ),
938 line,
939 );
940 }
941 }
942 i += 2;
943 }
944 }
945
946 fn method_resolves_in_hierarchy(&self, class_name: &str, method: &str) -> bool {
950 if matches!(
963 method,
964 "isa"
965 | "can"
966 | "DOES"
967 | "does"
968 | "VERSION"
969 | "new"
970 | "BUILD"
971 | "DESTROY"
972 | "destroy"
973 | "clone"
974 | "with"
975 | "to_hash"
976 | "to_hash_rec"
977 | "to_hash_deep"
978 | "fields"
979 | "methods"
980 | "superclass"
981 ) {
982 return true;
983 }
984 let mut seen: HashSet<String> = HashSet::new();
985 let mut queue: Vec<String> = vec![class_name.to_string()];
986 while let Some(c) = queue.pop() {
987 if !seen.insert(c.clone()) {
988 continue;
989 }
990 if self.type_fields.get(&c).is_some_and(|s| s.contains(method)) {
991 return true;
992 }
993 if self
994 .type_methods
995 .get(&c)
996 .is_some_and(|s| s.contains(method))
997 {
998 return true;
999 }
1000 if let Some(parents) = self.type_parents.get(&c) {
1001 queue.extend(parents.iter().cloned());
1002 }
1003 }
1004 false
1005 }
1006
1007 fn collect_hierarchy_members(&self, class_name: &str) -> Vec<String> {
1011 let mut seen: HashSet<String> = HashSet::new();
1012 let mut out: HashSet<String> = HashSet::new();
1013 let mut queue: Vec<String> = vec![class_name.to_string()];
1014 while let Some(c) = queue.pop() {
1015 if !seen.insert(c.clone()) {
1016 continue;
1017 }
1018 if let Some(fs) = self.type_fields.get(&c) {
1019 out.extend(fs.iter().cloned());
1020 }
1021 if let Some(ms) = self.type_methods.get(&c) {
1022 out.extend(ms.iter().cloned());
1023 }
1024 if let Some(parents) = self.type_parents.get(&c) {
1025 queue.extend(parents.iter().cloned());
1026 }
1027 }
1028 let mut v: Vec<String> = out.into_iter().collect();
1029 v.sort();
1030 v
1031 }
1032
1033 fn analyze_expr(&mut self, expr: &Expr) {
1034 match &expr.kind {
1035 ExprKind::ScalarVar(name)
1041 if self.strict_vars
1042 && name.len() > 1
1043 && name.starts_with('#')
1044 && !self.is_array_defined(&name[1..]) =>
1045 {
1046 self.error(
1047 ErrorKind::UndefinedVariable,
1048 format!(
1049 "Global symbol \"@{}\" requires explicit package name",
1050 &name[1..]
1051 ),
1052 expr.line,
1053 );
1054 }
1055 ExprKind::ScalarVar(name) if name.starts_with('#') => {
1056 }
1058 ExprKind::ScalarVar(name) if self.strict_vars && !self.is_scalar_defined(name) => {
1059 self.error(
1060 ErrorKind::UndefinedVariable,
1061 format!("Global symbol \"${}\" requires explicit package name", name),
1062 expr.line,
1063 );
1064 }
1065 ExprKind::ArrayVar(name) if self.strict_vars && !self.is_array_defined(name) => {
1066 self.error(
1067 ErrorKind::UndefinedVariable,
1068 format!("Global symbol \"@{}\" requires explicit package name", name),
1069 expr.line,
1070 );
1071 }
1072 ExprKind::HashVar(name) if self.strict_vars && !self.is_hash_defined(name) => {
1073 self.error(
1074 ErrorKind::UndefinedVariable,
1075 format!("Global symbol \"%{}\" requires explicit package name", name),
1076 expr.line,
1077 );
1078 }
1079 ExprKind::ArrayElement { array, index } => {
1080 if self.strict_vars
1081 && !self.is_array_defined(array)
1082 && !self.is_scalar_defined(array)
1083 {
1084 self.error(
1085 ErrorKind::UndefinedVariable,
1086 format!(
1087 "Global symbol \"@{}\" requires explicit package name",
1088 array
1089 ),
1090 expr.line,
1091 );
1092 }
1093 self.analyze_expr(index);
1094 }
1095 ExprKind::HashElement { hash, key } => {
1096 if self.strict_vars && !self.is_hash_defined(hash) && !self.is_scalar_defined(hash)
1097 {
1098 self.error(
1099 ErrorKind::UndefinedVariable,
1100 format!("Global symbol \"%{}\" requires explicit package name", hash),
1101 expr.line,
1102 );
1103 }
1104 self.analyze_expr(key);
1105 }
1106 ExprKind::ArraySlice { array, indices } => {
1107 if self.strict_vars && !self.is_array_defined(array) {
1108 self.error(
1109 ErrorKind::UndefinedVariable,
1110 format!(
1111 "Global symbol \"@{}\" requires explicit package name",
1112 array
1113 ),
1114 expr.line,
1115 );
1116 }
1117 for i in indices {
1118 self.analyze_expr(i);
1119 }
1120 }
1121 ExprKind::HashSlice { hash, keys } => {
1122 if self.strict_vars && !self.is_hash_defined(hash) {
1123 self.error(
1124 ErrorKind::UndefinedVariable,
1125 format!("Global symbol \"%{}\" requires explicit package name", hash),
1126 expr.line,
1127 );
1128 }
1129 for k in keys {
1130 self.analyze_expr(k);
1131 }
1132 }
1133 ExprKind::FuncCall { name, args } => {
1134 if !self.is_sub_defined(name) {
1135 self.error(
1136 ErrorKind::UndefinedSubroutine,
1137 format!("Undefined subroutine &{}", name),
1138 expr.line,
1139 );
1140 }
1141 self.check_constructor_keys(name, args, expr.line);
1144 for a in args {
1145 self.analyze_expr(a);
1146 }
1147 }
1148 ExprKind::MethodCall {
1149 object,
1150 method,
1151 args,
1152 ..
1153 } => {
1154 self.analyze_expr(object);
1155 if let ExprKind::Bareword(n) = &object.kind {
1157 self.check_constructor_keys(n, args, expr.line);
1158 if !self.type_fields.is_empty()
1165 && !self.type_fields.contains_key(n)
1166 && !self.is_sub_defined(n)
1167 {
1168 let bare = n.rsplit("::").next().unwrap_or(n);
1169 if !bare.is_empty()
1170 && bare.chars().next().is_some_and(|c| c.is_ascii_uppercase())
1171 {
1172 self.error(
1173 ErrorKind::UndefinedSubroutine,
1174 format!(
1175 "Unknown class `{n}` — `{n}->{method}` calls a constructor on a type that isn't declared in this file or its `require`d libs"
1176 ),
1177 expr.line,
1178 );
1179 }
1180 }
1181 }
1182 if let ExprKind::ScalarVar(name) = &object.kind {
1188 if name != "self" {
1189 if let Some(class_name) =
1190 self.resolve_scalar_type(name).map(|s| s.to_string())
1191 {
1192 if !self.method_resolves_in_hierarchy(&class_name, method) {
1193 let suggestions =
1194 self.collect_hierarchy_members(&class_name);
1195 let avail = if suggestions.is_empty() {
1196 "(no fields or methods declared)".to_string()
1197 } else {
1198 suggestions.join(", ")
1199 };
1200 self.error(
1201 ErrorKind::UndefinedSubroutine,
1202 format!(
1203 "`${name}->{method}` — no field or method `{method}` on `{class_name}`; available: {avail}",
1204 ),
1205 expr.line,
1206 );
1207 }
1208 }
1209 }
1210 }
1211 if let ExprKind::ScalarVar(name) = &object.kind {
1214 if name == "self" {
1215 if let Some(class_name) = self.current_class.clone() {
1216 if !self.method_resolves_in_hierarchy(&class_name, method) {
1220 let suggestions =
1221 self.collect_hierarchy_members(&class_name);
1222 let avail = if suggestions.is_empty() {
1223 "(no fields or methods declared)".to_string()
1224 } else {
1225 suggestions.join(", ")
1226 };
1227 self.error(
1228 ErrorKind::UndefinedSubroutine,
1229 format!(
1230 "`$self->{method}` — no field or method `{method}` on `{class_name}`; available: {avail}",
1231 ),
1232 expr.line,
1233 );
1234 }
1235 }
1236 }
1237 }
1238 for a in args {
1239 self.analyze_expr(a);
1240 }
1241 }
1242 ExprKind::IndirectCall { target, args, .. } => {
1243 self.analyze_expr(target);
1244 for a in args {
1245 self.analyze_expr(a);
1246 }
1247 }
1248 ExprKind::BinOp { left, right, .. } => {
1249 self.analyze_expr(left);
1250 self.analyze_expr(right);
1251 }
1252 ExprKind::UnaryOp { expr: e, .. } => {
1253 self.analyze_expr(e);
1254 }
1255 ExprKind::PostfixOp { expr: e, .. } => {
1256 self.analyze_expr(e);
1257 }
1258 ExprKind::Assign { target, value } => {
1259 if let ExprKind::ScalarVar(name) = &target.kind {
1260 self.declare_scalar(name);
1261 } else if let ExprKind::ArrayVar(name) = &target.kind {
1262 self.declare_array(name);
1263 } else if let ExprKind::HashVar(name) = &target.kind {
1264 self.declare_hash(name);
1265 } else {
1266 self.analyze_expr(target);
1267 }
1268 self.analyze_expr(value);
1269 }
1270 ExprKind::CompoundAssign { target, value, .. } => {
1271 self.analyze_expr(target);
1272 self.analyze_expr(value);
1273 }
1274 ExprKind::Ternary {
1275 condition,
1276 then_expr,
1277 else_expr,
1278 } => {
1279 self.analyze_expr(condition);
1280 self.analyze_expr(then_expr);
1281 self.analyze_expr(else_expr);
1282 }
1283 ExprKind::List(exprs) | ExprKind::ArrayRef(exprs) => {
1284 for e in exprs {
1285 self.analyze_expr(e);
1286 }
1287 }
1288 ExprKind::HashRef(pairs) => {
1289 for (k, v) in pairs {
1290 self.analyze_expr(k);
1291 self.analyze_expr(v);
1292 }
1293 }
1294 ExprKind::CodeRef { params, body } => {
1295 self.push_scope();
1296 for p in params {
1297 self.declare_param(p);
1298 }
1299 self.analyze_block(body);
1300 self.pop_scope();
1301 }
1302 ExprKind::ScalarRef(e)
1303 | ExprKind::Deref { expr: e, .. }
1304 | ExprKind::Defined(e)
1305 | ExprKind::Delete(e) => {
1306 self.analyze_expr(e);
1307 }
1308 ExprKind::Exists(e)
1309 if !matches!(
1317 e.kind,
1318 ExprKind::SubroutineCodeRef(_) | ExprKind::SubroutineRef(_)
1319 ) => {
1320 self.analyze_expr(e);
1321 }
1322 ExprKind::ArrowDeref { expr, index, kind } => {
1323 self.analyze_expr(expr);
1324 if *kind != DerefKind::Call {
1325 self.analyze_expr(index);
1326 }
1327 }
1328 ExprKind::Range { from, to, step, .. } => {
1329 self.analyze_expr(from);
1330 self.analyze_expr(to);
1331 if let Some(s) = step {
1332 self.analyze_expr(s);
1333 }
1334 }
1335 ExprKind::SliceRange { from, to, step } => {
1336 if let Some(f) = from {
1337 self.analyze_expr(f);
1338 }
1339 if let Some(t) = to {
1340 self.analyze_expr(t);
1341 }
1342 if let Some(s) = step {
1343 self.analyze_expr(s);
1344 }
1345 }
1346 ExprKind::InterpolatedString(parts) => {
1347 for part in parts {
1359 match part {
1360 StringPart::Expr(e) => {
1361 match &e.kind {
1367 ExprKind::ScalarVar(_)
1368 | ExprKind::ArrayVar(_)
1369 | ExprKind::HashVar(_) => {}
1370 _ => self.analyze_expr(e),
1371 }
1372 }
1373 StringPart::ScalarVar(_)
1374 | StringPart::ArrayVar(_)
1375 | StringPart::Literal(_) => {}
1376 }
1377 }
1378 }
1379 ExprKind::Regex(_, _)
1380 | ExprKind::Substitution { .. }
1381 | ExprKind::Transliterate { .. }
1382 | ExprKind::Match { .. } => {}
1383 ExprKind::HashSliceDeref { container, keys } => {
1384 self.analyze_expr(container);
1385 for k in keys {
1386 self.analyze_expr(k);
1387 }
1388 }
1389 ExprKind::AnonymousListSlice { source, indices } => {
1390 self.analyze_expr(source);
1391 for i in indices {
1392 self.analyze_expr(i);
1393 }
1394 }
1395 ExprKind::SubroutineRef(name) | ExprKind::SubroutineCodeRef(name)
1396 if !self.is_sub_defined(name) =>
1397 {
1398 self.error(
1399 ErrorKind::UndefinedSubroutine,
1400 format!("Undefined subroutine &{}", name),
1401 expr.line,
1402 );
1403 }
1404 ExprKind::DynamicSubCodeRef(e) => self.analyze_expr(e),
1405 ExprKind::PostfixIf { expr, condition }
1406 | ExprKind::PostfixUnless { expr, condition }
1407 | ExprKind::PostfixWhile { expr, condition }
1408 | ExprKind::PostfixUntil { expr, condition } => {
1409 self.analyze_expr(expr);
1410 self.analyze_expr(condition);
1411 }
1412 ExprKind::PostfixForeach { expr, list } => {
1413 self.analyze_expr(list);
1414 self.analyze_expr(expr);
1415 }
1416 ExprKind::Do(e) | ExprKind::Eval(e) => {
1417 self.analyze_expr(e);
1418 }
1419 ExprKind::Caller(Some(e)) => {
1420 self.analyze_expr(e);
1421 }
1422 ExprKind::Length(e) => {
1423 self.analyze_expr(e);
1424 }
1425 ExprKind::Print { args, .. }
1426 | ExprKind::Say { args, .. }
1427 | ExprKind::Printf { args, .. } => {
1428 for a in args {
1429 self.analyze_expr(a);
1430 }
1431 }
1432 ExprKind::Die(args)
1433 | ExprKind::Warn(args)
1434 | ExprKind::Unlink(args)
1435 | ExprKind::Chmod(args)
1436 | ExprKind::System(args)
1437 | ExprKind::Exec(args) => {
1438 for a in args {
1439 self.analyze_expr(a);
1440 }
1441 }
1442 ExprKind::Push { array, values } | ExprKind::Unshift { array, values } => {
1443 self.analyze_expr(array);
1444 for v in values {
1445 self.analyze_expr(v);
1446 }
1447 }
1448 ExprKind::Splice {
1449 array,
1450 offset,
1451 length,
1452 replacement,
1453 } => {
1454 self.analyze_expr(array);
1455 if let Some(o) = offset {
1456 self.analyze_expr(o);
1457 }
1458 if let Some(l) = length {
1459 self.analyze_expr(l);
1460 }
1461 for r in replacement {
1462 self.analyze_expr(r);
1463 }
1464 }
1465 ExprKind::MapExpr { block, list, .. } | ExprKind::GrepExpr { block, list, .. } => {
1466 self.push_scope();
1467 self.analyze_block(block);
1468 self.pop_scope();
1469 self.analyze_expr(list);
1470 }
1471 ExprKind::SortExpr { list, .. } => {
1472 self.analyze_expr(list);
1473 }
1474 ExprKind::Open { handle, mode, file } => {
1475 if let ExprKind::OpenMyHandle { name } = &handle.kind {
1479 self.declare_scalar(name);
1480 } else {
1481 self.analyze_expr(handle);
1482 }
1483 self.analyze_expr(mode);
1484 if let Some(f) = file {
1485 self.analyze_expr(f);
1486 }
1487 }
1488 ExprKind::Close(e)
1489 | ExprKind::Pop(e)
1490 | ExprKind::Shift(e)
1491 | ExprKind::Keys(e)
1492 | ExprKind::Values(e)
1493 | ExprKind::Each(e)
1494 | ExprKind::Chdir(e)
1495 | ExprKind::Require(e)
1496 | ExprKind::Ref(e)
1497 | ExprKind::Chomp(e)
1498 | ExprKind::Chop(e)
1499 | ExprKind::Lc(e)
1500 | ExprKind::Uc(e)
1501 | ExprKind::Lcfirst(e)
1502 | ExprKind::Ucfirst(e)
1503 | ExprKind::Abs(e)
1504 | ExprKind::Int(e)
1505 | ExprKind::Sqrt(e)
1506 | ExprKind::Sin(e)
1507 | ExprKind::Cos(e)
1508 | ExprKind::Exp(e)
1509 | ExprKind::Log(e)
1510 | ExprKind::Chr(e)
1511 | ExprKind::Ord(e)
1512 | ExprKind::Hex(e)
1513 | ExprKind::Oct(e)
1514 | ExprKind::Readlink(e)
1515 | ExprKind::Readdir(e)
1516 | ExprKind::Closedir(e)
1517 | ExprKind::Rewinddir(e)
1518 | ExprKind::Telldir(e) => {
1519 self.analyze_expr(e);
1520 }
1521 ExprKind::Exit(Some(e)) | ExprKind::Rand(Some(e)) | ExprKind::Eof(Some(e)) => {
1522 self.analyze_expr(e);
1523 }
1524 ExprKind::Mkdir { path, mode } => {
1525 self.analyze_expr(path);
1526 if let Some(m) = mode {
1527 self.analyze_expr(m);
1528 }
1529 }
1530 ExprKind::Rename { old, new }
1531 | ExprKind::Link { old, new }
1532 | ExprKind::Symlink { old, new } => {
1533 self.analyze_expr(old);
1534 self.analyze_expr(new);
1535 }
1536 ExprKind::Chown(files) => {
1537 for f in files {
1538 self.analyze_expr(f);
1539 }
1540 }
1541 ExprKind::Substr {
1542 string,
1543 offset,
1544 length,
1545 replacement,
1546 } => {
1547 self.analyze_expr(string);
1548 self.analyze_expr(offset);
1549 if let Some(l) = length {
1550 self.analyze_expr(l);
1551 }
1552 if let Some(r) = replacement {
1553 self.analyze_expr(r);
1554 }
1555 }
1556 ExprKind::Index {
1557 string,
1558 substr,
1559 position,
1560 }
1561 | ExprKind::Rindex {
1562 string,
1563 substr,
1564 position,
1565 } => {
1566 self.analyze_expr(string);
1567 self.analyze_expr(substr);
1568 if let Some(p) = position {
1569 self.analyze_expr(p);
1570 }
1571 }
1572 ExprKind::Sprintf { format, args } => {
1573 self.analyze_expr(format);
1574 for a in args {
1575 self.analyze_expr(a);
1576 }
1577 }
1578 ExprKind::Bless { ref_expr, class } => {
1579 self.analyze_expr(ref_expr);
1580 if let Some(c) = class {
1581 self.analyze_expr(c);
1582 }
1583 }
1584 ExprKind::AlgebraicMatch { subject, arms } => {
1585 self.analyze_expr(subject);
1586 for arm in arms {
1587 self.check_match_pattern(&arm.pattern, expr.line);
1588 if let Some(g) = &arm.guard {
1589 self.analyze_expr(g);
1590 }
1591 self.analyze_expr(&arm.body);
1592 }
1593 }
1594 _ => {}
1595 }
1596 }
1597
1598 fn check_match_pattern(&mut self, pat: &crate::ast::MatchPattern, line: usize) {
1606 use crate::ast::{MatchArrayElem, MatchHashPair, MatchPattern};
1607 match pat {
1608 MatchPattern::Value(e) => {
1609 if let ExprKind::String(s) = &e.kind {
1610 self.check_qualified_variant_string(s, line);
1611 }
1612 self.analyze_expr(e);
1613 }
1614 MatchPattern::Array(elems) => {
1615 for el in elems {
1616 if let MatchArrayElem::Expr(e) = el {
1617 self.analyze_expr(e);
1618 }
1619 }
1620 }
1621 MatchPattern::Hash(pairs) => {
1622 for p in pairs {
1623 match p {
1624 MatchHashPair::KeyOnly { key } => self.analyze_expr(key),
1625 MatchHashPair::Capture { key, .. } => self.analyze_expr(key),
1626 }
1627 }
1628 }
1629 MatchPattern::Any | MatchPattern::Regex { .. } | MatchPattern::OptionSome(_) => {}
1630 }
1631 }
1632
1633 fn check_qualified_variant_string(&mut self, s: &str, line: usize) {
1639 let Some(idx) = s.rfind("::") else { return };
1640 let type_name = &s[..idx];
1641 let variant = &s[idx + 2..];
1642 if type_name.is_empty() || variant.is_empty() {
1643 return;
1644 }
1645 let type_bare = type_name.rsplit("::").next().unwrap_or(type_name);
1646 let known = self
1647 .type_fields
1648 .get(type_name)
1649 .or_else(|| self.type_fields.get(type_bare));
1650 let Some(variants) = known else { return };
1651 if variants.contains(variant) {
1652 return;
1653 }
1654 let mut available: Vec<&str> = variants.iter().map(String::as_str).collect();
1655 available.sort();
1656 let avail = if available.is_empty() {
1657 "(no variants declared)".to_string()
1658 } else {
1659 available.join(", ")
1660 };
1661 self.error(
1662 ErrorKind::UndefinedSubroutine,
1663 format!(
1664 "`{type_name}::{variant}` — no variant `{variant}` on `{type_name}`; available: {avail}"
1665 ),
1666 line,
1667 );
1668 }
1669}
1670
1671fn is_special_var(name: &str) -> bool {
1672 if name.len() == 1 {
1673 return true;
1674 }
1675 if name.starts_with('^') {
1681 return true;
1682 }
1683 if name == "$$" {
1688 return true;
1689 }
1690 matches!(
1691 name,
1692 "ARGV"
1693 | "ENV"
1694 | "SIG"
1695 | "INC"
1696 | "AUTOLOAD"
1697 | "STDERR"
1698 | "STDIN"
1699 | "STDOUT"
1700 | "DATA"
1701 | "UNIVERSAL"
1702 | "VERSION"
1703 | "ISA"
1704 | "EXPORT"
1705 | "EXPORT_OK"
1706 | "INTERCEPT_NAME"
1711 | "INTERCEPT_ARGS"
1712 | "INTERCEPT_RESULT"
1713 | "INTERCEPT_MS"
1714 | "INTERCEPT_US"
1715 | "stryke::VERSION"
1718 )
1719}
1720
1721fn is_topic_var(name: &str) -> bool {
1729 if !name.starts_with('_') {
1739 return false;
1740 }
1741 let rest = &name[1..];
1742 if rest.is_empty() {
1743 return true; }
1745 let bytes = rest.as_bytes();
1746 let mut i = 0;
1747 while i < bytes.len() && bytes[i].is_ascii_digit() {
1749 i += 1;
1750 }
1751 let digits_consumed = i;
1752 if i == bytes.len() {
1753 return digits_consumed > 0;
1755 }
1756 if bytes[i] != b'<' {
1758 return false;
1759 }
1760 while i < bytes.len() && bytes[i] == b'<' {
1761 i += 1;
1762 }
1763 while i < bytes.len() && bytes[i].is_ascii_digit() {
1765 i += 1;
1766 }
1767 i == bytes.len()
1768}
1769
1770pub fn resolve_require_path_from_file(file: &str, spec: &str) -> Option<PathBuf> {
1779 let p = Path::new(spec);
1780 if p.is_absolute() {
1781 return p.exists().then(|| p.to_path_buf());
1782 }
1783 let file_dir = Path::new(file)
1784 .parent()
1785 .map(Path::to_path_buf)
1786 .unwrap_or_else(|| PathBuf::from("."));
1787 let project_root = find_project_root(&file_dir);
1795 let candidate_via_root = if spec.starts_with("./") || spec.starts_with("../") {
1798 project_root.join(spec)
1799 } else if spec.contains("::") {
1800 let relpath: PathBuf = PathBuf::from(spec.replace("::", "/")).with_extension("pm");
1801 project_root.join("lib").join(relpath)
1802 } else {
1803 project_root.join(spec)
1804 };
1805 if candidate_via_root.exists() {
1806 return Some(candidate_via_root);
1807 }
1808 if spec.starts_with("./") || spec.starts_with("../") {
1811 let direct = file_dir.join(spec);
1812 if direct.exists() {
1813 return Some(direct);
1814 }
1815 }
1816 None
1817}
1818
1819fn find_project_root(start_dir: &Path) -> PathBuf {
1824 let mut cur = start_dir.to_path_buf();
1825 for _ in 0..16 {
1826 if cur.join("lib").is_dir() {
1828 return cur;
1829 }
1830 match cur.parent() {
1831 Some(p) if p != cur => cur = p.to_path_buf(),
1832 _ => break,
1833 }
1834 }
1835 match start_dir.file_name().and_then(|s| s.to_str()) {
1836 Some("t") | Some("tests") | Some("test") | Some("spec") | Some("xt") => start_dir
1837 .parent()
1838 .map(Path::to_path_buf)
1839 .unwrap_or_else(|| start_dir.to_path_buf()),
1840 _ => start_dir.to_path_buf(),
1841 }
1842}
1843
1844pub fn static_string_value(e: &Expr) -> Option<String> {
1848 match &e.kind {
1849 ExprKind::String(s) => Some(s.clone()),
1850 ExprKind::Bareword(s) => Some(s.clone()),
1851 ExprKind::InterpolatedString(parts) => {
1852 let mut out = String::new();
1854 for part in parts {
1855 match part {
1856 StringPart::Literal(s) => out.push_str(s),
1857 _ => return None,
1858 }
1859 }
1860 Some(out)
1861 }
1862 _ => None,
1863 }
1864}
1865
1866pub fn analyze_program(program: &Program, file: &str) -> StrykeResult<()> {
1867 StaticAnalyzer::new(file).analyze(program)
1868}
1869
1870pub fn analyze_program_with_strict(
1875 program: &Program,
1876 file: &str,
1877 strict_vars: bool,
1878) -> StrykeResult<()> {
1879 StaticAnalyzer::with_strict_vars(file, strict_vars).analyze(program)
1880}
1881
1882#[cfg(test)]
1883mod tests {
1884 use super::*;
1885 use crate::parse_with_file;
1886
1887 fn lint(code: &str) -> StrykeResult<()> {
1894 let prog = parse_with_file(code, "test.stk").expect("parse");
1895 analyze_program_with_strict(&prog, "test.stk", true)
1896 }
1897
1898 #[test]
1899 fn undefined_scalar_detected() {
1900 let r = lint("p $undefined");
1901 assert!(r.is_err());
1902 let e = r.unwrap_err();
1903 assert_eq!(e.kind, ErrorKind::UndefinedVariable);
1904 assert!(e.message.contains("$undefined"));
1905 }
1906
1907 #[test]
1908 fn defined_scalar_ok() {
1909 assert!(lint("my $x = 1; p $x").is_ok());
1910 }
1911
1912 #[test]
1913 fn undefined_sub_detected() {
1914 let r = lint("nonexistent_function()");
1915 assert!(r.is_err());
1916 let e = r.unwrap_err();
1917 assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
1918 assert!(e.message.contains("nonexistent_function"));
1919 }
1920
1921 #[test]
1922 fn defined_sub_ok() {
1923 assert!(lint("fn foo { 1 } foo()").is_ok());
1924 }
1925
1926 #[test]
1927 fn builtin_sub_ok() {
1928 assert!(lint("p 'hello'").is_ok());
1929 assert!(lint("print 'hello'").is_ok());
1930 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
1931 }
1932
1933 #[test]
1934 fn special_vars_ok() {
1935 assert!(lint("p $_").is_ok());
1936 assert!(lint("p @_").is_ok());
1937 assert!(lint("p $a <=> $b").is_ok());
1938 }
1939
1940 #[test]
1941 fn foreach_var_in_scope() {
1942 assert!(lint("foreach my $i (1..3) { p $i; }").is_ok());
1943 }
1944
1945 #[test]
1946 fn sub_params_in_scope() {
1947 assert!(lint("fn foo($x) { p $x; } foo(1)").is_ok());
1948 }
1949
1950 #[test]
1951 fn assignment_declares_var() {
1952 assert!(lint("$x = 1; p $x").is_ok());
1953 }
1954
1955 #[test]
1956 fn builtin_inc_ok() {
1957 assert!(lint("my $x = 1; inc($x)").is_ok());
1958 }
1959
1960 #[test]
1961 fn builtin_dec_ok() {
1962 assert!(lint("my $x = 1; dec($x)").is_ok());
1963 }
1964
1965 #[test]
1966 fn builtin_rev_ok() {
1967 assert!(lint("my $s = rev 'hello'").is_ok());
1968 }
1969
1970 #[test]
1971 fn builtin_p_alias_for_say_ok() {
1972 assert!(lint("p 'hello'").is_ok());
1973 }
1974
1975 #[test]
1976 fn builtin_t_thread_ok() {
1977 assert!(lint("t 1 inc inc").is_ok());
1978 }
1979
1980 #[test]
1981 fn thread_with_undefined_var_detected() {
1982 let r = lint("t $undefined inc");
1983 assert!(r.is_err());
1984 }
1985
1986 #[test]
1987 fn try_catch_var_in_scope() {
1988 assert!(lint("try { die 'err'; } catch ($e) { p $e; }").is_ok());
1989 }
1990
1991 #[test]
1992 fn interpolated_string_undefined_var() {
1993 assert!(
2000 lint(r#"p "hello $undefined""#).is_ok(),
2001 "undefined scalar inside string-interp must NOT flag",
2002 );
2003 assert!(lint(r#"use strict; p $undefined"#).is_err());
2005 }
2006
2007 #[test]
2008 fn interpolated_string_defined_var_ok() {
2009 assert!(lint(r#"my $x = 1; p "hello $x""#).is_ok());
2010 }
2011
2012 #[test]
2013 fn coderef_params_in_scope() {
2014 assert!(lint("my $f = fn ($x) { p $x; }; $f->(1)").is_ok());
2015 }
2016
2017 #[test]
2018 fn nested_sub_scope() {
2019 assert!(lint("fn wrap { my $x = 1; fn inner { p $x; } }").is_ok());
2020 }
2021
2022 #[test]
2023 fn hash_element_access_ok() {
2024 assert!(lint("my %h = (a => 1); p $h{a}").is_ok());
2025 }
2026
2027 #[test]
2028 fn array_element_access_ok() {
2029 assert!(lint("my @a = (1, 2, 3); p $a[0]").is_ok());
2030 }
2031
2032 #[test]
2033 fn undefined_hash_detected() {
2034 let r = lint("p $undefined_hash{key}");
2035 assert!(r.is_err());
2036 }
2037
2038 #[test]
2039 fn undefined_array_detected() {
2040 let r = lint("p $undefined_array[0]");
2041 assert!(r.is_err());
2042 }
2043
2044 #[test]
2045 fn map_with_topic_ok() {
2046 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
2047 }
2048
2049 #[test]
2050 fn grep_with_topic_ok() {
2051 assert!(lint("my @x = grep { $_ > 1 } 1..3").is_ok());
2052 }
2053
2054 #[test]
2055 fn sort_with_ab_ok() {
2056 assert!(lint("my @x = sort { $a <=> $b } 1..3").is_ok());
2057 }
2058
2059 #[test]
2060 fn ternary_undefined_var_detected() {
2061 let r = lint("my $x = $undefined ? 1 : 0");
2062 assert!(r.is_err());
2063 }
2064
2065 #[test]
2066 fn binop_undefined_var_detected() {
2067 let r = lint("my $x = 1 + $undefined");
2068 assert!(r.is_err());
2069 }
2070
2071 #[test]
2072 fn postfix_if_undefined_detected() {
2073 let r = lint("p 'x' if $undefined");
2074 assert!(r.is_err());
2075 }
2076
2077 #[test]
2078 fn while_loop_var_ok() {
2079 assert!(lint("my $i = 0; while ($i < 10) { p $i; $i++; }").is_ok());
2080 }
2081
2082 #[test]
2083 fn for_loop_init_var_in_scope() {
2084 assert!(lint("for (my $i = 0; $i < 10; $i++) { p $i; }").is_ok());
2085 }
2086
2087 #[test]
2088 fn given_when_ok() {
2089 assert!(lint("my $x = 1; given ($x) { when (1) { p 'one'; } }").is_ok());
2090 }
2091
2092 #[test]
2093 fn arrow_deref_ok() {
2094 assert!(lint("my $h = { a => 1 }; p $h->{a}").is_ok());
2095 }
2096
2097 #[test]
2098 fn method_call_ok() {
2099 assert!(lint("my $obj = bless {}, 'Foo'; $obj->method()").is_ok());
2100 }
2101
2102 #[test]
2103 fn push_builtin_ok() {
2104 assert!(lint("my @a; push @a, 1, 2, 3").is_ok());
2105 }
2106
2107 #[test]
2108 fn splice_builtin_ok() {
2109 assert!(lint("my @a = (1, 2, 3); splice @a, 1, 1, 'x'").is_ok());
2110 }
2111
2112 #[test]
2113 fn substr_builtin_ok() {
2114 assert!(lint("my $s = 'hello'; p substr($s, 0, 2)").is_ok());
2115 }
2116
2117 #[test]
2118 fn sprintf_builtin_ok() {
2119 assert!(lint("my $s = sprintf('%d', 42)").is_ok());
2120 }
2121
2122 #[test]
2123 fn range_ok() {
2124 assert!(lint("my @a = 1..10").is_ok());
2125 }
2126
2127 #[test]
2128 fn qw_ok() {
2129 assert!(lint("my @a = qw(a b c)").is_ok());
2130 }
2131
2132 #[test]
2133 fn regex_ok() {
2134 assert!(lint("my $x = 'hello'; $x =~ /ell/").is_ok());
2135 }
2136
2137 #[test]
2138 fn anonymous_sub_captures_outer_var() {
2139 assert!(lint("my $x = 1; my $f = fn { p $x; }").is_ok());
2140 }
2141
2142 #[test]
2143 fn state_var_ok() {
2144 assert!(lint("fn Test::counter { state $n = 0; $n++; }").is_ok());
2145 }
2146
2147 #[test]
2148 fn our_var_ok() {
2149 assert!(lint("our $VERSION = '1.0'").is_ok());
2150 }
2151
2152 #[test]
2153 fn local_var_ok() {
2154 assert!(lint("local $/ = undef").is_ok());
2155 }
2156
2157 #[test]
2158 fn chained_method_calls_ok() {
2159 assert!(lint("my $x = Foo->new->bar->baz").is_ok());
2160 }
2161
2162 #[test]
2163 fn list_assignment_ok() {
2164 assert!(lint("my ($a, $b, $c) = (1, 2, 3); p $a + $b + $c").is_ok());
2165 }
2166
2167 #[test]
2168 fn hash_slice_ok() {
2169 assert!(lint("my %h = (a => 1, b => 2); my @v = @h{qw(a b)}").is_ok());
2170 }
2171
2172 #[test]
2173 fn array_slice_ok() {
2174 assert!(lint("my @a = (1, 2, 3, 4); my @b = @a[0, 2]").is_ok());
2175 }
2176
2177 #[test]
2178 fn instance_method_on_typed_var_flags_unknown_method() {
2179 let r = lint(
2183 "class Point { x : Float\n y : Float\n fn mag_sq { 1 } }\n\
2184 my $p = Point(x => 3, y => 4)\n\
2185 $p->dgdd()",
2186 );
2187 assert!(r.is_err(), "expected error for `$$p->dgdd`");
2188 let e = r.unwrap_err();
2189 assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
2190 assert!(
2191 e.message.contains("dgdd") && e.message.contains("Point"),
2192 "expected message to name `dgdd` and `Point`, got: {}",
2193 e.message,
2194 );
2195 }
2196
2197 #[test]
2198 fn instance_method_on_typed_var_known_method_ok() {
2199 assert!(lint(
2201 "class Point { x : Float\n y : Float\n fn mag_sq { 1 } }\n\
2202 my $p = Point(x => 3, y => 4)\n\
2203 $p->mag_sq()"
2204 )
2205 .is_ok());
2206 }
2207
2208 #[test]
2209 fn instance_method_on_typed_var_field_ok() {
2210 assert!(lint(
2212 "class Point { x : Float\n y : Float }\n\
2213 my $p = Point(x => 3, y => 4)\n\
2214 p $p->x"
2215 )
2216 .is_ok());
2217 }
2218
2219 #[test]
2220 fn strict_never_flags_topic_variants() {
2221 let cases = [
2224 "_", "_0", "_1", "_42", "_<", "_<<<<<", "_<3", "_<10", "_2<", "_2<<<", "_2<5",
2229 ];
2230 for name in cases {
2231 assert!(
2232 super::is_topic_var(name),
2233 "is_topic_var({name:?}) must return true (topic/block-param form)",
2234 );
2235 }
2236 let src = "use strict\np _ + _1 + _< + _<2 + _<<<<< + _2<<< + _2<5\n";
2239 let prog = crate::parse_with_file(src, "test.stk").expect("parse");
2240 super::analyze_program_with_strict(&prog, "test.stk", true)
2241 .expect("strict-vars must not flag topic-variant block params");
2242 }
2243
2244 #[test]
2245 fn is_topic_var_rejects_non_topic_underscore_names() {
2246 for bad in [
2251 "_x", "_foo", "_3abc", "_<bad", "_2<xyz", "x_", "__", "_<<<x", ] {
2260 assert!(
2261 !super::is_topic_var(bad),
2262 "is_topic_var({bad:?}) must return false (not a topic form)",
2263 );
2264 }
2265 }
2266
2267 #[test]
2268 fn universal_methods_skip_hierarchy_lookup() {
2269 let mut a = super::StaticAnalyzer::new("test.stk");
2273 a.type_methods.insert("Empty".to_string(), HashSet::new());
2275 a.type_fields.insert("Empty".to_string(), HashSet::new());
2276 for method in ["isa", "can", "DOES", "does", "new", "BUILD", "DESTROY"] {
2277 assert!(
2278 a.method_resolves_in_hierarchy("Empty", method),
2279 "method `{method}` must resolve on any class via the universal whitelist",
2280 );
2281 }
2282 assert!(
2284 !a.method_resolves_in_hierarchy("Empty", "totally_made_up"),
2285 "non-universal unknown method must still be flagged",
2286 );
2287 }
2288
2289 #[test]
2290 fn method_resolves_walks_extends_chain() {
2291 let mut a = super::StaticAnalyzer::new("test.stk");
2294 let mut animal_methods = HashSet::new();
2295 animal_methods.insert("trail".to_string());
2296 a.type_methods.insert("Animal".to_string(), animal_methods);
2297 a.type_fields.insert("Animal".to_string(), HashSet::new());
2298 a.type_methods.insert("Dog".to_string(), HashSet::new());
2299 a.type_fields.insert("Dog".to_string(), HashSet::new());
2300 a.type_parents.insert("Dog".to_string(), vec!["Animal".to_string()]);
2301 assert!(
2302 a.method_resolves_in_hierarchy("Dog", "trail"),
2303 "`trail` on Dog must resolve via Animal in extends chain",
2304 );
2305 assert!(
2307 !a.method_resolves_in_hierarchy("Dog", "fly"),
2308 "`fly` on Dog must NOT resolve (absent from Dog AND Animal)",
2309 );
2310 }
2311
2312 #[test]
2313 fn method_resolves_cycle_protected() {
2314 let mut a = super::StaticAnalyzer::new("test.stk");
2317 a.type_methods.insert("A".to_string(), HashSet::new());
2318 a.type_fields.insert("A".to_string(), HashSet::new());
2319 a.type_methods.insert("B".to_string(), HashSet::new());
2320 a.type_fields.insert("B".to_string(), HashSet::new());
2321 a.type_parents.insert("A".to_string(), vec!["B".to_string()]);
2322 a.type_parents.insert("B".to_string(), vec!["A".to_string()]);
2323 assert!(!a.method_resolves_in_hierarchy("A", "missing"));
2325 assert!(!a.method_resolves_in_hierarchy("B", "missing"));
2326 }
2327
2328 #[test]
2329 fn dollar_obj_method_in_string_interp_not_flagged() {
2330 assert!(
2336 lint(
2337 "class P { x: Int = 0\n fn show { $self->x } }\n\
2338 my $p = P(x => 5)\np \"got #{ $p->show }\""
2339 )
2340 .is_ok(),
2341 );
2342 }
2343
2344 #[test]
2345 fn dollar_hash_with_complex_expression_in_string_interp() {
2346 assert!(
2350 lint(
2351 "use strict\n\
2352 my %h = (n => 5)\n\
2353 p \"got #{ $h{n} + 1 }\""
2354 )
2355 .is_ok(),
2356 "defined hash element inside #{{}} must not flag",
2357 );
2358 let r = lint("use strict\np \"got #{ $undef_typo + 1 }\"");
2359 assert!(
2360 r.is_err(),
2361 "complex-expr #{{}} block must still strict-check unknown vars",
2362 );
2363 }
2364
2365 #[test]
2366 fn is_topic_var_accepts_extra_grammar_variants() {
2367 for good in [
2369 "_99", "_<<<<<<<<<<", "_<999", "_42<<<", "_42<42", ] {
2375 assert!(
2376 super::is_topic_var(good),
2377 "is_topic_var({good:?}) must return true",
2378 );
2379 }
2380 }
2381
2382 #[test]
2383 fn strict_never_flags_sigiled_topic_variants() {
2384 let src = "use strict\nmy $tot = $_ + $_0 + $_1 + $_< + $_<2 + $_<<<<< + $_2<<< + $_2<5\np $tot\n";
2388 let prog = crate::parse_with_file(src, "test.stk").expect("parse");
2389 super::analyze_program_with_strict(&prog, "test.stk", true)
2390 .expect("strict-vars must not flag sigiled topic-variant block params");
2391 }
2392
2393 #[test]
2394 fn strict_still_flags_undefined_underscore_prefixed_ident() {
2395 let r = lint("p $_underscore_name");
2398 assert!(r.is_err(), "expected $_underscore_name to be flagged");
2399 }
2400
2401 #[test]
2402 fn qualified_our_scalar_visible_across_packages() {
2403 assert!(
2407 lint("package Foo\nour $x = 1\npackage main\np $Foo::x").is_ok(),
2408 "qualified `$Foo::x` must resolve to `our $x` declared in package Foo",
2409 );
2410 }
2411
2412 #[test]
2413 fn qualified_oursync_scalar_visible_across_packages() {
2414 assert!(
2416 lint("package Counter\noursync $val = 0\npackage main\np $Counter::val").is_ok(),
2417 "qualified `$Counter::val` must resolve to `oursync $val` in package Counter",
2418 );
2419 }
2420
2421 #[test]
2422 fn qualified_our_array_visible_across_packages() {
2423 assert!(lint("package Foo\nour @xs = (1,2,3)\npackage main\np @Foo::xs").is_ok());
2424 }
2425
2426 #[test]
2427 fn qualified_our_hash_visible_across_packages() {
2428 assert!(lint("package Foo\nour %h = (a=>1)\npackage main\np %Foo::h").is_ok());
2429 }
2430
2431 #[test]
2432 fn thread_par_run_compiler_generated_call_not_flagged() {
2433 assert!(
2436 lint("my @xs = (1,2,3)\nmy @r = ~p> @xs map { _ * 2 }").is_ok(),
2437 "compiler-generated _thread_par_run must be whitelisted",
2438 );
2439 }
2440
2441 #[test]
2442 fn deque_constructor_not_flagged() {
2443 assert!(
2447 lint("my $dq = deque(1, 2, 3)\np $dq->len").is_ok(),
2448 "deque constructor must not be flagged as undefined sub",
2449 );
2450 }
2451
2452 #[test]
2453 fn defer_block_compiler_generated_call_not_flagged() {
2454 assert!(
2457 lint("fn ff { defer { p \"cleanup\" }; 42 }").is_ok(),
2458 "compiler-generated defer__internal must be whitelisted",
2459 );
2460 let r = lint("use strict; _unknown_helper()");
2462 assert!(r.is_err(), "arbitrary `_foo` must still flag");
2463 }
2464
2465 #[test]
2466 fn aop_intercept_context_vars_not_flagged() {
2467 assert!(lint("before \"fetch\" { p $INTERCEPT_NAME, @INTERCEPT_ARGS }").is_ok());
2471 assert!(
2472 lint("after \"fetch\" { p $INTERCEPT_RESULT, $INTERCEPT_MS, $INTERCEPT_US }").is_ok()
2473 );
2474 }
2475
2476 #[test]
2477 fn string_interpolation_never_flags_undefined_simple_var() {
2478 assert!(
2484 lint("p \"printf $fh writes to STDOUT\"").is_ok(),
2485 "simple $var inside string-interp must not be flagged",
2486 );
2487 assert!(
2488 lint("p \"got @items here\"").is_ok(),
2489 "simple @var inside string-interp must not be flagged",
2490 );
2491 assert!(
2493 lint("p \"got $#missing_array items\"").is_ok(),
2494 "$#arr-style inside string-interp must not be flagged",
2495 );
2496 assert!(
2499 lint("use strict\np $fh").is_err(),
2500 "bare $fh outside string must still flag",
2501 );
2502 }
2503
2504 #[test]
2505 fn string_interp_complex_expr_still_walks_strict() {
2506 let r = lint("use strict\np \"got #{ $undef + 1 }\"");
2509 assert!(
2510 r.is_err(),
2511 "complex expr inside #{{}} must still strict-check vars",
2512 );
2513 }
2514
2515 #[test]
2516 fn qualified_main_var_visible_in_default_package() {
2517 assert!(lint("our $log_begin = \"\"\nBEGIN { $main::log_begin .= \"B:\" }").is_ok(),);
2521 assert!(lint("our @items = (1,2)\np @main::items").is_ok());
2522 assert!(lint("our %map = ()\np keys %main::map").is_ok());
2523 }
2524
2525 #[test]
2526 fn dollar_hash_array_last_index_uses_underlying_array() {
2527 assert!(lint("my @arr = (1,2,3); p $#arr").is_ok());
2530 let r = lint("use strict\np $#undefined_array");
2531 assert!(r.is_err(), "$#undefined_array must flag @undefined_array");
2532 assert!(
2533 r.unwrap_err().message.contains("@undefined_array"),
2534 "error must name @undefined_array, not $#undefined_array",
2535 );
2536 }
2537
2538 #[test]
2539 fn match_arm_enum_variant_typo_flagged() {
2540 let r = lint(
2544 "enum Sig { Hup, Int, Term, Kill }\n\
2545 fn handle($s) {\n\
2546 match ($s) {\n\
2547 Sig::Hup => \"reload\",\n\
2548 Sig::Term2 => \"drain\",\n\
2549 Sig::Kill => \"reap\",\n\
2550 }\n\
2551 }",
2552 );
2553 assert!(r.is_err(), "expected flag on Sig::Term2");
2554 let msg = r.unwrap_err().message;
2555 assert!(
2556 msg.contains("Term2") && msg.contains("Sig"),
2557 "message must name Term2 and Sig: {msg}",
2558 );
2559 }
2560
2561 #[test]
2562 fn match_arm_known_enum_variant_passes() {
2563 assert!(lint(
2565 "enum Sig { Hup, Int, Term, Kill }\n\
2566 fn handle($s) {\n\
2567 match ($s) {\n\
2568 Sig::Hup => \"reload\",\n\
2569 Sig::Int => \"shutdown\",\n\
2570 Sig::Term => \"drain\",\n\
2571 Sig::Kill => \"reap\",\n\
2572 }\n\
2573 }"
2574 )
2575 .is_ok());
2576 }
2577
2578 #[test]
2579 fn dollar_caret_perl_special_vars_not_flagged() {
2580 for name in ["$^X", "$^O", "$^V", "$^W", "$^T", "$^R", "$^N", "$^H"] {
2583 assert!(
2584 lint(&format!("use strict\np {name}")).is_ok(),
2585 "{name} must not be flagged by strict-vars",
2586 );
2587 }
2588 }
2589
2590 #[test]
2591 fn lexical_filehandle_open_my_var_not_flagged() {
2592 assert!(lint(
2596 "use strict\nmy $efile = \"/tmp/x\"\n\
2597 open(my $wfh, \">\", $efile) or die\n\
2598 print $wfh \"line1\\n\"\nclose $wfh"
2599 )
2600 .is_ok(),);
2601 }
2602
2603 #[test]
2604 fn exists_subroutine_ref_does_not_flag_undefined() {
2605 assert!(lint(
2608 "package Foo\nfn greet = 1\npackage main\n\
2609 p exists(&Foo::greet) ? \"y\" : \"n\"\n\
2610 p exists(&Foo::missing) ? \"y\" : \"n\""
2611 )
2612 .is_ok(),);
2613 }
2614
2615 #[test]
2616 fn universal_methods_resolve_on_any_class() {
2617 assert!(lint(
2624 "class Square { side: Float\n fn area { 1 } }\n\
2625 my $sq = Square->new(side => 5)\n\
2626 p $sq->isa(\"Square\")\n\
2627 p $sq->can(\"area\")\n\
2628 p $sq->DOES(\"Shape\")\n\
2629 my $cloned = $sq->clone()\n\
2630 my $changed = $sq->with(side => 9)\n\
2631 p $sq->to_hash()\n\
2632 p $sq->fields()"
2633 )
2634 .is_ok(),);
2635 }
2636
2637 #[test]
2638 fn builtin_struct_methods_resolve_on_any_struct() {
2639 assert!(lint(
2642 "struct Point { x: Float\n y: Float }\n\
2643 my $p = Point(x => 1.0, y => 2.0)\n\
2644 my $c = $p->clone()\n\
2645 my $u = $p->with(x => 9.0)\n\
2646 p $p->to_hash()\n\
2647 p $p->fields()"
2648 )
2649 .is_ok(),);
2650 }
2651
2652 #[test]
2653 fn class_inheritance_resolves_parent_methods_on_self() {
2654 assert!(lint(
2657 "class Animal { name: Str = \"\"\n fn trail { \"...\" } }\n\
2658 class Dog extends Animal {\n\
2659 breed: Str = \"\"\n\
2660 fn show { $self->trail }\n\
2661 }"
2662 )
2663 .is_ok(),);
2664 }
2665
2666 #[test]
2667 fn class_inheritance_resolves_parent_fields_in_constructor() {
2668 assert!(lint(
2671 "class Animal { name: Str = \"\" }\n\
2672 class Dog extends Animal { breed: Str = \"\" }\n\
2673 my $d = Dog(name => \"Rex\", breed => \"Lab\")\n\
2674 p $d->name"
2675 )
2676 .is_ok(),);
2677 }
2678
2679 #[test]
2680 fn resolve_require_path_finds_lib_root_from_nested_source() {
2681 let tmp = std::env::temp_dir().join(format!("stryke_resolve_test_{}", std::process::id()));
2688 let _ = std::fs::remove_dir_all(&tmp);
2689 std::fs::create_dir_all(tmp.join("lib").join("ai")).unwrap();
2690 let mat = tmp.join("lib").join("ai").join("matrix.stk");
2691 std::fs::write(&mat, "1\n").unwrap();
2692 let nn = tmp.join("lib").join("ai").join("neural_network.stk");
2693 std::fs::write(&nn, "1\n").unwrap();
2694 let resolved =
2695 super::resolve_require_path_from_file(nn.to_str().unwrap(), "./lib/ai/matrix.stk");
2696 assert!(
2697 resolved.as_ref().is_some_and(|p| p == &mat)
2698 || resolved
2699 .as_ref()
2700 .is_some_and(|p| { p.canonicalize().ok() == mat.canonicalize().ok() }),
2701 "expected to resolve to {mat:?}, got {resolved:?}",
2702 );
2703 let _ = std::fs::remove_dir_all(&tmp);
2704 }
2705
2706 #[test]
2707 fn resolve_require_path_sibling_in_same_dir() {
2708 let tmp =
2711 std::env::temp_dir().join(format!("stryke_resolve_sibling_{}", std::process::id()));
2712 let _ = std::fs::remove_dir_all(&tmp);
2713 std::fs::create_dir_all(&tmp).unwrap();
2714 let sib = tmp.join("sibling.stk");
2715 std::fs::write(&sib, "1\n").unwrap();
2716 let me = tmp.join("me.stk");
2717 std::fs::write(&me, "1\n").unwrap();
2718 let resolved = super::resolve_require_path_from_file(me.to_str().unwrap(), "./sibling.stk");
2719 assert!(
2720 resolved.is_some(),
2721 "expected to resolve ./sibling.stk in same dir, got None",
2722 );
2723 let _ = std::fs::remove_dir_all(&tmp);
2724 }
2725
2726 #[test]
2727 fn positional_constructor_args_not_checked_as_keys() {
2728 assert!(lint(
2732 "enum Priority { Low, Medium, High, Critical }\n\
2733 class Task {\n\
2734 id: Int\n\
2735 title: Str = \"\"\n\
2736 priority: Any = undef\n\
2737 }\n\
2738 my $t = Task(1, \"Setup\", Priority::High)"
2739 )
2740 .is_ok(),);
2741 }
2742
2743 #[test]
2744 fn positional_constructor_with_string_value_not_flagged() {
2745 assert!(lint(
2749 "class Person { name: Str = \"\"\n age: Int = 0 }\n\
2750 my $p = Person(\"Alice\", 30)"
2751 )
2752 .is_ok(),);
2753 }
2754
2755 #[test]
2756 fn keyed_constructor_still_flags_typo() {
2757 let r = lint(
2761 "class Point { x: Float\n y: Float }\n\
2762 my $p = Point(x => 10, yyg => 20)",
2763 );
2764 assert!(r.is_err(), "expected `yyg` typo to be flagged");
2765 assert!(
2766 r.unwrap_err().message.contains("yyg"),
2767 "error must name `yyg` field",
2768 );
2769 }
2770
2771 #[test]
2772 fn dollar_dollar_pid_not_flagged() {
2773 assert!(lint("use strict\np $$").is_ok());
2777 assert!(lint("use strict\n$$ > 0 ? 1 : 0").is_ok());
2778 }
2779
2780 #[test]
2781 fn class_impl_trait_resolves_default_method() {
2782 assert!(lint(
2787 "trait Greetable {\n\
2788 fn greeting { \"Hello\" }\n\
2789 fn name\n\
2790 }\n\
2791 class Person impl Greetable {\n\
2792 n: Str = \"\"\n\
2793 fn name { $self->n }\n\
2794 }\n\
2795 my $p = Person(n => \"Alice\")\n\
2796 p $p->greeting()"
2797 )
2798 .is_ok(),);
2799 }
2800
2801 #[test]
2802 fn class_impl_multiple_traits_resolves_methods_from_all() {
2803 assert!(lint(
2804 "trait Greetable { fn greeting { \"hi\" } }\n\
2805 trait Loggable { fn log_it { 1 } }\n\
2806 class Hybrid impl Greetable, Loggable {}\n\
2807 my $h = Hybrid->new\n\
2808 p $h->greeting()\n\
2809 p $h->log_it()"
2810 )
2811 .is_ok(),);
2812 }
2813
2814 #[test]
2815 fn class_impl_trait_still_flags_unknown_method() {
2816 let r = lint(
2819 "trait Greetable { fn greeting }\n\
2820 class Person impl Greetable { n: Str = \"\" }\n\
2821 my $p = Person->new\n\
2822 p $p->fly()",
2823 );
2824 assert!(r.is_err(), "expected $p->fly to be flagged");
2825 }
2826
2827 #[test]
2828 fn class_inheritance_still_flags_unknown_method() {
2829 let r = lint(
2832 "class Animal { fn trail { \"\" } }\n\
2833 class Dog extends Animal {\n\
2834 fn show { $self->fly }\n\
2835 }",
2836 );
2837 assert!(r.is_err(), "expected $self->fly to be flagged");
2838 }
2839
2840 #[test]
2841 fn instance_method_on_arrow_new_form_typed_var_flags() {
2842 let r = lint(
2844 "class Point { x : Float\n y : Float }\n\
2845 my $p = Point->new(x => 3, y => 4)\n\
2846 $p->whatever()",
2847 );
2848 assert!(r.is_err(), "expected error for `$$p->whatever`");
2849 }
2850}