emend Grammar & Cookbook¶
This reference covers the full syntax for selectors, patterns, and commands, followed by cookbook recipes for common refactoring tasks. Call this tool whenever you need to construct an emend command and aren’t sure of the exact syntax.
Selector syntax¶
Selectors address Python symbols and their components. The general form is:
[path]::[symbol.path][component][accessor]
If the path is omitted (::func), it defaults to ** (all Python files).
A bare name that doesn’t match a file (func) is also treated as
**::func.
Symbol selectors¶
file.py::func # module-level function
file.py::Class # class
file.py::Class.method # method
file.py::Class.method.nested # nested function inside a method
::func # search all files for func
func # same (bare name fallback)
Class.method # search all files for Class.method
Extended selectors (components)¶
file.py::func[params] # all parameters
file.py::func[params][ctx] # parameter by name
file.py::func[params][0] # parameter by index
file.py::func[returns] # return annotation
file.py::func[decorators] # decorator list
file.py::Class[bases] # base classes
file.py::func[body] # function body
file.py::[imports] # module-level imports
Components: params, returns, decorators, bases, body,
imports.
Accessors: [name] by name, [0] by index, [-1] last.
Pseudo-class selectors¶
For the add command, specify the parameter kind slot:
file.py::func[params]:KEYWORD_ONLY # after *
file.py::func[params]:POSITIONAL_ONLY # before /
file.py::func[params]:POSITIONAL_OR_KEYWORD # regular
Wildcard selectors¶
file.py::*[params] # all functions' params
file.py::Test*[decorators] # decorators on Test* symbols
file.py::*.*[returns] # all method return types
file.py::Class.*[body] # all method bodies in Class
File globs¶
'src/**/*.py::func' # match across multiple files
'**::func' # all Python files recursively
File scope with patterns¶
The :: separator also works with code patterns, not just selectors.
When the right side of :: doesn’t parse as a valid selector, it is
treated as a code pattern:
'**::print($X)' # search all files for pattern
'src/::assert False' # search src/ for literal pattern
'file.py::print()' # search one file for pattern
Detection order:
Right side contains
$→ pattern mode (metavar search)Right side parses as valid selector → selector mode (symbol lookup)
Right side fails selector parse → pattern mode (literal code search)
Line selectors¶
file.py:42 # single line
file.py:42-100 # line range
Formal selector grammar (Lark)¶
start: selector
selector: explicit_selector | dotted_selector
explicit_selector: file_path DOUBLE_COLON symbol_path? type_filter? component*
dotted_selector: symbol_path type_filter? component*
file_path: PATH
symbol_path: symbol_segment ("." symbol_segment)*
symbol_segment: WILDCARD | IDENTIFIER
type_filter: TYPE_FILTER
component: "[" COMPONENT_NAME "]" accessor? pseudo_class?
accessor: "[" (IDENTIFIER | INT) "]"
pseudo_class: PSEUDO_CLASS
COMPONENT_NAME: "params" | "returns" | "decorators" | "bases" | "body" | "imports"
DOUBLE_COLON: "::"
PATH: /[^:\[\s]+/
WILDCARD: "*" | /[a-zA-Z_*][a-zA-Z0-9_*]*/
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
INT: /-?\d+/
PSEUDO_CLASS: /:KEYWORD_ONLY|:POSITIONAL_ONLY|:POSITIONAL_OR_KEYWORD/
TYPE_FILTER: /:(?:returns|type)\[(?:[^\[\]]*(?:\[[^\[\]]*\])?)*\]/
%declare _LITERALS_ALLOWED_TO_COLLIDE
%import common.WS
%ignore WS
Pattern syntax¶
Patterns match code structures using metavariables ($-prefixed).
Metavariables¶
$X # any single expression
$_ # wildcard (match but don't capture)
$...ARGS # zero or more arguments (variadic/ellipsis)
$X:int # type-constrained metavar
$X:!int # negated type constraint
Syntactic type constraints: int, str, float, expr, stmt,
identifier, call, attr.
Oracle type constraints (require --type-engine):
$X:type[Connection] # inferred type matches Connection
$F:returns[str | None] # inferred return type matches
Expression patterns¶
print($X) # call with one arg
func($A, $B) # call with two args
func($...ARGS) # call with any number of args
$A + $B # binary operation
$X[$Y] # subscript
$OBJ.method($X) # method call
self.$ATTR # attribute access
$X is None # comparison
$A == $B # equality
$A < $B < $C # chained comparison
lambda $X: $EXPR # lambda
*$X # star unpack
**$X # double-star unpack
{'key': $V, ...} # partial dict match
[$X, $Y] # list with two elements
Statement patterns¶
return $X # return
raise $EXC($MSG) # raise
assert $A == $B # assert
$NAME = $VALUE # assignment
$NAME += $VALUE # augmented assignment
if $COND: # if statement
for $VAR in $ITER: # for loop
while $COND: # while loop
with $CTX as $VAR: # with statement
try: # try block
except $EXC as $VAR: # except clause
async for $VAR in $ITER: # async for
async with $CTX as $VAR: # async with
Decorator patterns:
@$DEC\ndef $FUNC($...ARGS): # decorated function
@property\ndef $F($...A): # specific decorator
Formal pattern grammar (Lark)¶
start: pattern
pattern: (code_chunk | metavar)+
metavar: DOLLAR (ELLIPSIS)? METAVAR_NAME type_constraint?
| DOLLAR UNDERSCORE
type_constraint: SIMPLE_TYPE_CONSTRAINT
| ORACLE_TYPE_CONSTRAINT
DOLLAR: "$"
ELLIPSIS: "..."
UNDERSCORE: "_"
METAVAR_NAME: /[A-Z][A-Z0-9_]*/
SIMPLE_TYPE_CONSTRAINT: /:!?(?:expr|stmt|identifier|int|str|float|call|attr|any)/
ORACLE_TYPE_CONSTRAINT: /:(?:type|returns)\[(?:[^\[\]]*(?:\[[^\[\]]*\])?)*\]/
code_chunk: /[^$:]+/ | ":"
Replacement syntax¶
In edit replace, reference captured metavariables:
emend edit replace 'print($X)' 'logger.info($X)' src/ --apply
String content interpolation – ${X.content} strips quotes from a
captured string literal:
emend edit replace 'Union["$X", $Y]' '${X.content} | $Y' src/ --apply
Commands¶
All write operations default to dry-run (show diff). Pass --apply to
write changes.
find¶
Auto-detects mode from the query:
Pattern mode: query contains
$metavariables, or right side of::doesn’t parse as a valid selector (e.g.assert False,print())Lookup mode: query contains
::with a valid selectorSummary mode: bare file/dir path without filters
Bare name: name that doesn’t match a file searches as
**::name
Canonical command name is find. Hidden compatibility aliases include search,
grep, show, get, lookup, and ls.
emend find 'print($X)' src/ # pattern (has $)
emend find '**::print($X)' # pattern with file scope
emend find '**::assert False' # literal pattern via ::
emend find 'src/::import os' # pattern scoped to src/
emend find file.py::func[params] # lookup (valid selector)
emend find file.py # summary
emend find process_encounter # bare name
emend find ::MyClass # ::name shorthand
emend find hello src/ # bare name scoped to src/
- Output formats (
--output/-o): code,location,selector,summary,metadata,json,count. Modifiers:summary::flat,code::dedent.- Filters (lookup/summary mode):
--kind function|method|class|async_function|async_method,--name 'test_*',--returns str,--depth 1,--has-param ctx,-i(case-insensitive),--smart-case.- Scope constraints (
--where): 'def','class','def test_*','async def fetch_*','not class','MyClass.method','@decorator'.- Other:
--imported-from MODULE,--scope-local,--type-engine auto|pyrefly|pyright|ty.
replace¶
emend edit replace PATTERN REPLACEMENT PATH [--apply] [--where EXPR]
emend edit replace 'print($X)' 'logger.info($X)' src/ --apply
emend edit replace 'assertEqual($A, $B)' 'assert $A == $B' tests/ --apply
emend edit replace 'old_api($X)' 'new_api($X)' src/ --where 'def' --apply
edit¶
Edit or replace existing symbol components. Canonical grouped form is
emend edit set. Hidden alias: set.
emend edit set SELECTOR VALUE [--apply]
emend edit set api.py::get_user[returns] "User | None" --apply
emend edit rm api.py::get_user[params][debug] --apply
emend edit rm api.py::deprecated_func --apply
add¶
Insert new items into list components. Hidden alias: insert.
emend edit add SELECTOR VALUE [--at N] [--before NAME] [--after NAME] [--apply]
emend edit add api.py::get_user[params] "timeout: int = 30" --apply
emend edit add api.py::get_user[params] "ctx: Context" --after self --apply
emend edit add api.py::func[params]:KEYWORD_ONLY "force: bool = False" --apply
emend edit add api.py::func[decorators] "@cache" --at 0 --apply
emend edit add models.py::User[bases] "TimestampMixin" --apply
rm¶
Remove a symbol or component. Hidden aliases: remove and delete.
Canonical grouped form is emend edit rm.
emend edit rm SELECTOR [--apply]
emend edit rm api.py::get_user[params][debug] --apply
emend edit rm api.py::deprecated_func --apply
refs¶
Find all references (scope-aware). Hidden aliases: references, find-references.
emend analyze refs SELECTOR [--exclude-definition] [--exclude-imports]
[--writes-only] [--reads-only] [--calls-only] [--project DIR]
emend analyze refs src/api.py::process_request --calls-only
emend analyze refs config.py::settings --writes-only
rename¶
Rename a symbol or module across the project:
emend edit rename SELECTOR --to NEW_NAME [--apply] [--docs] [--unsure]
emend edit rename api.py::OldClass --to NewClass --apply
emend edit rename old_utils.py --to new_utils --apply # module rename
mv¶
Move a symbol or module, updating imports. Canonical grouped form is
emend edit mv. Hidden alias: move.
emend edit mv SELECTOR DESTINATION [--dedent] [--apply]
emend edit mv utils.py::helper other.py --apply
emend edit mv utils.py pkg/ --project . --apply # module move
cp¶
Copy a symbol to another file (exact AST copy, no hallucination risk). Canonical
grouped form is emend edit cp. Hidden aliases: copy-to, copy.
emend edit cp SELECTOR DESTINATION [--append] [--dedent] [--apply]
emend edit cp module.py::OuterClass.inner_func helpers.py --dedent --apply
graph¶
Generate a call graph:
emend analyze graph FILE [--format plain|json|dot] [--project DIR]
emend analyze graph src/app.py --format dot | dot -Tsvg > deps.svg
deadcode¶
Find potentially dead (unreferenced) code:
emend analyze deadcode [PATH] [--kind function|class] [--include-private]
[--exclude-references-from DIR] [--no-strings]
[--no-last-reference] [--unused-modules] [--json]
Inline suppression: # noqa: emend:deadcode.
lint¶
Lint using rules from .emend/rules.yaml:
emend lint PATH [--config FILE] [--fix] [--rule NAME]
check¶
Run unified match, flow, deadcode, and type rules from
.emend/rules.yaml:
emend check [PATHS]... [--rule NAME] [--kind KIND] [--fix] [--json]
batch¶
Apply batch operations from a YAML/JSON file:
emend edit batch OPS_FILE [--apply]
Operation types: rename, replace, add, edit, remove.
Example YAML:
operations:
- rename: {selector: "api.py::get_user", to: "fetch_user"}
- replace: {pattern: "get_user($ID)", replacement: "fetch_user(user_id=$ID)", path: "src/"}
- add: {selector: "api.py::fetch_user[params]:KEYWORD_ONLY", value: "timeout: float = 30.0"}
Fact graph (facts_query)¶
The facts_query tool provides structured access to the project fact graph.
Use the fact_type parameter to query: symbols, calls, references,
trace_flows, types, or imports.
Cookbook recipes¶
Migrate unittest to pytest¶
emend edit replace 'self.assertEqual($A, $B)' 'assert $A == $B' tests/ --apply
emend edit replace 'self.assertTrue($X)' 'assert $X' tests/ --apply
emend edit replace 'self.assertFalse($X)' 'assert not $X' tests/ --apply
emend edit replace 'self.assertIsNone($X)' 'assert $X is None' tests/ --apply
emend edit replace 'self.assertIn($A, $B)' 'assert $A in $B' tests/ --apply
Replace print with logging¶
emend edit replace 'print($X)' 'logger.info($X)' src/ --apply
Add a parameter to every method in a class¶
emend find api.py::MyClass --kind method --output selector | while read sel; do
emend edit add "$sel[params]" "ctx: Context" --after self --apply
done
Add return type annotations¶
emend edit set api.py::get_user[returns] "User | None" --apply
Find and fix open() calls without encoding¶
emend find 'open($PATH)' src/
emend edit replace 'open($PATH)' 'open($PATH, encoding="utf-8")' src/ --apply
Extract a nested function to a new module¶
emend edit cp module.py::Outer.inner_func helpers.py --dedent --apply
emend edit rm module.py::Outer.inner_func --apply
Find all callers of a function¶
emend analyze refs src/api.py::process_request --calls-only
Find where a variable is mutated vs read¶
emend analyze refs config.py::settings --writes-only
emend analyze refs config.py::settings --reads-only
Batch-add a decorator to all async functions¶
emend find src/ --kind async_function --output selector | while read sel; do
emend edit add "$sel[decorators]" "@trace" --at 0 --apply
done
Rename a class everywhere (including docstrings)¶
emend edit rename api.py::OldName --to NewName --docs --apply
Move a function and update all imports¶
emend edit mv utils.py::helper_func helpers/core.py --apply
Set up a lint rule to catch prints¶
.emend/rules.yaml:
rules:
no-print:
match: "print($...ARGS)"
not-within: "def test_*"
message: "Use logger instead of print"
fix: "logger.info($...ARGS)"
Then:
emend check src/
emend check src/ --fix
Multi-step refactoring with batch¶
refactor.yaml:
operations:
- rename: {selector: "api.py::get_user", to: "fetch_user"}
- replace:
pattern: "get_user($ID)"
replacement: "fetch_user(user_id=$ID)"
path: "src/"
- add:
selector: "api.py::fetch_user[params]:KEYWORD_ONLY"
value: "timeout: float = 30.0"
emend edit batch refactor.yaml # preview
emend edit batch refactor.yaml --apply
Find functions that raise a specific exception¶
emend find 'raise ValueError($MSG)' src/ --output json
Scope-aware searching¶
emend find 'print($X)' tests/ --within 'def test_*' # inside test funcs
emend find 'await $X' src/ --within 'async def fetch_*' # inside async funcs
emend find 'print($X)' src/ --not-within 'class' # outside classes
emend find 'config' src/ --scope-local # only local definitions
emend find 'json.loads($X)' src/ --imported-from json # import-aware