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