Static Code Analysis ✅
Welcome to my newsletter! A mix of Flutter, community, tech content creation, software development and everything in between :)
My first real programming language was Java. It is an object-oriented language, and I learned what OOP was at the same time as my dive into Java. It was 2015, but I was still old school and wrote down my programming notes in a physical notebook. One of the first things that I wrote down in it was the definition of a “method” - a function that belongs to a class. I wrote it down, but I didn’t really understand what this means until much later. And even when I did, I never thought I would need to remember this definition for anything practical…
Welcome to the next episode of Daria’s Flutter diaries, and today we will be talking about static code analysis in Dart and Flutter. To find out how it relates to Java and methods, read until the end 😛
I work in an agency, and the nature of an agency is to work on many, many client projects. Even though there are many, the practices that we use in them are standardized. This helps us to make our developers “plug & play” -able (we can talk about this in another newsletter), which means that they can switch projects and easily orient in the codebase and follow the established coding practices. Setting standards is one thing, but enforcing them is another. And a great tool for that is static code analysis.
Static code analysis is performed by specialized tools without actually running your code, hence static. It follows the set of rules (or lints) that you’ve specified for your project. In Dart and Flutter it is done by the Dart analyzer and potentially its plugins.
All new flutter projects come with the flutter_lints package enabled by default, which is great. According to the docs, the package is updated ~once a year. It includes some Flutter specific lints, but also general Dart lints from the package lints. But there are many more rules available to us that are not included in these packages, and Mike Rydstrom has an amazing overview of these lints, their availability in various packages and many more valuable insights. I highly recommend giving his article a read. Mike also offers an interesting approach to configuring lints: turning all on by default and explicitly turning off the ones that don’t work for you.
We configure our own set of lints and we try to keep them as strict as possible, which of course makes them opinionated by Chili approaches and allow configurations to be very specific and customised to our practices. About 6 months ago we started using another tool that is a game changer for static analysis, especially for teams - DCM. Among other analyzer tools, it offers a huge list of highly useful and customisable lints, some of them are for specific libraries such as bloc or equatable (both of which we use in our projects). It is also actively developed, with valuable updates coming on a regular basis. And no, I’m not sponsored 😂 As someone who reviews code every day on various projects by various developers of various levels and experiences, I can say that it saves a lot of my time. It also gives me more confidence in the quality of the codebase as well as less possibilities to overlook something.
On every PR we run the analyzer and the policy is very strict – 0 info level lints, everything has to be addressed explicitly. This really helps in making the teams consistent, efficient and have less useless arguments. Here are a couple of examples with customisable rules:
- banned-usage:
exclude:
- domain/lib/time/time_provider.dart
entries:
- ident: DateTime.now
description: Please use TimeProvider.now() instead
- ident: SharedPreferences.getInstance
description: Please use designated repository instead
- ident: RichText
description: Please use Text.rich constructor to enable accessibility support
If you have attended Manuela’s talk about accessibility at FlutterCon, you know that RichText
doesn’t handle text scaling very well and a way to fix this is to use the Text.rich
constructor. It is easy to forget something like this, so we turned it into a lint rule.
Or, for example, to stay consistent with our widget structure, there is a rule to configure the order of the methods:
- member-ordering:
alphabetize: false
widgets-order:
- constructor
- init-state-method
- did-change-dependencies-method
- did-update-widget-method
- build-method
- dispose-method
- private-methods
All of these things add up and help greatly with eliminating the human factor errors, having a source of truth for team conventions and unambiguous guidelines. Also, while we’re on the topic, I found a comment that resonated with me a lot in this thread. If there are lints that are true in 99% cases, then it’s worth to have them, since an ignore for a valid case would actually make the implementer explicit about the intention of violating this rule, as opposed to doing it accidentally and shooting oneself in the foot. So I’m more keen on turning on lints and using them if in the majority of cases they’re true, and explicitly documenting their exceptions via ignore
.
Another tool was in my “to try” list for a while - the custom_lint
library by Invertase. This library offers a nice API to create your own lints. There is not that much information and tutorials yet, but here are some resources that you should check out:
Custom Linter Rule for Dart Analyzer - Flutter Global Summit'23 talk by Oleksandr Leushchenko
Creating custom lint rules for Dart and Flutter projects by Majid Hajian
How to create a custom lint in Flutter with custom_lints article on Medium
Join the Invertase Discord server, there is a dedicated channel to
custom_lint
So I decided to experiment and implement a lint that was inspired by recent code reviews: don’t make nullable named parameters required. While such a statement itself can be controversial and highly subjective, I thought it was a great base for experiment, so I set out to do that. Now, I haven’t done anything like that before, so it was a lot of trial & error for me, especially because I dived in straight into the implementation. While I know about the concept of AST tree, I’m not very familiar with the details and specifics, so this is like a whole new world to me.
Warning: whatever I show you is just what I managed to figure out on my first attempts, I currently have no idea how correct it is, how many better ways exist to do that and also potential problems or errors that I’m currently unaware off.
Next I just want to show a bit of my experience. This is not a tutorial (I’ve left some useful links), so I won’t be focusing on every aspect, rather some tids-bits that might be helpful to you if you decide to try it out for yourself, as well as to give you a glimpse into how implementing a custom lint with custom_lint
package looks like.
I started out by copying 1:1 the example from README: https://github.com/invertase/dart_custom_lint#usage
In a nutshell, to create your own lint rule, you need to extend a DartLintRule
class, taken from the example. Initially I wanted to paste code as code, but it formats very bad, so I will insert images. Forgive me 😅 I promise once a tutorial is out, it will be copy-able code 😁
This example lint highlights all of the variable declarations. The magic happens inside the run function. This is where you analyse code and decide whether it has your issue or not. I wanted to firstly implement my NullableNotRequiredLintRule for functions, so following the example of context.registry.addVariableDeclaration, I used context.registry.addFunctionDeclaration to find all functions.
Which worked… kinda. The linter highlighted only top level functions, but completely ignored class functions. Which also made sense, but I wanted all of the functions. So I was confused at first how to get my hands on the class functions. Should I use context.registry.addClassDeclaration and somehow access functions declarations from there? I’ve tried several things, nothing worked as expected, until at some point out of nowhere I remembered… that class functions are called methods.
That’s how I found out about context.registry.addMethodDeclaration, remembered 2015 and also felt a little bit stupid. But in a good way, the one that you feel after you found a solution to a problem that was starting to bug you. I better start learning actual syntax terminology if I want to be productive 😅
At first I wanted to show you that lint as an example, but as I learned a bit more, I’ve started refactoring it and now it’s back to being work in progress 😂 But I will show you another one, also inspired by the code review. The rule is called rename_unused_params_to_underscore
and it highlights cases when the function parameters are unused in the function body, so that they will be renamed to _ and __ as per convention. Let me show it to you in action:
I will briefly show you the code, but because this is not a tutorial (yet), I won’t go into the details. So the first step is to create the rule itself:
Create a class that extends
DartLintRule
Pass an instance of
LintCode
with info about your rule to the constructorImplement the actual logic in the
run
methodOptionally supply a fix in the
getFixes
method
Speaking of fixes, for that you need to extend another class, conveniently called DartFix
:
In here you again override the run
method, and inside create a ChangeBuilder
. This class helps you implement fixes for your rule, such as replacements or deletions.
You can check the full code here. And here is an example app with the custom lints enabled.
I was sooooo excited to see this work! I don’t remember the last time I was so excited about something coding related like this. It’s small, but it was so much fun (and a bit of frustration, just how I like it 😂) and I really want to experiment more with custom lints. And learn to do it the right way, not just “make it work” way 😅 I haven’t tested this on big projects yet, only a basic counter app with 3 extra methods. I also didn’t look into the topic of testing my lints or running them on CI, and in general, there is so much to learn and to do. I will share more as I learn more, so stay tuned!
One thing to keep in mind about analyzer plugins though, is that currently you can only use one per context. Also check the docs. Beauty of the custom_lint
library is that you can use as many implementations of it as you need, since it’s still only one plugin. BUT if you use any other plugin, then you’re out of luck. Before we migrated to the DCM Teams, we also used DCM as a plugin, and we would be in trouble. But now DCM is an independent tool and we can enjoy both worlds!
And that’s it for today! I hope you enjoyed this read or/and found something useful for yourself. This weekend I’m going for a vacation - finally a real vacation, with chilling, relaxing and the sea for a whole week! 🏝️ Which means I won’t be doing anything work or programming related (Daria… I said I WON’T BE 😂) and probably the next newsletter will come in ~3 weeks or so. See you then!
P.S. - I would love to actually see you all at the upcoming conferences: Flutter & Friends and Flutter Firebase Festival. A lot of exciting things are on the agenda 🤩
Let’s connect on Twitter (or X? whatever 😂)
-Daria 💙