Automated breaking change migration

One nice thing about the Angular ecosystem is the concept of schematics. It is common and even expected whenever a high-quality library introduces breaking changes, that they include a script that can be run to migrate users to the new version with minimal interaction. The same tools can also be used when installing a library for the first time to scaffold out any boilerplate that is required to get started with the library.

I am interested if anyone has thought about bringing this concept to the Swift ecosystem. I know that Xcode includes some migrations tools - for example, this guide outlines how Xcode can be used to migrate to Swift 4.2. However, as far as I know, this tool is private to Xcode, and cannot be used by developers on other platforms. There also aren't tools available for library developers to do something similar.

I don't know if this kind of tool would be a good fit to include in some existing project, or if it would be its own thing. I think any solution would likely take advantage of SwiftSyntax.

5 Likes

As part of my work on swift-doc, I developed a tool using SwiftSyntax that prints public declarations on separate lines, allowing you to identify potentially breaking changes across releases using diff.

.swiftinterface files provide a canonical representation of APIs, but I'd like for there to be more tooling / representations available so that systems can work with this information in a language-agnostic way (i.e. without SwiftSyntax)

8 Likes

This may be a bit on the "dumb question" side, but how do you get to those .swiftinterface files - is that something that's being exposed/generated from the compilation process these days? (the terms are inherently difficult to google about due to overloads and conflicts).

I have seen some references to reverse engineering what Xcode does to create the generated header references, but hadn't tracked it down far enough to see what might be available from the built in swift toolchains to help track API surface, and particularly to see what's new, changed, different as packages evolve.

This is a very reasonable question! The Swift compiler can generate .swiftinterface files when library evolution is enabled. For example, if you do something like

swiftc -enable-library-evolution -emit-module-interface-path /path/to/MyModule.swiftinterface MyModule.swift

then you'll get a swiftinterface generated at that path. There's no need to reverse engineer anything, you can explore the stable commandline flags using swiftc --help. :slight_smile:

1 Like

I've been thinking about this more, and I started a project aiming to get a super simple proof of concept going. My goal would be to make a program that can update the name of a variable that gets renamed in a library. I started down the road of SwiftSyntax, but I quickly realized that that tool does not include the contextual awareness to know where a token was declared. It would be good enough much of the time, but more accuracy would definitely be better if I'm planning on modifying source code automatically.

I also am realizing that, especially for something like renaming a symbol, there is a lot of overlap between what I have in mind and LSP functionality (although, I don't think renaming is yet implemented in SourceKit-LSP). I fear that to do this right, I would need to bring in indexstore-db and use that to get the correct information about token locations.

@mattt The API Inventory looks helpful. I love the idea of using something like that to prevent accidental breaking changes, and to track changes between releases!

1 Like

I'm in the process of solving the same kind of problems for swift-doc. The syntactic parse approach gets us most of the way there, but it gets difficult to resolve references in, for example, code with nested type declarations. The usr field made available for symbols by IndexStoreDB solves this, but my investigations so far lead me to believe that the index can't fully replace the information you get from SwiftSyntax. I've written up more about this in this issue thread.

On the subject of LSP and indexes, have you take a look at LSIF?

Following up here in case someone else tries my same footsteps - getting the same details from a SwiftPM based build was a touch tricker, but thanks to help on Slack from @Aciid - doable. The invocation is:
swift build -Xswiftc=-enable-library-evolution --enable-parseable-module-interfaces

And the .swiftinterface files will be in the .build/x86_64-apple-macosx/debug directory

1 Like

Thanks for your responses! It definitely looks like we’re running into similar issues. I hadn’t seen LSIF before. It’s cool to see more standards like that rising.

If I do continue to look into this, I will likely stick with SwiftSyntax as the main source, and avoid getting stuck in the weeds of aiming for 100% accuracy. I’ll report back here if I make progress.

1 Like

@Aciid The --enable-parseable-module-interfaces flag should probably be changed to --emit-module-interface :sweat_smile:

You can use a library I used for myself to generate the module interface of a project or library or whichever you use, you can check it out the implementation in: https://github.com/minuscorp/ModuleInterface

Contributions are very welcome.