refactor(routes): share CommonJS checks and trim per-node work

- `isCommonJsGlobal`/`isModuleExports` in lexical-bindings replace the two
  copies in fetch-wrapper and framework-route-facts (both resolve the same
  scope from the same start position).
- LEXICAL_FUNCTION_TYPES is derived from the TypeScript FUNCTION_NODE_TYPES
  instead of a third hand-kept copy; the drift test still pins it.
- `unwrap` reads UNWRAPPED_TYPES, the set the cursor walk mirrors, and the
  class-body region is no longer shadowed under the name `deferred`.
- framework-route-facts reads each node's type once per loop iteration.
- The position index returns the latest-starting scope when it already
  contains the point, before the max-end tree search; that entry is the
  search's rightmost match.
- carried() returns a shared empty set for argument forms that cannot carry
  a value, the resolution file set is built without intermediate arrays,
  and the routes phase reads the call list once.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-10-20 07:25:04 +00:00
parent d8833c7ddf
commit 3db8657ba7
6 changed files with 78 additions and 69 deletions

View file

@ -203,6 +203,9 @@ function wrapIndex(files: ReadonlyMap<string, FileIndex>, size: number): Positio
const endIdx = findLastStartLteIndex(file.entries, line, col);
if (endIdx < 0) return undefined;
// The innermost scope usually starts last; it is then the rightmost match.
const latest = file.entries[endIdx];
if (latest && endIsAtOrAfter(latest.range, line, col)) return latest.id;
const match = findLastContainingEntry(file, endIdx, line, col);
return match < 0 ? undefined : file.entries[match]!.id;

View file

