Bytes is my collection of short-form posts, tips, and things I learn as I build software.
Loading...
Loading...
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.
/usr/local/bin/tags
#!/bin/bashcwd="$(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.
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).
/usr/local/bin/imports
#!/bin/bashcwd="$(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>
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.*/'
-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