All findings · Lucene finding · LFV-1119 · icutransform-direction-forward-case-reversed

ICUTransformFilterFactory accepts direction="Forward" but builds the reverse transliterator

Configure the ICU transform filter with direction="Forward" and the factory accepts it, then runs the transform backwards: with Cyrillic-Latin, Федерация stays Федерация instead of becoming Federaciâ, and nothing reports an error. Only exact lower-case forward (or no direction) works; every release since 4.3.0 has it.

Summary

Priority
Low, rank 498Every token is silently transformed in the wrong direction in every release since 4.3.0, but only in the opt-in analysis-icu ICUTransformFilterFactory when the optional direction value is spelled with a capital letter, such as Forward. The documented lower-case spelling and the default both work, so the trigger is rare.
Impact
silent wrong results
Since
4.3.0 (LUCENE-4877, 2013); a regression, 4.2.1 is correctintroduced 2013-04-01 · over 13 years
Affected
4.3.0 to 10.6.0 (118 release tags; 9 releases run, all fail) and main; not fixed upstream
Status
documented
Found by
non-formal: code reading of ICUTransformFilterFactory, witnessed by a one-test JUnit

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

One capitalised letter in an optional setting flips the transform.

The ICU transform filter in two lines

  1. ICUTransformFilter rewrites every token with an ICU transliterator named by id: Cyrillic-Latin turns Федерация into Federaciâ.
  2. The optional direction says which way it runs: forward (the default) as named, or reverse, the inverse (here Latin-Cyrillic, which leaves Cyrillic text unchanged).
Diagram of the bug: ICUTransformFilterFactory receives id=Cyrillic-Latin and direction="Forward". The validation step, which ignores case, accepts "Forward" and returns it unchanged. The next step tests "forward".equals("Forward"), which is false, so the factory builds the reverse (Latin-Cyrillic) transliterator. The token Федерация comes out unchanged instead of Federaciâ, with no error.
How the direction value travels through the factory: the case-insensitive check accepts Forward and passes it on unchanged, the case-sensitive "forward".equals test fails, and the reverse transliterator leaves Федерация unchanged instead of producing Federaciâ. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Build the ICU transform filter with id=Cyrillic-Latin and direction="Forward" (through new ICUTransformFilterFactory(args), CustomAnalyzer or the SPI name icuTransform) and analyze the token Федерация.
  2. You expect Federaciâ, as with direction="forward" or no direction at all. The factory checks the value case-insensitively, so Forward is accepted; sideways would be rejected with an error.
  3. But the check hands back your own spelling, and the factory then tests "forward".equals(direction). "Forward" is not equal to "forward", so it builds the reverse (Latin-Cyrillic) transliterator.
  4. You get Федерация unchanged, with no exception, at index and query time alike. FORWARD or any other capitalised spelling does the same; every spelling of reverse still works.

Who is affected

Users of the opt-in analysis-icu module who configure this filter with a capitalised direction (constructor, CustomAnalyzer, SPI name or a schema). Every token is transformed the wrong way at index and query time, silently. Whether searches still match depends on the transform. The default, forward and any reverse work, so the trigger is rare.

Mechanism, terms and numbers310 words

ICUTransformFilterFactory validates its optional direction argument case-insensitively (get(args, "direction", [forward, reverse], "forward", false), ICUTransformFilterFactory.java:51-52 at 5adff78bb168), so Forward and FORWARD are accepted while sideways is rejected. The validation returns the caller's own spelling (AbstractAnalysisFactory.java:190-191), and the factory then picks the direction with a case-sensitive "forward".equals(direction) (:53). Every accepted spelling other than exact lower-case forward therefore builds the reverse transliterator: with id=Cyrillic-Latin and direction="Forward", Федерация comes back unchanged instead of Federaciâ, at index and query time, with no error and the same result with assertions on and off. Until 4.2.1 the factory compared with equalsIgnoreCase("forward"); the LUCENE-4877 parameter-parsing refactor changed that in 4.3.0, and every release through 10.6.0 has it. A one-line equalsIgnoreCase fix restores the old meaning. The module is opt-in and the trigger is a capitalised optional value, so the priority is Low.

