-
Notifications
You must be signed in to change notification settings - Fork 36
feat(#20): add mystery-object lint #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
VasilevNStas
wants to merge
3
commits into
objectionary:master
Choose a base branch
from
VasilevNStas:fix/issue-20-mystery-objects
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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_-]*$"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/main/resources/org/eolang/motives/names/mystery-object.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
|
|
||
| @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") | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.