Skip to content

[codex] Fix symlinked relative import resolution#5210

Merged
proggeramlug merged 4 commits into
PerryTS:mainfrom
andrewtdiz:codex/symlinked-relative-import-resolution
Jun 15, 2026
Merged

[codex] Fix symlinked relative import resolution#5210
proggeramlug merged 4 commits into
PerryTS:mainfrom
andrewtdiz:codex/symlinked-relative-import-resolution

Conversation

@andrewtdiz

@andrewtdiz andrewtdiz commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Resolve relative imports against the source-visible importer path before falling back to the canonical module path during module collection.
  • Add lexical . / .. normalization as a fallback for relative specifiers whose importer path contains a symlinked component.
  • Add a regression covering imported class fields, prototype method lookup/calls, and instanceof through a symlinked entry path.

Root Cause

Perry stores and reads collected modules by canonical path, but source relative import specifiers are written against the path passed to the compiler. When that path contains a symlinked component, resolving .. after filesystem canonicalization can probe a different sibling directory. The import can then be missed, leaving imported class metadata absent and allowing construction to fall back to the empty placeholder shape.

Verification

  • cargo fmt --check
  • cargo test -p perry symlinked_entry_resolves_relative_imports_from_lexical_path
  • cargo build -p perry -p perry-runtime
  • PERRY_BIN=target/debug/perry tests/test_symlinked_entry_imported_class.sh
  • PERRY_BIN=target/debug/perry tests/test_xmod_imported_zero_arg_super.sh
  • PERRY_BIN=target/debug/perry tests/test_xmod_empty_imported_derived_arg_forward.sh
  • Integration smoke with the patched local Perry CLI reported imported-super, two-argument min/max, and public prototype compatibility as passed with no failed checks.

Summary by CodeRabbit

  • Bug Fixes
    • Improved JavaScript/TypeScript module resolution for relative and absolute imports and re-exports, including correct behavior when the entry path is reached via symbolic links.
    • Ensures the dependency set is collected correctly and prevents duplicate/mismatched module targets in symlinked setups.
  • Tests
    • Added Unix regression tests for symlinked entry scenarios to catch relative-import resolution and runtime class import behavior regressions.
  • Documentation
    • Updated the Perry API reference to include commander.args as an instance method.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60ba71a7-8524-41fe-b3f0-ab60dae7dcb0

📥 Commits

Reviewing files that changed from the base of the PR and between c3fe7d1 and 8b2954d.

📒 Files selected for processing (2)
  • crates/perry-api-manifest/src/entries.rs
  • docs/src/api/reference.md
✅ Files skipped from review due to trivial changes (1)
  • docs/src/api/reference.md

📝 Walkthrough

Walkthrough

Adds symlink-safe relative import resolution to perry's compiler by introducing lexical path normalization and dual-path (source and canonical) resolution infrastructure. resolve.rs gains normalize_path_lexically and new helpers resolve_relative_import_paths and resolve_absolute_import_paths that return both resolved and canonical paths. collect_modules.rs refactors to use these helpers and introduces cached_resolve_import_with_lexical_base to resolve imports relative to a lexical importer path with canonical fallback. Enqueue targets switch from canonical to source-visible paths. Includes unit tests, shell regression test, and a small API manifest update for commander.args.

Changes

Symlink-safe import resolution

Layer / File(s) Summary
Path resolution contracts and normalization
crates/perry/src/commands/compile/resolve.rs
Adds Component import, introduces ResolvedPath struct carrying both source and canonical paths, implements resolve_relative_import_paths with lexical-then-fallback strategy (normalize path lexically, try extension resolution on normalized form, optionally retry on original), implements resolve_absolute_import_paths, implements normalize_path_lexically for component-wise ./.. collapse without filesystem access, and refactors resolve_relative_import_path to delegate to the new dual-path helpers.
Path normalization unit tests
crates/perry/src/commands/compile/resolve/tests.rs
Adds lexical_path_tests module with unit tests for normalize_path_lexically behavior, verifying preservation of leading .. segments and correct collapsing of normal path components.
Collection logic refactoring
crates/perry/src/commands/compile/collect_modules.rs
Adds Path import, removes old manual parent-joining logic, refactors collect_js_module_imports to use resolve_relative_import_paths and resolve_absolute_import_paths for dual-path resolution, introduces cached_resolve_import_with_lexical_base helper attempting lexical importer resolution with canonical fallback, changes transitive JS-import collection base from module canonical path to entry_path, and switches all import and re-export resolution call sites to the new helper with enqueue targets changed from canonical to source_path.
Rust integration test
crates/perry/src/commands/compile/collect_modules/tests.rs
Adds collect_js_module_imports import, introduces Unix-only test symlinked_entry_resolves_relative_imports_from_lexical_path that creates real and symlinked directory parents with dual dep.ts implementations, runs collect_modules on a symlinked entry importing via relative path, and asserts canonical-path dependency collection while excluding the decoy.
Shell regression test
tests/test_symlinked_entry_imported_class.sh
Adds end-to-end bash test that locates the perry binary, creates real and symlinked directory trees with dual dep.ts implementations distinguishing symlink-visible from canonical path resolution, compiles a TypeScript entry from the symlinked path with --no-cache --no-auto-optimize, executes the binary, and validates exact stdout for class field type, method signature, marker function result, and instanceof check.
API manifest and documentation updates
crates/perry-api-manifest/src/entries.rs, docs/src/api/reference.md
Adds commander.args entry to API_MANIFEST as a native-method dispatch with has_receiver: true, modeling program.args reads as a 0-arg instance getter. Updates auto-generated API reference to include args in commander module's Methods list and increments manifest entry count from 2804 to 2805.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PerryTS/perry#5151: Modifies resolve_relative_import_path in the same file (resolve.rs) to recognize bare "." and ".." as relative specifiers, overlapping with the relative-specifier classification and function changes modified by this PR.

