Skip to content

issue-2088 plan: generator function declarations (function* name(){}) are invisible to findTopLevelFunctionNodeByName's redeclaration count #2636

Description

@carlos-alm

What

docs/plans/issue-2088.md's findTopLevelFunctionNodeByName (condition 4's identifier-resolution chain, WU-2) recognizes a top-level function-declaration statement via an exact string match, stmt.type === 'function_declaration'. tree-sitter-javascript gives a generator function declaration (function* name() {}) a DIFFERENT node type, generator_function_declaration — confirmed by this repo's own already-shipped FUNCTION_SCOPE_NODE_TYPES constant (src/extractors/javascript.ts), which lists function_declaration and generator_function_declaration as two separate members precisely because they are distinct grammar kinds (the same pairing exists for the expression forms, function_expression vs generator_function).

So a top-level function* name() { ... } is invisible to declarationCount entirely, both for the ORIGINAL round-14 direct-children loop and for round 15's new countHoistedVarScopeDeclarations walk (which uses the identical === 'function_declaration' test, since it targets the same statement shape one hoisting hop further). Concretely:

function* run() { return 0; }
function* run() { return this.alpha(); }
const T = { alpha: fnA, run };
T.run();

Neither declaration of run is ever counted — declarationCount stays at 0 for this name via the function_declaration branch — so findTopLevelFunctionNodeByName returns null (the same "no module-level declaration found" fallback as if run didn't exist at all), and the caller fails safe. This happens to still be SAFE (fail-safe, not confidently wrong), because the function-shape branch never sets result for a generator_function_declaration either, so nothing is ever returned with false confidence for this shape — but it is a silent detection gap in a resolution chain that has otherwise been careful to enumerate every declaration statement shape recognized as a REDECLARATION CANDIDATE, and it applies identically to a hoisted (round-15) generator declaration nested in a block.

Note this is narrower than it might first appear: the DECLARATOR path (const run = function*() {}, under lexical_declaration/variable_declaration) is unaffected — declarationCount++ there fires on the declarator's OWN name match, regardless of what shape the value is, so only the standalone generator-declaration STATEMENT form is invisible, not a generator function assigned to a const/let/var.

Why this wasn't fixed inline

It is a narrow (fail-safe-already, not confidently-wrong) gap in a pre-existing, already-verified-correct part of round 14's own code (findTopLevelFunctionNodeByName's function_declaration string match), discovered while verifying round 15's own extension reuses the identical test — fixing it would mean touching round 14's already-settled work, which round 15 is scoped not to disturb, for an idiom (a dispatch-table handler declared as a generator function, redeclared or hoisted) with no observed instance in this codebase.

Suggested fix shape (not binding — decide at execute/fix time)

Wherever this chain tests stmt.type === 'function_declaration' for a candidate redeclaration (both the direct-children loop and countHoistedVarScopeDeclarations), also accept 'generator_function_declaration' — mirrored in the Rust engine's find_top_level_function_node_by_name / count_hoisted_var_scope_declarations. Add a WU-10 escape-fallback case proving a duplicate top-level (or hoisted) generator-function declaration correlates once implemented.

Where

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions