In the recent months I have been working on some updates for dart_apitool.
The main changes I did were:
After thinking more about the use cases for the option to store a public API model to a file in order to use it later to diff against a new version I came to the conclusion that this won’t be used in practice very often.
The amount of maintenance needed to support loading older versions of models and support comparing that with a new model based on the current code base was too high for that feature not being used by anyone.
So I decided to just remove that feature completely.
Dart_apitool already supported the analysis of real “classes”. As interfaces in Dart are just abstract classes they were supported automatically.
But there are other concepts in Dart, that are quite similar to classes but not exactly the same. Things like Mixins for example.
So I decided to change the API used in dart_apitool to handle all those entities as “interfaces”.
We had a problem with the sqflite package at work. It published an update to an internal package that broke our build. Using that as perfect feature request and integration test for dart_apitool I analyzed what exactly caused the issue.
Dart_apitool would not have been able to detect that breaking change. The reason for that is that it was not aware of the difference between “required” and “provided” interfaces.
I’m not sure if this is an official CS term, but I learned those terms in my past job. The difference is basically how interfaces are intended to be used.
Provided interfaces are interfaces defined by the package that also delivers implementations for it. This is mainly used to select between different behaviors or functionalities or to hide implementation details.
Required interfaces are also defined by the package but are meant to be implemented by the user of the package. A very obvious example for that would be a logger interface with the logging framework expecting the user of that package to implement that interface and pass the functionality in.
You may know a very similar differentiation by separating “API” and “SPI” (in Java land).
Sqflite extended an interface defined in an internal common package. This interface was a “required” interface that is used by users of the package to pass adapters in.
Adding something to an interface was handled as a non-breaking change by dart_apitool. If this had been a provided interface, that would have been OK as extending the functionality doesn’t break the user’s code.
But the moment an interface is a “required” one, means is probably implemented on the user side and adding something to it is breaking the user’s code.
Dart_apitool now detects if an interface is a “required” one or not. It does so by watching if an interface is used to be passed into the package (executable argument, writable field). This might also contain some false positives but better be safe than sorry I guess? 😉
The last bigger change I did was to add support for analyzing package dependencies.
I did not think of that in the beginning but after having some discussions with colleagues it became clear to me that changing the dependencies of a package can also break the user’s code and therefore (according to semver) should lead to a major version bump.
Dependencies can be changed in 3 ways:
- Removing a dependency
This is not a problem. The only situation that would lead to a breaking change on the user side is if the user would rely on that dependency transitively by using symbols from it without specifying that dependency. As this is considered bad practice I decided to ignore that case.
- Adding a dependency
This might not be obvious but adding a dependency has to be treated as a breaking change. From the package’s perspective we can not ensure that we don’t break the dependency tree on the user side by introducing a new dependency. Imagine that the user package depends on our package and on package A in version ^2.0.0. If our package now adds a dependency to package A in version ^3.0.0 we break the user’s code.
This is why adding is a breaking change by default. To allow for certain special cases (e.g. internal packages) to add new dependencies that implicitly are non-breaking there is a new option for dart_apitool that can change the mode dependencies are checked (none, allowAdding and strict (default)).
- Changing a dependency
Changing a dependency version has to directly reflect to the own version number. If the dependency makes a breaking jump our package also has to do a breaking jump.
For now I think dart_apitool has reached a state where it is feature complete. As the usage out there is rather low I will stay on a “0.” version for now as there are not many users that might reveal major issues in the way dart_apitool works.
And you never know. The next time the internet™️ breaks our build I might have another aspect that dart_apitool does not cover yet. 😉