From fa3221731a951058446c7944760c4a49d7a41b39 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Tue, 14 Jul 2026 21:52:04 +0000 Subject: [PATCH] feat: distinguish uncacheable declared deps from unknown imports An `UnresolvedImport` on a name the project declares as a dependency (`regular_packages` / `stdlib_packages` / `deved_packages`) now reports that the dependency's symbols could not be indexed/cached, rather than the generic "Failed to resolve" used for names that aren't in the environment at all (typo or missing dependency). The consequence clause (wildcard vs explicit) is unchanged, so existing messages for undeclared names are preserved. Co-Authored-By: Claude Opus 4.8 --- src/layer_static_lint.jl | 25 ++++++++++++++++------ test/test_diagnostics.jl | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/layer_static_lint.jl b/src/layer_static_lint.jl index b73e7e9..0234b1d 100644 --- a/src/layer_static_lint.jl +++ b/src/layer_static_lint.jl @@ -193,6 +193,18 @@ Salsa.@derived function derived_static_lint_diagnostics_for_root(rt, root) workspace_packages = lint_result.workspace_packages env = derived_environment(rt, project_uri) + # Names the project declares as dependencies. An unresolved import of one of + # these is a dependency whose symbols could not be indexed/cached (it would + # otherwise be in the env and resolve), as opposed to a name that simply + # isn't in the environment (typo / missing dependency). + project = derived_project(rt, project_uri) + declared_deps = project === nothing ? Set{String}() : + Set{String}(Iterators.flatten(( + keys(project.regular_packages), + keys(project.stdlib_packages), + keys(project.deved_packages), + ))) + for uri in derived_include_closure(rt, root) current_res = get!(res, uri, Set{Diagnostic}()) @@ -209,12 +221,13 @@ Salsa.@derived function derived_static_lint_diagnostics_for_root(rt, root) push!(current_res, Diagnostic(rng, :warning, "Missing reference: $(err[2].val)", nothing, Symbol[], "StaticLint.jl")) elseif StaticLint.haserror(err[2], meta_dict) && StaticLint.errorof(err[2], meta_dict) === StaticLint.UnresolvedImport name = CSTParser.str_value(err[2]) - msg = if StaticLint.is_in_wildcard_import(err[2]) - "Failed to resolve `$name`. Missing-reference checks are disabled in this scope and all nested scopes." - else - "Failed to resolve `$name`. Anything imported through this statement is assumed to exist and will not be checked." - end - push!(current_res, Diagnostic(rng, :warning, msg, nothing, Symbol[], "StaticLint.jl")) + cause = name in declared_deps ? + "`$name` is a declared dependency but its symbols could not be indexed." : + "Failed to resolve `$name`." + consequence = StaticLint.is_in_wildcard_import(err[2]) ? + "Missing-reference checks are disabled in this scope and all nested scopes." : + "Anything imported through this statement is assumed to exist and will not be checked." + push!(current_res, Diagnostic(rng, :warning, "$cause $consequence", nothing, Symbol[], "StaticLint.jl")) elseif StaticLint.haserror(err[2], meta_dict) && StaticLint.errorof(err[2], meta_dict) isa StaticLint.LintCodes code = StaticLint.errorof(err[2], meta_dict) description = get(StaticLint.LintCodeDescriptions, code, "") diff --git a/test/test_diagnostics.jl b/test/test_diagnostics.jl index 42b3e62..c1f44d7 100644 --- a/test/test_diagnostics.jl +++ b/test/test_diagnostics.jl @@ -1428,6 +1428,51 @@ end @test any(d -> startswith(d.message, "Failed to resolve `ActuallyUnresolved`"), diags) end +@testitem "unresolved import: declared-but-uncacheable dep vs unknown name" begin + using JuliaWorkspaces.URIs2: URI + + # `DeclaredButUncached` is listed in the manifest (so it's a declared + # dependency) but no symbols are cached for it, so it never enters the env. + # `TotallyUnknownPkg` is not declared anywhere. + project_toml = """ + name = "UncachedDep" + uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee41" + version = "0.1.0" + + [deps] + DeclaredButUncached = "12345678-1234-1234-1234-123456789abc" + """ + manifest_toml = """ + julia_version = "1.11.0" + manifest_format = "2.0" + project_hash = "abc123" + + [[deps.DeclaredButUncached]] + git-tree-sha1 = "0000000000000000000000000000000000000000" + uuid = "12345678-1234-1234-1234-123456789abc" + version = "1.0.0" + """ + source = """ + module UncachedDep + import DeclaredButUncached: foo + using TotallyUnknownPkg + end + """ + + jw = JuliaWorkspace() + add_file!(jw, TextFile(URI("file:///uncacheddep/Project.toml"), SourceText(project_toml, "toml"))) + add_file!(jw, TextFile(URI("file:///uncacheddep/Manifest.toml"), SourceText(manifest_toml, "toml"))) + add_file!(jw, TextFile(URI("file:///uncacheddep/src/UncachedDep.jl"), SourceText(source, "julia"))) + JuliaWorkspaces.set_input_env_ready!(jw.runtime, true) + + diags = get_diagnostic(jw, URI("file:///uncacheddep/src/UncachedDep.jl")) + + # Declared dependency: message attributes the failure to indexing/caching + @test any(d -> d.message == "`DeclaredButUncached` is a declared dependency but its symbols could not be indexed. Anything imported through this statement is assumed to exist and will not be checked.", diags) + # Undeclared name: keeps the generic "Failed to resolve" wording + @test any(d -> d.message == "Failed to resolve `TotallyUnknownPkg`. Missing-reference checks are disabled in this scope and all nested scopes.", diags) +end + @testitem "missing-refs: default is all (getfield refs checked)" begin using JuliaWorkspaces.URIs2: URI