From 7be426c61303130956fef287b9bbf145e77c5f37 Mon Sep 17 00:00:00 2001 From: volodya-lombrozo Date: Thu, 28 May 2026 11:33:21 +0300 Subject: [PATCH 1/3] feat(#896): Add Fix interface and FxUnsortedMetas implementation --- src/main/java/org/eolang/lints/Fix.java | 23 ++++++++ .../org/eolang/lints/FxUnsortedMetas.java | 52 +++++++++++++++++++ .../org/eolang/fixes/metas/unsorted-metas.xsl | 21 ++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/main/java/org/eolang/lints/Fix.java create mode 100644 src/main/java/org/eolang/lints/FxUnsortedMetas.java create mode 100644 src/main/resources/org/eolang/fixes/metas/unsorted-metas.xsl diff --git a/src/main/java/org/eolang/lints/Fix.java b/src/main/java/org/eolang/lints/Fix.java new file mode 100644 index 000000000..a1318a786 --- /dev/null +++ b/src/main/java/org/eolang/lints/Fix.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import com.jcabi.xml.XML; +import java.io.IOException; + +/** + * A fix for a lint defect. + * @since 0.2.1 + */ +public interface Fix { + + /** + * Apply the fix and return the corrected XMIR. + * @param xmir The XMIR to fix + * @return Fixed XMIR + * @throws IOException If something goes wrong + */ + XML apply(XML xmir) throws IOException; +} diff --git a/src/main/java/org/eolang/lints/FxUnsortedMetas.java b/src/main/java/org/eolang/lints/FxUnsortedMetas.java new file mode 100644 index 000000000..1f04221ee --- /dev/null +++ b/src/main/java/org/eolang/lints/FxUnsortedMetas.java @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import com.jcabi.xml.ClasspathSources; +import com.jcabi.xml.XML; +import com.jcabi.xml.XSL; +import com.jcabi.xml.XSLDocument; +import java.io.IOException; +import org.cactoos.io.ResourceOf; +import org.cactoos.scalar.Sticky; +import org.cactoos.scalar.Unchecked; +import org.cactoos.text.IoCheckedText; +import org.cactoos.text.TextOf; + +/** + * Fix for the {@code unsorted-metas} lint. + * Sorts {@code meta} elements inside {@code metas} alphabetically + * by their {@code head} and {@code tail} content. + * @since 0.2.1 + */ +public final class FxUnsortedMetas implements Fix { + + /** + * The XSL stylesheet. + */ + private final Unchecked sheet; + + /** + * Ctor. + */ + public FxUnsortedMetas() { + this.sheet = new Unchecked<>( + new Sticky<>( + () -> new XSLDocument( + new IoCheckedText( + new TextOf( + new ResourceOf("org/eolang/fixes/metas/unsorted-metas.xsl") + ) + ).asString() + ).with(new ClasspathSources()) + ) + ); + } + + @Override + public XML apply(final XML xmir) throws IOException { + return this.sheet.value().transform(xmir); + } +} diff --git a/src/main/resources/org/eolang/fixes/metas/unsorted-metas.xsl b/src/main/resources/org/eolang/fixes/metas/unsorted-metas.xsl new file mode 100644 index 000000000..e6ea66644 --- /dev/null +++ b/src/main/resources/org/eolang/fixes/metas/unsorted-metas.xsl @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + From 16eafb55cc91fee9efd97d6aebf65e7d1c4a50e9 Mon Sep 17 00:00:00 2001 From: volodya-lombrozo Date: Thu, 28 May 2026 11:52:15 +0300 Subject: [PATCH 2/3] feat(#896): add FxUnsortedMetas fix with YAML-driven tests --- org/eolang/xax/XtYaml.java | 179 ++++++++++++++++++ .../org/eolang/lints/FxUnsortedMetasTest.java | 47 +++++ .../sorts-version-and-spdx.yaml | 19 ++ 3 files changed, 245 insertions(+) create mode 100644 org/eolang/xax/XtYaml.java create mode 100644 src/test/java/org/eolang/lints/FxUnsortedMetasTest.java create mode 100644 src/test/resources/org/eolang/lints/fixes/unsorted-metas/sorts-version-and-spdx.yaml diff --git a/org/eolang/xax/XtYaml.java b/org/eolang/xax/XtYaml.java new file mode 100644 index 000000000..6795da68d --- /dev/null +++ b/org/eolang/xax/XtYaml.java @@ -0,0 +1,179 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2022-2025 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.eolang.xax; + +import com.jcabi.xml.XML; +import com.jcabi.xml.XMLDocument; +import com.jcabi.xml.XSLDocument; +import com.yegor256.xsline.Shift; +import com.yegor256.xsline.StClasspath; +import com.yegor256.xsline.StXSL; +import com.yegor256.xsline.TrDefault; +import com.yegor256.xsline.Train; +import com.yegor256.xsline.Xsline; +import java.io.FileNotFoundException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import org.yaml.snakeyaml.Yaml; + +/** + * A story parsed from YAML and then processed through XSL + * stylesheets. + * + * @since 0.1.0 + */ +public final class XtYaml implements Xtory { + + /** + * The YAML to work with. + */ + private final String yaml; + + /** + * The train to start with. + */ + private final Train train; + + /** + * The parser to use, when {@code input} is provided in the YAML. + */ + private final Parser parser; + + /** + * Ctor. + * @param yml The story in YAML + */ + public XtYaml(final String yml) { + this( + yml, + input -> { + throw new UnsupportedOperationException( + "Parser is not provided, while YAML doesn't have the 'document' property" + ); + } + ); + } + + /** + * Ctor. + * @param yml The story in YAML + * @param prsr The parser to use + */ + public XtYaml(final String yml, final Parser prsr) { + this(yml, prsr, new TrDefault<>()); + } + + /** + * Ctor. + * @param yml The story in YAML + * @param prsr The parser to use + * @param trn The train to start with + * @since 0.1.1 + */ + public XtYaml(final String yml, final Parser prsr, final Train trn) { + this.yaml = yml; + this.parser = prsr; + this.train = trn; + } + + @Override + public Map map() { + return new Yaml().load( + String.class.cast(this.yaml) + ); + } + + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public XML before() { + final Object doc = this.map().get("document"); + final XML xml; + if (doc == null) { + final Object input = this.map().get("input"); + if (input == null) { + throw new IllegalArgumentException( + "Neither 'document' nor 'input' exists in the YAML" + ); + } + try { + xml = this.parser.parse(input.toString()); + // @checkstyle IllegalCatchCheck (1 line) + } catch (final Exception ex) { + throw new IllegalArgumentException(ex); + } + } else { + xml = new XMLDocument(doc.toString()); + } + return xml; + } + + @Override + public XML after() { + return this.xsline().pass(this.before()); + } + + @Override + @SuppressWarnings("unchecked") + public Xsline xsline() { + Object sheets = this.map().get("sheets"); + if (sheets == null) { + sheets = Arrays.asList(); + } + Train trn = this.train; + for (final String sheet : (Iterable) sheets) { + if (sheet.startsWith("file://")) { + try { + trn = trn.with( + new StXSL( + new XSLDocument(Paths.get(sheet.substring(7))) + ) + ); + } catch (final FileNotFoundException ex) { + throw new IllegalArgumentException(ex); + } + } else { + trn = trn.with(new StClasspath(sheet)); + } + } + return new Xsline(trn); + } + + @Override + @SuppressWarnings("unchecked") + public Collection asserts() { + Object asserts = this.map().get("asserts"); + if (asserts == null) { + asserts = Arrays.asList(); + } + final Collection xpaths = new LinkedList<>(); + for (final String xpath : (Iterable) asserts) { + xpaths.add(xpath); + } + return xpaths; + } + +} diff --git a/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java b/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java new file mode 100644 index 000000000..25431d68f --- /dev/null +++ b/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import com.jcabi.xml.XML; +import fixtures.EoProgram; +import java.util.Map; +import org.cactoos.io.InputOf; +import org.eolang.jucs.ClasspathSource; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.yaml.snakeyaml.Yaml; + +/** + * Tests for {@link FxUnsortedMetas}. + * @since 0.2.1 + */ +final class FxUnsortedMetasTest { + + @ParameterizedTest + @ClasspathSource(value = "org/eolang/lints/fixes/unsorted-metas/", glob = "**.yaml") + void fixesUnsortedMetas(final String yaml) throws Exception { + final Map pack = new Yaml().load(yaml); + final Object input = pack.get("input"); + final XML fixed = new FxUnsortedMetas().apply( + new EoProgram(String.valueOf(input.hashCode()), new InputOf(input.toString())).parse() + ); + final Object output = pack.get("output"); + final XML expected = new EoProgram( + String.valueOf(output.hashCode()), + new InputOf(output.toString()) + ).parse(); + MatcherAssert.assertThat( + "Meta heads after fix must match the expected order", + fixed.xpath("/object/metas/meta/head/text()"), + Matchers.equalTo(expected.xpath("/object/metas/meta/head/text()")) + ); + MatcherAssert.assertThat( + "Meta tails after fix must match the expected order", + fixed.xpath("/object/metas/meta/tail/text()"), + Matchers.equalTo(expected.xpath("/object/metas/meta/tail/text()")) + ); + } +} diff --git a/src/test/resources/org/eolang/lints/fixes/unsorted-metas/sorts-version-and-spdx.yaml b/src/test/resources/org/eolang/lints/fixes/unsorted-metas/sorts-version-and-spdx.yaml new file mode 100644 index 000000000..cad6b4a83 --- /dev/null +++ b/src/test/resources/org/eolang/lints/fixes/unsorted-metas/sorts-version-and-spdx.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +sheets: + - /org/eolang/fixes/metas/unsorted-metas.xsl +input: | + +version 0.0.0 + +spdx SPDX-License-Identifier: MIT + + # No comments. + [] > foo + 42 > @ +output: | + +spdx SPDX-License-Identifier: MIT + +version 0.0.0 + + # No comments. + [] > foo + 42 > @ From b5f22e9594106a072489a91bc15bd61811d58fa0 Mon Sep 17 00:00:00 2001 From: volodya-lombrozo Date: Thu, 28 May 2026 12:08:14 +0300 Subject: [PATCH 3/3] fix(#896): Rename sibling-text to previous-text and allow fixes dir in test --- org/eolang/xax/XtYaml.java | 179 ------------------ src/main/java/org/eolang/lints/Fix.java | 1 + .../org/eolang/lints/metas/unsorted-metas.xsl | 4 +- .../org/eolang/lints/FxUnsortedMetasTest.java | 48 +++-- .../java/org/eolang/lints/LtByXslTest.java | 3 +- 5 files changed, 37 insertions(+), 198 deletions(-) delete mode 100644 org/eolang/xax/XtYaml.java diff --git a/org/eolang/xax/XtYaml.java b/org/eolang/xax/XtYaml.java deleted file mode 100644 index 6795da68d..000000000 --- a/org/eolang/xax/XtYaml.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2022-2025 Yegor Bugayenko - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package org.eolang.xax; - -import com.jcabi.xml.XML; -import com.jcabi.xml.XMLDocument; -import com.jcabi.xml.XSLDocument; -import com.yegor256.xsline.Shift; -import com.yegor256.xsline.StClasspath; -import com.yegor256.xsline.StXSL; -import com.yegor256.xsline.TrDefault; -import com.yegor256.xsline.Train; -import com.yegor256.xsline.Xsline; -import java.io.FileNotFoundException; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedList; -import java.util.Map; -import org.yaml.snakeyaml.Yaml; - -/** - * A story parsed from YAML and then processed through XSL - * stylesheets. - * - * @since 0.1.0 - */ -public final class XtYaml implements Xtory { - - /** - * The YAML to work with. - */ - private final String yaml; - - /** - * The train to start with. - */ - private final Train train; - - /** - * The parser to use, when {@code input} is provided in the YAML. - */ - private final Parser parser; - - /** - * Ctor. - * @param yml The story in YAML - */ - public XtYaml(final String yml) { - this( - yml, - input -> { - throw new UnsupportedOperationException( - "Parser is not provided, while YAML doesn't have the 'document' property" - ); - } - ); - } - - /** - * Ctor. - * @param yml The story in YAML - * @param prsr The parser to use - */ - public XtYaml(final String yml, final Parser prsr) { - this(yml, prsr, new TrDefault<>()); - } - - /** - * Ctor. - * @param yml The story in YAML - * @param prsr The parser to use - * @param trn The train to start with - * @since 0.1.1 - */ - public XtYaml(final String yml, final Parser prsr, final Train trn) { - this.yaml = yml; - this.parser = prsr; - this.train = trn; - } - - @Override - public Map map() { - return new Yaml().load( - String.class.cast(this.yaml) - ); - } - - @Override - @SuppressWarnings("PMD.AvoidCatchingGenericException") - public XML before() { - final Object doc = this.map().get("document"); - final XML xml; - if (doc == null) { - final Object input = this.map().get("input"); - if (input == null) { - throw new IllegalArgumentException( - "Neither 'document' nor 'input' exists in the YAML" - ); - } - try { - xml = this.parser.parse(input.toString()); - // @checkstyle IllegalCatchCheck (1 line) - } catch (final Exception ex) { - throw new IllegalArgumentException(ex); - } - } else { - xml = new XMLDocument(doc.toString()); - } - return xml; - } - - @Override - public XML after() { - return this.xsline().pass(this.before()); - } - - @Override - @SuppressWarnings("unchecked") - public Xsline xsline() { - Object sheets = this.map().get("sheets"); - if (sheets == null) { - sheets = Arrays.asList(); - } - Train trn = this.train; - for (final String sheet : (Iterable) sheets) { - if (sheet.startsWith("file://")) { - try { - trn = trn.with( - new StXSL( - new XSLDocument(Paths.get(sheet.substring(7))) - ) - ); - } catch (final FileNotFoundException ex) { - throw new IllegalArgumentException(ex); - } - } else { - trn = trn.with(new StClasspath(sheet)); - } - } - return new Xsline(trn); - } - - @Override - @SuppressWarnings("unchecked") - public Collection asserts() { - Object asserts = this.map().get("asserts"); - if (asserts == null) { - asserts = Arrays.asList(); - } - final Collection xpaths = new LinkedList<>(); - for (final String xpath : (Iterable) asserts) { - xpaths.add(xpath); - } - return xpaths; - } - -} diff --git a/src/main/java/org/eolang/lints/Fix.java b/src/main/java/org/eolang/lints/Fix.java index a1318a786..8bdcd6957 100644 --- a/src/main/java/org/eolang/lints/Fix.java +++ b/src/main/java/org/eolang/lints/Fix.java @@ -11,6 +11,7 @@ * A fix for a lint defect. * @since 0.2.1 */ +@FunctionalInterface public interface Fix { /** diff --git a/src/main/resources/org/eolang/lints/metas/unsorted-metas.xsl b/src/main/resources/org/eolang/lints/metas/unsorted-metas.xsl index d8f836ca5..94a74365c 100644 --- a/src/main/resources/org/eolang/lints/metas/unsorted-metas.xsl +++ b/src/main/resources/org/eolang/lints/metas/unsorted-metas.xsl @@ -13,8 +13,8 @@ - - + + diff --git a/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java b/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java index 25431d68f..31dd518e9 100644 --- a/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java +++ b/src/test/java/org/eolang/lints/FxUnsortedMetasTest.java @@ -4,9 +4,9 @@ */ package org.eolang.lints; -import com.jcabi.xml.XML; import fixtures.EoProgram; import java.util.Map; +import java.util.stream.Collectors; import org.cactoos.io.InputOf; import org.eolang.jucs.ClasspathSource; import org.hamcrest.MatcherAssert; @@ -17,6 +17,12 @@ /** * Tests for {@link FxUnsortedMetas}. * @since 0.2.1 + * @todo #896:60min Replace partial meta comparison with exact XMIR match. + * Currently the test only compares the ordered sequence of "head tail" + * strings extracted from the metas section. Since we have a clear 'input' + * and 'output' EO program in each YAML pack, we should compare the full + * XMIR structure of the fixed result against the expected XMIR, ignoring + * only volatile attributes such as @line numbers. */ final class FxUnsortedMetasTest { @@ -25,23 +31,33 @@ final class FxUnsortedMetasTest { void fixesUnsortedMetas(final String yaml) throws Exception { final Map pack = new Yaml().load(yaml); final Object input = pack.get("input"); - final XML fixed = new FxUnsortedMetas().apply( - new EoProgram(String.valueOf(input.hashCode()), new InputOf(input.toString())).parse() - ); final Object output = pack.get("output"); - final XML expected = new EoProgram( - String.valueOf(output.hashCode()), - new InputOf(output.toString()) - ).parse(); - MatcherAssert.assertThat( - "Meta heads after fix must match the expected order", - fixed.xpath("/object/metas/meta/head/text()"), - Matchers.equalTo(expected.xpath("/object/metas/meta/head/text()")) - ); MatcherAssert.assertThat( - "Meta tails after fix must match the expected order", - fixed.xpath("/object/metas/meta/tail/text()"), - Matchers.equalTo(expected.xpath("/object/metas/meta/tail/text()")) + "Metas after fix must match the expected order", + new FxUnsortedMetas().apply( + new EoProgram( + String.valueOf(input.hashCode()), + new InputOf(input.toString()) + ).parse() + ).nodes("/object/metas/meta").stream().map( + m -> String.format( + "%s %s", + m.xpath("head/text()").get(0), + m.xpath("tail/text()").get(0) + ) + ).collect(Collectors.toList()), + Matchers.equalTo( + new EoProgram( + String.valueOf(output.hashCode()), + new InputOf(output.toString()) + ).parse().nodes("/object/metas/meta").stream().map( + m -> String.format( + "%s %s", + m.xpath("head/text()").get(0), + m.xpath("tail/text()").get(0) + ) + ).collect(Collectors.toList()) + ) ); } } diff --git a/src/test/java/org/eolang/lints/LtByXslTest.java b/src/test/java/org/eolang/lints/LtByXslTest.java index b2900d2e7..cd2f60590 100644 --- a/src/test/java/org/eolang/lints/LtByXslTest.java +++ b/src/test/java/org/eolang/lints/LtByXslTest.java @@ -109,7 +109,7 @@ void checksLocationsOfYamlPacks() throws IOException { @SuppressWarnings("StreamResourceLeak") void catchesLostYamls() throws IOException { MatcherAssert.assertThat( - "All YAML files must be in allowed locations (single or wpa packs)", + "All YAML files must be in allowed locations (single, wpa packs, or fixes)", Files.walk(Paths.get("src/test/resources/org/eolang/lints")) .filter(Files::isRegularFile) .filter(LtByXslTest.yamls()) @@ -117,6 +117,7 @@ void catchesLostYamls() throws IOException { path -> path.endsWith("org/eolang/lints/packs/single") || path.endsWith("org/eolang/lints/packs/wpa") + || path.contains("org/eolang/lints/fixes") ), Matchers.equalTo(true) );