Basic KSP validation

The set up and usage of the KSP (Kotlin symbol processing) api can be quite intimidating to begin with but it can be quite rewarding once you’ve found your feet. I started with KSP in my day job for an interesting project to generate code to target JVM/JS that is then compiled by KMP (Kotlin Multiplatform) and eventually run across SpringBoot, Android and iOS. The side benefit of doing a fairly deep dive is then seeing other interesting opportunities to utilise the technology.

This post recreates some validation that is not useful outside the scope of my project but really showcases how with a few lines of code you can get powerful validation for even niche use cases.


Context

One of the modules in my project needed to enforce that we only used properties with no backing field e.g. fields defined as computed properties val label get() = "Some Label". This module has lots of classes with many properties making it difficult to manually audit and keep on top of. After doing the first manual audit to verify everything was computed I had an idea to use KSP to do this for me in future.


Processor

In my use case the whole module needs the same validation applied so I don’t need to be precise about finding specific elements of the code. My high level strategy is to do the following:

  1. Enumerate every file in the project
  2. For each file enumerate all the classes it contains
  3. For each class enumerate all properties
  4. Log a helpful error for every property that has backing storage

There are different ways you can write your KSP code - for cases where you are collecting information about your code to act on you might want to use the visitor pattern and helper classes available. To achieve my goal I can forgo using visitors as the steps above map nicely onto the KSP api and I don’t need to collect any information I’m just going to log errors outright to cause a failed compile.

The code (with markers) ended up something like:

class Processor(private val logger: KSPLogger) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
/* 1 */ resolver.getAllFiles().forEach { file ->
/* 2 */     file.declarations .filterIsInstance<KSClassDeclaration>().forEach { klass ->
/* 3 */         klass.getAllProperties().forEach { property ->
                    if (property.hasBackingField) {
                        val message = """
                            All properties have to be computed. e.g.
                            
                            - val ${property.simpleName.asString()} = ...
                            + val ${property.simpleName.asString()} get() = ...
                        """.trimIndent()
/* 4 */                 logger.error(message, property)
                    }
                }
            }
        }
        return emptyList()
    }
}

The handful of lines above pack a big punch. If any property is added that has a backing field the build will fail to compile and output an error message with some useful tips and the exact source location e.g.

[ksp] .../example/src/main/kotlin/com/paulsamuels/Example.kt:4: All properties have to be computed. e.g.

- val example = ...
+ val example get() = ...

Full disclosure

The code above is the simplest possible processor I could write and in reality there is a little more work involved in getting everything wired up but it’s not too difficult and is very well documented in the KSP quickstart. I won’t repeat the quickstart guide as this post will probably just go out of date but as a rough illustration of how little code it takes to wire things up I have the following

processor/
├── build.gradle.kts
└── src
    └── main
        ├── kotlin
        │   └── com
        │       └── paulsamuels
        │           ├── Processor.kt
        │           └── ProcessorProvider.kt
        └── resources
            └── META-INF
                └── services
                    └── com.google.devtools.ksp.processing.SymbolProcessorProvider

The META-INFO is a one line file and the ProcessorProvider.kt is just

class ProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment) = Processor(environment.logger)
}

This is the entirely of the processer and then you just need to hook this up to the target you want to process.


Conclusion

The actual validator above probably has no practical use outside of my project but hopefully it illustrates that KSP isn’t all that scary and you can build out some fairly niche validations to match your needs with relatively few lines of code.

Xcode navigation tricks

Navigating code is one of those things where you accept that it is sometimes painful and slow but just get on it with it without a second thought on how to improve the experience. I’ve been using Xcode for a long time and I’ve got many different strategies/techniques that help me navigate a project effectively. Here’s the ones I use most often.

Project navigation

Getting roughly to the right place in a project can be really challenging in a large code base where the project navigator doesn’t fit on one screen. Here’s the techniques I use for broad brush navigation.


Open quickly

Hitting the combination ⌘+⬆+O will launch the Open quickly dialog box. This is a good first port of call when you can roughly guess the name of something - start typing and hopefully the list will filter down to what you are looking for or trigger other ideas on what to search.

To make this more powerful I utilise the fact that the search term doesn’t need to be 100% precise for example if I’m looking for a view and have a suspicion it might be named something like HomeView I can straight away include view in my term and just prepend h and I might already get a pretty short candidate list like below:

HeaderView.swift
HomescreenViewController.swift
HelpViewController.swift
HiddenSettingsView.swift

This is often close enough to help me narrow in on what I want.


View debugging

Sometimes I really just can’t figure out what something will be named and that’s where I reach for view debugging. I can navigate to the screen I want to locate the source for on a device/simulator and then hit the Capture View Hierarchy button in the Xcode debugging toolbar. Now I can navigate around the view hierarchy and see the names of views and view controllers. Once I have the name I go back to the trusty Open quickly dialog and I’m there.

This is such a simple technique but really powerful - I’ve recently been retrospectively adding accessibility ids to a large old codebase and this has really allowed me to drill in quickly make changes and move on.


Project find

Hitting ⌘+⬆+F lands you on the Project find pane where you can execute various searches of your project. There’s loads of options you can play with that I won’t cover here but it’s worth familiarising yourself with all the capabilities. The settings I play with most often are scopes to only search in certain projects/folders or changing to regex for when plain text search isn’t flexible enough.

Bonus tip: You can delete the items in the search results by selecting it and hitting back space. This allows me to treat a search like a todo list I can filter down as I do work.


Filter bar

At the bottom of the Project navigation pane there is a filter bar which I don’t use often but when I do it’s very handy. Being able to hit the +- button and filter by only files that have source control changes is great when you want to focus on tidying up work before committing.