The two lines that disagree

  1. ICUTransformFilterFactory.java:51-52: get(args, "direction", Arrays.asList("forward", "reverse"), "forward", false). The last argument is caseSensitive = false, so Forward, FORWARD and FoRwArD pass validation. Values outside the list fail: IllegalArgumentException("Configuration Error: 'direction' value must be one of [forward, reverse]") (AbstractAnalysisFactory.java:195-196).
  2. AbstractAnalysisFactory.java:190-191: on an equalsIgnoreCase match, get returns s, the caller's own spelling, not the allowed value.
  3. ICUTransformFilterFactory.java:53: int dir = "forward".equals(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;. For "Forward" the test is false, so :54 builds Transliterator.getInstance(id, REVERSE).
  4. The one transliterator serves both create() and normalize() (:65-73), so index-time analysis, query-time analysis and Analyzer.normalize all run the reverse transform.

This factory is the only caller in lucene/ that passes caseSensitive=false.

Other transforms flip the same way

Hiragana-Katakana with direction="Forward" through CustomAnalyzer returns ひらがな instead of ヒラガナ; Upper with "FoRwArD" turns MiXeD into mixed instead of MIXED. The default, forward, reverse and ReVeRsE behave as documented in the same runs.

Exposure and likelihood136 words
  • Who is hit. Analyzers built from factory arguments: the public Map constructor, TokenFilterFactory.forName("icuTransform", ...), CustomAnalyzer.builder().addTokenFilter("icuTransform", ...) or a schema-driven configuration, with direction spelled with any capital letter.
  • What happens. The stock inverse ICU transform runs on every token, at index and query time. The indexed terms are in the wrong script or form. One analyzer used on both sides applies the same wrong transform to both, so whether searches still match depends on the transform (by reading, not measured).
  • Not affected. The parameter omitted, exact forward, and every spelling of reverse.
  • Scale and likelihood. One token shows it; no data size or load is involved. Rare: the javadoc and the Solr reference guide spell the values in lower case, and the dated searches (2026-11-02 and 2026-11-03) found no report of it since 2013.

Minimal repro

One JUnit test method on the public factory API: build the factory with id=Cyrillic-Latin and direction="Forward", analyze one token, expect the Latin form. Deterministic, single thread; it fails with assertions on and off.

TestICUTransformForwardDirection.java (lucene/analysis/icu, package org.apache.lucene.analysis.icu, 1 test)35 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.analysis.icu;

import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestICUTransformForwardDirection extends BaseTokenStreamTestCase {

  // "Forward" passes validation, so it must transliterate like the default "forward".
  public void testCapitalizedForward() throws Exception {
    Map<String, String> args =
        new HashMap<>(Map.of("id", "Cyrillic-Latin", "direction", "Forward"));
    ICUTransformFilterFactory factory = new ICUTransformFilterFactory(args);
    assertTokenStreamContents(
        factory.create(whitespaceMockTokenizer(new StringReader("Федерация"))),
        new String[] {"Federaciâ"});
  }
}
Command
cp $REPRO/TestICUTransformForwardDirection.java $LUCENE/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/
cd $LUCENE && ./gradlew :lucene:analysis:icu:test --tests org.apache.lucene.analysis.icu.TestICUTransformForwardDirection -Ptests.seed=DEADBEEF
# add -Ptests.asserts=false: the same failure
Expected
Federaciâ, the forward Cyrillic-to-Latin result, as with direction="forward".
Observed
term 0 expected:<[Federaciâ]> but was:<[Федерация]>: the reverse transform ran; the same with assertions off.

All files of this repro: repro/LFV-1119/

More files, environment, full output134 words

TestICUTransformForwardDirection (one method, extends BaseTokenStreamTestCase, package org.apache.lucene.analysis.icu of the lucene/analysis/icu tests). It is the hunt's minimal repro re-wrapped to Lucene's 100-column format, with its one comment line reworded (same code); checkGoogleJavaFormat accepts it. As the regression test, the same method fits in TestICUTransformFilterFactory.

The expected value Federaciâ is the forward Cyrillic-Latin output that the upstream testForwardDirection asserts for the default direction.

Environment

lucenemain 5adff78bb168 (12.0.0-SNAPSHOT, icu4j 78.3): fails; 4.3.0 to 10.6.0 (9 releases run): fail; 4.0.0 and 4.2.1: pass
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (nothing platform-specific)
Command (environment)
cp $REPRO/TestICUTransformForwardDirection.java $LUCENE/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/
cd $LUCENE && ./gradlew :lucene:analysis:icu:test --tests org.apache.lucene.analysis.icu.TestICUTransformForwardDirection -Ptests.seed=DEADBEEF

Observed output

TestICUTransformForwardDirection > testCapitalizedForward FAILED
    org.junit.ComparisonFailure: term 0 expected:<[Federaciâ]> but was:<[Федерация]>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:5FEDD4318FC2297A]:0)
        at app/junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:117)
        at app/org.apache.lucene.test_framework@12.0.0-SNAPSHOT/org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
        ...
        at app//org.apache.lucene.analysis.icu.TestICUTransformForwardDirection.testCapitalizedForward(TestICUTransformForwardDirection.java:31)
