Skip to content

TS/JS: new Foo() emits no edge — call_function_field="function" does not match new_expression, whose callee field is constructor #3116

Description

@BenWaNH

Summary

In TS/JS, new Foo() emits no edge, in the same file or across files, even though
new_expression is listed in _TS_CONFIG.call_types. Static calls and plain function
calls in the same method body resolve normally, so walk_calls itself is fine — only the
construction site is dropped.

Same user-visible symptom as #2997 (C#, fixed in #2998) and the PHP gap I just filed
(#3115), but a different root cause: here the node type is dispatched, its callee field is
just never read.

Repro (graphify 0.9.50, tree_sitter_typescript 0.23.2)

One file, graphify extract . --code-only:

export class Foo { constructor(public x: number) {} static make(): Foo { return new Foo(0) } }
export function helper(n: number): number { return n + 1 }
export class Caller {
  run(): void {
    const a = new Foo(1)   // new
    const b = Foo.make()   // static method call
    const c = helper(2)    // plain function call
  }
}

Edges emitted from Caller.run (excluding contains/method):

call site edge
new Foo(1) none
Foo.make() run --calls--> Foo.make
helper(2) run --calls--> helper

Cross-file behaves identically: with Foo in models.ts and a named
import { Foo } from './models', the import edge is emitted, but new Foo(1) in the
method body still produces nothing. For contrast, the equivalent Python file
(a = Foo(1)) emits both calls and uses — instantiation is a call there, so the
generic path covers it.

Likely root cause

In tree-sitter-typescript the two node types expose the callee under different field names:

new_expression  : function=None       constructor=identifier  arguments=arguments
call_expression : function=identifier constructor=None        arguments=arguments

_TS_CONFIG (and _TSX_CONFIG, which inherits it) sets:

call_types=frozenset({"call_expression", "new_expression"}),
call_function_field="function",

So child_by_field_name("function") returns None for every new_expression, no callee
name is extracted, and the entry in call_types is effectively inert. #708 added
new_expression extraction, but the field name looks like it was never adjusted.

C# solves the equivalent problem with a node-type special case in walk_calls
(engine.py, elif config.ts_module == "tree_sitter_c_sharp" and node.type == "object_creation_expression"). A per-node-type callee field, or the same kind of special
case reading constructor, would cover TS/JS/TSX.

Why it matters

"What breaks if I change X" is the main reason to query a graph instead of grepping, and in
class-based TS a type that is only ever constructed at its call sites now looks unused.

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