Skip to content

Change Node.child and Node.attribute return type to immutable.Seq on 2.13+ #760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

lrytz
Copy link
Member

@lrytz lrytz commented May 19, 2025

XML values in Scala are immutable

Change the type of Node.child / Node.nonEmptyChildren and Node.attribute from collection.Seq to immutable.Seq in 2.13+.

A parent is added to Node which defines the old signatures for child / nonEmptyChildren. This (and the resulting bridge methods) ensures binary compatbility.

@lrytz lrytz force-pushed the imm-child branch 3 times, most recently from 6436666 to 08e6c71 Compare May 19, 2025 11:46
@lrytz
Copy link
Member Author

lrytz commented May 19, 2025

If we go ahead with this PR we'll need a new minor release (2.4.0).

There is more API with return type collection.Seq that probably always returns an immutable.Seq, e.g.

  • Metadata.value
  • NodeSeq.theSeq
  • MetaData.apply / MetaData.get / Node.attribute
  • TextBuffer.toText
  • BasicTransformer.transform

I'm happy take a look at those as well; I only did child for now because that's what is affecting the 2.13 migration I'm working on.

Change the type of `Node.child` and `Node.nonEmptyChildren` from
`collection.Seq` to `immutable.Seq` in 2.13+.

A parent is added to `Node` which defines the old signatures for
`child` / `nonEmptyChildren`. This (and the resulting bridge methods)
ensures binary compatbility.
@lrytz
Copy link
Member Author

lrytz commented May 19, 2025

For the record, there is an implicit conversion collection.Seq[Node] => NodeSeq and NodeSeq is a subclass of immutable.Seq on 2.13.

So the following works already without this PR

scala> val n: Node = <a><b/></a>
val n: scala.xml.Node = <a><b/></a>

scala> val c: collection.immutable.Seq[Node] = n.child
val c: Seq[scala.xml.Node] = Seq(<b/>)

But this fails:

scala> val c: collection.immutable.Seq[String] = n.child.map(_.toString)
                                                            ^
       error: type mismatch;
        found   : Seq[String] (in scala.collection)
        required: Seq[String] (in scala.collection.immutable)

@retronym retronym self-requested a review May 19, 2025 14:10
@lrytz
Copy link
Member Author

lrytz commented May 20, 2025

probably always returns an immutable.Seq

I took a closer look.

MarkupParser passes a NodeBuffer – I'll change that.

The compiler uses a NodeBuffer for XML literals and passes it directly to Elem($buf). That worked on 2.12, so child is actually a mutable collection. On 2.13 the implicit seqToNodeSeq makes it compile. Details here: scala/scala#11065

@lrytz lrytz force-pushed the imm-child branch 4 times, most recently from 68e70b7 to 80c3a0d Compare May 21, 2025 08:48
@lrytz lrytz changed the title Change Node.child return type to immutable.Seq on 2.13+ Change Node.child and Node.attribute return type to immutable.Seq on 2.13+ May 21, 2025
@lrytz lrytz force-pushed the imm-child branch 2 times, most recently from 8631ddb to c60701e Compare May 21, 2025 11:29
@lrytz
Copy link
Member Author

lrytz commented May 21, 2025

I also changed Node.attribute and the related MetaData.apply / MetaData.value / MetaData.get methods to return immutable.Seq.

And I changed NodeSeq.theSeq, even though this method is probably not used often as part of the public API. The reason is that NodeSeq extends immutable.Seq, and if theSeq is not immutable, it is essentially an unsafe immutable wrapper around a generic collection:

scala> val b = new NodeBuffer() += <b/>

scala> val x: NodeSeq = Elem(null, "a", Null, TopScope, true, NodeSeq.fromSeq(b): _*)
val x: scala.xml.NodeSeq = <a><b/></a>

scala> b += <c/>

scala> x
val res0: scala.xml.NodeSeq = <a><b/><c/></a>

I didn't change BasicTransformer.transform because that API is expected to be subclassed, changes would likely cause source incompatibilities.

@lrytz
Copy link
Member Author

lrytz commented May 21, 2025

This is ready, if you'd like to take a look @dubinsky.

Besides mima, I also used jardiff to compare the changes and it looks as expected.

@retronym
Copy link
Member

retronym commented May 23, 2025

One minor comment -- it might be more efficient to use VectorBuilder rather than NodeBuffer (which extends ArrayBuffer adding the utility method &+).

@lrytz
Copy link
Member Author

lrytz commented May 23, 2025

Right.. We have to keep NodeBuffer extends ArrayBuffer though, because of source and binary compatibility. So the code emitted by the compiler for literals will keep using ArrayBuffer.

We could use a VectorBuilder within scala-xml on 2.13, but it would involve changing public signatures, e.g. MarkupParser.content1(NamespaceBinding, NodeBuffer): Unit.

So it's probably not worth the effort...

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.

2 participants