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}
51pub struct StaticAnalyzer {
53 scopes: Vec<Scope>,
55 errors: Vec<StrykeError>,
57 file: String,
59 current_package: String,
61 strict_vars: bool,
70 seen_required_files: HashSet<PathBuf>,
74 type_fields: HashMap<String, HashSet<String>>,
79 type_methods: HashMap<String, HashSet<String>>,
84 type_parents: HashMap<String, Vec<String>>,
89 current_class: Option<String>,
93 has_dynamic_subs: bool,
100}
101
102impl StaticAnalyzer {
103 pub fn new(file: &str) -> Self {
105 Self::with_strict_vars(file, false)
106 }
107 pub fn with_strict_vars(file: &str, strict_vars: bool) -> Self {
109 let mut global = Scope::default();
110 for name in ["_", "a", "b", "ARGV", "ENV", "SIG", "INC"] {
111 global.declare_array(name);
112 }
113 for name in ["ENV", "SIG", "INC"] {
114 global.declare_hash(name);
115 }
116 for name in [
117 "_", "a", "b", "!", "$", "@", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "&",
118 "`", "'", "+", ".", "/", "\\", "|", "%", "=", "-", "~", "^", "*", "?", "\"",
119 ] {
120 global.declare_scalar(name);
121 }
122 let mut a = Self {
123 scopes: vec![global],
124 errors: Vec::new(),
125 file: file.to_string(),
126 current_package: "main".to_string(),
127 strict_vars,
128 seen_required_files: HashSet::new(),
129 type_fields: HashMap::new(),
130 type_methods: HashMap::new(),
131 type_parents: HashMap::new(),
132 current_class: None,
133 has_dynamic_subs: false,
134 };
135 a.declare_ffi_exports_for(Path::new(file));
142 a
143 }
144
145 fn push_scope(&mut self) {
146 self.scopes.push(Scope::default());
147 }
148
149 fn pop_scope(&mut self) {
150 if self.scopes.len() > 1 {
151 self.scopes.pop();
152 }
153 }
154
155 fn declare_scalar(&mut self, name: &str) {
156 if let Some(scope) = self.scopes.last_mut() {
157 scope.declare_scalar(name);
158 }
159 }
160
161 fn declare_array(&mut self, name: &str) {
162 if let Some(scope) = self.scopes.last_mut() {
163 scope.declare_array(name);
164 }
165 }
166
167 fn declare_hash(&mut self, name: &str) {
168 if let Some(scope) = self.scopes.last_mut() {
169 scope.declare_hash(name);
170 }
171 }
172
173 fn declare_sub(&mut self, name: &str) {
174 if let Some(scope) = self.scopes.first_mut() {
175 scope.declare_sub(name);
176 }
177 }
178
179 fn declare_scalar_type(&mut self, name: &str, ty: &str) {
183 if let Some(scope) = self.scopes.last_mut() {
184 scope.scalar_types.insert(name.to_string(), ty.to_string());
185 }
186 }
187
188 fn resolve_scalar_type(&self, name: &str) -> Option<&str> {
191 for s in self.scopes.iter().rev() {
192 if let Some(t) = s.scalar_types.get(name) {
193 return Some(t.as_str());
194 }
195 }
196 None
197 }
198
199 fn infer_constructor_type(&self, init: &Expr) -> Option<String> {
204 match &init.kind {
205 ExprKind::FuncCall { name, .. } => {
206 let bare = name.rsplit("::").next().unwrap_or(name);
207 if self.type_fields.contains_key(name) {
208 return Some(name.clone());
209 }
210 if self.type_fields.contains_key(bare) {
211 return Some(bare.to_string());
212 }
213 None
214 }
215 ExprKind::MethodCall { object, method, .. } if method == "new" => {
216 if let ExprKind::Bareword(n) = &object.kind {
217 let bare = n.rsplit("::").next().unwrap_or(n);
218 if self.type_fields.contains_key(n) {
219 return Some(n.clone());
220 }
221 if self.type_fields.contains_key(bare) {
222 return Some(bare.to_string());
223 }
224 }
225 None
226 }
227 _ => None,
228 }
229 }
230
231 fn is_scalar_defined(&self, name: &str) -> bool {
232 if is_special_var(name) || is_topic_var(name) {
233 return true;
234 }
235 if name.contains("::") {
239 return true;
240 }
241 self.scopes.iter().rev().any(|s| s.scalars.contains(name))
242 }
243
244 fn is_array_defined(&self, name: &str) -> bool {
245 if is_special_var(name) || is_topic_var(name) {
246 return true;
247 }
248 if name.contains("::") {
249 return true;
250 }
251 self.scopes.iter().rev().any(|s| s.arrays.contains(name))
252 }
253
254 fn is_hash_defined(&self, name: &str) -> bool {
255 if is_special_var(name) || is_topic_var(name) {
256 return true;
257 }
258 if name.contains("::") {
259 return true;
260 }
261 self.scopes.iter().rev().any(|s| s.hashes.contains(name))
262 }
263
264 fn is_sub_defined(&self, name: &str) -> bool {
265 if name.starts_with("static::") {
267 return true;
268 }
269 if self.has_dynamic_subs {
274 return true;
275 }
276 if matches!(
281 name,
282 "_thread_par_run"
283 | "__stryke_rust_compile"
284 | "defer__internal"
285 | "deque",
289 ) {
290 return true;
291 }
292 let base = name.rsplit("::").next().unwrap_or(name);
293 if builtins().contains(base) {
294 return true;
295 }
296 self.scopes
297 .iter()
298 .rev()
299 .any(|s| s.subs.contains(name) || s.subs.contains(base))
300 }
301
302 fn error(&mut self, kind: ErrorKind, msg: String, line: usize) {
303 self.errors
304 .push(StrykeError::new(kind, msg, line, &self.file));
305 }
306 pub fn analyze(mut self, program: &Program) -> StrykeResult<()> {
308 for stmt in &program.statements {
309 self.collect_declarations_stmt(stmt);
310 }
311 self.scan_dynamic_sub_sources_in_source();
318 for stmt in &program.statements {
319 self.analyze_stmt(stmt);
320 }
321 if let Some(e) = self.errors.into_iter().next() {
322 Err(e)
323 } else {
324 Ok(())
325 }
326 }
327
328 fn collect_declarations_stmt(&mut self, stmt: &Statement) {
329 match &stmt.kind {
330 StmtKind::Package { name } => {
331 self.current_package = name.clone();
332 }
333 StmtKind::SubDecl { name, .. } => {
334 let fqn = if name.contains("::") {
335 name.clone()
336 } else {
337 format!("{}::{}", self.current_package, name)
338 };
339 self.declare_sub(name);
340 self.declare_sub(&fqn);
341 }
342 StmtKind::Use { module, imports, version } => {
343 self.declare_sub(module);
344 self.follow_require_versioned(module, version.as_deref());
357 if module == "constant" {
362 self.collect_use_constant_names(imports);
363 }
364 }
365 StmtKind::Expression(e) => self.collect_required_subs_from_expr(e),
372 StmtKind::Block(b)
373 | StmtKind::StmtGroup(b)
374 | StmtKind::Begin(b)
375 | StmtKind::End(b)
376 | StmtKind::UnitCheck(b)
377 | StmtKind::Check(b)
378 | StmtKind::Init(b) => {
379 for s in b {
380 self.collect_declarations_stmt(s);
381 }
382 }
383 StmtKind::If {
384 body,
385 elsifs,
386 else_block,
387 ..
388 } => {
389 for s in body {
390 self.collect_declarations_stmt(s);
391 }
392 for (_, b) in elsifs {
393 for s in b {
394 self.collect_declarations_stmt(s);
395 }
396 }
397 if let Some(b) = else_block {
398 for s in b {
399 self.collect_declarations_stmt(s);
400 }
401 }
402 }
403 StmtKind::ClassDecl { def } => {
404 self.declare_sub(&def.name);
405 for m in &def.methods {
406 if m.is_static {
407 self.declare_sub(&format!("{}::{}", def.name, m.name));
408 }
409 }
410 for sf in &def.static_fields {
411 self.declare_sub(&format!("{}::{}", def.name, sf.name));
412 }
413 let mut fields: HashSet<String> = HashSet::new();
414 for f in &def.fields {
415 fields.insert(f.name.clone());
416 }
417 self.type_fields.insert(def.name.clone(), fields);
418 let mut methods: HashSet<String> = HashSet::new();
420 for m in &def.methods {
421 methods.insert(m.name.clone());
422 }
423 self.type_methods.insert(def.name.clone(), methods);
424 let mut parents = def.extends.clone();
430 parents.extend(def.implements.iter().cloned());
431 if !parents.is_empty() {
432 self.type_parents.insert(def.name.clone(), parents);
433 }
434 }
435 StmtKind::StructDecl { def } => {
436 self.declare_sub(&def.name);
437 let mut fields: HashSet<String> = HashSet::new();
438 for f in &def.fields {
439 fields.insert(f.name.clone());
440 }
441 self.type_fields.insert(def.name.clone(), fields);
442 let mut methods: HashSet<String> = HashSet::new();
443 for m in &def.methods {
444 methods.insert(m.name.clone());
445 }
446 self.type_methods.insert(def.name.clone(), methods);
447 }
448 StmtKind::EnumDecl { def } => {
449 self.declare_sub(&def.name);
450 for v in &def.variants {
451 self.declare_sub(&format!("{}::{}", def.name, v.name));
452 }
453 let mut fields: HashSet<String> = HashSet::new();
457 for v in &def.variants {
458 fields.insert(v.name.clone());
459 }
460 self.type_fields.insert(def.name.clone(), fields);
461 }
462 StmtKind::TraitDecl { def } => {
468 self.declare_sub(&def.name);
469 let mut methods: HashSet<String> = HashSet::new();
470 for m in &def.methods {
471 methods.insert(m.name.clone());
472 }
473 self.type_methods.insert(def.name.clone(), methods);
474 self.type_fields.entry(def.name.clone()).or_default();
477 }
478 _ => {}
479 }
480 }
481
482 fn collect_use_constant_names(&mut self, imports: &[Expr]) {
489 for imp in imports {
490 match &imp.kind {
491 ExprKind::List(items) => {
492 let mut i = 0;
493 while i + 1 < items.len() {
494 if let Some(name) = static_string_value(&items[i]) {
495 let fqn = format!("{}::{}", self.current_package, name);
496 self.declare_sub(&name);
497 self.declare_sub(&fqn);
498 }
499 i += 2;
500 }
501 }
502 ExprKind::HashRef(pairs) => {
503 for (k, _) in pairs {
504 if let Some(name) = static_string_value(k) {
505 let fqn = format!("{}::{}", self.current_package, name);
506 self.declare_sub(&name);
507 self.declare_sub(&fqn);
508 }
509 }
510 }
511 _ => {}
512 }
513 }
514 }
515
516 fn collect_required_subs_from_expr(&mut self, expr: &Expr) {
524 match &expr.kind {
525 ExprKind::Require(inner) => {
526 let Some(spec) = static_string_value(inner) else {
527 return;
528 };
529 self.follow_require(&spec);
530 }
531 ExprKind::PostfixIf { expr: inner, .. }
532 | ExprKind::PostfixUnless { expr: inner, .. } => {
533 self.collect_required_subs_from_expr(inner);
534 }
535 _ => {}
536 }
537 }
538
539 fn scan_dynamic_sub_sources_in_source(&mut self) {
545 let Ok(src) = std::fs::read_to_string(&self.file) else {
546 return;
547 };
548 let mut cleaned = String::with_capacity(src.len());
552 for line in src.split_inclusive('\n') {
553 if let Some(hash) = line.find('#') {
554 let before = &line[..hash];
555 let dq = before.matches('"').count();
559 let sq = before.matches('\'').count();
560 if dq % 2 == 0 && sq % 2 == 0 {
561 cleaned.push_str(before);
562 if line.ends_with('\n') {
563 cleaned.push('\n');
564 }
565 continue;
566 }
567 }
568 cleaned.push_str(line);
569 }
570 let bytes = cleaned.as_bytes();
574 let mut i = 0;
575 while i + 8 < bytes.len() {
576 if &bytes[i..i + 7] == b"require"
577 && (i == 0 || !is_ident_byte(bytes[i - 1]))
578 && !is_ident_byte(bytes[i + 7])
579 {
580 let mut j = i + 7;
581 while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'(')
582 {
583 j += 1;
584 }
585 if j < bytes.len() && bytes[j] == b'$' {
586 self.has_dynamic_subs = true;
587 }
588 }
589 i += 1;
590 }
591 let mut i = 0;
597 while i + 5 < bytes.len() {
598 if &bytes[i..i + 4] == b"eval"
599 && (i == 0 || !is_ident_byte(bytes[i - 1]))
600 && !is_ident_byte(bytes[i + 4])
601 {
602 let mut j = i + 4;
603 while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'(')
604 {
605 j += 1;
606 }
607 if j < bytes.len() {
608 match bytes[j] {
609 b'"' => {
610 let mut k = j + 1;
612 let start = k;
613 while k < bytes.len() && bytes[k] != b'"' {
614 if bytes[k] == b'\\' && k + 1 < bytes.len() {
615 k += 2;
616 } else {
617 k += 1;
618 }
619 }
620 let lit = &cleaned[start..k.min(cleaned.len())];
621 self.extract_fn_decls_from_str(lit);
622 if lit.contains('$') || lit.contains("#{") {
626 self.has_dynamic_subs = true;
627 }
628 }
629 b'\'' => {
630 let mut k = j + 1;
631 let start = k;
632 while k < bytes.len() && bytes[k] != b'\'' {
633 k += 1;
634 }
635 let lit = &cleaned[start..k.min(cleaned.len())];
636 self.extract_fn_decls_from_str(lit);
637 }
638 b'{' => {} _ => {
640 self.has_dynamic_subs = true;
642 }
643 }
644 }
645 }
646 i += 1;
647 }
648 }
649
650 fn extract_fn_decls_from_str(&mut self, src: &str) {
653 for token in ["fn", "sub"] {
654 let mut rest = src;
655 while let Some(pos) = rest.find(token) {
656 let before_ok = pos == 0
657 || !rest
658 .as_bytes()
659 .get(pos.saturating_sub(1))
660 .map(|c| c.is_ascii_alphanumeric() || *c == b'_')
661 .unwrap_or(false);
662 let after = &rest[pos + token.len()..];
663 let trimmed = after.trim_start();
664 if before_ok && after.len() > trimmed.len() {
665 let name: String = trimmed
666 .chars()
667 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == ':')
668 .collect();
669 if !name.is_empty() && !name.starts_with(':') {
670 self.declare_sub(&name);
671 if let Some(tail) = name.rsplit("::").next() {
672 if tail != name {
673 self.declare_sub(tail);
674 }
675 }
676 }
677 }
678 rest = &rest[pos + token.len()..];
679 }
680 }
681 }
682
683 fn follow_require(&mut self, spec: &str) {
690 self.follow_require_versioned(spec, None);
691 }
692
693 fn follow_require_versioned(&mut self, spec: &str, pin_version: Option<&str>) {
694 let spec = spec.trim();
695 if spec.is_empty() {
696 return;
697 }
698 if matches!(
701 spec,
702 "strict"
703 | "warnings"
704 | "utf8"
705 | "feature"
706 | "v5"
707 | "threads"
708 | "Thread::Pool"
709 | "Parallel::ForkManager"
710 ) {
711 return;
712 }
713 let Some(target) = self.resolve_require_path_versioned(spec, pin_version) else {
714 return;
715 };
716 let canon = target.canonicalize().unwrap_or(target.clone());
717 if !self.seen_required_files.insert(canon.clone()) {
718 return; }
720 let Ok(src) = std::fs::read_to_string(&target) else {
721 return;
722 };
723 let file_str = target.to_string_lossy().into_owned();
724 let Ok(program) = crate::parse_module_with_file(&src, &file_str) else {
725 return;
726 };
727 self.declare_ffi_exports_for(&target);
733 let saved_pkg = std::mem::replace(&mut self.current_package, "main".to_string());
736 for stmt in &program.statements {
737 self.collect_declarations_stmt(stmt);
738 }
739 self.current_package = saved_pkg;
740 }
741
742 fn resolve_require_path_versioned(
743 &self,
744 spec: &str,
745 pin_version: Option<&str>,
746 ) -> Option<PathBuf> {
747 resolve_require_path_from_file_versioned(&self.file, spec, pin_version)
748 }
749
750 fn declare_ffi_exports_for(&mut self, file_path: &Path) {
767 let mut dir = file_path.parent().map(Path::to_path_buf);
768 for _ in 0..3 {
769 let Some(d) = dir.as_ref() else { break };
770 let manifest_path = d.join("stryke.toml");
771 if manifest_path.is_file() {
772 if let Ok(m) = crate::pkg::manifest::Manifest::from_path(&manifest_path) {
773 if let Some(ffi) = m.ffi {
774 for export in &ffi.exports {
775 self.declare_sub(export);
776 }
777 }
778 }
779 return;
780 }
781 dir = d.parent().map(Path::to_path_buf);
782 }
783 }
784
785 fn analyze_stmt(&mut self, stmt: &Statement) {
786 match &stmt.kind {
787 StmtKind::Package { name } => {
788 self.current_package = name.clone();
789 }
790 StmtKind::My(decls)
791 | StmtKind::Our(decls)
792 | StmtKind::Local(decls)
793 | StmtKind::State(decls)
794 | StmtKind::MySync(decls)
795 | StmtKind::OurSync(decls) => {
796 let is_package_global =
801 matches!(stmt.kind, StmtKind::Our(_) | StmtKind::OurSync(_));
802 for d in decls {
803 match d.sigil {
804 Sigil::Scalar => {
805 self.declare_scalar(&d.name);
806 if is_package_global {
807 let q = format!("{}::{}", self.current_package, d.name);
808 self.declare_scalar(&q);
809 }
810 }
811 Sigil::Array => {
812 self.declare_array(&d.name);
813 if is_package_global {
814 let q = format!("{}::{}", self.current_package, d.name);
815 self.declare_array(&q);
816 }
817 }
818 Sigil::Hash => {
819 self.declare_hash(&d.name);
820 if is_package_global {
821 let q = format!("{}::{}", self.current_package, d.name);
822 self.declare_hash(&q);
823 }
824 }
825 Sigil::Typeglob => {}
826 }
827 if let Some(init) = &d.initializer {
828 if matches!(d.sigil, Sigil::Scalar) {
829 if let Some(ty) = self.infer_constructor_type(init) {
830 self.declare_scalar_type(&d.name, &ty);
831 }
832 }
833 self.analyze_expr(init);
834 }
835 }
836 }
837 StmtKind::Expression(e) => self.analyze_expr(e),
838 StmtKind::Return(Some(e)) => self.analyze_expr(e),
839 StmtKind::Return(None) => {}
840 StmtKind::If {
841 condition,
842 body,
843 elsifs,
844 else_block,
845 } => {
846 self.analyze_expr(condition);
847 self.push_scope();
848 self.analyze_block(body);
849 self.pop_scope();
850 for (cond, b) in elsifs {
851 self.analyze_expr(cond);
852 self.push_scope();
853 self.analyze_block(b);
854 self.pop_scope();
855 }
856 if let Some(b) = else_block {
857 self.push_scope();
858 self.analyze_block(b);
859 self.pop_scope();
860 }
861 }
862 StmtKind::Unless {
863 condition,
864 body,
865 else_block,
866 } => {
867 self.analyze_expr(condition);
868 self.push_scope();
869 self.analyze_block(body);
870 self.pop_scope();
871 if let Some(b) = else_block {
872 self.push_scope();
873 self.analyze_block(b);
874 self.pop_scope();
875 }
876 }
877 StmtKind::While {
878 condition,
879 body,
880 continue_block,
881 ..
882 }
883 | StmtKind::Until {
884 condition,
885 body,
886 continue_block,
887 ..
888 } => {
889 self.analyze_expr(condition);
890 self.push_scope();
891 self.analyze_block(body);
892 if let Some(cb) = continue_block {
893 self.analyze_block(cb);
894 }
895 self.pop_scope();
896 }
897 StmtKind::DoWhile { body, condition } => {
898 self.push_scope();
899 self.analyze_block(body);
900 self.pop_scope();
901 self.analyze_expr(condition);
902 }
903 StmtKind::For {
904 init,
905 condition,
906 step,
907 body,
908 continue_block,
909 ..
910 } => {
911 self.push_scope();
912 if let Some(i) = init {
913 self.analyze_stmt(i);
914 }
915 if let Some(c) = condition {
916 self.analyze_expr(c);
917 }
918 if let Some(s) = step {
919 self.analyze_expr(s);
920 }
921 self.analyze_block(body);
922 if let Some(cb) = continue_block {
923 self.analyze_block(cb);
924 }
925 self.pop_scope();
926 }
927 StmtKind::Foreach {
928 var,
929 list,
930 body,
931 continue_block,
932 ..
933 } => {
934 self.analyze_expr(list);
935 self.push_scope();
936 self.declare_scalar(var);
937 self.analyze_block(body);
938 if let Some(cb) = continue_block {
939 self.analyze_block(cb);
940 }
941 self.pop_scope();
942 }
943 StmtKind::SubDecl {
944 name, params, body, ..
945 } => {
946 let fqn = if name.contains("::") {
947 name.clone()
948 } else {
949 format!("{}::{}", self.current_package, name)
950 };
951 self.declare_sub(name);
952 self.declare_sub(&fqn);
953 self.push_scope();
954 for p in params {
955 self.declare_param(p);
956 }
957 self.analyze_block(body);
958 self.pop_scope();
959 }
960 StmtKind::Block(b)
961 | StmtKind::StmtGroup(b)
962 | StmtKind::Begin(b)
963 | StmtKind::End(b)
964 | StmtKind::UnitCheck(b)
965 | StmtKind::Check(b)
966 | StmtKind::Init(b)
967 | StmtKind::Continue(b) => {
968 self.push_scope();
969 self.analyze_block(b);
970 self.pop_scope();
971 }
972 StmtKind::TryCatch {
973 try_block,
974 catch_var,
975 catch_block,
976 finally_block,
977 } => {
978 self.push_scope();
979 self.analyze_block(try_block);
980 self.pop_scope();
981 self.push_scope();
982 self.declare_scalar(catch_var);
983 self.analyze_block(catch_block);
984 self.pop_scope();
985 if let Some(fb) = finally_block {
986 self.push_scope();
987 self.analyze_block(fb);
988 self.pop_scope();
989 }
990 }
991 StmtKind::EvalTimeout { body, .. } => {
992 self.push_scope();
993 self.analyze_block(body);
994 self.pop_scope();
995 }
996 StmtKind::Given { topic, body } => {
997 self.analyze_expr(topic);
998 self.push_scope();
999 self.analyze_block(body);
1000 self.pop_scope();
1001 }
1002 StmtKind::When { cond, body } => {
1003 self.analyze_expr(cond);
1004 self.push_scope();
1005 self.analyze_block(body);
1006 self.pop_scope();
1007 }
1008 StmtKind::DefaultCase { body } => {
1009 self.push_scope();
1010 self.analyze_block(body);
1011 self.pop_scope();
1012 }
1013 StmtKind::LocalExpr {
1014 target,
1015 initializer,
1016 } => {
1017 self.analyze_expr(target);
1018 if let Some(init) = initializer {
1019 self.analyze_expr(init);
1020 }
1021 }
1022 StmtKind::Goto { target } => {
1023 self.analyze_expr(target);
1024 }
1025 StmtKind::Tie { class, args, .. } => {
1026 self.analyze_expr(class);
1027 for a in args {
1028 self.analyze_expr(a);
1029 }
1030 }
1031 StmtKind::Use { imports, .. } | StmtKind::No { imports, .. } => {
1032 for e in imports {
1033 self.analyze_expr(e);
1034 }
1035 }
1036 StmtKind::StructDecl { def } => {
1037 let prev = self.current_class.take();
1041 self.current_class = Some(def.name.clone());
1042 for m in &def.methods {
1043 self.push_scope();
1044 self.declare_scalar("self");
1045 for p in &m.params {
1046 self.declare_param(p);
1047 }
1048 self.analyze_block(&m.body);
1049 self.pop_scope();
1050 }
1051 self.current_class = prev;
1052 }
1053 StmtKind::ClassDecl { def } => {
1054 let prev = self.current_class.take();
1055 self.current_class = Some(def.name.clone());
1056 for m in &def.methods {
1057 if let Some(body) = &m.body {
1058 self.push_scope();
1059 self.declare_scalar("self");
1060 for p in &m.params {
1061 self.declare_param(p);
1062 }
1063 self.analyze_block(body);
1064 self.pop_scope();
1065 }
1066 }
1067 self.current_class = prev;
1068 }
1069 StmtKind::EnumDecl { .. }
1070 | StmtKind::TraitDecl { .. }
1071 | StmtKind::FormatDecl { .. }
1072 | StmtKind::AdviceDecl { .. }
1073 | StmtKind::UsePerlVersion { .. }
1074 | StmtKind::UseOverload { .. }
1075 | StmtKind::Last(_)
1076 | StmtKind::Next(_)
1077 | StmtKind::Redo(_)
1078 | StmtKind::Empty => {}
1079 }
1080 }
1081
1082 fn declare_param(&mut self, param: &SubSigParam) {
1083 match param {
1084 SubSigParam::Scalar(name, _, _) => self.declare_scalar(name),
1085 SubSigParam::Array(name, _) => self.declare_array(name),
1086 SubSigParam::Hash(name, _) => self.declare_hash(name),
1087 SubSigParam::ArrayDestruct(elems) => {
1088 for e in elems {
1089 match e {
1090 MatchArrayElem::CaptureScalar(n) => self.declare_scalar(n),
1091 MatchArrayElem::RestBind(n) => self.declare_array(n),
1092 _ => {}
1093 }
1094 }
1095 }
1096 SubSigParam::HashDestruct(pairs) => {
1097 for (_, name) in pairs {
1098 self.declare_scalar(name);
1099 }
1100 }
1101 }
1102 }
1103
1104 fn analyze_block(&mut self, block: &Block) {
1105 for stmt in block {
1106 self.analyze_stmt(stmt);
1107 }
1108 }
1109
1110 fn check_constructor_keys(&mut self, type_name: &str, args: &[Expr], call_line: usize) {
1116 let bare_tail = type_name.rsplit("::").next().unwrap_or(type_name);
1117 let resolved = if self.type_fields.contains_key(type_name) {
1120 type_name.to_string()
1121 } else if self.type_fields.contains_key(bare_tail) {
1122 bare_tail.to_string()
1123 } else {
1124 return;
1125 };
1126 let mut all_fields: HashSet<String> = HashSet::new();
1130 let mut seen: HashSet<String> = HashSet::new();
1131 let mut queue: Vec<String> = vec![resolved.clone()];
1132 while let Some(c) = queue.pop() {
1133 if !seen.insert(c.clone()) {
1134 continue;
1135 }
1136 if let Some(fs) = self.type_fields.get(&c) {
1137 all_fields.extend(fs.iter().cloned());
1138 }
1139 if let Some(parents) = self.type_parents.get(&c) {
1140 queue.extend(parents.iter().cloned());
1141 }
1142 }
1143 let first_arg_is_field = args.first().is_some_and(|a| match &a.kind {
1150 ExprKind::String(s) | ExprKind::Bareword(s) => all_fields.contains(s),
1151 _ => false,
1152 });
1153 let looks_keyed = args.len().is_multiple_of(2)
1154 && first_arg_is_field
1155 && (0..args.len()).step_by(2).all(|i| {
1156 matches!(&args[i].kind, ExprKind::String(_))
1157 || matches!(&args[i].kind, ExprKind::Bareword(s) if !s.contains("::"))
1158 });
1159 if !looks_keyed {
1160 return;
1161 }
1162 let mut i = 0;
1163 while i < args.len() {
1164 let key_name = match &args[i].kind {
1165 ExprKind::String(s) => Some(s.clone()),
1166 ExprKind::Bareword(s) => Some(s.clone()),
1167 _ => None,
1168 };
1169 if let Some(name) = key_name {
1170 if !all_fields.contains(&name) {
1171 let line = if args[i].line > 0 {
1172 args[i].line
1173 } else {
1174 call_line
1175 };
1176 let bare_for_msg = type_name.rsplit("::").next().unwrap_or(type_name);
1177 self.error(
1178 ErrorKind::UndefinedSubroutine,
1179 format!(
1180 "Unknown field `{name}` in constructor call to `{bare_for_msg}` — \
1181 declared fields: {}",
1182 {
1183 let mut v: Vec<&String> = all_fields.iter().collect();
1184 v.sort();
1185 v.into_iter()
1186 .map(String::as_str)
1187 .collect::<Vec<_>>()
1188 .join(", ")
1189 }
1190 ),
1191 line,
1192 );
1193 }
1194 }
1195 i += 2;
1196 }
1197 }
1198
1199 fn method_resolves_in_hierarchy(&self, class_name: &str, method: &str) -> bool {
1203 if matches!(
1216 method,
1217 "isa"
1218 | "can"
1219 | "DOES"
1220 | "does"
1221 | "VERSION"
1222 | "new"
1223 | "BUILD"
1224 | "DESTROY"
1225 | "destroy"
1226 | "clone"
1227 | "with"
1228 | "to_hash"
1229 | "to_hash_rec"
1230 | "to_hash_deep"
1231 | "fields"
1232 | "methods"
1233 | "superclass"
1234 ) {
1235 return true;
1236 }
1237 let mut seen: HashSet<String> = HashSet::new();
1238 let mut queue: Vec<String> = vec![class_name.to_string()];
1239 while let Some(c) = queue.pop() {
1240 if !seen.insert(c.clone()) {
1241 continue;
1242 }
1243 if self.type_fields.get(&c).is_some_and(|s| s.contains(method)) {
1244 return true;
1245 }
1246 if self
1247 .type_methods
1248 .get(&c)
1249 .is_some_and(|s| s.contains(method))
1250 {
1251 return true;
1252 }
1253 if let Some(parents) = self.type_parents.get(&c) {
1254 queue.extend(parents.iter().cloned());
1255 }
1256 }
1257 false
1258 }
1259
1260 fn collect_hierarchy_members(&self, class_name: &str) -> Vec<String> {
1264 let mut seen: HashSet<String> = HashSet::new();
1265 let mut out: HashSet<String> = HashSet::new();
1266 let mut queue: Vec<String> = vec![class_name.to_string()];
1267 while let Some(c) = queue.pop() {
1268 if !seen.insert(c.clone()) {
1269 continue;
1270 }
1271 if let Some(fs) = self.type_fields.get(&c) {
1272 out.extend(fs.iter().cloned());
1273 }
1274 if let Some(ms) = self.type_methods.get(&c) {
1275 out.extend(ms.iter().cloned());
1276 }
1277 if let Some(parents) = self.type_parents.get(&c) {
1278 queue.extend(parents.iter().cloned());
1279 }
1280 }
1281 let mut v: Vec<String> = out.into_iter().collect();
1282 v.sort();
1283 v
1284 }
1285
1286 fn analyze_expr(&mut self, expr: &Expr) {
1287 match &expr.kind {
1288 ExprKind::ScalarVar(name)
1294 if self.strict_vars
1295 && name.len() > 1
1296 && name.starts_with('#')
1297 && !self.is_array_defined(&name[1..]) =>
1298 {
1299 self.error(
1300 ErrorKind::UndefinedVariable,
1301 format!(
1302 "Global symbol \"@{}\" requires explicit package name",
1303 &name[1..]
1304 ),
1305 expr.line,
1306 );
1307 }
1308 ExprKind::ScalarVar(name) if name.starts_with('#') => {
1309 }
1311 ExprKind::ScalarVar(name) if self.strict_vars && !self.is_scalar_defined(name) => {
1312 self.error(
1313 ErrorKind::UndefinedVariable,
1314 format!("Global symbol \"${}\" requires explicit package name", name),
1315 expr.line,
1316 );
1317 }
1318 ExprKind::ArrayVar(name) if self.strict_vars && !self.is_array_defined(name) => {
1319 self.error(
1320 ErrorKind::UndefinedVariable,
1321 format!("Global symbol \"@{}\" requires explicit package name", name),
1322 expr.line,
1323 );
1324 }
1325 ExprKind::HashVar(name) if self.strict_vars && !self.is_hash_defined(name) => {
1326 self.error(
1327 ErrorKind::UndefinedVariable,
1328 format!("Global symbol \"%{}\" requires explicit package name", name),
1329 expr.line,
1330 );
1331 }
1332 ExprKind::ArrayElement { array, index } => {
1333 if self.strict_vars
1334 && !array.starts_with("__topicstr__")
1335 && !self.is_array_defined(array)
1336 && !self.is_scalar_defined(array)
1337 {
1338 self.error(
1339 ErrorKind::UndefinedVariable,
1340 format!(
1341 "Global symbol \"@{}\" requires explicit package name",
1342 array
1343 ),
1344 expr.line,
1345 );
1346 }
1347 self.analyze_expr(index);
1348 }
1349 ExprKind::HashElement { hash, key } => {
1350 if self.strict_vars && !self.is_hash_defined(hash) && !self.is_scalar_defined(hash)
1351 {
1352 self.error(
1353 ErrorKind::UndefinedVariable,
1354 format!("Global symbol \"%{}\" requires explicit package name", hash),
1355 expr.line,
1356 );
1357 }
1358 self.analyze_expr(key);
1359 }
1360 ExprKind::ArraySlice { array, indices } => {
1361 if self.strict_vars && !self.is_array_defined(array) {
1362 self.error(
1363 ErrorKind::UndefinedVariable,
1364 format!(
1365 "Global symbol \"@{}\" requires explicit package name",
1366 array
1367 ),
1368 expr.line,
1369 );
1370 }
1371 for i in indices {
1372 self.analyze_expr(i);
1373 }
1374 }
1375 ExprKind::HashSlice { hash, keys } => {
1376 if self.strict_vars && !self.is_hash_defined(hash) {
1377 self.error(
1378 ErrorKind::UndefinedVariable,
1379 format!("Global symbol \"%{}\" requires explicit package name", hash),
1380 expr.line,
1381 );
1382 }
1383 for k in keys {
1384 self.analyze_expr(k);
1385 }
1386 }
1387 ExprKind::FuncCall { name, args } => {
1388 if !self.is_sub_defined(name) {
1389 self.error(
1390 ErrorKind::UndefinedSubroutine,
1391 format!("Undefined subroutine &{}", name),
1392 expr.line,
1393 );
1394 }
1395 self.check_constructor_keys(name, args, expr.line);
1398 for a in args {
1399 self.analyze_expr(a);
1400 }
1401 }
1402 ExprKind::MethodCall {
1403 object,
1404 method,
1405 args,
1406 ..
1407 } => {
1408 self.analyze_expr(object);
1409 if let ExprKind::Bareword(n) = &object.kind {
1411 self.check_constructor_keys(n, args, expr.line);
1412 if !self.type_fields.is_empty()
1419 && !self.type_fields.contains_key(n)
1420 && !self.is_sub_defined(n)
1421 {
1422 let bare = n.rsplit("::").next().unwrap_or(n);
1423 if !bare.is_empty()
1424 && bare.chars().next().is_some_and(|c| c.is_ascii_uppercase())
1425 {
1426 self.error(
1427 ErrorKind::UndefinedSubroutine,
1428 format!(
1429 "Unknown class `{n}` — `{n}->{method}` calls a constructor on a type that isn't declared in this file or its `require`d libs"
1430 ),
1431 expr.line,
1432 );
1433 }
1434 }
1435 }
1436 if let ExprKind::ScalarVar(name) = &object.kind {
1442 if name != "self" {
1443 if let Some(class_name) =
1444 self.resolve_scalar_type(name).map(|s| s.to_string())
1445 {
1446 if !self.method_resolves_in_hierarchy(&class_name, method) {
1447 let suggestions =
1448 self.collect_hierarchy_members(&class_name);
1449 let avail = if suggestions.is_empty() {
1450 "(no fields or methods declared)".to_string()
1451 } else {
1452 suggestions.join(", ")
1453 };
1454 self.error(
1455 ErrorKind::UndefinedSubroutine,
1456 format!(
1457 "`${name}->{method}` — no field or method `{method}` on `{class_name}`; available: {avail}",
1458 ),
1459 expr.line,
1460 );
1461 }
1462 }
1463 }
1464 }
1465 if let ExprKind::ScalarVar(name) = &object.kind {
1468 if name == "self" {
1469 if let Some(class_name) = self.current_class.clone() {
1470 if !self.method_resolves_in_hierarchy(&class_name, method) {
1474 let suggestions =
1475 self.collect_hierarchy_members(&class_name);
1476 let avail = if suggestions.is_empty() {
1477 "(no fields or methods declared)".to_string()
1478 } else {
1479 suggestions.join(", ")
1480 };
1481 self.error(
1482 ErrorKind::UndefinedSubroutine,
1483 format!(
1484 "`$self->{method}` — no field or method `{method}` on `{class_name}`; available: {avail}",
1485 ),
1486 expr.line,
1487 );
1488 }
1489 }
1490 }
1491 }
1492 for a in args {
1493 self.analyze_expr(a);
1494 }
1495 }
1496 ExprKind::IndirectCall { target, args, .. } => {
1497 self.analyze_expr(target);
1498 for a in args {
1499 self.analyze_expr(a);
1500 }
1501 }
1502 ExprKind::BinOp { left, right, .. } => {
1503 self.analyze_expr(left);
1504 self.analyze_expr(right);
1505 }
1506 ExprKind::UnaryOp { expr: e, .. } => {
1507 self.analyze_expr(e);
1508 }
1509 ExprKind::PostfixOp { expr: e, .. } => {
1510 self.analyze_expr(e);
1511 }
1512 ExprKind::Assign { target, value } => {
1513 if let ExprKind::ScalarVar(name) = &target.kind {
1514 self.declare_scalar(name);
1515 } else if let ExprKind::ArrayVar(name) = &target.kind {
1516 self.declare_array(name);
1517 } else if let ExprKind::HashVar(name) = &target.kind {
1518 self.declare_hash(name);
1519 } else {
1520 self.analyze_expr(target);
1521 }
1522 self.analyze_expr(value);
1523 }
1524 ExprKind::MyExpr { keyword, decls } => {
1533 let is_package_global = keyword == "our";
1534 for d in decls {
1535 match d.sigil {
1536 Sigil::Scalar => {
1537 self.declare_scalar(&d.name);
1538 if is_package_global {
1539 let q = format!("{}::{}", self.current_package, d.name);
1540 self.declare_scalar(&q);
1541 }
1542 }
1543 Sigil::Array => {
1544 self.declare_array(&d.name);
1545 if is_package_global {
1546 let q = format!("{}::{}", self.current_package, d.name);
1547 self.declare_array(&q);
1548 }
1549 }
1550 Sigil::Hash => {
1551 self.declare_hash(&d.name);
1552 if is_package_global {
1553 let q = format!("{}::{}", self.current_package, d.name);
1554 self.declare_hash(&q);
1555 }
1556 }
1557 Sigil::Typeglob => {}
1558 }
1559 if let Some(init) = &d.initializer {
1560 self.analyze_expr(init);
1561 }
1562 }
1563 }
1564 ExprKind::CompoundAssign { target, value, .. } => {
1565 self.analyze_expr(target);
1566 self.analyze_expr(value);
1567 }
1568 ExprKind::Ternary {
1569 condition,
1570 then_expr,
1571 else_expr,
1572 } => {
1573 self.analyze_expr(condition);
1574 self.analyze_expr(then_expr);
1575 self.analyze_expr(else_expr);
1576 }
1577 ExprKind::List(exprs) | ExprKind::ArrayRef(exprs) => {
1578 for e in exprs {
1579 self.analyze_expr(e);
1580 }
1581 }
1582 ExprKind::HashRef(pairs) => {
1583 for (k, v) in pairs {
1584 self.analyze_expr(k);
1585 self.analyze_expr(v);
1586 }
1587 }
1588 ExprKind::CodeRef { params, body } => {
1589 self.push_scope();
1590 for p in params {
1591 self.declare_param(p);
1592 }
1593 self.analyze_block(body);
1594 self.pop_scope();
1595 }
1596 ExprKind::ScalarRef(e)
1597 | ExprKind::Deref { expr: e, .. }
1598 | ExprKind::Defined(e)
1599 | ExprKind::Delete(e) => {
1600 self.analyze_expr(e);
1601 }
1602 ExprKind::Exists(e)
1603 if !matches!(
1611 e.kind,
1612 ExprKind::SubroutineCodeRef(_) | ExprKind::SubroutineRef(_)
1613 ) => {
1614 self.analyze_expr(e);
1615 }
1616 ExprKind::ArrowDeref { expr, index, kind } => {
1617 self.analyze_expr(expr);
1618 if *kind != DerefKind::Call {
1619 self.analyze_expr(index);
1620 }
1621 }
1622 ExprKind::Range { from, to, step, .. } => {
1623 self.analyze_expr(from);
1624 self.analyze_expr(to);
1625 if let Some(s) = step {
1626 self.analyze_expr(s);
1627 }
1628 }
1629 ExprKind::SliceRange { from, to, step } => {
1630 if let Some(f) = from {
1631 self.analyze_expr(f);
1632 }
1633 if let Some(t) = to {
1634 self.analyze_expr(t);
1635 }
1636 if let Some(s) = step {
1637 self.analyze_expr(s);
1638 }
1639 }
1640 ExprKind::InterpolatedString(parts) => {
1641 for part in parts {
1653 match part {
1654 StringPart::Expr(e) => {
1655 match &e.kind {
1661 ExprKind::ScalarVar(_)
1662 | ExprKind::ArrayVar(_)
1663 | ExprKind::HashVar(_) => {}
1664 _ => self.analyze_expr(e),
1665 }
1666 }
1667 StringPart::ScalarVar(_)
1668 | StringPart::ArrayVar(_)
1669 | StringPart::Literal(_) => {}
1670 }
1671 }
1672 }
1673 ExprKind::Regex(_, _)
1674 | ExprKind::Substitution { .. }
1675 | ExprKind::Transliterate { .. }
1676 | ExprKind::Match { .. } => {}
1677 ExprKind::HashSliceDeref { container, keys } => {
1678 self.analyze_expr(container);
1679 for k in keys {
1680 self.analyze_expr(k);
1681 }
1682 }
1683 ExprKind::AnonymousListSlice { source, indices } => {
1684 self.analyze_expr(source);
1685 for i in indices {
1686 self.analyze_expr(i);
1687 }
1688 }
1689 ExprKind::SubroutineRef(name) | ExprKind::SubroutineCodeRef(name)
1690 if !self.is_sub_defined(name) =>
1691 {
1692 self.error(
1693 ErrorKind::UndefinedSubroutine,
1694 format!("Undefined subroutine &{}", name),
1695 expr.line,
1696 );
1697 }
1698 ExprKind::DynamicSubCodeRef(e) => self.analyze_expr(e),
1699 ExprKind::PostfixIf { expr, condition }
1700 | ExprKind::PostfixUnless { expr, condition }
1701 | ExprKind::PostfixWhile { expr, condition }
1702 | ExprKind::PostfixUntil { expr, condition } => {
1703 self.analyze_expr(expr);
1704 self.analyze_expr(condition);
1705 }
1706 ExprKind::PostfixForeach { expr, list } => {
1707 self.analyze_expr(list);
1708 self.analyze_expr(expr);
1709 }
1710 ExprKind::Do(e) | ExprKind::Eval(e) => {
1711 self.analyze_expr(e);
1712 }
1713 ExprKind::Caller(Some(e)) => {
1714 self.analyze_expr(e);
1715 }
1716 ExprKind::Length(e) => {
1717 self.analyze_expr(e);
1718 }
1719 ExprKind::Print { args, .. }
1720 | ExprKind::Say { args, .. }
1721 | ExprKind::Printf { args, .. } => {
1722 for a in args {
1723 self.analyze_expr(a);
1724 }
1725 }
1726 ExprKind::Die(args)
1727 | ExprKind::Warn(args)
1728 | ExprKind::Unlink(args)
1729 | ExprKind::Chmod(args)
1730 | ExprKind::System(args)
1731 | ExprKind::Exec(args) => {
1732 for a in args {
1733 self.analyze_expr(a);
1734 }
1735 }
1736 ExprKind::Push { array, values } | ExprKind::Unshift { array, values } => {
1737 self.analyze_expr(array);
1738 for v in values {
1739 self.analyze_expr(v);
1740 }
1741 }
1742 ExprKind::Splice {
1743 array,
1744 offset,
1745 length,
1746 replacement,
1747 } => {
1748 self.analyze_expr(array);
1749 if let Some(o) = offset {
1750 self.analyze_expr(o);
1751 }
1752 if let Some(l) = length {
1753 self.analyze_expr(l);
1754 }
1755 for r in replacement {
1756 self.analyze_expr(r);
1757 }
1758 }
1759 ExprKind::MapExpr { block, list, .. } | ExprKind::GrepExpr { block, list, .. } => {
1760 self.push_scope();
1761 self.analyze_block(block);
1762 self.pop_scope();
1763 self.analyze_expr(list);
1764 }
1765 ExprKind::SortExpr { list, .. } => {
1766 self.analyze_expr(list);
1767 }
1768 ExprKind::Open { handle, mode, file } => {
1769 if let ExprKind::OpenMyHandle { name } = &handle.kind {
1773 self.declare_scalar(name);
1774 } else {
1775 self.analyze_expr(handle);
1776 }
1777 self.analyze_expr(mode);
1778 if let Some(f) = file {
1779 self.analyze_expr(f);
1780 }
1781 }
1782 ExprKind::Opendir { handle, path } => {
1783 if let ExprKind::OpendirMyHandle { name } = &handle.kind {
1785 self.declare_scalar(name);
1786 } else {
1787 self.analyze_expr(handle);
1788 }
1789 self.analyze_expr(path);
1790 }
1791 ExprKind::Close(e)
1792 | ExprKind::Pop(e)
1793 | ExprKind::Shift(e)
1794 | ExprKind::Keys(e)
1795 | ExprKind::Values(e)
1796 | ExprKind::Each(e)
1797 | ExprKind::Chdir(e)
1798 | ExprKind::Require(e)
1799 | ExprKind::Ref(e)
1800 | ExprKind::Chomp(e)
1801 | ExprKind::Chop(e)
1802 | ExprKind::Lc(e)
1803 | ExprKind::Uc(e)
1804 | ExprKind::Lcfirst(e)
1805 | ExprKind::Ucfirst(e)
1806 | ExprKind::Abs(e)
1807 | ExprKind::Int(e)
1808 | ExprKind::Sqrt(e)
1809 | ExprKind::Sin(e)
1810 | ExprKind::Cos(e)
1811 | ExprKind::Exp(e)
1812 | ExprKind::Log(e)
1813 | ExprKind::Chr(e)
1814 | ExprKind::Ord(e)
1815 | ExprKind::Hex(e)
1816 | ExprKind::Oct(e)
1817 | ExprKind::Readlink(e)
1818 | ExprKind::Readdir(e)
1819 | ExprKind::Closedir(e)
1820 | ExprKind::Rewinddir(e)
1821 | ExprKind::Telldir(e) => {
1822 self.analyze_expr(e);
1823 }
1824 ExprKind::Exit(Some(e)) | ExprKind::Rand(Some(e)) | ExprKind::Eof(Some(e)) => {
1825 self.analyze_expr(e);
1826 }
1827 ExprKind::Mkdir { path, mode } => {
1828 self.analyze_expr(path);
1829 if let Some(m) = mode {
1830 self.analyze_expr(m);
1831 }
1832 }
1833 ExprKind::Rename { old, new }
1834 | ExprKind::Link { old, new }
1835 | ExprKind::Symlink { old, new } => {
1836 self.analyze_expr(old);
1837 self.analyze_expr(new);
1838 }
1839 ExprKind::Chown(files) => {
1840 for f in files {
1841 self.analyze_expr(f);
1842 }
1843 }
1844 ExprKind::Substr {
1845 string,
1846 offset,
1847 length,
1848 replacement,
1849 } => {
1850 self.analyze_expr(string);
1851 self.analyze_expr(offset);
1852 if let Some(l) = length {
1853 self.analyze_expr(l);
1854 }
1855 if let Some(r) = replacement {
1856 self.analyze_expr(r);
1857 }
1858 }
1859 ExprKind::Index {
1860 string,
1861 substr,
1862 position,
1863 }
1864 | ExprKind::Rindex {
1865 string,
1866 substr,
1867 position,
1868 } => {
1869 self.analyze_expr(string);
1870 self.analyze_expr(substr);
1871 if let Some(p) = position {
1872 self.analyze_expr(p);
1873 }
1874 }
1875 ExprKind::Sprintf { format, args } => {
1876 self.analyze_expr(format);
1877 for a in args {
1878 self.analyze_expr(a);
1879 }
1880 }
1881 ExprKind::Bless { ref_expr, class } => {
1882 self.analyze_expr(ref_expr);
1883 if let Some(c) = class {
1884 self.analyze_expr(c);
1885 }
1886 }
1887 ExprKind::AlgebraicMatch { subject, arms } => {
1888 self.analyze_expr(subject);
1889 for arm in arms {
1890 self.check_match_pattern(&arm.pattern, expr.line);
1891 if let Some(g) = &arm.guard {
1892 self.analyze_expr(g);
1893 }
1894 self.analyze_expr(&arm.body);
1895 }
1896 }
1897 _ => {}
1898 }
1899 }
1900
1901 fn check_match_pattern(&mut self, pat: &crate::ast::MatchPattern, line: usize) {
1909 use crate::ast::{MatchArrayElem, MatchHashPair, MatchPattern};
1910 match pat {
1911 MatchPattern::Value(e) => {
1912 if let ExprKind::String(s) = &e.kind {
1913 self.check_qualified_variant_string(s, line);
1914 }
1915 self.analyze_expr(e);
1916 }
1917 MatchPattern::Array(elems) => {
1918 for el in elems {
1919 if let MatchArrayElem::Expr(e) = el {
1920 self.analyze_expr(e);
1921 }
1922 }
1923 }
1924 MatchPattern::Hash(pairs) => {
1925 for p in pairs {
1926 match p {
1927 MatchHashPair::KeyOnly { key } => self.analyze_expr(key),
1928 MatchHashPair::Capture { key, .. } => self.analyze_expr(key),
1929 }
1930 }
1931 }
1932 MatchPattern::Any | MatchPattern::Regex { .. } | MatchPattern::OptionSome(_) => {}
1933 }
1934 }
1935
1936 fn check_qualified_variant_string(&mut self, s: &str, line: usize) {
1942 let Some(idx) = s.rfind("::") else { return };
1943 let type_name = &s[..idx];
1944 let variant = &s[idx + 2..];
1945 if type_name.is_empty() || variant.is_empty() {
1946 return;
1947 }
1948 let type_bare = type_name.rsplit("::").next().unwrap_or(type_name);
1949 let known = self
1950 .type_fields
1951 .get(type_name)
1952 .or_else(|| self.type_fields.get(type_bare));
1953 let Some(variants) = known else { return };
1954 if variants.contains(variant) {
1955 return;
1956 }
1957 let mut available: Vec<&str> = variants.iter().map(String::as_str).collect();
1958 available.sort();
1959 let avail = if available.is_empty() {
1960 "(no variants declared)".to_string()
1961 } else {
1962 available.join(", ")
1963 };
1964 self.error(
1965 ErrorKind::UndefinedSubroutine,
1966 format!(
1967 "`{type_name}::{variant}` — no variant `{variant}` on `{type_name}`; available: {avail}"
1968 ),
1969 line,
1970 );
1971 }
1972}
1973
1974fn is_ident_byte(b: u8) -> bool {
1975 b.is_ascii_alphanumeric() || b == b'_' || b == b':'
1976}
1977
1978fn is_special_var(name: &str) -> bool {
1979 if let Some(rest) = name.strip_prefix("main::") {
1988 return is_special_var(rest);
1989 }
1990 if name.len() == 1 {
1991 return true;
1992 }
1993 if name.starts_with('^') {
1999 return true;
2000 }
2001 if name == "$$" {
2006 return true;
2007 }
2008 if crate::builtins::is_special_var(&format!("${}", name))
2018 || crate::builtins::is_special_var(&format!("@{}", name))
2019 || crate::builtins::is_special_var(&format!("%{}", name))
2020 || crate::builtins::is_special_var(name)
2021 {
2022 return true;
2023 }
2024 matches!(
2025 name,
2026 "ARGV"
2027 | "ENV"
2028 | "SIG"
2029 | "INC"
2030 | "AUTOLOAD"
2031 | "STDERR"
2032 | "STDIN"
2033 | "STDOUT"
2034 | "DATA"
2035 | "UNIVERSAL"
2036 | "VERSION"
2037 | "ISA"
2038 | "EXPORT"
2039 | "EXPORT_OK"
2040 | "INTERCEPT_NAME"
2045 | "INTERCEPT_ARGS"
2046 | "INTERCEPT_RESULT"
2047 | "INTERCEPT_MS"
2048 | "INTERCEPT_US"
2049 | "stryke::VERSION"
2052 )
2053}
2054
2055fn is_topic_var(name: &str) -> bool {
2063 if !name.starts_with('_') {
2073 return false;
2074 }
2075 let rest = &name[1..];
2076 if rest.is_empty() {
2077 return true; }
2079 let bytes = rest.as_bytes();
2080 let mut i = 0;
2081 while i < bytes.len() && bytes[i].is_ascii_digit() {
2083 i += 1;
2084 }
2085 let digits_consumed = i;
2086 if i == bytes.len() {
2087 return digits_consumed > 0;
2089 }
2090 if bytes[i] != b'<' {
2092 return false;
2093 }
2094 while i < bytes.len() && bytes[i] == b'<' {
2095 i += 1;
2096 }
2097 while i < bytes.len() && bytes[i].is_ascii_digit() {
2099 i += 1;
2100 }
2101 i == bytes.len()
2102}
2103
2104pub fn resolve_require_path_from_file(file: &str, spec: &str) -> Option<PathBuf> {
2113 resolve_require_path_from_file_versioned(file, spec, None)
2114}
2115
2116pub fn resolve_require_path_from_file_versioned(
2122 file: &str,
2123 spec: &str,
2124 pin_version: Option<&str>,
2125) -> Option<PathBuf> {
2126 let p = Path::new(spec);
2127 if p.is_absolute() {
2128 return p.exists().then(|| p.to_path_buf());
2129 }
2130 let file_dir = Path::new(file)
2131 .parent()
2132 .map(Path::to_path_buf)
2133 .unwrap_or_else(|| PathBuf::from("."));
2134 let project_root = find_project_root(&file_dir);
2142 let candidate_via_root = if spec.starts_with("./") || spec.starts_with("../") {
2145 project_root.join(spec)
2146 } else if spec.contains("::") {
2147 let relpath: PathBuf = PathBuf::from(spec.replace("::", "/")).with_extension("pm");
2148 project_root.join("lib").join(relpath)
2149 } else {
2150 project_root.join(spec)
2151 };
2152 if candidate_via_root.exists() {
2153 return Some(candidate_via_root);
2154 }
2155 if spec.starts_with("./") || spec.starts_with("../") {
2158 let direct = file_dir.join(spec);
2159 if direct.exists() {
2160 return Some(direct);
2161 }
2162 }
2163
2164 if !spec.starts_with("./") && !spec.starts_with("../") {
2169 let segments: Vec<&str> = spec.split("::").collect();
2170 if !segments.is_empty() && !segments.iter().any(|s| s.is_empty()) {
2171 let mut local_stk = project_root.join("lib");
2172 for s in &segments[..segments.len() - 1] {
2173 local_stk = local_stk.join(s);
2174 }
2175 local_stk = local_stk.join(format!("{}.stk", segments[segments.len() - 1]));
2176 if local_stk.exists() {
2177 return Some(local_stk);
2178 }
2179 }
2180 }
2181
2182 let segments: Vec<&str> = spec.split("::").collect();
2189 if !segments.is_empty() && !segments.iter().any(|s| s.is_empty()) {
2190 if let Some(v) = pin_version {
2191 if let Ok(store) = crate::pkg::store::Store::user_default() {
2192 let pkg_name_lower = segments[0].to_lowercase();
2193 for nm in [pkg_name_lower.clone(), format!("stryke-{}", pkg_name_lower)] {
2194 let store_pkg = store.package_dir(&nm, v);
2195 let resolved = build_pkg_lib_path(&store_pkg, &segments);
2196 if resolved.is_file() {
2197 return Some(resolved);
2198 }
2199 }
2200 }
2201 return None;
2205 }
2206 }
2207 if !segments.is_empty() && !segments.iter().any(|s| s.is_empty()) {
2215 if let Some(stryke_root) = crate::pkg::commands::find_project_root(Path::new(file)) {
2216 let lock_path = stryke_root.join(crate::pkg::commands::LOCKFILE_FILE);
2217 if lock_path.is_file() {
2218 if let Ok(lf) = crate::pkg::lockfile::Lockfile::from_path(&lock_path) {
2219 let pkg_name = segments[0].to_lowercase();
2220 let prefixed = format!("stryke-{}", pkg_name);
2221 let entry = lf.find(&pkg_name).or_else(|| lf.find(&prefixed));
2222 if let Some(entry) = entry {
2223 if let Ok(store) = crate::pkg::store::Store::user_default() {
2224 for store_pkg in store_dir_candidates(&store, &entry.name, &entry.version) {
2231 let resolved = build_pkg_lib_path(&store_pkg, &segments);
2232 if resolved.is_file() {
2233 return Some(resolved);
2234 }
2235 }
2236 }
2237 }
2238 }
2239 }
2240 }
2241
2242 if let Ok(store) = crate::pkg::store::Store::user_default() {
2250 let names = crate::pkg::commands::canonical_store_names_for_namespace(
2251 &store,
2252 &segments[0].to_lowercase(),
2253 );
2254 if let Some((name, version)) =
2255 crate::pkg::commands::scan_store_for_highest_version(&store, &names)
2256 {
2257 let store_pkg = store.package_dir(&name, &version);
2258 let resolved = build_pkg_lib_path(&store_pkg, &segments);
2259 if resolved.is_file() {
2260 return Some(resolved);
2261 }
2262 }
2263 }
2264 }
2265
2266 None
2267}
2268
2269fn store_dir_candidates(
2273 store: &crate::pkg::store::Store,
2274 name: &str,
2275 version: &str,
2276) -> Vec<PathBuf> {
2277 let mut out = Vec::with_capacity(2);
2278 out.push(store.package_dir(name, version));
2279 if !name.starts_with("stryke-") {
2280 out.push(store.package_dir(&format!("stryke-{}", name), version));
2281 }
2282 out
2283}
2284
2285fn build_pkg_lib_path(store_pkg: &Path, segments: &[&str]) -> PathBuf {
2293 debug_assert!(!segments.is_empty());
2294 let mut p = store_pkg.join("lib");
2295 if segments.len() == 1 {
2296 p.join(format!("{}.stk", segments[0]))
2297 } else {
2298 for s in &segments[1..segments.len() - 1] {
2299 p = p.join(s);
2300 }
2301 p.join(format!("{}.stk", segments[segments.len() - 1]))
2302 }
2303}
2304
2305fn find_project_root(start_dir: &Path) -> PathBuf {
2310 let mut cur = start_dir.to_path_buf();
2311 for _ in 0..16 {
2312 if cur.join("lib").is_dir() {
2314 return cur;
2315 }
2316 match cur.parent() {
2317 Some(p) if p != cur => cur = p.to_path_buf(),
2318 _ => break,
2319 }
2320 }
2321 match start_dir.file_name().and_then(|s| s.to_str()) {
2322 Some("t") | Some("tests") | Some("test") | Some("spec") | Some("xt") => start_dir
2323 .parent()
2324 .map(Path::to_path_buf)
2325 .unwrap_or_else(|| start_dir.to_path_buf()),
2326 _ => start_dir.to_path_buf(),
2327 }
2328}
2329
2330pub fn static_string_value(e: &Expr) -> Option<String> {
2334 match &e.kind {
2335 ExprKind::String(s) => Some(s.clone()),
2336 ExprKind::Bareword(s) => Some(s.clone()),
2337 ExprKind::InterpolatedString(parts) => {
2338 let mut out = String::new();
2340 for part in parts {
2341 match part {
2342 StringPart::Literal(s) => out.push_str(s),
2343 _ => return None,
2344 }
2345 }
2346 Some(out)
2347 }
2348 _ => None,
2349 }
2350}
2351pub fn analyze_program(program: &Program, file: &str) -> StrykeResult<()> {
2353 StaticAnalyzer::new(file).analyze(program)
2354}
2355
2356pub fn analyze_program_with_strict(
2361 program: &Program,
2362 file: &str,
2363 strict_vars: bool,
2364) -> StrykeResult<()> {
2365 StaticAnalyzer::with_strict_vars(file, strict_vars).analyze(program)
2366}
2367
2368#[cfg(test)]
2369mod tests {
2370 use super::*;
2371 use crate::parse_with_file;
2372
2373 fn lint(code: &str) -> StrykeResult<()> {
2380 let prog = parse_with_file(code, "test.stk").expect("parse");
2381 analyze_program_with_strict(&prog, "test.stk", true)
2382 }
2383
2384 #[test]
2385 fn undefined_scalar_detected() {
2386 let r = lint("p $undefined");
2387 assert!(r.is_err());
2388 let e = r.unwrap_err();
2389 assert_eq!(e.kind, ErrorKind::UndefinedVariable);
2390 assert!(e.message.contains("$undefined"));
2391 }
2392
2393 #[test]
2394 fn defined_scalar_ok() {
2395 assert!(lint("my $x = 1; p $x").is_ok());
2396 }
2397
2398 #[test]
2399 fn undefined_sub_detected() {
2400 let r = lint("nonexistent_function()");
2401 assert!(r.is_err());
2402 let e = r.unwrap_err();
2403 assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
2404 assert!(e.message.contains("nonexistent_function"));
2405 }
2406
2407 #[test]
2408 fn defined_sub_ok() {
2409 assert!(lint("fn foo { 1 } foo()").is_ok());
2410 }
2411
2412 #[test]
2413 fn builtin_sub_ok() {
2414 assert!(lint("p 'hello'").is_ok());
2415 assert!(lint("print 'hello'").is_ok());
2416 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
2417 }
2418
2419 #[test]
2420 fn special_vars_ok() {
2421 assert!(lint("p $_").is_ok());
2422 assert!(lint("p @_").is_ok());
2423 assert!(lint("p $a <=> $b").is_ok());
2424 }
2425
2426 #[test]
2434 fn my_in_while_condition_scopes_var_for_body() {
2435 assert!(lint("while (my $line = readline(STDIN)) { p $line; }").is_ok());
2436 }
2437
2438 #[test]
2439 fn my_in_if_condition_scopes_var_for_body() {
2440 assert!(lint("if (my $x = 42) { p $x; }").is_ok());
2441 }
2442
2443 #[test]
2444 fn my_in_until_condition_scopes_var_for_body() {
2445 assert!(lint("until (my $done = 1) { p $done; last; }").is_ok());
2446 }
2447
2448 #[test]
2452 fn our_in_expression_position_registers_qualified_spelling() {
2453 assert!(
2454 lint("package Foo; if (our $cfg = 1) { p $cfg; p $Foo::cfg; }").is_ok(),
2455 "expression-position `our` should register both `$cfg` and `$Foo::cfg`"
2456 );
2457 }
2458
2459 #[test]
2467 fn my_in_expression_position_array_and_hash() {
2468 assert!(lint("if (my @rows = (1, 2, 3)) { p $_ for @rows; }").is_ok());
2469 assert!(lint("if (my %m = (a => 1)) { p $_ for keys %m; }").is_ok());
2470 }
2471
2472 #[test]
2476 fn undefined_var_still_flagged_after_relaxations() {
2477 let r = lint("p $deliberately_never_declared");
2478 assert!(r.is_err(), "strict-vars must still catch real undefs");
2479 assert_eq!(r.unwrap_err().kind, ErrorKind::UndefinedVariable);
2480 }
2481
2482 #[test]
2496 fn sigil_agnostic_special_var_does_not_pass_arbitrary_names() {
2497 for src in [
2498 "p $absolutely_not_a_special_var",
2499 "my %x = %totally_made_up_reflection_hash",
2500 "my @y = @arr_that_was_never_declared",
2501 ] {
2502 let r = lint(src);
2503 assert!(
2504 r.is_err(),
2505 "strict-vars must still flag arbitrary undef vars, accepted: {src}"
2506 );
2507 assert_eq!(r.unwrap_err().kind, ErrorKind::UndefinedVariable);
2508 }
2509 }
2510
2511 #[test]
2527 fn nested_my_in_conditions_both_scopes_propagate() {
2528 assert!(
2529 lint(
2530 "while (my $a = readline(STDIN)) { \
2531 if (my $b = length($a)) { p \"$a → $b\"; } \
2532 }"
2533 )
2534 .is_ok(),
2535 "nested MyExpr in while+if must scope both decls correctly"
2536 );
2537 }
2538
2539 #[test]
2549 fn reflection_and_meta_special_vars_ok_under_strict() {
2550 for src in [
2551 "p scalar keys %all",
2552 "p scalar keys %b",
2553 "p scalar keys %limits",
2554 "p scalar keys %parameters",
2555 "p scalar keys %pc",
2556 "p scalar keys %term",
2557 "p scalar keys %uname",
2558 "p scalar keys %stryke::all",
2559 "p scalar keys %stryke::builtins",
2560 "p scalar keys %stryke::aliases",
2561 "p scalar keys %stryke::categories",
2562 "p scalar keys %stryke::descriptions",
2563 "p scalar keys %stryke::extensions",
2564 "p scalar keys %stryke::keywords",
2565 "p scalar keys %stryke::operators",
2566 "p scalar keys %stryke::perl_compats",
2567 "p scalar keys %stryke::primaries",
2568 "p scalar keys %stryke::special_vars",
2569 "p scalar keys %overload::",
2570 "p $stryke::VERSION",
2571 "p __FILE__",
2572 "p __LINE__",
2573 "p __PACKAGE__",
2574 "p $main::ARGV",
2581 "p $main::_",
2582 "p $main::!",
2583 "p $main::@",
2584 "p $main::/",
2585 "p $main::0",
2586 "p @main::ARGV",
2587 "p @main::INC",
2588 "p @main::F",
2589 "p %main::ENV",
2590 "p %main::INC",
2591 "p %main::SIG",
2592 "p %main::stryke::all",
2593 ] {
2594 assert!(
2595 lint(src).is_ok(),
2596 "strict-vars false-positive on legitimate stryke special var: {src}"
2597 );
2598 }
2599 }
2600
2601 #[test]
2602 fn foreach_var_in_scope() {
2603 assert!(lint("foreach my $i (1..3) { p $i; }").is_ok());
2604 }
2605
2606 #[test]
2607 fn sub_params_in_scope() {
2608 assert!(lint("fn foo($x) { p $x; } foo(1)").is_ok());
2609 }
2610
2611 #[test]
2612 fn assignment_declares_var() {
2613 assert!(lint("$x = 1; p $x").is_ok());
2614 }
2615
2616 #[test]
2617 fn builtin_inc_ok() {
2618 assert!(lint("my $x = 1; inc($x)").is_ok());
2619 }
2620
2621 #[test]
2622 fn builtin_dec_ok() {
2623 assert!(lint("my $x = 1; dec($x)").is_ok());
2624 }
2625
2626 #[test]
2627 fn builtin_rev_ok() {
2628 assert!(lint("my $s = rev 'hello'").is_ok());
2629 }
2630
2631 #[test]
2632 fn builtin_p_alias_for_say_ok() {
2633 assert!(lint("p 'hello'").is_ok());
2634 }
2635
2636 #[test]
2637 fn builtin_t_thread_ok() {
2638 assert!(lint("t 1 inc inc").is_ok());
2639 }
2640
2641 #[test]
2642 fn thread_with_undefined_var_detected() {
2643 let r = lint("t $undefined inc");
2644 assert!(r.is_err());
2645 }
2646
2647 #[test]
2648 fn try_catch_var_in_scope() {
2649 assert!(lint("try { die 'err'; } catch ($e) { p $e; }").is_ok());
2650 }
2651
2652 #[test]
2653 fn interpolated_string_undefined_var() {
2654 assert!(
2661 lint(r#"p "hello $undefined""#).is_ok(),
2662 "undefined scalar inside string-interp must NOT flag",
2663 );
2664 assert!(lint(r#"use strict; p $undefined"#).is_err());
2666 }
2667
2668 #[test]
2669 fn interpolated_string_defined_var_ok() {
2670 assert!(lint(r#"my $x = 1; p "hello $x""#).is_ok());
2671 }
2672
2673 #[test]
2674 fn coderef_params_in_scope() {
2675 assert!(lint("my $f = fn ($x) { p $x; }; $f->(1)").is_ok());
2676 }
2677
2678 #[test]
2679 fn nested_sub_scope() {
2680 assert!(lint("fn wrap { my $x = 1; fn inner { p $x; } }").is_ok());
2681 }
2682
2683 #[test]
2684 fn hash_element_access_ok() {
2685 assert!(lint("my %h = (a => 1); p $h{a}").is_ok());
2686 }
2687
2688 #[test]
2689 fn array_element_access_ok() {
2690 assert!(lint("my @a = (1, 2, 3); p $a[0]").is_ok());
2691 }
2692
2693 #[test]
2694 fn undefined_hash_detected() {
2695 let r = lint("p $undefined_hash{key}");
2696 assert!(r.is_err());
2697 }
2698
2699 #[test]
2700 fn undefined_array_detected() {
2701 let r = lint("p $undefined_array[0]");
2702 assert!(r.is_err());
2703 }
2704
2705 #[test]
2706 fn map_with_topic_ok() {
2707 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
2708 }
2709
2710 #[test]
2711 fn grep_with_topic_ok() {
2712 assert!(lint("my @x = grep { $_ > 1 } 1..3").is_ok());
2713 }
2714
2715 #[test]
2716 fn sort_with_ab_ok() {
2717 assert!(lint("my @x = sort { $a <=> $b } 1..3").is_ok());
2718 }
2719
2720 #[test]
2721 fn ternary_undefined_var_detected() {
2722 let r = lint("my $x = $undefined ? 1 : 0");
2723 assert!(r.is_err());
2724 }
2725
2726 #[test]
2727 fn binop_undefined_var_detected() {
2728 let r = lint("my $x = 1 + $undefined");
2729 assert!(r.is_err());
2730 }
2731
2732 #[test]
2733 fn postfix_if_undefined_detected() {
2734 let r = lint("p 'x' if $undefined");
2735 assert!(r.is_err());
2736 }
2737
2738 #[test]
2739 fn while_loop_var_ok() {
2740 assert!(lint("my $i = 0; while ($i < 10) { p $i; $i++; }").is_ok());
2741 }
2742
2743 #[test]
2744 fn for_loop_init_var_in_scope() {
2745 assert!(lint("for (my $i = 0; $i < 10; $i++) { p $i; }").is_ok());
2746 }
2747
2748 #[test]
2749 fn given_when_ok() {
2750 assert!(lint("my $x = 1; given ($x) { when (1) { p 'one'; } }").is_ok());
2751 }
2752
2753 #[test]
2754 fn arrow_deref_ok() {
2755 assert!(lint("my $h = { a => 1 }; p $h->{a}").is_ok());
2756 }
2757
2758 #[test]
2759 fn method_call_ok() {
2760 assert!(lint("my $obj = bless {}, 'Foo'; $obj->method()").is_ok());
2761 }
2762
2763 #[test]
2764 fn push_builtin_ok() {
2765 assert!(lint("my @a; push @a, 1, 2, 3").is_ok());
2766 }
2767
2768 #[test]
2769 fn splice_builtin_ok() {
2770 assert!(lint("my @a = (1, 2, 3); splice @a, 1, 1, 'x'").is_ok());
2771 }
2772
2773 #[test]
2774 fn substr_builtin_ok() {
2775 assert!(lint("my $s = 'hello'; p substr($s, 0, 2)").is_ok());
2776 }
2777
2778 #[test]
2779 fn sprintf_builtin_ok() {
2780 assert!(lint("my $s = sprintf('%d', 42)").is_ok());
2781 }
2782
2783 #[test]
2784 fn range_ok() {
2785 assert!(lint("my @a = 1..10").is_ok());
2786 }
2787
2788 #[test]
2789 fn qw_ok() {
2790 assert!(lint("my @a = qw(a b c)").is_ok());
2791 }
2792
2793 #[test]
2794 fn regex_ok() {
2795 assert!(lint("my $x = 'hello'; $x =~ /ell/").is_ok());
2796 }
2797
2798 #[test]
2799 fn anonymous_sub_captures_outer_var() {
2800 assert!(lint("my $x = 1; my $f = fn { p $x; }").is_ok());
2801 }
2802
2803 #[test]
2804 fn state_var_ok() {
2805 assert!(lint("fn Test::counter { state $n = 0; $n++; }").is_ok());
2806 }
2807
2808 #[test]
2809 fn our_var_ok() {
2810 assert!(lint("our $VERSION = '1.0'").is_ok());
2811 }
2812
2813 #[test]
2814 fn local_var_ok() {
2815 assert!(lint("local $/ = undef").is_ok());
2816 }
2817
2818 #[test]
2819 fn chained_method_calls_ok() {
2820 assert!(lint("my $x = Foo->new->bar->baz").is_ok());
2821 }
2822
2823 #[test]
2824 fn list_assignment_ok() {
2825 assert!(lint("my ($a, $b, $c) = (1, 2, 3); p $a + $b + $c").is_ok());
2826 }
2827
2828 #[test]
2829 fn hash_slice_ok() {
2830 assert!(lint("my %h = (a => 1, b => 2); my @v = @h{qw(a b)}").is_ok());
2831 }
2832
2833 #[test]
2834 fn array_slice_ok() {
2835 assert!(lint("my @a = (1, 2, 3, 4); my @b = @a[0, 2]").is_ok());
2836 }
2837
2838 #[test]
2839 fn instance_method_on_typed_var_flags_unknown_method() {
2840 let r = lint(
2844 "class Point { x : Float\n y : Float\n fn mag_sq { 1 } }\n\
2845 my $p = Point(x => 3, y => 4)\n\
2846 $p->dgdd()",
2847 );
2848 assert!(r.is_err(), "expected error for `$$p->dgdd`");
2849 let e = r.unwrap_err();
2850 assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
2851 assert!(
2852 e.message.contains("dgdd") && e.message.contains("Point"),
2853 "expected message to name `dgdd` and `Point`, got: {}",
2854 e.message,
2855 );
2856 }
2857
2858 #[test]
2859 fn instance_method_on_typed_var_known_method_ok() {
2860 assert!(lint(
2862 "class Point { x : Float\n y : Float\n fn mag_sq { 1 } }\n\
2863 my $p = Point(x => 3, y => 4)\n\
2864 $p->mag_sq()"
2865 )
2866 .is_ok());
2867 }
2868
2869 #[test]
2870 fn instance_method_on_typed_var_field_ok() {
2871 assert!(lint(
2873 "class Point { x : Float\n y : Float }\n\
2874 my $p = Point(x => 3, y => 4)\n\
2875 p $p->x"
2876 )
2877 .is_ok());
2878 }
2879
2880 #[test]
2881 fn strict_never_flags_topic_variants() {
2882 let cases = [
2885 "_", "_0", "_1", "_42", "_<", "_<<<<<", "_<3", "_<10", "_2<", "_2<<<", "_2<5",
2890 ];
2891 for name in cases {
2892 assert!(
2893 super::is_topic_var(name),
2894 "is_topic_var({name:?}) must return true (topic/block-param form)",
2895 );
2896 }
2897 let src = "use strict\np _ + _1 + _< + _<2 + _<<<<< + _2<<< + _2<5\n";
2900 let prog = crate::parse_with_file(src, "test.stk").expect("parse");
2901 super::analyze_program_with_strict(&prog, "test.stk", true)
2902 .expect("strict-vars must not flag topic-variant block params");
2903 }
2904
2905 #[test]
2906 fn is_topic_var_rejects_non_topic_underscore_names() {
2907 for bad in [
2912 "_x", "_foo", "_3abc", "_<bad", "_2<xyz", "x_", "__", "_<<<x", ] {
2921 assert!(
2922 !super::is_topic_var(bad),
2923 "is_topic_var({bad:?}) must return false (not a topic form)",
2924 );
2925 }
2926 }
2927
2928 #[test]
2929 fn universal_methods_skip_hierarchy_lookup() {
2930 let mut a = super::StaticAnalyzer::new("test.stk");
2934 a.type_methods.insert("Empty".to_string(), HashSet::new());
2936 a.type_fields.insert("Empty".to_string(), HashSet::new());
2937 for method in ["isa", "can", "DOES", "does", "new", "BUILD", "DESTROY"] {
2938 assert!(
2939 a.method_resolves_in_hierarchy("Empty", method),
2940 "method `{method}` must resolve on any class via the universal whitelist",
2941 );
2942 }
2943 assert!(
2945 !a.method_resolves_in_hierarchy("Empty", "totally_made_up"),
2946 "non-universal unknown method must still be flagged",
2947 );
2948 }
2949
2950 #[test]
2951 fn method_resolves_walks_extends_chain() {
2952 let mut a = super::StaticAnalyzer::new("test.stk");
2955 let mut animal_methods = HashSet::new();
2956 animal_methods.insert("trail".to_string());
2957 a.type_methods.insert("Animal".to_string(), animal_methods);
2958 a.type_fields.insert("Animal".to_string(), HashSet::new());
2959 a.type_methods.insert("Dog".to_string(), HashSet::new());
2960 a.type_fields.insert("Dog".to_string(), HashSet::new());
2961 a.type_parents
2962 .insert("Dog".to_string(), vec!["Animal".to_string()]);
2963 assert!(
2964 a.method_resolves_in_hierarchy("Dog", "trail"),
2965 "`trail` on Dog must resolve via Animal in extends chain",
2966 );
2967 assert!(
2969 !a.method_resolves_in_hierarchy("Dog", "fly"),
2970 "`fly` on Dog must NOT resolve (absent from Dog AND Animal)",
2971 );
2972 }
2973
2974 #[test]
2975 fn method_resolves_cycle_protected() {
2976 let mut a = super::StaticAnalyzer::new("test.stk");
2979 a.type_methods.insert("A".to_string(), HashSet::new());
2980 a.type_fields.insert("A".to_string(), HashSet::new());
2981 a.type_methods.insert("B".to_string(), HashSet::new());
2982 a.type_fields.insert("B".to_string(), HashSet::new());
2983 a.type_parents
2984 .insert("A".to_string(), vec!["B".to_string()]);
2985 a.type_parents
2986 .insert("B".to_string(), vec!["A".to_string()]);
2987 assert!(!a.method_resolves_in_hierarchy("A", "missing"));
2989 assert!(!a.method_resolves_in_hierarchy("B", "missing"));
2990 }
2991
2992 #[test]
2993 fn dollar_obj_method_in_string_interp_not_flagged() {
2994 assert!(lint(
3000 "class P { x: Int = 0\n fn show { $self->x } }\n\
3001 my $p = P(x => 5)\np \"got #{ $p->show }\""
3002 )
3003 .is_ok(),);
3004 }
3005
3006 #[test]
3007 fn dollar_hash_with_complex_expression_in_string_interp() {
3008 assert!(
3012 lint(
3013 "use strict\n\
3014 my %h = (n => 5)\n\
3015 p \"got #{ $h{n} + 1 }\""
3016 )
3017 .is_ok(),
3018 "defined hash element inside #{{}} must not flag",
3019 );
3020 let r = lint("use strict\np \"got #{ $undef_typo + 1 }\"");
3021 assert!(
3022 r.is_err(),
3023 "complex-expr #{{}} block must still strict-check unknown vars",
3024 );
3025 }
3026
3027 #[test]
3028 fn is_topic_var_accepts_extra_grammar_variants() {
3029 for good in [
3031 "_99", "_<<<<<<<<<<", "_<999", "_42<<<", "_42<42", ] {
3037 assert!(
3038 super::is_topic_var(good),
3039 "is_topic_var({good:?}) must return true",
3040 );
3041 }
3042 }
3043
3044 #[test]
3045 fn strict_never_flags_sigiled_topic_variants() {
3046 let src = "use strict\nmy $tot = $_ + $_0 + $_1 + $_< + $_<2 + $_<<<<< + $_2<<< + $_2<5\np $tot\n";
3050 let prog = crate::parse_with_file(src, "test.stk").expect("parse");
3051 super::analyze_program_with_strict(&prog, "test.stk", true)
3052 .expect("strict-vars must not flag sigiled topic-variant block params");
3053 }
3054
3055 #[test]
3056 fn strict_still_flags_undefined_underscore_prefixed_ident() {
3057 let r = lint("p $_underscore_name");
3060 assert!(r.is_err(), "expected $_underscore_name to be flagged");
3061 }
3062
3063 #[test]
3064 fn qualified_our_scalar_visible_across_packages() {
3065 assert!(
3069 lint("package Foo\nour $x = 1\npackage main\np $Foo::x").is_ok(),
3070 "qualified `$Foo::x` must resolve to `our $x` declared in package Foo",
3071 );
3072 }
3073
3074 #[test]
3075 fn qualified_oursync_scalar_visible_across_packages() {
3076 assert!(
3078 lint("package Counter\noursync $val = 0\npackage main\np $Counter::val").is_ok(),
3079 "qualified `$Counter::val` must resolve to `oursync $val` in package Counter",
3080 );
3081 }
3082
3083 #[test]
3084 fn qualified_our_array_visible_across_packages() {
3085 assert!(lint("package Foo\nour @xs = (1,2,3)\npackage main\np @Foo::xs").is_ok());
3086 }
3087
3088 #[test]
3089 fn qualified_our_hash_visible_across_packages() {
3090 assert!(lint("package Foo\nour %h = (a=>1)\npackage main\np %Foo::h").is_ok());
3091 }
3092
3093 #[test]
3094 fn thread_par_run_compiler_generated_call_not_flagged() {
3095 assert!(
3098 lint("my @xs = (1,2,3)\nmy @r = ~p> @xs map { _ * 2 }").is_ok(),
3099 "compiler-generated _thread_par_run must be whitelisted",
3100 );
3101 }
3102
3103 #[test]
3104 fn deque_constructor_not_flagged() {
3105 assert!(
3109 lint("my $dq = deque(1, 2, 3)\np $dq->len").is_ok(),
3110 "deque constructor must not be flagged as undefined sub",
3111 );
3112 }
3113
3114 #[test]
3115 fn defer_block_compiler_generated_call_not_flagged() {
3116 assert!(
3119 lint("fn ff { defer { p \"cleanup\" }; 42 }").is_ok(),
3120 "compiler-generated defer__internal must be whitelisted",
3121 );
3122 let r = lint("use strict; _unknown_helper()");
3124 assert!(r.is_err(), "arbitrary `_foo` must still flag");
3125 }
3126
3127 #[test]
3128 fn aop_intercept_context_vars_not_flagged() {
3129 assert!(lint("before \"fetch\" { p $INTERCEPT_NAME, @INTERCEPT_ARGS }").is_ok());
3133 assert!(
3134 lint("after \"fetch\" { p $INTERCEPT_RESULT, $INTERCEPT_MS, $INTERCEPT_US }").is_ok()
3135 );
3136 }
3137
3138 #[test]
3139 fn string_interpolation_never_flags_undefined_simple_var() {
3140 assert!(
3146 lint("p \"printf $fh writes to STDOUT\"").is_ok(),
3147 "simple $var inside string-interp must not be flagged",
3148 );
3149 assert!(
3150 lint("p \"got @items here\"").is_ok(),
3151 "simple @var inside string-interp must not be flagged",
3152 );
3153 assert!(
3155 lint("p \"got $#missing_array items\"").is_ok(),
3156 "$#arr-style inside string-interp must not be flagged",
3157 );
3158 assert!(
3161 lint("use strict\np $fh").is_err(),
3162 "bare $fh outside string must still flag",
3163 );
3164 }
3165
3166 #[test]
3167 fn string_interp_complex_expr_still_walks_strict() {
3168 let r = lint("use strict\np \"got #{ $undef + 1 }\"");
3171 assert!(
3172 r.is_err(),
3173 "complex expr inside #{{}} must still strict-check vars",
3174 );
3175 }
3176
3177 #[test]
3178 fn qualified_main_var_visible_in_default_package() {
3179 assert!(lint("our $log_begin = \"\"\nBEGIN { $main::log_begin .= \"B:\" }").is_ok(),);
3183 assert!(lint("our @items = (1,2)\np @main::items").is_ok());
3184 assert!(lint("our %map = ()\np keys %main::map").is_ok());
3185 }
3186
3187 #[test]
3188 fn dollar_hash_array_last_index_uses_underlying_array() {
3189 assert!(lint("my @arr = (1,2,3); p $#arr").is_ok());
3192 let r = lint("use strict\np $#undefined_array");
3193 assert!(r.is_err(), "$#undefined_array must flag @undefined_array");
3194 assert!(
3195 r.unwrap_err().message.contains("@undefined_array"),
3196 "error must name @undefined_array, not $#undefined_array",
3197 );
3198 }
3199
3200 #[test]
3201 fn match_arm_enum_variant_typo_flagged() {
3202 let r = lint(
3206 "enum Sig { Hup, Int, Term, Kill }\n\
3207 fn handle($s) {\n\
3208 match ($s) {\n\
3209 Sig::Hup => \"reload\",\n\
3210 Sig::Term2 => \"drain\",\n\
3211 Sig::Kill => \"reap\",\n\
3212 }\n\
3213 }",
3214 );
3215 assert!(r.is_err(), "expected flag on Sig::Term2");
3216 let msg = r.unwrap_err().message;
3217 assert!(
3218 msg.contains("Term2") && msg.contains("Sig"),
3219 "message must name Term2 and Sig: {msg}",
3220 );
3221 }
3222
3223 #[test]
3224 fn match_arm_known_enum_variant_passes() {
3225 assert!(lint(
3227 "enum Sig { Hup, Int, Term, Kill }\n\
3228 fn handle($s) {\n\
3229 match ($s) {\n\
3230 Sig::Hup => \"reload\",\n\
3231 Sig::Int => \"shutdown\",\n\
3232 Sig::Term => \"drain\",\n\
3233 Sig::Kill => \"reap\",\n\
3234 }\n\
3235 }"
3236 )
3237 .is_ok());
3238 }
3239
3240 #[test]
3241 fn dollar_caret_perl_special_vars_not_flagged() {
3242 for name in ["$^X", "$^O", "$^V", "$^W", "$^T", "$^R", "$^N", "$^H"] {
3245 assert!(
3246 lint(&format!("use strict\np {name}")).is_ok(),
3247 "{name} must not be flagged by strict-vars",
3248 );
3249 }
3250 }
3251
3252 #[test]
3253 fn lexical_filehandle_open_my_var_not_flagged() {
3254 assert!(lint(
3258 "use strict\nmy $efile = \"/tmp/x\"\n\
3259 open(my $wfh, \">\", $efile) or die\n\
3260 print $wfh \"line1\\n\"\nclose $wfh"
3261 )
3262 .is_ok(),);
3263 }
3264
3265 #[test]
3266 fn exists_subroutine_ref_does_not_flag_undefined() {
3267 assert!(lint(
3270 "package Foo\nfn greet = 1\npackage main\n\
3271 p exists(&Foo::greet) ? \"y\" : \"n\"\n\
3272 p exists(&Foo::missing) ? \"y\" : \"n\""
3273 )
3274 .is_ok(),);
3275 }
3276
3277 #[test]
3278 fn universal_methods_resolve_on_any_class() {
3279 assert!(lint(
3286 "class Square { side: Float\n fn area { 1 } }\n\
3287 my $sq = Square->new(side => 5)\n\
3288 p $sq->isa(\"Square\")\n\
3289 p $sq->can(\"area\")\n\
3290 p $sq->DOES(\"Shape\")\n\
3291 my $cloned = $sq->clone()\n\
3292 my $changed = $sq->with(side => 9)\n\
3293 p $sq->to_hash()\n\
3294 p $sq->fields()"
3295 )
3296 .is_ok(),);
3297 }
3298
3299 #[test]
3300 fn builtin_struct_methods_resolve_on_any_struct() {
3301 assert!(lint(
3304 "struct Point { x: Float\n y: Float }\n\
3305 my $p = Point(x => 1.0, y => 2.0)\n\
3306 my $c = $p->clone()\n\
3307 my $u = $p->with(x => 9.0)\n\
3308 p $p->to_hash()\n\
3309 p $p->fields()"
3310 )
3311 .is_ok(),);
3312 }
3313
3314 #[test]
3315 fn class_inheritance_resolves_parent_methods_on_self() {
3316 assert!(lint(
3319 "class Animal { name: Str = \"\"\n fn trail { \"...\" } }\n\
3320 class Dog extends Animal {\n\
3321 breed: Str = \"\"\n\
3322 fn show { $self->trail }\n\
3323 }"
3324 )
3325 .is_ok(),);
3326 }
3327
3328 #[test]
3329 fn class_inheritance_resolves_parent_fields_in_constructor() {
3330 assert!(lint(
3333 "class Animal { name: Str = \"\" }\n\
3334 class Dog extends Animal { breed: Str = \"\" }\n\
3335 my $d = Dog(name => \"Rex\", breed => \"Lab\")\n\
3336 p $d->name"
3337 )
3338 .is_ok(),);
3339 }
3340
3341 #[test]
3342 fn resolve_require_path_finds_lib_root_from_nested_source() {
3343 let tmp = std::env::temp_dir().join(format!("stryke_resolve_test_{}", std::process::id()));
3350 let _ = std::fs::remove_dir_all(&tmp);
3351 std::fs::create_dir_all(tmp.join("lib").join("ai")).unwrap();
3352 let mat = tmp.join("lib").join("ai").join("matrix.stk");
3353 std::fs::write(&mat, "1\n").unwrap();
3354 let nn = tmp.join("lib").join("ai").join("neural_network.stk");
3355 std::fs::write(&nn, "1\n").unwrap();
3356 let resolved =
3357 super::resolve_require_path_from_file(nn.to_str().unwrap(), "./lib/ai/matrix.stk");
3358 assert!(
3359 resolved.as_ref().is_some_and(|p| p == &mat)
3360 || resolved
3361 .as_ref()
3362 .is_some_and(|p| { p.canonicalize().ok() == mat.canonicalize().ok() }),
3363 "expected to resolve to {mat:?}, got {resolved:?}",
3364 );
3365 let _ = std::fs::remove_dir_all(&tmp);
3366 }
3367
3368 #[test]
3369 fn resolve_require_path_sibling_in_same_dir() {
3370 let tmp =
3373 std::env::temp_dir().join(format!("stryke_resolve_sibling_{}", std::process::id()));
3374 let _ = std::fs::remove_dir_all(&tmp);
3375 std::fs::create_dir_all(&tmp).unwrap();
3376 let sib = tmp.join("sibling.stk");
3377 std::fs::write(&sib, "1\n").unwrap();
3378 let me = tmp.join("me.stk");
3379 std::fs::write(&me, "1\n").unwrap();
3380 let resolved = super::resolve_require_path_from_file(me.to_str().unwrap(), "./sibling.stk");
3381 assert!(
3382 resolved.is_some(),
3383 "expected to resolve ./sibling.stk in same dir, got None",
3384 );
3385 let _ = std::fs::remove_dir_all(&tmp);
3386 }
3387
3388 #[test]
3389 fn positional_constructor_args_not_checked_as_keys() {
3390 assert!(lint(
3394 "enum Priority { Low, Medium, High, Critical }\n\
3395 class Task {\n\
3396 id: Int\n\
3397 title: Str = \"\"\n\
3398 priority: Any = undef\n\
3399 }\n\
3400 my $t = Task(1, \"Setup\", Priority::High)"
3401 )
3402 .is_ok(),);
3403 }
3404
3405 #[test]
3406 fn positional_constructor_with_string_value_not_flagged() {
3407 assert!(lint(
3411 "class Person { name: Str = \"\"\n age: Int = 0 }\n\
3412 my $p = Person(\"Alice\", 30)"
3413 )
3414 .is_ok(),);
3415 }
3416
3417 #[test]
3418 fn keyed_constructor_still_flags_typo() {
3419 let r = lint(
3423 "class Point { x: Float\n y: Float }\n\
3424 my $p = Point(x => 10, yyg => 20)",
3425 );
3426 assert!(r.is_err(), "expected `yyg` typo to be flagged");
3427 assert!(
3428 r.unwrap_err().message.contains("yyg"),
3429 "error must name `yyg` field",
3430 );
3431 }
3432
3433 #[test]
3434 fn dollar_dollar_pid_not_flagged() {
3435 assert!(lint("use strict\np $$").is_ok());
3439 assert!(lint("use strict\n$$ > 0 ? 1 : 0").is_ok());
3440 }
3441
3442 #[test]
3443 fn class_impl_trait_resolves_default_method() {
3444 assert!(lint(
3449 "trait Greetable {\n\
3450 fn greeting { \"Hello\" }\n\
3451 fn name\n\
3452 }\n\
3453 class Person impl Greetable {\n\
3454 n: Str = \"\"\n\
3455 fn name { $self->n }\n\
3456 }\n\
3457 my $p = Person(n => \"Alice\")\n\
3458 p $p->greeting()"
3459 )
3460 .is_ok(),);
3461 }
3462
3463 #[test]
3464 fn class_impl_multiple_traits_resolves_methods_from_all() {
3465 assert!(lint(
3466 "trait Greetable { fn greeting { \"hi\" } }\n\
3467 trait Loggable { fn log_it { 1 } }\n\
3468 class Hybrid impl Greetable, Loggable {}\n\
3469 my $h = Hybrid->new\n\
3470 p $h->greeting()\n\
3471 p $h->log_it()"
3472 )
3473 .is_ok(),);
3474 }
3475
3476 #[test]
3477 fn class_impl_trait_still_flags_unknown_method() {
3478 let r = lint(
3481 "trait Greetable { fn greeting }\n\
3482 class Person impl Greetable { n: Str = \"\" }\n\
3483 my $p = Person->new\n\
3484 p $p->fly()",
3485 );
3486 assert!(r.is_err(), "expected $p->fly to be flagged");
3487 }
3488
3489 #[test]
3490 fn class_inheritance_still_flags_unknown_method() {
3491 let r = lint(
3494 "class Animal { fn trail { \"\" } }\n\
3495 class Dog extends Animal {\n\
3496 fn show { $self->fly }\n\
3497 }",
3498 );
3499 assert!(r.is_err(), "expected $self->fly to be flagged");
3500 }
3501
3502 #[test]
3503 fn instance_method_on_arrow_new_form_typed_var_flags() {
3504 let r = lint(
3506 "class Point { x : Float\n y : Float }\n\
3507 my $p = Point->new(x => 3, y => 4)\n\
3508 $p->whatever()",
3509 );
3510 assert!(r.is_err(), "expected error for `$$p->whatever`");
3511 }
3512}