Lessons I May Have Learned From Working on Stylelint
A little over a year ago I started working on stylelint, a PostCSS-powered stylesheet linter. (To learn more about stylelint, you can read my article in CSS-Tricks about it.)
It’s a fairly large open source project, and continues to grow at a fairly quick pace. I say “large” referring not only to codebase size, but also to exposed functionality and general ambition; and I say “grow” referring not only to code written, but also to attention received, issues opened, contributions offered, extensions created, etc.
What I’ve heard has proven true: writing and maintaining open source code can be an effective course of study. The main reason I’ve been working on stylelint is to actively learn. And I definitely have. So lately I’ve been reflecting a little on some ideas that have crystallized for me during this experience with stylelint — that is, while working with a few international collaborators on a large open source project written in JavaScript, about CSS.
I’m going to try to write down a few of these lessons I may have learned, for my own edification, and for anybody who comes across this.
(Note that I call them “lessons I may have learned” because just saying “lessons I have learned” sounds a little too confident for me, as though I tapped into Authority and now channel it. These may not be very good lessons, or I may not have learned them well. Judge for yourself. I know you want to.)
1. One of the best ways to learn about a language is to statically analyze it
I now know a million or so times more about CSS than I did before starting stylelint. A million. Working on this project involving intense static analysis has compelled me to learn and repeatedly use precise terminology about language features; to read specs; to make fine distinctions and really understand subtle differences.
And all that has led me to a judgment …
2. Non-standard syntaxes are a huge obstacle to CSS tooling
I’ve come to believe that the wild popularity of preprocessors with non-standard syntactic constructs has made it so-so-so-so much harder to create powerful CSS tools. Too much harder, for entirely inadequate payoff.
Effective CSS tools rely on effective stylesheet parsing, and non-standard syntax spoils (or dramatically complicates) that parsing. This means that any attempt at a CSS tool must make countless exceptions and accommodations to avoid stumbling. And everybody seems to use Sass or Less, so if you want to build a general-purpose tool you just can’t ignore them.
Someone prone to bold and punchy Twitter pronouncements should make one blaming Sass and Less for the CSS ecosystem not being better than it is. Sass and Less have won huge followings because they are good at what they do, but some portion of what they do effectively stunts other tools — and a lot of that portion is not actually very helpful. Without those behemoths running the show, who knows what it might have become by now? (We certainly would have had a great, widely used linter long before stylelint.)
CSS has at least two built-in syntactic constructs that in theory allow for indefinite extension: functions and at-rules. You could create all the custom functions and at-rules you want without violating the grammar of CSS — that is, without breaking tools that rely on parsing. But preprocessors went too far, introducing non-standard constructs — operators, //
-comments, variable interpolation, lists and maps, etc. — and thereby hampering awhatever is not deliberately, specifically tailored to their eccentricities. They’ve fragmented the landscape, and required every tool-building effort to be dramatically more complex and ambitious than it would otherwise be, burdening creators and maintainers.
Ever since stylelint decided to support non-standard syntaxes, it seems that the majority of our actual bug reports have been — surprise, surprise — related to those non-standard syntaxes. I know those preprocessors were built on good intentions, but my work on stylelint makes me resent them. That’s demotivating. And most of the time (all of the time, really) I don’t think those constructs are worth the trouble they introduce. Not even close. Sass maps are not worth it. Less’s mixin syntax is not worth it (Sass got it right with at-rules). Even //
-comments are really not worth it (more on that below).
3. I prefer to write Node modules without ES2015, without Babel compilation
ES2015 is wonderful; and Babel is wonderful. We’re all better off that both exist. And for browser code that is already going to be compiled (because I use modules and bundle things), I have no qualms, I think, about using them. But for Node modules, I do have qualms.
Many times I’ve regretted the early decision to write stylelint in ES2015, compiling with Babel. It introduced various complexities, tooling incompatibilities, bug-sources, and other slight burdens that could have been entirely avoided. And for what payoff? For sugary syntax changes that are nice enough but definitely not necessary. (The really helpful stuff, like new data structures, can be polyfilled without compilation).
By the time I regretted this decision, I already felt like it was too late to turn back.
From now on I will not be writing Node modules that require compilation, if I can help it. I will write them using the syntax available in the Node versions that I plan to support.
4. It’s nice to establish a standard way to do a thing, even if it’s a trivial thing
Usually this means writing a function and reusing it instead of repeating a procedural pattern. In stylelint we’re always trying to figure out what a string of CSS means, so we find ourselves writing little heuristics to determine whether a value is a Sass variable, a hex color is valid, a character is “whitespace,” a word is a font family name, and on and on. We’ve ended up accumulating a utility belt of functions, and in the process found that there’s almost always at least one little trick to each of these games that would have been neglected half the time had we just re-implemented some logic, repeated a pattern. Also, these functions can be independently tested. I like them quite a bit.
But a reusable function is not the only “standard way to do a thing.” Writing documentation gets easier the more formulaic you make it (and the documentation itself gets more consistent, and probably clearer and more accurate). The same goes for writing tests (more on that below). And reviewing code. And writing lots of very similar modules (like linter rules).
If you think you may have done something well once, try to make it easy to do that same thing just as well every time.
Also: If you contemplate what you’re doing as Establishing The Way To Do A Thing, you will probably analyze and solve the problem better than you would have if you’d just whipped off a few lines that apparently work in one context.
(Keep in mind that you can always change your mind, and substitute a new way for the old. There is no eternally enduring and unchanging Way.)
Then there’s all the minutiae, some that is arbitrary (like deciding whether or not to capitalize “stylelint”) and just needs to be decided upon; and some not so arbitrary — but still tiny, buried among similar minutiae, as easy to overlook as to frenetically fuss over. With these details, I find it’s often best to make a decision and stick with it — whether that means picking arbitrarily, or really weighing options — at the risk of seeming a little pedantic or over-strict. That way you don’t have to think about that minutiae anymore until a problem arises. That’s one of the major benefits of linters in the first place, or of usage manuals in writing, or of some minor Good Habits in Life: reduce the cognitive load from minor, repeated decisions so that you can focus on the less minor, less common problems.
I’m especially thankful for the patience and thoroughness of one of my collaborators, Richard Hallows, in figuring out standard ways to do things.
5. Make meanings and motivations explicit
This comes up all the time in all the large project I’ve worked on, including stylelint. I’ll find myself looking at a file I haven’t touched for 10 months, or looking at a file somebody else wrote and I reviewed quickly over breakfast while grumpy and half-watching hummingbirds. In these cases, I cannot rely on any remembered context (“… this line solved that one issue with operators in calc()
… and this one addressed the fact that units can be uppercase…”).
There are always odd edge-cases to address, and the code that addresses them can be harmfully hermetic to a reader without context. So when I write or review code, I’m now trying to look at it as though I’ve already lost context, even when I have not (yet).
The more time passes, the more code you and others pile up, the less you are able to assume contextual knowledge. Instead, whenever reasonable, you should assume the lack of context — which means you must be explicit about the meanings of values and the motivations for decisions.
Look back at your code and imagine someone else or your future self asking “What does this mean?” and “Why did you do that?” Then figure out a way to communicate with that hypothetical interlocutor.
I’ve found a few practices that help with this.
- Try to name variables and functions with purpose and precision. It is always better to type more letters than to rely on implicit context.
- Write comments whenever variable and function names don’t mostly explain what’s going on.
- Write down what you’re thinking and what you’ve done in issues and pull requests.
- Write thorough tests. (Keep in mind, though, that tests can sometimes prove as mysterious as the source code if they do not include clear names and comments.)
If some bit of code is poorly named, uncommented, and untested, you can safely, sadly assume that someone is going to be confused and annoyed about it at some point — maybe tomorrow, maybe a year from now.
6. More trivial preferences
6a. I prefer object “options” arguments to long argument lists
Option keys can describe their values’ meanings, and that’s nice (see above). Also, unlike with an argument list, the order of properties in an object is arbitrary, the number indefinite. With an options object, it’s easy for function consumers to skip optional “arguments” they don’t need, or for you the author to add another “argument” in the future without worrying about where it falls in an already unwieldy list.
As stylelint changed and expanded, I found myself again and again refactoring functions to accept options objects instead of argument lists. Now, whenever I realize that a function is going to require more than a couple of arguments, I consider an object, instead.
6b. Return early when you can
Much of the logic in stylelint involves deciding what to ignore and what to analyze, because most of the rules only pertain to very specific chunks of your CSS. If a linting rule focuses on rgba()
arguments, for example, it can ignore any text that’s not in the value of a declaration in a function named rgba
, as well as any text in comments and strings. One way to think about this whittling down is as a series of nested conditionals:
if x is in a declaration
and x is in the value of that declaration
and x is in a function in that value
and the function's name is rgba
and x is not a comment
and x is not a string
check x
By inverting the statements and returning early, you flatten the logic:
if x is not in a declaration, ignore it
if x is not in the value of that declaration, ignore it
if x is not in a function in that value, ignore it
if that function's name is not rgba, ignore it
if x is in a comment, ignore it
if x is in a string, ignore it
check x
This reduces indentation and curly-braced blocks in the code, which everyone appreciates. I also find that it eases reading, at least a little bit, because I don’t have to think of each point in the sequence as the continuation of one long coordination.
6c. Make it as easy as possible to write tests, and you will have more tests
We have a lot of unit tests in stylelint. It looks like right now we’re at 21,174 assertions. We are able to write so many tests because we worked out a system that makes it as easy as can be to write them. This was a very good move, I think, because all of those tests we now have are very, very helpful. And new contributors don’t have much trouble writing new tests, because the process is so standard, so widely exemplified, so simple.
Whenever I find myself hesitant to write tests in a project, I now wonder whether the reason is because I’m worried that the act of writing those tests is going to be a pain in the ass. If that’s the case, if I fear that my ass will be pained, then I wonder if I should be dissatisfied with the testing setup, and should try to figure out some way to make writing tests for that project less painful.
In some cases, the ass pain may be inescapable. Some programs demand it. But in many cases, like stylelint’s, the pain can be ameliorated with a little planning and systematization upfront.
6d. Don’t worry about typing extra characters
Above I mentioned that I don’t think the //
-comments provided by stylesheet preprocessors are important enough to justify the troubles caused by non-standard syntax. I’m sure that plenty of Sass users would protest this with a complaint approximating this: “It’s tedious to close comments with */
.”
If I embedded GIFs into these posts, I’d embed one here showing somebody cocking an eyebrow suspiciously. Maybe next time I’ll think about GIFs.
Many common justifications for non-standard syntax features are essentially the same as this. You may think you need Sass lists because you can then make a list of states and loop through it to create 50 selectors like .state-[statename]
without having to repeat that .state-
part. You may think you need JS arrow functions because they save you from typing function
all the time. Or you may think you should name your variable p
instead of people
because you’ll avoid typing 5 letters again and again throughout the function.
Even if we didn’t have autocomplete in our editors, the complaint is pretty silly. I think everybody knows this, in their hearts. It is a mistake to give up any real benefit, any at all, for the apparent advantage of typing p
instead of people
.