More ast-grep usage!

This time, I needed to standardise the styling of input and dropdown fields across our application to have the same height. Because of how things developed, we ended up using two different heights in different pages (40px & 36px). needed to standardise on one height, which we decided to be 36px. Because we used Tailwind CSS, that meant finding instances of the h-10 class and changing it to h-9.

const From = () => {
  return (
    <Foo className={{
      container: 'flex flex-col h-10',
      content: 'text-typography-900',
    }}>
      <p className="h-10 w-10">hello world</p>
      <p className="text-sm">bye world</p>
    </div>
  )
}

const To = () => {
  return (
    <Foo className={{
      container: 'flex flex-col h-9',
      content: 'text-typography-900',
    }}>
      <p className="h-9 w-10">hello world</p>
      <p className="text-sm">bye world</p>
    </div>
  )
}

Once again, I thought that ast-grep would be the perfect tool for this. I only needed to match the substring h-10 that exists in a valid class prop, which in React is className, then change it to h-9 if applicable. With an AST, I could do exactly that.

Because of how we define the className prop for custom components1, we would also have to match string values inside objects that are passed to the prop.

These form the requirements for our ast-grep rule, which I’ve created as such:

language: tsx
rule:
  kind: string
  pattern: $CLASSES
  inside:
    kind: jsx_attribute
    regex: "className"
    stopBy: end

constraints:
  CLASSES:
    regex: \b(h-10)\b

transform:
  CHANGE_CLASS: replace($CLASSES, replace='h-10', by='h-9')

fix: "$CHANGE_CLASS"

For the rule2, we start with the node that we want to match which is a string node. This string node must be inside, i.e. a child node of, a jsx_attribute node. We use the stopBy: end option to specify that ast-grep should keep searching all the way down inside the jsx_attribute node for the nodes that we want to match. This is necessary because we also pass objects with key-string pairs to the className prop, and we want to match these strings as well. The stopBy: end option allows us to cover both cases: the jsx_attribute value being a string or an object with key-string pairs.

With just the above rules, we would match on every single className prop, which is unnecessary. So I added the constraints option to limit the nodes we match to only those that contain the string h-10. The configuration means we want to constrain the CLASSES variable that we defined in the rule as $CLASSES to only match nodes that satisfy the regex pattern \b(h-10)\b, which means that the string must have a standalone word (string separated by spaces on both ends) that is h-10. The parentheses around h-10 means to capture the string, but as of writing this, I don’t recall why I added them. I found out later as well that this line is probably not necessary either.

We still need to change h-10 to h-9, which is what the transform option is for. For all the matched $CLASSES, we replace instances of h-10 with h-9. Passing $CHANGE_CLASS to the fix option simply means to run the transform that we defined.

When running this rule, instead of doing a blanket apply to all matches, I selectively apply the changes as not all instances of the matched h-10 class needs to be changed. This concludes the change that I needed to make.

In hindsight, I could perhaps have used ripgrep and sd to achieve the same outcome in a simpler manner, because the string h-10 is quite specific to Tailwind CSS and shouldn’t really appear elsewhere (I briefly describe this other method in First use of ast-grep). This would perhaps have been more thorough as well, because it would match class strings that aren’t children of jsx_attribute nodes with the name “className”. In this case, I was still able to achieve the desired outcome, but in the future I also have a better understanding of which tools and methods to use to edit specific text across the codebase.


  1. We do this to get Tailwind CSS LSP support in the strings that are passed to the className prop. Custom components may accept several className strings because the component might allow the caller to configure different aspects of the component. By passing an object to the className prop, we remove the need to add additional patterns every time we have a new class prop that the LSP should match so that we get autocomplete for Tailwind classes. ↩︎

  2. Link to playground ↩︎