Skip to content

Commit 88a7962

Browse files
committed
feat(scanner): extract exported constants with object/array/call initializers
- Extract exported const with object literal initializers (config objects) - Extract exported const with array literal initializers (static lists) - Extract exported const with call expression initializers (factories) - Add metadata: isConstant, constantKind ('object' | 'array' | 'value') - Skip non-exported constants and primitive exports (low semantic value) - Add 6 tests for exported constant extraction patterns This completes Phase 2 of #119.
1 parent b5be744 commit 88a7962

File tree

4 files changed

+223
-8
lines changed

4 files changed

+223
-8
lines changed

packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,56 @@ const documentedArrow = (param: string) => {
5353
return param.toLowerCase();
5454
};
5555

56-
// These should NOT be extracted (not function-valued):
56+
// ============================================
57+
// EXPORTED CONSTANTS - Should be extracted
58+
// ============================================
59+
60+
/**
61+
* API configuration object.
62+
* This should be extracted as an exported constant.
63+
*/
64+
export const API_CONFIG = {
65+
baseUrl: '/api',
66+
timeout: 5000,
67+
retries: 3,
68+
};
69+
70+
// Exported array constant
71+
export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'go'];
72+
73+
// Exported call expression (factory pattern)
74+
// biome-ignore lint/suspicious/noEmptyBlockStatements: Test fixture
75+
export const AppContext = (() => ({ value: null }))();
76+
77+
// Typed exported constant
78+
export const THEME_CONFIG: { dark: boolean; primary: string } = {
79+
dark: false,
80+
primary: '#007bff',
81+
};
82+
83+
// ============================================
84+
// NON-EXPORTED - Should NOT be extracted
85+
// ============================================
5786
// biome-ignore lint/correctness/noUnusedVariables: Test fixtures for non-extraction
5887

59-
// Plain constant (primitive)
88+
// Plain constant (primitive) - never extracted
6089
const plainConstant = 42;
6190

62-
// Object constant
91+
// Non-exported object - not extracted (only exported objects are extracted)
6392
const configObject = {
6493
apiUrl: '/api',
6594
timeout: 5000,
6695
};
6796

68-
// Array constant
97+
// Non-exported array - not extracted
6998
const colorList = ['red', 'green', 'blue'];
7099

71100
// Suppress unused warnings - these are test fixtures
72101
void plainConstant;
73102
void configObject;
74103
void colorList;
75104

76-
// String constant
105+
// Exported primitive - NOT extracted (primitives have low semantic value)
77106
export const API_ENDPOINT = 'https://api.example.com';
78107

79108
// Re-exported for testing

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

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,23 +854,127 @@ describe('Scanner', () => {
854854
expect(fn?.metadata.docstring).toContain('documented');
855855
});
856856

