1use std::collections::HashSet;
4use std::sync::OnceLock;
5
6use crate::ast::{
7 Block, DerefKind, Expr, ExprKind, MatchArrayElem, Program, Sigil, Statement, StmtKind,
8 StringPart, SubSigParam,
9};
10use crate::error::{ErrorKind, PerlError, PerlResult};
11
12static BUILTINS: OnceLock<HashSet<&'static str>> = OnceLock::new();
13
14fn builtins() -> &'static HashSet<&'static str> {
15 BUILTINS.get_or_init(|| {
16 include_str!("lsp_completion_words.txt")
17 .lines()
18 .map(str::trim)
19 .filter(|l| !l.is_empty() && !l.starts_with('#'))
20 .collect()
21 })
22}
23
24#[derive(Default)]
25struct Scope {
26 scalars: HashSet<String>,
27 arrays: HashSet<String>,
28 hashes: HashSet<String>,
29 subs: HashSet<String>,
30}
31
32impl Scope {
33 fn declare_scalar(&mut self, name: &str) {
34 self.scalars.insert(name.to_string());
35 }
36 fn declare_array(&mut self, name: &str) {
37 self.arrays.insert(name.to_string());
38 }
39 fn declare_hash(&mut self, name: &str) {
40 self.hashes.insert(name.to_string());
41 }
42 fn declare_sub(&mut self, name: &str) {
43 self.subs.insert(name.to_string());
44 }
45}
46
47pub struct StaticAnalyzer {
48 scopes: Vec<Scope>,
49 errors: Vec<PerlError>,
50 file: String,
51 current_package: String,
52}
53
54impl StaticAnalyzer {
55 pub fn new(file: &str) -> Self {
56 let mut global = Scope::default();
57 for name in ["_", "a", "b", "ARGV", "ENV", "SIG", "INC"] {
58 global.declare_array(name);
59 }
60 for name in ["ENV", "SIG", "INC"] {
61 global.declare_hash(name);
62 }
63 for name in [
64 "_", "a", "b", "!", "$", "@", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "&",
65 "`", "'", "+", ".", "/", "\\", "|", "%", "=", "-", "~", "^", "*", "?", "\"",
66 ] {
67 global.declare_scalar(name);
68 }
69 Self {
70 scopes: vec![global],
71 errors: Vec::new(),
72 file: file.to_string(),
73 current_package: "main".to_string(),
74 }
75 }
76
77 fn push_scope(&mut self) {
78 self.scopes.push(Scope::default());
79 }
80
81 fn pop_scope(&mut self) {
82 if self.scopes.len() > 1 {
83 self.scopes.pop();
84 }
85 }
86
87 fn declare_scalar(&mut self, name: &str) {
88 if let Some(scope) = self.scopes.last_mut() {
89 scope.declare_scalar(name);
90 }
91 }
92
93 fn declare_array(&mut self, name: &str) {
94 if let Some(scope) = self.scopes.last_mut() {
95 scope.declare_array(name);
96 }
97 }
98
99 fn declare_hash(&mut self, name: &str) {
100 if let Some(scope) = self.scopes.last_mut() {
101 scope.declare_hash(name);
102 }
103 }
104
105 fn declare_sub(&mut self, name: &str) {
106 if let Some(scope) = self.scopes.first_mut() {
107 scope.declare_sub(name);
108 }
109 }
110
111 fn is_scalar_defined(&self, name: &str) -> bool {
112 if is_special_var(name) {
113 return true;
114 }
115 self.scopes.iter().rev().any(|s| s.scalars.contains(name))
116 }
117
118 fn is_array_defined(&self, name: &str) -> bool {
119 if name == "_" || name == "ARGV" {
120 return true;
121 }
122 self.scopes.iter().rev().any(|s| s.arrays.contains(name))
123 }
124
125 fn is_hash_defined(&self, name: &str) -> bool {
126 if matches!(name, "ENV" | "SIG" | "INC") {
127 return true;
128 }
129 self.scopes.iter().rev().any(|s| s.hashes.contains(name))
130 }
131
132 fn is_sub_defined(&self, name: &str) -> bool {
133 if name.starts_with("static::") {
135 return true;
136 }
137 let base = name.rsplit("::").next().unwrap_or(name);
138 if builtins().contains(base) {
139 return true;
140 }
141 self.scopes
142 .iter()
143 .rev()
144 .any(|s| s.subs.contains(name) || s.subs.contains(base))
145 }
146
147 fn error(&mut self, kind: ErrorKind, msg: String, line: usize) {
148 self.errors
149 .push(PerlError::new(kind, msg, line, &self.file));
150 }
151
152 pub fn analyze(mut self, program: &Program) -> PerlResult<()> {
153 for stmt in &program.statements {
154 self.collect_declarations_stmt(stmt);
155 }
156 for stmt in &program.statements {
157 self.analyze_stmt(stmt);
158 }
159 if let Some(e) = self.errors.into_iter().next() {
160 Err(e)
161 } else {
162 Ok(())
163 }
164 }
165
166 fn collect_declarations_stmt(&mut self, stmt: &Statement) {
167 match &stmt.kind {
168 StmtKind::Package { name } => {
169 self.current_package = name.clone();
170 }
171 StmtKind::SubDecl { name, .. } => {
172 let fqn = if name.contains("::") {
173 name.clone()
174 } else {
175 format!("{}::{}", self.current_package, name)
176 };
177 self.declare_sub(name);
178 self.declare_sub(&fqn);
179 }
180 StmtKind::Use { module, .. } => {
181 self.declare_sub(module);
182 }
183 StmtKind::Block(b)
184 | StmtKind::StmtGroup(b)
185 | StmtKind::Begin(b)
186 | StmtKind::End(b)
187 | StmtKind::UnitCheck(b)
188 | StmtKind::Check(b)
189 | StmtKind::Init(b) => {
190 for s in b {
191 self.collect_declarations_stmt(s);
192 }
193 }
194 StmtKind::If {
195 body,
196 elsifs,
197 else_block,
198 ..
199 } => {
200 for s in body {
201 self.collect_declarations_stmt(s);
202 }
203 for (_, b) in elsifs {
204 for s in b {
205 self.collect_declarations_stmt(s);
206 }
207 }
208 if let Some(b) = else_block {
209 for s in b {
210 self.collect_declarations_stmt(s);
211 }
212 }
213 }
214 StmtKind::ClassDecl { def } => {
215 self.declare_sub(&def.name);
217 for m in &def.methods {
219 if m.is_static {
220 self.declare_sub(&format!("{}::{}", def.name, m.name));
221 }
222 }
223 for sf in &def.static_fields {
224 self.declare_sub(&format!("{}::{}", def.name, sf.name));
225 }
226 }
227 StmtKind::StructDecl { def } => {
228 self.declare_sub(&def.name);
229 }
230 StmtKind::EnumDecl { def } => {
231 self.declare_sub(&def.name);
232 for v in &def.variants {
233 self.declare_sub(&format!("{}::{}", def.name, v.name));
234 }
235 }
236 _ => {}
237 }
238 }
239
240 fn analyze_stmt(&mut self, stmt: &Statement) {
241 match &stmt.kind {
242 StmtKind::Package { name } => {
243 self.current_package = name.clone();
244 }
245 StmtKind::My(decls)
246 | StmtKind::Our(decls)
247 | StmtKind::Local(decls)
248 | StmtKind::State(decls)
249 | StmtKind::MySync(decls) => {
250 for d in decls {
251 match d.sigil {
252 Sigil::Scalar => self.declare_scalar(&d.name),
253 Sigil::Array => self.declare_array(&d.name),
254 Sigil::Hash => self.declare_hash(&d.name),
255 Sigil::Typeglob => {}
256 }
257 if let Some(init) = &d.initializer {
258 self.analyze_expr(init);
259 }
260 }
261 }
262 StmtKind::Expression(e) => self.analyze_expr(e),
263 StmtKind::Return(Some(e)) => self.analyze_expr(e),
264 StmtKind::Return(None) => {}
265 StmtKind::If {
266 condition,
267 body,
268 elsifs,
269 else_block,
270 } => {
271 self.analyze_expr(condition);
272 self.push_scope();
273 self.analyze_block(body);
274 self.pop_scope();
275 for (cond, b) in elsifs {
276 self.analyze_expr(cond);
277 self.push_scope();
278 self.analyze_block(b);
279 self.pop_scope();
280 }
281 if let Some(b) = else_block {
282 self.push_scope();
283 self.analyze_block(b);
284 self.pop_scope();
285 }
286 }
287 StmtKind::Unless {
288 condition,
289 body,
290 else_block,
291 } => {
292 self.analyze_expr(condition);
293 self.push_scope();
294 self.analyze_block(body);
295 self.pop_scope();
296 if let Some(b) = else_block {
297 self.push_scope();
298 self.analyze_block(b);
299 self.pop_scope();
300 }
301 }
302 StmtKind::While {
303 condition,
304 body,
305 continue_block,
306 ..
307 }
308 | StmtKind::Until {
309 condition,
310 body,
311 continue_block,
312 ..
313 } => {
314 self.analyze_expr(condition);
315 self.push_scope();
316 self.analyze_block(body);
317 if let Some(cb) = continue_block {
318 self.analyze_block(cb);
319 }
320 self.pop_scope();
321 }
322 StmtKind::DoWhile { body, condition } => {
323 self.push_scope();
324 self.analyze_block(body);
325 self.pop_scope();
326 self.analyze_expr(condition);
327 }
328 StmtKind::For {
329 init,
330 condition,
331 step,
332 body,
333 continue_block,
334 ..
335 } => {
336 self.push_scope();
337 if let Some(i) = init {
338 self.analyze_stmt(i);
339 }
340 if let Some(c) = condition {
341 self.analyze_expr(c);
342 }
343 if let Some(s) = step {
344 self.analyze_expr(s);
345 }
346 self.analyze_block(body);
347 if let Some(cb) = continue_block {
348 self.analyze_block(cb);
349 }
350 self.pop_scope();
351 }
352 StmtKind::Foreach {
353 var,
354 list,
355 body,
356 continue_block,
357 ..
358 } => {
359 self.analyze_expr(list);
360 self.push_scope();
361 self.declare_scalar(var);
362 self.analyze_block(body);
363 if let Some(cb) = continue_block {
364 self.analyze_block(cb);
365 }
366 self.pop_scope();
367 }
368 StmtKind::SubDecl {
369 name, params, body, ..
370 } => {
371 let fqn = if name.contains("::") {
372 name.clone()
373 } else {
374 format!("{}::{}", self.current_package, name)
375 };
376 self.declare_sub(name);
377 self.declare_sub(&fqn);
378 self.push_scope();
379 for p in params {
380 self.declare_param(p);
381 }
382 self.analyze_block(body);
383 self.pop_scope();
384 }
385 StmtKind::Block(b)
386 | StmtKind::StmtGroup(b)
387 | StmtKind::Begin(b)
388 | StmtKind::End(b)
389 | StmtKind::UnitCheck(b)
390 | StmtKind::Check(b)
391 | StmtKind::Init(b)
392 | StmtKind::Continue(b) => {
393 self.push_scope();
394 self.analyze_block(b);
395 self.pop_scope();
396 }
397 StmtKind::TryCatch {
398 try_block,
399 catch_var,
400 catch_block,
401 finally_block,
402 } => {
403 self.push_scope();
404 self.analyze_block(try_block);
405 self.pop_scope();
406 self.push_scope();
407 self.declare_scalar(catch_var);
408 self.analyze_block(catch_block);
409 self.pop_scope();
410 if let Some(fb) = finally_block {
411 self.push_scope();
412 self.analyze_block(fb);
413 self.pop_scope();
414 }
415 }
416 StmtKind::EvalTimeout { body, .. } => {
417 self.push_scope();
418 self.analyze_block(body);
419 self.pop_scope();
420 }
421 StmtKind::Given { topic, body } => {
422 self.analyze_expr(topic);
423 self.push_scope();
424 self.analyze_block(body);
425 self.pop_scope();
426 }
427 StmtKind::When { cond, body } => {
428 self.analyze_expr(cond);
429 self.push_scope();
430 self.analyze_block(body);
431 self.pop_scope();
432 }
433 StmtKind::DefaultCase { body } => {
434 self.push_scope();
435 self.analyze_block(body);
436 self.pop_scope();
437 }
438 StmtKind::LocalExpr {
439 target,
440 initializer,
441 } => {
442 self.analyze_expr(target);
443 if let Some(init) = initializer {
444 self.analyze_expr(init);
445 }
446 }
447 StmtKind::Goto { target } => {
448 self.analyze_expr(target);
449 }
450 StmtKind::Tie { class, args, .. } => {
451 self.analyze_expr(class);
452 for a in args {
453 self.analyze_expr(a);
454 }
455 }
456 StmtKind::Use { imports, .. } | StmtKind::No { imports, .. } => {
457 for e in imports {
458 self.analyze_expr(e);
459 }
460 }
461 StmtKind::StructDecl { .. }
462 | StmtKind::EnumDecl { .. }
463 | StmtKind::ClassDecl { .. }
464 | StmtKind::TraitDecl { .. }
465 | StmtKind::FormatDecl { .. }
466 | StmtKind::UsePerlVersion { .. }
467 | StmtKind::UseOverload { .. }
468 | StmtKind::Last(_)
469 | StmtKind::Next(_)
470 | StmtKind::Redo(_)
471 | StmtKind::Empty => {}
472 }
473 }
474
475 fn declare_param(&mut self, param: &SubSigParam) {
476 match param {
477 SubSigParam::Scalar(name, _, _) => self.declare_scalar(name),
478 SubSigParam::Array(name, _) => self.declare_array(name),
479 SubSigParam::Hash(name, _) => self.declare_hash(name),
480 SubSigParam::ArrayDestruct(elems) => {
481 for e in elems {
482 match e {
483 MatchArrayElem::CaptureScalar(n) => self.declare_scalar(n),
484 MatchArrayElem::RestBind(n) => self.declare_array(n),
485 _ => {}
486 }
487 }
488 }
489 SubSigParam::HashDestruct(pairs) => {
490 for (_, name) in pairs {
491 self.declare_scalar(name);
492 }
493 }
494 }
495 }
496
497 fn analyze_block(&mut self, block: &Block) {
498 for stmt in block {
499 self.analyze_stmt(stmt);
500 }
501 }
502
503 fn analyze_expr(&mut self, expr: &Expr) {
504 match &expr.kind {
505 ExprKind::ScalarVar(name) if !self.is_scalar_defined(name) => {
506 self.error(
507 ErrorKind::UndefinedVariable,
508 format!("Global symbol \"${}\" requires explicit package name", name),
509 expr.line,
510 );
511 }
512 ExprKind::ArrayVar(name) if !self.is_array_defined(name) => {
513 self.error(
514 ErrorKind::UndefinedVariable,
515 format!("Global symbol \"@{}\" requires explicit package name", name),
516 expr.line,
517 );
518 }
519 ExprKind::HashVar(name) if !self.is_hash_defined(name) => {
520 self.error(
521 ErrorKind::UndefinedVariable,
522 format!("Global symbol \"%{}\" requires explicit package name", name),
523 expr.line,
524 );
525 }
526 ExprKind::ArrayElement { array, index } => {
527 if !self.is_array_defined(array) && !self.is_scalar_defined(array) {
528 self.error(
529 ErrorKind::UndefinedVariable,
530 format!(
531 "Global symbol \"@{}\" requires explicit package name",
532 array
533 ),
534 expr.line,
535 );
536 }
537 self.analyze_expr(index);
538 }
539 ExprKind::HashElement { hash, key } => {
540 if !self.is_hash_defined(hash) && !self.is_scalar_defined(hash) {
541 self.error(
542 ErrorKind::UndefinedVariable,
543 format!("Global symbol \"%{}\" requires explicit package name", hash),
544 expr.line,
545 );
546 }
547 self.analyze_expr(key);
548 }
549 ExprKind::ArraySlice { array, indices } => {
550 if !self.is_array_defined(array) {
551 self.error(
552 ErrorKind::UndefinedVariable,
553 format!(
554 "Global symbol \"@{}\" requires explicit package name",
555 array
556 ),
557 expr.line,
558 );
559 }
560 for i in indices {
561 self.analyze_expr(i);
562 }
563 }
564 ExprKind::HashSlice { hash, keys } => {
565 if !self.is_hash_defined(hash) {
566 self.error(
567 ErrorKind::UndefinedVariable,
568 format!("Global symbol \"%{}\" requires explicit package name", hash),
569 expr.line,
570 );
571 }
572 for k in keys {
573 self.analyze_expr(k);
574 }
575 }
576 ExprKind::FuncCall { name, args } => {
577 if !self.is_sub_defined(name) {
578 self.error(
579 ErrorKind::UndefinedSubroutine,
580 format!("Undefined subroutine &{}", name),
581 expr.line,
582 );
583 }
584 for a in args {
585 self.analyze_expr(a);
586 }
587 }
588 ExprKind::MethodCall { object, args, .. } => {
589 self.analyze_expr(object);
590 for a in args {
591 self.analyze_expr(a);
592 }
593 }
594 ExprKind::IndirectCall { target, args, .. } => {
595 self.analyze_expr(target);
596 for a in args {
597 self.analyze_expr(a);
598 }
599 }
600 ExprKind::BinOp { left, right, .. } => {
601 self.analyze_expr(left);
602 self.analyze_expr(right);
603 }
604 ExprKind::UnaryOp { expr: e, .. } => {
605 self.analyze_expr(e);
606 }
607 ExprKind::PostfixOp { expr: e, .. } => {
608 self.analyze_expr(e);
609 }
610 ExprKind::Assign { target, value } => {
611 if let ExprKind::ScalarVar(name) = &target.kind {
612 self.declare_scalar(name);
613 } else if let ExprKind::ArrayVar(name) = &target.kind {
614 self.declare_array(name);
615 } else if let ExprKind::HashVar(name) = &target.kind {
616 self.declare_hash(name);
617 } else {
618 self.analyze_expr(target);
619 }
620 self.analyze_expr(value);
621 }
622 ExprKind::CompoundAssign { target, value, .. } => {
623 self.analyze_expr(target);
624 self.analyze_expr(value);
625 }
626 ExprKind::Ternary {
627 condition,
628 then_expr,
629 else_expr,
630 } => {
631 self.analyze_expr(condition);
632 self.analyze_expr(then_expr);
633 self.analyze_expr(else_expr);
634 }
635 ExprKind::List(exprs) | ExprKind::ArrayRef(exprs) => {
636 for e in exprs {
637 self.analyze_expr(e);
638 }
639 }
640 ExprKind::HashRef(pairs) => {
641 for (k, v) in pairs {
642 self.analyze_expr(k);
643 self.analyze_expr(v);
644 }
645 }
646 ExprKind::CodeRef { params, body } => {
647 self.push_scope();
648 for p in params {
649 self.declare_param(p);
650 }
651 self.analyze_block(body);
652 self.pop_scope();
653 }
654 ExprKind::ScalarRef(e)
655 | ExprKind::Deref { expr: e, .. }
656 | ExprKind::Defined(e)
657 | ExprKind::Exists(e)
658 | ExprKind::Delete(e) => {
659 self.analyze_expr(e);
660 }
661 ExprKind::ArrowDeref { expr, index, kind } => {
662 self.analyze_expr(expr);
663 if *kind != DerefKind::Call {
664 self.analyze_expr(index);
665 }
666 }
667 ExprKind::Range { from, to, step, .. } => {
668 self.analyze_expr(from);
669 self.analyze_expr(to);
670 if let Some(s) = step {
671 self.analyze_expr(s);
672 }
673 }
674 ExprKind::InterpolatedString(parts) => {
675 for part in parts {
676 match part {
677 StringPart::ScalarVar(name) => {
678 if !self.is_scalar_defined(name) {
679 self.error(
680 ErrorKind::UndefinedVariable,
681 format!(
682 "Global symbol \"${}\" requires explicit package name",
683 name
684 ),
685 expr.line,
686 );
687 }
688 }
689 StringPart::ArrayVar(name) => {
690 if !self.is_array_defined(name) {
691 self.error(
692 ErrorKind::UndefinedVariable,
693 format!(
694 "Global symbol \"@{}\" requires explicit package name",
695 name
696 ),
697 expr.line,
698 );
699 }
700 }
701 StringPart::Expr(e) => self.analyze_expr(e),
702 StringPart::Literal(_) => {}
703 }
704 }
705 }
706 ExprKind::Regex(_, _)
707 | ExprKind::Substitution { .. }
708 | ExprKind::Transliterate { .. }
709 | ExprKind::Match { .. } => {}
710 ExprKind::HashSliceDeref { container, keys } => {
711 self.analyze_expr(container);
712 for k in keys {
713 self.analyze_expr(k);
714 }
715 }
716 ExprKind::AnonymousListSlice { source, indices } => {
717 self.analyze_expr(source);
718 for i in indices {
719 self.analyze_expr(i);
720 }
721 }
722 ExprKind::SubroutineRef(name) | ExprKind::SubroutineCodeRef(name)
723 if !self.is_sub_defined(name) =>
724 {
725 self.error(
726 ErrorKind::UndefinedSubroutine,
727 format!("Undefined subroutine &{}", name),
728 expr.line,
729 );
730 }
731 ExprKind::DynamicSubCodeRef(e) => self.analyze_expr(e),
732 ExprKind::PostfixIf { expr, condition }
733 | ExprKind::PostfixUnless { expr, condition }
734 | ExprKind::PostfixWhile { expr, condition }
735 | ExprKind::PostfixUntil { expr, condition } => {
736 self.analyze_expr(expr);
737 self.analyze_expr(condition);
738 }
739 ExprKind::PostfixForeach { expr, list } => {
740 self.analyze_expr(list);
741 self.analyze_expr(expr);
742 }
743 ExprKind::Do(e) | ExprKind::Eval(e) => {
744 self.analyze_expr(e);
745 }
746 ExprKind::Caller(Some(e)) => {
747 self.analyze_expr(e);
748 }
749 ExprKind::Length(e) => {
750 self.analyze_expr(e);
751 }
752 ExprKind::Print { args, .. }
753 | ExprKind::Say { args, .. }
754 | ExprKind::Printf { args, .. } => {
755 for a in args {
756 self.analyze_expr(a);
757 }
758 }
759 ExprKind::Die(args)
760 | ExprKind::Warn(args)
761 | ExprKind::Unlink(args)
762 | ExprKind::Chmod(args)
763 | ExprKind::System(args)
764 | ExprKind::Exec(args) => {
765 for a in args {
766 self.analyze_expr(a);
767 }
768 }
769 ExprKind::Push { array, values } | ExprKind::Unshift { array, values } => {
770 self.analyze_expr(array);
771 for v in values {
772 self.analyze_expr(v);
773 }
774 }
775 ExprKind::Splice {
776 array,
777 offset,
778 length,
779 replacement,
780 } => {
781 self.analyze_expr(array);
782 if let Some(o) = offset {
783 self.analyze_expr(o);
784 }
785 if let Some(l) = length {
786 self.analyze_expr(l);
787 }
788 for r in replacement {
789 self.analyze_expr(r);
790 }
791 }
792 ExprKind::MapExpr { block, list, .. } | ExprKind::GrepExpr { block, list, .. } => {
793 self.push_scope();
794 self.analyze_block(block);
795 self.pop_scope();
796 self.analyze_expr(list);
797 }
798 ExprKind::SortExpr { list, .. } => {
799 self.analyze_expr(list);
800 }
801 ExprKind::Open { handle, mode, file } => {
802 self.analyze_expr(handle);
803 self.analyze_expr(mode);
804 if let Some(f) = file {
805 self.analyze_expr(f);
806 }
807 }
808 ExprKind::Close(e)
809 | ExprKind::Pop(e)
810 | ExprKind::Shift(e)
811 | ExprKind::Keys(e)
812 | ExprKind::Values(e)
813 | ExprKind::Each(e)
814 | ExprKind::Chdir(e)
815 | ExprKind::Require(e)
816 | ExprKind::Ref(e)
817 | ExprKind::Chomp(e)
818 | ExprKind::Chop(e)
819 | ExprKind::Lc(e)
820 | ExprKind::Uc(e)
821 | ExprKind::Lcfirst(e)
822 | ExprKind::Ucfirst(e)
823 | ExprKind::Abs(e)
824 | ExprKind::Int(e)
825 | ExprKind::Sqrt(e)
826 | ExprKind::Sin(e)
827 | ExprKind::Cos(e)
828 | ExprKind::Exp(e)
829 | ExprKind::Log(e)
830 | ExprKind::Chr(e)
831 | ExprKind::Ord(e)
832 | ExprKind::Hex(e)
833 | ExprKind::Oct(e)
834 | ExprKind::Readlink(e)
835 | ExprKind::Readdir(e)
836 | ExprKind::Closedir(e)
837 | ExprKind::Rewinddir(e)
838 | ExprKind::Telldir(e) => {
839 self.analyze_expr(e);
840 }
841 ExprKind::Exit(Some(e)) | ExprKind::Rand(Some(e)) | ExprKind::Eof(Some(e)) => {
842 self.analyze_expr(e);
843 }
844 ExprKind::Mkdir { path, mode } => {
845 self.analyze_expr(path);
846 if let Some(m) = mode {
847 self.analyze_expr(m);
848 }
849 }
850 ExprKind::Rename { old, new }
851 | ExprKind::Link { old, new }
852 | ExprKind::Symlink { old, new } => {
853 self.analyze_expr(old);
854 self.analyze_expr(new);
855 }
856 ExprKind::Chown(files) => {
857 for f in files {
858 self.analyze_expr(f);
859 }
860 }
861 ExprKind::Substr {
862 string,
863 offset,
864 length,
865 replacement,
866 } => {
867 self.analyze_expr(string);
868 self.analyze_expr(offset);
869 if let Some(l) = length {
870 self.analyze_expr(l);
871 }
872 if let Some(r) = replacement {
873 self.analyze_expr(r);
874 }
875 }
876 ExprKind::Index {
877 string,
878 substr,
879 position,
880 }
881 | ExprKind::Rindex {
882 string,
883 substr,
884 position,
885 } => {
886 self.analyze_expr(string);
887 self.analyze_expr(substr);
888 if let Some(p) = position {
889 self.analyze_expr(p);
890 }
891 }
892 ExprKind::Sprintf { format, args } => {
893 self.analyze_expr(format);
894 for a in args {
895 self.analyze_expr(a);
896 }
897 }
898 ExprKind::Bless { ref_expr, class } => {
899 self.analyze_expr(ref_expr);
900 if let Some(c) = class {
901 self.analyze_expr(c);
902 }
903 }
904 _ => {}
905 }
906 }
907}
908
909fn is_special_var(name: &str) -> bool {
910 if name.len() == 1 {
911 return true;
912 }
913 matches!(
914 name,
915 "ARGV"
916 | "ENV"
917 | "SIG"
918 | "INC"
919 | "AUTOLOAD"
920 | "STDERR"
921 | "STDIN"
922 | "STDOUT"
923 | "DATA"
924 | "UNIVERSAL"
925 | "VERSION"
926 | "ISA"
927 | "EXPORT"
928 | "EXPORT_OK"
929 )
930}
931
932pub fn analyze_program(program: &Program, file: &str) -> PerlResult<()> {
933 StaticAnalyzer::new(file).analyze(program)
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::parse_with_file;
940
941 fn lint(code: &str) -> PerlResult<()> {
942 let prog = parse_with_file(code, "test.stk").expect("parse");
943 analyze_program(&prog, "test.stk")
944 }
945
946 #[test]
947 fn undefined_scalar_detected() {
948 let r = lint("p $undefined");
949 assert!(r.is_err());
950 let e = r.unwrap_err();
951 assert_eq!(e.kind, ErrorKind::UndefinedVariable);
952 assert!(e.message.contains("$undefined"));
953 }
954
955 #[test]
956 fn defined_scalar_ok() {
957 assert!(lint("my $x = 1; p $x").is_ok());
958 }
959
960 #[test]
961 fn undefined_sub_detected() {
962 let r = lint("nonexistent_function()");
963 assert!(r.is_err());
964 let e = r.unwrap_err();
965 assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
966 assert!(e.message.contains("nonexistent_function"));
967 }
968
969 #[test]
970 fn defined_sub_ok() {
971 assert!(lint("fn foo { 1 } foo()").is_ok());
972 }
973
974 #[test]
975 fn builtin_sub_ok() {
976 assert!(lint("p 'hello'").is_ok());
977 assert!(lint("print 'hello'").is_ok());
978 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
979 }
980
981 #[test]
982 fn special_vars_ok() {
983 assert!(lint("p $_").is_ok());
984 assert!(lint("p @_").is_ok());
985 assert!(lint("p $a <=> $b").is_ok());
986 }
987
988 #[test]
989 fn foreach_var_in_scope() {
990 assert!(lint("foreach my $i (1..3) { p $i; }").is_ok());
991 }
992
993 #[test]
994 fn sub_params_in_scope() {
995 assert!(lint("fn foo($x) { p $x; } foo(1)").is_ok());
996 }
997
998 #[test]
999 fn assignment_declares_var() {
1000 assert!(lint("$x = 1; p $x").is_ok());
1001 }
1002
1003 #[test]
1004 fn builtin_inc_ok() {
1005 assert!(lint("my $x = 1; inc($x)").is_ok());
1006 }
1007
1008 #[test]
1009 fn builtin_dec_ok() {
1010 assert!(lint("my $x = 1; dec($x)").is_ok());
1011 }
1012
1013 #[test]
1014 fn builtin_rev_ok() {
1015 assert!(lint("my $s = rev 'hello'").is_ok());
1016 }
1017
1018 #[test]
1019 fn builtin_p_alias_for_say_ok() {
1020 assert!(lint("p 'hello'").is_ok());
1021 }
1022
1023 #[test]
1024 fn builtin_t_thread_ok() {
1025 assert!(lint("t 1 inc inc").is_ok());
1026 }
1027
1028 #[test]
1029 fn thread_with_undefined_var_detected() {
1030 let r = lint("t $undefined inc");
1031 assert!(r.is_err());
1032 }
1033
1034 #[test]
1035 fn try_catch_var_in_scope() {
1036 assert!(lint("try { die 'err'; } catch ($e) { p $e; }").is_ok());
1037 }
1038
1039 #[test]
1040 fn interpolated_string_undefined_var() {
1041 let r = lint(r#"p "hello $undefined""#);
1042 assert!(r.is_err());
1043 }
1044
1045 #[test]
1046 fn interpolated_string_defined_var_ok() {
1047 assert!(lint(r#"my $x = 1; p "hello $x""#).is_ok());
1048 }
1049
1050 #[test]
1051 fn coderef_params_in_scope() {
1052 assert!(lint("my $f = fn ($x) { p $x; }; $f->(1)").is_ok());
1053 }
1054
1055 #[test]
1056 fn nested_sub_scope() {
1057 assert!(lint("fn wrap { my $x = 1; fn inner { p $x; } }").is_ok());
1058 }
1059
1060 #[test]
1061 fn hash_element_access_ok() {
1062 assert!(lint("my %h = (a => 1); p $h{a}").is_ok());
1063 }
1064
1065 #[test]
1066 fn array_element_access_ok() {
1067 assert!(lint("my @a = (1, 2, 3); p $a[0]").is_ok());
1068 }
1069
1070 #[test]
1071 fn undefined_hash_detected() {
1072 let r = lint("p $undefined_hash{key}");
1073 assert!(r.is_err());
1074 }
1075
1076 #[test]
1077 fn undefined_array_detected() {
1078 let r = lint("p $undefined_array[0]");
1079 assert!(r.is_err());
1080 }
1081
1082 #[test]
1083 fn map_with_topic_ok() {
1084 assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
1085 }
1086
1087 #[test]
1088 fn grep_with_topic_ok() {
1089 assert!(lint("my @x = grep { $_ > 1 } 1..3").is_ok());
1090 }
1091
1092 #[test]
1093 fn sort_with_ab_ok() {
1094 assert!(lint("my @x = sort { $a <=> $b } 1..3").is_ok());
1095 }
1096
1097 #[test]
1098 fn ternary_undefined_var_detected() {
1099 let r = lint("my $x = $undefined ? 1 : 0");
1100 assert!(r.is_err());
1101 }
1102
1103 #[test]
1104 fn binop_undefined_var_detected() {
1105 let r = lint("my $x = 1 + $undefined");
1106 assert!(r.is_err());
1107 }
1108
1109 #[test]
1110 fn postfix_if_undefined_detected() {
1111 let r = lint("p 'x' if $undefined");
1112 assert!(r.is_err());
1113 }
1114
1115 #[test]
1116 fn while_loop_var_ok() {
1117 assert!(lint("my $i = 0; while ($i < 10) { p $i; $i++; }").is_ok());
1118 }
1119
1120 #[test]
1121 fn for_loop_init_var_in_scope() {
1122 assert!(lint("for (my $i = 0; $i < 10; $i++) { p $i; }").is_ok());
1123 }
1124
1125 #[test]
1126 fn given_when_ok() {
1127 assert!(lint("my $x = 1; given ($x) { when (1) { p 'one'; } }").is_ok());
1128 }
1129
1130 #[test]
1131 fn arrow_deref_ok() {
1132 assert!(lint("my $h = { a => 1 }; p $h->{a}").is_ok());
1133 }
1134
1135 #[test]
1136 fn method_call_ok() {
1137 assert!(lint("my $obj = bless {}, 'Foo'; $obj->method()").is_ok());
1138 }
1139
1140 #[test]
1141 fn push_builtin_ok() {
1142 assert!(lint("my @a; push @a, 1, 2, 3").is_ok());
1143 }
1144
1145 #[test]
1146 fn splice_builtin_ok() {
1147 assert!(lint("my @a = (1, 2, 3); splice @a, 1, 1, 'x'").is_ok());
1148 }
1149
1150 #[test]
1151 fn substr_builtin_ok() {
1152 assert!(lint("my $s = 'hello'; p substr($s, 0, 2)").is_ok());
1153 }
1154
1155 #[test]
1156 fn sprintf_builtin_ok() {
1157 assert!(lint("my $s = sprintf('%d', 42)").is_ok());
1158 }
1159
1160 #[test]
1161 fn range_ok() {
1162 assert!(lint("my @a = 1..10").is_ok());
1163 }
1164
1165 #[test]
1166 fn qw_ok() {
1167 assert!(lint("my @a = qw(a b c)").is_ok());
1168 }
1169
1170 #[test]
1171 fn regex_ok() {
1172 assert!(lint("my $x = 'hello'; $x =~ /ell/").is_ok());
1173 }
1174
1175 #[test]
1176 fn anonymous_sub_captures_outer_var() {
1177 assert!(lint("my $x = 1; my $f = fn { p $x; }").is_ok());
1178 }
1179
1180 #[test]
1181 fn state_var_ok() {
1182 assert!(lint("fn my_counter { state $n = 0; $n++; }").is_ok());
1183 }
1184
1185 #[test]
1186 fn our_var_ok() {
1187 assert!(lint("our $VERSION = '1.0'").is_ok());
1188 }
1189
1190 #[test]
1191 fn local_var_ok() {
1192 assert!(lint("local $/ = undef").is_ok());
1193 }
1194
1195 #[test]
1196 fn chained_method_calls_ok() {
1197 assert!(lint("my $x = Foo->new->bar->baz").is_ok());
1198 }
1199
1200 #[test]
1201 fn list_assignment_ok() {
1202 assert!(lint("my ($a, $b, $c) = (1, 2, 3); p $a + $b + $c").is_ok());
1203 }
1204
1205 #[test]
1206 fn hash_slice_ok() {
1207 assert!(lint("my %h = (a => 1, b => 2); my @v = @h{qw(a b)}").is_ok());
1208 }
1209
1210 #[test]
1211 fn array_slice_ok() {
1212 assert!(lint("my @a = (1, 2, 3, 4); my @b = @a[0, 2]").is_ok());
1213 }
1214}