Outside any function
In a mixed Go/JavaScript checkout, impact analysis reported zero changed functions and claimed every edit fell outside all functions. Why root manifests blind polyglot analyzers, how arrow functions hide from LSP kinds, and why missing symbols must be a gap rather than a verdict.

On this page · 4 sections
A pull request edited three files in a repository with a Go backend and a browser extension in extension/background.js. Inside background.js, an agent modified handleTabCreated, added a nested helper addChildTabHandoff, and updated routeMethod. The Git diff showed sixty changed lines inside function bodies.
prod-code impact reported three changed files, zero changed functions, zero callers, zero tests, and printed:
the whole suite, because lines changed outside any function in extension/background.js
Instead of running the two Vitest cases that exercise the extension, CI launched the repository’s full twenty-minute test suite.
The Root Manifest Trap
The analyzer began by inspecting the checkout root. Finding go.mod, it resolved the repository’s expected engine as "go". It then opened a single LspSession to gopls for the entire analysis run.
When the analysis loop iterated over the diff hunks, it sent textDocument/documentSymbol for extension/background.js through that Go session. gopls does not index JavaScript files. It answered with an empty symbol list: [].
The analyzer then made an unverified leap:
let spans: Vec<(u32, u32)> = functions.iter().map(|f| (f.1, f.2)).collect();
let lines: Vec<&str> = text.lines().collect();
if file_hunks.iter().any(|h| !h.inside(&spans, &lines)) {
unattributed.push(file.clone());
}
With zero function spans in hand, every hunk was judged to fall outside all functions. The file was pushed into unattributed_files, which forces a whole-suite run under the assumption that top-level module code, imports, or global constants changed.
Functions That Are Variables
Even when routed to a JavaScript language server, JavaScript symbol trees break single-language assumptions in two ways.
First, arrow functions and expressions assigned to constants:
export const routeMethod = (req) => {
return req.method.toLowerCase();
};
In the Language Server Protocol, routeMethod is declared with SymbolKind::Variable (kind 13) or SymbolKind::Constant (kind 14), not SymbolKind::Function (kind 12) or SymbolKind::Method (kind 6). A filter looking only for callable symbol kinds misses every modern functional export. We extended is_callable_symbol to inspect symbol details and declaration text for =>, function, and async.
Second, closures and nested declarations: addChildTabHandoff declared inside handleTabCreated lives inside the outer symbol’s children array. If the collector does not recurse into child symbols, nested modifications fail span containment checks.
Gaps Over False Verdicts
We changed two parts of the architecture.
First, impact::analyze now maintains a SessionPool keyed by (subpath, engine). Each file in the diff is inspected via its file extension and project path, then routed to its matching language server: gopls for Go, the TypeScript server for .js and .ts, clangd for C++.
Second, we separated “this change is outside any function” from “the analyzer could not inspect this file”:
match session.query(&abs, "textDocument/documentSymbol", params).await {
Ok(serde_json::Value::Array(symbols)) => symbols,
Ok(serde_json::Value::Null) => {
let error = "textDocument/documentSymbol answered null".to_string();
note(&mut incomplete, Gap::Symbols { file: file.clone(), error });
continue;
}
Err(e) => {
let error = format!("{e:#}");
note(&mut incomplete, Gap::Symbols { file: file.clone(), error });
continue;
}
}
When a node lacks an engine, or a language server disconnects, the report records an explicit Gap::Symbols and skips unattributed_files. The reason reported to CI now reads:
the whole suite, because the analysis is incomplete: the symbols of extension/background.js could not be read
The analyzer never claims the author wrote module-level code when the truth is that the analyzer failed to read the file.
The Rule
Never let an analyzer’s failure to read symbols masquerade as code outside functions. An empty symbol list is an unverified boundary, not proof that a file has no functions.
Cite this article
Alexander Panasenko (2026-10-02). Outside any function. https://prod.codes/blog/outside-any-function/