Poem

🐇 A symlink led me down a path so twisted and long,
With .. jumping over aliases—surely that was wrong!
Now lexical normalization keeps the directory true,
While source_path and canonical_path see the journey through.
Hops through alias, hops through real—the module graph stays strong! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive Description covers summary, root cause analysis, and verification steps, but is missing the standard PR template structure (Changes as bullet list, Related issue field, Test plan checklist). Restructure to follow the repository template: add a bullet-point Changes section listing file modifications, specify Related issue, and complete the Test plan checklist to confirm all verification steps were performed.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely describes the main fix: symlinked relative import resolution, matching the core issue addressed across multiple files in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@andrewtdiz andrewtdiz marked this pull request as ready for review June 15, 2026 15:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
crates/perry/src/commands/compile/collect_modules/tests.rs (1)

155-180: ⚡ Quick win

Extend the regression to cover canonical-sibling collisions and transitive imports.

This fixture only proves the fallback works when the canonical sibling is missing. Add a decoy under real-parent/outside/dep.ts and route through app/child.ts -> ../outside/dep so the test catches both wrong “normal-first” resolution and loss of lexical base after the first queued child.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/collect_modules/tests.rs` around lines 155
- 180, The test fixture currently only verifies the fallback when the canonical
sibling is missing. Extend it to also test canonical-sibling collisions and
transitive imports by adding: a decoy module at real-parent/outside/dep.ts (with
similar or conflicting content), an intermediate file app/child.ts that imports
from ../outside/dep, and then update the entry.ts file to import through
app/child.ts instead of directly importing from ../outside/dep. This will ensure
the test catches both wrong "normal-first" resolution behavior and any loss of
lexical base tracking after the first queued child module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry/src/commands/compile/collect_modules.rs`:
- Around line 157-173: The function cached_resolve_import_with_lexical_base
resolves imports correctly using the lexical path but only returns the canonical
path, causing downstream modules queued from that canonical path to resolve
their own imports incorrectly when symlinks are involved. Modify the function to
return both the canonical path and the lexical path (for example, as a tuple or
structured pair), where the canonical path is used for tracking visited modules
and the lexical path is used when queuing child modules for processing via
WorkFrame::Enter. Update all callers of cached_resolve_import_with_lexical_base
to handle the returned pair appropriately. Additionally, add a regression test
that verifies symlinked modules correctly resolve transitive imports from the
lexical path, such as a scenario where alias-parent/app/child.ts imports
../outside/dep and a decoy real-parent/outside/dep.ts exists, ensuring the
traversal uses the lexical path and not the canonical path too early.
- Around line 138-147: The guard condition at the start of this block (checking
if spec starts_with "./", "../", or "/") is incomplete and doesn't account for
bare relative specifiers like "." and ".." that the new resolver can handle.
These patterns are being skipped when they should be processed for transitive
dependencies. Replace the manual string-matching guard with a shared
relative-specifier predicate function that properly identifies all relative
import patterns including ".", "..", "./", "../", and absolute paths, ensuring
that specs like "." and ".." are processed by the resolve_relative_import_path
or resolve_with_extensions calls rather than being skipped.

In `@crates/perry/src/commands/compile/resolve.rs`:
- Around line 902-913: The import resolution order is backwards in the fallback
logic. Currently, resolve_with_extensions is called first on the
filesystem-resolved path, then falls back to the lexically normalized path. This
causes symlinked paths to be resolved before considering the source-visible path
written by the programmer. Swap the order of resolution attempts: first try
resolve_with_extensions on the lexical path (created by
normalize_path_lexically), and only if that returns None should it fall back to
resolve_with_extensions on the original resolved path.
- Around line 922-925: The Component::ParentDir handling in the
normalize_path_lexically function (or similar path normalization logic)
incorrectly pops all components including leading `..` segments. Fix this by
checking whether the last component in the normalized path is an actual
directory segment before popping it. Only pop if the last component is a normal
directory (not ParentDir or CurDir); otherwise, push the ParentDir component to
preserve leading `..` sequences. Additionally, add the two provided test cases
to verify that `../../dep` normalizes to `../../dep` and `app/../../dep`
normalizes to `../dep`.

---

Nitpick comments:
In `@crates/perry/src/commands/compile/collect_modules/tests.rs`:
- Around line 155-180: The test fixture currently only verifies the fallback
when the canonical sibling is missing. Extend it to also test canonical-sibling
collisions and transitive imports by adding: a decoy module at
real-parent/outside/dep.ts (with similar or conflicting content), an
intermediate file app/child.ts that imports from ../outside/dep, and then update
the entry.ts file to import through app/child.ts instead of directly importing
from ../outside/dep. This will ensure the test catches both wrong "normal-first"
resolution behavior and any loss of lexical base tracking after the first queued
child module.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c3949f47-4f34-4bf9-b38e-3de27a1ad678

📥 Commits

Reviewing files that changed from the base of the PR and between a1d9ce6 and 75547ad.

📒 Files selected for processing (4)
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/collect_modules/tests.rs
  • crates/perry/src/commands/compile/resolve.rs
  • tests/test_symlinked_entry_imported_class.sh

Comment thread crates/perry/src/commands/compile/collect_modules.rs Outdated
Comment thread crates/perry/src/commands/compile/collect_modules.rs
Comment thread crates/perry/src/commands/compile/resolve.rs Outdated
Comment thread crates/perry/src/commands/compile/resolve.rs
@proggeramlug proggeramlug merged commit 3790850 into PerryTS:main Jun 15, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants