Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions src/main/java/org/eolang/lints/LtMystery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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.
*
* <p>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.</p>
*
* @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<String, String> reserved;

/**
* Ctor.
*/
LtMystery() {
this(new ReservedNames());
}

/**
* Ctor.
* @param names Reserved names
*/
LtMystery(final Map<String, String> names) {
this.reserved = names;
}

@Override
public String name() {
return "mystery-object";
}

@Override
public Collection<Defect> defects(final XML xmir) throws IOException {
final Collection<Defect> 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<Defect> find(final XML xmir,
final Map<String, String> reserved) {
final Xnav xnav = new Xnav(xmir.inner());
final Set<String> declared = xnav.path("//o[@name]").map(
elem -> elem.attribute("name").text().get()
).collect(Collectors.toSet());
return xnav.path("//o[@base]").filter(
LtMystery::simpleBase
).filter(
elem -> LtMystery.unknown(elem, declared, reserved)
).map(
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<String> 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<String> declared, final Map<String, String> 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}.
* @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_-]*$");
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/eolang/lints/MonoLints.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ final class MonoLints extends IterableEnvelope<Lint> {
List.of(
new LtAsciiOnly(),
new LtReservedName(),
new LtSyntaxVersion()
new LtSyntaxVersion(),
new LtMystery()
)
)
);
Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/org/eolang/motives/names/mystery-object.md
Original file line number Diff line number Diff line change
@@ -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.
121 changes: 121 additions & 0 deletions src/test/java/org/eolang/lints/LtMysteryTest.java
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
VasilevNStas marked this conversation as resolved.

@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<String, String> canonical() {
return new MapOf<>(
new MapEntry<>("number", "number.eo"),
new MapEntry<>("bytes", "bytes.eo"),
new MapEntry<>("string", "string.eo")
);
}
}
7 changes: 7 additions & 0 deletions src/test/resources/org/eolang/lints/aliased-object.eo
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/test/resources/org/eolang/lints/canonical-object.eo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
+spdx SPDX-License-Identifier: MIT

# Foo.
[] > foo
42 > x
7 changes: 7 additions & 0 deletions src/test/resources/org/eolang/lints/declared-object.eo
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/test/resources/org/eolang/lints/mystery-bipki.eo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
+spdx SPDX-License-Identifier: MIT

# Foo.
[] > foo
bipki 42 > x
6 changes: 6 additions & 0 deletions src/test/resources/org/eolang/lints/mystery-object.eo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
+spdx SPDX-License-Identifier: MIT

# Foo.
[] > foo
bar 42 > x
Loading