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, 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}