class

Noir::JSRouteExtractor

Inherits Reference < Object

JSRouteExtractor provides a unified interface for extracting routes from JavaScript files

Constants

BARE_NEXT_RE = /\bnext\s*\(\s*\)/
BRACKET_ROUTE_CALL_PATTERN = /\[\s*['"](?:get|post|put|delete|del|patch|options|head|all|query)['"]\s*\]\s*\(/i

Pre-filter for extract_routes: returns false when content contains no shape the JS parser knows how to emit (any verb invocation pattern like .get(/.post(/... or Fastify/Restify .route(, plus Express-style mounts .use( which feed into the cross-file router prefix table). Matching is millions of times cheaper than tokenizing the file.

CJS_EXPORT_OBJECT_RE = /(?:\bmodule\.exports\s*=|\bexport\s+default)\s*\{([^}]*)\}/
CJS_EXPORT_PROPERTY_RE = /\b(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*[;\n]/
CLIENT_SIDE_FRAMEWORK_MARKER = Regex.union(CLIENT_SIDE_FRAMEWORK_MARKERS)
CLIENT_SIDE_FRAMEWORK_MARKERS = ["from \"vue\"", "from 'vue'", "from \"@vue/", "from '@vue/", "from \"vue-router\"", "from 'vue-router'", "from \"@vueuse/", "from '@vueuse/", "from \"pinia\"", "from 'pinia'", "from \"react\"", "from 'react'", "from \"react-dom", "from 'react-dom", "from \"react-router", "from 'react-router", "from \"@angular/", "from '@angular/", "from \"svelte\"", "from 'svelte'", "from \"svelte/", "from 'svelte/", "from \"solid-js", "from 'solid-js", "from \"preact\"", "from 'preact'", "from \"preact/", "from 'preact/", ".vue\"", ".vue'", ".svelte\"", ".svelte'", "from \"mobx", "from 'mobx", "from \"redux\"", "from 'redux'", "from \"react-redux", "from 'react-redux", "from \"@reduxjs/", "from '@reduxjs/", "from \"zustand", "from 'zustand", "from \"swr\"", "from 'swr'", "from \"@tanstack/react-query\"", "from '@tanstack/react-query'", "from \"@sentry/react\"", "from '@sentry/react'", "from \"vuex\"", "from 'vuex'", "from \"jquery\"", "from 'jquery'", "from \"k6", "from 'k6"]

Client-side UI framework imports. A file that imports a browser UI framework (Vue, React, Angular, Svelte, Solid, Preact) and its satellite libs (pinia, vue-router, @vueuse, react-router, ...) is SPA/frontend code, not an HTTP server. Its route-shaped calls are outbound API-client requests against a configured client — e.g. directus's admin app does api.get(/users/${userId}) where api is a wrapped axios instance imported from @/api. The existing axios/got/ky markers miss these because the wrapper hides the raw client behind a local module, but the UI-framework import is an unambiguous "this is browser code" signal. directus's admin SPA alone parks ~61 phantom Express endpoints across app/src/{stores,composables,layouts,...} this way. Like the test-stub markers, this is gated by the HTTP-server-import exemption below: an SSR entrypoint that imports BOTH vue and express keeps its routes.

COMMENT_OPENER_RE = /\/[\/*]/
DECLARATION_SEARCH_BYTES = 2000

Bytes after a route's start searched for its declaration when the direct .verb( probe misses. The declaration is the call at the route's own position; searching the whole file found the first route elsewhere with the same path (wrong handler) and cost O(file) per route.

E2E_SPEC_FILENAME_MARKER = "e2e-spec."
ERROR_HANDLER_SIGNATURE_RE = /\A\s*(?:async\s+)?(?:function(?:\s+[A-Za-z_$][\w$]*)?\s*)?\(\s*err\w*\s*[,:]/
EXPORT_ALIAS_RE = /\A([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)\z/
EXPORT_DECL_RE = /\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/
EXPORT_IDENT_RE = /\A[A-Za-z_$][\w$]*\z/
EXPORT_KEY_VALUE_RE = /\A([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\z/
EXPORT_LIST_RE = /\bexport\s*\{([^}]*)\}(?!\s*from\b)/
FASTIFY_RECEIVER_MARKER = /\bfastify\s*\.\s*(?:get|post|put|patch|delete|head|options|query|all|route|register)\s*\(/

A @fastify/autoload plugin module names no framework at all — it receives the instance as a parameter:

export default async function (fastify) {
  fastify.get('/status', handler)
}

so no import marker fires and the shared extractor happily reads those registrations for whichever framework asked. Express reported /status and /go out of the fastify autoload fixture, without the autoPrefix the Fastify analyzer applies. The receiver name is the only evidence in the file, and fastify is unambiguous — nothing else calls its app instance that. Consulted only when the file imports no HTTP server.

FINALE_ENDPOINTS_RE = /\bendpoints\s*:\s*\[([^\]]*)\]/
FINALE_FOR_OF_RE = /\bfor\s*\(\s*(?:const|let|var)\s*\{([^}]*)\}\s*of\s+([A-Za-z_$][\w$]*)\s*\)\s*\{/
FINALE_PLURAL_METHODS = ["GET", "POST"] of ::String
FINALE_RESOURCE_RE = /\b(?:finale|epilogue)\s*\.\s*resource\s*\(/
FINALE_SINGULAR_METHODS = ["GET", "PUT", "DELETE"] of ::String
FLEXIBLE_ROUTE_CALL_PATTERN = /\.(?:\s|\n|\r)*(?:get|post|put|delete|del|patch|options|head|all|query|route|register|use)(?:\s|\n|\r)*\(/i
GUARD_REFUSAL_RE = /\bexpress[jJ]wt\s*\(|\bpassport\.authenticate\s*\(|(?:\b(?:res|response|reply)\s*\.\s*)?(?:status|sendStatus|send)\s*\(\s*40[13]\s*\)\s*(?:\.\s*(?:json|send|end)\s*\([^)]*\))?|\bnext\s*\(\s*(?:new\s+)?\w*(?:Unauthori[sz]ed|Forbidden)\w*|\bcreateError\s*\(\s*40[13]\b|\bnext\s*\(\s*\{[^}]*\b40[13]\b/

Mirrors ExpressAuthTagger::REFUSING_DEFINITION: a definition whose only response is a 401/403 refusal (or that calls bare next() without sending a non-refusal response) is a middleware/guard, not the route's handler, so its body must not pollute the route's params or callees.

HANDLER_ANCHOR_RE = /\bfunction\b|=>/
HTTP_CLIENT_CONSTRUCTOR_MARKER = Regex.union("restify-clients", "createJSONClient(", "createStringClient(", "createHttpClient(")

Constructors that build an HTTP client. client.get('/todo', cb) is the same call shape as a route registration, so a client module reads as a server to the shared extractor: the Express analyzer reported GET /todo and DELETE /todo/example out of a restify-clients module that only calls a remote API.

The restify analyzer already refused these; the check belongs here so every framework sharing the extractor gets it. It is deliberately limited to client constructors rather than client package names — a genuine route file may well require('axios') to call downstream services, and gating on that would drop its routes.

HTTP_SERVER_LIBRARY_MARKER = Regex.union(HTTP_SERVER_LIBRARY_MARKERS)
HTTP_SERVER_LIBRARY_MARKERS = ["from \"express\"", "from 'express'", "require(\"express\")", "require('express')", "from \"fastify\"", "from 'fastify'", "require(\"fastify\")", "require('fastify')", "from \"koa\"", "from 'koa'", "require(\"koa\")", "require('koa')", "from \"hono\"", "from 'hono'", "require(\"hono\")", "require('hono')", "from \"restify\"", "from 'restify'", "require(\"restify\")", "require('restify')", "from \"polka\"", "from 'polka'", "from \"h3\"", "from 'h3'", "from \"@nestjs/", "from '@nestjs/", "from \"@oak/oak\"", "from '@oak/oak'", "deno.land/x/oak"]

Real HTTP-server library imports. When any of these is present alongside a test-stub marker, the file is doing legitimate server work (e.g., spinning up a test instance of an Express app) and we still want to extract its routes.

HYPHEN_TEST_FILENAME_MARKER = Regex.union("-spec.", "-test.")

The hyphen forms are also ordinary English in production router names (routes/ab-test.js, routes/api-spec.js), so they count as a test only without an HTTP server import, or with a test harness (a supertest suite imports express too). NestJS's app.e2e-spec.ts is always a test.

INLINE_CLOSURE_START_RE = /\A\s*(?:async\s+)?(?:function\b|\(|[A-Za-z_$][\w$]*\s*=>)/
INLINE_FUNCTION_RE = /\A\s*(?:async\s+)?(?:function\b|[A-Za-z_$][\w$]*\s*=>|\([^()]*(?:\([^()]*\)[^()]*)*\)\s*(?::[^=]*)?=>)/

A function literal: function, x =>, or a parameter list then => (async (ctx: Context<{ id: string }>) => …). (a || b) is not one.

LOCALS_READ_RE = /\b(?:res|response)\s*\.\s*locals\b/
LOCALS_WRITE_RE = /\b(?:res|response)\s*\.\s*locals\s*(?:\.\s*[\w$]+|\[[^\]]+\])?\s*=(?!=)/
MAX_MOUNT_PREFIXES = JSParser::MAX_MOUNT_PREFIXES

Ceiling on the prefixes one router collects. The (router, prefix) dedupe already bounds the work; this only stops a diamond lattice of parallel edges from multiplying without end. A shared router mounted under many paths stays well below it. Shared with JSParser's same-file router resolution.

MAX_REQUEST_CALLEES = 12
MINIFIED_AVG_LINE_THRESHOLD = 1000

Average bytes-per-line above which a file is considered dominated by long lines, i.e. a bundle rather than hand-written source that merely carries one fat literal (a big inline JSON seed, an embedded base64 data URI, a long regex). Real code keeps the average low because it has many short lines around any such literal.

MINIFIED_LINE_THRESHOLD = 5000

Byte length above which a single source line is considered "long". Hand-written JS/TS keeps lines well under this even in dense route tables (noir's own widest fixture line is ~150 bytes); webpack/ rollup/esbuild bundles and *.min.js assets routinely pack tens of thousands of bytes onto one line, so 5000 leaves a wide margin. NB: the metric is bytes, not characters — a dense single-line non-Latin blob (>=5000 bytes but fewer chars) can trip it, which is acceptable since real route registrations are ASCII verbs/paths.

MULTER_FIELD_RE = /\.(?:single|array)\s*\(\s*['"`]([^'"`]+)['"`]/

multer (and @koa/multer) middleware names the multipart file fields: upload.single('avatar'), upload.array('photos', 3), upload.fields([{ name: 'icon' }]).

MULTER_FIELDS_RE = /\.fields\s*\(\s*\[([^\]]*)\]/
MULTER_NAME_RE = /\bname\s*:\s*['"`]([^'"`]+)['"`]/
MULTIPART_AWAIT_RE = /(?:const|let|var)\s*\[\s*([\w$]+)\s*,\s*([\w$]+)\s*\]\s*=\s*await\s+[\w$.]+\.parse\s*\(\s*(?:req|request)\s*\)/
MULTIPART_CALLBACK_RE = /\.parse\s*\(\s*(?:req|request)\s*,\s*(?:async\s+)?(?:function\s*[\w$]*\s*)?\(\s*[\w$]+\s*,\s*([\w$]+)\s*,\s*([\w$]+)\s*\)/

formidable hands the parsed multipart body to a callback, form.parse(req, (err, fields, files) => …), or resolves it, const [fields, files] = await form.parse(req): files.image is an uploaded file, fields.title a form field.

NAMED_RELATIVE_IMPORT_RE = /\bimport\s*(?:[\w$]+\s*,\s*)?\{([^}]*)\}\s*from\s*['"](\.[^'"]*)['"]|\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"](\.[^'"]*)['"]\s*\)/

import { A, B as C } from './x', const { A, B: C } = require('./x').

NOT_A_CALLEE_RE = /\A(?:if|for|while|switch|catch|function|async|return|typeof|await|new|next|super)\z|\A(?:res|response)\./
OTHER_EXTRACTOR_MARKER = SHARED_EXTRACTOR_FRAMEWORK_MARKERS.keys.to_h do |framework| others = SHARED_EXTRACTOR_FRAMEWORK_MARKERS .reject do |other, _| other == framework end .values .flatten {framework, Regex.union(others)} end
OTHER_FRAMEWORK_MARKERS = {:nestjs => ["from \"@nestjs/", "from '@nestjs/", "require(\"@nestjs/", "require('@nestjs/"], :hapi => ["from \"@hapi/hapi\"", "from '@hapi/hapi'", "require(\"@hapi/hapi\")", "require('@hapi/hapi')", "from \"hapi\"", "from 'hapi'", "require(\"hapi\")", "require('hapi')"], :elysia => ["from \"elysia\"", "from 'elysia'", "require(\"elysia\")", "require('elysia')"], :adonisjs => ["from \"@adonisjs/", "from '@adonisjs/", "require(\"@adonisjs/", "require('@adonisjs/", "from \"@ioc:Adonis/", "from '@ioc:Adonis/"]}

Sibling JS/TS server frameworks that DON'T call extract_routes (NestJS uses decorators, Hapi/Elysia/AdonisJS have their own tree-sitter extractors) but whose files are still walked by every JS/TS analyzer's parallel_file_scan. These markers are exclusion- only: recognizing "this file belongs to NestJS" keeps Express/ Fastify/Koa/Hono/Restify from re-extracting a route-shaped call (an inline example, a raw app.use() bridge, ...) out of it.

OWN_EXTRACTOR_MARKER = SHARED_EXTRACTOR_FRAMEWORK_MARKERS .transform_values do |markers| Regex.union(markers) end

True when content carries definitive import evidence of a different shared-extractor (or sibling) framework than framework, with no evidence of framework itself. Guards the main extract_routes call in Express/Fastify/Koa/Hono/Restify's analyzers: a file that only imports 'hono' should never be re-attributed to js_express just because its .get()/.post() chaining looks the same as Express's (issue #2368) — whichever analyzer runs over it first no longer matters once #2367 made the dedup tiebreak deterministic, because the over-matching analyzer never produces a competing endpoint in the first place.

Files with no shared-extractor/sibling import at all (a router module that only receives app/router as a bare parameter, with no import in that file) return false here and fall through to the existing, permissive whole-tree scan — there's no import to disambiguate on, so narrowing further is left as follow-up scope (a confirmed package.json dependency or cross-file mount signal would be needed to resolve those). Per-framework precompiled unions of the two tables above: the framework's own import markers, and the markers of every other shared-extractor framework. Together with the sibling union this turns the up-to-44-literal includes? walk below into three matches. Five analyzers call this on every JS/TS file, so the old shape scanned some trees over 200 times per file.

PLAIN_RETURN_TYPE = /\A\s*:\s*[A-Za-z_$][\w$.]*(?:\[\])*\s*\z/

): void {, ): Response {, ): string[] {: a return type with no brackets of its own, so the next { opens the body.

REQUEST_CALL_RE = /(?<![\w$.])((?:[A-Za-z_$][\w$]*\s*\.\s*)*[A-Za-z_$][\w$]*)\s*\(/
REQUEST_RECEIVER_NAME = /\A(?:req|request)\z/
REQUEST_WRITE_RE = /\bdelete\s+[\w$.?\[\]'"]+|(?<![\w$.])(?:req|request|ctx(?:\s*\.\s*request)?)\s*\??\.\s*(?:body|query|headers|cookies|params)\s*\??(?:\.\s*[\w$]+|\[[^\]]+\])\s*=(?![=>])/

delete req.query.lang drops an input and req.body.sort = 'asc' or req.headers['x-tenant'] = 'default' sets one; neither reads what the client sent. req.query.page ||= '1' does read it, and is kept.

RESPONDS_RE = /\b(?:res|response|reply)\s*(?:\.status\s*\([^)]*\)\s*)?\.(?:send|sendStatus|sendFile|sendfile|json|jsonp|render|end|redirect|download|write)\s*\(/
ROUTE_RECEIVER_RE = /\A([A-Za-z_$][\w$]*)\s*\./
ROUTER_BODY_RE = /\bRouter\s*\(|\breturn\s+\w*[rR]outer\b|\.(?:get|post|put|delete|patch|all|route)\s*\(\s*['"`]\//

A router factory (module.exports = () => { router.get('/:id', …); return router }) sends inside the routes it registers; it is a mount, not a handler.

SERVER_EVIDENCE_MARKER = Regex.union(HTTP_SERVER_LIBRARY_MARKERS + SHARED_EXTRACTOR_FRAMEWORK_MARKERS.values.flatten)

Any import of a server framework the shared extractor serves. A file without one reads this.post(...) as an outbound client call, not a route (JSParser#initialize).

SHARED_EXTRACTOR_FRAMEWORK_MARKERS = {:express => ["from \"express\"", "from 'express'", "require(\"express\")", "require('express')"], :fastify => ["from \"fastify\"", "from 'fastify'", "require(\"fastify\")", "require('fastify')", "from \"fastify-plugin\"", "from 'fastify-plugin'", "require(\"fastify-plugin\")", "require('fastify-plugin')", "from \"@fastify/", "from '@fastify/", "require(\"@fastify/", "require('@fastify/"], :koa => ["from \"koa\"", "from 'koa'", "require(\"koa\")", "require('koa')", "from \"koa-router\"", "from 'koa-router'", "require(\"koa-router\")", "require('koa-router')", "from \"@koa/router\"", "from '@koa/router'", "require(\"@koa/router\")", "require('@koa/router')"], :hono => ["from \"hono\"", "from 'hono'", "require(\"hono\")", "require('hono')", "from \"hono/", "from 'hono/"], :restify => ["from \"restify\"", "from 'restify'", "require(\"restify\")", "require('restify')", "from \"restify-router\"", "from 'restify-router'", "require(\"restify-router\")", "require('restify-router')"], :oak => ["from \"@oak/oak\"", "from '@oak/oak'", "from \"jsr:@oak/oak", "from 'jsr:@oak/oak", "require(\"@oak/oak\")", "require('@oak/oak')", "deno.land/x/oak"]}

Import markers for the five frameworks whose analyzers call extract_routes directly and therefore share its framework-agnostic verb-chaining shape (.get(/.post(/...). Keyed by the same Symbol each analyzer passes to other_shared_extractor_framework? below.

SIBLING_FRAMEWORK_MARKER = Regex.union(OTHER_FRAMEWORK_MARKERS.values.flatten)
STATIC_MOUNT_MARKER = Regex.union(".use(", ".use (", ".register(", ".register (", "ServeStaticModule.forRoot", "serveStatic")

Extract static path declarations from JavaScript content Returns array of hashes with static_path (URL prefix) and file_path (directory) framework scopes the scan to one framework's static-mount idiom so a framework analyzer running over a sibling project's file (every JS analyzer walks all .js/.ts files) doesn't pick up another framework's static declaration and re-emit it under the wrong tech. nil runs every pattern (back-compat for un-scoped callers).

STRICT_TEST_PATH_MARKER = Regex.union(STRICT_TEST_PATH_MARKERS)
STRICT_TEST_PATH_MARKERS = ["/e2e/", "/cypress/", "/playwright/", "/__mocks__/", "/__tests__/", "/e2e-tests/", "/mirage/"]

True when the file's route-shaped calls are almost certainly mock-server stubs (Ember pretender, MSW, nock, ...) rather than real route registrations. Two routes:

Path markers strict enough that the HTTP-server-import exemption shouldn't apply: /e2e/, /cypress/, /playwright/, /__mocks__/, /__tests__/, /e2e-tests/, /mirage/. Real apps never park production handlers under any of these — even when the harness file imports express to spin up a faked service (Ghost's e2e/helpers/services/stripe/fake-stripe-server.ts is the canonical example). Keeping the exemption out of these paths catches the harness fakes without affecting legit backend code.

STRING_CONST_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[\w.]+\s*)?=\s*(?:process\.env(?:\.\w+|\[['"][^'"]+['"]\])\s*(?:\|\||\?\?)\s*)?(?:'([^'\n]*)'|"([^"\n]*)"|`([^`$]*)`)(?:\s+as\s+const)?(?=\s*(?:[;,\n]|\z))/

const NAME = 'lit', and process.env.X || 'lit' (?? too): the literal is what the app serves when the variable is unset. The literal must end the expression: '/api/' + version is no constant.

TEST_DIR_MARKER = /\/tests?\//

A suite file that registers its own throwaway app is a test whatever server library it imports. Line-anchored: .describe( is also zod's and Joi's schema annotation in ordinary route files.

TEST_HARNESS_MARKER = /\bdescribe\s*\(|\bDeno\.test\s*\(|\bsupertest\b|@nestjs\/testing/
TEST_STUB_FILENAME_MARKER = Regex.union(TEST_STUB_FILENAME_MARKERS)
  • Filename markers fire unconditionally — foo.test.ts is a test no matter what it imports.
  • Strict path markers also fire unconditionally — e2e/, cypress/, etc. are dedicated test/mock trees that never contain production handlers, even when the harness file imports a server lib.
  • Library + the remaining directory markers honor an exemption — if the file also imports a real HTTP server lib (express, fastify, ...), keep it so legit test-server harnesses (e.g. mattermost's webhook_serve.js) keep their routes. include_client_frameworks controls whether a client-side UI framework import (Vue/React/...) counts as a skip signal. It must be ON for the verb-DSL extractor (a React/Vue file calling api.get(...) is an outbound client call, not a route), but OFF for analyzers whose OWN route definitions live in client-side files — TanStack Router (createFileRoute) and tRPC route modules routinely import { ... } from 'react', and skipping them on that basis dropped every such route. The test-stub library markers (msw/supertest/...) and path/ filename markers still apply in both modes. Precompiled unions of the marker lists above. Regex.union escapes every String argument, so each is exactly the any? includes? it replaces — but the content lists are long (72 test-stub libraries, 32 client frameworks, 26 server libraries), and every JS/TS file in the tree used to be walked once per literal.
TEST_STUB_FILENAME_MARKERS = [".test.", ".spec.", ".test-d.", ".tst."]

Hard test-file markers: when the filename itself follows a ubiquitous test convention, the file practically never defines real routes. Skip these even when the file imports a real HTTP server lib — NestJS e2e tests routinely import @nestjs/platform-express for type-only references, and supertest harnesses import the same modules they exercise. The supertest request(app).get(...) shape would otherwise ride the HTTP-server-import exemption straight back into the parser.

TEST_STUB_LIBRARY_MARKER = Regex.union(TEST_STUB_LIBRARY_MARKERS)
TEST_STUB_LIBRARY_MARKERS = ["pretender", "miragejs", "ember-cli-mirage", "from \"msw\"", "from 'msw'", "from \"msw/", "from 'msw/", "require(\"msw\")", "require('msw')", "from \"nock\"", "from 'nock'", "require(\"nock\")", "require('nock')", "from \"fetch-mock\"", "from 'fetch-mock'", "from \"fetch-mock/", "from 'fetch-mock/", "require(\"fetch-mock\")", "require('fetch-mock')", "setupApplicationTest", "setupRenderingTest", "/// <reference types=\"cypress\" />", "from \"cypress\"", "from 'cypress'", "require(\"cypress\")", "require('cypress')", "from \"@playwright/test\"", "from '@playwright/test'", "from \"playwright\"", "from 'playwright'", "from \"supertest\"", "from 'supertest'", "require(\"supertest\")", "require('supertest')", "from \"axios\"", "from 'axios'", "require(\"axios\")", "require('axios')", "from \"purest\"", "from 'purest'", "require(\"purest\")", "require('purest')", "from \"got\"", "from 'got'", "require(\"got\")", "require('got')", "from \"ky\"", "from 'ky'", "require(\"ky\")", "require('ky')", "from \"superagent\"", "from 'superagent'", "require(\"superagent\")", "require('superagent')", "from \"node-fetch\"", "from 'node-fetch'", "require(\"node-fetch\")", "require('node-fetch')", "from \"ofetch\"", "from 'ofetch'", "require(\"ofetch\")", "require('ofetch')", "from \"undici\"", "from 'undici'", "require(\"undici\")", "require('undici')", "from \"request\"", "from 'request'", "require(\"request\")", "require('request')", "from \"apollo-datasource-rest\"", "from 'apollo-datasource-rest'", "require(\"apollo-datasource-rest\")", "require('apollo-datasource-rest')", "from \"@apollo/datasource-rest\"", "from '@apollo/datasource-rest'", "require(\"@apollo/datasource-rest\")", "require('@apollo/datasource-rest')", "@epic-web/test-server", "@open-draft/test-server", "create-test-server"]

Test-fixture libraries whose API mimics route registration: pretender/miragejs expose server.get("/x", ...), MSW and nock expose handler builders, sinon-via-faker likewise. When these libraries are imported, virtually every route-shaped call in the file is a stub, not a real registration. Substring match is enough — these tokens never appear in production HTTP server source under normal circumstances.

TEST_STUB_PATH_MARKER = Regex.union(TEST_STUB_PATH_MARKERS)
TEST_STUB_PATH_MARKERS = ["-pretender.", "-pretenders.", ".pretender.", "-mirage.", ".mirage.", "/tests/helpers/", "/test/helpers/", "/tests/api/", "/__tests__/", "/test/integration/", "/tests/integration/", "/test/e2e/", "/tests/e2e/", "/cypress/", "/playwright/", "/e2e-tests/", "/e2e/", "/mirage/", "/tests/frontend/", "/test/frontend/", "/test/fixtures/", "/tests/fixtures/", "/__fixtures__/", "/__mocks__/", "/dist/", "/build/", "/.next/", "/.nuxt/", "/.output/", "/coverage/", "/vendor/", "/app/javascript/", "/public/"]

Path-level evidence that a file is a mock-server fixture. Pretender helpers in particular get a helper/this arg and call this.get(...) / this.post(...) directly, so they have no library-name imports the content filter can hook on — fall back to the convention-based filename match.

TEST_SUITE_MARKER = /^[ \t]*(?:describe(?:\.(?:only|skip))?|Deno\.test)\s*\(|\bLab\.script\(/m
TRPC_NEXT_MOUNT_BASENAME = /\A\[(?:\.\.\.)?trpc\]\.[cm]?[jt]sx?\z/

A Next.js pages-router tRPC mount: a [trpc] / [...trpc] file (the adapter reads the procedure path from that query key) handing a router to createNextApiHandler. Its URLs are the router's procedures, which ts_trpc emits, not a {trpc} catch-all.

UNDERSCORE_TEST_FILENAME_MARKER = Regex.union("_spec.", "_test.")

Deno/mocha-style foo_test.ts / foo_spec.js. snake_case is ordinary in production names too (routes/ab_test.js, convex/load_test.ts, speed_test.js), so these are a test only when the file holds a suite (TEST_SUITE_MARKER) — never on the name alone and never because of an import (a fastify-plugin or router module imports nothing a test would).

Class methods

apply_handler_facts(endpoint : Endpoint, facts : HandlerFacts | Nil)
Source
attach_callees(endpoint : Endpoint, callees_by_route : Hash(String, Array(JSCalleeExtractor::Entry)), method : String, path : String, line : Int32)
Source
chain_step_callees(steps : Array(String), content : String, file_path : String) : Array(JSCalleeExtractor::Entry)

Callees of the route-specific steps before the handler. In Express's handler-chain style they do part of the route's work: juice-shop's app.post('/file-upload', …, checkFileType, handleZipFileUpload, handleXmlUpload, handleYamlUpload) extracts the zip and parses the XML in steps the last argument never reaches. Only steps that resolve to app source count (uploadToMemory.single('file') and express.json() do not), a guard step reads as no callees (read_handler_facts), and the caller adds these after the handler's own. Steps nearest the handler come first, so under Callee::MAX_PER_ENDPOINT they outrank the leading validators.

Source
definition_body(content : String, line : Int32) : String

The body of the function defined at line of content, its signature allowed to wrap lines (function v(\n ctx,\n): Promise<{ a: A }> {): the first { after the last => on the line, or after the parameter list, that follows ) or > — braces inside a return type follow < or :. A concise body (= () => jwt({ … })) yields the definition line.

Source
definition_params(content : String, line : Int32) : Array(String) | Nil

The parameter names of the function defined at line (function f(req, res), f = async (req, res) =>, f = req =>); "" for a destructured or rest parameter.

Source
export_bindings(content : String) : Hash(String, Array(String))

Local binding => the names it is exported under. An importer that takes a router by name (const { usersRouter } = require('./users'), import { usersRouter } from './users', users.router) records the mount prefix under that exported name, and the routes are registered on the local binding, so the two have to be joined here. Covers export const x = …, export { a as b }, module.exports = { a, b: c }, export default { a, b: c } and (module.)exports.b = a.

Source
extract_body_params(handler_body : String, endpoint : Endpoint)
Source
extract_header_params(handler_body : String, endpoint : Endpoint)
Source
extract_params_from_context(offsets : ByteOffsets, pattern : JSRoutePattern, endpoint : Endpoint, file_path : String | Nil = nil)

Every offset here is a BYTE offset into offsets.content (see ByteOffsets) so a route's cost is local to its own text.

Source
extract_path_params(handler_body : String, endpoint : Endpoint)
Source
extract_query_params(handler_body : String, endpoint : Endpoint)
Source
extract_routes(file_path : String, content : String | Nil = nil, debug : Bool = false, *, include_callees : Bool = false, route_callees : Hash(String, Array(JSCalleeExtractor::Entry)) | Nil = nil, unwrap_guards : Bool = false) : Array(Endpoint)
Source
extract_static_paths(content : String, framework : Symbol | Nil = nil) : Array(Hash(String, String))
Source
file_string_constants(file_path : String, content : String) : Hash(String, String)

The file's own string constants plus those it imports by name from a relative module (import { BASE_URL } from './config'); local wins.

Source
finale_resource_routes(content : String) : Array(Tuple(String, String, Int32))

finale-rest (and its predecessor epilogue) finale.resource({ endpoints: ['/users', '/users/:id'] }) registers list + create on the first path and read + update + delete on the second (the first again when there is no second). A ${name} placeholder is expanded when an enclosing for (const { name } of models) walks an array literal declared in the same file (juice-shop's autoModels); any other placeholder drops the path. Returns {method, url, call offset}; pass comment-stripped source.

Source
find_matching_brace(content : String, open_brace_idx : Int32) : Int32 | Nil

Delegate to JSLiteralScanner for literal-aware brace matching

Source
find_matching_paren(content : String, open_paren_idx : Int32) : Int32 | Nil

Delegate to JSLiteralScanner for literal-aware paren matching

Source
handler_definition_facts(handler : Handler, include_callees : Bool) : HandlerFacts | Nil
Source
handler_step(content : String, start_byte : Int32, file_path : String, offsets : ByteOffsets, unwrap_guards : Bool = false, last_step_counts : Proc(Hash(String, Int32)) | Nil = nil) : Tuple(String, Int32, Int32, Bool) | Nil

The route argument that serves the request, as {source, line it starts on, index, whether it precedes the renderer}: the last one, unless the last one is a shared renderer (router.get('/', load, respond): respond sends whatever the step before it left on res.locals, and ends many registrations). Then it is the step before it, when that step is the route's own work: an inline closure, or a definition that writes res.locals, that calls next() and is not a guard. A pass-through like noCache is not.

Source
inline_closure_facts(source : String, path : String, line : Int32, include_callees : Bool) : HandlerFacts | Nil

Facts for an inline closure passed directly to app.use('/path', (req, res) => { … }). Returns nil when source is not a function expression or arrow closure.

Source
inline_handler(step : String, line : Int32, content : String, file_path : String, unwrap_guards : Bool = false) : Handler | Nil

The closure written inline as the handler step (router.get('/x', auth(), async (ctx) => {…}), or inside a transparent wrapper: asyncHandler(async (req, res) => {…})) has no name to resolve; it is the handler, at the line it starts on.

Source
line_for_char_pos(content : String, pos : Int32) : Int32

1-based line number for a CHAR index into content.

Source
minified_content?(content : String, line_threshold : Int32 = MINIFIED_LINE_THRESHOLD, avg_threshold : Int32 = MINIFIED_AVG_LINE_THRESHOLD) : Bool

True when content looks like a minified/bundled asset rather than hand-written source. Two conditions must BOTH hold so we never drop the routes of a normal file that just happens to carry one long line (issue #1903 review):

  1. at least one line reaches MINIFIED_LINE_THRESHOLD bytes, and
  2. the rest of the file — every code line except that single fattest one — still averages MINIFIED_AVG_LINE_THRESHOLD bytes or more, i.e. long lines dominate and newline density is low.

Condition (2) deliberately sets aside the longest line and every blank/comment-only line before averaging. Averaging over the whole file (the original rule) let one fat literal drag a small module over the threshold: a 13-line Express server carrying a 20 KB const LOGO = 'data:image/png;base64,…' line averages ~1.5 KB/line and was classified as a bundle, so all ten of its routes were dropped silently. Dropping the single fattest line barely moves a real bundle's average (it has several enormous lines, or nothing else at all) but collapses such a module's to a few dozen bytes.

Blank and comment-only lines are excluded for the same reason in the other direction: the canonical *.min.js is one enormous line wrapped in a /*! license */ banner and a //# sourceMappingURL= trailer. Those decorations are not source, so a bundle that carries them must still be recognised as a bundle.

A file with no code line left over (a single enormous line, with or without banner/sourcemap decoration) is judged on that line alone — it is the bundle shape by definition.

webpack/rollup output and *.min.js satisfy both conditions; a route module with a fat inline payload amid real route lines satisfies neither, so its endpoints survive. Skipping such a file is purely a parser optimization — small files lex fast regardless — so there is no need to skip one merely because it embeds a fat literal.

Source
named_handler(step : String, content : String, file_path : String, unwrap_guards : Bool = false) : Handler | Nil

The handler step (handler_step) resolved to a definition when it is a plain reference (login, users.create, auth()), possibly in a transparent wrapper (asyncHandler(login)); nil for an inline closure. unwrap_guards: the caller's auth tagger reports a refusing wrapper (withAuth(list)) as the guard, so the handler is what it wraps.

Source
normalize_http_method(method : String) : String

Normalize HTTP method names to standard format

Source
other_shared_extractor_framework?(content : String, framework : Symbol) : Bool
Source
propagate_mount_prefixes(edges : Array(Tuple(String, String, String)), prefixes : Hash(String, Array(String)), fallback : Array(String) = [] of String) : Bool

Pushes mount prefixes along {parent, prefix, child} edges into prefixes, starting from each parent's existing prefixes (or fallback when it has none, without recording that on the parent).

Depth-first with the routers on the current mount path excluded, so a self-mount (router.use('/v1', router.routes())), a plugin that registers itself, or an a -> b -> a cycle is expanded at most once instead of growing the prefix list without bound — the fixpoint loops this replaced appended to the array they were iterating.

Returns true when some router hit MAX_MOUNT_PREFIXES and lost prefixes.

Source
push_callees(endpoint : Endpoint, callees : Array(JSCalleeExtractor::Entry) | Nil)
Source
resolve_mount_template(template : String, file_path : String, content : String) : String

A template-literal mount path (`/${rootPath}`) as a rooted path.

Source
resolve_template(template : String, constants : Hash(String, String)) : String

Substitutes every ${NAME} that constants knows; the rest stay.

Source
route_arguments(content : String, start_byte : Int32) : Array(String)
Source
route_call_candidate?(content : String) : Bool

A 22-literal includes? pre-pass used to run ahead of these two patterns (".get(", ".get (", ... for each verb). Every one of those literals is . + verb + optional space + (, which FLEXIBLE_ROUTE_CALL_PATTERN already matches with zero or one whitespace character — so the pre-pass could never change the result, and each miss cost 22 Rabin-Karp walks of the whole file.

Source
serves?(facts : HandlerFacts | Nil, before_renderer : Bool = false) : Bool

Whether a resolved step serves the request: not a guard, and not a pass-through that only calls next() (router.all('/api/*', prepareAPI) registers no handler). Unknown facts keep the step. The step before a shared renderer (handler_step) passes on by design.

Source
string_constants(content : String) : Hash(String, String)
Source
strip_js_comments(content : String) : String

Replace JS/TS comments with whitespace of the same shape. Preserves newlines and column offsets so downstream line/column math (controller_start_line, regex .begin(0), etc.) stays accurate. Comment bodies are blanked to spaces so a commented- out decorator like // @Get('/old') never matches the route regex.

Regex literals get their own state because their bodies routinely contain quote characters (str.replace(/'/g, "&#39;")). Without it a single such line inverted the string state for the rest of the file, which cut both ways: real comments stopped being blanked, so commented-out routes were reported as live endpoints, and real string bodies were scanned as code, so a // or /* inside a string opened a comment that swallowed every route below it.

Source
test_file?(file_path : String, content : String) : Bool

Whether the file is a test by its name, a dedicated test tree, or a suite under test//tests/, ignoring what it imports. Callers whose frameworks HTTP_SERVER_LIBRARY_MARKER does not list (hapi, Elysia) use this instead of test_stub_only?, which would let any server import keep a test's routes and drop real routes beside axios/react.

Source
test_named_or_in_test_tree?(basename : String, relative_path : String) : Bool

The content-free half of test_file?: a test by its name (*.test.*, *.spec.*, ...) or inside a dedicated test tree (__tests__/, e2e/, ...). Shared with JavascriptEngine#js_test_file?, which has to decide before it reads the file.

Source
test_stub_only?(file_path : String, content : String, include_client_frameworks : Bool = true) : Bool

Every marker below names a directory inside the project (__tests__/, dist/, vendor/, public/, ...), so they are matched on the scan-base-relative path. Matched on the absolute path they also fired on directories ABOVE the base: a checkout under ~/build/ or ~/vendor/ looked like bundled output and every route in it disappeared (the JS fixture tree dropped from 430 endpoints to 394, and to 191 under __tests__/).

Source
trpc_next_mount?(file_path : String, content : String) : Bool
Source

Nested types