Skip to content

Fix relative import resolution #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

gcv
Copy link

@gcv gcv commented Aug 9, 2025

This PR improves name resolution for relative using/import with leading dots (., .., ...) and forward references (imports that appear before the target module is defined in the same file tree). It also adds a diagnostic when the number of leading dots exceeds the available module nesting.

Today, LanguageServer/StaticLint resolves relative imports only when the target module is already bound, and often treats top-level file scopes as “parents”, which makes sibling imports fragile. Common project layouts (one wrapper file including multiple modules) hit:

  • using ..Sibling before Sibling is defined → unresolved references in the importing module
  • import ..Another (no selectors) → module name “Another” is not bound locally, so qualified calls (Another.f()) don’t resolve
  • leading dots that exceed the root silently do nothing (no diagnostic)

This PR implements:

  • Late retry (forward-relative imports): when a relative segment can’t be resolved yet, schedule the import expression and its enclosing module for the existing ResolveOnly pass; in that late pass, re-run import resolution before resolving references. This lets sibling/grandparent modules that are defined later in the file tree become visible for references in the importing module.
  • Resolve from module Scope: allow resolving a module’s own name when iterating scope.modules that contain a module Scope (not only ModuleStore), so identifiers like Sibling bind to the module in analysis.
  • Bind bare import names: plain import Module (no selectors) now binds the module’s identifier in the current scope (matches Julia semantics), so qualified calls like Another.f() resolve without needing selectors.
  • New lint code RelativeImportTooManyDots with message: “Relative import has more leading dots than available module nesting.”

Implementation details

  • imports.jl:
    • resolve_import_block: walk leading dots up from state.scope; set RelativeImportTooManyDots when exceeding the root; when a segment is unresolved (cand === nothing), push the import expr and enclosing module expr into state.resolveonly; return to retry later.
    • resolve_import: selector-head failure (root2 === nothing) also schedules a retry.
    • _mark_import_arg: when usinged == false (plain import), bind the imported module name into scope.names; using keeps existing behavior (add module to scope.modules).
    • add_to_imported_modules: ensure scope.modules initialized when first used.
  • references.jl:
    • resolve_ref_from_module(::Scope): resolve the module’s own name (bindingof(scope.expr)) and exported names via scope_exports.
  • StaticLint.jl:
    • ResolveOnly: call resolve_import(x, state) before resolve_ref/traverse (idempotent; only updates unresolved imports).
  • linting/checks.jl:
    • add RelativeImportTooManyDots to LintCodes and descriptions.

Tests

  • Forward relative using/import: module C using ..Sibling binds Sibling; calls like Sibling.g() resolve even if Sibling is defined later in B.
  • Bare import binds name: import ..Another makes Another visible, so Another.f() resolves.
  • “Too many dots” lint: import ....X triggers RelativeImportTooManyDots.

Compatibility and performance

  • No breaking changes; behavior now matches Julia semantics more closely.
  • Work is gated: late import resolution only runs in the ResolveOnly pass (which exists today).
  • Minimal additional cost; only schedules retries when an import segment can’t be resolved yet.

- Schedule unresolved relative using/import (., .., …) for a late retry:
  push the import expr and its enclosing module into state.resolveonly.
- In ResolveOnly, run resolve_import(x, state) before resolve_ref/traverse.
- Resolve module identifiers from module Scope:
  resolve_ref_from_module(::Scope) now binds the module’s own name.
- Bind names for bare import (no selectors):
  _mark_import_arg adds the imported module name to scope.names.
- Initialize scope.modules when needed in add_to_imported_modules.
- Add LintCodes.RelativeImportTooManyDots and surface a diagnostic when
  leading dots exceed available module nesting.

Tests:
- Forward relative using/import resolves across sibling/grandparent modules.
- Bare import binds module name (Another.f() resolves).
- “Too many dots” triggers RelativeImportTooManyDots.
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.

1 participant