class
Analyzer::Javascript::RouteHelperScanner
- Analyzer::Javascript::RouteHelperScanner
- Reference
- Object
Overview
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.
Included Modules
Defined in:
analyzer/analyzers/javascript/express/route_helper_scanner.crConstant Summary
-
FORWARD_CALL_RE =
/([A-Za-z_$][\w$]*)\s*(?:\[\s*([A-Za-z_$][\w$]*)\s*\]|\.\s*(get|post|put|delete|patch|head|options|all))\s*\(\s*(`[^`\n]*`|[A-Za-z_$][\w$]*)\s*,/ -
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_DEF_RES =
[/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/, /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\s*\*?\s*\(/, /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/, /(?:^|[;{}\n])\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\s*\*?\s*\(/, /(?:^|[;{}\n])\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/] -
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 throughconst { setupApiRoute } = require('./helpers').Every pattern ends at the opening parenthesis of the parameter list, so
end(0) - 1is that parenthesis. -
FUNCTION_GLUE_MAX =
120 -
FUNCTION_GLUE_RE =
/\A[\s=>:|,.<>\[\]A-Za-z_$0-9]*\z/ -
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. -
HTTP_VERBS =
["get", "post", "put", "delete", "patch", "head", "options", "all"] of ::String -
MAX_ATTRIBUTION_WORK =
1000000 -
Ceiling on
functions x forward matchesin one file, past which the innermost-enclosing-function attribution is abandoned rather than run. -
MODULE_IMPORT_RES =
[/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/, /import\s+([A-Za-z_$][\w$]*)\s+from\s*['"]([^'"]+)['"]/] -
NAMED_IMPORT_RES =
[/(?:const|let|var)\s*\{\s*([\s\S]*?)\s*\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/, /import\s*\{\s*([\s\S]*?)\s*\}\s*from\s*['"]([^'"]+)['"]/] -
REST_DESTRUCTURE_RE =
/(?:const|let|var)\s*\[([^\]\n]*)\]\s*=\s*([A-Za-z_$][\w$]*)/ -
const [router, verb, name] = args— the positional read of a rest parameter. Holes (const [, verb] = args) keep their position. -
ROUTER_PARAM_RE =
/\A_?(?:router|app|server|[A-Za-z_$][\w$]*(?:Router|App|Server|router|app|server))\z/ -
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 asrequest(axios, 'get', '/api/remote/users')— satisfies every other condition, and every such call becomes a route. The computedrecv[verb](path, …)shape looked distinctive enough to exempt until a hostile fixture showed it is exactly how a verb-parameterised HTTP client is called.apiis 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. -
TEST_TREE_RE =
/(?:\A|\/)(?:tests?|specs?|e2e|e2e-tests|__tests__|__mocks__|cypress|playwright)\/|[.\-](?:test|spec)\.[jt]sx?\z/ -
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 plaintest/api.jsthrough — 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
- .new(all_files : Array(String), base_paths : Array(String), base_path : String, logger : NoirLogger)
Instance Method Summary
- #any_helpers? : Bool
-
#forward_lines(file : String) : Set(Int32)
Lines of the forwarded calls inside the helpers defined in
file. -
#helpers : Hash(String, Hash(String, HelperSpec))
Defining file → helper name → spec.
-
#index : Nil
PASS 1: index every forwarding helper in the tree.
-
#routes_for(file : String, content : String) : Array(HelperRoute)
PASS 2: routes registered by calling an indexed helper from
file.
Class methods inherited from module Analyzer::Javascript::ExpressConstants
file_key(file_path : String) : Noir::LocatorKey(Array(String))
file_key,
function_key(file_path : String, function_name : String) : Noir::LocatorKey(Array(String))
function_key
Constructor Detail
Instance Method Detail
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.
Defining file → helper name → spec.
PASS 2: routes registered by calling an indexed helper from file.