Skip to content

Diagnose references through a use path that resolves nowhere - #92

Merged
henrygarner merged 2 commits into
juxt:mainfrom
yenda:fix/unresolved-import-references
Aug 26, 2026
Merged

Diagnose references through a use path that resolves nowhere#92
henrygarner merged 2 commits into
juxt:mainfrom
yenda:fix/unresolved-import-references

Conversation

@yenda

@yenda yenda commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #87.

Bug

When a use path does not resolve, references through the alias produce no diagnostic of any kind — the single allium.use.unresolvedPath warning on the use line is the only signal. That warning is suppressible with one line-scoped allium-ignore, after which a spec referencing completely made-up names through the dead alias checks clean with exit 0:

-- allium-ignore allium.use.unresolvedPath
use "./does-not-exist.allium" as p

rule R {
    when: Go(x)
    requires: p/CompletelyMadeUpName(x)   -- no diagnostic anywhere, exit 0
    ...
}

Three asymmetries make this a soundness hole rather than a UX choice:

  1. Deleting the import is louder than breaking it. An alias with no use statement errors on every reference (undefinedImportedAlias); an alias whose use exists but resolves nowhere produces nothing per reference.
  2. The suppression's blast radius is invisible. The directive reads as acknowledging one cosmetic warning; its actual effect is alias-wide vacuity with no residual signal.
  3. It defeats the cross-module checks' purpose — a rename or deletion of the imported file goes green, which is exactly the scenario the reference checks exist to catch.

Users add the suppression for a legitimate-looking reason: single-file checks of an importing spec always warn unresolvedPath (the sibling is not in the 1-file check set), so the directive keeps that output clean — while silently blinding the multi-file gate.

Fix

The multi-file layer now distinguishes a broken import from an out-of-set one:

  • Broken — the path resolves neither in the check set nor on disk. Every reference through its alias draws a new warning, allium.reference.unresolvedImport ("Reference 'p/Name' goes through use path "./does-not-exist.allium", which does not resolve to a file in the check set or on disk."), independent of the use-line suppression (which is line-scoped and cannot reach the reference lines).
  • Out-of-set — the file exists on disk but is not part of a narrower check set (the single-file-check-of-a-pair-member case, editors included). References stay unknowable and silent, exactly as before. The use-line unresolvedPath warning is unchanged in both cases.

So the only behaviour change is for imports that are genuinely dead — precisely the bug — and single-file workflows are untouched. Callers without filesystem context (the wasm crate, legacy single-file analyze) pass nothing and behave as before.

analyze_with_cross_module / analyse_with_cross_module gain a trailing missing_use_paths: &HashSet<String> parameter (workspace-internal API).

Tests

  • Integration: the suppressed-broken-import repro now exits 1 with unresolvedImport naming both the reference and the path (was exit 0 with zero diagnostics); an unsuppressed broken import reports both codes; an on-disk-but-out-of-set target draws unresolvedPath only — references stay silent (pins the single-file workflow).
  • A registry-coordinate import (use "github.com/specs/auth/abc123") draws no unresolvedImport — coordinates are immutable remote references not expected on disk, so only local file references (paths ending .allium) participate in the broken classification. Found by soaking this repo's own parser fixtures, which use exactly that form.
  • Full workspace suite green: 650 tests, 0 failures.

Real-world impact

A ~180-spec production corpus carries 8 of these suppressions (added for single-file ergonomics, annotated "Resolves in a multi-file check set"). ~37 cross-module references flow through the blinded aliases, and for two of the imported modules every importer suppresses — verified there: renaming one of those files leaves the whole-tree gate green today. With this fix the rename goes loud regardless of the directives.

yenda and others added 2 commits August 13, 2026 10:05
When a use path did not resolve, references through its alias produced no
diagnostic of any kind: the single unresolvedPath warning on the use line was
the only signal, and one line-scoped allium-ignore silenced it — after which
a spec referencing completely made-up names through the dead alias checked
clean, exit 0. Deleting the use line entirely was louder (an error per
reference) than breaking it.

The multi-file layer now distinguishes a BROKEN import (the path resolves
neither in the check set nor on disk) from an out-of-set one (the file exists
on disk but is not part of a narrower check set, e.g. a single-file check of
one member of a pair). References through a broken alias each draw a new
allium.reference.unresolvedImport warning, independent of the use-line
suppression; references through an out-of-set alias stay unknowable and
silent, exactly as before, so single-file workflows are unaffected.

Callers without filesystem context (the wasm crate, legacy single-file
analyze) pass nothing and behave as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Soaking the repo's own parser fixtures surfaced the gap: a registry
coordinate (use "github.com/specs/auth/abc123") is an immutable remote
reference that is not expected on local disk, so the exists() probe
classified it as broken and drew false unresolvedImport warnings. Only local
file references (paths ending .allium) participate in the broken
classification; coordinates stay out-of-set and their references unknowable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@henrygarner
henrygarner merged commit 148e5f7 into juxt:main Aug 26, 2026
2 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.

check: a suppressed allium.use.unresolvedPath silently disables all name checking through that alias (exit 0 on arbitrary dangling references)

2 participants