857-
it('should not extract variables without function initializers', async () => {
857+
it('should not extract non-exported variables without function initializers', async () => {
858858
const result = await scanRepository({
859859
repoRoot,
860860
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
861861
exclude: fixtureExcludes,
862862
});
863863

864-
// Should NOT find plain constants
864+
// Should NOT find non-exported plain constants
865865
const constant = result.documents.find(
866866
(d) => d.type === 'variable' && d.metadata.name === 'plainConstant'
867867
);
868868
expect(constant).toBeUndefined();
869869

870+
// Should NOT find non-exported object constants
870871
const objectConst = result.documents.find(
871872
(d) => d.type === 'variable' && d.metadata.name === 'configObject'
872873
);
873874
expect(objectConst).toBeUndefined();
875+
876+
// Should NOT find exported primitive constants (low semantic value)
877+
const primitiveExport = result.documents.find(
878+
(d) => d.type === 'variable' && d.metadata.name === 'API_ENDPOINT'
879+
);
880+
expect(primitiveExport).toBeUndefined();
881+
});
882+
});
883+
884+
describe('Exported Constant Extraction', () => {
885+
// Note: We override exclude to allow fixtures directory (excluded by default)
886+
const fixtureExcludes = ['**/node_modules/**', '**/dist/**'];
887+
888+
it('should extract exported object constants', async () => {
889+
const result = await scanRepository({
890+
repoRoot,
891+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
892+
exclude: fixtureExcludes,
893+
});
894+
895+
const config = result.documents.find(
896+
(d) => d.type === 'variable' && d.metadata.name === 'API_CONFIG'
897+
);
898+
expect(config).toBeDefined();
899+
expect(config?.metadata.exported).toBe(true);
900+
expect(config?.metadata.isConstant).toBe(true);
901+
expect(config?.metadata.constantKind).toBe('object');
902+
});
903+
904+
it('should extract exported array constants', async () => {
905+
const result = await scanRepository({
906+
repoRoot,
907+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
908+
exclude: fixtureExcludes,
909+
});
910+
911+
const languages = result.documents.find(
912+
(d) => d.type === 'variable' && d.metadata.name === 'SUPPORTED_LANGUAGES'
913+
);
914+
expect(languages).toBeDefined();
915+
expect(languages?.metadata.exported).toBe(true);
916+
expect(languages?.metadata.isConstant).toBe(true);
917+
expect(languages?.metadata.constantKind).toBe('array');
918+
});
919+
920+
it('should extract exported call expression constants (factories)', async () => {
921+
const result = await scanRepository({
922+
repoRoot,
923+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
924+
exclude: fixtureExcludes,
925+
});
926+
927+
const context = result.documents.find(
928+
(d) => d.type === 'variable' && d.metadata.name === 'AppContext'
929+
);
930+
expect(context).toBeDefined();
931+
expect(context?.metadata.exported).toBe(true);
932+
expect(context?.metadata.isConstant).toBe(true);
933+
expect(context?.metadata.constantKind).toBe('value');
934+
});
935+
936+
it('should extract typed exported constants with signature', async () => {
937+
const result = await scanRepository({
938+
repoRoot,
939+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
940+
exclude: fixtureExcludes,
941+
});
942+
943+
const theme = result.documents.find(
944+
(d) => d.type === 'variable' && d.metadata.name === 'THEME_CONFIG'
945+
);
946+
expect(theme).toBeDefined();
947+
expect(theme?.metadata.signature).toContain('THEME_CONFIG');
948+
expect(theme?.metadata.signature).toContain('dark');
949+
});
950+
951+
it('should extract JSDoc from exported constants', async () => {
952+
const result = await scanRepository({
953+
repoRoot,
954+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
955+
exclude: fixtureExcludes,
956+
});
957+
958+
const config = result.documents.find(
959+
(d) => d.type === 'variable' && d.metadata.name === 'API_CONFIG'
960+
);
961+
expect(config).toBeDefined();
962+
expect(config?.metadata.docstring).toBeDefined();
963+
expect(config?.metadata.docstring).toContain('API configuration');
964+
});
965+
966+
it('should not extract non-exported object constants', async () => {
967+
const result = await scanRepository({
968+
repoRoot,
969+
include: ['packages/core/src/scanner/__tests__/fixtures/arrow-functions.ts'],
970+
exclude: fixtureExcludes,
971+
});
972+
973+
// configObject is not exported, should not be extracted
974+
const config = result.documents.find(
975+
(d) => d.type === 'variable' && d.metadata.name === 'configObject'
976+
);
977+
expect(config).toBeUndefined();
874978
});
875979
});
876980
});

packages/core/src/scanner/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface DocumentMetadata {
6262
isArrowFunction?: boolean; // True if variable initialized with arrow function
6363
isHook?: boolean; // True if name starts with 'use' (React convention)
6464
isAsync?: boolean; // True if async function/arrow function
65+
isConstant?: boolean; // True if exported constant (object/array/call expression)
66+
constantKind?: 'object' | 'array' | 'value'; // Kind of constant initializer
6567

6668
// Extensible for future use
6769
custom?: Record<string, unknown>;

packages/core/src/scanner/typescript.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,15 @@ export class TypeScriptScanner implements Scanner {
119119
if (doc) documents.push(doc);
120120
}
121121

122-
// Extract variables with arrow functions or function expressions
122+
// Extract variables with arrow functions, function expressions, or exported constants
123123
for (const varStmt of sourceFile.getVariableStatements()) {
124124
for (const decl of varStmt.getDeclarations()) {
125125
const initializer = decl.getInitializer();
126126
if (!initializer) continue;
127127

128128
const kind = initializer.getKind();
129+
130+
// Arrow functions and function expressions (any export status)
129131
if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) {
130132
const doc = this.extractVariableWithFunction(
131133
decl,
@@ -136,6 +138,16 @@ export class TypeScriptScanner implements Scanner {
136138
);
137139
if (doc) documents.push(doc);
138140
}
141+
// Exported constants with object/array/call expression initializers
142+
else if (
143+
varStmt.isExported() &&
144+
(kind === SyntaxKind.ObjectLiteralExpression ||
145+
kind === SyntaxKind.ArrayLiteralExpression ||
146+
kind === SyntaxKind.CallExpression)
147+
) {
148+
const doc = this.extractExportedConstant(decl, varStmt, relativeFile, imports);
149+
if (doc) documents.push(doc);
150+
}
139151
}
140152
}
141153

@@ -471,6 +483,74 @@ export class TypeScriptScanner implements Scanner {
471483
};
472484
}
473485

486+
/**
487+
* Extract an exported constant with object literal, array literal, or call expression initializer.
488+
* Captures configuration objects, contexts, and factory-created values.
489+
*/
490+
private extractExportedConstant(
491+
decl: VariableDeclaration,
492+
varStmt: VariableStatement,
493+
file: string,
494+
imports: string[]
495+
): Document | null {
496+
const name = decl.getName();
497+
if (!name) return null;
498+
499+
const initializer = decl.getInitializer();
500+
if (!initializer) return null;
501+
502+
const startLine = decl.getStartLineNumber();
503+
const endLine = decl.getEndLineNumber();
504+
const fullText = decl.getText();
505+
const docComment = this.getDocComment(varStmt);
506+
const snippet = this.truncateSnippet(fullText);
507+
508+
// Determine the kind of constant for better embedding text
509+
const kind = initializer.getKind();
510+
let constantKind: 'object' | 'array' | 'value';
511+
if (kind === SyntaxKind.ObjectLiteralExpression) {
512+
constantKind = 'object';
513+
} else if (kind === SyntaxKind.ArrayLiteralExpression) {
514+
constantKind = 'array';
515+
} else {
516+
constantKind = 'value'; // Call expression or other
517+
}
518+
519+
// Build signature
520+
const typeAnnotation = decl.getTypeNode()?.getText();
521+
const signature = typeAnnotation
522+
? `export const ${name}: ${typeAnnotation}`
523+
: `export const ${name}`;
524+
525+
const text = this.buildEmbeddingText({
526+
type: 'constant',
527+
name,
528+
signature,
529+
docComment,
530+
language: 'typescript',
531+
});
532+
533+
return {
534+
id: `${file}:${name}:${startLine}`,
535+
text,
536+
type: 'variable',
537+
language: 'typescript',
538+
metadata: {
539+
file,
540+
startLine,
541+
endLine,
542+
name,
543+
signature,
544+
exported: true, // Always true for this method
545+
docstring: docComment,
546+
snippet,
547+
imports,
548+
isConstant: true,
549+
constantKind,
550+
},
551+
};
552+
}
553+
474554
private getDocComment(node: Node): string | undefined {
475555
// ts-morph doesn't export getJsDocs on base Node type, but it exists on declarations
476556
const nodeWithJsDocs = node as unknown as {

0 commit comments

Comments
 (0)