From 581dcb179f2ebca900e51ed76ecae32e3db685ee Mon Sep 17 00:00:00 2001 From: VasilevNStas Date: Mon, 10 Aug 2026 23:10:34 +0300 Subject: [PATCH 1/3] feat(#20): add mystery-object lint --- src/main/java/org/eolang/lints/LtMystery.java | 124 ++++++++++++++++++ src/main/java/org/eolang/lints/MonoLints.java | 3 +- .../eolang/motives/names/mystery-object.md | 20 +++ .../java/org/eolang/lints/LtMysteryTest.java | 121 +++++++++++++++++ .../org/eolang/lints/aliased-object.eo | 7 + .../org/eolang/lints/canonical-object.eo | 6 + .../org/eolang/lints/declared-object.eo | 7 + .../org/eolang/lints/mystery-bipki.eo | 6 + .../org/eolang/lints/mystery-object.eo | 6 + 9 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/eolang/lints/LtMystery.java create mode 100644 src/main/resources/org/eolang/motives/names/mystery-object.md create mode 100644 src/test/java/org/eolang/lints/LtMysteryTest.java create mode 100644 src/test/resources/org/eolang/lints/aliased-object.eo create mode 100644 src/test/resources/org/eolang/lints/canonical-object.eo create mode 100644 src/test/resources/org/eolang/lints/declared-object.eo create mode 100644 src/test/resources/org/eolang/lints/mystery-bipki.eo create mode 100644 src/test/resources/org/eolang/lints/mystery-object.eo diff --git a/src/main/java/org/eolang/lints/LtMystery.java b/src/main/java/org/eolang/lints/LtMystery.java new file mode 100644 index 000000000..9265f623e --- /dev/null +++ b/src/main/java/org/eolang/lints/LtMystery.java @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import com.github.lombrozo.xnav.Xnav; +import com.jcabi.xml.XML; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Lint to catch mystery objects. + * + *

A mystery object is a free application of an object that is neither + * declared anywhere in the program nor is one of the {@code org.eolang} + * prime objects (like {@code number} or {@code bytes}). It almost always + * means a typo in the name of an object or a missing {@code +alias} meta. + * Such a call is compiled to {@code Φ.NAME} reference, which this lint + * inspects.

+ * + * @since 0.0.52 + */ +final class LtMystery implements Lint { + + /** + * Reserved names. + * The key is object name, the value is the path to EO file. + */ + private final Map reserved; + + /** + * Ctor. + */ + LtMystery() { + this(new ReservedNames()); + } + + /** + * Ctor. + * @param names Reserved names + */ + LtMystery(final Map names) { + this.reserved = names; + } + + @Override + public String name() { + return "mystery-object"; + } + + @Override + public Collection defects(final XML xmir) throws IOException { + final Collection defects; + if (this.reserved.isEmpty()) { + defects = new ArrayList<>(0); + } else { + defects = LtMystery.find(xmir, this.reserved); + } + return defects; + } + + @Override + public String motive() throws IOException { + return new MotiveFrom("names", this.name()).asString(); + } + + @Override + public Fix fix() { + return new FxEmpty(); + } + + /** + * Detect mystery objects in the program. + * @param xmir XMIR document + * @param reserved Reserved org.eolang names + * @return Detected defects + */ + private static Collection find(final XML xmir, + final Map reserved) { + final Xnav xnav = new Xnav(xmir.inner()); + final Set declared = xnav.path("//o[@name]").map( + elem -> elem.attribute("name").text().get() + ).collect(Collectors.toSet()); + return xnav.path("//o[@base]").filter( + elem -> { + final Optional base = elem.attribute("base").text(); + return base.isPresent() + && LtMystery.simple(base.get()); + } + ).filter( + elem -> { + final String base = elem.attribute("base").text().get(); + return !declared.contains(base.substring(2)) + && !reserved.containsKey(base.substring(2)); + } + ).map( + elem -> new Defect.Default( + "mystery-object", + Severity.ERROR, + new LineOf(elem).value(), + String.format( + "Object \"%s\" is not defined in the program and is not part of org.eolang", + elem.attribute("base").text().get().substring(2) + ) + ) + ).collect(Collectors.toList()); + } + + /** + * Check if the base is a reference to a single object, like + * {@code Φ.bar}, and not to a path like {@code Φ.org.eolang.io.stdout}. + * @param base Base attribute value + * @return True if it's a simple object reference + */ + private static boolean simple(final String base) { + return base.matches("^Φ\\.[a-z][a-z0-9_-]*$"); + } +} diff --git a/src/main/java/org/eolang/lints/MonoLints.java b/src/main/java/org/eolang/lints/MonoLints.java index 1ce9c4bb9..246a149db 100644 --- a/src/main/java/org/eolang/lints/MonoLints.java +++ b/src/main/java/org/eolang/lints/MonoLints.java @@ -27,7 +27,8 @@ final class MonoLints extends IterableEnvelope { List.of( new LtAsciiOnly(), new LtReservedName(), - new LtSyntaxVersion() + new LtSyntaxVersion(), + new LtMystery() ) ) ); diff --git a/src/main/resources/org/eolang/motives/names/mystery-object.md b/src/main/resources/org/eolang/motives/names/mystery-object.md new file mode 100644 index 000000000..5a18106cf --- /dev/null +++ b/src/main/resources/org/eolang/motives/names/mystery-object.md @@ -0,0 +1,20 @@ +# Mystery object + +Objects, applied in the code, must be defined somewhere in the program, +be the objects of `org.eolang.*` or be imported via `+alias` meta. Free +usage of an unknown object is a mystery object — it is almost always a +typo in the name, since this object is not declared anywhere. + +Incorrect: + +```eo +# Foo. +[] > foo + bar 42 > x +``` + +Here, `bar` is not defined in the program, not an object from `org.eolang.*` +and not imported via `+alias`. It will be compiled to `Φ.bar` reference, +which can't be validated by the parser. This is what we call a mystery +object, and it should be fixed by adding the definition of `bar`, importing +it, or using a proper object name. diff --git a/src/test/java/org/eolang/lints/LtMysteryTest.java b/src/test/java/org/eolang/lints/LtMysteryTest.java new file mode 100644 index 000000000..850b9481c --- /dev/null +++ b/src/test/java/org/eolang/lints/LtMysteryTest.java @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import fixtures.EoProgram; +import java.io.IOException; +import java.util.Map; +import org.cactoos.io.InputOf; +import org.cactoos.list.ListOf; +import org.cactoos.map.MapEntry; +import org.cactoos.map.MapOf; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link LtMystery}. + * @since 0.0.52 + */ +final class LtMysteryTest { + + @Test + void catchesMysteryObject() throws IOException { + MatcherAssert.assertThat( + "It is expected to catch a mystery object here", + new LtMystery(this.canonical()).defects( + new EoProgram("org/eolang/lints/mystery-object.eo").parse() + ), + Matchers.hasSize(1) + ); + } + + @Test + void allowsDeclaredObject() throws IOException { + MatcherAssert.assertThat( + "Defects are not empty, but the object is declared in the program", + new LtMystery(this.canonical()).defects( + new EoProgram("org/eolang/lints/declared-object.eo").parse() + ), + Matchers.emptyIterable() + ); + } + + @Test + void allowsAliasedObject() throws IOException { + MatcherAssert.assertThat( + "Defects are not empty, but the object is imported via +alias", + new LtMystery(this.canonical()).defects( + new EoProgram("org/eolang/lints/aliased-object.eo").parse() + ), + Matchers.emptyIterable() + ); + } + + @Test + void allowsCanonicalObject() throws IOException { + MatcherAssert.assertThat( + "Defects are not empty, but the object is part of org.eolang", + new LtMystery(this.canonical()).defects( + new EoProgram("org/eolang/lints/canonical-object.eo").parse() + ), + Matchers.emptyIterable() + ); + } + + @Test + void reportsCorrectMessageForMysteryObject() throws IOException { + MatcherAssert.assertThat( + "The message should mention the mystery object", + new ListOf<>( + new LtMystery(this.canonical()).defects( + new EoProgram("org/eolang/lints/mystery-object.eo").parse() + ) + ).get(0).text(), + Matchers.equalTo( + "Object \"bar\" is not defined in the program and is not part of org.eolang" + ) + ); + } + + @Test + void allowsAllObjectsDeclaredInline() throws IOException { + final String src = String.format( + "[] > foo%n [a] > bar%n bar > x" + ); + MatcherAssert.assertThat( + "Objects should not be reported, since all of them are declared", + new LtMystery(this.canonical()).defects( + new EoProgram(src, new InputOf(src)).parse() + ), + Matchers.emptyIterable() + ); + } + + @Tag("reserved") + @Test + void scansMysteryFromHome() throws Exception { + MatcherAssert.assertThat( + "It is expected to catch a mystery object using reserved names from home", + new LtMystery().defects( + new EoProgram("org/eolang/lints/mystery-bipki.eo").parse() + ), + Matchers.hasSize(1) + ); + } + + /** + * Dummy reserved names for the tests. + * @return Reserved names map + */ + private Map canonical() { + return new MapOf<>( + new MapEntry<>("number", "number.eo"), + new MapEntry<>("bytes", "bytes.eo"), + new MapEntry<>("string", "string.eo") + ); + } +} diff --git a/src/test/resources/org/eolang/lints/aliased-object.eo b/src/test/resources/org/eolang/lints/aliased-object.eo new file mode 100644 index 000000000..bc7dc8c08 --- /dev/null +++ b/src/test/resources/org/eolang/lints/aliased-object.eo @@ -0,0 +1,7 @@ ++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com ++spdx SPDX-License-Identifier: MIT ++alias org.eolang.io.stdout + +# Foo. +[] > foo + stdout "Hello" > x diff --git a/src/test/resources/org/eolang/lints/canonical-object.eo b/src/test/resources/org/eolang/lints/canonical-object.eo new file mode 100644 index 000000000..6c0ae5309 --- /dev/null +++ b/src/test/resources/org/eolang/lints/canonical-object.eo @@ -0,0 +1,6 @@ ++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com ++spdx SPDX-License-Identifier: MIT + +# Foo. +[] > foo + 42 > x diff --git a/src/test/resources/org/eolang/lints/declared-object.eo b/src/test/resources/org/eolang/lints/declared-object.eo new file mode 100644 index 000000000..520cdcb12 --- /dev/null +++ b/src/test/resources/org/eolang/lints/declared-object.eo @@ -0,0 +1,7 @@ ++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com ++spdx SPDX-License-Identifier: MIT + +# Foo. +[] > foo + [t] > bar + bar 42 > x diff --git a/src/test/resources/org/eolang/lints/mystery-bipki.eo b/src/test/resources/org/eolang/lints/mystery-bipki.eo new file mode 100644 index 000000000..1e7f67a9b --- /dev/null +++ b/src/test/resources/org/eolang/lints/mystery-bipki.eo @@ -0,0 +1,6 @@ ++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com ++spdx SPDX-License-Identifier: MIT + +# Foo. +[] > foo + bipki 42 > x \ No newline at end of file diff --git a/src/test/resources/org/eolang/lints/mystery-object.eo b/src/test/resources/org/eolang/lints/mystery-object.eo new file mode 100644 index 000000000..c00d8c7ae --- /dev/null +++ b/src/test/resources/org/eolang/lints/mystery-object.eo @@ -0,0 +1,6 @@ ++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com ++spdx SPDX-License-Identifier: MIT + +# Foo. +[] > foo + bar 42 > x From 61e5f519ee24d36def2d8e582a845b2a79d5d8a4 Mon Sep 17 00:00:00 2001 From: VasilevNStas Date: Mon, 10 Aug 2026 23:14:46 +0300 Subject: [PATCH 2/3] fix(#20): comply vale rules in mystery-object motive --- src/main/resources/org/eolang/motives/names/mystery-object.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/eolang/motives/names/mystery-object.md b/src/main/resources/org/eolang/motives/names/mystery-object.md index 5a18106cf..d6677ee5c 100644 --- a/src/main/resources/org/eolang/motives/names/mystery-object.md +++ b/src/main/resources/org/eolang/motives/names/mystery-object.md @@ -2,7 +2,7 @@ Objects, applied in the code, must be defined somewhere in the program, be the objects of `org.eolang.*` or be imported via `+alias` meta. Free -usage of an unknown object is a mystery object — it is almost always a +usage of an unknown object is a mystery object—it is almost always a typo in the name, since this object is not declared anywhere. Incorrect: From 4745435f1031c52919acac16a038777e9ffbd309 Mon Sep 17 00:00:00 2001 From: VasilevNStas Date: Thu, 13 Aug 2026 09:00:08 +0300 Subject: [PATCH 3/3] fix(#20): extract lambdas into private methods per review --- src/main/java/org/eolang/lints/LtMystery.java | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/eolang/lints/LtMystery.java b/src/main/java/org/eolang/lints/LtMystery.java index 9265f623e..9c6aeac2e 100644 --- a/src/main/java/org/eolang/lints/LtMystery.java +++ b/src/main/java/org/eolang/lints/LtMystery.java @@ -88,30 +88,69 @@ private static Collection find(final XML xmir, elem -> elem.attribute("name").text().get() ).collect(Collectors.toSet()); return xnav.path("//o[@base]").filter( - elem -> { - final Optional base = elem.attribute("base").text(); - return base.isPresent() - && LtMystery.simple(base.get()); - } + LtMystery::simpleBase ).filter( - elem -> { - final String base = elem.attribute("base").text().get(); - return !declared.contains(base.substring(2)) - && !reserved.containsKey(base.substring(2)); - } + elem -> LtMystery.unknown(elem, declared, reserved) ).map( - elem -> new Defect.Default( - "mystery-object", - Severity.ERROR, - new LineOf(elem).value(), - String.format( - "Object \"%s\" is not defined in the program and is not part of org.eolang", - elem.attribute("base").text().get().substring(2) - ) - ) + LtMystery::defect ).collect(Collectors.toList()); } + /** + * Is the {@code @base} a bare single-object reference? + * @param elem Object element + * @return True if it's a simple object reference + */ + private static boolean simpleBase(final Xnav elem) { + final Optional base = elem.attribute("base").text(); + return base.isPresent() + && LtMystery.simple(base.get()); + } + + /** + * Is the referenced object unknown? + * The name is the {@code @base} value without the leading two characters, + * which are the {@code Φ.} prefix. + * @param elem Object element + * @param declared Objects declared in the program + * @param reserved Reserved org.eolang names + * @return True if the object is neither declared nor reserved + */ + private static boolean unknown(final Xnav elem, + final Set declared, final Map reserved) { + final String name = LtMystery.objectName(elem); + return !declared.contains(name) + && !reserved.containsKey(name); + } + + /** + * Object name from the {@code @base} attribute. + * The name is the value without the leading two characters, + * which are the {@code Φ.} prefix. + * @param elem Object element + * @return Object name + */ + private static String objectName(final Xnav elem) { + return elem.attribute("base").text().get().substring(2); + } + + /** + * Defect for an unknown object. + * @param elem Object element + * @return Defect + */ + private static Defect defect(final Xnav elem) { + return new Defect.Default( + "mystery-object", + Severity.ERROR, + new LineOf(elem).value(), + String.format( + "Object \"%s\" is not defined in the program and is not part of org.eolang", + LtMystery.objectName(elem) + ) + ); + } + /** * Check if the base is a reference to a single object, like * {@code Φ.bar}, and not to a path like {@code Φ.org.eolang.io.stdout}.