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
93 changes: 93 additions & 0 deletions src/main/java/org/eolang/lints/LtRedundantRho.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2025 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.List;
import java.util.stream.Collectors;
import org.cactoos.io.ResourceOf;
import org.cactoos.text.TextOf;
import org.cactoos.text.UncheckedText;
import org.eolang.parser.OnDefault;

/**
* Lint that warns if a redundant {@code ^} is used.
* @since 0.0.59
*/
final class LtRedundantRho implements Lint<XML> {
@Override
public Collection<Defect> defects(final XML xmir) throws IOException {
final Collection<Defect> defects = new ArrayList<>(0);
final Xnav xml = new Xnav(xmir.inner());
final List<Xnav> objs = xml
.path("//o[starts-with(@base,'ξ.ρ')]")
.collect(Collectors.toList());
for (final Xnav obj : objs) {
Comment on lines +28 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don't match bare ξ.ρ; restrict to attribute-qualified uses.

Bare ^ (base=ξ.ρ) can be required; we only want ^.<attr>. Tighten the selector or guard in code to avoid false positives.

Apply either of the following:

Option A (selector):

-            .path("//o[starts-with(@base,'ξ.ρ')]")
+            .path("//o[starts-with(@base,'ξ.ρ.')]")

Option B (guard):

-        for (final Xnav obj : objs) {
-            final String name = obj.attribute("name").text().orElse("");
+        for (final Xnav obj : objs) {
+            final String base = obj.attribute("base").text().orElse("");
+            if ("ξ.ρ".equals(base) || !base.startsWith("ξ.ρ.")) {
+                continue; // ignore bare ^ or malformed cases
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final List<Xnav> objs = xml
.path("//o[starts-with(@base,'ξ.ρ')]")
.collect(Collectors.toList());
for (final Xnav obj : objs) {
final List<Xnav> objs = xml
.path("//o[starts-with(@base,'ξ.ρ.')]")
.collect(Collectors.toList());
for (final Xnav obj : objs) {
🤖 Prompt for AI Agents
In src/main/java/org/eolang/lints/LtRedundantRho.java around lines 28 to 31, the
XPath selector currently matches any element whose base starts with "ξ.ρ", which
also hits bare ξ.ρ; restrict matches to attribute-qualified uses by either
tightening the XPath to require the dot (e.g. starts-with(@base,'ξ.ρ.') or
matching a pattern that includes a dot/attribute) or keep the XPath but add a
guard inside the loop that only processes objs whose @base contains a '.' after
"ξ.ρ" (e.g. check that the base string startsWith "ξ.ρ." or contains "ξ.ρ.").
Implement one of these changes so bare ξ.ρ is ignored.

final String name = obj.attribute("name").text().orElse("");
final List<Xnav> matches = xml
.path(String.format("//o[@name='%s']", name))
.collect(Collectors.toList());
Comment on lines +32 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong identifier extracted; use attribute after ξ.ρ. instead of @name.

@name is the alias of the current <o>, not the target attribute referenced via ^. This causes incorrect matching and messages.

Apply this diff to extract the correct attribute:

-            final String name = obj.attribute("name").text().orElse("");
+            final String base = obj.attribute("base").text().orElse("");
+            final String attr = base.startsWith("ξ.ρ.") ? base.substring("ξ.ρ.".length()) : "";
+            if (attr.isEmpty()) {
+                continue;
+            }

Then replace subsequent usages of name in this method with attr (messages and queries).

🤖 Prompt for AI Agents
In src/main/java/org/eolang/lints/LtRedundantRho.java around lines 32 to 35, the
code extracts the current <o>'s @name but should extract the referenced
attribute after the ξ.ρ. expression; change the extraction to read the attribute
name from the ξ.ρ. reference (save it to a variable named attr) and then replace
all subsequent uses of the previous name variable in this method (queries, match
collection and message text) with attr so queries target the referenced
attribute and messages report the correct identifier.

final List<Xnav> ancestors = obj
.path(String.format("ancestor::o[@name='%s']", name))
.collect(Collectors.toList());
if (matches.size() == 1) {
defects.add(
new Defect.Default(
this.name(),
Severity.WARNING,
new OnDefault(xmir).get(),
Integer.parseInt(obj.attribute("line").text().orElse("0")),
String.format(
"Redundant 'ξ.ρ' notation: '%s' can be accessed without it",
name
)
)
);
} else if (!ancestors.isEmpty() && matches.size() > 1) {
final Xnav target = ancestors.get(0);
for (final Xnav match : matches) {
if (match.equals(target)) {
defects.add(
new Defect.Default(
this.name(),
Severity.WARNING,
new OnDefault(xmir).get(),
Integer.parseInt(obj.attribute("line").text().orElse("0")),
String.format(
"Redundant 'ξ.ρ' notation: '%s' resolves to the same object without it",
name
)
)
);
break;
}
}
}
Comment on lines +36 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Redundancy check ignores local shadowing; produces false positives.

Current logic counts all @name across the whole document and compares to ancestors, which will warn even when a local definition shadows the parent (a case where ^ is necessary).

Use a scope-aware check: warn only if the immediate scope doesn’t define attr locally and a parent scope does. Example refactor:

-            final List<Xnav> matches = xml
-                .path(String.format("//o[@name='%s']", name))
-                .collect(Collectors.toList());
-            final List<Xnav> ancestors = obj
-                .path(String.format("ancestor::o[@name='%s']", name))
-                .collect(Collectors.toList());
-            if (matches.size() == 1) {
-                defects.add(
-                    new Defect.Default(
-                        this.name(),
-                        Severity.WARNING,
-                        new OnDefault(xmir).get(),
-                        Integer.parseInt(obj.attribute("line").text().orElse("0")),
-                        String.format(
-                            "Redundant 'ξ.ρ' notation: '%s' can be accessed without it",
-                            name
-                        )
-                    )
-                );
-            } else if (!ancestors.isEmpty() && matches.size() > 1) {
-                final Xnav target = ancestors.get(0);
-                for (final Xnav match : matches) {
-                    if (match.equals(target)) {
-                        defects.add(
-                            new Defect.Default(
-                                this.name(),
-                                Severity.WARNING,
-                                new OnDefault(xmir).get(),
-                                Integer.parseInt(obj.attribute("line").text().orElse("0")),
-                                String.format(
-                                    "Redundant 'ξ.ρ' notation: '%s' resolves to the same object without it",
-                                    name
-                                )
-                            )
-                        );
-                        break;
-                    }
-                }
-            }
+            // Is there a local definition in the current scope that would shadow the parent?
+            final boolean hasLocal =
+                !obj.path(String.format("ancestor::o[1]/o[@name='%s']", attr))
+                    .collect(Collectors.toList()).isEmpty();
+            // Does the parent scope define the attribute?
+            final boolean parentHasAttr =
+                !obj.path(String.format("ancestor::o[2]/o[@name='%s']", attr))
+                    .collect(Collectors.toList()).isEmpty();
+            if (!hasLocal && parentHasAttr) {
+                defects.add(
+                    new Defect.Default(
+                        this.name(),
+                        Severity.WARNING,
+                        new OnDefault(xmir).get(),
+                        Integer.parseInt(obj.attribute("line").text().orElse("0")),
+                        String.format(
+                            "Redundant 'ξ.ρ' notation: '%s' can be accessed without it",
+                            attr
+                        )
+                    )
+                );
+            }

Note: If XMIR nesting differs in your project, adjust ancestor::o[1]/[2] accordingly. Add tests to cover shadowing and parent access.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/org/eolang/lints/LtRedundantRho.java around lines 36 to 71, the
current check counts all @name occurrences across the document and emits
warnings even when the attribute is shadowed locally; change the logic to be
scope-aware: first check the immediate scope (the current object's direct
children or attributes) for a local definition of the name and skip the warning
if present; only proceed to inspect the nearest parent scope (the closest
ancestor o node) and emit the redundant-ρ warning when the name is not defined
locally and it is defined in a parent scope (i.e., localMatches.isEmpty() &&
parentMatches.notEmpty()); implement this by selecting only same-scope attribute
nodes for the local check and only the nearest ancestor o for the parent check,
update the existing conditional branches to use these results, and add unit
tests covering local shadowing, parent access, and the no-parent case.

}
return defects;
}

@Override
public String motive() throws IOException {
return new UncheckedText(
new TextOf(
new ResourceOf(
String.format(
"org/eolang/motives/errors/%s.md", this.name()
)
)
)
).asString();
Comment on lines +78 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Resource lookup: verify motive path and existence.

Path hardcodes errors/. Ensure resources/org/eolang/motives/errors/redundant-hat.md exists and is packaged. Add a unit test for motive() returning non-empty.


🏁 Script executed:

#!/bin/bash
# Verify motive file exists and is included.
fd -a 'redundant-hat.md' resources | sed -n '1p'
rg -n 'redundant-hat' -g resources/org/eolang/motives/**

# Also check the lint is registered in any aggregator/factory.
rg -nP '(redundant-hat|LtRedundantRho)' -g 'src/main/java/**'

Length of output: 435


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo
echo "Searching for 'redundant-hat.md' and occurrences..."
rg -n --hidden --no-ignore -S 'redundant-hat.md' || true
rg -n --hidden --no-ignore -S 'redundant-hat' || true
echo
echo "Searching for motives/errors md files..."
rg -n --hidden --no-ignore -S 'org/eolang/motives/errors' || true
echo
echo "Listing src/main/resources MD files (if present)..."
if [ -d src/main/resources ]; then
  find src/main/resources -type f -name '*.md' -print || true
else
  echo "src/main/resources not found"
fi
echo
echo "Showing LtRedundantRho.java (first 240 lines)..."
sed -n '1,240p' src/main/java/org/eolang/lints/LtRedundantRho.java || true
echo
echo "Searching repo for motive() usages and tests referencing LtRedundantRho or redundant-hat..."
rg -n --hidden --no-ignore -S 'motive\s*\(|LtRedundantRho|redundant-hat' || true

Length of output: 15067


Create missing motive resource and add a motive test

motive() loads "org/eolang/motives/errors/redundant-hat.md" but that file is not present under src/main/resources/org/eolang/motives/errors/ — add src/main/resources/org/eolang/motives/errors/redundant-hat.md (or change the path in src/main/java/org/eolang/lints/LtRedundantRho.java). Ensure the resource is packaged, and add/extend src/test/java/org/eolang/lints/LtRedundantRhoTest.java to assert new LtRedundantRho().motive() returns a non-empty string.

🤖 Prompt for AI Agents
In src/main/java/org/eolang/lints/LtRedundantRho.java around lines 78 to 86 the
motive() call expects a resource at "org/eolang/motives/errors/redundant-hat.md"
which is missing; either add the resource file
src/main/resources/org/eolang/motives/errors/redundant-hat.md (with the
appropriate message content) and ensure it is included in the build resources,
or change the String.format path in this file to point to an existing resource;
additionally add or extend
src/test/java/org/eolang/lints/LtRedundantRhoTest.java to instantiate new
LtRedundantRho() and assert that motive() returns a non-empty string so the
resource loading is validated.

}

@Override
public String name() {
return "redundant-hat";
}
}
65 changes: 65 additions & 0 deletions src/test/java/org/eolang/lints/LtRedundantRhoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import com.jcabi.xml.XMLDocument;
import java.io.IOException;
import org.eolang.parser.EoSyntax;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

/**
* Test for {@link LtRedundantRho}.
*
* @since 0.0.1
*/
final class LtRedundantRhoTest {

@Test
void reportsNoDefectsForNecessaryHat() throws Exception {
MatcherAssert.assertThat(
"Should not report a defect when '^' is necessary to resolve ambiguity",
new LtRedundantRho().defects(
new EoSyntax("[] > foo\n+test > @\n^foo > bar").parsed()
),
Matchers.allOf(
Matchers.iterableWithSize(Matchers.equalTo(0))
)
);
}

@Test
void namesStableId() {
MatcherAssert.assertThat(
"Rule id must be 'redundant-hat'",
new LtRedundantRho().name(),
Matchers.equalTo("redundant-hat")
);
}

@Test
void doesNotReportDefectWhenCaretIsNecessary() throws IOException {
MatcherAssert.assertThat(
"Should not report a defect when '^' is necessary due to ambiguity",
new LtRedundantRho().defects(
new EoSyntax("[] > foo\n+test > @\n^foo > bar").parsed()
),
Matchers.empty()
);
}

@Test
void reportsDefectWhenCaretIsRedundantWithMultipleMatches() throws IOException {
MatcherAssert.assertThat(
"Should report a defect when '^' is redundant among multiple matches",
new LtRedundantRho().defects(
new XMLDocument("<object><o name='foo' base='ξ.ρ' line='12'></o></object>")
),
Matchers.hasSize(1)
);
Comment on lines +55 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Test name/content mismatch; second branch not covered.

The test claims “multiple matches” but provides a single <o>; it only exercises the single‑match branch. Either rename the test or extend XML to include both a parent attribute and the ξ.ρ.<attr> usage.

Suggested test data to cover the intended case (adjust if your XMIR differs):

-                new XMLDocument("<object><o name='foo' base='ξ.ρ' line='12'></o></object>")
+                new XMLDocument(
+                  "<object>" +
+                    "<o name='foo'/>" +
+                    "<o name='inner'>" +
+                      "<o name='bar' base='ξ.ρ.foo' line='12'/>" +
+                    "</o>" +
+                  "</object>"
+                )

And add a negative test to prevent false positives when local shadowing exists:

@Test
void doesNotReportWhenLocalShadowingRequiresHat() throws IOException {
  MatcherAssert.assertThat(
    new LtRedundantRho().defects(
      new XMLDocument(
        "<object>" +
          "<o name='foo'/>" +
          "<o name='inner'>" +
            "<o name='foo'/>" +                       // local shadow
            "<o name='bar' base='ξ.ρ.foo' line='7'/>" +
          "</o>" +
        "</object>"
      )
    ),
    Matchers.emptyIterable()
  );
}

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for LtRedundantRhoTest..."
rg -n --hidden -S "LtRedundantRhoTest" || true
echo "---- Showing file (if exists) ----"
sed -n '1,240p' src/test/java/org/eolang/lints/LtRedundantRhoTest.java || true
echo "---- Searching for related test names ----"
rg -n --hidden -S "reportsDefectWhenCaretIsRedundantWithMultipleMatches|doesNotReportWhenLocalShadowingRequiresHat|doesNotReport|reportsDefect" src/test/java/org/eolang/lints || true

Length of output: 2734


Test name/content mismatch — second branch not covered.

The test reportsDefectWhenCaretIsRedundantWithMultipleMatches (src/test/java/org/eolang/lints/LtRedundantRhoTest.java:55-62) supplies a single (base='ξ.ρ') and only exercises the single-match branch; rename the test or extend the XML to produce multiple matches.

Suggested change:

-                new XMLDocument("<object><o name='foo' base='ξ.ρ' line='12'></o></object>")
+                new XMLDocument(
+                  "<object>" +
+                    "<o name='foo'/>" +
+                    "<o name='inner'>" +
+                      "<o name='bar' base='ξ.ρ.foo' line='12'/>" +
+                    "</o>" +
+                  "</object>"
+                )

Add a negative test to avoid false positives with local shadowing:

@Test
void doesNotReportWhenLocalShadowingRequiresHat() throws IOException {
  MatcherAssert.assertThat(
    new LtRedundantRho().defects(
      new XMLDocument(
        "<object>" +
          "<o name='foo'/>" +
          "<o name='inner'>" +
            "<o name='foo'/>" +                       // local shadow
            "<o name='bar' base='ξ.ρ.foo' line='7'/>" +
          "</o>" +
        "</object>"
      )
    ),
    Matchers.emptyIterable()
  );
}
🤖 Prompt for AI Agents
In src/test/java/org/eolang/lints/LtRedundantRhoTest.java around lines 55-62,
the test named reportsDefectWhenCaretIsRedundantWithMultipleMatches only
supplies a single <o> so it covers the single-match branch; update the test to
actually produce multiple matches or add the suggested negative test to cover
local shadowing: either extend the XML input to include multiple <o> elements
with bases that will generate more than one match (so the caret-redundant
multi-match path is exercised) or add the new
doesNotReportWhenLocalShadowingRequiresHat test that constructs a nested object
with a locally shadowed name and asserts no defects; ensure assertions use
Matchers.hasSize(1) for the multi-match case or Matchers.emptyIterable() for the
negative case and keep line attributes present for accurate diagnostics.

}

}