BUILD FAILED

# -Ptests.asserts=false: the same ComparisonFailure

Why it looks unintended

  • The factory opts into case-insensitive validation (the only caller that does), so Forward is accepted; the javadoc names just two values, with no case rule.
  • Through 4.2.1 it compared with equalsIgnoreCase("forward") under the same javadoc; the 4.3.0 refactor aimed to reject invalid arguments, not to change valid ones.
  • No test, CHANGES or MIGRATE entry pins the current behaviour.
The full argument165 words
  • Contract. Javadoc (ICUTransformFilterFactory.java:32): "direction (optional): Either 'forward' or 'reverse'. Default is forward." The code accepts any case (:51-52) and rejects other values loudly; an accepted spelling of forward that means reverse is documented nowhere.
  • History. The factory was added by SOLR-2210 (5c6b4f4f650, 2010) with direction == null || direction.equalsIgnoreCase("forward") -> FORWARD, equalsIgnoreCase("reverse") -> REVERSE, else an exception; 4.2.1 still has that (ICUTransformFilterFactory.java:56-62 at the 4.2.1 tag). LUCENE-4877 (#5942; 4.3.0 CHANGES: "Throw exception for invalid arguments in analysis factories") replaced it with the two current lines. Its thread proposed the case-insensitive get overload but never mentions this factory or direction.
  • Tests. TestICUTransformFilterFactory uses only the default direction (:41-49) and lower-case reverse (:57).
  • Caveat. No document promises that the values are case-insensitive; the javadoc only spells them in lower case. The case for "not intended" rests on the code's own opt-in (:51-52) and on the pre-4.3 behaviour: strong evidence, not a documented guarantee.
The devil's-advocate check79 words

Strongest case for "intended": the javadoc spells the values in lower case, and sibling factories compare their values case-sensitively (PatternReplaceFilterFactory.java:56, ICUNormalizer2FilterFactory.java:58, :63), so Forward is a user error. Why it fails: those siblings also validate case-sensitively, so a wrong case gets an exception there; this factory alone opts into case-insensitive acceptance and then misreads what it accepted. The same lower-case javadoc stood from 2010 to 2013 beside case-insensitive parsing, so it was never a case-sensitive contract.

Suggested fix

Recommended Compare case-insensitively at the fix site (fix A)

Compare with "forward".equalsIgnoreCase(direction) in ICUTransformFilterFactory (one line, wrapped to two by the formatter). The repro then passes with assertions on and off, the whole analysis-icu test suite passes (159 tests, 10 skipped) and the format check is clean.

    int dir =
        "forward".equalsIgnoreCase(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;
All options and trade-offs3 options · 315 words

Option A was built and checked through gradle on 5adff78bb168; B and C were not run. PR #15 (LUCENE-8972, open on 2026-11-03, last updated 2025-10-18) copies the same two lines into a new ICUTransform2CharFilterFactory, so a fix should cover both.

Compare case-insensitively at the fix site (fix A) (validated by the checks) (recommended)

Replace "forward".equals(direction) with "forward".equalsIgnoreCase(direction) at ICUTransformFilterFactory.java:53, which restores the pre-4.3 meaning.

    int dir =
        "forward".equalsIgnoreCase(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;

Trade-offs

Every spelling that works today keeps working (reverse, Reverse and REVERSE still mean reverse); only the capitalised spellings of forward change, to what they always meant before 4.3.0. Add a CHANGES.txt line under Bug Fixes.

What the checks validated

  • Gradle in a scratch worktree at 5adff78bb168, seed DEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (on and off).
  • With the fix, all of :lucene:analysis:icu:test: 159 tests, 0 failures, 10 skipped (TestICUTokenizerCJK), including TestICUTransformFilterFactory (4) and TestICUTransformFilter (8). :lucene:analysis:icu:checkGoogleJavaFormat passes with the fix and the repro in place.
  • Earlier hunt runs of the same logic (on one unwrapped line) passed the finder's test, the cross-model reviewer's two tests and the upstream TestICUTransformFilterFactory (4 of 4); the independent reproducer's test was not run with the fix.

Return the canonical value from AbstractAnalysisFactory.get(..., caseSensitive=false) (not built)

Return allowedValue instead of s at AbstractAnalysisFactory.java:191, so callers always see the canonical spelling.

Trade-offs

Would also protect future callers such as the char-filter factory in PR #15, but it changes a public method of lucene/core that downstream factories may call, so it needs its own review; A is the minimal fix.

Make the validation case-sensitive (not recommended, not built)

Pass true instead of false at ICUTransformFilterFactory.java:52, so Forward fails loudly.

Trade-offs

Working Reverse and REVERSE configurations would start throwing: a behaviour break for users who are not affected today.

Downstream note

Solr schemas configure this same factory (its reference guide documents the direction option, read 2026-11-02), so a capitalised value there is hit the same way; as read on 2026-11-02, Elasticsearch and OpenSearch parse their own setting and do not use this factory.

How it was found

Code reading of ICUTransformFilterFactory (non-formal): a case-insensitive check followed by a case-sensitive comparison of the same value stood out, and a one-test JUnit on stock main witnessed the reversed transform. Independent checks then reproduced it.

The method in full116 words

Method: code reading of ICUTransformFilterFactory's parameter parsing (2026-11-02, main 5adff78bb168). The reader noticed that direction is validated with caseSensitive=false and then compared with "forward".equals, wrote a one-test JUnit with direction="Forward" on the public constructor, and ran it on main (fails with assertions on and off), 10.6.0 and 9.12.3, plus the one-line fix (passes).

An independent reproducer wrote its own test from the claim through CustomAnalyzer and the SPI name with a different transform; a cross-model reviewer (OpenAI GPT-5.6, refute lens, executed) wrote two more; a devil's-advocate verifier argued the intended case; an adjudication re-ran the tests and the fix. A final repro check then reduced the repro to one method and one token.

Prior reports, history and checks

Prior reports

No matching prior report found in apache/lucene issues and PRs (including the JIRA archive), the mailing lists, downstream trackers or the web (searched 2026-11-02, re-checked 2026-11-03). PR #15 (open on 2026-11-03) copies the same pattern into a new char-filter factory.

Searches4 searches

No item found reports this bug; the nearest items are background. Search limits: keyword search (50 results per GitHub query); private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene, incl. the migrated JIRA archive)2026-11-02, re-checked 2026-11-0316 scripted queries and about 35 manual phrasings (ICUTransformFilterFactory, icuTransform direction, transliterator direction, ...); the file lists of all 408 open PRs and of every PR updated since 2026-10-31; commit searchno report; #10663 (module descriptors) only lists the class
Related items2026-11-02the full LUCENE-4877 discussion (23 comments) and PR #15's reviews and conversation#5942 / LUCENE-4877 is the origin and never mentions this factory or direction; PR #15 (LUCENE-8972, open on 2026-11-03) copies the two lines without discussion; #10015, #3484, #3584 are background
Mailing lists and JIRA2026-11-02dev@, java-user@, issues@, commits@, solr-user@ (all 13 mails naming the factory), users@solr; 4 JQL queries on LUCENE and SOLRconfiguration questions only; nothing about direction or case
Downstream and web2026-11-02apache/solr, apache/lucene-solr, apache/lucenenet, Elasticsearch, OpenSearch; GitHub code search; 6 web queriesno report; Lucene.NET has the same pattern; Elasticsearch and OpenSearch use their own factories

History

Introduced in 4.3.0 by the LUCENE-4877 parameter-parsing refactor (2013), which kept the case-insensitive check but compared the result case-sensitively; 4.2.1 is correct. Every release from 4.3.0 through 10.6.0 and main is affected; not fixed upstream.

Versions tested6 rows

Introduced by LUCENE-4877 "Fix analyzer factories to throw exception when arguments are invalid" (#5942; SVN commits, no PR): branch_4x 14a85ce98c9 (Robert Muir, 2013-04-06, "LUCENE-4877: additional improvements"), the copy that shipped first, in 4.3.0 and every later 4.x and every 5.x release; trunk 6de43dea0af (Steven Rowe, 2013-04-01, "LUCENE-4877: more param parsing verbs"), in every release from 6.0.0. Both replaced the equalsIgnoreCase parsing with the two current lines and added the get(..., caseSensitive) overload. git tag --contains: 118 release tags. The equals line has not changed since; the last commit on the factory is 7f8b7ffbcad (2021, reformatting). Upstream main 1c2072f0284 (2026-11-02) has both files byte-identical to 5adff78bb168.

4.2.1 and 4.3.0 ran with the same icu4j 49.1, so the flip comes from Lucene alone. Runs before 9.0 use a main() probe through the public factory API, since the test's org.apache.lucene.tests.analysis package exists only from 9.1.0 (LUCENE-10301).

Versions and branches executed, with results
Version or commitVerdictResult
main 5adff78bb168 affectedfails through gradle and a standalone runner, assertions on and off
10.6.0, 10.5.2, 9.12.3 affectedthe JUnit tests fail the same way (Российская / ひらがな unchanged)
8.11.4, 7.7.3, 6.6.6, 5.5.5, 4.10.4 affectedmain() probe: Forward accepted, gives the reverse output
4.3.0 first affectedForward gives Российская (reverse), forward gives Rossijskaâ; icu4j 49.1
4.2.1, 4.0.0 not affectedForward gives Rossijskaâ, the same as forward; icu4j 49.1
fix A on 5adff78bb168 not affectedrepro passes; analysis-icu suite 159 tests, 0 failures

Independent checks

A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, refute lens), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it and agree on the root cause. This doc's gradle runs reproduced it on main and validated the fix.

Checks and verdicts263 words

The cross-model review ran on main ea57360912ed, where ICUTransformFilterFactory.java and AbstractAnalysisFactory.java are byte-identical to 5adff78bb168; its tests were then re-run on 5adff78bb168 (fail with assertions on and off). Every other check ran on 5adff78bb168.

Each independent check and its verdict
CheckVerdictWhat it found
finder (code reading) reproducedOne-test JUnit fails on main (assertions on and off), 10.6.0 and 9.12.3; the one-line fix passes it.
independent reproducer realOwn test through CustomAnalyzer and the SPI name with Hiragana-Katakana: Forward gives ひらがな instead of ヒラガナ; fails on main, 10.6.0, 10.5.2 and 9.12.3; the lower-case control passes.
cross-model review, refute lens (OpenAI GPT-5.6, executed) real (high)Own tests (Upper with FoRwArD; forName with FORWARD) fail with assertions on and off, and the Upper test also over five iterations; controls pass; the fix passes them and the upstream test (4 of 4); intended argument weak.
devil's-advocate verifier realsideways is rejected while Forward and FORWARD are accepted and act as reverse; the lower-case-javadoc defence fails.
adjudication confirmed (high), LowRe-ran the finder's and the reproducer's tests on clean main (fail), and the finder's test with the fix (pass).
separate re-run, audit and final repro check confirmed (Low)Fails on main with assertions on and off; the final check reduced the repro to one method and one token, which passes with the fix.
this doc reproduced; fix A validatedGradle without the fix fails (on, off); with it the repro and 159 analysis-icu tests pass and the format check is clean.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,320 words

Finding record

LFV-1119, snapshot of 2026-11-06

Registry title
ICUTransformFilterFactory accepts direction="Forward" or "FORWARD" without error (it validates the value case-insensitively) but then builds the reverse transliterator, so every token is silently transformed the wrong way (regression of LUCENE-4877, since 4.3.0)
Area
analysis / icu (ICUTransformFilterFactory: 'direction' parameter parsing through AbstractAnalysisFactory.get(..., caseSensitive=false))
Priority
Low, rank 498: Every token is silently transformed in the wrong direction in every release since 4.3.0, but only in the opt-in analysis-icu ICUTransformFilterFactory when the optional direction value is spelled with a capital letter, such as Forward. The documented lower-case spelling and the default both work, so the trigger is rare.
Impact class
silent wrong results
Impact note
Only analyzers built with the opt-in analysis-icu module's ICUTransformFilterFactory (public Map constructor, TokenFilterFactory.forName("icuTransform", ...), CustomAnalyzer or a schema-driven configuration) whose optional direction value is spelled other than exact lower-case 'forward' (for example 'Forward' or 'FORWARD') are hit: the value passes validation and the factory silently applies Transliterator.REVERSE to every token at index and query time, so terms come out in the wrong script or form, identically with -ea and -da [exec]; whether searches still match depends on the transform, because one analyzer used on both sides applies the same wrong transform to both [read, not measured]; no scale is needed, every release from 4.3.0 through 10.6.0 is affected, and the default (parameter omitted), 'forward' and any spelling of 'reverse' work.
Summary
You build an ICUTransformFilterFactory with id=Cyrillic-Latin and direction="Forward", which the factory accepts because it checks direction case-insensitively, and analyze 'Российская Федерация'. You expect the forward Cyrillic-Latin transliteration [Rossijskaâ, Federaciâ], as with direction="forward". You get the Cyrillic tokens back unchanged, with no error: the validation hands back the caller's own spelling, the factory then tests "forward".equals(direction), and every accepted spelling other than exact lower-case 'forward' builds the reverse (Latin-Cyrillic) transliterator.
Introduced
14a85ce98c91ed8b03b98379ac8e0db086d25120 (branch_4x, svn r1465225, Robert Muir, 'LUCENE-4877: additional improvements'; the copy that shipped first, in 4.3.0, and the one in every 4.x and 5.x release) and trunk commit 6de43dea0af9d0407d7c218d077c6025b04a4282 (svn r1463335, Steven Rowe, 'LUCENE-4877: more param parsing verbs'; in every release from 6.0.0), both for LUCENE-4877 'Fix analyzer factories to throw exception when arguments are invalid' (migrated as apache/lucene#5942; SVN commits, no GitHub PR). Both replaced the factory's own parsing (direction == null || direction.equalsIgnoreCase("forward") -> FORWARD; equalsIgnoreCase("reverse") -> REVERSE; else IAE) with get(args, "direction", [forward, reverse], "forward", false) plus "forward".equals(direction), and added that get() overload, which returns the caller's spelling on an equalsIgnoreCase match; the equals line is unchanged since [read: git show of both commits; git merge-base --is-ancestor against the release tags; exec: the history probe passes on 4.2.1 and fails on 4.3.0 with the same icu4j 49.1] (2013-04-01 (author date of the trunk commit 6de43dea0af); 2013-04-06 (author date of the branch_4x commit 14a85ce98c9, which shipped first))
First affected version
4.3.0 [exec: the history probe fails on 4.3.0 and passes on 4.2.1, both with icu4j 49.1; releases/lucene-solr/4.2.1 contains neither commit and still has the equalsIgnoreCase parsing], so this is a regression of LUCENE-4877
Affected versions
every release from 4.3.0 through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT): 118 release tags in the history clone, 4.3.0-5.5.5 through 14a85ce98c9 and 6.0.0-10.6.0 through 6de43dea0af [read: git tag --contains]. Executed: the one-test JUnit fails on main (-ea and -da, also in the re-run), 10.6.0 (-ea and -da) and 9.12.3 (-ea and -da); the independent reproduction's SPI-path test fails on main (-ea and -da), 10.6.0, 10.5.2 and 9.12.3 (-ea); a main() history probe through the public factory API fails on 4.3.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.6.0 and main and passes on 4.0.0 and 4.2.1 (-ea) [exec]. Lucene 4.0.0-4.2.1 are not affected; before 4.0 the factory was a Solr class. Not new in 10.6 (10.5.2 fails). Not fixed upstream: upstream main 1c2072f0284 (author date 2026-11-02) has ICUTransformFilterFactory.java and AbstractAnalysisFactory.java byte-identical to 5adff78bb168 (blobs 3880c4a6816 and 53a456aeaea, also at 10.6.0 and ea57360912ed), and the newest commit on the factory is 7f8b7ffbcad (author date 2021-01-03, reformatting) [read through the GitHub API, 2026-11-02]. Executed on Linux aarch64 with JDK 25.0.3 only.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading
Upstream and prior reports
none found (2026-11-02, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's GitHub searches; rechecked the same day): a scripted prior-report search over 16 GitHub issue and PR queries on apache/lucene, 3 commit queries, 2 downstream-repository queries, 20 mailing-list queries (dev@, java-user@, issues@, commits@), 4 JIRA-archive queries and 6 web queries (541 distinct hits, none with high overlap), plus manual GitHub searches on apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch and OpenSearch, GitHub code search, the file lists of all 408 open apache/lucene PRs and of the 31 PRs updated since 2026-10-31 (none touches ICUTransformFilterFactory.java or AbstractAnalysisFactory.java), the full LUCENE-4877 discussion (#5942), JIRA full-text queries over SOLR and LUCENE and the solr-user@ threads on the factory: no report of a mixed-case direction building the reverse transliterator. Same-day recheck: GitHub search on apache/lucene for 'ICUTransformFilterFactory' (only #10663, module descriptors), 'icuTransform direction' (0), PRs 'ICUTransform' (0), 'transliterator direction' (0), and PRs or issues updated since 2026-10-31 that mention icu, analysis factory or transliterator (0); upstream main 1c2072f0284 has both files byte-identical to 5adff78bb168. Related, not reports: #5942 / LUCENE-4877 (the change that introduced it; its discussion never mentions this factory or the direction parameter), open PR #15 / LUCENE-8972 (its new ICUTransform2CharFilterFactory copies the same two lines, without discussion), #10015, #3484 and #3584.
Lucene commit tested
main 5adff78bb168 (2026-11-02) through gradle in a scratch worktree with assertions on and off, without and with the fix (the repro, the whole analysis-icu test suite, checkGoogleJavaFormat), and through a standalone JUnit runner on the main jars. The hunt ran the test, an independent CustomAnalyzer test or a main() probe through the public factory API on 4.0.0 and 4.2.1 (pass) and on 4.3.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and 10.6.0 (fail). Only Linux aarch64 with OpenJDK 25.0.3 was run.

Lucene and upstream sources

Finding record
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:28-33 (at 5adff78bb168) (class javadoc: 'direction (optional): Either 'forward' or 'reverse'. Default is forward.'; no case rule), :48-58 (Map constructor: :51-52 get(args, "direction", Arrays.asList("forward", "reverse"), "forward", false); :53 "forward".equals(direction) ? Transliterator.FORWARD : Transliterator.REVERSE, fix site; :54 Transliterator.getInstance(id, dir))
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:174-198 (at 5adff78bb168) (get(args, name, allowedValues, defaultVal, caseSensitive): :190-191 returns the caller's own spelling s on an equalsIgnoreCase match; :195-196 IllegalArgumentException "Configuration Error: ... must be one of ..." for other values); grep of lucene/ on main: ICUTransformFilterFactory.java:52 is the only non-test caller that passes caseSensitive=false
  • lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49 (at 5adff78bb168) (testForwardDirection uses the default direction and expects [Rossijskaâ, Federaciâ]), :57 (the only explicit direction in the upstream tests is lower-case 'reverse'; no test passes an explicit 'forward' or a mixed case)
  • https://github.com/apache/lucene/issues/5942 (LUCENE-4877), https://github.com/apache/lucene/pull/15 (LUCENE-8972, open; ICUTransform2CharFilterFactory copies the pattern), https://github.com/apache/lucene/issues/10015, https://github.com/apache/lucene/issues/10663, read 2026-11-02
The finding in 30 seconds
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:48-58 (at 5adff78bb168) (constructor; :51-52 validation, :53 fix site, :54 getInstance), :65-73 (create and normalize)
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:174-198 (at 5adff78bb168) (get with caseSensitive; :190-191 return s; :195-196 IllegalArgumentException)
  • grep of lucene/ on main 5adff78bb168: ICUTransformFilterFactory.java:52 is the only get(..., false) caller
Minimal repro
  • lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49 (at 5adff78bb168) (expected forward output)
Why it looks unintended
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:32 (at 5adff78bb168) (javadoc), :51-53
  • lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49, (at 5adff78bb168) :57
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/pattern/PatternReplaceFilterFactory.java:56; (at 5adff78bb168) lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUNormalizer2FilterFactory.java:58 (case-sensitive validation), :63 ("compose".equals(mode))
  • https://github.com/apache/lucene/issues/5942 (LUCENE-4877 discussion)
Suggested fix
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:190-191 (at 5adff78bb168)
Diagrams
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:51-54 (at 5adff78bb168)
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:190-191 (at 5adff78bb168)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

The diagram illustrates the repro's scenario (main 5adff78bb168, ICUTransformFilterFactory.java:51-54, AbstractAnalysisFactory.java:190-191).

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.