Skip to main content

stryke/
static_analysis.rs

1//! Static analysis pass for detecting undefined variables and subroutines.
2
3use 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        // Late static binding: static::method() is always valid (runtime-resolved)
134        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                // Register class name as a callable (constructor)
216                self.declare_sub(&def.name);
217                // Register static methods and static fields as Class::name
218                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, .. } => {
668                self.analyze_expr(from);
669                self.analyze_expr(to);
670            }
671            ExprKind::InterpolatedString(parts) => {
672                for part in parts {
673                    match part {
674                        StringPart::ScalarVar(name) => {
675                            if !self.is_scalar_defined(name) {
676                                self.error(
677                                    ErrorKind::UndefinedVariable,
678                                    format!(
679                                        "Global symbol \"${}\" requires explicit package name",
680                                        name
681                                    ),
682                                    expr.line,
683                                );
684                            }
685                        }
686                        StringPart::ArrayVar(name) => {
687                            if !self.is_array_defined(name) {
688                                self.error(
689                                    ErrorKind::UndefinedVariable,
690                                    format!(
691                                        "Global symbol \"@{}\" requires explicit package name",
692                                        name
693                                    ),
694                                    expr.line,
695                                );
696                            }
697                        }
698                        StringPart::Expr(e) => self.analyze_expr(e),
699                        StringPart::Literal(_) => {}
700                    }
701                }
702            }
703            ExprKind::Regex(_, _)
704            | ExprKind::Substitution { .. }
705            | ExprKind::Transliterate { .. }
706            | ExprKind::Match { .. } => {}
707            ExprKind::HashSliceDeref { container, keys } => {
708                self.analyze_expr(container);
709                for k in keys {
710                    self.analyze_expr(k);
711                }
712            }
713            ExprKind::AnonymousListSlice { source, indices } => {
714                self.analyze_expr(source);
715                for i in indices {
716                    self.analyze_expr(i);
717                }
718            }
719            ExprKind::SubroutineRef(name) | ExprKind::SubroutineCodeRef(name)
720                if !self.is_sub_defined(name) =>
721            {
722                self.error(
723                    ErrorKind::UndefinedSubroutine,
724                    format!("Undefined subroutine &{}", name),
725                    expr.line,
726                );
727            }
728            ExprKind::DynamicSubCodeRef(e) => self.analyze_expr(e),
729            ExprKind::PostfixIf { expr, condition }
730            | ExprKind::PostfixUnless { expr, condition }
731            | ExprKind::PostfixWhile { expr, condition }
732            | ExprKind::PostfixUntil { expr, condition } => {
733                self.analyze_expr(expr);
734                self.analyze_expr(condition);
735            }
736            ExprKind::PostfixForeach { expr, list } => {
737                self.analyze_expr(list);
738                self.analyze_expr(expr);
739            }
740            ExprKind::Do(e) | ExprKind::Eval(e) => {
741                self.analyze_expr(e);
742            }
743            ExprKind::Caller(Some(e)) => {
744                self.analyze_expr(e);
745            }
746            ExprKind::Length(e) => {
747                self.analyze_expr(e);
748            }
749            ExprKind::Print { args, .. }
750            | ExprKind::Say { args, .. }
751            | ExprKind::Printf { args, .. } => {
752                for a in args {
753                    self.analyze_expr(a);
754                }
755            }
756            ExprKind::Die(args)
757            | ExprKind::Warn(args)
758            | ExprKind::Unlink(args)
759            | ExprKind::Chmod(args)
760            | ExprKind::System(args)
761            | ExprKind::Exec(args) => {
762                for a in args {
763                    self.analyze_expr(a);
764                }
765            }
766            ExprKind::Push { array, values } | ExprKind::Unshift { array, values } => {
767                self.analyze_expr(array);
768                for v in values {
769                    self.analyze_expr(v);
770                }
771            }
772            ExprKind::Splice {
773                array,
774                offset,
775                length,
776                replacement,
777            } => {
778                self.analyze_expr(array);
779                if let Some(o) = offset {
780                    self.analyze_expr(o);
781                }
782                if let Some(l) = length {
783                    self.analyze_expr(l);
784                }
785                for r in replacement {
786                    self.analyze_expr(r);
787                }
788            }
789            ExprKind::MapExpr { block, list, .. } | ExprKind::GrepExpr { block, list, .. } => {
790                self.push_scope();
791                self.analyze_block(block);
792                self.pop_scope();
793                self.analyze_expr(list);
794            }
795            ExprKind::SortExpr { list, .. } => {
796                self.analyze_expr(list);
797            }
798            ExprKind::Open { handle, mode, file } => {
799                self.analyze_expr(handle);
800                self.analyze_expr(mode);
801                if let Some(f) = file {
802                    self.analyze_expr(f);
803                }
804            }
805            ExprKind::Close(e)
806            | ExprKind::Pop(e)
807            | ExprKind::Shift(e)
808            | ExprKind::Keys(e)
809            | ExprKind::Values(e)
810            | ExprKind::Each(e)
811            | ExprKind::Chdir(e)
812            | ExprKind::Require(e)
813            | ExprKind::Ref(e)
814            | ExprKind::Chomp(e)
815            | ExprKind::Chop(e)
816            | ExprKind::Lc(e)
817            | ExprKind::Uc(e)
818            | ExprKind::Lcfirst(e)
819            | ExprKind::Ucfirst(e)
820            | ExprKind::Abs(e)
821            | ExprKind::Int(e)
822            | ExprKind::Sqrt(e)
823            | ExprKind::Sin(e)
824            | ExprKind::Cos(e)
825            | ExprKind::Exp(e)
826            | ExprKind::Log(e)
827            | ExprKind::Chr(e)
828            | ExprKind::Ord(e)
829            | ExprKind::Hex(e)
830            | ExprKind::Oct(e)
831            | ExprKind::Readlink(e)
832            | ExprKind::Readdir(e)
833            | ExprKind::Closedir(e)
834            | ExprKind::Rewinddir(e)
835            | ExprKind::Telldir(e) => {
836                self.analyze_expr(e);
837            }
838            ExprKind::Exit(Some(e)) | ExprKind::Rand(Some(e)) | ExprKind::Eof(Some(e)) => {
839                self.analyze_expr(e);
840            }
841            ExprKind::Mkdir { path, mode } => {
842                self.analyze_expr(path);
843                if let Some(m) = mode {
844                    self.analyze_expr(m);
845                }
846            }
847            ExprKind::Rename { old, new }
848            | ExprKind::Link { old, new }
849            | ExprKind::Symlink { old, new } => {
850                self.analyze_expr(old);
851                self.analyze_expr(new);
852            }
853            ExprKind::Chown(files) => {
854                for f in files {
855                    self.analyze_expr(f);
856                }
857            }
858            ExprKind::Substr {
859                string,
860                offset,
861                length,
862                replacement,
863            } => {
864                self.analyze_expr(string);
865                self.analyze_expr(offset);
866                if let Some(l) = length {
867                    self.analyze_expr(l);
868                }
869                if let Some(r) = replacement {
870                    self.analyze_expr(r);
871                }
872            }
873            ExprKind::Index {
874                string,
875                substr,
876                position,
877            }
878            | ExprKind::Rindex {
879                string,
880                substr,
881                position,
882            } => {
883                self.analyze_expr(string);
884                self.analyze_expr(substr);
885                if let Some(p) = position {
886                    self.analyze_expr(p);
887                }
888            }
889            ExprKind::Sprintf { format, args } => {
890                self.analyze_expr(format);
891                for a in args {
892                    self.analyze_expr(a);
893                }
894            }
895            ExprKind::Bless { ref_expr, class } => {
896                self.analyze_expr(ref_expr);
897                if let Some(c) = class {
898                    self.analyze_expr(c);
899                }
900            }
901            _ => {}
902        }
903    }
904}
905
906fn is_special_var(name: &str) -> bool {
907    if name.len() == 1 {
908        return true;
909    }
910    matches!(
911        name,
912        "ARGV"
913            | "ENV"
914            | "SIG"
915            | "INC"
916            | "AUTOLOAD"
917            | "STDERR"
918            | "STDIN"
919            | "STDOUT"
920            | "DATA"
921            | "UNIVERSAL"
922            | "VERSION"
923            | "ISA"
924            | "EXPORT"
925            | "EXPORT_OK"
926    )
927}
928
929pub fn analyze_program(program: &Program, file: &str) -> PerlResult<()> {
930    StaticAnalyzer::new(file).analyze(program)
931}
932
933#[cfg(test)]
934mod tests {
935    use super::*;
936    use crate::parse_with_file;
937
938    fn lint(code: &str) -> PerlResult<()> {
939        let prog = parse_with_file(code, "test.stk").expect("parse");
940        analyze_program(&prog, "test.stk")
941    }
942
943    #[test]
944    fn undefined_scalar_detected() {
945        let r = lint("say $undefined");
946        assert!(r.is_err());
947        let e = r.unwrap_err();
948        assert_eq!(e.kind, ErrorKind::UndefinedVariable);
949        assert!(e.message.contains("$undefined"));
950    }
951
952    #[test]
953    fn defined_scalar_ok() {
954        assert!(lint("my $x = 1; say $x").is_ok());
955    }
956
957    #[test]
958    fn undefined_sub_detected() {
959        let r = lint("nonexistent_function()");
960        assert!(r.is_err());
961        let e = r.unwrap_err();
962        assert_eq!(e.kind, ErrorKind::UndefinedSubroutine);
963        assert!(e.message.contains("nonexistent_function"));
964    }
965
966    #[test]
967    fn defined_sub_ok() {
968        assert!(lint("sub foo { 1 } foo()").is_ok());
969    }
970
971    #[test]
972    fn builtin_sub_ok() {
973        assert!(lint("say 'hello'").is_ok());
974        assert!(lint("print 'hello'").is_ok());
975        assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
976    }
977
978    #[test]
979    fn special_vars_ok() {
980        assert!(lint("say $_").is_ok());
981        assert!(lint("say @_").is_ok());
982        assert!(lint("say $a <=> $b").is_ok());
983    }
984
985    #[test]
986    fn foreach_var_in_scope() {
987        assert!(lint("foreach my $i (1..3) { say $i; }").is_ok());
988    }
989
990    #[test]
991    fn sub_params_in_scope() {
992        assert!(lint("sub foo($x) { say $x; } foo(1)").is_ok());
993    }
994
995    #[test]
996    fn assignment_declares_var() {
997        assert!(lint("$x = 1; say $x").is_ok());
998    }
999
1000    #[test]
1001    fn builtin_inc_ok() {
1002        assert!(lint("my $x = 1; inc($x)").is_ok());
1003    }
1004
1005    #[test]
1006    fn builtin_dec_ok() {
1007        assert!(lint("my $x = 1; dec($x)").is_ok());
1008    }
1009
1010    #[test]
1011    fn builtin_rev_ok() {
1012        assert!(lint("my $s = rev 'hello'").is_ok());
1013    }
1014
1015    #[test]
1016    fn builtin_p_alias_for_say_ok() {
1017        assert!(lint("p 'hello'").is_ok());
1018    }
1019
1020    #[test]
1021    fn builtin_t_thread_ok() {
1022        assert!(lint("t 1 inc inc").is_ok());
1023    }
1024
1025    #[test]
1026    fn thread_with_undefined_var_detected() {
1027        let r = lint("t $undefined inc");
1028        assert!(r.is_err());
1029    }
1030
1031    #[test]
1032    fn try_catch_var_in_scope() {
1033        assert!(lint("try { die 'err'; } catch ($e) { say $e; }").is_ok());
1034    }
1035
1036    #[test]
1037    fn interpolated_string_undefined_var() {
1038        let r = lint(r#"say "hello $undefined""#);
1039        assert!(r.is_err());
1040    }
1041
1042    #[test]
1043    fn interpolated_string_defined_var_ok() {
1044        assert!(lint(r#"my $x = 1; say "hello $x""#).is_ok());
1045    }
1046
1047    #[test]
1048    fn coderef_params_in_scope() {
1049        assert!(lint("my $f = sub ($x) { say $x; }; $f->(1)").is_ok());
1050    }
1051
1052    #[test]
1053    fn nested_sub_scope() {
1054        assert!(lint("sub outer { my $x = 1; sub inner { say $x; } }").is_ok());
1055    }
1056
1057    #[test]
1058    fn hash_element_access_ok() {
1059        assert!(lint("my %h = (a => 1); say $h{a}").is_ok());
1060    }
1061
1062    #[test]
1063    fn array_element_access_ok() {
1064        assert!(lint("my @a = (1, 2, 3); say $a[0]").is_ok());
1065    }
1066
1067    #[test]
1068    fn undefined_hash_detected() {
1069        let r = lint("say $undefined_hash{key}");
1070        assert!(r.is_err());
1071    }
1072
1073    #[test]
1074    fn undefined_array_detected() {
1075        let r = lint("say $undefined_array[0]");
1076        assert!(r.is_err());
1077    }
1078
1079    #[test]
1080    fn map_with_topic_ok() {
1081        assert!(lint("my @x = map { $_ * 2 } 1..3").is_ok());
1082    }
1083
1084    #[test]
1085    fn grep_with_topic_ok() {
1086        assert!(lint("my @x = grep { $_ > 1 } 1..3").is_ok());
1087    }
1088
1089    #[test]
1090    fn sort_with_ab_ok() {
1091        assert!(lint("my @x = sort { $a <=> $b } 1..3").is_ok());
1092    }
1093
1094    #[test]
1095    fn ternary_undefined_var_detected() {
1096        let r = lint("my $x = $undefined ? 1 : 0");
1097        assert!(r.is_err());
1098    }
1099
1100    #[test]
1101    fn binop_undefined_var_detected() {
1102        let r = lint("my $x = 1 + $undefined");
1103        assert!(r.is_err());
1104    }
1105
1106    #[test]
1107    fn postfix_if_undefined_detected() {
1108        let r = lint("say 'x' if $undefined");
1109        assert!(r.is_err());
1110    }
1111
1112    #[test]
1113    fn while_loop_var_ok() {
1114        assert!(lint("my $i = 0; while ($i < 10) { say $i; $i++; }").is_ok());
1115    }
1116
1117    #[test]
1118    fn for_loop_init_var_in_scope() {
1119        assert!(lint("for (my $i = 0; $i < 10; $i++) { say $i; }").is_ok());
1120    }
1121
1122    #[test]
1123    fn given_when_ok() {
1124        assert!(lint("my $x = 1; given ($x) { when (1) { say 'one'; } }").is_ok());
1125    }
1126
1127    #[test]
1128    fn arrow_deref_ok() {
1129        assert!(lint("my $h = { a => 1 }; say $h->{a}").is_ok());
1130    }
1131
1132    #[test]
1133    fn method_call_ok() {
1134        assert!(lint("my $obj = bless {}, 'Foo'; $obj->method()").is_ok());
1135    }
1136
1137    #[test]
1138    fn push_builtin_ok() {
1139        assert!(lint("my @a; push @a, 1, 2, 3").is_ok());
1140    }
1141
1142    #[test]
1143    fn splice_builtin_ok() {
1144        assert!(lint("my @a = (1, 2, 3); splice @a, 1, 1, 'x'").is_ok());
1145    }
1146
1147    #[test]
1148    fn substr_builtin_ok() {
1149        assert!(lint("my $s = 'hello'; say substr($s, 0, 2)").is_ok());
1150    }
1151
1152    #[test]
1153    fn sprintf_builtin_ok() {
1154        assert!(lint("my $s = sprintf('%d', 42)").is_ok());
1155    }
1156
1157    #[test]
1158    fn range_ok() {
1159        assert!(lint("my @a = 1..10").is_ok());
1160    }
1161
1162    #[test]
1163    fn qw_ok() {
1164        assert!(lint("my @a = qw(a b c)").is_ok());
1165    }
1166
1167    #[test]
1168    fn regex_ok() {
1169        assert!(lint("my $x = 'hello'; $x =~ /ell/").is_ok());
1170    }
1171
1172    #[test]
1173    fn anonymous_sub_captures_outer_var() {
1174        assert!(lint("my $x = 1; my $f = sub { say $x; }").is_ok());
1175    }
1176
1177    #[test]
1178    fn state_var_ok() {
1179        assert!(lint("sub counter { state $n = 0; $n++; }").is_ok());
1180    }
1181
1182    #[test]
1183    fn our_var_ok() {
1184        assert!(lint("our $VERSION = '1.0'").is_ok());
1185    }
1186
1187    #[test]
1188    fn local_var_ok() {
1189        assert!(lint("local $/ = undef").is_ok());
1190    }
1191
1192    #[test]
1193    fn chained_method_calls_ok() {
1194        assert!(lint("my $x = Foo->new->bar->baz").is_ok());
1195    }
1196
1197    #[test]
1198    fn list_assignment_ok() {
1199        assert!(lint("my ($a, $b, $c) = (1, 2, 3); say $a + $b + $c").is_ok());
1200    }
1201
1202    #[test]
1203    fn hash_slice_ok() {
1204        assert!(lint("my %h = (a => 1, b => 2); my @v = @h{qw(a b)}").is_ok());
1205    }
1206
1207    #[test]
1208    fn array_slice_ok() {
1209        assert!(lint("my @a = (1, 2, 3, 4); my @b = @a[0, 2]").is_ok());
1210    }
1211}