Skip to content

[Scala 3] convert implicit class to extension#868

Open
halotukozak wants to merge 25 commits into
AVSystem:scala-3from
halotukozak:03-01-implicit-class-to-extension
Open

[Scala 3] convert implicit class to extension#868
halotukozak wants to merge 25 commits into
AVSystem:scala-3from
halotukozak:03-01-implicit-class-to-extension

Conversation

@halotukozak

@halotukozak halotukozak commented Jun 1, 2026

Copy link
Copy Markdown
Member

Sweeps remaining implicit class XOps[A](...) extends AnyVal declarations AND the de-facto-implicit-class pattern (class XOps(...) extends AnyVal + companion implicit def xOps(x: T): XOps = new XOps(x)) to Scala 3 extension blocks. Phase-2 left many wrappers in the split-form post stubbing; both forms collapsed here.

The mongo UpdateOperatorsDsl.ForCollection HKT-receiver DSL uses given Conversion[…] instead of extension because plain extension methods cannot infer C[T] from named-argument call sites such as push(sort = ...) — fork commit eef0edce explanatory comment carried verbatim. TODO-marker added for future tightening once the inference regression is fixed.

Scope

Literal implicit classextension (initial sweep, 8 commits)

  • core/serialization: GenCodec.scala — 4 private value-class wrappers → extension
  • mongo/typed:
    • MongoEntityCompanion.macroDslExtensions, MongoPolyDataCompanion.macroDslExtensions
    • MongoFormat.{collectionFormatOps, dictionaryFormatOps, typedMapFormatOps}
    • MongoPropertyRef.{CollectionRefOps, DictionaryRefOps, TypedMapRefOps} (+ @targetName("typedMapApply") for erasure clash)
    • QueryOperatorsDsl.ForCollection (×2) → plain extension
    • UpdateOperatorsDsl.ForCollectiongiven Conversion (Pitfall 7)

De-facto-implicit-class follow-up sweep (12 commits)

Files where Phase 2 had split implicit class into class X extends AnyVal + implicit def xOps conversion shim — collapsed both into extension:

  • core: SharedExtensions.scala (18 wrappers), concurrent/TaskExtensions.scala, concurrent/DurationPostfixConverters.scala, misc/Opt.LazyOptOps, jiop/JCollectionUtils.pairIterableOps
  • core/jvm: jiop/{GuavaInterop, JOptionalUtils, JStreamUtils, Java8CollectionUtils, JavaTimeInterop}.scala
  • core/js: jsiop/JsInterop.scala (3 wrappers)
  • mongo: mongo/sync/MongoOps.scala (DBOps, FindIterableOps), mongo/reactive/ReactiveMongoExtensions.scala

JOptionalUtils consolidated to an OptionLike-constrained generic extension (fork shape) to avoid erasure clash + shadowing of Seq.asJava. SharedExtensionsUtils companion object eliminated; helper singletons promoted into object SharedExtensions.

Other

  • MIGRATION.md §3 entries (both sweeps)
  • style(scala-3,mongo): import targetName, drop fully-qualified annotation
  • docs(scala-3,mongo): TODO note for ForCollection conversion → extension

Fork-shape parity (verified per file)

File Our ext/given/impl Fork ext/given/impl
SharedExtensions 20/0/0 20/0/0 ✓
JStreamUtils 7/0/0 7/0/0 ✓
JOptionalUtils 8/0/0 8/0/0 ✓
MongoOps 2/0/0 2/0/0 ✓

Acceptance

  • git grep 'implicit class' core/src/main/scala mongo/jvm/src/main/scala mongo/js/src/main/scala → 0 hits
  • de-facto-implicit-class pattern grep → 0 hits in scope
  • sbt compile + Test/compile + scalafmtCheckAll green
  • No new @nowarn / -Wconf

Translated from fork origin/master commits eef0edce (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)

…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).
@halotukozak halotukozak added this to the Scala 3 milestone Jun 1, 2026
halotukozak and others added 17 commits June 1, 2026 18:29
@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`.
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`).
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