Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/map/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export function rootIdentifier(node: any, ts: TsModule): string | undefined {
return undefined;
}

/** The leftmost identifier NODE of a chain, for callers that resolve it to its declaration. */
export function rootIdentifierNode(node: any, ts: TsModule): any | undefined {
let cur = node;
while (cur) {
if (ts.isIdentifier(cur)) return cur;
if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) || ts.isNewExpression(cur) || ts.isNonNullExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAwaitExpression(cur)) {
cur = cur.expression;
} else return undefined;
}
return undefined;
}

// Source span of a node: the auditable coordinate, AND the sink's identity for flow analysis (a line is
// not an identity — two sinks can share one, and an enclosing statement can hold unrelated expressions).
export function spanOf(node: any): { line?: number; start?: number; end?: number } {
Expand Down Expand Up @@ -140,6 +152,17 @@ export function opCallOf(propAccess: any, ts: TsModule): any {
return p && ts.isCallExpression(p) && p.expression === propAccess ? p : propAccess;
}

/** The callee identifiers of plain calls (`run(x)`) in a subtree, for resolving each to its declaration. */
export function localCallIdentifiers(node: any, ts: TsModule): any[] {
const out: any[] = [];
const visit = (n: any) => {
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) out.push(n.expression);
ts.forEachChild(n, visit);
};
visit(node);
return out;
}

export function localCalls(node: any, ts: TsModule): string[] {
const names: string[] = [];
const visit = (n: any) => {
Expand Down
12 changes: 6 additions & 6 deletions src/map/entries.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type { Endpoint, Sink, TsModule } from './types.js';
import { hasExport, isFnLike, methodFromObjectArg, spanOf, unwindChain } from './ast.js';
import type { Bindings } from './bindings.js';
import { functionNameFromPath, ROUTE_REGISTER, routeFromChain, routeObject } from './routes.js';
import { functionNameFromPath, isRoutePath, ROUTE_REGISTER, routeFromChain, routeObject } from './routes.js';
import { withCoordinates } from './coordinates.js';
import { inputsFromHandler, inputsFromValidator } from './inputs.js';
import { sinksFrom, type SinkContext } from './sinks.js';
import { sinksFrom, type LocalSinks, type SinkContext } from './sinks.js';
import { linkedFlows } from './flows.js';
import { collectInvocations } from './invocations.js';

const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);

// --- entry-point recognizers -----------------------------------------------
export function extractFromFile(sf: any, ts: TsModule, localSinks: Map<string, Sink[]>, bindings: Bindings, ctx: SinkContext): Omit<Endpoint, 'file'>[] {
export function extractFromFile(sf: any, ts: TsModule, localSinks: LocalSinks, bindings: Bindings, ctx: SinkContext): Omit<Endpoint, 'file'>[] {
const out: Omit<Endpoint, 'file'>[] = [];
const isServerActionsFile = fileHasUseServer(sf, ts);

Expand Down Expand Up @@ -88,7 +88,7 @@ export function extractFromFile(sf: any, ts: TsModule, localSinks: Map<string, S
const first = args[0];
const route = first && ts.isStringLiteralLike(first) ? first.text : routeFromChain(node.expression.expression, ts);
const handler = args[args.length - 1];
if (route !== undefined && handler && isFnLike(handler, ts)) {
if (route !== undefined && isRoutePath(route) && handler && isFnLike(handler, ts)) {
out.push(handlerEntry(route, 'route-registration', handler.parameters, handler.body, ts, localSinks, bindings, ctx, {
// `use`/`all` register handlers but are not HTTP methods — leave method undefined.
method: HTTP_METHODS.has(mname.toUpperCase()) ? mname.toUpperCase() : undefined,
Expand All @@ -102,7 +102,7 @@ export function extractFromFile(sf: any, ts: TsModule, localSinks: Map<string, S
const arg = node.arguments[0];
if (arg && ts.isObjectLiteralExpression(arg)) {
const reg = routeObject(arg, ts);
if (reg.url && reg.handler) {
if (reg.url && isRoutePath(reg.url) && reg.handler) {
for (const m of reg.methods.length ? reg.methods : [undefined]) {
out.push(handlerEntry(reg.url, 'route-registration', reg.handler.parameters, reg.handler.body, ts, localSinks, bindings, ctx, { method: m, route: reg.url, ...spanOf(node) }));
}
Expand Down Expand Up @@ -138,7 +138,7 @@ function handlerEntry(
params: any,
body: any,
ts: TsModule,
localSinks: Map<string, Sink[]>,
localSinks: LocalSinks,
bindings: Bindings,
ctx: SinkContext,
extra: { method?: string; route?: string; line?: number; start?: number; end?: number } = {},
Expand Down
4 changes: 3 additions & 1 deletion src/map/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export async function extractInputMap(cwd: string, ts: TsModule, options: Extrac
const fn = functionNameFromPath(relFile);
if (fn) ep.route = '/' + fn; // how the platform invokes it (…/functions/v1/<name>)
}
if (ep.route === undefined && (ep.entryKind === 'route-handler' || ep.entryKind === 'server-action')) {
// A server action has no URL of its own: it is posted to whichever page renders it, so its file
// location names no route, and a route scope derived from it would never match its traffic.
if (ep.route === undefined && ep.entryKind === 'route-handler') {
const derived = routeFromFilePath(relFile);
if (derived.route) {
ep.route = derived.route;
Expand Down
Loading
Loading