Files
socraticode/tests/unit/graph-resolution.test.ts
Giancarlo Erra 8c26ed8b49 fix(graph): allow Go resolution for projects with golang.org/* module paths
Address CodeRabbit review on PR #48. The early `isExternalModule` check
in resolveImport was filtering out any import starting with `golang.org/`
before the Go case had a chance to match it against the local module
path. This blocked legitimate local imports for any project whose own
module path starts with `golang.org/` (the Go team's own packages like
golang.org/x/sync, golang.org/x/net, etc., where each one's go.mod
declares `module golang.org/x/<name>`).

Skip the early external check for Go specifically. The Go case in
resolveImport already does its own module-path-aware classification
and returns null for everything outside the local module, including
stdlib and third-party deps. No regression in those cases.

New regression test asserts that
`module golang.org/x/custom` + `import "golang.org/x/custom/internal"`
resolves to the local internal/ package. Confirmed the test fails
without the fix and passes with it. Total: 752 unit tests pass.

Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
2026-05-05 01:57:07 +01:00

1993 lines
56 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
buildCsNamespaceMap,
buildGoModuleInfo,
buildJvmSuffixMap,
resolveImport,
} from "../../src/services/graph-resolution.js";
// ── Helper to create temp project layouts ─────────────────────────────
interface TempProject {
root: string;
fileSet: Set<string>;
cleanup: () => void;
}
function createTempProject(
files: Record<string, string>,
): TempProject {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-resolve-"));
const fileSet = new Set<string>();
for (const [relPath, content] of Object.entries(files)) {
const fullPath = path.join(root, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
fileSet.add(relPath);
}
return {
root,
fileSet,
cleanup: () => {
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
},
};
}
describe("graph-resolution", () => {
let project: TempProject | null = null;
afterEach(() => {
project?.cleanup();
project = null;
});
describe("TypeScript/JavaScript resolution", () => {
it("resolves relative imports with .js extension to .ts files", () => {
project = createTempProject({
"src/index.ts": "",
"src/utils.ts": "",
});
const result = resolveImport(
"./utils.js",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBe("src/utils.ts");
});
it("resolves relative imports without extension", () => {
project = createTempProject({
"src/index.ts": "",
"src/helpers.ts": "",
});
const result = resolveImport(
"./helpers",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBe("src/helpers.ts");
});
it("resolves imports to index files", () => {
project = createTempProject({
"src/app.ts": "",
"src/utils/index.ts": "",
});
const result = resolveImport(
"./utils",
path.join(project.root, "src/app.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBe("src/utils/index.ts");
});
it("resolves parent directory imports", () => {
project = createTempProject({
"src/utils/helper.ts": "",
"src/types.ts": "",
});
const result = resolveImport(
"../types",
path.join(project.root, "src/utils/helper.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBe("src/types.ts");
});
it("returns null for npm package imports", () => {
project = createTempProject({
"src/index.ts": "",
});
const result = resolveImport(
"lodash",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBeNull();
});
it("returns null for npm scoped package imports", () => {
project = createTempProject({
"src/index.ts": "",
});
const result = resolveImport(
"@types/node",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBeNull();
});
it("resolves direct .ts file imports", () => {
project = createTempProject({
"src/index.ts": "",
"src/config.ts": "",
});
const result = resolveImport(
"./config.ts",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBe("src/config.ts");
});
});
describe("Python resolution", () => {
it("resolves relative imports", () => {
project = createTempProject({
"src/main.py": "",
"src/models.py": "",
});
const result = resolveImport(
".models",
path.join(project.root, "src/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("src/models.py");
});
it("resolves absolute package imports", () => {
project = createTempProject({
"app.py": "",
"utils/helpers.py": "",
});
const result = resolveImport(
"utils.helpers",
path.join(project.root, "app.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("utils/helpers.py");
});
it("resolves __init__.py for package imports", () => {
project = createTempProject({
"app.py": "",
"utils/__init__.py": "",
});
const result = resolveImport(
"utils",
path.join(project.root, "app.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("utils/__init__.py");
});
it("returns null for stdlib imports", () => {
project = createTempProject({
"app.py": "",
});
const result = resolveImport(
"os",
path.join(project.root, "app.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBeNull();
});
it("returns null for json stdlib", () => {
project = createTempProject({
"app.py": "",
});
const result = resolveImport(
"json",
path.join(project.root, "app.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBeNull();
});
it("resolves absolute imports under src/ directory (src layout)", () => {
project = createTempProject({
"app.py": "",
"src/mypackage/utils.py": "",
});
const result = resolveImport(
"mypackage.utils",
path.join(project.root, "app.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("src/mypackage/utils.py");
});
it("resolves sibling-flat imports in service-style monorepos (#46)", () => {
// service-a/main.py runs as `python main.py` from inside service-a/,
// so `import config` resolves to service-a/config.py at runtime.
project = createTempProject({
"service-a/main.py": "",
"service-a/config.py": "",
"service-b/main.py": "",
"service-b/config.py": "",
});
const result = resolveImport(
"config",
path.join(project.root, "service-a/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("service-a/config.py");
});
it("resolves sibling-flat imports for dotted module paths", () => {
// `import shared.utils` from service-a/main.py with shared/utils.py
// sitting next to main.py.
project = createTempProject({
"service-a/main.py": "",
"service-a/shared/utils.py": "",
});
const result = resolveImport(
"shared.utils",
path.join(project.root, "service-a/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("service-a/shared/utils.py");
});
it("resolves sibling packages via __init__.py", () => {
// `import config` resolves to service-a/config/__init__.py when no
// bare service-a/config.py exists.
project = createTempProject({
"service-a/main.py": "",
"service-a/config/__init__.py": "",
});
const result = resolveImport(
"config",
path.join(project.root, "service-a/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("service-a/config/__init__.py");
});
it("preserves project-root precedence when the same name exists at root and as a sibling", () => {
// Backward-compat guarantee: `import config` from service-a/main.py
// still resolves to the project-root config.py if one exists, so
// existing layouts do not change after this PR. The sibling fallback
// only fires when the project-root lookup fails.
project = createTempProject({
"config.py": "",
"service-a/main.py": "",
"service-a/config.py": "",
});
const result = resolveImport(
"config",
path.join(project.root, "service-a/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBe("config.py");
});
it("returns null when neither project-root, src/, lib/, nor sibling have the module", () => {
project = createTempProject({
"service-a/main.py": "",
});
const result = resolveImport(
"config",
path.join(project.root, "service-a/main.py"),
project.root,
project.fileSet,
"python",
);
expect(result).toBeNull();
});
});
describe("Rust resolution", () => {
it("resolves mod declarations to .rs files", () => {
project = createTempProject({
"src/main.rs": "",
"src/config.rs": "",
});
const result = resolveImport(
"config",
path.join(project.root, "src/main.rs"),
project.root,
project.fileSet,
"rust",
);
expect(result).toBe("src/config.rs");
});
it("resolves mod declarations to mod.rs", () => {
project = createTempProject({
"src/main.rs": "",
"src/utils/mod.rs": "",
});
const result = resolveImport(
"utils",
path.join(project.root, "src/main.rs"),
project.root,
project.fileSet,
"rust",
);
expect(result).toBe("src/utils/mod.rs");
});
it("returns null for std:: imports", () => {
project = createTempProject({
"src/main.rs": "",
});
const result = resolveImport(
"std::collections::HashMap",
path.join(project.root, "src/main.rs"),
project.root,
project.fileSet,
"rust",
);
expect(result).toBeNull();
});
});
describe("C/C++ resolution", () => {
it("resolves relative header includes", () => {
project = createTempProject({
"src/main.c": "",
"src/utils.h": "",
});
const result = resolveImport(
"utils.h",
path.join(project.root, "src/main.c"),
project.root,
project.fileSet,
"c",
);
expect(result).toBe("src/utils.h");
});
it("resolves parent directory includes", () => {
project = createTempProject({
"src/sub/app.c": "",
"src/common.h": "",
});
const result = resolveImport(
"../common.h",
path.join(project.root, "src/sub/app.c"),
project.root,
project.fileSet,
"c",
);
expect(result).toBe("src/common.h");
});
});
describe("Ruby resolution", () => {
it("resolves relative requires", () => {
project = createTempProject({
"lib/app.rb": "",
"lib/models/user.rb": "",
});
const result = resolveImport(
"./models/user",
path.join(project.root, "lib/app.rb"),
project.root,
project.fileSet,
"ruby",
);
expect(result).toBe("lib/models/user.rb");
});
});
describe("PHP resolution", () => {
it("resolves PSR-4 namespace with lowercase first segment (Laravel convention)", () => {
project = createTempProject({
"app/Models/User.php": "",
"app/Http/Controllers/UserController.php": "",
});
const result = resolveImport(
"App\\Models\\User",
path.join(project.root, "app/Http/Controllers/UserController.php"),
project.root,
project.fileSet,
"php",
);
expect(result).toBe("app/Models/User.php");
});
it("resolves PSR-4 namespace with exact case match", () => {
project = createTempProject({
"App/Models/User.php": "",
});
const result = resolveImport(
"App\\Models\\User",
path.join(project.root, "index.php"),
project.root,
project.fileSet,
"php",
);
expect(result).toBe("App/Models/User.php");
});
it("resolves relative require paths", () => {
project = createTempProject({
"config.php": "",
"bootstrap/app.php": "",
});
const result = resolveImport(
"../config.php",
path.join(project.root, "bootstrap/app.php"),
project.root,
project.fileSet,
"php",
);
expect(result).toBe("config.php");
});
it("returns null for unresolvable vendor namespaces", () => {
project = createTempProject({
"app/Http/Controllers/UserController.php": "",
});
const result = resolveImport(
"Illuminate\\Http\\Request",
path.join(project.root, "app/Http/Controllers/UserController.php"),
project.root,
project.fileSet,
"php",
);
expect(result).toBeNull();
});
it("resolves namespace to src directory", () => {
project = createTempProject({
"src/Models/User.php": "",
"index.php": "",
});
const result = resolveImport(
"MyPackage\\Models\\User",
path.join(project.root, "index.php"),
project.root,
project.fileSet,
"php",
);
expect(result).toBe("src/Models/User.php");
});
});
describe("Java resolution", () => {
it("resolves fully qualified class imports", () => {
project = createTempProject({
"src/App.java": "",
"com/example/models/User.java": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/App.java"),
project.root,
project.fileSet,
"java",
);
expect(result).toBe("com/example/models/User.java");
});
it("resolves imports under src/main/java (Maven convention)", () => {
project = createTempProject({
"src/main/java/com/example/App.java": "",
"src/main/java/com/example/models/User.java": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/main/java/com/example/App.java"),
project.root,
project.fileSet,
"java",
);
expect(result).toBe("src/main/java/com/example/models/User.java");
});
it("resolves imports under src/ directory", () => {
project = createTempProject({
"src/com/example/App.java": "",
"src/com/example/models/User.java": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/com/example/App.java"),
project.root,
project.fileSet,
"java",
);
expect(result).toBe("src/com/example/models/User.java");
});
it("resolves Kotlin imports under src/main/kotlin", () => {
project = createTempProject({
"src/main/kotlin/com/example/App.kt": "",
"src/main/kotlin/com/example/models/User.kt": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/main/kotlin/com/example/App.kt"),
project.root,
project.fileSet,
"kotlin",
);
expect(result).toBe("src/main/kotlin/com/example/models/User.kt");
});
it("returns null for java stdlib imports", () => {
project = createTempProject({
"src/App.java": "",
});
const result = resolveImport(
"java.util.List",
path.join(project.root, "src/App.java"),
project.root,
project.fileSet,
"java",
);
expect(result).toBeNull();
});
});
describe("Dart resolution", () => {
it("resolves relative imports", () => {
project = createTempProject({
"lib/main.dart": "",
"lib/utils/helpers.dart": "",
});
const result = resolveImport(
"utils/helpers.dart",
path.join(project.root, "lib/main.dart"),
project.root,
project.fileSet,
"dart",
);
expect(result).toBe("lib/utils/helpers.dart");
});
it("returns null for package: imports", () => {
project = createTempProject({
"lib/main.dart": "",
});
const result = resolveImport(
"package:flutter/material.dart",
path.join(project.root, "lib/main.dart"),
project.root,
project.fileSet,
"dart",
);
expect(result).toBeNull();
});
it("returns null for dart: imports", () => {
project = createTempProject({
"lib/main.dart": "",
});
const result = resolveImport(
"dart:async",
path.join(project.root, "lib/main.dart"),
project.root,
project.fileSet,
"dart",
);
expect(result).toBeNull();
});
});
describe("Lua resolution", () => {
it("resolves dot-separated module paths", () => {
project = createTempProject({
"main.lua": "",
"utils/math.lua": "",
});
const result = resolveImport(
"utils.math",
path.join(project.root, "main.lua"),
project.root,
project.fileSet,
"lua",
);
expect(result).toBe("utils/math.lua");
});
it("returns null for stdlib modules", () => {
project = createTempProject({
"main.lua": "",
});
const result = resolveImport(
"string",
path.join(project.root, "main.lua"),
project.root,
project.fileSet,
"lua",
);
expect(result).toBeNull();
});
});
describe("Bash resolution", () => {
it("resolves relative source paths", () => {
project = createTempProject({
"run.sh": "",
"config.sh": "",
});
const result = resolveImport(
"./config.sh",
path.join(project.root, "run.sh"),
project.root,
project.fileSet,
"bash",
);
expect(result).toBe("config.sh");
});
});
describe("unknown language", () => {
it("returns null", () => {
project = createTempProject({
"file.xyz": "",
});
const result = resolveImport(
"./other",
path.join(project.root, "file.xyz"),
project.root,
project.fileSet,
"unknown",
);
expect(result).toBeNull();
});
});
// ── Go resolution ──────────────────────────────────────────────────────
describe("Go resolution", () => {
it("returns null when no goModuleInfo is supplied (no go.mod path active)", () => {
// Back-compat: when the resolver is called without Go module info
// (e.g. for projects with no go.mod), every Go import resolves to
// null. This is the path before issue #45's fix.
project = createTempProject({
"main.go": "",
"internal/helper.go": "",
});
const result = resolveImport(
"github.com/example/pkg",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
);
expect(result).toBeNull();
});
it("resolves a subpackage import to the representative .go file (#45)", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"main.go": "",
"internal/helper.go": "",
"internal/util.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo).not.toBeNull();
const result = resolveImport(
"example.com/myapp/internal",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
// helper.go is the lexically smallest non-test .go file in
// internal/, so it's picked as the package's representative.
expect(result).toBe("internal/helper.go");
});
it("resolves the root-package import to a project-root .go file", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"main.go": "",
"doc.go": "",
"internal/helper.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"example.com/myapp",
path.join(project.root, "internal/helper.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBe("doc.go");
});
it("returns null for external imports (not under module path)", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"github.com/spf13/cobra",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBeNull();
});
it("returns null when go.mod is missing", () => {
project = createTempProject({
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
// No go.mod → buildGoModuleInfo returns null → resolver returns null
// for any Go import.
expect(goInfo).toBeNull();
});
it("returns null when go.mod has no module directive", () => {
project = createTempProject({
"go.mod": "go 1.21\n",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo).toBeNull();
});
it("excludes _test.go files from representative selection", () => {
// service/foo.go and service/foo_test.go both exist in the same
// directory. The map's representative should be foo.go (the
// non-test file), not foo_test.go.
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"service/foo.go": "",
"service/foo_test.go": "",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"example.com/myapp/service",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBe("service/foo.go");
});
it("uses lexically smallest non-test file as the representative (determinism)", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"pkg/zeta.go": "",
"pkg/alpha.go": "",
"pkg/middle.go": "",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"example.com/myapp/pkg",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBe("pkg/alpha.go");
});
it("resolves local imports when the module path starts with golang.org/", () => {
// Real-world case: someone working ON one of the Go-team packages
// like golang.org/x/sync. Their go.mod declares
// `module golang.org/x/sync` and internal subpackages must resolve
// locally, even though isExternalModule treats `golang.org/...`
// imports as external for non-local-module projects.
project = createTempProject({
"go.mod": "module golang.org/x/custom\n\ngo 1.21\n",
"main.go": "",
"internal/foo.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"golang.org/x/custom/internal",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBe("internal/foo.go");
});
it("returns null for a similar-prefix import that is not a subpackage", () => {
// `example.com/myapp-other/pkg` shares the prefix `example.com/myapp`
// textually but is a separate module. Must not resolve to anything
// inside the local project.
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"pkg/foo.go": "",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"example.com/myapp-other/pkg",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBeNull();
});
});
describe("buildGoModuleInfo", () => {
it("parses a simple go.mod and returns module path + package map", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n\ngo 1.21\n",
"main.go": "",
"internal/helper.go": "",
"internal/util.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo).not.toBeNull();
expect(goInfo?.modulePath).toBe("example.com/myapp");
// Root package
expect(goInfo?.packageMap.get(".")).toBe("main.go");
// Subpackage — first lex-sorted file wins
expect(goInfo?.packageMap.get("internal")).toBe("internal/helper.go");
});
it("parses go.mod with leading whitespace and trailing content", () => {
project = createTempProject({
"go.mod": " module github.com/user/repo\n\ngo 1.21\n\nrequire (\n\tgithub.com/x/y v1.0.0\n)\n",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo?.modulePath).toBe("github.com/user/repo");
});
it("returns null when go.mod is missing", () => {
project = createTempProject({
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo).toBeNull();
});
it("returns null when go.mod has no module directive", () => {
project = createTempProject({
"go.mod": "// no module line\ngo 1.21\n",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo).toBeNull();
});
it("excludes _test.go files from the package map representatives", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n",
"service/foo_test.go": "",
"service/foo.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo?.packageMap.get("service")).toBe("service/foo.go");
});
it("does not include directories that contain only _test.go files", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n",
"internal/only_test.go": "",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
// Map has "." (for main.go) but no "internal" entry, because the
// only file there is a test file.
expect(goInfo?.packageMap.has(".")).toBe(true);
expect(goInfo?.packageMap.has("internal")).toBe(false);
});
it("uses forward-slash keys for nested packages (cross-platform lookup)", () => {
// Go imports always use forward slashes; the map keys must match
// that form so resolution works on Windows, where path.dirname
// would otherwise produce backslash-separated keys for nested
// packages. The map value preserves the fileSet's native form
// (test fixtures use forward slashes since createTempProject keys
// its fileSet by the input relPath).
project = createTempProject({
"go.mod": "module example.com/myapp\n",
"pkg/subpkg/file.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
expect(goInfo?.packageMap.has("pkg/subpkg")).toBe(true);
expect(goInfo?.packageMap.get("pkg/subpkg")).toBe("pkg/subpkg/file.go");
});
it("resolves nested-package imports", () => {
project = createTempProject({
"go.mod": "module example.com/myapp\n",
"pkg/subpkg/file.go": "",
"main.go": "",
});
const goInfo = buildGoModuleInfo(project.fileSet, project.root);
const result = resolveImport(
"example.com/myapp/pkg/subpkg",
path.join(project.root, "main.go"),
project.root,
project.fileSet,
"go",
undefined,
undefined,
undefined,
goInfo,
);
expect(result).toBe("pkg/subpkg/file.go");
});
});
// ── C# resolution ─────────────────────────────────────────────────────
describe("C# resolution", () => {
it("returns null when no namespace map is supplied (back-compat)", () => {
project = createTempProject({
"Models/User.cs": "namespace MyApp.Models { public class User {} }",
"Program.cs": "using MyApp.Models;",
});
const result = resolveImport(
"MyApp.Models",
path.join(project.root, "Program.cs"),
project.root,
project.fileSet,
"csharp",
);
expect(result).toBeNull();
});
it("resolves a using directive to a file via the namespace map", () => {
project = createTempProject({
"Models/User.cs": "namespace MyApp.Models { public class User {} }",
"Program.cs": "using MyApp.Models;\nnamespace MyApp { class Program {} }",
});
const csNamespaceMap = buildCsNamespaceMap(project.fileSet, project.root);
const result = resolveImport(
"MyApp.Models",
path.join(project.root, "Program.cs"),
project.root,
project.fileSet,
"csharp",
undefined,
undefined,
csNamespaceMap,
);
expect(result).toBe("Models/User.cs");
});
it("returns the first candidate when a namespace spans multiple files", () => {
project = createTempProject({
"Services/UserService.cs":
"namespace MyApp.Services { public class UserService {} }",
"Services/OrderService.cs":
"namespace MyApp.Services { public class OrderService {} }",
"Program.cs": "using MyApp.Services;",
});
const csNamespaceMap = buildCsNamespaceMap(project.fileSet, project.root);
const result = resolveImport(
"MyApp.Services",
path.join(project.root, "Program.cs"),
project.root,
project.fileSet,
"csharp",
undefined,
undefined,
csNamespaceMap,
);
// Multi-file namespaces resolve to the first registered file. Files are
// visited in lexicographic order, so OrderService.cs precedes
// UserService.cs. Multi-file fan-out is a known follow-up.
expect(result).toBe("Services/OrderService.cs");
});
it("returns null for unknown namespaces even with a populated map", () => {
project = createTempProject({
"Models/User.cs": "namespace MyApp.Models { public class User {} }",
"Program.cs": "using MyApp.Unknown;",
});
const csNamespaceMap = buildCsNamespaceMap(project.fileSet, project.root);
const result = resolveImport(
"MyApp.Unknown",
path.join(project.root, "Program.cs"),
project.root,
project.fileSet,
"csharp",
undefined,
undefined,
csNamespaceMap,
);
expect(result).toBeNull();
});
it("filters System.* and Microsoft.* as external before consulting the map", () => {
project = createTempProject({
"Program.cs": "namespace System.Collections { class Stub {} }",
});
const csNamespaceMap = buildCsNamespaceMap(project.fileSet, project.root);
const result = resolveImport(
"System.Collections",
path.join(project.root, "Program.cs"),
project.root,
project.fileSet,
"csharp",
undefined,
undefined,
csNamespaceMap,
);
expect(result).toBeNull();
});
});
// ── buildCsNamespaceMap ───────────────────────────────────────────────
describe("buildCsNamespaceMap", () => {
it("indexes block-scoped namespace declarations in lexicographic order", () => {
project = createTempProject({
"Models/User.cs": "namespace MyApp.Models { public class User {} }",
"Models/Order.cs": "namespace MyApp.Models { public class Order {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
// Files are sorted lexically, so Order.cs comes before User.cs.
expect(map.get("MyApp.Models")).toEqual([
"Models/Order.cs",
"Models/User.cs",
]);
});
it("returns the same candidate order regardless of fileSet insertion order", () => {
// Build two projects on the same physical layout but feed buildCsNamespaceMap
// a Set populated in two different orders, mimicking how fs.readdir() can
// hand back entries in arbitrary order across filesystems.
project = createTempProject({
"Services/UserService.cs":
"namespace MyApp.Services { public class UserService {} }",
"Services/OrderService.cs":
"namespace MyApp.Services { public class OrderService {} }",
"Services/AccountService.cs":
"namespace MyApp.Services { public class AccountService {} }",
});
const forward = new Set([
"Services/AccountService.cs",
"Services/OrderService.cs",
"Services/UserService.cs",
]);
const reverse = new Set([
"Services/UserService.cs",
"Services/OrderService.cs",
"Services/AccountService.cs",
]);
const a = buildCsNamespaceMap(forward, project.root);
const b = buildCsNamespaceMap(reverse, project.root);
expect(a.get("MyApp.Services")).toEqual(b.get("MyApp.Services"));
expect(a.get("MyApp.Services")?.[0]).toBe("Services/AccountService.cs");
});
it("indexes file-scoped namespace declarations (C# 10+)", () => {
project = createTempProject({
"Services/UserService.cs":
"namespace MyApp.Services;\n\npublic class UserService {}",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.get("MyApp.Services")).toEqual(["Services/UserService.cs"]);
});
it("registers multiple namespaces declared in the same file", () => {
project = createTempProject({
"Mixed.cs":
"namespace MyApp.A { class A {} }\nnamespace MyApp.B { class B {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.get("MyApp.A")).toEqual(["Mixed.cs"]);
expect(map.get("MyApp.B")).toEqual(["Mixed.cs"]);
});
it("does not match commented-out namespace lines", () => {
project = createTempProject({
"Program.cs":
"// namespace MyApp.Hidden;\nnamespace MyApp.Real { class C {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.has("MyApp.Hidden")).toBe(false);
expect(map.get("MyApp.Real")).toEqual(["Program.cs"]);
});
it("ignores non-.cs files", () => {
project = createTempProject({
"notes.txt": "namespace Fake.Namespace;",
"Program.cs": "namespace Real.Namespace { class C {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.has("Fake.Namespace")).toBe(false);
expect(map.get("Real.Namespace")).toEqual(["Program.cs"]);
});
it("returns an empty map for a project with no .cs files", () => {
project = createTempProject({ "index.ts": "", "style.css": "" });
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.size).toBe(0);
});
it("does not duplicate the same file when re-indexed", () => {
project = createTempProject({
"Dup.cs": "namespace MyApp.X { class A {} }\nnamespace MyApp.X { class B {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.get("MyApp.X")).toEqual(["Dup.cs"]);
});
it("captures nested namespace declarations indented inside an outer block", () => {
project = createTempProject({
"Nested.cs":
"namespace Outer\n{\n namespace Inner\n {\n class C {}\n }\n}\n",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.get("Outer")).toEqual(["Nested.cs"]);
expect(map.get("Inner")).toEqual(["Nested.cs"]);
});
it("rejects identifiers that do not start with a letter or underscore", () => {
project = createTempProject({
// Invalid C# (digit-leading), should not be captured.
"Bad.cs": "namespace 1Foo { class C {} }",
// Valid neighbour to make sure the scan still works.
"Good.cs": "namespace Real.NS { class C {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.has("1Foo")).toBe(false);
expect(map.get("Real.NS")).toEqual(["Good.cs"]);
});
it("requires a `;` or `{` after the namespace name to avoid false positives", () => {
project = createTempProject({
// The token `namespace MyApp.Hint` appears here but is not a real
// declaration (no terminator), so it must not be captured. The
// real declaration on the next line should be captured.
"Mixed.cs":
"// see also: namespace MyApp.Hint in legacy code\nnamespace MyApp.Real { class C {} }",
});
const map = buildCsNamespaceMap(project.fileSet, project.root);
expect(map.has("MyApp.Hint")).toBe(false);
expect(map.get("MyApp.Real")).toEqual(["Mixed.cs"]);
});
});
// ── Swift resolution ──────────────────────────────────────────────────
describe("Swift resolution", () => {
it("resolves relative imports", () => {
project = createTempProject({
"Sources/App/main.swift": "",
"Sources/App/helper.swift": "",
});
const result = resolveImport(
"./helper",
path.join(project.root, "Sources/App/main.swift"),
project.root,
project.fileSet,
"swift",
);
expect(result).toBe("Sources/App/helper.swift");
});
it("returns null for framework imports", () => {
project = createTempProject({
"main.swift": "",
});
const result = resolveImport(
"Foundation",
path.join(project.root, "main.swift"),
project.root,
project.fileSet,
"swift",
);
expect(result).toBeNull();
});
});
// ── Scala resolution ──────────────────────────────────────────────────
describe("Scala resolution", () => {
it("resolves package path to file in src/main/scala", () => {
project = createTempProject({
"src/main/scala/com/example/models/User.scala": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/main/scala/com/example/App.scala"),
project.root,
project.fileSet,
"scala",
);
expect(result).toBe("src/main/scala/com/example/models/User.scala");
});
it("returns null for stdlib imports", () => {
project = createTempProject({
"Main.scala": "",
});
const result = resolveImport(
"scala.collection.mutable.ListBuffer",
path.join(project.root, "Main.scala"),
project.root,
project.fileSet,
"scala",
);
expect(result).toBeNull();
});
});
// ── Kotlin resolution ─────────────────────────────────────────────────
describe("Kotlin resolution", () => {
it("resolves package path to file in src/main/kotlin", () => {
project = createTempProject({
"src/main/kotlin/com/example/models/User.kt": "",
});
const result = resolveImport(
"com.example.models.User",
path.join(project.root, "src/main/kotlin/com/example/App.kt"),
project.root,
project.fileSet,
"kotlin",
);
expect(result).toBe("src/main/kotlin/com/example/models/User.kt");
});
it("returns null for stdlib imports", () => {
project = createTempProject({
"Main.kt": "",
});
const result = resolveImport(
"kotlinx.coroutines.launch",
path.join(project.root, "Main.kt"),
project.root,
project.fileSet,
"kotlin",
);
expect(result).toBeNull();
});
});
// ── Path alias resolution ──────────────────────────────────────────────
describe("Path alias resolution", () => {
it("resolves $lib/ alias to src/lib/", () => {
project = createTempProject({
"src/lib/Component.svelte": "",
"src/routes/page.svelte": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib/"]]]),
};
const result = resolveImport(
"$lib/Component.svelte",
path.join(project.root, "src/routes/page.svelte"),
project.root,
project.fileSet,
"svelte",
aliases,
);
expect(result).toBe("src/lib/Component.svelte");
});
it("resolves @/ alias to src/", () => {
project = createTempProject({
"src/utils/helper.ts": "",
"src/index.ts": "",
});
const aliases = {
entries: new Map([["@/", ["src/"]]]),
};
const result = resolveImport(
"@/utils/helper",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(result).toBe("src/utils/helper.ts");
});
it("resolves alias with extensionless import", () => {
project = createTempProject({
"src/lib/utils.ts": "",
"src/app.ts": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib/"]]]),
};
const result = resolveImport(
"$lib/utils",
path.join(project.root, "src/app.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(result).toBe("src/lib/utils.ts");
});
it("returns null when alias does not match any file", () => {
project = createTempProject({
"src/index.ts": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib/"]]]),
};
const result = resolveImport(
"$lib/NonExistent",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(result).toBeNull();
});
it("falls back to null without aliases (backwards compatible)", () => {
project = createTempProject({
"src/index.ts": "",
});
const result = resolveImport(
"$lib/Component",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
);
expect(result).toBeNull();
});
it("tries multiple alias targets in order (first match wins)", () => {
project = createTempProject({
"src/types.ts": "",
"generated/types.ts": "",
"src/index.ts": "",
});
const aliases = {
entries: new Map([["@/", ["src", "generated"]]]),
};
const result = resolveImport(
"@/types",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
// src/ is listed first, so it should win over generated/
expect(result).toBe("src/types.ts");
});
it("falls back to second alias target when first has no match", () => {
project = createTempProject({
"generated/types.ts": "",
"src/index.ts": "",
});
const aliases = {
entries: new Map([["@/", ["src", "generated"]]]),
};
const result = resolveImport(
"@/types",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(result).toBe("generated/types.ts");
});
it("resolves CSS alias imports", () => {
project = createTempProject({
"src/lib/styles/variables.css": "",
"src/app.css": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib/"]]]),
};
const result = resolveImport(
"$lib/styles/variables.css",
path.join(project.root, "src/app.css"),
project.root,
project.fileSet,
"css",
aliases,
);
expect(result).toBe("src/lib/styles/variables.css");
});
it("resolves extensionless CSS alias import via extension-try loop", () => {
project = createTempProject({
"src/lib/styles/variables.scss": "",
"src/app.css": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib"]]]),
};
const result = resolveImport(
"$lib/styles/variables",
path.join(project.root, "src/app.css"),
project.root,
project.fileSet,
"css",
aliases,
);
expect(result).toBe("src/lib/styles/variables.scss");
});
it("resolves CSS relative imports", () => {
project = createTempProject({
"src/styles/variables.css": "",
"src/styles/main.css": "",
});
const result = resolveImport(
"./variables.css",
path.join(project.root, "src/styles/main.css"),
project.root,
project.fileSet,
"css",
);
expect(result).toBe("src/styles/variables.css");
});
it("resolves SCSS relative imports (language=scss)", () => {
project = createTempProject({
"src/styles/theme.scss": "",
"src/styles/main.scss": "",
});
const result = resolveImport(
"./theme.scss",
path.join(project.root, "src/styles/main.scss"),
project.root,
project.fileSet,
"scss",
);
expect(result).toBe("src/styles/theme.scss");
});
it("resolves SCSS partial with _ prefix", () => {
project = createTempProject({
"src/styles/_variables.scss": "",
"src/styles/main.scss": "",
});
const result = resolveImport(
"./variables",
path.join(project.root, "src/styles/main.scss"),
project.root,
project.fileSet,
"scss",
);
expect(result).toBe("src/styles/_variables.scss");
});
it("resolves SCSS partial via alias", () => {
project = createTempProject({
"src/lib/styles/_colors.scss": "",
"src/app.scss": "",
});
const aliases = {
entries: new Map([["$lib/", ["src/lib"]]]),
};
const result = resolveImport(
"$lib/styles/colors",
path.join(project.root, "src/app.scss"),
project.root,
project.fileSet,
"scss",
aliases,
);
expect(result).toBe("src/lib/styles/_colors.scss");
});
it("prefers non-partial over partial when both exist", () => {
project = createTempProject({
"src/styles/variables.scss": "",
"src/styles/_variables.scss": "",
"src/styles/main.scss": "",
});
const result = resolveImport(
"./variables",
path.join(project.root, "src/styles/main.scss"),
project.root,
project.fileSet,
"scss",
);
// Direct match with extension should win before trying _ prefix
expect(result).toBe("src/styles/variables.scss");
});
it("resolves SCSS partial when import has explicit .scss extension", () => {
project = createTempProject({
"src/styles/_variables.scss": "",
"src/styles/main.scss": "",
});
const result = resolveImport(
"./variables.scss",
path.join(project.root, "src/styles/main.scss"),
project.root,
project.fileSet,
"scss",
);
expect(result).toBe("src/styles/_variables.scss");
});
it("resolves Less relative imports (language=less)", () => {
project = createTempProject({
"src/styles/theme.less": "",
"src/styles/main.less": "",
});
const result = resolveImport(
"./theme.less",
path.join(project.root, "src/styles/main.less"),
project.root,
project.fileSet,
"less",
);
expect(result).toBe("src/styles/theme.less");
});
it("resolves Sass relative imports (language=sass)", () => {
project = createTempProject({
"src/styles/_base.sass": "",
"src/styles/main.sass": "",
});
const result = resolveImport(
"./base",
path.join(project.root, "src/styles/main.sass"),
project.root,
project.fileSet,
"sass",
);
expect(result).toBe("src/styles/_base.sass");
});
it("exact alias pattern only matches exact specifier", () => {
project = createTempProject({
"src/index.ts": "",
"src/utils/helper.ts": "",
});
const aliases = {
entries: new Map([["~", ["src"]]]),
};
// "~utils/helper" should NOT match exact alias "~"
const noMatch = resolveImport(
"~utils/helper",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(noMatch).toBeNull();
// Exact "~" should resolve to src directory index
const exactMatch = resolveImport(
"~",
path.join(project.root, "src/index.ts"),
project.root,
project.fileSet,
"typescript",
aliases,
);
expect(exactMatch).toBe("src/index.ts");
});
it("returns null for bare CSS package specifier", () => {
project = createTempProject({
"src/styles/main.css": "",
});
const result = resolveImport(
"normalize.css",
path.join(project.root, "src/styles/main.css"),
project.root,
project.fileSet,
"css",
);
expect(result).toBeNull();
});
});
// ── buildJvmSuffixMap + multi-module resolution ──────────────────────────
describe("JVM multi-module resolution (buildJvmSuffixMap)", () => {
it("builds a suffix map keyed by class path after src/main/java", () => {
project = createTempProject({
[`module-a${path.sep}sub${path.sep}src${path.sep}main${path.sep}java${path.sep}com${path.sep}example${path.sep}Foo.java`]: "",
[`module-b${path.sep}src${path.sep}main${path.sep}kotlin${path.sep}com${path.sep}example${path.sep}Bar.kt`]: "",
[`module-c${path.sep}src${path.sep}main${path.sep}scala${path.sep}com${path.sep}example${path.sep}Baz.scala`]: "",
});
const map = buildJvmSuffixMap(project.fileSet);
expect(map.has(`com${path.sep}example${path.sep}Foo.java`)).toBe(true);
expect(map.has(`com${path.sep}example${path.sep}Bar.kt`)).toBe(true);
expect(map.has(`com${path.sep}example${path.sep}Baz.scala`)).toBe(true);
});
it("returns empty map when project has no JVM files", () => {
project = createTempProject({ "index.ts": "", "style.css": "" });
const map = buildJvmSuffixMap(project.fileSet);
expect(map.size).toBe(0);
});
it("ignores JVM files outside src/main/<lang> (e.g. test sources)", () => {
project = createTempProject({
// test source — should be ignored
[`module-a${path.sep}src${path.sep}test${path.sep}java${path.sep}com${path.sep}example${path.sep}FooTest.java`]: "",
// main source — should be registered
[`module-a${path.sep}src${path.sep}main${path.sep}java${path.sep}com${path.sep}example${path.sep}Foo.java`]: "",
});
const map = buildJvmSuffixMap(project.fileSet);
expect(map.has(`com${path.sep}example${path.sep}Foo.java`)).toBe(true);
expect(map.has(`com${path.sep}example${path.sep}FooTest.java`)).toBe(false);
});
it("resolves a Java import in a multi-module Maven project via suffix map", () => {
// Simulate: module-sso/module-sso-service/src/main/java/cn/sino/sso/UserService.java
const userServicePath =
`module-sso${path.sep}module-sso-service${path.sep}src${path.sep}main${path.sep}java${path.sep}cn${path.sep}sino${path.sep}sso${path.sep}UserService.java`;
const callerPath =
`module-opt${path.sep}src${path.sep}main${path.sep}java${path.sep}cn${path.sep}sino${path.sep}opt${path.sep}Service.java`;
project = createTempProject({
[userServicePath]: "",
[callerPath]: "",
});
const jvmSuffixMap = buildJvmSuffixMap(project.fileSet);
const result = resolveImport(
"cn.sino.sso.UserService",
path.join(project.root, callerPath),
project.root,
project.fileSet,
"java",
undefined,
jvmSuffixMap,
);
expect(result).toBe(userServicePath);
});
it("resolves Kotlin import in multi-module project via suffix map", () => {
const barPath =
`module-core${path.sep}src${path.sep}main${path.sep}kotlin${path.sep}com${path.sep}example${path.sep}Bar.kt`;
const callerPath =
`module-api${path.sep}src${path.sep}main${path.sep}kotlin${path.sep}com${path.sep}example${path.sep}Caller.kt`;
project = createTempProject({ [barPath]: "", [callerPath]: "" });
const jvmSuffixMap = buildJvmSuffixMap(project.fileSet);
const result = resolveImport(
"com.example.Bar",
path.join(project.root, callerPath),
project.root,
project.fileSet,
"kotlin",
undefined,
jvmSuffixMap,
);
expect(result).toBe(barPath);
});
it("returns null when class exists nowhere in the project", () => {
project = createTempProject({
[`module-a${path.sep}src${path.sep}main${path.sep}java${path.sep}com${path.sep}example${path.sep}Foo.java`]: "",
});
const jvmSuffixMap = buildJvmSuffixMap(project.fileSet);
const result = resolveImport(
"com.example.NonExistent",
path.join(project.root, `module-a${path.sep}src${path.sep}main${path.sep}java${path.sep}com${path.sep}example${path.sep}Foo.java`),
project.root,
project.fileSet,
"java",
undefined,
jvmSuffixMap,
);
expect(result).toBeNull();
});
it("still returns null for java stdlib even with suffix map", () => {
project = createTempProject({
[`module-a${path.sep}src${path.sep}main${path.sep}java${path.sep}java${path.sep}util${path.sep}List.java`]: "",
});
const jvmSuffixMap = buildJvmSuffixMap(project.fileSet);
const result = resolveImport(
"java.util.List",
path.join(project.root, `module-a${path.sep}src${path.sep}main${path.sep}java${path.sep}Caller.java`),
project.root,
project.fileSet,
"java",
undefined,
jvmSuffixMap,
);
expect(result).toBeNull();
});
});
});