Skip to content

#1450: read a version only when the whole string is one - #1469

Open
opusstudiohq-max wants to merge 4 commits into
objectionary:masterfrom
opusstudiohq-max:fix-1450
Open

opusstudiohq-max wants to merge 4 commits into
objectionary:masterfrom
opusstudiohq-max:fix-1450

Conversation

@opusstudiohq-max

Copy link
Copy Markdown
Contributor

Closes #1450.

Version.CORE was anchored at the start only, and parsed used find, so it took the first three numbers it met and ignored the rest of the string:

1.2.3garbage  -> Optional[1.2.3]
1.2.3.4       -> Optional[1.2.3]
999.0.0oops   -> Optional[999.0.0]

That reaches a user through LtSyntaxVersion, the only caller, which documents that a non-SemVer +syntax value is ignored. A malformed value was instead compared as if it were a version, so +syntax 999.0.0oops could raise a hard error demanding a newer parser — a false positive produced by the parser being lenient, not by anything in the object.

The pattern now describes the whole spelling and matches requires all of it: the numeric core, then an optional SemVer pre-release or build suffix. 1.2.3-alpha.1 is still read by its numeric core, which is what newerThan compares.

Deliberately not tightened further. The components stay \d+ rather than SemVer's stricter no-leading-zero form, so nothing that parses today stops parsing except the three malformed shapes above. Making them strict would be a second, unrelated behaviour change.

It does not undo #1442. A component above Integer.MAX_VALUE still matches the pattern and is still turned into an empty result by the existing NumberFormatException guard. returnsEmptyWhenComponentOverflows pins that, so this change cannot quietly regress it.

Verified on my own fork before opening this, rather than here: mvn green on ubuntu, macos and windows, and the log shows org.eolang.lints.VersionTest — Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 on each, so the tests genuinely execute. Full suite 714 tests, 0 failures.

By the way, #1442 itself looks already fixed on master by 7c5a762; the issue is just still open.


🤖 Generated with Claude Code

`Version.CORE` was anchored at the start only, and `parsed` used
`find`, so it read the first three numbers it met and ignored whatever
followed. `1.2.3garbage`, `1.2.3.4` and `999.0.0oops` all came back as
valid versions.

That reaches a user through `LtSyntaxVersion`, which documents that a
non-SemVer `+syntax` value is ignored. A malformed value was instead
compared as if it were a version, so `+syntax 999.0.0oops` could raise
a hard error demanding a newer parser.

The pattern now describes the whole spelling and `matches` requires all
of it: the numeric core, then an optional SemVer pre-release or build
suffix. A real pre-release such as `1.2.3-alpha.1` is still read by its
numeric core, which is what the comparison wants.

The digits are still `\d+` rather than SemVer's stricter
no-leading-zero form, so nothing that parses today stops parsing except
the malformed spellings above. A component above `Integer.MAX_VALUE`
still matches the pattern and is still turned into an empty result by
the existing `NumberFormatException` guard; the last test pins that, so
this change cannot quietly undo objectionary#1442.
Qulice forbids concatenating string literals
(StringLiteralsConcatenationCheck), and the pattern was split across
three of them. It is one literal now, and short enough to stay within
the line limit.

The shape it accepts is unchanged for everything the tests cover, and
slightly tighter otherwise: a suffix must have at least one character
after its `-` or `+`, so `1.2.3-` is no longer read as a version, and a
pre-release followed by build metadata such as `1.2.3-rc.1+exp.sha.1`
is now accepted where the previous spelling stopped at the `+`.
The previous spelling of the pattern stopped at the plus, so this
shape was not accepted and nothing said so. It is accepted now, and
this pins it.

Copy link
Copy Markdown
Member

CI is green here and the change reads correctly for #1450, but I am not confident enough to merge it unreviewed.

Switching find() to matches() turns Version.parsed from lenient into strict, so every string that used to yield a version by its leading numeric core now yields Optional.empty(). That is the point of the issue, but the consequence depends on what the callers do with an empty result: if a caller treats "no version" as "nothing to check", a malformed +version that used to be compared will now silently skip its check instead of being reported. Worth confirming each call site reports rather than skips before this lands.

Assigning to @yegor256 for a look.


Generated by Claude Code

@yegor256 yegor256 self-assigned this Sep 16, 2026
…not compared

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@opusstudiohq-max

Copy link
Copy Markdown
Contributor Author

Checked every call site. Version.parsed is called from exactly two places, both in LtSyntaxVersion:

1. The +syntax meta (outdated(), the one you are asking about). On an empty result it yields no defect. That is not new: the lint already has two packs asserting exactly this — ignores-malformed-syntax-value.yaml (+syntax abracadabra → 0 defects) and ignores-overflowing-syntax-value.yaml (+syntax 999999999999999999999999.2.3 → 0 defects). "Malformed → ignored" is the tested contract, and this PR keeps it.

What the PR changes is which strings count as malformed. A numeric core with junk glued on — +syntax 99.0.0junk — used to be read by find() as 99.0.0 and compared; it is now ignored like abracadabra is. I have added ignores-syntax-with-trailing-junk.yaml so that boundary is pinned rather than implicit. A real SemVer suffix still parses: 99.0.0-rc1 matches the optional (?:[-+]…) group, reads as 99.0.0, and is still reported as newer than the parser — catches-newer-syntax covers that path.

2. The parser's own /object/@version (defects()). On an empty result the whole lint returns Collections.emptyList(). That skip predates this PR and is about a value the parser writes, not one a user types; under find() a malformed value there would have been read by its numeric core and compared against something, which is worse than skipping.

So the audit result is: one site reports-or-ignores exactly as it did, with the ignored set widened and now tested at its edge; the other skips as it did before. If you would rather the lint report a +syntax value it cannot read — which I think is the better behaviour, and closer to what you are describing — that flips a contract two existing packs assert, so I would do it as its own issue rather than fold it into #1450.

@opusstudiohq-max

Copy link
Copy Markdown
Contributor Author

I went through the call sites. Version.parsed has one production caller, LtSyntaxVersion (the Severity.parsed and LineOf.parsed hits are unrelated methods that happen to share the name). It calls it in two places: actual on line 37 is the parser's own @version, which is always well-formed SemVer, so strict-vs-lenient makes no difference there; declared on line 65, the +syntax tail, is the only place the change bites.

An empty Optional for declared produces no syntax-version defect, and that's the lint's stated contract — the class Javadoc (lines 18–21) says non-SemVer values are ignored, not flagged. So skip-rather-than-report for a malformed +syntax is the intended behaviour; this change just makes the code honour its own documentation.

On the specific worry that a malformed +version which used to be compared now silently skips: under the old find(), a tail like 1.2.3garbage was truncated to 1.2.3 and compared, and the defect message then printed the raw string back ("requires EO 1.2.3garbage or newer") — a comparison against a version the author never wrote, not a real report. Declining to invent a version from trailing junk is exactly what #1450 asks for. Reporting a malformed +syntax as a defect in its own right would be a separate lint, out of scope here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Version.parsed() incorrectly accepts invalid semver trailing inputs

2 participants