Structural Code Search With ast-grep
ast-grep is an amazing tool for structural code search. With it, you can search for code by it’s AST shape, rather than just a simple string or regex search. I’ve been using it a fair bit at work, and I developed a few scripts that replaced the core logic of the Flashlight project I built last year.
Find JSX tags
This script searches for JSX tags in your code. You can specify a tag name, and optionally an attribute/value where only the tags containing the attribute or attribute/value pair will be listed.
#!/bin/bash
cwd="$(pwd)"
format="rich"
_print_help() {
if [ -n "$1" ]; then
printf "%s\n\n" "$1"
fi
echo "Usage: tags [options] <name> [attribute] [value]
Options:
-h, --help Display this help message and exit
-c, --cwd <dir> Set the current working directory to <dir>
-f, --format <format> Set the output format (short|long)
Arguments:
<name> The tag name to search for
[attribute] Filter by attribute
[value] Filter by attribute value"
}
_parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
_print_help "Find JSX tags"
exit 0
;;
-c | --cwd)
cwd="$2"
shift 2
;;
-f | --format)
if [[ "$2" == 'short' ]]; then
format="short"
elif [[ "$2" == 'long' ]]; then
format="rich"
else
_print_help "Invalid format: $2"
exit 1
fi
shift 2
;;
*)
if [ -z "$name" ]; then
name="$1"
elif [ -z "$attribute" ]; then
attribute="$1"
elif [ -z "$value" ]; then
value="$1"
else
_print_help "Unexpected argument: $1"
exit 1
fi
shift
;;
esac
done
}
_split() {
echo "$1" | while IFS='.' read -ra parts; do
for part in "${parts[@]}"; do
echo "$part"
done
done
}
_get_rule() {
namespaced_tag=($(_split "$name"))
head="
id: tags
language: TSX
utils:
is-tag:
any:
- kind: jsx_opening_element
- kind: jsx_self_closing_element
has:
any:
- kind: identifier
regex: '^$name$'
- kind: member_expression
all:
- has:
kind: identifier
field: object
regex: '^${namespaced_tag[0]}$'
- has:
kind: property_identifier
field: property
regex: '^${namespaced_tag[1]}$'
"
if [ -n "$attribute" ]; then
echo "
$head
rule:
kind: jsx_attribute
all:
- has:
kind: property_identifier
regex: '^$attribute$'
$(if [ -n "$value" ]; then
# TODO: Support boolean, boolean shorthand, null, undefined, and template literals
echo "
- has:
any:
- kind: string
has:
kind: string_fragment
regex: '^$value$'
- has:
kind: number
regex: '^$value$'
"
fi)
inside:
matches: is-tag
"
else
echo "
$head
rule:
matches: is-tag
"
fi
}
_find_tags() {
if [ -z "$name" ]; then
_print_help "Missing name argument"
exit 1
fi
sg scan \
--config "$HOME/.sgconfig.yml" \
--inline-rules "$(_get_rule)" \
--report-style="$format" \
"$cwd"
}
_parse_args "$@"
_find_tags
For this script to work, we need to specify a global config file to instruct ast-grep to search for all js, jsx, ts, and tsx file extensions since by default it will only search for the specified language.
ruleDirs: []
languageGlobs:
tsx: ["*.js", "*.jsx", "*.ts", "*.tsx"]
To use this script, we can run the following command to find code like
this: <MyButton onClick={...}>
tags MyButton onClick
Find imports
This script searches for imports in your code. You can specify an import
source (e.g. react
), and optionally an import specifier (e.g.
useState
).
#!/bin/bash
cwd="$(pwd)"
format="rich"
_print_help() {
if [ -n "$1" ]; then
printf "%s\n\n" "$1"
fi
echo "Usage: imports [options] <source> [specifier]
Options:
-h, --help Display this help message and exit
-c, --cwd <dir> Set the current working directory to <dir>
-f, --format <format> Set the output format (short|long)
Arguments:
<source> The import source to search for
[specifier] Only return imports containing this specifier"
}
_parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
_print_help "Find JavaScript imports"
exit 0
;;
-c | --cwd)
cwd="$2"
shift 2
;;
-f | --format)
if [[ "$2" == 'short' ]]; then
format="short"
elif [[ "$2" == 'long' ]]; then
format="rich"
else
_print_help "Invalid format: $2"
exit 1
fi
shift 2
;;
*)
if [ -z "$source" ]; then
source="$1"
elif [ -z "$specifier" ]; then
specifier="$1"
else
_print_help "Unexpected argument: $1"
exit 1
fi
shift
;;
esac
done
}
_to_regex() {
# If already in regex format, return as is removing the leading and traililng slash
if [[ "$1" == /* && "$1" == */ ]]; then
echo "${1:1:${#1}-2}"
else
echo "^$1$"
fi
}
_get_rule() {
head="
id: imports
language: TSX
utils:
is-import:
kind: import_statement
has:
kind: string
field: source
has:
kind: string_fragment
regex: '$(_to_regex "$1")'
"
# If a specifier is provided, we search for the specifier within the
# import given the specified source.
if [ -n "$2" ]; then
echo "
$head
rule:
kind: import_specifier
regex: '$(_to_regex "$2")'
inside:
kind: named_imports
inside:
kind: import_clause
inside:
matches: is-import
"
else
echo "
$head
rule:
matches: is-import
"
fi
}
_find_imports() {
if [ -z "$source" ]; then
_print_help "Missing source argument"
exit 1
fi
sg scan \
--config "$HOME/.sgconfig.yml" \
--inline-rules "$(_get_rule "$source" "$specifier")" \
--report-style="$format" \
"$cwd"
}
_parse_args "$@"
_find_imports
To use this script, we can run the following command to find code like
this: import { useState } from 'react'
:
imports react useState
It even supports searching by regex! If we want to search for all React hook imports from any module, we can use this command:
imports '/.*/' '/^use.*/'