Reveal in project

Once I’ve found a helpful class I instinctively hit ⌘+⬆+J to reveal in project. This expands the Project Navigator to show where the file is located, this is often helpful as related code tends to be grouped together. Seeing the file you are looking at in the context of similar classes can often help ground you and give you an idea of other things you might need to explore.


In file navigation

The challenges with navigation don’t necessarily stop once you’ve found a source file. Here’s the tricks I use when navigating in a single file.


Jump bar

If I’m new to a class and I want to see what methods it has available my go to technique is to use the jump bar with ctrl+6. This jump bar lists all the methods available and just like Open quickly I can start to filter the methods without needing to be too precise with my guess. This is a great way to get a sense of how a class is structured and filter to methods of relevance quickly.


We can hit ⌘+F (without the ) to search within a file. This is great but often I am searching for a word I have highlighted so if I use this technique I actually need to do ⌘+C, ⌘+F, ⌘+V and then hit enter. That’s far too much effort luckily if you have a word selected you can hit ⌘+E to add the word to the global search and then hit ⌘+G to cycle through the matches. If you go too far or have a feeling that navigating bottom to top would be faster then add a shift into the mix to change direction ⌘+⬆+G.

Bonus tip: ⌘+E is the global search so most well behaved text fields on a mac will work with this e.g. I can select a word in Safari hit ⌘+E then go to Xcode and cycle through matches with ⌘+G.


Jump to definition

A large percentage of our time is spent reading and understanding code. The ultimate short cut for this is ⌘+^+j to navigate to the code that defined a symbol or a method.

Bonus tip: jumping around is great if you can find your way back so remember ⌘+^+← to jump back in your navigation stack and ⌘+^+→ to navigate forwards.


Undo/Redo

In large files it’s often the case that you can be writing code in one place and then need to reference another part of the same file - probably using a combination of these techniques. Instead of trying to find my way back to the code I was working on by manually scrolling I instead hit ⌘+Z to undo my last change and then immediately hit ⌘+⬆+Z to redo the change. This combination might seem pointless but the side effect is that the editor will jump the caret back to the place being edited.


Conclusion

There’s loads of ways to navigate - the above list isn’t exhaustive it’s just the things I use most often. I love watching other people develop and I’m often the annoying person stopping things to say “what was the keyboard shortcut” or “how did you do that”. The above is my attempt to share some things that I would find interesting if someone else did it in front of me.

More unit testing with Excel Lambdas revisited

Like all programming there are many ways to solve any problem and I saw a comment that had a pretty neat, more Excelesque way (I think - I’m not an Excel user) of solving the problem. To drive home the benefit of testing this post adds a couple of tests to the current approach, then I’ll rewrite the solution entirely but keep the tests the same. If all goes well the formula will be completely different but my level of confidence in the calculation will still be high as it satisfies my assumptions that I encoded in some tests.


Let’s add some tests

For a post that was about testing I shockingly didn’t test the final formula 🤦🏼‍♂️, which currently looks like this

=Pair.First(REDUCE(PairMake(0,Data!A1),Data!A1:Data!A2000,NextPartialResult))

The PairMake function and NextPartialResult function had tests written but the whole thing did not. Therefore it’s not safe for me to just change the above formula until I get it behind some tests.

For testing this I’m going to use my current data set and pick different slices of it to produce different results from the formula:

  A (Formula) A (Result) B C (Formula) C (Result) D E F
1 Actual Actual Expected Result Result Result Text   199
2 =SolveTask1(F1:F10) 7 7 =Assert.Equals(B2,C2)     200
3 =SolveTask1(F8:F10) 1 1 =Assert.Equals(B3,C3)     208
4 =SolveTask1(F3:F10) 5 5 =Assert.Equals(B4,C4)     210
5               200
6               207
7               240
8               269
9               260
10               263

With the above I can massage my original solution into a named function called SolveTask1 (naming is hard) that will make all the above tests pass.

SolveTask1 = LAMBDA(range,
    Pair.First(REDUCE(PairMake(0,OFFSET(range,0,0,1)),range,NextPartialResult))
);

Throw everything away and write again

With these tests in place I have a level of confidence that if I call SolveTask1 then it should behave as long as I keep the same tests. The solution I’m going to implement (there could be many more) is based on a comment from twobitshifter who suggested ={SUM(—-(A1:A1999<A2:2000)}.

If I rejig the suggested formula to work in a named function I come up with the following that also passes my tests

SolveTask1 = LAMBDA(range,
    SUM(--(OFFSET(range,0,0,ROWS(range) - 1)<OFFSET(range,1,0,ROWS(range) - 1)))
);

This is a real improvement as I’ve got my final calculation behind some tests that gives me confidence. It also means I can delete all the old tests from my previous post and remove the NextPartialResult helper function entirely - throwing code away is a good thing as less code = less bugs.


Of course we don’t have to stop right there, I might look at the solution above and not be happy about the duplication of ROWS(range) - 1. Because this is all tested I can very quickly introduce a LET and as all of my tests still pass I know I didn’t fudge something up whilst making the change.

SolveTask1 = LAMBDA(range,
    LET(
        sliceSize, ROWS(range) - 1,
        SUM(--(OFFSET(range,0,0,sliceSize)<OFFSET(range,1,0,sliceSize)))
    )
);

Conclusion

I was able to take a working formula and change it a few times without worrying if I was going to break anything. The first change was a complete rewrite and change of approach and the second change was more of an iterative improvement. Having the tests present gave me the confidence to make broad changes without needing to go and do a load of manual verification or to spend forever statically analysing the code to guess that it still works.