<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tyler Jang</title>
    <description>The latest articles on DEV Community by Tyler Jang (@tylerjang27).</description>
    <link>https://dev.to/tylerjang27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1679006%2Ff43d8a89-1c27-4aaf-9b53-61163be52c87.PNG</url>
      <title>DEV Community: Tyler Jang</title>
      <link>https://dev.to/tylerjang27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tylerjang27"/>
    <language>en</language>
    <item>
      <title>FIXME Please: An Exercise in TODO Linters</title>
      <dc:creator>Tyler Jang</dc:creator>
      <pubDate>Fri, 12 Jul 2024 03:13:16 +0000</pubDate>
      <link>https://dev.to/tylerjang27/fixme-please-an-exercise-in-todo-linters-f2a</link>
      <guid>https://dev.to/tylerjang27/fixme-please-an-exercise-in-todo-linters-f2a</guid>
      <description>&lt;p&gt;A few weeks ago, I was talking with a developer in our &lt;a href="https://slack.trunk.io/" rel="noopener noreferrer"&gt;Community Slack&lt;/a&gt; who was interested in adding their own TODO linter. At face value, this is a trivial problem. There are several linters that already support this to varying degrees, and many of them offer decently extensible configuration and their own plugin ecosystems. But the more I thought about it, the more the question piqued my interest. Trunk supports &lt;a href="https://docs.trunk.io/check/configuration/supported" rel="noopener noreferrer"&gt;100+ linters&lt;/a&gt; out of the box (OOTB), but which one would solve this problem best? So I set out to evaluate them all. Here are my findings...&lt;/p&gt;

