mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-25 05:53:44 +00:00
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:
parent
d8833c7ddf
commit
3db8657ba7
6 changed files with 78 additions and 69 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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) ?? '');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue