Add type alias and exported variable extraction

Extend extraction to index two additional categories of symbols
that were previously invisible:

1. Type aliases (e.g. `export type X = ...` in TypeScript,
   `type X` in Go, `type X = ...` in Rust, `typealias X` in Swift,
   `type_alias` in Kotlin). Adds `typeAliasTypes` to the
   LanguageExtractor interface with values for all 13 languages.

2. Exported variable declarations that aren't functions, including:
   - Zustand stores: `export const useX = create(...)`
   - XState machines: `export const xMachine = createMachine(...)`
   - Zod schemas: `export const schema = z.object(...)`
   - Config objects: `export const config = { ... }`
   - Constants: `export const MAX = 3`
   - Arrays: `export const NAMES = [...] as const`

   The extractExportedVariables() method is called when visiting
   export_statement nodes. It skips variable_declarator values that
   are already handled by functionTypes (arrow_function,
   function_expression) to avoid duplicate extraction.

Adds 11 new test cases (59 total extraction tests, 215 total).

Tested on production monorepo: nodes increased from 958 to 1,172
(+22%), with 109 new variable nodes and 105 new type_alias nodes.
Only 4 files remain at 0 nodes — all are re-export barrels or
ambient declaration files with no extractable symbols.
This commit is contained in:
Tanner Balluff
2026-02-09 14:48:09 -05:00
parent b6d5346c3c
commit ea9d0bf549
3 changed files with 267 additions and 2 deletions
+168
View File
@@ -290,6 +290,174 @@ export const fetchData = async () => {
});
});
describe('Type Alias Extraction', () => {
it('should extract exported type aliases in TypeScript', () => {
const code = `
export type AuthContextValue = {
user: User | null;
login: () => void;
logout: () => void;
};
`;
const result = extractFromSource('types.ts', code);
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0]).toMatchObject({
kind: 'type_alias',
name: 'AuthContextValue',
isExported: true,
});
});
it('should extract non-exported type aliases', () => {
const code = `
type InternalState = {
loading: boolean;
error: string | null;
};
`;
const result = extractFromSource('internal.ts', code);
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0]).toMatchObject({
kind: 'type_alias',
name: 'InternalState',
isExported: false,
});
});
it('should extract multiple type aliases from the same file', () => {
const code = `
export type UnitSystem = 'metric' | 'imperial';
export type DateFormat = 'ISO' | 'US' | 'EU';
type Internal = string;
`;
const result = extractFromSource('config.ts', code);
const typeAliases = result.nodes.filter((n) => n.kind === 'type_alias');
expect(typeAliases).toHaveLength(3);
const exported = typeAliases.filter((n) => n.isExported);
expect(exported).toHaveLength(2);
expect(exported.map((n) => n.name).sort()).toEqual(['DateFormat', 'UnitSystem']);
});
});
describe('Exported Variable Extraction', () => {
it('should extract exported const with call expression (Zustand store)', () => {
const code = `
export const useUIStore = create<UIState>((set) => ({
isOpen: false,
toggle: () => set((s) => ({ isOpen: !s.isOpen })),
}));
`;
const result = extractFromSource('store.ts', code);
const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'useUIStore');
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});
it('should extract exported const with object literal', () => {
const code = `
export const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
`;
const result = extractFromSource('config.ts', code);
const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'config');
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});
it('should extract exported const with array literal', () => {
const code = `
export const SCREEN_NAMES = ['home', 'settings', 'profile'] as const;
`;
const result = extractFromSource('constants.ts', code);
const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'SCREEN_NAMES');
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});
it('should extract exported const with primitive value', () => {
const code = `
export const MAX_RETRIES = 3;
export const API_VERSION = "v2";
`;
const result = extractFromSource('constants.ts', code);
const variables = result.nodes.filter((n) => n.kind === 'variable');
expect(variables).toHaveLength(2);
expect(variables.map((n) => n.name).sort()).toEqual(['API_VERSION', 'MAX_RETRIES']);
});
it('should NOT duplicate arrow functions as both function and variable', () => {
const code = `
export const useAuth = () => {
return useContext(AuthContext);
};
`;
const result = extractFromSource('hooks.ts', code);
// Should be extracted as function (from arrow function handler), NOT as variable
const funcNodes = result.nodes.filter((n) => n.kind === 'function' && n.name === 'useAuth');
const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'useAuth');
expect(funcNodes).toHaveLength(1);
expect(varNodes).toHaveLength(0);
});
it('should not extract non-exported const as exported variable', () => {
const code = `
const internalConfig = {
debug: true,
};
`;
const result = extractFromSource('internal.ts', code);
// Non-exported const should NOT create a variable node
// (only export_statement triggers extractExportedVariables)
const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'internalConfig');
expect(varNodes).toHaveLength(0);
});
it('should extract Zod schema exports', () => {
const code = `
export const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
`;
const result = extractFromSource('schemas.ts', code);
const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'userSchema');
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});
it('should extract XState machine exports', () => {
const code = `
export const authMachine = createMachine({
id: "auth",
initial: "idle",
states: {
idle: {},
authenticated: {},
},
});
`;
const result = extractFromSource('machine.ts', code);
const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'authMachine');
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});
});
describe('Python Extraction', () => {
it('should extract function definitions', () => {
const code = `