1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use std::sync::Arc;
use super::helpers::extract_simple_var;
use super::ExpressionAnalyzer;
use crate::flow_state::FlowState;
use crate::symbol::ReferenceKind;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Type};
use php_ast::owned::Expr;
impl<'a> ExpressionAnalyzer<'a> {
pub(super) fn analyze_variable(
&mut self,
name: &str,
expr: &Expr,
ctx: &mut FlowState,
) -> Type {
let name_str = name.trim_start_matches('$');
// Interned once: this runs for every `$var` read, and each string-keyed
// FlowState call would re-hash + lock the global interner.
let sym = mir_types::Name::from(name_str);
// View template files (blade templates and files under resources/views/) have
// variables injected from the calling scope, so undefined-variable diagnostics
// are false positives there.
let is_view_template = crate::diagnostics::is_view_template_path(&self.file);
if !ctx.var_is_defined_sym(sym)
&& !self.in_existence_check
&& !is_view_template
&& !ctx.has_dynamic_var_def
{
if ctx.var_possibly_defined_sym(sym) {
self.emit(
IssueKind::PossiblyUndefinedVariable {
name: name_str.to_string(),
},
Severity::Warning,
expr.span,
);
} else if name_str == "this" {
self.emit(
IssueKind::InvalidScope {
in_class: ctx.self_fqcn.is_some(),
},
Severity::Error,
expr.span,
);
} else {
self.emit(
IssueKind::UndefinedVariable {
name: name_str.to_string(),
},
Severity::Error,
expr.span,
);
}
}
// Purity check: a bare (whole-array) superglobal read (`return
// $_SERVER;`) reaches the same external mutable state as
// `$_SERVER['x']`, already checked in arrays.rs::analyze_array_access
// — but only for the indexed-access shape. Skipped when THIS read is
// itself that check's own array-access base (`in_array_access_base`),
// since that call site already emits its own check for the whole
// expression; without the guard this would double-report the same
// access at two different spans.
if ctx.is_in_pure_fn
&& !self.in_array_access_base
&& crate::util::is_superglobal_name(name_str)
{
self.emit(
IssueKind::ImpureGlobalVariable {
variable: name_str.to_string(),
},
Severity::Warning,
expr.span,
);
}
ctx.read_vars.insert(sym);
ctx.mark_consumed_sym(sym);
let ty = if name_str == "this" && !ctx.var_is_defined_sym(sym) {
Type::never()
} else {
ctx.get_var_sym(sym)
};
if self.collect_symbols {
self.record_symbol(
expr.span,
ReferenceKind::Variable(Arc::from(name_str)),
ty.clone(),
);
}
ty
}
pub(super) fn analyze_variable_variable(&mut self, inner: &Expr, ctx: &mut FlowState) -> Type {
let inner_ty = self.analyze(inner, ctx);
// `$$name` where `$name` holds a known literal string (or union of
// them) resolves to that variable's OWN real type — this used to
// always return bare `mixed`, even when the accessed variable name
// was fully known at analysis time. A non-literal-string `$name`
// (or no `$name` at all) stays `mixed`: the accessed variable is
// genuinely unknown.
let mut result = Type::empty();
if let Some(var_name) = extract_simple_var(inner) {
ctx.read_vars
.insert(mir_types::Name::from(var_name.as_str()));
for atomic in &inner_ty.types {
if let Atomic::TLiteralString(accessed_var_name) = atomic {
ctx.read_vars
.insert(mir_types::Name::from(accessed_var_name.as_ref()));
result.merge_with(&ctx.get_var(accessed_var_name.as_ref()));
}
}
}
if result.is_empty() {
Type::mixed()
} else {
result
}
}
pub(super) fn analyze_identifier(
&mut self,
name: &str,
expr: &Expr,
ctx: &mut FlowState,
) -> Type {
let name_str: &str = name;
let name_str = name_str.strip_prefix('\\').unwrap_or(name_str);
let ns_qualified = self
.db
.file_namespace(self.file.as_ref())
.map(|ns| format!("{}\\{}", ns, name_str));
let resolve_pull = |fqn: &str| -> Option<mir_types::Type> {
let here = crate::db::Fqcn::from_str(self.db, fqn);
crate::db::find_global_constant(self.db, here).map(|arc_union| (*arc_union).clone())
};
let resolved = ns_qualified
.as_deref()
.and_then(|fqn| resolve_pull(fqn).map(|ty| (fqn.to_string(), ty)))
.or_else(|| resolve_pull(name_str).map(|ty| (name_str.to_string(), ty)));
if let Some((fqn, ty)) = resolved {
self.record_ref(Arc::from(format!("gcnst:{fqn}")), expr.span);
self.record_symbol(
expr.span,
ReferenceKind::GlobalConstant(Arc::from(fqn.as_str())),
ty.clone(),
);
ty
} else if ctx.defined_guards.contains(name_str)
|| ns_qualified
.as_deref()
.is_some_and(|q| ctx.defined_guards.contains(q))
{
// Guarded by `defined('NAME')` — the constant is defined at runtime.
Type::mixed()
} else {
self.emit(
IssueKind::UndefinedConstant {
name: name_str.to_string(),
},
Severity::Error,
expr.span,
);
Type::mixed()
}
}
}