Analyzer::Javascript::RouteHelperScanner
Inherits Analyzer::Javascript::ExpressConstants < Reference < Object
Express routes registered through a project-local forwarding helper
rather than by calling router.get(...) at the call site.
// src/routes/helpers.js helpers.setupApiRoute = function (...args) { const [router, verb, name] = args; ... router[verb](name, middlewares, tryRoute(controller)); };
// src/routes/write/categories.js const { setupApiRoute } = require('../helpers'); setupApiRoute(router, 'get', '/:cid', [], controllers.write.categories.get);
The verb and the path are arguments, so no verb-DSL pattern matches the call site, and the helper's own body carries no literal path — the route is invisible to every other pass. NodeBB registers its entire write API (208 calls), its page routes (69) and its admin page routes (61) this way.
What counts as a forwarding helper
A function whose body calls <recv>[<verb>](<path>, …) or
<recv>.<literal verb>(<path>, …) where <recv> and <path> are both
bound to that function's own parameters. The binding may be direct
(function (router, verb, name)) or through a rest parameter that is
destructured positionally (function (...args) +
const [router, verb, name] = args), which is the shape NodeBB uses.
Why this does not mint routes for arbitrary calls
An earlier audit found ~250 NodeBB "endpoints" that were really HTTP
client calls in test/*.js. Anything that lets a plain function call
produce a route risks reopening that, so acceptance is deliberately
narrow:
- The receiver, the path — and, for the computed form, the verb —
must all be parameters of the same function.
request.get(url)insidefunction fetch(url)fails on the receiver. - The forwarded call must pass at least one argument after the path.
A one-argument
client.get(url)is not a registration. - The receiver parameter must be named like a router (
router,app,server,*Router,*App,*Server). That is what separates a route helper from a generic client wrapper such asfunction request(client, method, url) { client[method](url, o) }, which satisfies every other condition. - At the call site both the verb and the path must be literals. A computed path yields nothing rather than a guess.
- Definitions and call sites in test-stub / minified files are
skipped, exactly as
JSRouteExtractor.extract_routesskips them.
Constants
name(, obj.key(, obj['key'](: what a file calls, so only the
bindings it calls are resolved on disk.
FORWARD_CALL_RE minus its leading receiver: a necessary condition for
it, checked first. The full pattern opens with an identifier class, so
PCRE2 has no first byte to skip to and tries it at every position of
the file; this one opens at a [ or ., which it can search for.
Over 15k JS/TS files (78 MB) the gate alone takes 23ms against 330ms
for the full pattern, and it rejects all but a few hundred files.
The forwarded-registration shape, used both as the per-file gate and as the extractor. Group 1 is the receiver, group 2 the verb variable of the computed form, group 3 a literal verb, group 4 the path expression. The trailing comma is what enforces "at least one argument after the path".
Function definitions whose parameter list can be read positionally.
The captured name is the local one; for helpers.setupApiRoute = …
it is the last dotted segment, which is also the name importers see
through const { setupApiRoute } = require('./helpers').
Every pattern ends at the opening parenthesis of the parameter list,
so end(0) - 1 is that parenthesis.
Between the parameter list and the body there may only be syntactic
glue: =>, a TypeScript return type, whitespace. Anything else means
the { we found opens a later construct, not this function's body.
Ceiling on functions x forward matches in one file, past which the
innermost-enclosing-function attribution is abandoned rather than run.
const [router, verb, name] = args — the positional read of a rest
parameter. Holes (const [, verb] = args) keep their position.
A router-shaped parameter name. Required of the receiver, whichever form the forward takes.
This is the load-bearing false-positive guard. Without it a generic
client wrapper — exports.request = function (client, method, url) { return client[method](url, opts) }, called as
request(axios, 'get', '/api/remote/users') — satisfies every other
condition, and every such call becomes a route. The computed
recv[verb](path, …) shape looked distinctive enough to exempt until
a hostile fixture showed it is exactly how a verb-parameterised HTTP
client is called.
api is deliberately absent: api[method](url, body) on an axios
instance is a common client idiom. The cost of the rule is that a
helper whose router parameter is named something else (r, mux) is
not recognised, which is the narrow-but-defensible side of the trade.
A test tree, matched on the scan-base-relative path. Broader than
JSRouteExtractor.test_stub_only?, which only fires on dedicated
mock/e2e directories and so lets a plain test/api.js through —
NodeBB's does exactly that. A route helper invoked from a test is
never a production registration, and this pass only ever adds
endpoints, so excluding the whole test tree costs nothing real.
Constructors
Instance methods
Lines of the forwarded calls inside the helpers defined in file.
Express uses them to drop the phantom endpoint the generic parser
mints from a helper's own router.get(/api${name}, …): that line
declares a rewrite, it does not register a path.