Skip to main content

innermost_declarator

Function innermost_declarator 

Source
pub fn innermost_declarator<'tree, T: Checker>(
    node: &Node<'tree>,
) -> Option<Node<'tree>>
Expand description

The innermost declarator along a C-family function’s declarator chain: the node whose parameters field holds the function’s own formal arguments. declarator_name takes the name from the same node’s declarator field.

C declarator syntax nests outward from the declared name, so a function node’s declarator field is only the function’s parameter list when the return type is plain. Anything the return type contributes — a *, a &, a parenthesised group — wraps the function_declarator that owns the real list, and the outermost parameters a chain carries can belong to the return type rather than to the function (int (*f(int a))(int b) returns a pointer to a one-argument function and itself takes one argument). Taking the innermost is what makes both of those come out right (#1200), and the same node carries the name f that a leftmost search misses (#1208).

“Innermost” has one exception, and it is the one shape where the grammar’s reading and the preprocessor’s disagree. An unexpanded function-like macro — RUN_STATS_METHOD(allocate)(JNIEnv *env, jclass clazz), which is what every JNI shim looks like — parses as a function_declarator sitting in another one’s declarator field, so the innermost list is the macro’s (allocate) and the function’s own arguments are discarded. Neither language permits that chain: a function may not return a function type (C11 6.7.6.3p1, C++ [dcl.fct]), so a legitimate function returning a function pointer always interposes a parenthesized_declarator, and the direct nesting can only be a macro (or an ERROR, below). The walk stops at the outer link there and reports the function’s arity (#1213).

The walk is by field name, per .claude/rules/grammar-dispatch.md §3, which also sidesteps §1: PointerDeclarator2, FunctionDeclarator2/3 and ReferenceDeclarator2/3/4 are numeric-suffix aliases that a kind_id match would have to enumerate and would silently regress on the next grammar bump.

Three of the rules on the chain expose no field at all, which is why the field alone is not enough. Every entry below is from the pinned grammars’ node-types.json (tree-sitter-cpp 0.23.4, tree-sitter-c 0.24.2, tree-sitter-objc 3.0.2, vendored tree-sitter-mozcpp), and the fieldless list is the complete set of *_declarator rules with no fields that a function definition’s name side can reach — the rest (variadic_declarator, structured_binding_declarator, Objective-C’s keyword_declarator and struct_declarator, and the abstract_* family) sit in parameter, binding or type position, never here.

ruledeclarator field
pointer_declaratorrequired
function_declaratorrequired
abstract_function_declaratoroptional
reference_declaratorabsent — no fields
parenthesized_declaratorabsent — no fields
attributed_declaratorabsent — no fields

In all three fieldless rules the inner declarator is the last named child once attributes are set aside, so that is the fallback:

  • reference_declarator is seq(choice('&', '&&'), _declarator).
  • parenthesized_declarator is seq('(', optional(ms_call_modifier), _declarator, ')') — last rather than sole, so int (__cdecl *f(int a))(int b) does not defeat it.
  • attributed_declarator is seq(_declarator, repeat1(attribute_declaration)), the one rule that puts the declarator first. Excluding attribute_declaration — its only non-declarator child type in all four grammars — restores “last” as the right answer, and without that exclusion int f(int a, int b) [[deprecated]] reports 0.

template_argument_list is excluded for the same reason as attribute_declaration, and it is the one exclusion the fallback needs beyond the three rules above. The fallback also runs on the name forms, which have no declarator field either, and two of them — template_function and template_method — put their argument list last: void f<int (*)(int x, int y)>(int a). A type argument spelling a function type carries a parameters field of its own, so descending into it made that function read as taking two arguments and made its name resolve to nothing at all, the abstract declarator the chain landed on spelling no identifier. Excluding the argument list leaves the name itself as the last named child, which terminates the chain where it should.

Comments are excluded for the same reason, tree-sitter admitting one anywhere.

The fallback stops at a node that already carries parameters, which is the C++ lambda: abstract_function_declarator’s declarator field is optional, so [](int a, int (*cb)(int x)) would otherwise descend into the parameter_list and return cb’s (int x) — one argument instead of two.

Every step strictly descends a finite tree, so the walk terminates without a depth cap.

§ERROR-recovery trees are outside this contract

Every rule above is the grammar’s, and none of them holds once tree-sitter starts recovering. An unexpanded macro in declarator position — T *f() TF_ATTRIBUTE_NOINLINE { … } — puts the real function_declarator inside an ERROR node and leaves the macro’s field_identifier as the pointer_declarator’s last named child, so the fallback follows the macro and the walk answers None.

Give that macro an argument — T *f() TF_LOCKS_EXCLUDED(mu_) { … }, which is the spelling the TensorFlow / Abseil annotations actually take — and it is a function_declarator carrying parameters, so the walk answers with the macro’s name rather than with nothing. That is the one shape this change made worse: the leftmost pre-order search it replaced descended into the ERROR and got f right. a_parenthesised_macro_takes_the_name_of_the_function_it_annotates pins it.

Recovery also manufactures the direct function_declarator nesting the macro rule keys on, from source containing no macro-obscured declarator at all. A statement macro followed by an if — TensorFlow’s TF_ASSIGN_OR_RETURN(bool ok, Try(x)); if (ok) { … } — recovers into a function_declarator whose declarator field is the macro call and whose parameters field is the if condition. So the rule changes the answer for 19 of the 46 corpus spaces it touches, from the macro’s argument count to the condition’s, neither of which is an arity. There is no fixture for it: whether the grammar recovers this way depends on where the line breaks fall, tree-sitter costing a recovery by the extent it skips, so any pinned spelling would be a claim about whitespace (#1213).

Whatever any strategy returns there is arbitrary, and the walk does not try to be clever about it. Measured over DeepSpeech and pdf.js (14,269 files), moving the four getters onto this walk named 46 previously-nameless function spaces, un-named 2 and renamed 4 — 354 nameless spaces down to 310, a net 44. All six of the latter sit inside recovery subtrees: one of the un-named had been reporting an if statement’s callee as a function name, and one of the renamed is the TF_LOCKS_EXCLUDED case above (#1208).