&lt;p&gt;To simplify this experiment, we should clarify what makes for a good TODO linter. Depending on your team’s culture, you may want to prevent &lt;em&gt;any&lt;/em&gt; TODOs from making it to main, or you may just want to keep tabs on them. But at a minimum, a TODO linter should satisfy the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Easily and quickly report what files have “TODO” strings and where&lt;/li&gt;
&lt;li&gt;Support multiple languages/file types&lt;/li&gt;
&lt;li&gt;Don’t generate additional noise (“mas&lt;u&gt;todo&lt;/u&gt;n” isn’t a todo)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a bonus, some TODO linters might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Require specific syntax for TODO comments (e.g. &lt;a href="https://clang.llvm.org/extra/clang-tidy/checks/google/readability-todo.html" rel="noopener noreferrer"&gt;clang-tidy&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Support other keywords and cases (e.g. FIXME)&lt;/li&gt;
&lt;li&gt;Be able to ignore false positives as appropriate (automatically handled with &lt;a href="https://docs.trunk.io/check/configuration/ignoring-issues" rel="noopener noreferrer"&gt;trunk-ignore&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Now that we have our criteria, let’s dive in. All examples (both with and without Trunk) can be found in this &lt;a href="https://github.com/trunk-io/todo-linter-demo" rel="noopener noreferrer"&gt;sample repo&lt;/a&gt;, so feel free to follow along! If you haven’t used Trunk before, you can follow our setup instructions in our &lt;a href="https://docs.trunk.io/check/usage" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Sample File
&lt;/h1&gt;

&lt;p&gt;We'll lint this file with all the tools we test in this blog. This file has some real TODO comments and some fake TODOs meant to confuse linters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Test Data&lt;/span&gt;

A collection of different ways that TODO might show up.

&lt;span class="sb"&gt;``yaml
# TODO: Make this better
version: 0.1
``&lt;/span&gt;

&lt;span class="sb"&gt;``typescript
// TODO(Tyler): Optimize this
const a = !!!false;
``&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- MASTODON is not a fixme --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Another Heading&lt;/span&gt;

Look at all the ways to check for todo!

&lt;span class="c"&gt;&amp;lt;!-- trunk-ignore-begin(todo-grep-wrapped,codespell,cspell,vale,semgrep,trunk-toolbox) --&amp;gt;&lt;/span&gt;

Let's ignore this TODO though

&lt;span class="c"&gt;&amp;lt;!-- trunk-ignore-end(todo-grep-wrapped,codespell,cspell,vale,semgrep,trunk-toolbox) --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Per-Language Rules
&lt;/h1&gt;

&lt;p&gt;Let’s try a naive approach. Several linters have built-in rules to check for TODOs (e.g. &lt;a href="https://docs.astral.sh/ruff/rules/line-contains-todo/" rel="noopener noreferrer"&gt;ruff&lt;/a&gt;, &lt;a href="https://eslint.org/docs/latest/rules/no-warning-comments" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt;). Many others support plugin ecosystems to add your own rules. Let’s take a look at markdownlint’s approach to this, using the &lt;a href="https://www.npmjs.com/package/markdownlint-rule-search-replace" rel="noopener noreferrer"&gt;markdownlint-rule-search-replace&lt;/a&gt; package. Run &lt;code&gt;trunk check enable markdownlint&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;In order to configure the rule, we must modify &lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/.markdownlint.json" rel="noopener noreferrer"&gt;.markdownlint.json&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"markdownlint/style/prettier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"search-replace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"found-todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Don't use todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"searchPattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/TODO/gi"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we can run it and inspect the output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yg1hg6ztuv1aigupwbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yg1hg6ztuv1aigupwbw.png" alt="Running markdownlint with Trunk Check" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaze5zoligzzsiu5mtcp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaze5zoligzzsiu5mtcp.png" alt="Running markdownlint standalone" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that we have a &lt;code&gt;trunk-ignore&lt;/code&gt; to suppress the &lt;code&gt;TODO&lt;/code&gt; on line 24.&lt;/p&gt;

&lt;p&gt;Markdownlint here gets the job done, but will of course only work on MD files. As soon as you start to add other file types, even YAML or JS, it doesn’t scale, and you’ll lose coverage and consistency, and chasing down the particular incantation to do this for every linter is intractable. Let’s look at some other more sustainable options.&lt;/p&gt;

&lt;h1&gt;
  
  
  CSpell
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://cspell.org/" rel="noopener noreferrer"&gt;CSpell&lt;/a&gt; is a relatively extensible code spellchecker. It’s easy to use OOTB, and it runs on all file types. However, it has a high false positive rate and requires that you manually tune it by importing and defining new &lt;a href="https://cspell.org/docs/dictionaries/" rel="noopener noreferrer"&gt;dictionaries&lt;/a&gt;. Let’s see what it takes to turn it into a TODO linter. First, run &lt;code&gt;trunk check enable cspell&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;We can define our own dictionary or simply add a list of &lt;a href="https://cspell.org/docs/forbidden-words/" rel="noopener noreferrer"&gt;forbidden words&lt;/a&gt; to &lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/cspell.yaml" rel="noopener noreferrer"&gt;cspell.yaml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.2"&lt;/span&gt;
&lt;span class="c1"&gt;# Suggestions can sometimes take longer on CI machines,&lt;/span&gt;
&lt;span class="c1"&gt;# leading to inconsistent results.&lt;/span&gt;
&lt;span class="na"&gt;suggestionsTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt; &lt;span class="c1"&gt;# ms&lt;/span&gt;
&lt;span class="na"&gt;words&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!todo"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!TODO"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feee40hdg0y9nc2k20s6n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feee40hdg0y9nc2k20s6n.png" alt="Running cspell with Trunk Check" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsuoyua3htujj8dg4xq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsuoyua3htujj8dg4xq1.png" alt="Running cspell standalone" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We end up with a quick case-insensitive search for TODOs, albeit with some messy suggestions. It gets the job done, but getting it production-ready for the rest of our codebase will usually require curating additional dictionaries. Running it on the sample repo flags 22 additional false positive issues.&lt;/p&gt;

&lt;h1&gt;
  
  
  codespell
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/codespell-project/codespell" rel="noopener noreferrer"&gt;codespell&lt;/a&gt; is a code spellchecker that takes a different approach. Much like CSpell, it is prone to false positives, but rather than defining dictionaries of allowlists, it looks for specific common misspellings and provides suggestions. This reduces its false positive rate, but it usually still requires some tuning. Run &lt;code&gt;trunk check enable codespell&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;To teach codespell to flag TODOs, we need to define our own dictionary and reference it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/todo_dict.txt" rel="noopener noreferrer"&gt;todo_dict.txt&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;todo-&amp;gt;,encountered todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/.codespellrc" rel="noopener noreferrer"&gt;.codespellrc&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[codespell]
dictionary = todo_dict.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5hhjf6nfi16a4h8ir7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5hhjf6nfi16a4h8ir7p.png" alt="Running codespell with Trunk Check" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzkyygeseu587uu449jxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzkyygeseu587uu449jxi.png" alt="Running cspell standalone" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still a bit cumbersome, but we can fine-tune the replacements if desired. Let’s examine some other options.&lt;/p&gt;

&lt;h1&gt;
  
  
  Vale
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://vale.sh/" rel="noopener noreferrer"&gt;Vale&lt;/a&gt; is a code prose checker. It takes a more opinionated approach to editorial style, and thus can require lots of tuning, but it is very extensible. Let’s have it check for TODOs. Run &lt;code&gt;trunk check enable vale&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;Vale has an opinionated, nested structure to define its configuration. For now, we will only do the minimum to check for TODOs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/.vale.ini" rel="noopener noreferrer"&gt;.vale.ini&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;StylesPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"styles"&lt;/span&gt;

&lt;span class="py"&gt;MinAlertLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;suggestion&lt;/span&gt;
&lt;span class="py"&gt;Packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="nn"&gt;[*]&lt;/span&gt;
&lt;span class="py"&gt;BasedOnStyles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Vale, base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/styles/base/todo.yml" rel="noopener noreferrer"&gt;styles/base/todo.yml&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;existence&lt;/span&gt;
&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Don't use TODO&lt;/span&gt;
&lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;raw&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TODO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosnhc066ke29yd1q6wzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosnhc066ke29yd1q6wzu.png" alt="Running vale with Trunk Check" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk8f3uqf35o81pmzlu22.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk8f3uqf35o81pmzlu22.png" alt="Running vale standalone" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re already using Vale, and you’re willing to eat the cost of configuration, it can work quite well! Additionally, you can easily customize which file types and scopes it applies to. Let’s try a few more.&lt;/p&gt;

&lt;h1&gt;
  
  
  Semgrep
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://semgrep.dev/docs/cli-reference" rel="noopener noreferrer"&gt;Semgrep&lt;/a&gt; is a static analysis tool that offers semantic-aware grep. It catches a number of vulnerabilities out of the box, and it’s fairly extensible. It handles most file types, although anecdotally it struggles in some edge cases (e.g. C++ macros, networkless settings). Run &lt;code&gt;trunk check enable semgrep&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;Thankfully, Semgrep is configured pretty easily and lets us just specify words or patterns to check for. We can add a config file like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/.semgrep.yaml" rel="noopener noreferrer"&gt;.semgrep.yaml&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check-for-todo&lt;/span&gt;
    &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generic&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ERROR&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Don't use TODO&lt;/span&gt;
    &lt;span class="na"&gt;pattern-either&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TODO&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcoeflwo9twrn1i2cg1xf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcoeflwo9twrn1i2cg1xf.png" alt="Running semgrep with Trunk Check" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0l9n76tz8hy086ur4sa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0l9n76tz8hy086ur4sa.png" alt="Running semgrep standalone" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works pretty well!! And we can customize it however we want in their &lt;a href="https://semgrep.dev/playground/r/3qUzQD/ievans.print-to-logger?editorMode=advanced" rel="noopener noreferrer"&gt;playground&lt;/a&gt;, even modifying our pattern to require specific TODO styling. Semgrep seems like a decent contender for a best-effort solution, but let’s give a couple more a try.&lt;/p&gt;

&lt;h1&gt;
  
  
  trunk-toolbox
&lt;/h1&gt;

&lt;p&gt;trunk-toolbox is our &lt;a href="https://github.com/trunk-io/toolbox" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; homegrown linter Swiss Army knife. It supports a few different rules, including searching for TODO and FIXME. It works on all file types and is available just by running &lt;code&gt;trunk check enable trunk-toolbox&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Enable TODO checking in &lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/toolbox.toml" rel="noopener noreferrer"&gt;toolbox.toml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[todo]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7k31c98qnwv9jtz92wqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7k31c98qnwv9jtz92wqe.png" alt="Running trunk-toolbox with Trunk Check" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej9q86s1gjsls9hapxft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej9q86s1gjsls9hapxft.png" alt="Running trunk-toolbox standlone" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This immediately accomplishes the stated goal of a TODO linter–if you just want to find TODOs, just use trunk-toolbox–but it isn’t configurable beyond that.&lt;/p&gt;

&lt;h1&gt;
  
  
  Grep Linter
&lt;/h1&gt;

&lt;p&gt;Let’s take this one step further. How difficult is it to prototype a solution from scratch? Building a wrapper around grep is the no-brainer solution for this, so let’s start with that.&lt;/p&gt;

&lt;p&gt;At its simplest, we can build something like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/plugin.yaml#L27-L46" rel="noopener noreferrer"&gt;.trunk/trunk.yaml&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-grep-linter&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Uses grep to look for TODOs&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c "grep -E -i 'TODO\W' --line-number --with-filename ${target}"&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass_fail&lt;/span&gt;
          &lt;span class="na"&gt;success_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;pass_fail&lt;/code&gt; linter will just report when we have TODOs. In order to get line numbers, we can wrap this in a script and make it a &lt;code&gt;regex&lt;/code&gt; linter with an output that Trunk Check understands:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/todo_grep.sh" rel="noopener noreferrer"&gt;todo_grep.sh&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;LINT_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;TODO_REGEX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"TODO&lt;/span&gt;&lt;span class="se"&gt;\W&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;GREP_FORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"([^:]*):([0-9]+):(.*)"&lt;/span&gt;
&lt;span class="nv"&gt;PARSER_FORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\1&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\2&lt;/span&gt;&lt;span class="s2"&gt;:0: [error] Found TODO in line (TODO)"&lt;/span&gt;

&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODO_REGEX&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--line-number&lt;/span&gt; &lt;span class="nt"&gt;--with-filename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LINT_TARGET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"s/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREP_FORMAT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PARSER_FORMAT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/trunk-io/todo-linter-demo/blob/main/plugin.yaml#L27-L46" rel="noopener noreferrer"&gt;.trunk/trunk.yaml&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-grep-wrapped&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Uses grep to look for TODOs&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sh ${cwd}/todo_grep.sh ${target}&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regex&lt;/span&gt;
          &lt;span class="na"&gt;parse_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;((?P&amp;lt;path&amp;gt;.*):(?P&amp;lt;line&amp;gt;-?&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;d+):(?P&amp;lt;col&amp;gt;-?&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;d+):&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;[(?P&amp;lt;severity&amp;gt;.*)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(?P&amp;lt;message&amp;gt;.*)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;((?P&amp;lt;code&amp;gt;.*)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;))"&lt;/span&gt;
          &lt;span class="na"&gt;success_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm29cxyrkrctb6ja3vyta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm29cxyrkrctb6ja3vyta.png" alt="Running grep-linter with Trunk Check" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtgfznn2zk9zcje3x51c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtgfznn2zk9zcje3x51c.png" alt="Running grep-linter standalone" width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a bit messy, but it gets the job done. It’s another thing to maintain, but you can tune it as much as you want. We’ll definitely be using one of the pre-built solutions, though.&lt;/p&gt;

&lt;h1&gt;
  
  
  What did we learn?
&lt;/h1&gt;

&lt;p&gt;There are more than a couple of reasonable options, and depending on your appetite for configuration vs. plug-and-play, some make more sense than others. But overall, using an existing language-agnostic tool performs much better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7w953tdotxiyas2k6k9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7w953tdotxiyas2k6k9.png" alt="Configurability vs. ease for each option" width="781" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And regardless of your preference, all of these options can be super-charged by Trunk. Using &lt;a href="https://trunk.io/blog/githook-management" rel="noopener noreferrer"&gt;githooks&lt;/a&gt; and &lt;a href="https://docs.trunk.io/check/check-cloud-ci-integration/get-started" rel="noopener noreferrer"&gt;CI gating&lt;/a&gt;, you can prevent TODOs from ever landing if that’s your taste. Or, you can burn them down incrementally, only tackling new issues with &lt;a href="https://docs.trunk.io/check/reference/under-the-hood#hold-the-line" rel="noopener noreferrer"&gt;Hold the Line&lt;/a&gt;. You can always make TODOs a non-blocking &lt;a href="https://docs.trunk.io/check/configuration#blocking-thresholds" rel="noopener noreferrer"&gt;threshold&lt;/a&gt; if need be, or &lt;a href="https://docs.trunk.io/check/reference/user-yaml" rel="noopener noreferrer"&gt;turn them on for yourself&lt;/a&gt; without blocking your team.&lt;/p&gt;

&lt;p&gt;We all end up with more TODOs than we’d like, but it’s important to build processes that track them (and if necessary gate them) so they don’t get out of hand, just like any other linting issue. There are lots of reasonable options to choose from, but it’s important to make an informed decision when adopting a generalizable approach to linting.&lt;/p&gt;

&lt;p&gt;If this post interests you, come check out our other linter definitions in our open-source &lt;a href="https://github.com/trunk-io/plugins" rel="noopener noreferrer"&gt;plugins repo&lt;/a&gt; or come chat with us on &lt;a href="http://slack.trunk.io" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linters</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Safely Upgrading our Open Source Dependencies at Scale</title>
      <dc:creator>Tyler Jang</dc:creator>
      <pubDate>Fri, 28 Jun 2024 19:03:20 +0000</pubDate>
      <link>https://dev.to/tylerjang27/safely-upgrading-our-open-source-dependencies-at-scale-1ha3</link>
      <guid>https://dev.to/tylerjang27/safely-upgrading-our-open-source-dependencies-at-scale-1ha3</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.trunk.io/check" rel="noopener noreferrer"&gt;Trunk Check&lt;/a&gt; installs and manages over a hundred linters, code formatters, and other code quality tools. &lt;strong&gt;The underlying tools are open source projects that each run on their own release schedule and, like all software, sometimes have bugs.&lt;/strong&gt; Figuring out which of our 100+ &lt;a href="https://docs.trunk.io/check/configuration/supported" rel="noopener noreferrer"&gt;tools&lt;/a&gt; are safe to upgrade for our customers is a really hard problem. &lt;strong&gt;We have to validate every new version of every tool we support&lt;/strong&gt;, and their accompanying runtimes and upstream dependencies, before shipping it to our customers. With so many integrations, across all different languages, it’s important to ensure compatibility and continuity as new versions are released. &lt;em&gt;Even something as simple as changing the name of a cli option could break thousands of workflows.&lt;/em&gt; Here's how we solved the problem of testing, validating, and automating our linter upgrades.&lt;/p&gt;

&lt;h1&gt;
  
  
  Black Box Integration Testing
&lt;/h1&gt;

&lt;p&gt;Within the our execution model, each linter can have a runtime, a download, a parser, and configuration for how to run. With so many linter idiosyncrasies, it isn't practical to assert on the behavior of each individual step or linter output; instead, we rely on black box testing.&lt;/p&gt;

&lt;p&gt;Our aim is to guarantee Trunk output continuity. When a new linter version is released, it should have the same or a similar set of diagnostics as its predecessor. Fortunately, this problem lends itself well to &lt;a href="https://jestjs.io/docs/snapshot-testing" rel="noopener noreferrer"&gt;Jest snapshots&lt;/a&gt;. We can capture the JSON result of a linter’s &lt;code&gt;trunk check&lt;/code&gt; run, or the contents of a formatted file, and use that as our source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioned Linter Snapshots
&lt;/h2&gt;

&lt;p&gt;When we first run tests on a linter, &lt;em&gt;we save a snapshot of its Trunk output&lt;/em&gt;, named with the linter’s version. Subsequent test runs will use the &lt;em&gt;most recent matching snapshot&lt;/em&gt; for our assertions, allowing us to keep a historical record of linter output and maintain compatibility.&lt;/p&gt;

&lt;p&gt;We detect the latest version of a linter by querying its runtime or by using &lt;a href="https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release" rel="noopener noreferrer"&gt;GitHub APIs&lt;/a&gt;. Using our snapshot setup, and the assumption that the latest release of a linter will pass the most recent snapshot (&lt;em&gt;in practice, true about 95% of the time for new releases&lt;/em&gt;), we can run nightly tests on the latest versions of all our supported linters. If a linter fails, we can investigate and adapt as needed. If it passes, we can upload its version to our own &lt;em&gt;Release Version Service&lt;/em&gt;, which provides the source of truth for linter upgrades.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5crl2rv384ivxwxrnz1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5crl2rv384ivxwxrnz1r.png" alt="Linter Upgrade Validation Pipeline Diagram" width="800" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Example of Linters Breaking
&lt;/h1&gt;

&lt;p&gt;With this system in place, let’s see what happens when a linter breaks compatibility. Trunk integrated with the linter &lt;code&gt;tidy&lt;/code&gt; when it was released at &lt;code&gt;known_good_version&lt;/code&gt; 1.0.0. So, we generated a snapshot named &lt;code&gt;tidy_v1.0.0.check.shot&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;“issues”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tidy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing-return"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“test_data/adder.py&lt;/span&gt;&lt;span class="s2"&gt;",
      "&lt;/span&gt;&lt;span class="err"&gt;line&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="s2"&gt;",
      "&lt;/span&gt;&lt;span class="err"&gt;column&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="s2"&gt;",
      "&lt;/span&gt;&lt;span class="err"&gt;level&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="err"&gt;LEVEL_HIGH&lt;/span&gt;&lt;span class="s2"&gt;",
      "&lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="err"&gt;’add’&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="s2"&gt;"
    }
]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that running &lt;code&gt;tidy&lt;/code&gt; with Trunk generates one file issue for &lt;code&gt;adder.py&lt;/code&gt;. We expect new versions to return the same result - anything else would indicate there was a failure or regression worth investigating.&lt;/p&gt;

&lt;p&gt;After we generated this snapshot, &lt;code&gt;tidy&lt;/code&gt; then released versions 1.0.1, 1.1.0, and 1.1.1, all of which had an identical output for our test data, so each passed our tests. No need to generate any new snapshots. One week later, &lt;code&gt;tidy&lt;/code&gt; released a new version 1.2.0, which renamed one of its CLI arguments. Suddenly, our test reports a failure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9vxqympeiv3zzt4c4u8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9vxqympeiv3zzt4c4u8.png" alt="Slack Notification of Test Failure" width="800" height="72"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"failures"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"report"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`tidy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exited&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exit_code=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;stdout:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(none)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;stderr:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;usage:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tidy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;options&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;tidy:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unrecognized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;option&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;found:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--verify`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trunk doesn’t know how to run &lt;code&gt;tidy@1.2.0&lt;/code&gt;. In response, we can add a new versioned subcommand to Trunk configuration, and it runs again!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tidy&lt;/span&gt;
      &lt;span class="s"&gt;…&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;“&amp;gt;=1.2.0”&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sarif&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tidy --sarif --verify-target {target}&lt;/span&gt;
          &lt;span class="na"&gt;success_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;“&amp;gt;=1.0.0”&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sarif&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tidy --sarif --verify {target}&lt;/span&gt;
          &lt;span class="na"&gt;success_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output also changed slightly, so we can create another snapshot, and name it &lt;code&gt;tidy_v1.2.0.check.shot&lt;/code&gt;. Going forward, new releases of tidy will use this snapshot, but we always have the option to go back in time and verify that older versions still work as intended, or we can add more test data if we need to. Snapshots are lightweight enough that we can store historical information as long as we need, and Trunk accommodates hermetically installing and running old and new linter versions.&lt;/p&gt;

&lt;p&gt;Keeping your linters up to date should be painless. You shouldn’t have to worry about missing downloads, compatibility issues, or sudden flakiness. Trunk will only ever upgrade you to validated linter versions that have passed snapshot tests, sparing you the burden of any bleeding-edge headaches.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy428a34nnd8dmscri8lx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy428a34nnd8dmscri8lx.png" alt="Successful Linter Upgrade Screenshot" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Paying Dividends in the Real World
&lt;/h1&gt;

&lt;p&gt;With this system in place, we have already caught several bugs before they could hit user repos.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/returntocorp/semgrep/issues/6904" rel="noopener noreferrer"&gt;Semgrep&lt;/a&gt; released a binary that was broken on macOS. We identified this issue and helped push for a quick fix.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ansible/ansible-lint/issues/3277" rel="noopener noreferrer"&gt;Ansible-lint&lt;/a&gt; introduced a bug that executed user playbooks. We blocked this version from installing on user machines until a fix was landed.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stackrox/kube-linter/releases/tag/v0.6.1" rel="noopener noreferrer"&gt;Kube-linter&lt;/a&gt; changed their download URL schema. Within hours, we had landed a fix supporting this new URL.&lt;/li&gt;
&lt;li&gt;Trunk runs tools hermetically, and several tools (e.g. &lt;code&gt;nixpkgs-fmt&lt;/code&gt;, &lt;code&gt;golangci-lint&lt;/code&gt;, &lt;code&gt;gofmt&lt;/code&gt;) deprecated support for older runtimes. Seeing this, we automatically upgraded the runtimes for users running these linters.&lt;/li&gt;
&lt;li&gt;Using older snapshots of these linters, we can block any potential regression before it happens, remaining backwards-compatible whenever possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Contribute
&lt;/h2&gt;

&lt;p&gt;Trunk’s &lt;a href="https://github.com/trunk-io/plugins" rel="noopener noreferrer"&gt;plugins repository&lt;/a&gt; is open-source, and we are always welcoming new contributions for additional linter integrations and improvements, as well as new actions. If you want to try out Trunk for yourself, &lt;a href="https://docs.trunk.io/check/usage" rel="noopener noreferrer"&gt;it's just one shell command away&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linters</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
