mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Add file editing capabilities: - EditLinesTool: replace lines with hash conflict detection - CreateFileTool: create files with directory auto-creation - DeleteFileTool: delete files from filesystem and storage Total: 664 tests, 97.77% coverage
649 lines
24 KiB
TypeScript
649 lines
24 KiB
TypeScript
import { describe, it, expect, beforeAll } from "vitest"
|
|
import { IndexBuilder } from "../../../../src/infrastructure/indexer/IndexBuilder.js"
|
|
import { ASTParser } from "../../../../src/infrastructure/indexer/ASTParser.js"
|
|
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
|
import { createEmptyFileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
|
|
|
describe("IndexBuilder", () => {
|
|
let builder: IndexBuilder
|
|
let parser: ASTParser
|
|
const projectRoot = "/project"
|
|
|
|
beforeAll(() => {
|
|
builder = new IndexBuilder(projectRoot)
|
|
parser = new ASTParser()
|
|
})
|
|
|
|
describe("buildSymbolIndex", () => {
|
|
it("should index function declarations", () => {
|
|
const code = `
|
|
export function greet(name: string): string {
|
|
return \`Hello, \${name}!\`
|
|
}
|
|
|
|
function privateHelper(): void {}
|
|
`
|
|
const ast = parser.parse(code, "ts")
|
|
const asts = new Map<string, FileAST>([["/project/src/utils.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("greet")).toBe(true)
|
|
expect(index.has("privateHelper")).toBe(true)
|
|
expect(index.get("greet")).toEqual([
|
|
{ path: "/project/src/utils.ts", line: 2, type: "function" },
|
|
])
|
|
})
|
|
|
|
it("should index class declarations and methods", () => {
|
|
const code = `
|
|
export class UserService {
|
|
async findById(id: string): Promise<User> {
|
|
return this.db.find(id)
|
|
}
|
|
|
|
private validate(data: unknown): void {}
|
|
}
|
|
`
|
|
const ast = parser.parse(code, "ts")
|
|
const asts = new Map<string, FileAST>([["/project/src/UserService.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("UserService")).toBe(true)
|
|
expect(index.get("UserService")).toEqual([
|
|
{ path: "/project/src/UserService.ts", line: 2, type: "class" },
|
|
])
|
|
|
|
expect(index.has("UserService.findById")).toBe(true)
|
|
expect(index.has("UserService.validate")).toBe(true)
|
|
})
|
|
|
|
it("should index interface declarations", () => {
|
|
const code = `
|
|
export interface User {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
interface InternalConfig {
|
|
debug: boolean
|
|
}
|
|
`
|
|
const ast = parser.parse(code, "ts")
|
|
const asts = new Map<string, FileAST>([["/project/src/types.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("User")).toBe(true)
|
|
expect(index.has("InternalConfig")).toBe(true)
|
|
expect(index.get("User")).toEqual([
|
|
{ path: "/project/src/types.ts", line: 2, type: "interface" },
|
|
])
|
|
})
|
|
|
|
it("should index type alias declarations", () => {
|
|
const code = `
|
|
export type UserId = string
|
|
type Handler = (event: Event) => void
|
|
`
|
|
const ast = parser.parse(code, "ts")
|
|
const asts = new Map<string, FileAST>([["/project/src/types.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("UserId")).toBe(true)
|
|
expect(index.has("Handler")).toBe(true)
|
|
expect(index.get("UserId")).toEqual([
|
|
{ path: "/project/src/types.ts", line: 2, type: "type" },
|
|
])
|
|
})
|
|
|
|
it("should index exported variables", () => {
|
|
const code = `
|
|
export const API_URL = "https://api.example.com"
|
|
export const DEFAULT_TIMEOUT = 5000
|
|
`
|
|
const ast = parser.parse(code, "ts")
|
|
const asts = new Map<string, FileAST>([["/project/src/config.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("API_URL")).toBe(true)
|
|
expect(index.has("DEFAULT_TIMEOUT")).toBe(true)
|
|
})
|
|
|
|
it("should handle multiple files", () => {
|
|
const userCode = `export class User { name: string }`
|
|
const orderCode = `export class Order { id: string }`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/User.ts", parser.parse(userCode, "ts")],
|
|
["/project/src/Order.ts", parser.parse(orderCode, "ts")],
|
|
])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("User")).toBe(true)
|
|
expect(index.has("Order")).toBe(true)
|
|
expect(index.get("User")?.[0].path).toBe("/project/src/User.ts")
|
|
expect(index.get("Order")?.[0].path).toBe("/project/src/Order.ts")
|
|
})
|
|
|
|
it("should handle duplicate symbol names across files", () => {
|
|
const file1 = `export function helper(): void {}`
|
|
const file2 = `export function helper(): void {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/a/utils.ts", parser.parse(file1, "ts")],
|
|
["/project/src/b/utils.ts", parser.parse(file2, "ts")],
|
|
])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
expect(index.has("helper")).toBe(true)
|
|
expect(index.get("helper")).toHaveLength(2)
|
|
})
|
|
|
|
it("should return empty index for empty ASTs", () => {
|
|
const asts = new Map<string, FileAST>()
|
|
const index = builder.buildSymbolIndex(asts)
|
|
expect(index.size).toBe(0)
|
|
})
|
|
|
|
it("should not index empty names", () => {
|
|
const ast = createEmptyFileAST()
|
|
ast.functions.push({
|
|
name: "",
|
|
lineStart: 1,
|
|
lineEnd: 3,
|
|
params: [],
|
|
isAsync: false,
|
|
isExported: false,
|
|
})
|
|
const asts = new Map<string, FileAST>([["/project/src/test.ts", ast]])
|
|
|
|
const index = builder.buildSymbolIndex(asts)
|
|
expect(index.has("")).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("buildDepsGraph", () => {
|
|
it("should build import relationships", () => {
|
|
const indexCode = `
|
|
import { helper } from "./utils"
|
|
export function main() { return helper() }
|
|
`
|
|
const utilsCode = `export function helper() { return 42 }`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(indexCode, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.imports.get("/project/src/index.ts")).toContain("/project/src/utils.ts")
|
|
expect(graph.imports.get("/project/src/utils.ts")).toEqual([])
|
|
})
|
|
|
|
it("should build reverse import relationships", () => {
|
|
const indexCode = `import { helper } from "./utils"`
|
|
const utilsCode = `export function helper() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(indexCode, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.importedBy.get("/project/src/utils.ts")).toContain("/project/src/index.ts")
|
|
expect(graph.importedBy.get("/project/src/index.ts")).toEqual([])
|
|
})
|
|
|
|
it("should handle multiple imports from same file", () => {
|
|
const code = `
|
|
import { a } from "./utils"
|
|
import { b } from "./utils"
|
|
`
|
|
const utilsCode = `export const a = 1; export const b = 2;`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(code, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const imports = graph.imports.get("/project/src/index.ts") ?? []
|
|
expect(imports.filter((i) => i === "/project/src/utils.ts")).toHaveLength(1)
|
|
})
|
|
|
|
it("should ignore external imports", () => {
|
|
const code = `
|
|
import React from "react"
|
|
import { helper } from "./utils"
|
|
`
|
|
const utilsCode = `export function helper() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(code, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const imports = graph.imports.get("/project/src/index.ts") ?? []
|
|
expect(imports).not.toContain("react")
|
|
expect(imports).toContain("/project/src/utils.ts")
|
|
})
|
|
|
|
it("should ignore builtin imports", () => {
|
|
const code = `
|
|
import * as fs from "node:fs"
|
|
import { helper } from "./utils"
|
|
`
|
|
const utilsCode = `export function helper() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(code, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const imports = graph.imports.get("/project/src/index.ts") ?? []
|
|
expect(imports).not.toContain("node:fs")
|
|
})
|
|
|
|
it("should handle index.ts imports", () => {
|
|
const code = `import { util } from "./utils"`
|
|
const indexCode = `export function util() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/main.ts", parser.parse(code, "ts")],
|
|
["/project/src/utils/index.ts", parser.parse(indexCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.imports.get("/project/src/main.ts")).toContain(
|
|
"/project/src/utils/index.ts",
|
|
)
|
|
})
|
|
|
|
it("should handle .js extension imports", () => {
|
|
const code = `import { helper } from "./utils.js"`
|
|
const utilsCode = `export function helper() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(code, "ts")],
|
|
["/project/src/utils.ts", parser.parse(utilsCode, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.imports.get("/project/src/index.ts")).toContain("/project/src/utils.ts")
|
|
})
|
|
|
|
it("should sort dependencies", () => {
|
|
const code = `
|
|
import { c } from "./c"
|
|
import { a } from "./a"
|
|
import { b } from "./b"
|
|
`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(code, "ts")],
|
|
["/project/src/a.ts", parser.parse("export const a = 1", "ts")],
|
|
["/project/src/b.ts", parser.parse("export const b = 2", "ts")],
|
|
["/project/src/c.ts", parser.parse("export const c = 3", "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.imports.get("/project/src/index.ts")).toEqual([
|
|
"/project/src/a.ts",
|
|
"/project/src/b.ts",
|
|
"/project/src/c.ts",
|
|
])
|
|
})
|
|
|
|
it("should return empty graph for empty ASTs", () => {
|
|
const asts = new Map<string, FileAST>()
|
|
const graph = builder.buildDepsGraph(asts)
|
|
expect(graph.imports.size).toBe(0)
|
|
expect(graph.importedBy.size).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("findSymbol", () => {
|
|
it("should find existing symbol", () => {
|
|
const code = `export function greet(): void {}`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/utils.ts", parser.parse(code, "ts")],
|
|
])
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
const locations = builder.findSymbol(index, "greet")
|
|
expect(locations).toHaveLength(1)
|
|
expect(locations[0].path).toBe("/project/src/utils.ts")
|
|
})
|
|
|
|
it("should return empty array for non-existent symbol", () => {
|
|
const asts = new Map<string, FileAST>()
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
const locations = builder.findSymbol(index, "nonexistent")
|
|
expect(locations).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe("searchSymbols", () => {
|
|
it("should find symbols matching pattern", () => {
|
|
const code = `
|
|
export function getUserById(): void {}
|
|
export function getUserByEmail(): void {}
|
|
export function createOrder(): void {}
|
|
`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/api.ts", parser.parse(code, "ts")],
|
|
])
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
const results = builder.searchSymbols(index, "getUser")
|
|
expect(results.size).toBe(2)
|
|
expect(results.has("getUserById")).toBe(true)
|
|
expect(results.has("getUserByEmail")).toBe(true)
|
|
})
|
|
|
|
it("should be case insensitive", () => {
|
|
const code = `export function MyFunction(): void {}`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/test.ts", parser.parse(code, "ts")],
|
|
])
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
const results = builder.searchSymbols(index, "myfunction")
|
|
expect(results.has("MyFunction")).toBe(true)
|
|
})
|
|
|
|
it("should return empty map for no matches", () => {
|
|
const code = `export function test(): void {}`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/test.ts", parser.parse(code, "ts")],
|
|
])
|
|
const index = builder.buildSymbolIndex(asts)
|
|
|
|
const results = builder.searchSymbols(index, "xyz123")
|
|
expect(results.size).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("getDependencies", () => {
|
|
it("should return file dependencies", () => {
|
|
const indexCode = `import { a } from "./a"`
|
|
const aCode = `export const a = 1`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(indexCode, "ts")],
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const deps = builder.getDependencies(graph, "/project/src/index.ts")
|
|
expect(deps).toContain("/project/src/a.ts")
|
|
})
|
|
|
|
it("should return empty array for file not in graph", () => {
|
|
const asts = new Map<string, FileAST>()
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const deps = builder.getDependencies(graph, "/nonexistent.ts")
|
|
expect(deps).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe("getDependents", () => {
|
|
it("should return file dependents", () => {
|
|
const indexCode = `import { a } from "./a"`
|
|
const aCode = `export const a = 1`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/index.ts", parser.parse(indexCode, "ts")],
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const dependents = builder.getDependents(graph, "/project/src/a.ts")
|
|
expect(dependents).toContain("/project/src/index.ts")
|
|
})
|
|
|
|
it("should return empty array for file not in graph", () => {
|
|
const asts = new Map<string, FileAST>()
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const dependents = builder.getDependents(graph, "/nonexistent.ts")
|
|
expect(dependents).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe("findCircularDependencies", () => {
|
|
it("should detect simple circular dependency", () => {
|
|
const aCode = `import { b } from "./b"; export const a = 1;`
|
|
const bCode = `import { a } from "./a"; export const b = 2;`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
["/project/src/b.ts", parser.parse(bCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
expect(cycles.length).toBe(1)
|
|
expect(cycles[0]).toContain("/project/src/a.ts")
|
|
expect(cycles[0]).toContain("/project/src/b.ts")
|
|
})
|
|
|
|
it("should detect three-way circular dependency", () => {
|
|
const aCode = `import { b } from "./b"; export const a = 1;`
|
|
const bCode = `import { c } from "./c"; export const b = 2;`
|
|
const cCode = `import { a } from "./a"; export const c = 3;`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
["/project/src/b.ts", parser.parse(bCode, "ts")],
|
|
["/project/src/c.ts", parser.parse(cCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
expect(cycles.length).toBe(1)
|
|
expect(cycles[0]).toHaveLength(4)
|
|
})
|
|
|
|
it("should return empty array when no cycles", () => {
|
|
const aCode = `export const a = 1`
|
|
const bCode = `import { a } from "./a"; export const b = a + 1;`
|
|
const cCode = `import { b } from "./b"; export const c = b + 1;`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
["/project/src/b.ts", parser.parse(bCode, "ts")],
|
|
["/project/src/c.ts", parser.parse(cCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
expect(cycles).toEqual([])
|
|
})
|
|
|
|
it("should handle self-reference", () => {
|
|
const aCode = `import { helper } from "./a"; export const a = 1; export function helper() {}`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/a.ts", parser.parse(aCode, "ts")],
|
|
])
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
expect(cycles.length).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe("getStats", () => {
|
|
it("should return comprehensive statistics", () => {
|
|
const code1 = `
|
|
export function func1(): void {}
|
|
export class Class1 {}
|
|
export interface Interface1 {}
|
|
export type Type1 = string
|
|
export const VAR1 = 1
|
|
`
|
|
const code2 = `
|
|
import { func1 } from "./file1"
|
|
export function func2(): void {}
|
|
`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/file1.ts", parser.parse(code1, "ts")],
|
|
["/project/src/file2.ts", parser.parse(code2, "ts")],
|
|
])
|
|
|
|
const symbolIndex = builder.buildSymbolIndex(asts)
|
|
const depsGraph = builder.buildDepsGraph(asts)
|
|
const stats = builder.getStats(symbolIndex, depsGraph)
|
|
|
|
expect(stats.totalSymbols).toBeGreaterThan(0)
|
|
expect(stats.symbolsByType.function).toBeGreaterThan(0)
|
|
expect(stats.symbolsByType.class).toBe(1)
|
|
expect(stats.symbolsByType.interface).toBe(1)
|
|
expect(stats.symbolsByType.type).toBe(1)
|
|
expect(stats.totalFiles).toBe(2)
|
|
expect(stats.totalDependencies).toBe(1)
|
|
})
|
|
|
|
it("should identify hubs", () => {
|
|
const hubCode = `export const shared = 1`
|
|
const consumerCodes = Array.from({ length: 6 }, () => `import { shared } from "./hub"`)
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/hub.ts", parser.parse(hubCode, "ts")],
|
|
])
|
|
consumerCodes.forEach((code, i) => {
|
|
asts.set(`/project/src/consumer${i}.ts`, parser.parse(code, "ts"))
|
|
})
|
|
|
|
const symbolIndex = builder.buildSymbolIndex(asts)
|
|
const depsGraph = builder.buildDepsGraph(asts)
|
|
const stats = builder.getStats(symbolIndex, depsGraph)
|
|
|
|
expect(stats.hubs).toContain("/project/src/hub.ts")
|
|
})
|
|
|
|
it("should identify orphans", () => {
|
|
const orphanCode = `const internal = 1`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/orphan.ts", parser.parse(orphanCode, "ts")],
|
|
])
|
|
|
|
const symbolIndex = builder.buildSymbolIndex(asts)
|
|
const depsGraph = builder.buildDepsGraph(asts)
|
|
const stats = builder.getStats(symbolIndex, depsGraph)
|
|
|
|
expect(stats.orphans).toContain("/project/src/orphan.ts")
|
|
})
|
|
})
|
|
|
|
describe("integration with ASTParser", () => {
|
|
it("should work with complex TypeScript code", () => {
|
|
const code = `
|
|
import { BaseService } from "./base"
|
|
import type { User, UserDTO } from "./types"
|
|
|
|
export class UserService extends BaseService {
|
|
private readonly cache = new Map<string, User>()
|
|
|
|
async findById(id: string): Promise<User | null> {
|
|
if (this.cache.has(id)) {
|
|
return this.cache.get(id)!
|
|
}
|
|
return this.repository.find(id)
|
|
}
|
|
|
|
toDTO(user: User): UserDTO {
|
|
return { id: user.id, name: user.name }
|
|
}
|
|
}
|
|
|
|
export type ServiceResult<T> = { success: true; data: T } | { success: false; error: string }
|
|
`
|
|
const baseCode = `export class BaseService { protected repository: any }`
|
|
const typesCode = `export interface User { id: string; name: string }; export interface UserDTO { id: string; name: string }`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/UserService.ts", parser.parse(code, "ts")],
|
|
["/project/src/base.ts", parser.parse(baseCode, "ts")],
|
|
["/project/src/types.ts", parser.parse(typesCode, "ts")],
|
|
])
|
|
|
|
const symbolIndex = builder.buildSymbolIndex(asts)
|
|
const depsGraph = builder.buildDepsGraph(asts)
|
|
|
|
expect(symbolIndex.has("UserService")).toBe(true)
|
|
expect(symbolIndex.has("UserService.findById")).toBe(true)
|
|
expect(symbolIndex.has("UserService.toDTO")).toBe(true)
|
|
expect(symbolIndex.has("ServiceResult")).toBe(true)
|
|
expect(symbolIndex.has("BaseService")).toBe(true)
|
|
expect(symbolIndex.has("User")).toBe(true)
|
|
|
|
expect(depsGraph.imports.get("/project/src/UserService.ts")).toContain(
|
|
"/project/src/base.ts",
|
|
)
|
|
expect(depsGraph.imports.get("/project/src/UserService.ts")).toContain(
|
|
"/project/src/types.ts",
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("jsx to tsx resolution", () => {
|
|
it("should resolve .jsx imports to .tsx files", () => {
|
|
const mainCode = `import { Button } from "./Button.jsx"`
|
|
const buttonCode = `export function Button() { return null }`
|
|
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/main.ts", parser.parse(mainCode, "ts")],
|
|
["/project/src/Button.tsx", parser.parse(buttonCode, "tsx")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
|
|
expect(graph.imports.get("/project/src/main.ts")).toContain("/project/src/Button.tsx")
|
|
})
|
|
})
|
|
|
|
describe("edge cases", () => {
|
|
it("should handle empty deps graph for circular dependencies", () => {
|
|
const graph = {
|
|
imports: new Map<string, string[]>(),
|
|
importedBy: new Map<string, string[]>(),
|
|
}
|
|
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
expect(cycles).toEqual([])
|
|
})
|
|
|
|
it("should handle single file with no imports", () => {
|
|
const code = `export const x = 1`
|
|
const asts = new Map<string, FileAST>([
|
|
["/project/src/single.ts", parser.parse(code, "ts")],
|
|
])
|
|
|
|
const graph = builder.buildDepsGraph(asts)
|
|
const cycles = builder.findCircularDependencies(graph)
|
|
|
|
expect(cycles).toEqual([])
|
|
})
|
|
})
|
|
})
|