Skip to content

JS/TS: object-literal members get no node unless the container is an export const — plain consts, window.X = {…} and IIFE-scoped objects are invisible #2300

Description

@tkhoaaa

Summary

Object-literal members (shorthand methods, key: function () {}, key: () => {}) are extracted as nodes only when the containing object is an export const. The same object without export, an object literal assigned to a member expression (window.api = { … }, ns.mod = { … }), or one declared inside an IIFE produces no member nodes at all.

That makes classic script-tag JavaScript — no modules, namespaces hung off window — almost invisible: the namespace constant is indexed, every method inside it is not. Related but distinct from #1573 / #1932, which are about edges to members that were already extracted.

Repro (v1.6.1, Windows x64, npm global install)

// src/a_export.ts
export const objTsExp = { tsExpM() { return 1; }, tsExpProp: () => 1 };
// src/b_plain.ts
const objTsPlain = { tsPlainM() { return 1; }, tsPlainProp: () => 1 };
// src/c_export.js
export const objJsExp = { jsExpM() { return 1; }, jsExpProp: () => 1 };
// src/d_plain.js
const objJsPlain = { jsPlainM() { return 1; }, jsPlainProp: () => 1 };
// src/e_iife.js
(function () {
  const objIife = { iifeM() { return 1; } };
  window.WS = { wsM() { return objIife.iifeM(); } };
})();

git init && git add -A && codegraph init -y ., then all non-file nodes:

src/a_export.ts    constant  objTsExp
src/a_export.ts    function  tsExpM
src/a_export.ts    function  tsExpProp
src/b_plain.ts     constant  objTsPlain
src/c_export.js    constant  objJsExp
src/c_export.js    function  jsExpM
src/c_export.js    function  jsExpProp
src/d_plain.js     constant  objJsPlain
Container Members extracted?
export const o = { m() {} } (.ts / .js) yes
const o = { m() {} } (.ts / .js) no
window.X = { m() {} } no (no node for X either)
object literal inside an IIFE no

A class method in the same file is extracted (class Svc { classMethod() {} } → method classMethod), so it is specific to object-literal containers.

Knock-on effects

  • Calls made inside an unextracted member are attributed to the enclosing constant: codegraph callers helper lists constant store instead of store.shorthand.
  • For window.api = { load() { return store.shorthand(); } } the call is lost entirely — there is no node to hang the edge on.
  • codegraph_explore for such a method name does not say "not found"; it returns ~26 KB of unrelated, loosely matching source, which costs more tokens than a grep would.

Real-world scale

A vanilla-JS browser app (script tags, no bundler, namespaces on window): 200 files under public/js, 670 method-shorthand definitions inside object literals, 0 present in the graph. The single largest module has 251 such methods; the shared auth / API-client modules are written the same way. Plain function declarations and nested arrows in the same files are indexed fine (~13k function nodes).

Expected

Object-literal members get nodes (qualified as container.member) regardless of whether the container is exported — including const objects, member-expression assignments (window.X = {…}, a.b = {…}), and objects inside IIFEs — the same way the exported case already works.

Activity

  1. colbymchenry commented on Oct 5, 2026

    @colbymchenry
    Owner

    Thanks @tkhoaaa for the detailed report and the real-world numbers. They made the scale of this clear.

    Fixed on main in #2363, building on #2310 by @danusha2345. Object-literal members used to become symbols only when the object was an export const. Any other literal was walked as one initializer, so calls made inside its members were credited to the constant or lost. Now every named object literal's functions are symbols of their own: shorthand methods, key: function () {} and key: () => {}. That covers:

    • a plain const api = { load() {…} }, exported or not
    • an object declared inside an IIFE or a function
    • a namespace hung on the page or on another object: window.App = { init() {…} }, App.utils = {…}, dw_page = {…}

    Each member's qualified name carries its object (api::load, window.App::init), and the calls made inside a member belong to that member. Calls reach them through their object: api.load(), window.App.init(), App.init() from another script on the page, App.utils.pad(), and a sibling's this.render(). A bare init() is not taken for App.init.

    On your repro, every member in b_plain.ts, d_plain.js and e_iife.js is now a symbol, including window.WS::wsM and objIife::iifeM. wsM's call to objIife.iifeM() is linked, and so is a WS.wsM() call from another file. On real projects, DokuWiki gained 85 member symbols and about 100 call links, and TodoMVC's script-tag examples gained over 400 symbols, with no measurable change in index time. Released builds parse JavaScript and TypeScript in the native parser, which has the same change.

    Not covered yet: object literals passed as arguments ($.extend({…}), enyo.kind({…})), prototype objects, module.exports = {…}, and revealing-module return {…} objects.

    This ships in the next release. Re-index after upgrading (codegraph index) to pick it up. You and @danusha2345 are credited in the changelog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions