From d35b9ed4c2573c9b1a74239deaa422fb5ff40bbf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 12:18:21 +0000 Subject: [PATCH 1/4] #841: add failing tests reproducing NPE when +unlint range references absent lint A range-pattern +unlint (e.g. name:1-5) whose lint never fired causes a NullPointerException in DefectMissing because this.defects.get(name) returns null and no null-check is performed before lines.stream(). These tests cover both the unit level (DefectMissing) and the integration level (LtUnlintNonExistingDefect) paths and currently fail with NPE. --- .../org/eolang/lints/DefectMissingTest.java | 19 ++++++++++++++++ .../lints/LtUnlintNonExistingDefectTest.java | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/test/java/org/eolang/lints/DefectMissingTest.java b/src/test/java/org/eolang/lints/DefectMissingTest.java index 25f89784f..d531ec65a 100644 --- a/src/test/java/org/eolang/lints/DefectMissingTest.java +++ b/src/test/java/org/eolang/lints/DefectMissingTest.java @@ -80,4 +80,23 @@ void returnsTrueIfSomeLineIsOutOfRange() { Matchers.equalTo(true) ); } + + @Test + void returnsTrueWhenRangeReferencesAbsentLint() { + MatcherAssert.assertThat( + "Defect should be missing when range pattern references an absent lint", + new DefectMissing(new MapOf<>(), new ListOf<>()).apply("ascii-only:1-5"), + Matchers.equalTo(true) + ); + } + + @Test + void returnsFalseWhenRangeReferencesExcludedAbsentLint() { + MatcherAssert.assertThat( + "Defect should not be missing when the absent lint is excluded", + new DefectMissing(new MapOf<>(), new ListOf<>("ascii-only")) + .apply("ascii-only:1-5"), + Matchers.equalTo(false) + ); + } } diff --git a/src/test/java/org/eolang/lints/LtUnlintNonExistingDefectTest.java b/src/test/java/org/eolang/lints/LtUnlintNonExistingDefectTest.java index eca2a1deb..ef8853f3f 100644 --- a/src/test/java/org/eolang/lints/LtUnlintNonExistingDefectTest.java +++ b/src/test/java/org/eolang/lints/LtUnlintNonExistingDefectTest.java @@ -222,4 +222,26 @@ void catchesUnlintWithOutOfRangeLines() throws IOException { Matchers.iterableWithSize(1) ); } + + @Test + void catchesUnlintWithRangeForAbsentLint() throws IOException { + MatcherAssert.assertThat( + "Non-existing unlint with range should be reported, not crash with NPE", + new LtUnlintNonExistingDefect( + new ListOf<>(new LtAsciiOnly()), + new ListOf<>() + ).defects( + new EoSyntax( + String.join( + "\n", + "+unlint ascii-only:1-5", + "[] > main", + " QQ.io.stdout > @", + " \"Hello\"" + ) + ).parsed() + ), + Matchers.iterableWithSize(1) + ); + } } From 817ec18f56203ad4b6742acf368d8a1ee898ab15 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 12:22:13 +0000 Subject: [PATCH 2/4] #841: null-check absent lint in DefectMissing DefectMissing.apply threw NullPointerException when +unlint used the range pattern name:line-line for a lint whose defects never fired. In that case this.defects.get(name) returns null and the range branch dereferenced it without a guard. When lines is null, treat the defect as missing unless the name is in the excluded set, matching the behaviour of the non-range branch for the same situation. Also defends the single-line branch against a null lines list for consistency. --- src/main/java/org/eolang/lints/DefectMissing.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/eolang/lints/DefectMissing.java b/src/main/java/org/eolang/lints/DefectMissing.java index c6c2ed17a..a3d681e35 100644 --- a/src/main/java/org/eolang/lints/DefectMissing.java +++ b/src/main/java/org/eolang/lints/DefectMissing.java @@ -44,7 +44,11 @@ public Boolean apply(final String unlint) { final String name = split[0]; final List lines = this.defects.get(name); if (unlint.matches(String.format("%s:\\d+-\\d+", name))) { - missing = !lines.stream().allMatch(new UnlintInRange(unlint)); + if (lines == null) { + missing = !this.excluded.contains(name); + } else { + missing = !lines.stream().allMatch(new UnlintInRange(unlint)); + } } else { final Set names; if (this.defects != null) { @@ -53,7 +57,9 @@ public Boolean apply(final String unlint) { names = new SetOf<>(); } if (split.length > 1) { - missing = (!names.contains(name) || !lines.contains(Integer.parseInt(split[1]))) + missing = (!names.contains(name) + || lines == null + || !lines.contains(Integer.parseInt(split[1]))) && !this.excluded.contains(name); } else { missing = !names.contains(name) && !this.excluded.contains(name); From a744074c96e242d136b1f2d92e08bef7cf4741b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 14:33:28 +0000 Subject: [PATCH 3/4] #841: adapt reserved profile to new objectionary/home layout objectionary/home restructured on 2026-04-20 so that .eo files now live under objects/ directly (e.g. objects/io/stdout.eo) instead of the old objects/org/eolang/... nesting, and object paths no longer use the org.eolang. prefix. - HomeNames.placeCsv: widen the source filter to include any .eo file under objects/, not only under objects/org/eolang/. - Tests: update the path-format expectations to drop the org.eolang. prefix (e.g. stdout is now reserved by "io.stdout.eo" rather than "org.eolang.io.stdout.eo"). --- src/main/groovy/org/eolang/lints/HomeNames.groovy | 2 -- src/test/groovy/org/eolang/lints/HomeNamesTest.groovy | 2 +- src/test/java/org/eolang/lints/LtReservedNameTest.java | 2 +- src/test/java/org/eolang/lints/ReservedNamesTest.java | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/groovy/org/eolang/lints/HomeNames.groovy b/src/main/groovy/org/eolang/lints/HomeNames.groovy index c3052deca..f42c9aef3 100644 --- a/src/main/groovy/org/eolang/lints/HomeNames.groovy +++ b/src/main/groovy/org/eolang/lints/HomeNames.groovy @@ -118,8 +118,6 @@ final class HomeNames { && file.contains( Path.of(this.location) .resolve("objects") - .resolve("org") - .resolve("eolang") .toString().replace("\\", "/") ) } diff --git a/src/test/groovy/org/eolang/lints/HomeNamesTest.groovy b/src/test/groovy/org/eolang/lints/HomeNamesTest.groovy index f3ca22f96..b12c26f04 100644 --- a/src/test/groovy/org/eolang/lints/HomeNamesTest.groovy +++ b/src/test/groovy/org/eolang/lints/HomeNamesTest.groovy @@ -48,7 +48,7 @@ final class HomeNamesTest { Matchers.everyItem( Matchers.hasToString( Matchers.matchesRegex( - "org\\.eolang(?:\\.[a-zA-Z_][a-zA-Z0-9_-]*)+\\.eo" + "(?:[a-zA-Z_][a-zA-Z0-9_-]*\\.)*[a-zA-Z_][a-zA-Z0-9_-]*\\.eo" ) ) ) diff --git a/src/test/java/org/eolang/lints/LtReservedNameTest.java b/src/test/java/org/eolang/lints/LtReservedNameTest.java index da37c9ba4..e4fbe5441 100644 --- a/src/test/java/org/eolang/lints/LtReservedNameTest.java +++ b/src/test/java/org/eolang/lints/LtReservedNameTest.java @@ -207,7 +207,7 @@ void scansReservedFromHomeWithCorrectMessage() throws Exception { ) ).get(0).text(), Matchers.equalTo( - "Object name \"stdout\" is already reserved by object in the \"org.eolang.io.stdout.eo\"" + "Object name \"stdout\" is already reserved by object in the \"io.stdout.eo\"" ) ); } diff --git a/src/test/java/org/eolang/lints/ReservedNamesTest.java b/src/test/java/org/eolang/lints/ReservedNamesTest.java index b1bfa91bd..d63edd713 100644 --- a/src/test/java/org/eolang/lints/ReservedNamesTest.java +++ b/src/test/java/org/eolang/lints/ReservedNamesTest.java @@ -35,7 +35,7 @@ void readsReservedInCorrectFormat() { Matchers.everyItem( Matchers.hasToString( Matchers.matchesRegex( - "org\\.eolang(?:\\.[a-zA-Z_][a-zA-Z0-9_-]*)+\\.eo" + "(?:[a-zA-Z_][a-zA-Z0-9_-]*\\.)*[a-zA-Z_][a-zA-Z0-9_-]*\\.eo" ) ) ) From a8e01f383f71f443ba033f66921e4929f4e9f00a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 15:00:15 +0000 Subject: [PATCH 4/4] #841: drop org.eolang. prefix from LtReservedName unit fixtures The objectionary/home restructure dropped the org.eolang. prefix from object paths; the hand-crafted MapOf<> fixtures in LtReservedNameTest still carried the old "org.eolang..eo" strings and the expected defect message in reportsCorrectMessageForReservedNameInTopObject was hard-coded with the same prefix. Update them to match the new layout so the fixtures reflect what the live scan actually produces. --- .../org/eolang/lints/LtReservedNameTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/eolang/lints/LtReservedNameTest.java b/src/test/java/org/eolang/lints/LtReservedNameTest.java index e4fbe5441..ae72daca5 100644 --- a/src/test/java/org/eolang/lints/LtReservedNameTest.java +++ b/src/test/java/org/eolang/lints/LtReservedNameTest.java @@ -26,7 +26,7 @@ final class LtReservedNameTest { void catchesReservedName() throws IOException { MatcherAssert.assertThat( "It is expected to catch only one defect here", - new LtReservedName(new MapOf<>("true", "org.eolang.true.eo")) + new LtReservedName(new MapOf<>("true", "true.eo")) .defects( new EoSyntax( String.join( @@ -45,7 +45,7 @@ void catchesReservedName() throws IOException { void allowsNonReservedName() throws IOException { MatcherAssert.assertThat( "Defects are not empty, but they should", - new LtReservedName(new MapOf<>("true", "org.eolang.true.eo")) + new LtReservedName(new MapOf<>("true", "true.eo")) .defects( new EoSyntax( String.join( @@ -63,7 +63,7 @@ void allowsNonReservedName() throws IOException { void allowsUniqueNameInTopSourceObject() throws IOException { MatcherAssert.assertThat( "Defects are not empty, but they should", - new LtReservedName(new MapOf<>("f", "org.eolang.f.eo")) + new LtReservedName(new MapOf<>("f", "f.eo")) .defects( new EoSyntax( String.join( @@ -80,7 +80,7 @@ void allowsUniqueNameInTopSourceObject() throws IOException { @Test void catchesReservedNameWithPackage() throws IOException { final Collection found = new LtReservedName( - new MapOf<>("stdout", "org.eolang.stdout.eo") + new MapOf<>("stdout", "stdout.eo") ).defects( new EoSyntax( String.join( @@ -109,8 +109,8 @@ void reportsMultipleNames() throws IOException { "Defects size does not match with expected", new LtReservedName( new MapOf( - new MapEntry<>("ja", "org.eolang.ja.eo"), - new MapEntry<>("spb", "org.eolang.spb.eo") + new MapEntry<>("ja", "ja.eo"), + new MapEntry<>("spb", "spb.eo") ) ).defects( new EoSyntax( @@ -131,7 +131,7 @@ void reportsReservedNameInTopObject() throws IOException { MatcherAssert.assertThat( "Defects size does not match with expected", new LtReservedName( - new MapOf<>("foo", "org.eolang.foo.eo") + new MapOf<>("foo", "foo.eo") ).defects( new EoSyntax( String.join( @@ -152,7 +152,7 @@ void reportsCorrectMessageForReservedNameInTopObject() throws IOException { "The name of high-level object 'foo' should be reported", new ListOf<>( new LtReservedName( - new MapOf<>("foo", "org.eolang.foo.eo") + new MapOf<>("foo", "foo.eo") ).defects( new EoSyntax( String.join( @@ -165,7 +165,7 @@ void reportsCorrectMessageForReservedNameInTopObject() throws IOException { ) ).get(0).text(), Matchers.equalTo( - "Object name \"foo\" is already reserved by object in the \"org.eolang.foo.eo\"" + "Object name \"foo\" is already reserved by object in the \"foo.eo\"" ) ); }