Getting ZLS to analyze unused/new code

I have ZLS set up build-on-save enabled. It works, but only for code that’s called from main. Any code that isn’t doesn’t have type-checking applied. This is annoying, as it includes any new additions that I’m in the middle of writing. Is there a way to configure the check step in my build.zig to pick up type errors in unused code?

1 Like

Hi @nsaritzky, welcome to Ziggit!

This doesn’t really have to do with ZLS and has to do with how Zig does lazy compilation. Zig does not evaluate all code, only code used from main.

It sounds like you are used to developing bottom up. Write the function first, then use it. This is how I used to do it, still do. However, this is incompatible with Zig and how Zig type checks.

Instead, the best approach is top down. Write the usage of the function, then write the contents of the function. It takes some getting used to, as most other compilers/interpreters/LSPs will evaluate all contents of all files. So it is understandable that you are running into this frustration.

That being said, one could make the case the that the LSP for Zig should type check the whole file. That would require a non-trivial change and decoupling from the compiler. Since ZLS is community driven and has limited resources, the rely heavily on the compiler infrastructure, and so they inherit the details of the compiler.
The counter argument for this is that if the LSP type checks it, but the compiler doesn’t use it, do you get surprises from what your editor reports vs. what the compiler includes.

2 Likes

As a workaround you could add a call to your function in a test and let the check step depend on the test of that module.

E.g. assuming the template that zig init creates, and in root.zig you add a function that is not referenced by main together with a test like this

// root.zig
fn dead_code(a: i32) f32 {
    const b: f32 = a; //oops
    return b;
}

test "basic test" {
    _ = dead_code(31);
}

In build.zig you can then reference the test executable like so

// build.zig

// this is creating the root module and the corresponding test executable. 
// In the template this code is already generated
   const mod = b.addModule("foo", .{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
    });
    const mod_tests = b.addTest(.{
        .root_module = mod,
    });

// ... usual code for creating the check step
    
// this is the actual line that will add the test executable to the dependencies of your check step
    check.dependOn(&mod_tests.step);



Now, ZLS should show the type error in dead_code.

Now the function does not need to be referenced by main anymore. If it helps with your workflow is another question :slight_smile:

3 Likes

Thanks, this actually gives me what I wanted. Inside the root test I can use std.testing.refAllDecls(@This()). Then the check step can see all the code without, I think, having to actually run any tests.

1 Like

Yes, for the tests also to be run, you would have to add the run_mod_tests/ run_exe_testsstep as a dependency instead. I tried that and in principle it seems to work. However, ZLS does not show anything for failed tests, the parsing logic is probably just not there. Actually, it could be a nice feature to be able to run tests and highlight/jump to failed tests. I dont know anything about LSPs, so no clue how feasible that would be

That sounds like more of an editor feature than an LSP feature. I don’t think I’ve seen any other LSP servers execute tests. Tests can take time to execute, and “immediately on save” is usually about as slow as you want diagnostics to show up.

Before I got the ZLS working with build-on-save, I managed to get basically the same functionality in Emacs by having flycheck run and parse zig build check. I’m sure one could do something similar for test failures.

(flycheck-define-checker zig-build-check
  "Run zig build check to detect compilation errors."
  :command ("zig" "build" "check")
  :error-filter flycheck-sanitize-errors
  :error-patterns
  ((error line-start (file-name) ":" line ":" column ": error: " (message) line-end))
  :modes zig-mode
  :working-directory (lambda (_) (locate-dominating-file "." "build.zig"))
  :predicate (lambda ()
               (file-exists-p "build.zig")))

I remember experiencing ZLS correctly pointing me to the precise line where the test failed at runtime, once I configured it to run the test step on save. Maybe that feature regressed (I haven’t done this in a while), but at the very least it was there in the past.

Ah nice, good to know! But yes, then it must have regressed. I just checked again with version 0.16.0.