Skip to content

Commit b5be744

Browse files
committed
feat(scanner): extract arrow functions and function expressions (#119)
- Add 'variable' to DocumentType for variable-assigned functions - Add metadata: isArrowFunction, isHook, isAsync - Extract arrow functions assigned to const/let variables - Extract function expressions assigned to variables - Detect React hooks by 'use*' naming convention - Extract callees and JSDoc from arrow function bodies - Add 11 tests covering extraction patterns Closes #119
1 parent 2464f5e commit b5be744

File tree

4 files changed

+353
-1
lines changed

4 files changed

+353
-1
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Test fixtures for arrow function and function expression extraction.
3+
* This file contains various patterns that should be extracted by the scanner.
4+
*/
5+
6+
// Simple arrow function
7+
const simpleArrow = () => {
8+
return 'hello';
9+
};
10+
11+
// Arrow function with parameters
12+
const typedArrow = (name: string, age: number): string => {
13+
return `${name} is ${age} years old`;
14+
};
15+
16+
// Exported arrow function
17+
export const exportedArrow = (value: number) => value * 2;
18+
19+
// Non-exported (private) helper
20+
const privateHelper = (x: number) => x + 1;
21+
22+
// React-style hook (name starts with 'use')
23+
export const useCustomHook = (initialValue: string) => {
24+
const value = initialValue;
25+
const setValue = (newValue: string) => newValue;
26+
return { value, setValue };
27+
};
28+
29+
// Async arrow function
30+
export const fetchData = async (url: string) => {
31+
const response = await fetch(url);
32+
return response.json();
33+
};
34+
35+
// Function expression (legacy style)
36+
// biome-ignore lint/complexity/useArrowFunction: Testing function expression extraction
37+
const legacyFunction = function (a: number, b: number) {
38+
return a + b;
39+
};
40+
41+
// Arrow function that calls other functions (for callee extraction)
42+
const composedFunction = (input: string) => {
43+
const trimmed = input.trim();
44+
const upper = trimmed.toUpperCase();
45+
return privateHelper(upper.length);
46+
};
47+
48+
/**
49+
* A well-documented arrow function.
50+
* This should have its JSDoc extracted.
51+
*/
52+
const documentedArrow = (param: string) => {
53+
return param.toLowerCase();
54+
};
55+
56+
// These should NOT be extracted (not function-valued):
57+
// biome-ignore lint/correctness/noUnusedVariables: Test fixtures for non-extraction
58+
59+
// Plain constant (primitive)
60+
const plainConstant = 42;
61+
62+
// Object constant
63+
const configObject = {
64+
apiUrl: '/api',
65+
timeout: 5000,
66+
};
67+
68+
// Array constant
69+
const colorList = ['red', 'green', 'blue'];
70+
71+
// Suppress unused warnings - these are test fixtures
72+
void plainConstant;
73+
void configObject;
74+
void colorList;
75+
76+
// String constant
77+
export const API_ENDPOINT = 'https://api.example.com';
78+
79+
// Re-exported for testing
80+
export { simpleArrow, typedArrow, composedFunction, documentedArrow, legacyFunction };

packages/core/src/scanner/__tests__/scanner.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,170 @@ describe('Scanner', () => {
707707
expect(calleeNames.some((n) => n.includes('register'))).toBe(true);
708708
});
709709
});
710+
711+
describe('Arrow Function Extraction', () => {
712+
// Note: We override exclude to allow fixtures directory (excluded by default)
713+
const fixtureExcludes = ['**/node_modules/**', '**/dist/**'];
714+
715+
it('should extract arrow functions assigned to variables', async () => {
716+
const result = await scanRepository({
717+
repoRoot,
718+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
719+
exclude: fixtureExcludes,
720+
});
721+
722+
// Should find arrow function variables
723+
const variables = result.documents.filter((d) => d.type === 'variable');
724+
expect(variables.length).toBeGreaterThan(0);
725+
});
726+
727+
it('should mark arrow functions with isArrowFunction metadata', async () => {
728+
const result = await scanRepository({
729+
repoRoot,
730+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
731+
exclude: fixtureExcludes,
732+
});
733+
734+
const arrowFn = result.documents.find(
735+
(d) => d.type === 'variable' && d.metadata.name === 'simpleArrow'
736+
);
737+
expect(arrowFn).toBeDefined();
738+
expect(arrowFn?.metadata.isArrowFunction).toBe(true);
739+
});
740+
741+
it('should detect React hooks by naming convention', async () => {
742+
const result = await scanRepository({
743+
repoRoot,
744+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
745+
exclude: fixtureExcludes,
746+
});
747+
748+
const hook = result.documents.find(
749+
(d) => d.type === 'variable' && d.metadata.name === 'useCustomHook'
750+
);
751+
expect(hook).toBeDefined();
752+
expect(hook?.metadata.isHook).toBe(true);
753+
expect(hook?.metadata.isArrowFunction).toBe(true);
754+
});
755+
756+
it('should detect async arrow functions', async () => {
757+
const result = await scanRepository({
758+
repoRoot,
759+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
760+
exclude: fixtureExcludes,
761+
});
762+
763+
const asyncFn = result.documents.find(
764+
(d) => d.type === 'variable' && d.metadata.name === 'fetchData'
765+
);
766+
expect(asyncFn).toBeDefined();
767+
expect(asyncFn?.metadata.isAsync).toBe(true);
768+
});
769+
770+
it('should extract exported arrow functions', async () => {
771+
const result = await scanRepository({
772+
repoRoot,
773+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
774+
exclude: fixtureExcludes,
775+
});
776+
777+
const exportedFn = result.documents.find(
778+
(d) => d.type === 'variable' && d.metadata.name === 'exportedArrow'
779+
);
780+
expect(exportedFn).toBeDefined();
781+
expect(exportedFn?.metadata.exported).toBe(true);
782+
});
783+
784+
it('should extract non-exported arrow functions', async () => {
785+
const result = await scanRepository({
786+
repoRoot,
787+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
788+
exclude: fixtureExcludes,
789+
});
790+
791+
const privateFn = result.documents.find(
792+
(d) => d.type === 'variable' && d.metadata.name === 'privateHelper'
793+
);
794+
expect(privateFn).toBeDefined();
795+
expect(privateFn?.metadata.exported).toBe(false);
796+
});
797+
798+
it('should extract function expressions assigned to variables', async () => {
799+
const result = await scanRepository({
800+
repoRoot,
801+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
802+
exclude: fixtureExcludes,
803+
});
804+
805+
const funcExpr = result.documents.find(
806+
(d) => d.type === 'variable' && d.metadata.name === 'legacyFunction'
807+
);
808+
expect(funcExpr).toBeDefined();
809+
expect(funcExpr?.metadata.isArrowFunction).toBe(false);
810+
});
811+
812+
it('should include signature for arrow functions', async () => {
813+
const result = await scanRepository({
814+
repoRoot,
815+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
816+
exclude: fixtureExcludes,
817+
});
818+
819+
const fn = result.documents.find(
820+
(d) => d.type === 'variable' && d.metadata.name === 'typedArrow'
821+
);
822+
expect(fn).toBeDefined();
823+
expect(fn?.metadata.signature).toContain('typedArrow');
824+
expect(fn?.metadata.signature).toContain('=>');
825+
});
826+
827+
it('should extract callees from arrow functions', async () => {
828+
const result = await scanRepository({
829+
repoRoot,
830+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
831+
exclude: fixtureExcludes,
832+
});
833+
834+
const fn = result.documents.find(
835+
(d) => d.type === 'variable' && d.metadata.name === 'composedFunction'
836+
);
837+
expect(fn).toBeDefined();
838+
expect(fn?.metadata.callees).toBeDefined();
839+
expect(fn?.metadata.callees?.length).toBeGreaterThan(0);
840+
});
841+
842+
it('should extract JSDoc from arrow functions', async () => {
843+
const result = await scanRepository({
844+
repoRoot,
845+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
846+
exclude: fixtureExcludes,
847+
});
848+
849+
const fn = result.documents.find(
850+
(d) => d.type === 'variable' && d.metadata.name === 'documentedArrow'
851+
);
852+
expect(fn).toBeDefined();
853+
expect(fn?.metadata.docstring).toBeDefined();
854+
expect(fn?.metadata.docstring).toContain('documented');
855+
});
856+
857+
it('should not extract variables without function initializers', async () => {
858+
const result = await scanRepository({
859+
repoRoot,
860+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
861+
exclude: fixtureExcludes,
862+
});
863+
864+
// Should NOT find plain constants
865+
const constant = result.documents.find(
866+
(d) => d.type === 'variable' && d.metadata.name === 'plainConstant'
867+
);
868+
expect(constant).toBeUndefined();
869+
870+
const objectConst = result.documents.find(
871+
(d) => d.type === 'variable' && d.metadata.name === 'configObject'
872+
);
873+
expect(objectConst).toBeUndefined();
874+
});
875+
});
710876
});

packages/core/src/scanner/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export type DocumentType =
77
| 'type'
88
| 'struct'
99
| 'method'
10-
| 'documentation';
10+
| 'documentation'
11+
| 'variable';
1112

1213
/**
1314
* Information about a function/method that calls this component
@@ -57,6 +58,11 @@ export interface DocumentMetadata {
5758
callees?: CalleeInfo[]; // Functions/methods this component calls
5859
// Note: callers are computed at query time via reverse lookup
5960

61+
// Variable/function metadata
62+
isArrowFunction?: boolean; // True if variable initialized with arrow function
63+
isHook?: boolean; // True if name starts with 'use' (React convention)
64+
isAsync?: boolean; // True if async function/arrow function
65+
6066
// Extensible for future use
6167
custom?: Record<string, unknown>;
6268
}

0 commit comments

Comments
 (0)