mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
* fix(typescript): fix HOC pattern false positives and add export default HOC support Fixes two related issues from #1876: 1. False positives: `const x = arr.map(a => ...)` was incorrectly classified as a Function node. Split HOC patterns into identifier vs member_expression variants and apply a #not-any-of? blocklist for 36 array methods (map, filter, reduce, forEach, etc.) across all four query files. Runtime safety-net in tsExtractFunctionName uses a module-level ARRAY_METHODS constant (avoids per-call Set re-allocation). 2. Missing support: `export default defineEventHandler(async (e) => { ... })` and similar HOC-wrapped default exports were invisible. Added 4 export_statement patterns (TS + JS, legacy + registry-primary) and extended tsExtractFunctionName to derive the function name from the callee identifier. Now correctly distinguishes: - `const data = arr.map(account => ({...}))` → Const only (was Function+Const) - `const Button = forwardRef(...)` → Function:Button (unchanged) - `const Card = React.memo(...)` → Function:memo (unchanged) - `export default defineEventHandler(...)` → Function:defineEventHandler (new) Tests: add 2 fixture files and 4 test cases to typescript-hoc-wrapped suite covering the export default HOC positive case and array method exclusion negative case. Closes #1876 Co-authored-by: Claude <noreply@anthropic.com> AI-model: claude-sonnet-4-6 * fix(ingestion): tighten HOC callback attribution Share the TypeScript and JavaScript HOC blocklists across query and runtime paths, suppress stale array-method and built-in export-default wrappers, and derive export-default HOC names from the file instead of the wrapper helper. Also update the pinned unit and integration tests so CI reflects the new callback suppression contract. Co-authored-by: Claude <noreply@anthropic.com> AI-model: claude-sonnet-4-6 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
148 lines
6 KiB
TypeScript
148 lines
6 KiB
TypeScript
/**
|
|
* Coverage for the JavaScript scope-captures orchestrator, focused on the
|
|
* #1876 array-method-callback narrowing.
|
|
*
|
|
* `const x = arr.map(a => …)` must NOT produce a `@declaration.function`
|
|
* named `x` (the binding holds a value, not a callable) — only the
|
|
* `@declaration.const`. Identifier-callee HOCs (`forwardRef`, `useMemo`)
|
|
* and direct arrow assignments keep their `@declaration.function`.
|
|
*
|
|
* Runs against tree-sitter-javascript so it catches grammar drift before
|
|
* the integration parity gate.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { emitJsScopeCaptures } from '../../../../src/core/ingestion/languages/javascript/captures.js';
|
|
|
|
function matchesFor(src: string) {
|
|
return emitJsScopeCaptures(src, 'test.js');
|
|
}
|
|
|
|
/** True when some match carries `tag` and its @declaration.name is `name`. */
|
|
function hasDecl(src: string, tag: string, name: string): boolean {
|
|
return matchesFor(src).some((m) => m[tag] !== undefined && m['@declaration.name']?.text === name);
|
|
}
|
|
|
|
/** Count matches carrying `tag` (any name). */
|
|
function countTag(src: string, tag: string): number {
|
|
return matchesFor(src).filter((m) => m[tag] !== undefined).length;
|
|
}
|
|
|
|
describe('emitJsScopeCaptures — #1876 array-method-callback narrowing', () => {
|
|
it('does not emit @declaration.function for `const x = arr.map(a => …)`', () => {
|
|
const src = 'const exportData = accountsList.map(account => ({ id: account.id }));';
|
|
expect(hasDecl(src, '@declaration.const', 'exportData')).toBe(true);
|
|
expect(hasDecl(src, '@declaration.function', 'exportData')).toBe(false);
|
|
// Exactly one binding-bearing declaration for the name.
|
|
expect(countTag(src, '@declaration.function')).toBe(0);
|
|
});
|
|
|
|
// Every method in ARRAY_CALLBACK_METHODS except `map` (covered above).
|
|
it.each([
|
|
'filter',
|
|
'find',
|
|
'findIndex',
|
|
'findLast',
|
|
'findLastIndex',
|
|
'reduce',
|
|
'reduceRight',
|
|
'forEach',
|
|
'some',
|
|
'every',
|
|
'flatMap',
|
|
'sort',
|
|
])('suppresses the Function def for array method .%s()', (method) => {
|
|
const src = `const x = arr.${method}((a) => a);`;
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
expect(hasDecl(src, '@declaration.const', 'x')).toBe(true);
|
|
});
|
|
|
|
it('keeps @declaration.function for an identifier-callee HOC (forwardRef)', () => {
|
|
const src = 'const Button = forwardRef((props, ref) => null);';
|
|
expect(hasDecl(src, '@declaration.function', 'Button')).toBe(true);
|
|
});
|
|
|
|
it('keeps @declaration.function for useMemo (identifier callee, unchanged this round)', () => {
|
|
const src = 'const value = useMemo(() => compute(), []);';
|
|
expect(hasDecl(src, '@declaration.function', 'value')).toBe(true);
|
|
});
|
|
|
|
it('keeps dual classification for a direct arrow `const fn = () => {}`', () => {
|
|
const src = 'const fn = () => { doThing(); };';
|
|
expect(hasDecl(src, '@declaration.function', 'fn')).toBe(true);
|
|
expect(hasDecl(src, '@declaration.const', 'fn')).toBe(true);
|
|
});
|
|
|
|
it('keeps @declaration.function for a non-array fluent-API member call (accepted limitation)', () => {
|
|
const src = 'const q = qb.where((row) => row.ok);';
|
|
expect(hasDecl(src, '@declaration.function', 'q')).toBe(true);
|
|
});
|
|
|
|
it('suppresses an in-set method name on a NON-array receiver (accepted receiver-blind limitation)', () => {
|
|
// The predicate keys on the method NAME only, never the receiver type —
|
|
// tree-sitter has no type info. So `.map` on an RxJS observable (or
|
|
// Map/Set `.forEach`, a query builder `.sort`, a lodash chain `.filter`)
|
|
// is also treated as a callback and loses its Function def. Accepted: the
|
|
// binding holds the call's result value, so a value def is correct anyway.
|
|
const src = 'const stream = source$.map((event) => handle(event));';
|
|
expect(hasDecl(src, '@declaration.function', 'stream')).toBe(false);
|
|
expect(hasDecl(src, '@declaration.const', 'stream')).toBe(true);
|
|
});
|
|
|
|
it('suppresses the outer .map() callback in a chained array call', () => {
|
|
const src = 'const x = arr.filter((a) => a).map((b) => b);';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
expect(hasDecl(src, '@declaration.const', 'x')).toBe(true);
|
|
});
|
|
|
|
it('suppresses through an export_statement wrapper', () => {
|
|
const src = 'export const x = arr.map((a) => a);';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
expect(hasDecl(src, '@declaration.const', 'x')).toBe(true);
|
|
});
|
|
|
|
it('suppresses a function_expression callback', () => {
|
|
const src = 'const x = arr.map(function (a) { return a; });';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
expect(hasDecl(src, '@declaration.const', 'x')).toBe(true);
|
|
});
|
|
|
|
it('suppresses an optional-chained array call `arr?.map(...)`', () => {
|
|
const src = 'const x = arr?.map((a) => a);';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
});
|
|
|
|
it('suppresses a parenthesized callee `(arr.map)(cb)`', () => {
|
|
const src = 'const x = (arr.map)((a) => a);';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
});
|
|
|
|
it('suppresses a computed callee `arr["map"](cb)`', () => {
|
|
const src = 'const x = arr["map"]((a) => a);';
|
|
expect(hasDecl(src, '@declaration.function', 'x')).toBe(false);
|
|
});
|
|
|
|
it('suppresses export-default array-method wrappers', () => {
|
|
const src = 'export default arr.map((a) => a);';
|
|
expect(countTag(src, '@declaration.function')).toBe(0);
|
|
});
|
|
|
|
it('suppresses obvious built-in callback wrappers in export default', () => {
|
|
const src = 'export default setTimeout(() => work());';
|
|
expect(countTag(src, '@declaration.function')).toBe(0);
|
|
});
|
|
|
|
it('rewrites export-default HOC names to the file stem', () => {
|
|
const matches = emitJsScopeCaptures(
|
|
'export default React.memo((props) => props);',
|
|
'routes/health-check.jsx',
|
|
);
|
|
expect(
|
|
matches.some(
|
|
(m) =>
|
|
m['@declaration.function'] !== undefined &&
|
|
m['@declaration.name']?.text === 'health-check',
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|