You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
JS/TS: a class defined inside a function body is never extracted (declaration or expression), so mixin factories function f(Base) { return class X extends Base {…} } lose their methods and the inheritance chain dead-ends at the applied const #3349
Two related extraction gaps, both JS and TS (tested on 0.9.54):
A. Classes nested inside a function body get no node at all. This applies to a named class declaration (function outer() { class Inner {…} }), a named class expression in a return, and an anonymous return class extends Base {…}. Their methods therefore have no method edges and no call edges in either direction.
B. A module-level class expression bound to a const gets a bare variable node.export const Foo = class Foo extends Root { m() {} } produces a Foo node but no method edge for m() and no inherits Foo -> Root, even though Root is in-corpus and imported.
The case this hits hardest is the mixin-factory idiom, where A and B combine: a factory returns a class extending its parameter, callers apply it to an environment-specific base, and subclass the result. On a codebase built that way, class X extends Y sites vs. inherits edges after 0.9.54 (#1790 covers the module-scope declarations):
count
class X extends Y sites (shared/client/server dirs)
673
with an inherits edge
383
missing, return class X extends <param> inside a factory
188
missing, inline class X extends createSharedFoo(Base)
15
missing, extends Map etc. (correctly not fabricated)
3
The ~200 unresolved sites are the base-class links of every entity hierarchy, so graphify path between a concrete class and its base has to route through imports_from.
Not asking for a calls edge from the top-level const Y = f(Z) to f here; that's #1972 and the proposal below doesn't depend on it, because the const binding already has a node.
One node for the factory's class body (methods live there), and each application site keeps its own const node with its own inherits to the base it was applied with; the two applications have different superclasses:
graphify path "Widget" "ClientEntity" then walks pure inherits, and .render() -> .describe() resolves through the chain.
Suggested resolution, three independent steps, all static
Extract class declarations and class expressions wherever they occur, not only at module scope: nested in a function body, as a return value, or bound by const. Parent them to the enclosing function/file the way nested functions already are (inner() in the variant above does get a node). This alone fixes A and B and recovers the mixin methods; it can land independently of 2 and 3.
Tag mixin factories: a function whose body returns a class whose extends target is one of the function's own parameters. Record the parameter index. JS analogue of the Ruby Class.new(Super) handling already in the engine.
Bind at the application site. For const Y = f(Z) (or the inline class X extends f(Z)) where f resolves to exactly one in-corpus mixin factory and Z to exactly one class, emit inherits Y -> <factory's class> and inherits Y -> Z. Ambiguous resolution emits nothing, same single-match rule as C# dispatches_to (0.9.49) and the cross-repo type link (0.9.54). No caller node is needed, so this is independent of Calls and constant reads in top-level / script context never get a caller node, so they emit no edges (uniform across languages) #1972.
Environment
graphifyy 0.9.54, Python 3.12, Windows 11. Real-world corpus: 1,581 files / 14.5k nodes; pattern is export function createShared<Name>(Base) { return class Shared<Name> extends Base {…} } applied once each in clientjs/ and serverjs/.
Summary
Two related extraction gaps, both JS and TS (tested on 0.9.54):
A. Classes nested inside a function body get no node at all. This applies to a named class declaration (
function outer() { class Inner {…} }), a named class expression in areturn, and an anonymousreturn class extends Base {…}. Their methods therefore have nomethodedges and no call edges in either direction.B. A module-level class expression bound to a
constgets a bare variable node.export const Foo = class Foo extends Root { m() {} }produces aFoonode but nomethodedge form()and noinherits Foo -> Root, even thoughRootis in-corpus and imported.The case this hits hardest is the mixin-factory idiom, where A and B combine: a factory returns a class extending its parameter, callers apply it to an environment-specific base, and subclass the result. On a codebase built that way,
class X extends Ysites vs.inheritsedges after 0.9.54 (#1790 covers the module-scope declarations):class X extends Ysites (shared/client/server dirs)inheritsedgereturn class X extends <param>inside a factoryclass X extends createSharedFoo(Base)extends Mapetc. (correctly not fabricated)The ~200 unresolved sites are the base-class links of every entity hierarchy, so
graphify pathbetween a concrete class and its base has to route throughimports_from.Not asking for a
callsedge from the top-levelconst Y = f(Z)tofhere; that's #1972 and the proposal below doesn't depend on it, because theconstbinding already has a node.Minimal repro (5 files, copy-paste)
shared/Widget.jsclientjs/ClientEntity.jsclientjs/Widget.jsserverjs/ServerEntity.jsserverjs/Widget.jsActual (0.9.54,
contains/imports_fromomitted)Nodes:
createSharedWidget(),Widget×2,SharedWidget×2 (theconstbindings in clientjs/serverjs),ClientEntity,ServerEntity,.render(),.persist(),.constructor()×2.No node for the
SharedWidgetclass insidecreateSharedWidget, and none for.describe().Missing: any
inheritsreachingClientEntity/ServerEntity;methodfor.describe(); consequentlycalls .render() -> .describe().Variants confirming the scope (each run separately, same result shape)
TypeScript, same outcome (
AppliedandUsesTsexist,SharedTs/tsMethoddon't, noinheritstoRoot):Expected for the repro
One node for the factory's class body (methods live there), and each application site keeps its own
constnode with its owninheritsto the base it was applied with; the two applications have different superclasses:graphify path "Widget" "ClientEntity"then walks pureinherits, and.render() -> .describe()resolves through the chain.Suggested resolution, three independent steps, all static
returnvalue, or bound byconst. Parent them to the enclosing function/file the way nested functions already are (inner()in the variant above does get a node). This alone fixes A and B and recovers the mixin methods; it can land independently of 2 and 3.extendstarget is one of the function's own parameters. Record the parameter index. JS analogue of the RubyClass.new(Super)handling already in the engine.const Y = f(Z)(or the inlineclass X extends f(Z)) wherefresolves to exactly one in-corpus mixin factory andZto exactly one class, emitinherits Y -> <factory's class>andinherits Y -> Z. Ambiguous resolution emits nothing, same single-match rule as C#dispatches_to(0.9.49) and the cross-repo type link (0.9.54). No caller node is needed, so this is independent of Calls and constant reads in top-level / script context never get a caller node, so they emit no edges (uniform across languages) #1972.Environment
graphifyy 0.9.54, Python 3.12, Windows 11. Real-world corpus: 1,581 files / 14.5k nodes; pattern is
export function createShared<Name>(Base) { return class Shared<Name> extends Base {…} }applied once each inclientjs/andserverjs/.