Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
161 changes: 161 additions & 0 deletions src/main/java/org/eolang/lints/LtSyntaxVersion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import com.github.lombrozo.xnav.Xnav;
import com.google.common.base.Splitter;
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.IoCheckedText;
import org.cactoos.text.TextOf;
import org.eolang.parser.OnDefault;

/**
* Checks that the version specified in the +syntax meta is not
* newer than the current parser version.
*
* <p>If the +syntax meta specifies a version higher than the parser's
* version, it means the code requires a newer parser, and this
* lint reports an error.</p>
*
* @since 0.1.0
*/
final class LtSyntaxVersion implements Lint<XML> {

/**
* The parser version.
*/
private final String parserVersion;

/**
* Default ctor.
*/
LtSyntaxVersion() {
this.parserVersion = "0.0.0";
}

/**
* Ctor.
* @param parserVersion The parser version (must be valid SemVer, e.g. 1.2.3).
* @throws IllegalArgumentException If parserVersion is not valid SemVer.
*/
LtSyntaxVersion(final String parserVersion) {
if (parserVersion == null || !LtSyntaxVersion.valid(parserVersion)) {
throw new IllegalArgumentException(
String.format(
"parser version must be valid SemVer (e.g. 1.2.3), got: %s",
parserVersion == null ? "null" : String.format("\"%s\"", parserVersion)
)
);
}
this.parserVersion = parserVersion;
}

@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> metas = xml.path("/object/metas/meta[head='syntax']")
.collect(Collectors.toList());
for (final Xnav meta : metas) {
final String tail = meta.element("tail").text().orElse("");
final String line = meta.attribute("line").text().orElse("0");
if (!LtSyntaxVersion.valid(tail)) {
defects.add(
new Defect.Default(
this.name(),
Severity.ERROR,
new OnDefault(xmir).get(),
Integer.parseInt(line),
String.format(
"The format of the +syntax meta is wrong: %s (SemVer expected instead)",
tail
)
)
);
continue;
}
if (this.compareVersions(tail) < 0) {
defects.add(
new Defect.Default(
this.name(),
Severity.ERROR,
new OnDefault(xmir).get(),
Integer.parseInt(line),
String.format(
"The +syntax meta requires version %s, but the current parser version is %s (older)",
tail,
this.parserVersion
)
)
);
}
}
return defects;
}

@Override
public String name() {
return "syntax-version-mismatch";
}

@Override
public String motive() throws IOException {
return new IoCheckedText(
new TextOf(
new ResourceOf(
"org/eolang/motives/metas/syntax-version-mismatch.md"
)
)
).asString();
}

/**
* Check if the version string is a valid SemVer.
* @param version The version to validate.
* @return True if valid.
*/
private static boolean valid(final String version) {
return version.matches("^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?$");
}
Comment thread
Daniilmipt marked this conversation as resolved.
Outdated

/**
* Compare parser version to the given syntax version.
* @param syntaxVersion The version from +syntax meta.
* @return -1 if parser is older, 0 if equal, 1 if parser is newer.
*/
private int compareVersions(final String syntaxVersion) {
final int[] syntaxVer = LtSyntaxVersion.parts(syntaxVersion);
final int[] parserVer = LtSyntaxVersion.parts(this.parserVersion);
for (int i = 0; i < syntaxVer.length; i++) {
if (parserVer[i] < syntaxVer[i]) {
return -1;
}
if (parserVer[i] > syntaxVer[i]) {
return 1;
}
}
return 0;
}
Comment thread
Daniilmipt marked this conversation as resolved.

/**
* Parse the numeric parts of a SemVer string.
* @param version The version string.
* @return Array of [major, minor, patch].
*/
private static int[] parts(final String version) {
final List<String> segments = Splitter.on('.').splitToList(version.split("-", 2)[0]);
return new int[] {
segments.size() > 0 ? Integer.parseInt(segments.get(0)) : 0,
segments.size() > 1 ? Integer.parseInt(segments.get(1)) : 0,
segments.size() > 2 ? Integer.parseInt(segments.get(2)) : 0
};
}
}
2 changes: 1 addition & 1 deletion src/main/resources/org/eolang/lints/metas/unique-metas.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<xsl:import href="/org/eolang/funcs/lineno.xsl"/>
<xsl:import href="/org/eolang/funcs/defect-context.xsl"/>
<xsl:output encoding="UTF-8"/>
<xsl:variable name="unique" select="('version', 'architect', 'home', 'package')"/>
<xsl:variable name="unique" select="('version', 'architect', 'home', 'package', 'syntax')"/>
<xsl:variable name="metas" select="/object/metas/meta"/>
<xsl:variable name="heads" select="/object/metas/meta/head"/>
<xsl:template match="/">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<defects>
<xsl:for-each select="/object/metas/meta">
<xsl:variable name="meta-head" select="head"/>
<xsl:variable name="predefined" select="('package', 'alias', 'version', 'rt', 'architect', 'home', 'unlint', 'probe', 'spdx')"/>
<xsl:variable name="predefined" select="('package', 'alias', 'version', 'rt', 'architect', 'home', 'unlint', 'probe', 'spdx', 'syntax')"/>
<xsl:if test="not($meta-head = $predefined)">
<xsl:element name="defect">
<xsl:variable name="line" select="eo:lineno(@line)"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Syntax version mismatch

