Skip to content

Commit ba83d03

Browse files
test(graph): expand TS/JS extraction regression fixtures (#100)
Co-authored-by: Yashasvi Pandey <yashasvipandey2912@gmail.com>
1 parent ac1a187 commit ba83d03

5 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { readFileSync } from "node:fs";
2+
import { dirname, join } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { beforeAll, describe, expect, it } from "vitest";
5+
import { extractFile, loadGrammars } from "../extraction/index.js";
6+
import type { FileExtraction } from "../extraction/index.js";
7+
8+
const FIXTURES_DIR = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
9+
10+
describe("Graph Extraction Regression", () => {
11+
beforeAll(async () => {
12+
await loadGrammars(["typescript", "javascript", "tsx", "jsx"]);
13+
});
14+
15+
const extractFixture = (filename: string): FileExtraction => {
16+
const path = join(FIXTURES_DIR, filename);
17+
const source = readFileSync(path, "utf-8");
18+
const result = extractFile(`fixtures/${filename}`, source);
19+
expect(result).not.toBeNull();
20+
return result!;
21+
};
22+
23+
const node = (result: FileExtraction, kind: string, name: string) =>
24+
result.nodes.find((n) => n.kind === kind && n.name === name);
25+
const hasEdge = (result: FileExtraction, kind: string, targetName: string) =>
26+
result.edges.some((e) => e.kind === kind && e.targetName === targetName);
27+
const hasContainsEdge = (result: FileExtraction, sourceId: string, targetId: string) =>
28+
result.edges.some((e) => e.kind === "contains" && e.source === sourceId && e.target === targetId);
29+
30+
describe("TypeScript Edge Cases", () => {
31+
let result: FileExtraction;
32+
beforeAll(() => {
33+
result = extractFixture("typescript-edge-cases.ts");
34+
});
35+
36+
it("detects language correctly", () => {
37+
expect(result.language).toBe("typescript");
38+
});
39+
40+
it("extracts interfaces, types, and enums", () => {
41+
expect(node(result, "interface", "ProcessorOptions")).toBeDefined();
42+
expect(node(result, "type_alias", "Status")).toBeDefined();
43+
expect(node(result, "enum", "ErrorCode")).toBeDefined();
44+
expect(node(result, "enum_member", "Timeout")).toBeDefined();
45+
});
46+
47+
it("extracts classes with visibility modifiers and return types", () => {
48+
const cls = node(result, "class", "Processor");
49+
expect(cls).toBeDefined();
50+
51+
const options = node(result, "property", "options");
52+
expect(options).toBeDefined();
53+
expect(options!.visibility).toBe("protected");
54+
55+
const run = node(result, "method", "run");
56+
expect(run).toBeDefined();
57+
expect(run!.visibility).toBe("public");
58+
expect(run!.isAsync).toBe(true);
59+
expect(run!.returnType).toBe("Promise<void>");
60+
});
61+
62+
it("captures qualified names and containment", () => {
63+
const cls = node(result, "class", "Processor");
64+
const run = node(result, "method", "run");
65+
expect(run!.qualifiedName).toBe("Processor::run");
66+
expect(hasContainsEdge(result, cls!.id, run!.id)).toBe(true);
67+
});
68+
69+
it("captures imports and calls", () => {
70+
expect(hasEdge(result, "imports", "external-lib")).toBe(true);
71+
expect(hasEdge(result, "calls", "externalHelper")).toBe(true);
72+
});
73+
});
74+
75+
describe("JavaScript Edge Cases", () => {
76+
let result: FileExtraction;
77+
beforeAll(() => {
78+
result = extractFixture("javascript-edge-cases.js");
79+
});
80+
81+
it("detects language correctly", () => {
82+
expect(result.language).toBe("javascript");
83+
});
84+
85+
it("extracts classes, static methods, and calls", () => {
86+
expect(node(result, "class", "Manager")).toBeDefined();
87+
88+
const create = node(result, "method", "create");
89+
expect(create).toBeDefined();
90+
expect(create!.isStatic).toBe(true);
91+
92+
expect(hasEdge(result, "instantiates", "Manager")).toBe(true);
93+
expect(hasEdge(result, "calls", "api.save")).toBe(true);
94+
});
95+
96+
it("degrades gracefully on ambiguous syntax", () => {
97+
// The file should parse and extract the valid symbols despite any syntax errors
98+
expect(node(result, "function", "withWeirdSyntax")).toBeDefined();
99+
});
100+
});
101+
102+
describe("TSX Components", () => {
103+
let result: FileExtraction;
104+
beforeAll(() => {
105+
result = extractFixture("tsx-component.tsx");
106+
});
107+
108+
it("detects language correctly", () => {
109+
expect(result.language).toBe("tsx");
110+
});
111+
112+
it("extracts components, arrow functions, and calls", () => {
113+
expect(node(result, "interface", "Props")).toBeDefined();
114+
expect(node(result, "function", "Widget")).toBeDefined();
115+
116+
// Arrow functions inside functions are local variables and are not extracted as nodes
117+
// But the calls they make should attribute to the enclosing function
118+
expect(hasEdge(result, "calls", "setCount")).toBe(true);
119+
});
120+
121+
it("captures JSX component usage via imports", () => {
122+
expect(hasEdge(result, "imports", "react")).toBe(true);
123+
expect(hasEdge(result, "imports", "./Header")).toBe(true);
124+
});
125+
});
126+
127+
describe("JSX Components", () => {
128+
let result: FileExtraction;
129+
beforeAll(() => {
130+
result = extractFixture("jsx-component.jsx");
131+
});
132+
133+
it("detects language correctly", () => {
134+
expect(result.language).toBe("jsx");
135+
});
136+
137+
it("extracts functions and internal arrow functions", () => {
138+
const page = node(result, "function", "Page");
139+
expect(page).toBeDefined();
140+
141+
// Local arrow functions are not extracted, but their instantiations attribute to the parent
142+
expect(hasEdge(result, "instantiates", "Promise")).toBe(true);
143+
});
144+
145+
it("captures promise instantiations", () => {
146+
expect(hasEdge(result, "instantiates", "Promise")).toBe(true);
147+
});
148+
});
149+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { api } from "./api.js";
2+
3+
// IIFE to test containment and calls
4+
(function init() {
5+
api.setup();
6+
})();
7+
8+
export const utils = {
9+
// Method in object literal
10+
format() {
11+
return "fmt";
12+
}
13+
};
14+
15+
// Class with static method and private field
16+
export class Manager {
17+
#internalState = 0;
18+
19+
static create() {
20+
return new Manager();
21+
}
22+
23+
update() {
24+
this.#internalState++;
25+
api.save(this.#internalState);
26+
}
27+
}
28+
29+
// Ambiguous or unsupported syntax that degrades safely
30+
function withWeirdSyntax() {
31+
const x = ; // missing value
32+
return x;
33+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Section } from "./Section";
2+
3+
export function Page() {
4+
const loadData = () => {
5+
return new Promise((resolve) => resolve());
6+
};
7+
8+
return (
9+
<main>
10+
<Section onMount={loadData} />
11+
</main>
12+
);
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { useState } from "react";
2+
import { Header } from "./Header";
3+
4+
interface Props {
5+
title: string;
6+
}
7+
8+
export const Widget: React.FC<Props> = ({ title }) => {
9+
const [count, setCount] = useState(0);
10+
11+
const increment = () => setCount(count + 1);
12+
13+
return (
14+
<div className="widget">
15+
<Header title={title} />
16+
<button onClick={increment}>Count {count}</button>
17+
{/* Ambiguous JSX that safely degrades */}
18+
<div data-value={ = } />
19+
</div>
20+
);
21+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { externalHelper } from "external-lib";
2+
3+
export interface ProcessorOptions {
4+
timeout: number;
5+
}
6+
7+
export type Status = "idle" | "running";
8+
9+
export enum ErrorCode {
10+
Timeout = 1,
11+
Unknown = 2,
12+
}
13+
14+
export class Processor {
15+
private status: Status = "idle";
16+
protected options: ProcessorOptions;
17+
public id: string;
18+
19+
constructor(options: ProcessorOptions) {
20+
this.options = options;
21+
this.id = "proc";
22+
}
23+
24+
public async run(): Promise<void> {
25+
this.status = "running";
26+
const callback = () => {
27+
externalHelper(this.id);
28+
};
29+
callback();
30+
}
31+
}

0 commit comments

Comments
 (0)