nested-declarator-functions.test.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. /**
  2. * A function bound by a `const` inside another function is a symbol (#1669).
  3. *
  4. * `const handleClear = () => {…}` inside a component is how every React
  5. * handler that skips `useCallback` is written. At module scope the same
  6. * declaration already names a function; inside a body it was skipped, so the
  7. * handler was absent from callers / impact — "Symbol not found", which reads
  8. * exactly like "no callers" — and its calls attributed to the component.
  9. */
  10. import { describe, it, expect, beforeAll } from 'vitest';
  11. import { extractFromSource } from '../src/extraction';
  12. import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
  13. beforeAll(async () => {
  14. await initGrammars();
  15. await loadAllGrammars();
  16. });
  17. const refsFrom = (result: ReturnType<typeof extractFromSource>, id: string) =>
  18. result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => r.referenceName);
  19. describe('declarator-bound functions inside a body', () => {
  20. it('extracts const arrows and function expressions as functions of the enclosing one', () => {
  21. const code = `
  22. import { formatLabel, parseLabel } from './labels'
  23. export default function Widget({ items, onPick }) {
  24. const handleClear = () => {
  25. onPick(null, null)
  26. }
  27. const describe = function (item) {
  28. return formatLabel(item)
  29. }
  30. let later = (x) => parseLabel(x)
  31. const count = items.length
  32. const [a, b] = [() => 1, () => 2]
  33. return items.map((i) => <button onClick={handleClear} onDoubleClick={() => describe(i)}>{later(i)}</button>)
  34. }
  35. `;
  36. const result = extractFromSource('src/widget.jsx', code);
  37. const fns = result.nodes.filter((n) => n.kind === 'function');
  38. const names = fns.map((n) => n.name);
  39. expect(names).toEqual(expect.arrayContaining(['Widget', 'handleClear', 'describe', 'later']));
  40. // A value, a destructuring and an inline arrow stay out.
  41. expect(names).not.toContain('count');
  42. expect(names).not.toContain('a');
  43. expect(names.filter((n) => n === '<anonymous>')).toEqual([]);
  44. const widget = fns.find((n) => n.name === 'Widget')!;
  45. const handleClear = fns.find((n) => n.name === 'handleClear')!;
  46. const describeFn = fns.find((n) => n.name === 'describe')!;
  47. expect(handleClear.qualifiedName).toBe('Widget::handleClear');
  48. expect(handleClear.startLine).toBe(4);
  49. expect(describeFn.startLine).toBe(7);
  50. // The handler's calls are its own; the component keeps what it does itself.
  51. expect(refsFrom(result, handleClear.id)).toContain('onPick');
  52. expect(refsFrom(result, widget.id)).not.toContain('onPick');
  53. expect(refsFrom(result, describeFn.id)).toContain('formatLabel');
  54. expect(refsFrom(result, widget.id)).toContain('handleClear');
  55. // Containment: the component contains its handlers.
  56. const contains = result.edges.filter((e) => e.kind === 'contains' && e.source === widget.id).map((e) => e.target);
  57. expect(contains).toContain(handleClear.id);
  58. expect(contains).toContain(describeFn.id);
  59. });
  60. it('does not apply outside the JS family', () => {
  61. const code = `
  62. def outer():
  63. inner = lambda x: x + 1
  64. return inner(1)
  65. `;
  66. const result = extractFromSource('src/mod.py', code);
  67. expect(result.nodes.filter((n) => n.kind === 'function').map((n) => n.name)).toEqual(['outer']);
  68. });
  69. });