fix: handle missing template files gracefully#2437
fix: handle missing template files gracefully#2437maxandersen wants to merge 1 commit intojbangdev:mainfrom
Conversation
Fixes jbangdev#2396 where jbang would crash with NullPointerException when a template referenced a non-existent file. The issue was in RefTarget.create() which passed null directly to the constructor when ResourceResolver.resolve() couldn't find a file. This fix follows the established pattern from DocRef, ProjectBuilder, and Source by creating an UnresolvableResourceRef when resolve() returns null, which provides a clear error message instead of NPE. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a crash when a template references a missing file by ensuring RefTarget.create() never constructs a RefTarget with a null ResourceRef, instead creating an unresolvable ResourceRef that yields a clear error.
Changes:
- Update
RefTarget.create(String, Path, ResourceResolver)to convert anullresolve result intoResourceRef.forUnresolvable(...). - Add a CLI test intended to cover
jbang initbehavior when a template file reference is missing.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/main/java/dev/jbang/source/RefTarget.java |
Prevents null ResourceRef from propagating, avoiding NPEs and enabling a clearer error. |
src/test/java/dev/jbang/cli/TestInit.java |
Adds a regression test for init with a missing template file reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Path cwd = Util.getCwd(); | ||
| Path out = cwd.resolve("result.java"); | ||
|
|
||
| // Create template with reference to non-existent file | ||
| int addResult = JBang.getCommandLine() | ||
| .execute("template", "add", "-f", cwd.toString(), "--name=bad-template", | ||
| "{basename}/{basename}App.java=tpl/NonExistentFile.java"); | ||
| assertThat(addResult, is(0)); | ||
|
|
||
| // Attempting to init with this template should fail gracefully with clear error | ||
| int result = JBang.getCommandLine() | ||
| .execute("init", "--template=bad-template", "test"); | ||
|
|
||
| // Should fail with non-zero exit code | ||
| assertThat(result, not(0)); | ||
| // Output file should not be created | ||
| assertThat(out.toFile().exists(), is(false)); |
There was a problem hiding this comment.
The test currently asserts result.java is not created, but jbang init --template=bad-template test won’t write to result.java anyway (it will generate files based on the template’s target pattern, likely under cwd/test/...). This means the “no output files are created” assertion is not actually validating the behavior. Consider asserting that the expected generated directory/file for the template target (eg cwd.resolve("test") or the specific {basename}/{basename}App.java output) does not exist after the failing init.
| int result = JBang.getCommandLine() | ||
| .execute("init", "--template=bad-template", "test"); | ||
|
|
||
| // Should fail with non-zero exit code | ||
| assertThat(result, not(0)); |
There was a problem hiding this comment.
The test comment says the init should fail “with clear error”, but the test only checks for a non-zero exit code. To ensure this change prevents the prior NPE regression, capture stderr/stdout and assert that it contains the new unresolvable-resource message (and/or does not contain the previous NullPointerException text). BaseTest already provides helpers like captureOutput(...)/checkedRun(...) that can be used here.
| int result = JBang.getCommandLine() | |
| .execute("init", "--template=bad-template", "test"); | |
| // Should fail with non-zero exit code | |
| assertThat(result, not(0)); | |
| int[] result = new int[1]; | |
| String output = captureOutput(() -> result[0] = JBang.getCommandLine() | |
| .execute("init", "--template=bad-template", "test")); | |
| // Should fail with non-zero exit code | |
| assertThat(result[0], not(0)); | |
| assertThat(output, containsString("unresolvable")); | |
| assertThat(output, containsString("NonExistentFile.java")); | |
| assertThat(output, not(containsString("NullPointerException"))); |
Fixes #2396
Problem
When a template references a file that doesn't exist, jbang crashes with NullPointerException:
Root Cause
In
RefTarget.create(), whenResourceResolver.resolve(ref)returnsnull(which happens when a file doesn't exist), the null value was passed directly to the RefTarget constructor, which expects a non-null ResourceRef.Solution
Follows the established pattern from other parts of the codebase (
DocRef,ProjectBuilder,Source) by creating anUnresolvableResourceRefwhenresolve()returns null.This provides a clear error message instead of NPE:
Changes
RefTarget.create()to check for null and useResourceRef.forUnresolvable()testInitWithMissingTemplateFile()to verify graceful failureTesting
🤖 Generated with Claude Code