Check a single scope for unused variables. `promoted_params` is `Some` when checking a method — it lists constructor promoted parameter names that should be skipped.
(
scope: &ScopeMap,
ctx: &mut DiagnosticCtx<'_>,
promoted_params: Option<&HashSet<String>>,
compact_vars: &HashSet<String>,
)
| 173 | /// `promoted_params` is `Some` when checking a method — it lists |
| 174 | /// constructor promoted parameter names that should be skipped. |
| 175 | fn check_scope( |
| 176 | scope: &ScopeMap, |
| 177 | ctx: &mut DiagnosticCtx<'_>, |
| 178 | promoted_params: Option<&HashSet<String>>, |
| 179 | compact_vars: &HashSet<String>, |
| 180 | ) { |
| 181 | if scope.frames.is_empty() { |
| 182 | return; |
| 183 | } |
| 184 | |
| 185 | let always_skip: HashSet<&str> = { |
| 186 | let mut set: HashSet<&str> = HashSet::new(); |
| 187 | for sg in SUPERGLOBALS { |
| 188 | set.insert(sg); |
| 189 | } |
| 190 | set.insert("$this"); |
| 191 | set |
| 192 | }; |
| 193 | |
| 194 | // Build a set of parameter names for each nested frame so we can |
| 195 | // exclude them from the parent frame's writes. Closure and arrow |
| 196 | // function parameters are written at offsets that are inside the |
| 197 | // parent frame but outside the child frame body — the parent must |
| 198 | // not claim them as its own writes. |
| 199 | for frame in scope.frames.iter() { |
| 200 | // Skip top-level frames — global scope has too many implicit defs. |
| 201 | if frame.kind == FrameKind::TopLevel { |
| 202 | continue; |
| 203 | } |
| 204 | |
| 205 | // For catch frames, we only check the catch variable (which is |
| 206 | // in frame.parameters). We don't re-check variables inherited |
| 207 | // from the parent — those are the parent frame's responsibility. |
| 208 | // This avoids duplicate diagnostics. |
| 209 | if frame.kind == FrameKind::Catch { |
| 210 | // Before PHP 8.0, catch variables are mandatory syntax — |
| 211 | // there is no way to omit them, so flagging them is noise. |
| 212 | if ctx.php_version >= PhpVersion::new(8, 0) { |
| 213 | check_catch_frame(frame, scope, ctx, &always_skip); |
| 214 | } |
| 215 | continue; |
| 216 | } |
| 217 | |
| 218 | // Collect all variables written in this frame (directly, not |
| 219 | // inside nested frames that create their own scope). |
| 220 | let mut written_vars: HashMap<&str, u32> = HashMap::new(); |
| 221 | |
| 222 | for access in &scope.accesses { |
| 223 | if !matches!(access.kind, AccessKind::Write | AccessKind::ReadWrite) { |
| 224 | continue; |
| 225 | } |
| 226 | if access.offset < frame.start || access.offset > frame.end { |
| 227 | continue; |
| 228 | } |
| 229 | // Skip writes inside nested frames (closures, arrow fns, |
| 230 | // catch blocks) — those belong to the child scope. |
| 231 | if is_in_nested_frame(access.offset, frame, &scope.frames) { |
| 232 | continue; |
no test coverage detected