The `+syntax` meta specifies the version of the EO language that
the source code was written for. If the parser version is older
than the version specified in `+syntax`, the code may use features
that are not supported by the parser, leading to compilation errors.

The parser will refuse to process such files.

Incorrect (if parser version is 0.58.0):

```eo
+syntax 0.59.0

[] > foo
```

Correct (if parser version is 0.59.0 or newer):

```eo
+syntax 0.59.0

[] > foo
```
2 changes: 2 additions & 0 deletions src/main/resources/org/eolang/motives/metas/unknown-metas.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The following metas are supported:
* `+home`
* `+unlint`
* `+probe`
* `+syntax`

Incorrect:

Expand All @@ -29,6 +30,7 @@ Correct:
+architect yegor256@gmail.com
+rt jvm
+home https://earth.com
+syntax 0.59.0
+unlint unsorted-metas

[] > foo
Expand Down
175 changes: 175 additions & 0 deletions src/test/java/org/eolang/lints/LtSyntaxVersionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import java.io.IOException;
import org.eolang.parser.EoSyntax;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* Test for {@link LtSyntaxVersion}.
*
* @since 0.1.0
*/
final class LtSyntaxVersionTest {

@Test
void catchesMismatchWhenSyntaxIsNewer() throws IOException {
MatcherAssert.assertThat(
"should report error when +syntax version is newer than parser",
new LtSyntaxVersion("0.58.0").defects(
new EoSyntax(
"+syntax 0.59.0\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.<Defect>iterableWithSize(1)
);
}

@Test
void allowsWhenParserIsNewer() throws IOException {
MatcherAssert.assertThat(
"should not report error when parser version is newer",
new LtSyntaxVersion("0.60.0").defects(
new EoSyntax(
"+syntax 0.59.0\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.emptyIterable()
);
}

@Test
void allowsWhenVersionsMatch() throws IOException {
MatcherAssert.assertThat(
"should not report error when versions match exactly",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"+syntax 0.59.0\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.emptyIterable()
);
}

@Test
void usesDefaultVersionWhenNoArgCtor() throws IOException {
MatcherAssert.assertThat(
"with default ctor (0.0.0), +syntax 0.0.0 should not report error",
new LtSyntaxVersion().defects(
new EoSyntax(
"+syntax 0.0.0\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.emptyIterable()
);
MatcherAssert.assertThat(
"with default ctor (0.0.0), +syntax 0.0.1 should report error",
new LtSyntaxVersion().defects(
new EoSyntax(
"+syntax 0.0.1\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.<Defect>iterableWithSize(1)
);
}

@Test
void ignoresWhenNoSyntaxMeta() throws IOException {
MatcherAssert.assertThat(
"should not report error when no +syntax meta is present",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"# Foo.\n[] > foo\n"
).parsed()
),
Matchers.emptyIterable()
);
}

@Test
void reportsErrorOnInvalidSyntaxFormat() throws IOException {
MatcherAssert.assertThat(
"should report error for invalid +syntax format",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"+syntax alpha\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.<Defect>iterableWithSize(1)
);
}

@Test
void reportsErrorSeverityForInvalidFormat() throws IOException {
MatcherAssert.assertThat(
"invalid format defect should have error severity",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"+syntax alpha\n\n# Foo.\n[] > foo\n"
).parsed()
).iterator().next().severity(),
Matchers.equalTo(Severity.ERROR)
);
}

@Test
void reportsErrorSeverity() throws IOException {
MatcherAssert.assertThat(
"defect should have error severity",
new LtSyntaxVersion("0.58.0").defects(
new EoSyntax(
"+syntax 0.59.0\n\n# Foo.\n[] > foo\n"
).parsed()
).iterator().next().severity(),
Matchers.equalTo(Severity.ERROR)
);
}

@Test
void catchesMajorVersionMismatch() throws IOException {
MatcherAssert.assertThat(
"should detect when major version is newer",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"+syntax 1.0.0\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.<Defect>iterableWithSize(1)
);
}

@Test
void catchesPatchVersionMismatch() throws IOException {
MatcherAssert.assertThat(
"should detect when patch version is newer",
new LtSyntaxVersion("0.59.0").defects(
new EoSyntax(
"+syntax 0.59.1\n\n# Foo.\n[] > foo\n"
).parsed()
),
Matchers.<Defect>iterableWithSize(1)
);
}

@Test
void rejectsInvalidParserVersion() {
Assertions.assertThrows(
IllegalArgumentException.class,
() -> new LtSyntaxVersion("latest")
);
Assertions.assertThrows(
IllegalArgumentException.class,
() -> new LtSyntaxVersion("")
);
Assertions.assertThrows(
IllegalArgumentException.class,
() -> new LtSyntaxVersion(null)
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading