Skip to content

Dart

This page curates example ast-grep rules for Dart code.

Why does a Dart call pattern return no matches?

Dart does not allow expressions at the top level. As a result, a standalone pattern such as debugPrint($$$ARGS) can be parsed as a function_signature instead of a call_expression. You can confirm the parsed node with --debug-query:

sh
ast-grep run -p 'debugPrint($$$ARGS)' -l dart --debug-query=pattern

For a direct call whose callee is an identifier, provide a valid enclosing function and select the call expression to remove the ambiguity:

sh
ast-grep run \
  -p 'void _() { debugPrint($$$ARGS); }' \
  --selector call_expression \
  -l dart

The equivalent YAML rule uses a pattern object:

yaml
id: find-direct-debug-print
language: dart
rule:
  pattern:
    context: 'void _() { debugPrint($$$ARGS); }'
    selector: call_expression

This example intentionally matches only direct debugPrint(...) calls. For a copy-ready rule that also covers member, null-aware, generic, parenthesized, null-asserted, common single-level arrow-body, and direct cascade calls, see Find Dart Calls by Callee Name. That catalog entry includes an exact CLI command, a live Dart playground, and the rule's tested boundaries.

Find Dart Calls by Callee Name

Description

A standalone Dart pattern such as targetFn($$$ARGS) can be parsed as a function signature instead of a call expression. This rule avoids that ambiguity and finds common call forms whose syntactic callee is the exact identifier targetFn.

It covers direct, member, null-aware, generic, nested, parenthesized, null-asserted, common single-level sync or async arrow-body, and direct cascade calls. It does not match tear-offs, comments, strings, explicit new or const constructor expressions, operators, or indirect invocations whose callee is not syntactically named targetFn.

Copy the YAML into find-dart-calls-by-name.yml, replace every occurrence of targetFn with the simple Dart identifier you need, and run:

sh
ast-grep scan --rule find-dart-calls-by-name.yml lib test

YAML

yaml
id: find-dart-calls-by-name
message: Call to targetFn
severity: hint
language: dart
rule:
  any:
    # Ordinary, member, null-aware, generic, nested, parenthesized, and
    # null-asserted calls.
    - all:
        - kind: call_expression
        - has:
            field: function
            stopBy: neighbor
            all:
              - regex: '(?s)^(?:.*\.)?(?:\s|\(|/\*.*?\*/|//[^\n]*(?:\n|$))*targetFn(?:\s|/\*.*?\*/|//[^\n]*(?:\n|$))*(?:<.*>)?(?:\s|\)|!)*$'
              - not:
                  kind: function_expression
              - not:
                  has:
                    kind: function_expression
                    stopBy: end
    # tree-sitter-dart represents a direct arrow-body call as a call_expression
    # whose function field contains the enclosing function expression.
    - all:
        - kind: call_expression
        - has:
            field: function
            stopBy: neighbor
            all:
              - regex: '(?s)^.*=>(?:\s|\(|/\*.*?\*/|//[^\n]*(?:\n|$)|await\b)*(?:.*\.)?targetFn(?:\s|/\*.*?\*/|//[^\n]*(?:\n|$))*(?:<.*>)?(?:\s|\)|!)*$'
              - any:
                  - all:
                      - kind: function_expression
                      - not:
                          has:
                            kind: function_expression
                            stopBy: end
                  - all:
                      - any:
                          - kind: member_expression
                          - kind: null_aware_member_expression
                          - kind: instantiation_expression
                          - kind: null_assertion_expression
                      - has:
                          kind: function_expression
                          stopBy: end
    # obj..targetFn(...) and obj?..targetFn(...)
    - all:
        - kind: cascade_call_expression
        - has:
            field: property
            stopBy: neighbor
            regex: '^targetFn$'

Example

dart
void example(Service service, Service? maybeService) {
  targetFn('direct');
  service.targetFn('member');
  maybeService?.targetFn('null-aware');
  service..targetFn('cascade');
  targetFn<int>(1);
  final callback = () => targetFn('arrow body');

  final tearOff = targetFn;
  service.targetFn.toString();
  const text = 'targetFn("not code")';
}

For an arrow-body call, the reported match can include the whole closure because that is the range exposed by the current Dart grammar.

This is a syntax rule, not type resolution. Dart can omit new and const, so Type.targetFn() can be either a static method call or a named constructor and is included. Chained cascade sections such as service..child.targetFn() and nested arrow bodies such as () => () => targetFn() are outside this rule's current scope.

Contributed by

Vijay Muthukumaran

Made with ❤️ with Rust