@ -1566,12 +1566,14 @@ export async function runChunkedParseAndResolve(
const routeFilePaths = new Set(allPaths);
const routeResolutionConfigs = new Map<SupportedLanguages, unknown>();
const resolutionFilePaths = new Set([
...allRouteFacts.map((file) => file.filePath),
...allFetchWrapperDefs.map((definition) => definition.filePath),
...allFetchWrapperCalls.map((call) => call.filePath),
...allFetchWrapperReexports.map((reexport) => reexport.filePath),
]);
const resolutionFilePaths = new Set<string>();
for (const facts of [
allRouteFacts,
allFetchWrapperDefs,
allFetchWrapperCalls,
allFetchWrapperReexports,
] as const)
for (const { filePath } of facts) resolutionFilePaths.add(filePath);
for (const filePath of resolutionFilePaths) {
const language = getLanguageFromFilename(filePath);
if (language === null || routeResolutionConfigs.has(language)) continue;

View file

@ -507,9 +507,10 @@ export const routesPhase: PipelinePhase<RoutesOutput> = {
(n): n is string => typeof n === 'string' && n.trim().length > 0,
);
// The index is only worth building when a candidate call can match a route.
if (routeRegistry.size > 0 && (allFetchWrapperCalls?.length ?? 0) > 0) {
const wrapperCalls = allFetchWrapperCalls ?? [];
if (routeRegistry.size > 0 && wrapperCalls.length > 0) {
const wrappers = buildFetchWrapperIndex(allFetchWrapperDefs ?? [], configuredWrappers);
for (const call of allFetchWrapperCalls ?? []) {
for (const call of wrapperCalls) {
if (wrappers.isWrapperCall(call)) allFetchCalls.push(call);
}
}

View file

@ -7,6 +7,8 @@ import {
bindingKey,
createLexicalBindings,
importBindingKey,
isCommonJsGlobal,
isModuleExports,
LEXICAL_FUNCTION_TYPES,
parameterPattern,
patternBindingNames,
@ -76,15 +78,21 @@ function calleeName(node: Parser.SyntaxNode | null): string | undefined {
return parts.reverse().join('.');
}
function unwrap(node: Node | null): Node | null {
while (node && (TRANSPARENT_EXPRESSION_TYPES.has(node.type) || node.type === 'await_expression'))
node = firstNamed(node);
return node;
}
const UNWRAPPED_TYPES: ReadonlySet<string> = new Set([
...TRANSPARENT_EXPRESSION_TYPES,
'await_expression',
]);
function unwrap(node: Node | null): Node | null {
while (node && UNWRAPPED_TYPES.has(node.type)) node = firstNamed(node);
return node;
}
/** Argument forms that can carry a parameter's value into a call. */
const CARRYING_TYPES: ReadonlySet<string> = new Set([
'identifier',
'template_string',
'binary_expression',
]);
const NO_BINDINGS: ReadonlySet<string> = new Set();
const SKIPPED_TYPES: ReadonlySet<string> = new Set([
'comment',
'type_annotation',
@ -312,16 +320,10 @@ export function createFetchWrapperContext(
exported(node, name);
else if (node) exportedValues.push({ value: node, name });
};
const commonJsGlobal = (node: Node | null, name: string) =>
node?.type === 'identifier' && node.text === name && bindings.isGlobal(node, name);
const moduleExports = (node: Node | null) =>
node?.type === 'member_expression' &&
node.childForFieldName('property')?.text === 'exports' &&
commonJsGlobal(node.childForFieldName('object'), 'module');
// `module.exports = …`, `module.exports.name = …` and `exports.name = …`.
const commonJsExport = (left: Node | null, right: Node | null) => {
if (left?.type !== 'member_expression') return;
if (moduleExports(left)) {
if (isModuleExports(bindings, left)) {
const value = unwrap(right);
if (value?.type !== 'object') return exportValue(value, 'default');
for (const member of value.namedChildren) {
@ -343,7 +345,7 @@ export function createFetchWrapperContext(
const property = left.childForFieldName('property');
if (
property?.type === 'property_identifier' &&
(commonJsGlobal(object, 'exports') || moduleExports(object))
(isCommonJsGlobal(bindings, object, 'exports') || isModuleExports(bindings, object))
)
exportValue(right, property.text);
};
@ -479,13 +481,13 @@ export function createFetchWrapperContext(
}
if (type === 'class_declaration' || type === 'class') {
const node = cursor.currentNode;
const deferred = region(node);
const classBody = region(node);
const name = node.childForFieldName('name');
const stored = storedAt(frames, first, cursor.currentFieldName);
bindRegion(stored ?? name, deferred);
if (stored && name) bindRegion(name, deferred);
if (!stored && !name) active.edges.add(deferred);
return { ...frame, children: 'class', inner: deferred };
bindRegion(stored ?? name, classBody);
if (stored && name) bindRegion(name, classBody);
if (!stored && !name) active.edges.add(classBody);
return { ...frame, children: 'class', inner: classBody };
}
if (type === 'field_definition' || type === 'public_field_definition') {
const isStatic = cursor.currentNode.children.some((child) => child.type === 'static');
@ -576,13 +578,11 @@ export function createFetchWrapperContext(
};
const argumentBindings = new Map<number, ReadonlySet<string>>();
const carried = (argument: Node | undefined): ReadonlySet<string> => {
if (!argument) return new Set();
if (!argument || !CARRYING_TYPES.has(argument.type)) return NO_BINDINGS;
const cached = argumentBindings.get(argument.id);
if (cached) return cached;
const result = new Set<string>();
const stack = ['identifier', 'template_string', 'binary_expression'].includes(argument.type)
? [argument]
: [];
const stack = [argument];
while (stack.length) {
const node = stack.pop()!;
if (node.type === 'identifier') {

View file

@ -5,6 +5,8 @@ import type { FrameworkExpression, FrameworkRouteFacts } from './framework-route
import {
createLexicalBindings,
importBindingKey,
isCommonJsGlobal,
isModuleExports,
LEXICAL_FUNCTION_TYPES,
parameterPattern,
patternBindingNames,
@ -201,11 +203,12 @@ export function extractFrameworkRouteFacts(
const nodes: { node: Node; enclosingFunction?: Node }[] = [{ node: tree.rootNode }];
while (nodes.length) {
const { node, enclosingFunction } = nodes.pop()!;
if (node.type === 'ERROR') continue;
const childFunction = LEXICAL_FUNCTION_TYPES.has(node.type) ? node : enclosingFunction;
const type = node.type;
if (type === 'ERROR') continue;
const childFunction = LEXICAL_FUNCTION_TYPES.has(type) ? node : enclosingFunction;
for (const child of children(node).reverse())
nodes.push({ node: child, enclosingFunction: childFunction });
if (node.type === 'import_statement') {
if (type === 'import_statement') {
// ESM decomposition already carries precise aliases/type-only flags. CJS
// imports are intentionally handled at their AST calls to guard require.
const position = `${node.startPosition.row + 1 + lineOffset}:${node.startPosition.column}`;
@ -224,7 +227,7 @@ export function extractFrameworkRouteFacts(
},
});
}
} else if (node.type === 'variable_declarator') {
} else if (type === 'variable_declarator') {
const name = node.childForFieldName('name');
const value = node.childForFieldName('value');
if (value && enclosingFunction) {
@ -241,17 +244,14 @@ export function extractFrameworkRouteFacts(
name,
value && FUNCTION_TYPES.includes(value.type) ? functionValue(value) : expression(value),
);
} else if (
node.type === 'function_declaration' ||
node.type === 'generator_function_declaration'
) {
} else if (type === 'function_declaration' || type === 'generator_function_declaration') {
const name = node.childForFieldName('name');
if (name && enclosingFunction) {
const id = bindingId(name);
if (id && parametersOf(enclosingFunction).has(id)) invalid.add(id);
}
if (name) bind(name, functionValue(node));
} else if (node.type === 'export_statement') {
} else if (type === 'export_statement') {
const sourceNode = node.childForFieldName('source');
const source = sourceNode && plainString(sourceNode);
if (node.children.some((child) => child.type === 'type')) continue;
@ -302,31 +302,26 @@ export function extractFrameworkRouteFacts(
}
}
} else if (
node.type === 'assignment_expression' ||
node.type === 'augmented_assignment_expression' ||
node.type === 'update_expression' ||
node.type === 'for_in_statement' ||
(node.type === 'unary_expression' && node.children.some((child) => child.type === 'delete'))
type === 'assignment_expression' ||
type === 'augmented_assignment_expression' ||
type === 'update_expression' ||
type === 'for_in_statement' ||
(type === 'unary_expression' && node.children.some((child) => child.type === 'delete'))
) {
const left = node.childForFieldName('left') ?? node.childForFieldName('argument');
const right = node.childForFieldName('right');
const object = left?.childForFieldName('object');
const property = left?.childForFieldName('property');
if (left?.type === 'identifier' && left.text === 'exports' && isGlobal(left, 'exports'))
cjsShortcutDetached = true;
const moduleExports = (candidate: Node | null | undefined): boolean =>
candidate?.type === 'member_expression' &&
candidate.childForFieldName('object')?.text === 'module' &&
candidate.childForFieldName('property')?.text === 'exports' &&
isGlobal(candidate, 'module');
if (isCommonJsGlobal(lexical, left, 'exports')) cjsShortcutDetached = true;
const writesExportProperty =
moduleExports(object) || (object?.text === 'exports' && isGlobal(object, 'exports'));
isModuleExports(lexical, object) || isCommonJsGlobal(lexical, object, 'exports');
if (
(left?.type === 'subscript_expression' && writesExportProperty) ||
(node.type !== 'assignment_expression' && (moduleExports(left) || writesExportProperty))
(type !== 'assignment_expression' &&
(isModuleExports(lexical, left) || writesExportProperty))
)
cjsUnknownWrite = true;
if (node.type === 'assignment_expression' && moduleExports(left)) {
if (type === 'assignment_expression' && isModuleExports(lexical, left)) {
cjsDefaultAssignments++;
cjsExports.length = 0;
if (right?.type === 'object') {
@ -349,7 +344,7 @@ export function extractFrameworkRouteFacts(
}
} else cjsExports.push({ name: 'default', value: expression(right) });
} else if (
node.type === 'assignment_expression' &&
type === 'assignment_expression' &&
left?.type === 'member_expression' &&
property?.type === 'property_identifier' &&
writesExportProperty
@ -412,7 +407,7 @@ export function extractFrameworkRouteFacts(
};
invalidate(left);
}
} else if (node.type === 'call_expression') {
} else if (type === 'call_expression') {
const callee = node.childForFieldName('function');
const method = callee?.childForFieldName('property')?.text;
if (callee?.type === 'member_expression' && method && METHODS.has(method)) {

View file

@ -1,6 +1,7 @@
import type Parser from 'tree-sitter';
import { buildPositionIndex, buildScopeTree, lookupLexicalName } from 'gitnexus-shared';
import type { NameClaimResult, ParsedFile, ParsedImport } from 'gitnexus-shared';
import { FUNCTION_NODE_TYPES } from '../languages/typescript/captures.js';
type Node = Parser.SyntaxNode;
export interface LexicalBindings {
@ -10,18 +11,9 @@ export interface LexicalBindings {
ambiguous(bindingId: string): boolean;
}
/** Callable boundaries, including methods that can contain their own var declarations. */
export const LEXICAL_FUNCTION_TYPES: ReadonlySet<string> = new Set([
'arrow_function',
'function_expression',
'function_declaration',
'generator_function',
'generator_function_declaration',
'method_definition',
'function_signature',
'method_signature',
'abstract_method_signature',
]);
/** Callable boundaries: the TypeScript function scopes, which cover JavaScript's. */
export const LEXICAL_FUNCTION_TYPES: ReadonlySet<string> = new Set(FUNCTION_NODE_TYPES);
const LEXICAL_FUNCTION_TYPE_LIST: string[] = [...FUNCTION_NODE_TYPES];
/** Expressions that wrap a value or an assignment target without changing it. */
export const TRANSPARENT_EXPRESSION_TYPES: ReadonlySet<string> = new Set([
@ -46,6 +38,22 @@ export const importBindingKey = (
imported: Extract<ParsedImport, { localName: string }>,
): string => bindingKey(imported.declaredAtScope ?? parsedFile.moduleScope, imported.localName);
/** An unshadowed CommonJS `module` or `exports` reference. */
export const isCommonJsGlobal = (
bindings: LexicalBindings,
node: Node | null | undefined,
name: 'module' | 'exports',
): boolean => node?.type === 'identifier' && node.text === name && bindings.isGlobal(node, name);
/** `module.exports` through an unshadowed `module`. */
export const isModuleExports = (
bindings: LexicalBindings,
node: Node | null | undefined,
): boolean =>
node?.type === 'member_expression' &&
node.childForFieldName('property')?.text === 'exports' &&
isCommonJsGlobal(bindings, node.childForFieldName('object'), 'module');
/** The pattern a formal parameter declares, without its type or default value. */
export function parameterPattern(node: Node | null): Node | null {
if (node?.type === 'required_parameter' || node?.type === 'optional_parameter')
@ -234,7 +242,7 @@ export function createLexicalBindings(
// functions have neither, so a native type search finds the few that do and
// only their formals and bodies are walked.
const ambiguous = new Set<string>();
for (const fn of rootNode.descendantsOfType([...LEXICAL_FUNCTION_TYPES])) {
for (const fn of rootNode.descendantsOfType(LEXICAL_FUNCTION_TYPE_LIST)) {
const parameters = fn.childForFieldName('parameters') ?? fn.childForFieldName('parameter');
if (!parameters || !containsParameterExpression(parameters)) continue;
let functionScope = scopes.getScope(scopeAt(parameters) ?? '');