Skip to content

Conversation

@Gankra
Copy link
Contributor

@Gankra Gankra commented Oct 23, 2025

This is a fix for the issue described here, where ty found my homebrew python site-packages, and then panicked because it was handling both symlink and non-symlink versions of the same directory (which, syntactically, are not subdirectories of eachother).

I am always suspicious of using canonicalize but it's not clear to me there's a way to avoid it here.

@Gankra Gankra changed the title Do not crash if a FileRoot is a symlink [ty] Do not crash if a FileRoot is a symlink Oct 23, 2025
@Gankra Gankra added ty Multi-file analysis & type inference server Related to the LSP server labels Oct 23, 2025
Comment on lines 192 to 194
let absolute = SystemPath::absolute(path, db.system().current_directory());
roots.at(&absolute)
// We need to resolve away symlinks here to avoid getting confused about subdirectories.
let canonicalized = db.system().canonicalize_path(&absolute).unwrap_or(absolute);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On paper the first absolute is redundant but I'm not sure if db.system().current_directory() is able to be not-the-process-level-current-directory that canonicalize_path uses.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 23, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 23, 2025

CodSpeed Performance Report

Merging #21052 will not alter performance

Comparing gankra/rootcause (8e12163) with main (b93d8f2)

Summary

✅ 52 untouched

@Gankra Gankra requested a review from AlexWaygood as a code owner October 23, 2025 23:41
@github-actions
Copy link
Contributor

github-actions bot commented Oct 23, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Oct 23, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

Comment on lines +183 to +194
// Make the path relative to the current directory if the path doesn't require
// UNC gunk (`//?/`) to be valid (`strip_prefix` gets really messy otherwise).
if let Some(Utf8Component::Prefix(prefix)) = canonicalized.components().next()
&& let Utf8Prefix::VerbatimDisk(_) = prefix.kind()
{
Ok(canonicalized)
} else {
Ok(canonicalized
.strip_prefix(os_system.current_directory())
.unwrap()
.to_owned())
}
Copy link
Contributor Author

@Gankra Gankra Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a "Hillarious" interaction between strip_prefix and dunce.

dunce removes "unnecessary" UNC prefixes, but the import_basic mdtest actually passes in a really really really long path, which dunce then goes "ah UNC is needed".

This results in canonicalized.strip_prefix(os_system.current_directory()) failing.

If you then go "ah I will simply canonicalize os_system.current_directory()" you still lose because dunce is very helpful and removes the unnecessary UNC prefix from the much shorter cwd!

@MichaReiser
Copy link
Member

I'd very much prefer not to call canonicalize in Files and, instead, call canonicalize where we add the FileRoot (and where we look it up if that turns out to be necessary). It limits the scope of where we have an undesired canonicalize call and also allows us to better document why it is necessary in this specific instance.

@AlexWaygood AlexWaygood removed their request for review October 24, 2025 07:03
@Gankra
Copy link
Contributor Author

Gankra commented Oct 24, 2025

I'd very much prefer not to call canonicalize in Files and, instead, call canonicalize where we add the FileRoot (and where we look it up if that turns out to be necessary).

Do you mean you want this logic moved into the routine it's calling (FileRoot::try_add, FileRoot::at)?
Or do you mean out to each caller of try_add_root / expect_root / root?

@MichaReiser
Copy link
Member

Ideally, we'd canonicalize the path where we create it before adding it as a search path (or site package path or whatever it is). This should then also ensure that we only create paths that use the canonicalized representation. But I don't understand the specific problem enough to say whether that's possible

@Gankra
Copy link
Contributor Author

Gankra commented Oct 30, 2025

Bafflingly I can't reproduce this anymore so I'm just gonna close this

@Gankra Gankra closed this Oct 30, 2025
@Gankra Gankra reopened this Oct 31, 2025
@Gankra
Copy link
Contributor Author

Gankra commented Oct 31, 2025

Ok micha got a reproducer. If you go in vscode and via the pallete select "Python Interpretter: /opt/homebrew/bin/python3" the crash occurs:

No root found for path '/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages'. Known roots: FileRoots(
...
/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages

On my system the path we select is a fractal symlink:

  1. /opt/homebrew/bin/python3
  2. /opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3
  3. /opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3
  4. /opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13

Path 4 is a real file. If you pass 1 or 2 to ty check --python ... everything works fine. If you pass 3 or 4 we have this panic.

The actual crash involves two other dirs in a symlink relationship:

  1. /opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages
  2. /opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages

Path 6 is an actual directory. We also see that path 6 is, weirdly, what you would discover if you searched for site-packages from path 2, which is wild because path 2 was a bloody symlink!

So when handed path 3 or 4 as --python we find that we're in a real directory (so even if we canonicalized at that point it would do nothing). At this point we search for and discover site-packages at path 5, which is a symlink but we don't normalize that away.

Then at some later point we end up with a copy of the same path but with the symlink resolved (path 6), and we freak out because the two paths aren't lexically nested.

@Gankra
Copy link
Contributor Author

Gankra commented Oct 31, 2025

Ok so the "root cause" is this canonicalize (and the absence of a paired canonicalize for adding a root):

for site_packages_search_path in site_packages {
let site_packages_dir = site_packages_search_path
.as_system_path()
.expect("Expected site package path to be a system path");
let site_packages_dir = system
.canonicalize_path(site_packages_dir)
.unwrap_or_else(|_| site_packages_dir.to_path_buf());
if !existing_paths.insert(Cow::Owned(site_packages_dir.clone())) {
continue;
}
let site_packages_root = files.expect_root(db, &site_packages_dir);

@Gankra
Copy link
Contributor Author

Gankra commented Oct 31, 2025

An unfortunate game of whackamole if you don't want to unconditionally canonicalize all paths.

Gankra added a commit that referenced this pull request Nov 12, 2025
@Gankra
Copy link
Contributor Author

Gankra commented Nov 12, 2025

Obsolete

@Gankra Gankra closed this Nov 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants