diff --git a/CHANGELOG.md b/CHANGELOG.md index b323a369..d2935bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,10 @@ Status of the `main` branch. Changes prior to the next official version change w VTS initialization options now override defaults per top-level key rather than replacing the entire configuration; a user-provided `typescript` block replaces the ATA default too. `initializationOptions` takes precedence over the legacy `initialization_options` alias. + - Fix: activating an additional TypeScript workspace folder could open a root-level tool + config (`vitest.config.ts`, `jest.config.ts`, etc.) adjacent to `tsconfig.json` instead of + a real source file, starting the wrong inferred project and silently losing cross-package + references (#2090) - Fix: a C# file created after the project was already indexed was analyzed by Roslyn as a standalone Miscellaneous Files document instead of being folded into the loaded project, causing phantom diagnostics on the new file and on files referencing its symbols (#1961) diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index e3ae0868..fcea901d 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -516,20 +516,25 @@ class TypeScriptLanguageServer(SolidLanguageServer): def _find_representative_source_file(self, directory: str) -> str | None: """Find a TypeScript file suitable for triggering project loading. - Prefers a file adjacent to tsconfig.json (indicating the project root), - then falls back to the first .ts/.tsx file found. + Prefers a file under a `src` subdirectory adjacent to tsconfig.json (the + conventional source root), so a root-level tool config that tsconfig excludes + (vitest.config.ts, jest.config.ts, etc.) is not picked over the project's real + source tree. Falls back to a file directly adjacent to tsconfig.json, then to + the first .ts/.tsx file found anywhere in the directory. """ for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if not self.is_ignored_dirname(d)] if "tsconfig.json" in files: + src_dir = os.path.join(root, "src") + if os.path.isdir(src_dir): + for src_root, src_dirs, src_files in os.walk(src_dir): + src_dirs[:] = [d for d in src_dirs if not self.is_ignored_dirname(d)] + for f in src_files: + if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"): + return os.path.join(src_root, f) for f in files: if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"): return os.path.join(root, f) - src_dir = os.path.join(root, "src") - if os.path.isdir(src_dir): - for f in os.listdir(src_dir): - if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"): - return os.path.join(src_dir, f) for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if not self.is_ignored_dirname(d)] diff --git a/test/solidlsp/test_typescript_representative_source_file.py b/test/solidlsp/test_typescript_representative_source_file.py new file mode 100644 index 00000000..3c156101 --- /dev/null +++ b/test/solidlsp/test_typescript_representative_source_file.py @@ -0,0 +1,68 @@ +"""Regression test for oraios/serena#2090: additional-workspace activation must not pick a +root-level tool config (vitest.config.ts, jest.config.ts, ...) adjacent to tsconfig.json over +the project's actual source tree. + +Exercises TypeScriptLanguageServer._find_representative_source_file directly against a real +filesystem layout, without spawning a language server process; same technique as +test_typescript_timeout_policy.py's _bare_ts_server. +""" + +from solidlsp.language_servers.typescript_language_server import TypeScriptLanguageServer + + +def _bare_ts_server() -> TypeScriptLanguageServer: + return object.__new__(TypeScriptLanguageServer) + + +class TestFindRepresentativeSourceFile: + def test_prefers_nested_src_file_over_adjacent_tool_config(self, tmp_path) -> None: + """The issue's own minimal layout: a tsconfig-adjacent vitest.config.ts must lose to a + real source file nested under src/, even when that source file is not directly inside + src/ itself (it is one level further down, under src/routes/). + """ + pkg = tmp_path / "apps" / "api" + routes = pkg / "src" / "routes" + routes.mkdir(parents=True) + (pkg / "tsconfig.json").write_text("{}") + (pkg / "vitest.config.ts").write_text("export default {};") + (routes / "money.ts").write_text("export const x = 1;") + + result = _bare_ts_server()._find_representative_source_file(str(tmp_path)) + + assert result == str(routes / "money.ts") + + def test_falls_back_to_adjacent_file_when_no_src_dir_exists(self, tmp_path) -> None: + """Projects with source files directly next to tsconfig.json (no src/ subdirectory) + must keep working exactly as before. + """ + pkg = tmp_path / "pkg" + pkg.mkdir() + (pkg / "tsconfig.json").write_text("{}") + (pkg / "index.ts").write_text("export const x = 1;") + + result = _bare_ts_server()._find_representative_source_file(str(tmp_path)) + + assert result == str(pkg / "index.ts") + + def test_ignores_node_modules_under_src(self, tmp_path) -> None: + """The src/ subtree walk must still respect is_ignored_dirname, or a vendored .ts file + under src/node_modules could be selected instead of real project source. + """ + pkg = tmp_path / "pkg" + vendored = pkg / "src" / "node_modules" / "dep" + real_src = pkg / "src" / "lib" + vendored.mkdir(parents=True) + real_src.mkdir(parents=True) + (pkg / "tsconfig.json").write_text("{}") + (pkg / "vitest.config.ts").write_text("export default {};") + (vendored / "vendored.ts").write_text("export const y = 1;") + (real_src / "app.ts").write_text("export const x = 1;") + + result = _bare_ts_server()._find_representative_source_file(str(tmp_path)) + + assert result == str(real_src / "app.ts") + + def test_returns_none_when_nothing_matches(self, tmp_path) -> None: + (tmp_path / "README.md").write_text("no typescript here") + + assert _bare_ts_server()._find_representative_source_file(str(tmp_path)) is None