[Scala 3] convert implicit class to extension#868
Open
halotukozak wants to merge 25 commits into
Open
Conversation
…e wrappers)
Converts the 4 internal value-class wrappers (IterableOps, PairIterableOps,
ListInputOps, ObjectInputOps) used inside GenCodec from Scala 2-era
`private implicit class … extends AnyVal` to Scala 3 `extension` blocks.
The wrappers are package-private internal helpers; conversion is call-site
transparent (no downstream API impact). Method bodies preserved byte-identical
including their `(implicit …)` parameter lists (slice 3.3 territory).
Translated from origin/master shape; fork's scala-3 overlay of GenCodec.scala
diverges substantially from current single-source layout, so the mechanical
`implicit class XOps[A](private val a: A) extends AnyVal { … }` →
`extension [A](a: A) { … }` transform was applied per CONTEXT.md's
"read fork's intent, apply the syntax change only" guidance.
…nversion (HKT receiver) `ForCollection[C[X] <: Iterable[X], T, R]` has a higher-kinded receiver `UpdateOperatorsDsl[C[T], R]`. Plain `extension` cannot infer `C`/`T` from named-argument call sites like `push(sort = ...)`, so the conversion is expressed as a `given Conversion[…]` instead (fork eef0edc shape). Hoisted `import MongoUpdateOperator._` to the object level (extension/given body cannot contain imports per Pitfall 6). Added `scala.language.implicitConversions` import to enable the implicit conversion. Translated from origin/master@eef0edce.
Two `implicit class ForCollection` blocks (in `VanillaQueryOperatorsDsl` and `QueryOperatorsDsl` companions) converted to plain `extension`. Unlike `UpdateOperatorsDsl`, these methods do not use named-argument defaults, so plain `extension` infers `C`/`T` successfully — matching fork eef0edc shape. Hoisted `import MongoQueryOperator._` to the object level (Pitfall 6). Renamed inner helper `format` → `elemFormat` to match fork's naming and avoid ambiguity with the dsl's own `format` member. Translated from origin/master@eef0edce.
…icit class → extension Converts the `implicit class macroDslExtensions(value: T)` block inside `BaseMongoCompanion` to a Scala 3 `extension` block. Receiver is concrete (`T`), no HKT inference issue — plain `extension` shape. Call-site transparent. Other `implicit def/val` declarations in this file (codec/format/isMongoAdtOrSubtype) are out of scope for slice 3.1 and deferred to slice 3.3 (`implicit def/val → given`). Translated from origin/master@eef0edce.
…plicit class → extension Converts the polymorphic `implicit class macroDslExtensions[T](value: D[T])` block inside `AbstractMongoPolyDataCompanion` to `extension [T](value: D[T])`. Same mechanical transform as MongoEntityCompanion's sibling; receiver is `D[T]` where `D[_]` is a kind parameter of the enclosing class — kind-parameter declaration preserved as-is per Pitfall 3. Translated from origin/master@eef0edce.
…on (3 ops) Three companion `implicit class … extends AnyVal` blocks (`collectionFormatOps`, `dictionaryFormatOps`, `typedMapFormatOps`) converted to plain `extension`. Each exposes a single `assume*` cast helper; no named-argument inference issue, plain `extension` shape suffices. Matches fork eef0edc shape. Translated from origin/master@eef0edce.
…RefOps/TypedMapRefOps implicit class → extension
Three companion `implicit class … extends AnyVal` blocks on
`MongoPropertyRef`-typed receivers converted to plain `extension`:
- `CollectionRefOps` → `extension [E, C[X] <: Iterable[X], T]`
- `DictionaryRefOps` → `extension [E, M[X, Y] <: BMap[X, Y], K, V]`
- `TypedMapRefOps` → `extension [E, K[_]]`
`TypedMapRefOps.apply[T](key: K[T])` and `DictionaryRefOps.apply(key: K)`
both erase to `apply(Object)`, so `@scala.annotation.targetName("typedMapApply")`
was added to the typed-map variant (Rule 1 - Bug auto-fix; required since
`extension` methods share the companion's namespace where the value-class
wrapper types previously kept them separate).
Translated from origin/master@eef0edce.
Adds §3 entries documenting Scala 3 `extension` migrations landed in slice 3.1:
- core/GenCodec.scala (4 private internal wrappers — transparent)
- mongo/typed/{MongoEntityCompanion, MongoPolyDataCompanion}.macroDslExtensions
- mongo/typed/MongoFormat.{collection,dictionary,typedMap}FormatOps (3 assume* helpers)
- mongo/typed/MongoPropertyRef.{Collection,Dictionary,TypedMap}RefOps (+ @TargetNAME)
- mongo/typed/QueryOperatorsDsl (Vanilla + non-Vanilla ForCollection — plain extension)
- mongo/typed/UpdateOperatorsDsl.ForCollection — given Conversion (HKT receiver Pitfall 7)
Each entry notes call-site impact (mostly transparent for extension-method resolution;
named-type reference to the old wrapper classes breaks).
@scala.annotation.targetName → @TargetNAME via scala.annotation.{tailrec, targetName} import. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tag the given Conversion workaround with a TODO[scala3-port] marker so the follow-up to convert ForCollection into a proper extension block is tracked once the HKT receiver-inference issue is addressed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…l → extension Sweeps the de-facto-implicit-class pattern (`implicit def publisherOps + final class PublisherOps extends AnyVal`) in `ReactiveMongoExtensions` to a single `extension [T](publisher: Publisher[T])` block. Translated from origin/master:mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala.
Sweeps the de-facto-implicit-class pattern in `MongoOps` (sync) — `implicit def dbOps + final class DBOps extends AnyVal`, `implicit def findIterableOps + final class FindIterableOps extends AnyVal` — to two `extension` blocks on `MongoDatabase` and `FindIterable[T]`.
Sweeps the de-facto-implicit-class pattern (`implicit def instantOps + class InstantOps extends AnyVal`) to a single `extension (instant: Instant)` block.
…extension Sweeps 7 de-facto-implicit-class pairs (jIteratorOps, jIterableOps, jCollectionOps, intJCollectionOps, longJCollectionOps, doubleJCollectionOps, jMapOps) to `extension` blocks.
Sweeps 3 de-facto-implicit-class pairs (DecorateFutureAsScala, DecorateSettableFutureAsScala, DecorateFutureAsGuava) to `extension` blocks. Inner self-reference `asScala.toUnit` → `gfut.asScala.toUnit` because Scala 3 extension bodies do not implicitly resolve own siblings without naming the receiver.
…ion (OptionLike unified)
Sweeps the de-facto-implicit-class pattern in JOptionalUtils — 11 pairs (optional2AsScala, optionalDouble2AsScala, optionalInt2AsScala, optionalLong2AsScala, option2AsJava, opt2AsJava, nopt2AsJava, optArg2AsJava, option2AsJavaDouble, option2AsJavaInt, option2AsJavaLong) — to Scala 3 `extension` blocks.
Per fork shape (origin/master scala-3 overlay), the 4 `O[T] → JOptional[T]` wrappers (Option, Opt, NOpt, OptArg) consolidate into ONE generic extension parameterized by `OptionLike.Aux[O[T], T]`. This avoids the erasure clash between value-class wrappers (Opt, NOpt, OptArg all erase to Object) AND avoids the source-level clash where a per-receiver `extension (option: Option[T]) { def asJava }` was being eagerly picked over `scala.collection.convert.AsJavaExtensions.asJava` for `Seq`/`Iterable` receivers.
Added `import JavaInterop._` in JavaInteropTest.scala because Scala 3 extension resolution prefers imported extensions (GuavaInterop.asScala for ListenableFuture, already imported in test) over package-object-mixed-in extensions (JOptionalUtils.asScala for JOptional, available via the package object). Without the explicit JavaInterop import, `JOptional(x).asScala` resolved against GuavaInterop.asScala first and short-circuited before trying JOptionalUtils.asScala. Mirrors fork test's import shape.
Sweeps 7 de-facto-implicit-class pairs (JStream2AsScala, JStream2AsScalaIntStream, JStream2AsScalaLongStream, JStream2AsScalaDoubleStream, JDoubleStream2AsScala, JIntStream2AsScala, JLongStream2AsScala) to `extension` blocks.
…ension Sweeps the de-facto-implicit-class pattern in TaskExtensions to `extension` blocks: `TaskOps` (local AnyVal) becomes `extension [T](task: Task[T])`; `TaskCompanionOps` (singleton object) becomes `extension (task: Task.type)`. Hoisted `import ObservableExtensions.observableOps` above the trait (Pitfall 6: extension body cannot contain imports). Internal cross-references rewritten (`traverseMap`, `currentTimestamp`) to use the receiver name `task`.
… + AnyVal → extension
Sweeps the de-facto-implicit-class pattern in `Opt.LazyOptOps` to `extension [A](opt: => Opt[A])`. The by-name receiver replaces the thunked `() => Opt[A]` value-class workaround; method bodies reference `opt` directly (Scala 3 by-name extension receiver is evaluated per access).
…nsion
Sweeps 18 de-facto-implicit-class pairs in SharedExtensions (UniversalOps, LazyUniversalOps, NullableOps, StringOps, IntOps, FutureOps, LazyFutureOps, FutureCompanionOps, OptionOps, TryOps, LazyTryOps, TryCompanionOps, PartialFunctionOps, SetOps, IterableOnceOps, IterableOps, PairIterableOnceOps, MapOps, IteratorOps, IteratorCompanionOps) to Scala 3 `extension` blocks.
Structural notes vs prior single-source layout:
- `SharedExtensionsUtils` companion object eliminated (no value-class wrappers needed).
- Helper singletons promoted to `object SharedExtensions` companion: `FutureCompanionOps`, `TryCompanionOps`, `IteratorCompanionOps` (real impls), `PartialFunctionOps.{NoValueMarker, NoValueMarkerFunc}` (the marker carrier), `MapOps.Entry`.
- `extension (companion: Future.type) { def eval, def traverseCompleted, def sequenceCompleted }` etc. delegate to the matching helper-object methods.
- Method bodies preserved verbatim (no `inline`/`using` adoption — that is slice 3.4/3.3 territory).
- Dropped orphan `OrderingOps` per existing MiMa exclusion `SharedExtensionsUtils.orderingOps` (was already unreachable via `implicit def`).
- Dropped dead `IteratorOps.distinct/distinctBy` per existing MiMa exclusion (stdlib-covered, removed in fork commit ade8d4a).
`SharedExtensions.MapOps.Entry` and `SharedExtensions.PartialFunctionOps.{NoValueMarker, NoValueMarkerFunc}` import paths preserved for external callers.
…AnyVal → extension
Sweeps the de-facto-implicit-class pattern in `JsInterop` to `extension` blocks: `extension [A](undefOr: UndefOr[A]) { def toOpt }` and `extension [A](opt: Opt[A]) { def orUndefined }`. This depends on the prior SharedExtensions sweep: with `OptionOps.toOpt` still wrapped as an implicit-class conversion, the new `UndefOr[A].toOpt` extension would shadow it via Scala 3 resolution preference (Opt[Option[Int]] inferred via `Option[Int] <: UndefOr[Option[Int]]` widening). Now that `Option[A].toOpt` is also an `extension`, both compete at equal rank and the compiler picks by receiver-type specificity (`Option[Int]` resolves to `extension [A](option: Option[A])`).
`jsDateTimestampConversions` (wrapping shared TimestampConversions) intentionally NOT converted — slice 3.3 territory (`implicit def → given Conversion`).
…ice 3.1 follow-up)
This was referenced Jun 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sweeps remaining
implicit class XOps[A](...) extends AnyValdeclarations AND the de-facto-implicit-class pattern (class XOps(...) extends AnyVal+ companionimplicit def xOps(x: T): XOps = new XOps(x)) to Scala 3extensionblocks. Phase-2 left many wrappers in the split-form post stubbing; both forms collapsed here.The mongo
UpdateOperatorsDsl.ForCollectionHKT-receiver DSL usesgiven Conversion[…]instead ofextensionbecause plain extension methods cannot inferC[T]from named-argument call sites such aspush(sort = ...)— fork commiteef0edceexplanatory comment carried verbatim. TODO-marker added for future tightening once the inference regression is fixed.Scope
Literal
implicit class→extension(initial sweep, 8 commits)GenCodec.scala— 4 private value-class wrappers →extensionMongoEntityCompanion.macroDslExtensions,MongoPolyDataCompanion.macroDslExtensionsMongoFormat.{collectionFormatOps, dictionaryFormatOps, typedMapFormatOps}MongoPropertyRef.{CollectionRefOps, DictionaryRefOps, TypedMapRefOps}(+@targetName("typedMapApply")for erasure clash)QueryOperatorsDsl.ForCollection(×2) → plainextensionUpdateOperatorsDsl.ForCollection→given Conversion(Pitfall 7)De-facto-implicit-class follow-up sweep (12 commits)
Files where Phase 2 had split
implicit classintoclass X extends AnyVal+implicit def xOpsconversion shim — collapsed both intoextension:SharedExtensions.scala(18 wrappers),concurrent/TaskExtensions.scala,concurrent/DurationPostfixConverters.scala,misc/Opt.LazyOptOps,jiop/JCollectionUtils.pairIterableOpsjiop/{GuavaInterop, JOptionalUtils, JStreamUtils, Java8CollectionUtils, JavaTimeInterop}.scalajsiop/JsInterop.scala(3 wrappers)mongo/sync/MongoOps.scala(DBOps, FindIterableOps),mongo/reactive/ReactiveMongoExtensions.scalaJOptionalUtils consolidated to an
OptionLike-constrained generic extension (fork shape) to avoid erasure clash + shadowing ofSeq.asJava.SharedExtensionsUtilscompanion object eliminated; helper singletons promoted intoobject SharedExtensions.Other
style(scala-3,mongo): import targetName, drop fully-qualified annotationdocs(scala-3,mongo): TODO note for ForCollection conversion → extensionFork-shape parity (verified per file)
Acceptance
git grep 'implicit class' core/src/main/scala mongo/jvm/src/main/scala mongo/js/src/main/scala→ 0 hitssbt compile + Test/compile + scalafmtCheckAllgreen@nowarn/-WconfTranslated from fork
origin/mastercommitseef0edce(mongo),a4ddad6b(SharedExtensions), and others per file.Slice: 3.1 of Phase 3 (Scala 3 syntax modernization)
Merge order: 3.1 → 3.2 → 3.3 → 3.4
Depends on: (none — first slice)
Base branch: upstream/scala-3 (not stacked)