Skip to content
Language profiles

Language profiles

The profiles @codegraph/core ships: per entity kind, the traits an extractor must emit and the traits it may emit, plus the edge kinds the language can produce, the words the vocabulary reads of it, and the profile’s documented blind spots.

Generated content. Every table on this page is a rendering of codegraph profiles --json; run it for the machine-readable form.

codegraph profiles --json          # all eleven profiles
codegraph profiles --lang java     # one profile, as text

A profile licenses an entity by the rule required(kind) ⊆ traits ⊆ required(kind) ∪ optional(kind). What a profile is — and why it is data rather than code — is on Profiles.

Summary

langLanguageKindsEdge kindsNotes
cljClojure10513
csharpC#15821
exElixir9621
goGo1069
javaJava12817
jsJavaScript8513
phpPHP12810
pythonPython9610
rbRuby7727
rustRust13512
tsTypeScript14828

A profile that is specified but has no shipped extractor is complete, not deficient: the profile is the contract an extractor is written against. Today java, csharp, ts, ex and rb have one: see the Java, C#, TypeScript, Elixir and Ruby extractor pages. Every shipped extractor’s model can then be measured by the metrics pass.

Clojure — clj

Edge kinds: import · interfaceImplementation · invocation · access · reference

KindRequired traitsOptional traits
defmethodTInvocable, TAttachedTo, TChildOf, TSourceAnchorTComment, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
functionTNamed, TStructural, TInvocableTSourceAnchor, TComment, TChildOf, TWithChildren, TAttachedTo, TTypedEntity, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
implBlockTWithChildren, TAttachedTo, TSourceAnchorTChildOf, TComment
localBindingTNamed, TStructural, TChildOfTTypedEntity, TSourceAnchor
multimethodTNamed, TStructural, TInvocable, TChildOfTSourceAnchor, TComment, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
namespaceTNamed, TModule, TWithChildrenTSourceAnchor, TComment
parameterTNamed, TStructural, TChildOfTTypedEntity, TSourceAnchor
protocolTNamed, TType, TChildOfTWithChildren, TSourceAnchor, TComment
recordTNamed, TType, TChildOfTWithImplements, TWithChildren, TSourceAnchor, TComment
varTNamed, TStructuralTSourceAnchor, TComment, TChildOf, TWithChildren, TAttachedTo, TTypedEntity, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses

Lexicon — what the vocabulary reads of this language

  • Reserved words: def · if · do · let · quote · var · fn · loop · recur · throw · try · catch · finally · monitor-enter · monitor-exit · new · set! · nil · true · false
  • Conventional names: -main · -init · -post-init · -toString · -equals · -hashCode
  • Technical words: coll · ctx · kw · ns · opts · seq · sym · xf · xform

Notes

  • Planned extractor: clj-kondo analysis output (--config '{:output {:analysis true}}'), which resolves var definitions, usages and namespace requires without loading the code.
  • Multimethod dispatch is not statically resolvable: the dispatch fn can compute any value. Invocation edges into a multimethod carry provenance dynamic-candidate with every known defmethod of that multimethod in candidates, and to set to the best guess.
  • Macro-generated code does not exist before expansion. Definitions a macro emits are invisible to a reader-level extractor; where an extractor does expand (or where the macro is known, e.g. defrecord expanding to a class), the resulting entities and edges carry provenance generated.
  • extend-type / extend-protocol are reified as anonymous implBlock entities (METAMODEL.md §7). The block is attachedTo the extended type; the interfaceImplementation edge runs type -> protocol and uses the block as its anchor.
  • Protocol methods are namespace-level vars: they are emitted as function children of the namespace and attachedTo the protocol — the canonical containment-vs-attachment split, same shape as defmethod.
  • Protocols are open: any namespace may extend any type to any protocol at any time, so the implementer set of a protocol is never closed by the file that declares it, and a partial corpus systematically under-reports interfaceImplementation.
  • No inheritance edge kind: Clojure has no class inheritance. derive builds ad-hoc keyword hierarchies used for dispatch, which are values, not entities, and are not modeled.
  • declaredType is populated only from ^TypeHint metadata, which is optional and rare; for the vast majority of vars, parameters and bindings it is absent — this is precisely why TTypedEntity.declaredType is optional.
  • defrecord/deftype positional fields are emitted as parameter children of the record: in Clojure they are literally the positional constructor parameters.
  • Destructuring forms ({:keys [a b]}, [x & rest]) are flattened to one parameter per bound symbol, in source order; the pattern itself is not reified.
  • Higher-order use of vars ((map inc coll), (partial f x)) yields reference edges to the var, never invocation edges — the call happens elsewhere and its site is unknown.
  • Runtime resolution (resolve, requiring-resolve, eval, (var-get (ns-resolve ...))) and dynamically computed require are static blind spots: no import or invocation edge is emitted for them.
  • :refer :all and :refer bring vars into scope without qualification; the import edge is namespace -> namespace, so the module layer stays exact even when a symbol’s origin needs var resolution.

C# — csharp

Edge kinds: import · inheritance · interfaceImplementation · invocation · access · reference · annotationUse · throws

KindRequired traitsOptional traits
classTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
constructorTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTComment, TMetrics
delegateTNamed, TType, TInvocable, TWithChildren, TWithParameters, TTypedEntity, TChildOf, TSourceAnchorTComment, TMetrics
enumTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTTypedEntity, TComment, TMetrics
eventTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
fieldTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment, TWithValue
interfaceTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
lambdaTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity, TMetrics
localVariableTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
methodTNamed, TInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTAttachedTo, TComment, TMetrics
namespaceTNamed, TModule, TWithChildrenTChildOf, TComment
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor, TWithValue
propertyTNamed, TStructural, TTypedEntity, TWithChildren, TChildOf, TSourceAnchorTWithInvocations, TWithAccesses, TAttachedTo, TComment, TMetrics
recordTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
structTNamed, TType, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics

Lexicon — what the vocabulary reads of this language

  • Reserved words: abstract · as · base · bool · break · byte · case · catch · char · checked · class · const · continue · decimal · default · delegate · do · double · else · enum · event · explicit · extern · false · finally · fixed · float · for · foreach · goto · if · implicit · in · int · interface · internal · is · lock · long · namespace · new · null · object · operator · out · override · params · private · protected · public · readonly · ref · return · sbyte · sealed · short · sizeof · stackalloc · static · string · struct · switch · this · throw · true · try · typeof · uint · ulong · unchecked · unsafe · ushort · using · virtual · void · volatile · while
  • Conventional names: ToString · Equals · GetHashCode · Finalize · Main · CompareTo · Dispose · DisposeAsync · GetEnumerator · GetAsyncEnumerator · MoveNext · MoveNextAsync · Clone · Deconstruct · this[] · op_Addition · op_Subtraction · op_Multiply · op_Division · op_Modulus · op_Equality · op_Inequality · op_LessThan · op_GreaterThan · op_LessThanOrEqual · op_GreaterThanOrEqual · op_Implicit · op_Explicit · op_UnaryNegation · op_UnaryPlus · op_LogicalNot · op_OnesComplement · op_Increment · op_Decrement · op_True · op_False · op_BitwiseAnd · op_BitwiseOr · op_ExclusiveOr · op_LeftShift · op_RightShift · op_UnsignedRightShift
  • Technical words: args · async · attribute · enumerable · enumerator · extension · extensions · linq
  • Interface prefix: a leading I before Upper + lower is a marker, not a word

Notes

  • Partial classes (and partial methods) mean one entity id is declared across several files: the entity carries a single anchor for its primary declaration, while each edge carries sourceFile to say which declaration site produced it. Merging by id is correct here, not a collision.
  • var, target-typed new(), anonymous types and inferred lambda parameters leave TTypedEntity present with declaredType absent — the trait states that the entity has a type, not that the extractor resolved one.
  • Extension methods are children of their static host class but carry TAttachedTo pointing at the extended type; a call site written as instance syntax is still an invocation edge to the static method.
  • Reflection is invisible: Type.GetType, Activator.CreateInstance, expression trees and source-generated partials leave no edge unless the generated source is part of the analyzed root (then provenance is “generated”).
  • Dependency-injection wiring is invisible: container registrations (Microsoft.Extensions.DependencyInjection, Autofac, …) bind an interface to an implementation at runtime, so no invocation edge links a consumer to the concrete type it will receive.
  • Virtual dispatch and calls through an interface resolve to the DECLARED member (the one the source names); the implementation actually run is a whole-corpus question the analyzer answers. A dynamic receiver binds to no symbol at all, so the call site is dropped and counted in the stderr summary — an extractor guessing targets from the file’s using directives would be inventing edges.
  • using directives, global using and using aliases are folded to module-level import edges to the imported namespace, never to individual types.
  • Corpus membership is a whitelist of the type symbols DECLARED in the analyzed sources, never a namespace-prefix test. Roslyn resolves what it can against the BCL reference assemblies the extractor carries (so string is a stub in module System, with its real namespace); a name it cannot bind is an error type, emitted as a stub in the reserved module <unresolved> under the name as written — the honest form, since guessing a namespace from the file’s using directives would invent a fully-qualified name.
  • Generic arity is part of a type’s symbol (Repository`1, Roslyn’s metadata name): Foo, Foo<T> and Foo<T,U> legally coexist in one namespace, so Java-style erasure to the bare name would merge three declarations into one entity. Signatures use fully-qualified metadata names WITH type arguments (System.Func`2<!!0,System.String>), because C# can overload on type arguments alone (Humanize(Func<T,string>) beside Humanize(Func<T,object>), found on Humanizer); type parameters are ECMA-335 ordinals, nullable annotations and ref/out/in are dropped.
  • A corpus is not a compilation unit: two projects that never see each other may both declare a non-partial static class Extensions with the same member, and every service has its own top-level Program.cs (found on dotnet/eShop). In one compilation they are one type with several same-keyed members. Every one is kept: the first declaration in ordinal file order owns the plain key, each later one is re-keyed by its file (…#in:Catalog.API/Program.cs, a source fact like a lambda’s position), and every re-keying is named on stderr. Dropping later ones — the JDT rule the Java profile documents — would erase nine services’ DI wiring out of ten.
  • Members of a C# 14 extension block (extension(Money m) { … }, found on Humanizer) are members of the enclosing static class carrying TAttachedTo → the receiver type, exactly like classic extension methods; the block itself, a nameless nested type to Roslyn, is no entity.
  • Microsoft.NET.Sdk’s seven implicit global usings (System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading, System.Threading.Tasks — what <ImplicitUsings>enable generates into obj/, which is build output and skipped) are added to the compilation by default (--implicit-usings sdk); without them Task, List<T> and CancellationToken were the most-referenced unresolved names on OrchardCore. The Web SDK’s additions (Microsoft.AspNetCore.*, Microsoft.Extensions.*) are opt-in (web): one compilation cannot apply them per project, and on a mixed corpus they make names ambiguous — OrchardCore’s own StartupBase collided 345 times with Microsoft.AspNetCore.Hosting.StartupBase. Implicit usings write no import edge: no file wrote them. The ASP.NET Core shared framework’s reference pack is embedded beside the BCL’s when the building SDK has it.
  • An ambiguous name is unresolved: two corpus types with one simple name whose projects each global using their own namespace (dotnet/eShop’s two CatalogItem) collide once those usings merge into one compilation, and the reference lands in <unresolved> rather than on a guess between the two.
  • Two parameters may share a name ((_, _) => … discards, found on OrchardCore): the second and later same-named parameters carry their ordinal (#param:_:1).
  • A nameless invocable (lambda, anonymous method) is identified by #file:line:column; a column is a source fact, and two can start on one line. A local’s id carries line AND column for the same reason (#local:name:line:column); a local function is a method below its invocable (#fn:Name(params)).
  • Members Roslyn synthesizes and nobody wrote — a record’s Equals/GetHashCode/Deconstruct/copy constructor, backing fields, an enum’s value__ — are not entities, and neither are accessors (their bodies charge to the property). A call to such a member folds to its containing type, the rule that already governs members of stubs. The one implicit member emitted is the parameterless constructor, anchored at the type’s header line so new T() does not dangle.
  • An attribute is an annotationUse edge carrying its arguments as written values — positional ones named after the bound constructor’s parameters — and nothing inside the attribute produces an access or reference edge of its own. A method group used as a value (new Handler(Zero)) is a reference to the method, not an invocation.
  • A reference edge comes from the narrowest declared owner of the written type — a parameter’s type from the parameter, a field’s from the field — and locals never own edges: an initializer’s call is the invocable’s dependency, and the local’s own type rides on declaredType.
  • Primitives resolve: int IS System.Int32, so unlike Java they become BCL stubs rather than being omitted. void, dynamic, anonymous types, pointer types and type parameters are not entities and leave declaredType absent.
  • Neither a .sln nor a .csproj is read: the extractor parses every *.cs under the source roots into ONE compilation (no MSBuild, no restore), so a legacy tree with no restorable project graph still extracts, and a missing package is a stub rather than a build failure. Consequences: one preprocessor configuration is parsed (no --define yet); source-generated and XAML/Razor code-behind partials are absent unless their output is under the roots; a name declared twice across projects that never see each other collides as one entity.
  • The metrics pass (codegraph-metrics, PLAN.md §18): a property with accessors is joined to its accessor spaces — { get; set; } is two, so it carries the pass’s charged keys only (cognitive, exits, ABC). An implicit constructor (anchored on the type’s name) and a record’s positional members have no space and carry none of its keys. tree-sitter-c-sharp 0.23.5 misparses a C# 14 extension block: its members go unmeasured by the pass, and the enclosing class is joined by containment (charged keys only). tree-sitter does not evaluate #if: in a file with conditional blocks big-code-analysis measures EVERY branch, where Roslyn — and so this extractor’s sloc — keeps the active one; on Humanizer such files are the 37 exact joins whose sloc and big-code-analysis’s ploc disagree.

Elixir — ex

Edge kinds: import · interfaceImplementation · invocation · access · reference · throws

KindRequired traitsOptional traits
attributeTNamed, TStructural, TChildOf, TSourceAnchorTWithValue, TWithAccesses, TWithInvocations, TComment
callbackTNamed, TInvocable, TWithChildren, TWithParameters, TChildOf, TSourceAnchorTComment
fieldTNamed, TStructural, TChildOf, TSourceAnchorTWithValue, TComment
fileTNamed, TModule, TWithChildrenTSourceAnchor, TComment, TMetrics, TWithInvocations, TWithAccesses
functionTNamed, TInvocable, TWithChildren, TWithParameters, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTWithLocalVariables, TComment, TMetrics
macroTNamed, TInvocable, TWithChildren, TWithParameters, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTWithLocalVariables, TComment, TMetrics
moduleTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTWithImplements, TAttachedTo, TComment, TMetrics, TWithInvocations, TWithAccesses
parameterTStructural, TChildOfTNamed, TSourceAnchor, TWithValue
protocolTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics

Lexicon — what the vocabulary reads of this language

  • Reserved words: true · false · nil · when · and · or · not · in · fn · do · end · catch · rescue · after · else
  • Conventional names: __using__ · __before_compile__ · __after_compile__ · __after_verify__ · __on_definition__ · __struct__ · __info__ · __deriving__ · __impl__ · __protocol__ · start_link · child_spec · init · handle_call · handle_cast · handle_info · handle_continue · terminate · code_change · format_status · to_string · inspect · @moduledoc · @doc · @typedoc · @spec · @type · @typep · @opaque · @callback · @macrocallback · @optional_callbacks · @impl · @behaviour · @derive · @enforce_keys · @external_resource · @compile · @deprecated · @dialyzer · @on_load · @vsn · @before_compile · @after_compile · @after_verify · @on_definition · @nifs · @fallback_to_any · @file
  • Technical words: attrs · behaviour · callback · changeset · conn · ecto · ets · gen · live · mix · opts · otp · phoenix · pid · plug · repo · schema · server · socket · sup · supervisor · web
  • File kinds: file

Notes

  • The module is the file (the TypeScript rule): TModule.definedIn has exactly one entry, and every entity’s module is the file it is written in. A defmodule is a module entity, child of its file, carrying TType — the module IS the type in Elixir (a struct is %Mod{}, a behaviour and a protocol are modules, @spec f :: Mod.t()). Nesting (defmodule Inner inside defmodule Outer) defines Outer.Inner as a second child of the file and aliases Inner inside Outer: it is an alias, not a containment.
  • Key components are percent-encoded: /, # and % in every path segment and name, and . inside a module atom (Acme%2EOrder), because . is the nesting separator inside a symbol (Acme%2EOrder.total). Relying on the rule that alias segments start uppercase while function names do not would make raw dots injective for defmodule A.B and fail for defmodule :"a.b", which is legal; the encoding is injective by construction, and the entity’s name is the atom as written.
  • Arity is identity: f/1 and f/2 are unrelated functions, so a function, macro or callback always carries its arity as the disambiguator (total#1). def f(a, b \\ 1) declares f/1 and f/2 and is ONE entity, f#2, to which a call f(x) resolves; every clause of one f/2 is folded into that entity, anchored from the first clause’s line to the last clause’s end. signature is name/arity.
  • No inheritance edge kind and no embedding: Elixir has neither class inheritance nor embedding. interfaceImplementation is emitted declared for @behaviour B and for defimpl P, for: T (an edge T -> P anchored at the block), and generated for @derive P, whose expansion the language itself defines (Protocol.derive/3).
  • defimpl P, for: T is a named module on the BEAM (P.T, the compiler’s own name): it is a module entity keyed by that name in the file where the block is written, attachedTo T — the containment-versus-attachment split of the Clojure implBlock, with the name Elixir gives it. defimpl written inside defmodule T with no for: implements for T.
  • Corpus membership is a whitelist of the defmodules under the roots, never a name-prefix test. Every other module is a stub module entity below one of two reserved stub modules: <otp> for a module the BEAM ships (Elixir’s standard library and Erlang/OTP, decided by a module and export table embedded in the extractor at build time from the building toolchain — Enum and :ets side by side, one VM; the built-in protocol targets Any, BitString, Function, PID, Port, Reference, Tuple included), and <deps> for a module the roots do not declare and the table does not know (a dependency, whichever one: with no deps/ read, the model cannot say which). Keys therefore never depend on what is checked out beside the corpus. A stub has no members (the C# fold): a call to Enum.map/2 is an invocation of the stub module <otp>/Enum, and a field of an external struct is never an entity. An alias of a namespace PREFIX no module declares (alias Plausible.Stats.SQL then SQL.Expression) is dropped and counted as prefix_alias, never stubbed.
  • alias, import, require and use are one import edge kind, file -> the target: another corpus file when the target is declared under the roots, else the stub module below <otp> or <deps>. Folded to the module layer by the analyzer, an external import reads as a dependency on the reserved module — the honest statement that the model does not know which dependency provides it. The four forms are counted separately on stderr; the edge itself does not say which was written. A form written inside a function body applies to that body alone and is still an edge from the file.
  • use X is defined by the language as require X followed by X.__using__(opts) at compile time: it yields the import edge AND a declared invocation of X.__using__#1. What __using__ injects — a @behaviour, imports, definitions — does not exist before expansion: it is absent from the baseline and every local call it would have bound is dropped and counted as local-unbound on stderr (a Phoenix router’s routes, an Ecto schema’s fields, a plug pipeline). The --trace enrichment recovers those as generated.
  • A local call f(a, b) resolves in this order: a def/defp/defmacro of the enclosing module with that name and arity (defaults folded); an explicit import M that exports it (a corpus M, the OTP table, or the --deps exports), most recent import first; Kernel / Kernel.SpecialForms unless import Kernel, except: removed it — and a Kernel binding is the LANGUAGE, resolved but never an edge (length/1, +/2, the to_string/1 of a string interpolation), counted as kernel on stderr; the SOLE foreign import M whose exports are unknown (a stub below <deps>, counted) — unless the module uses a foreign module whose __using__ may have injected the name, when nothing honest can be said and the site is dropped as local_injected; two or more candidates: ambiguous_import; none: local_unbound. A remote call M.f/n on a corpus M that declares no f/n (an injected definition, a typo) is dropped as remote_unbound.
  • Dynamic dispatch is reported, never guessed. A call to a corpus protocol function P.f(x) is an invocation with provenance dynamic-candidate whose candidates are every corpus defimpl of P’s f; GenServer.call(Server, msg) / GenServer.cast(Server, msg) with Server a statically named corpus module (__MODULE__ or an alias) is, beside the invocation of <otp>/GenServer, a dynamic-candidate invocation of that module’s clause-folded handle_call/3 / handle_cast/2 — the one message-passing form that is statically honest, because the server is named in the source; a server the module does not define a handler for is dropped as handler_unbound. Any other first argument (a pid, a registered name, a variable), apply/3, mod.f() through a variable, Module.concat, Code.eval_* and send/2 are dropped and counted as dynamic-dispatch.
  • Higher-order use (&M.f/2, &f/2, &M.f(&1, x)) yields a reference edge to the function, never an invocation: the call happens elsewhere. A module atom in argument position (children = [MyWorker, {Registry, keys: :unique}]) is a reference to the module — how a supervision tree surfaces. %M{…} in an expression or a pattern is a reference to M (struct expansion) and each named key an access to M.<key> (isRead in a pattern, isWrite in a construction or update) — the only static field access Elixir has; s.name is a runtime map access, dropped and counted. @attr read in a body is an access to the attribute.
  • raise M, raise M, msg and reraise M, … are throws edges to M (RuntimeError below <otp> for raise "text"); throw and exit are not exceptions and yield nothing. @spec, @type, @typep and @opaque yield reference edges to every named remote type’s module; @type is not an entity. @moduledoc and @doc are the entity’s comments, as is the # comment block written directly above a definition.
  • A defdelegate f(a), to: M, as: :g is a function with one declared invocation of M.g#1: the delegation is written. defp and def are one kind — the metamodel has no visibility — and private: true rides as a pass-through key the container contract tolerates; defaults: n says how many lower arities a default-carrying head also declares.
  • A module body is compile-time code: use X, plug :x, schema "t" do field … end, an if Mix.env() == :test do def … end — its calls and reads are edges FROM the module entity (hence the optional TWithInvocations/TWithAccesses on the kind), the calls injected macros would bind being counted rather than guessed. A module attribute’s value owns the edges it writes (the narrowest declared owner): @table :ets.new(…) invokes from the attribute, @names [Acme.A, Acme.B] references from it, @limit @max * 2 reads from it. A reserved attribute (@moduledoc, @doc, @spec, @impl, @enforce_keys, …) is metadata, never an entity; a redefined attribute keeps its first definition (accumulating attributes are written many times) and the rest are counted.
  • Locals are deferred: TWithLocalVariables is licensed on invocables and not emitted in M15. Every match binds, so a local per = would multiply entities by ten for no edge the analyzer reads today; the trait stays licensed so adding them needs no profile change.
  • Blind spots, stated: definitions a used macro injects and the locals that call them; callback implementations as edges (handle_call/3 under @behaviour GenServer — the analyzer recovers “implements callback” from interfaceImplementation plus name/arity, as it does for Java overrides); message passing beyond the __MODULE__ form; process topology at runtime (Registry, :via tuples, dynamic supervisors); .erl files in a mixed corpus (an Erlang profile is its own phase). A file Code.string_to_quoted/2 rejects (a syntax error, invalid UTF-8) is skipped and counted as unparsed; its file record is still written, with no entities.
  • The --trace enrichment: mix codegraph.trace recompiles a project that COMPILES under a compilation tracer and writes every call the compiler resolved after macro expansion (remote, local, imported, macros, struct expansions) with its site and caller; the extractor merges the file after its own pass. An event the baseline already wrote at the same site is the same fact; a new one is an edge with provenance generated, resolved through the same rules (an external callee lands on its stub module); a call to a function the module does not declare in source (__struct__/1, an injected greet/1) has no entity to land on and is counted as injected, never invented; a Kernel binding is kernel. Keys never come from the trace — a model with and without it has the same entities, and every declared edge stays as it was. Phoenix (205 files): 16 797 events, 5 895 generated edges added to 14 314 declared ones.
  • Kernel.SpecialForms are variadic (for, try, with, case take any number of arguments) and are matched by name alone; x :: size(8) inside a binary is a specifier, not a call. A repeated name in one head (def f(mode, mode) pins the second) names one position only; the other is keyed by ordinal.
  • Audit (M15c, every model validate-clean and byte-identical to core’s encoder, no deps/ read): elixir-lang/elixir lib (557 files, 27 061 entities / 78 564 edges, 17 s) resolves 80.8 % of 164 123 sites (52 140 Kernel), the rest counted — 30 629 local_injected (its own tests’ assert/test under use ExUnit.Case, use MixTest.Case), 2 659 map accesses, 736 dynamic dispatches, 460 remote calls to injected definitions; phoenixframework/phoenix (205 files, 5 828 / 14 314, 2.3 s) 76.7 % (6 616 injected, 1 900 map accesses); plausible/analytics (1 256 files, 16 369 / 43 661, 5 s) 58.2 % (46 793 injected — use PlausibleWeb, :controller, Ecto schemas, ExUnit; 14 401 map accesses). The injected share IS the macro ceiling the trace exists for: on Phoenix, mix xref graph (148 file-level compile and runtime edges) is witnessed 147 times by the baseline alone (the miss is a module reached only through Code.ensure_loaded?), and the trace adds 5 895 generated edges.
  • Measures (TMetrics, METAMODEL.md §3.8): sloc on the file, every module and every invocable — lines of its own span that are neither blank nor comment-only, a comment being what the compiler’s own parser reports as one: every non-blank line a heredoc, a sigil or a multi-line string spans is code, a # inside one included; cyclomatic on invocables: 1 + clause heads beyond the first + case/cond/with/receive/try clauses beyond the first + if/unless + and/or/&&/|| + rescue/catch clauses. A nested fn contributes to its enclosing invocable: an anonymous function is not an entity.
  • The metrics pass (codegraph-metrics, PLAN.md §18): big-code-analysis opens one space per def clause, so a function of several clauses is joined by containment and carries the charged keys only (cognitive, exits, ABC) — no lloc, Halstead or mi, which do not add up over clauses. defprotocol and defimpl bodies, defguard and defdelegate have no space and carry none of the pass’s keys. The join’s witness (sloc = big-code-analysis’s ploc) cannot hold over a heredoc: big-code-analysis leaves a heredoc’s text lines out of ploc, which METAMODEL’s sloc counts (Phoenix’s mix_app_path: sloc 19, ploc 13).

Go — go

Edge kinds: import · interfaceImplementation · invocation · access · reference · embedding

KindRequired traitsOptional traits
constTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
fieldTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
funcTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
interfaceTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTWithImplements, TComment
localVariableTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
methodTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TAttachedTo, TSourceAnchorTComment
packageTNamed, TModule, TWithChildrenTComment
parameterTStructural, TTypedEntity, TChildOfTNamed, TSourceAnchor
structTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTWithImplements, TComment
typeAliasTNamed, TType, TTypedEntity, TChildOf, TSourceAnchorTComment

Lexicon — what the vocabulary reads of this language

  • Reserved words: break · case · chan · const · continue · default · defer · else · fallthrough · for · func · go · goto · if · import · interface · map · package · range · return · select · struct · switch · type · var
  • Conventional names: init · main · String · GoString · Error · Unwrap · Is · As · Len · Less · Swap · MarshalJSON · UnmarshalJSON · MarshalText · UnmarshalText · ServeHTTP
  • Technical words: cfg · cmd · ctx · err · errs · internal · mu · mutex · pkg · srv · svc · wg

Notes

  • Go has no inheritance: neither the TWithInheritances trait nor the inheritance edge kind belongs to this profile. Their absence is the profile’s statement about the language, not a missing feature.
  • Interface satisfaction is implicit and structural: nothing in the source says a type implements an interface, so interfaceImplementation edges are always emitted with provenance “derived” and never “declared”. An analysis restricted to facts (declared edges) sees no implementation relation in Go at all.
  • struct { Base } and interface { io.Reader } are embedding edges, not inheritance and not fields: the embedded type’s methods are promoted onto the outer type, which changes which interfaces the outer type satisfies but creates no subtype relation.
  • A method’s receiver drives TAttachedTo (the receiver type) while TChildOf stays the package where the method is written; value and pointer receivers attach to the same type entity and are separated by the id’s disambiguator.
  • Multiple return values do not fit TTypedEntity’s single declaredType: the first result is the declaredType and every further result type is emitted as a reference edge.
  • Short variable declarations (:=), untyped constants and iota runs leave TTypedEntity present with declaredType absent.
  • Calls through an interface value cannot be resolved statically: they are emitted against the interface method with provenance “dynamic-candidate” and the satisfying implementations as candidates.
  • Invisible to static extraction: reflect, the plugin package, cgo, go:generate and go:linkname directives, and side effects of init() functions pulled in by blank imports (import _ "…"), which are still emitted as ordinary import edges.
  • Build tags and GOOS/GOARCH constrained files mean a single extraction run sees one build configuration; entities excluded by the active tags are absent from the model.

Java — java

Edge kinds: import · inheritance · interfaceImplementation · invocation · access · reference · annotationUse · throws

KindRequired traitsOptional traits
annotationTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
attributeTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment, TWithValue
classTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
constructorTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTComment, TMetrics
enumTNamed, TType, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
interfaceTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
lambdaTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity, TMetrics
localVariableTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
methodTNamed, TInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment, TMetrics, TWithValue
packageTNamed, TModule, TWithChildrenTComment, TChildOf
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
recordTNamed, TType, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics

Lexicon — what the vocabulary reads of this language

  • Reserved words: abstract · assert · boolean · break · byte · case · catch · char · class · const · continue · default · do · double · else · enum · extends · final · finally · float · for · goto · if · implements · import · instanceof · int · interface · long · native · new · package · private · protected · public · return · short · static · strictfp · super · switch · synchronized · this · throw · throws · transient · try · void · volatile · while · true · false · null
  • Conventional names: toString · equals · hashCode · clone · finalize · main · compareTo · iterator · hasNext · valueOf · serialVersionUID · readObject · writeObject · readObjectNoData · readResolve · writeReplace
  • Technical words: annotation · bean · beans · ejb · jaxb · jdbc · jndi · jpa · pojo · servlet

Notes

  • Measures (TMetrics, METAMODEL.md §3.8): sloc on every type and invocable — lines of its own span that are neither blank nor comment-only, counted by a scanner that knows string and text-block literals, so a "/*" in the source does not swallow the rest of the file. cyclomatic on invocables only: 1 + if / for / foreach / while / do / non-default case label / catch / ternary / short-circuit && and || / switch pattern guard. Purely syntactic, hence immune to the noClasspath resolution ceiling. A nested lambda or anonymous class does NOT contribute to its enclosing method: it is its own invocable and carries its own count. A type’s complexity is therefore not stored — it is the sum over its members, which the consumer computes.
  • A corpus is not a compilation unit. Spoon compiles every source root as a single JDT batch, so two Maven/Gradle modules that each declare the same fully-qualified name — legal, since neither module sees the other — collide as IProblem.DuplicateTypes. The extractor tolerates the collision rather than aborting (measured: one collision in a test helper aborted the extraction of a 4,972-file repository), but JDT keeps only the FIRST declaration it reached: the later file contributes no entity and no edge. Every dropped declaration is named on stderr, so a model of a multi-module corpus can legitimately be missing a type its sources contain.
  • Spoon in noClasspath mode invents plausible fully-qualified names for unresolved types. Corpus membership is therefore decided by a whitelist of ids actually declared by the corpus (built in a first pass); anything else is emitted as an isStub entity. Never decide membership by package or name prefix — invented FQNs look exactly like real ones and a prefix filter would launder them into facts.
  • Reflection is invisible: Class.forName, Method.invoke, proxies, Spring XML/annotation wiring, ServiceLoader/META-INF/services, and JNDI lookups produce no edge. The import/invocation graph of a reflection-heavy corpus is a lower bound.
  • Lombok-generated members (getters, setters, @Builder, @Data constructors) are emitted with provenance “generated” when the expansion is visible to Spoon, and are missing entirely when it is not.
  • An interface’s extends list is emitted as inheritance edges between types; interfaceImplementation is reserved for a class/enum/record implements clause, always with provenance “declared”.
  • A throws edge is emitted per written throw statement whose static exception type resolves, anchored at the throw site — the evidence a guard clause (if (x) throw new E(...)) leaves in the model. A method’s throws CLAUSE stays a plain reference edge: it declares propagation, not a failure exit of this body. A rethrow (throw e;) targets the caught variable’s static type; a throw whose type Spoon cannot name is dropped and counted, like any unidentifiable target.
  • Static and on-demand (import x.y.*) imports are folded to module-level import edges; the wildcard case names the package, not the individual types it brings in.
  • Overloads are distinguished by the id’s signature disambiguator, so an unresolved parameter type changes the id — a resolution failure shows up as a stub target, never as a merged entity.
  • Java generics are erased in the model: type arguments are emitted as reference edges from the declaring entity, and declaredType carries the raw type.
  • noClasspath resolution rate, measured (M2 audit): 100.0% on apache/commons-lang (263 files, 133478 type references, 0 unresolved) and 77.8% on spring-petclinic (30 files, 2189 references, 487 unresolved, all of them Spring and Jakarta types whose jars are absent). The rate is a property of the corpus’s DEPENDENCY SURFACE, not of the extractor: commons-lang is self-contained and depends on nothing but the JDK, which resolves against the runner’s own classpath. A jar that is not on the classpath cannot be resolved by any extractor, so a corpus with third-party dependencies has a structural ceiling well below 100% and a single cross-corpus target number is not meaningful. The stub discipline, not the resolution rate, is the property worth asserting.
  • The resolution rate counts neither type variables nor <nulltype>: neither names anything that could have a declaration, so counting them measures the corpus’s writing style. This is not cosmetic — <nulltype>, Spoon’s static type for the null literal, was ALL 1466 of commons-lang’s originally-reported unresolved references, making the headline number a function of how many return null; statements the corpus contains.
  • In noClasspath Spoon promotes an unresolvable RECEIVER to a type in the enclosing package: typeHint -> typeHint.with(...) and cm.setStatisticsEnabled(...) produced stub classes named typeHint and cm inside the corpus’s own package on spring-petclinic. They are correctly stubbed (membership is the declared-id whitelist), and they are why the whitelist exists — but the stub set of a real corpus therefore contains a tail of entities named after local variables, and a stub count is not a count of external types.
  • An array type is not an entity, and neither is its component where a member is concerned. xs.length and int[]::new declare their member on int[] / Money[]; folding that up names the component and states a fact the source never wrote. Such facts are dropped, not degraded — the dependency on the component is already carried by the written type reference that mentions it.
  • An anonymous class has exactly one id, the #file:line:column form pass 1 declared. Spoon names it Outer$N, which renders as a plausible nested-type id in the corpus’s own package; a reference resolved through that name gives one declared class two ids and launders the second into a stub. Type references must be resolved to their declaration before being named.
  • Spoon materializes the implicit constructor of an anonymous class with synthetic parameters named $anonymousN. They are emitted (implicit members are, deliberately, so that new Foo() does not dangle), so the model contains a handful of parameter entities nobody wrote, carrying no anchor of their own — 5 of 15338 entities on commons-lang.
  • The metrics pass (codegraph-metrics, PLAN.md §18) joins big-code-analysis spaces to entities on their anchors. Spoon starts a declaration AFTER its annotations and tree-sitter AT them; the pass bridges the two when nothing but annotation lines lies between (a LEADING join — most of a Spring corpus). It measures nothing on a lambda (big-code-analysis opens no space for one and charges its cognitive, exits and ABC to the enclosing method), on an implicit constructor (anchored on its class) or on an annotation element (no body): those carry none of the pass’s keys.

JavaScript — js

Edge kinds: import · inheritance · invocation · access · reference

KindRequired traitsOptional traits
arrowFunctionTInvocable, TSourceAnchorTComment, TChildOf, TWithChildren, TTypedEntity, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
classTNamed, TType, TChildOfTWithInheritances, TWithChildren, TSourceAnchor, TComment
functionTInvocableTNamed, TSourceAnchor, TComment, TChildOf, TWithChildren, TTypedEntity, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
methodTNamed, TInvocable, TChildOfTSourceAnchor, TComment, TWithChildren, TTypedEntity, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses
moduleTNamed, TModule, TWithChildrenTSourceAnchor, TComment
parameterTNamed, TStructural, TChildOfTTypedEntity, TSourceAnchor
propertyTNamed, TStructural, TChildOfTTypedEntity, TSourceAnchor, TComment
variableTNamed, TStructuralTSourceAnchor, TComment, TChildOf, TWithChildren, TTypedEntity

Lexicon — what the vocabulary reads of this language

  • Reserved words: await · break · case · catch · class · const · continue · debugger · default · delete · do · else · enum · export · extends · false · finally · for · function · if · import · in · instanceof · new · null · return · super · switch · this · throw · true · try · typeof · var · void · while · with · yield · implements · interface · let · package · private · protected · public · static
  • Conventional names: constructor · toString · toLocaleString · valueOf · toJSON · [Symbol.iterator] · [Symbol.asyncIterator] · [Symbol.hasInstance] · [Symbol.toPrimitive] · [Symbol.toStringTag] · [Symbol.dispose] · [Symbol.asyncDispose]
  • Technical words: args · async · callback · cb · fn · json · opts · promise · prototype
  • File kinds: module

Notes

  • A module is a file: TModule.definedIn always has exactly one entry (1-1 cardinality, unlike a Java package).
  • No interfaceImplementation edge kind. JS has no implements clause and conformance is duck-typed, so it is never observable statically — the absence of the edge kind is the honest statement, not a gap to fill later with guesses.
  • TTypedEntity is licensed on variables, parameters, properties and function returns, but declaredType is essentially always absent: JS annotates nothing. This is exactly why the attribute is optional rather than required by the trait. An extractor may populate it from JSDoc @type/@param/@returns when present; everything else is left absent rather than inferred.
  • Arrow functions and anonymous function expressions carry TInvocable without TNamed; their id disambiguator is (file, startLine), so they require TSourceAnchor.
  • const f = () => {} yields two entities: the variable f and its child arrowFunction. Call sites resolve through the variable to the arrow, and invocation edges target the arrowFunction.
  • Dynamic import(expr) and require(expr) with a computed, template-literal or variable path are unresolvable: no import edge is emitted. Only statically literal specifiers produce edges.
  • CommonJS/ESM interop is a blind spot: module.exports = X, exports.a = ..., __esModule interop shims and conditional package.json exports mean the same physical file can be reached under several specifiers. Ids are keyed on the resolved file path, so a resolver failure produces a stub module rather than a wrong merge.
  • Monkey patching (Obj.prototype.m = fn, Object.assign(target, mixin), Object.defineProperty) adds members at runtime. No method or property entity is created for them; the assignment surfaces only as access and reference edges at the patch site.
  • Duck typing means a call x.m() on an unannotated receiver has no resolvable target. Such invocations are either omitted or emitted with provenance dynamic-candidate and every same-named method in the corpus listed in candidates.
  • Computed member access o[k] and dynamic property names give access edges whose target is unknown; they are omitted rather than guessed at a single field.
  • this rebinding (call/apply/bind, arrow lexical this, extracted methods) breaks receiver-based resolution; edges through a rebound this are not reconstructed.
  • Getters/setters are emitted as method entities; reads and writes of the underlying value at call sites are access edges, so a property read that runs code is visible as access, not invocation.
  • Class fields and object literal members share the property kind; object literals used as namespaces therefore appear as a variable with property children.

PHP — php

Edge kinds: import · inheritance · interfaceImplementation · invocation · access · reference · traitUsage · fileInclude

KindRequired traitsOptional traits
classTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment
closureTInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity
codeFileTNamedTWithChildren, TSourceAnchor, TComment
constantTNamed, TStructural, TChildOf, TSourceAnchorTTypedEntity, TComment
enumTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTWithImplements, TTypedEntity, TComment
functionTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
interfaceTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTComment
methodTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
namespaceTNamed, TModule, TWithChildrenTChildOf, TComment
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
propertyTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
traitTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTComment

Lexicon — what the vocabulary reads of this language

  • Reserved words: __halt_compiler · abstract · and · array · as · break · callable · case · catch · class · clone · const · continue · declare · default · die · do · echo · else · elseif · empty · enddeclare · endfor · endforeach · endif · endswitch · endwhile · eval · exit · extends · final · finally · fn · for · foreach · function · global · goto · if · implements · include · include_once · instanceof · insteadof · interface · isset · list · match · namespace · new · or · print · private · protected · public · readonly · require · require_once · return · static · switch · throw · trait · try · unset · use · var · while · xor · yield · __CLASS__ · __DIR__ · __FILE__ · __FUNCTION__ · __LINE__ · __METHOD__ · __NAMESPACE__ · __PROPERTY__ · __TRAIT__ · true · false · null
  • Conventional names: __construct · __destruct · __call · __callStatic · __get · __set · __isset · __unset · __sleep · __wakeup · __serialize · __unserialize · __toString · __invoke · __set_state · __clone · __debugInfo
  • Technical words: arr · closure · facade · middleware · php · str
  • File kinds: codeFile

Notes

  • fileInclude (include/require/_once) is the metamodel’s only file-to-file dependency; its endpoints are codeFile entities. A file both declares entities and includes other files, so the same file appears as a codeFile node and as the anchor.file of the declarations it contains.
  • include/require paths built from variables, concatenation, constants or DIR arithmetic are invisible: only literal paths produce a fileInclude edge.
  • use TraitX; is a traitUsage edge Type -> Trait and the trait’s members are NEVER flattened into the using class. Conflict resolution (insteadof, as, visibility changes) and abstract trait members are not represented; a resolver must read the trait’s own children.
  • use Foo\Bar; statements are per-file aliases, not namespace-level facts: emit them as import edges and set sourceFile to the declaring file so several files sharing a namespace stay distinguishable.
  • __call/__callStatic/__get/__set/__invoke are dynamic dispatch: invocation and access edges through magic methods carry provenance ‘dynamic-candidate’ with a candidates list, or are absent when no candidate can be named.
  • Variable variables ($$name), variable functions ($fn()), call_user_func, new $class and string class names resolve only for literal arguments; everything else is invisible.
  • eval(), conditional declarations inside if/function bodies, and class_alias() are not modelled.
  • Single inheritance for classes (at most one inheritance edge); interfaces may extend several, so N edges are legal there.
  • Gradual typing: parameter, property and return type declarations are optional, so TTypedEntity.declaredType is often absent. Docblock types (@var, @param) are not authoritative and must not be promoted to declaredType.
  • Namespaces are 1-N with files: TModule.definedIn lists every file declaring into the namespace. A file with no namespace statement declares into the global namespace.

Python — python

Edge kinds: import · inheritance · interfaceImplementation · invocation · access · reference

KindRequired traitsOptional traits
attributeTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
classTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTWithImplements, TWithInvocations, TWithAccesses, TComment
decoratorTAttachedTo, TWithInvocations, TSourceAnchorTNamed, TChildOf, TComment
functionTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
lambdaTInvocable, TWithParameters, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity
methodTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTAttachedTo, TComment
moduleTNamed, TModule, TWithChildrenTChildOf, TSourceAnchor, TComment
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
variableTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor, TComment

Lexicon — what the vocabulary reads of this language

  • Reserved words: False · None · True · and · as · assert · async · await · break · class · continue · def · del · elif · else · except · finally · for · from · global · if · import · in · is · lambda · nonlocal · not · or · pass · raise · return · try · while · with · yield
  • Conventional names: self · cls · __init__ · __new__ · __del__ · __repr__ · __str__ · __bytes__ · __format__ · __hash__ · __bool__ · __eq__ · __ne__ · __lt__ · __le__ · __gt__ · __ge__ · __len__ · __iter__ · __next__ · __aiter__ · __anext__ · __reversed__ · __contains__ · __getitem__ · __setitem__ · __delitem__ · __missing__ · __getattr__ · __getattribute__ · __setattr__ · __delattr__ · __dir__ · __get__ · __set__ · __delete__ · __set_name__ · __call__ · __enter__ · __exit__ · __aenter__ · __aexit__ · __await__ · __init_subclass__ · __class_getitem__ · __post_init__ · __add__ · __sub__ · __mul__ · __matmul__ · __truediv__ · __floordiv__ · __mod__ · __pow__ · __neg__ · __pos__ · __abs__ · __invert__ · __and__ · __or__ · __xor__ · __lshift__ · __rshift__ · __int__ · __float__ · __index__ · __round__ · __copy__ · __deepcopy__ · __reduce__ · __getstate__ · __setstate__ · __slots__ · __all__ · __name__ · __main__ · __doc__ · __dict__
  • Technical words: args · dict · kwargs · metaclass · mixin · py · str · tuple

Notes

  • A module is a file: TModule.definedIn holds exactly one path (1-1 cardinality). A package is the module of its init.py.
  • Multiple inheritance emits N inheritance edges from the class. MRO (C3 linearization) order is NOT represented: the edge set is unordered, so method-resolution questions cannot be answered from the model.
  • Protocols (PEP 544) are structural, never declared: conformance yields interfaceImplementation edges with provenance ‘derived’. Explicit class X(Protocol) subclassing is a plain declared inheritance edge instead.
  • Decorators that synthesize members (dataclasses, attrs, ORM/metaclass bases) emit the members and their edges with provenance ‘generated’; a pre-expansion extraction sees none of them.
  • Duck typing: a call whose receiver type is not statically known resolves to provenance ‘dynamic-candidate’ with a candidates list, or is absent entirely.
  • Monkey patching (assigning functions or attributes onto classes and modules at runtime) is invisible to static extraction; the model shows the original definition site only.
  • getattr/setattr/hasattr and importlib.import_module/import resolve only for literal string arguments; computed names produce no edge.
  • Type hints are optional and may be deferred strings (PEP 563 / from __future__ import annotations), so TTypedEntity.declaredType is frequently absent even where the trait is present.
  • from x import * yields a module-level import edge only; the individual names it binds cannot be attributed.
  • Conditional and if TYPE_CHECKING: imports are emitted like any other import — this profile declares no Space, so type-only dependencies are indistinguishable from runtime ones.

Ruby — rb

Edge kinds: import · inheritance · traitUsage · invocation · access · reference · throws

KindRequired traitsOptional traits
classTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTAttachedTo, TComment, TMetrics, TWithInvocations, TWithAccesses
constantTNamed, TStructural, TChildOf, TSourceAnchorTWithValue, TComment, TWithInvocations, TWithAccesses
fieldTNamed, TStructural, TChildOf, TSourceAnchorTWithValue, TComment
fileTNamed, TModule, TWithChildrenTSourceAnchor, TComment, TMetrics, TWithInvocations, TWithAccesses
methodTNamed, TInvocable, TWithChildren, TWithParameters, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTWithLocalVariables, TComment, TMetrics
moduleTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTAttachedTo, TComment, TMetrics, TWithInvocations, TWithAccesses
parameterTStructural, TChildOfTNamed, TSourceAnchor, TWithValue

Lexicon — what the vocabulary reads of this language

  • Reserved words: __ENCODING__ · __LINE__ · __FILE__ · BEGIN · END · alias · and · begin · break · case · class · def · defined? · do · else · elsif · end · ensure · false · for · if · in · module · next · nil · not · or · redo · rescue · retry · return · self · super · then · true · undef · unless · until · when · while · yield
  • Conventional names: initialize · initialize_copy · to_s · to_str · to_a · to_ary · to_h · to_hash · to_proc · inspect · hash · eql? · == · === · =~ · !~ · != · <=> · < · > · <= · >= · + · - · * · / · % · ** · << · >> · & · | · ^ · ~ · ! · +@ · -@ · [] · []= · each · coerce · method_missing · respond_to_missing? · respond_to? · included · extended · inherited · prepended · method_added · const_missing
  • Technical words: accessor · attr · concern · concerns · ext · gem · gemspec · klass · lambda · mixin · proc · rake · rakefile
  • File kinds: file

Notes

  • The module is the file (the TypeScript and Elixir rule): TModule.definedIn has exactly one entry, and every entity’s module is the file it is written in. class and module are types below it, named and keyed by their fully qualified constant path (Acme::Order) — :: is part of the name, never a separator of the key. A type written inside the body of a type that is an entity in the same file is its child; otherwise it is a child of the file. A value constant is keyed the same way (Acme::Order::MAX_LINES), a child of the type whose body assigns it.
  • Ruby classes are open, so a class or a module is ONE ENTITY PER DECLARING FILE (the TypeScript declaration-merging rule): every body of Acme::Order in one file folds into one entity anchored at the first, and a second file reopening it declares a second entity with the same name. A reference to the constant targets the PRIMARY declaration, chosen deterministically — the one that writes a superclass, else the first in canonical file order — and every other declaration is attachedTo it: containment says where a body is written, attachment which type it belongs to. A method is looked up across every body, and lands on the one in the calling file when there is one.
  • A monkey patch is a reopening by construction: the VM defined String before any file ran, so a corpus class String cannot be its definition. It is a declared entity in its file, attachedTo the stub <ruby>/String, and a reference to String anywhere still names the VM’s — the patch file does not become the hub every string in the corpus points at. A method the patch adds IS found, before the VM’s own ("".blank? lands on the corpus String.blank?). The one corpus this misreads is a library that itself implements a constant of the core table (rubygems’ own Gem): its references land on the stub. A library’s class the corpus reopens (class ActiveRecord::Base) has no such table to consult and IS the declaration the corpus knows.
  • A wrapper body is not a declaration: module Acme; class Order; end; end and class Acme::Order; end are two spellings of one thing, and both yield Acme::Order alone. A module or class body with no superclass whose statements are all class or module declarations — Point = Struct.new(…) being one — declares nothing of its own in that file: it is lexical scope, Ruby’s spelling of a package statement, so a namespace reopened in fifty files is not fifty entities. An empty body (class NotFound < StandardError; end, module Marker; end) IS a declaration. A constant path no file gives a body to has no entity, and a reference to it is dropped and counted (namespace-only) — UNLESS the corpus itself proves the body exists elsewhere: a message is sent to it that no bare module answers (Rails.env, where the corpus only writes class Rails::Patch), or a class inherits from a path nothing in the corpus opens. That namespace is a library’s, a stub below <gems>; what the corpus declares under it stays the corpus’s.
  • Point = Struct.new(:x, :y) do … end, Coord = Data.define(:lat), Handler = Class.new(Base) and Mixin = Module.new are types named by the constant, the block their body: its defs are their methods. Struct.new and Data.define — as a superclass too, class Money < Struct.new(:cents) — yield an inheritance edge to <ruby>/Struct / <ruby>/Data with provenance generated: the source writes no < Struct, the language defines it. The superclass given to Class.new is written, hence declared. Each member is a field.
  • Key components are percent-encoded: /, # and % in every path segment and name (def /(other) is a method named /, keyed Money.%2F), and < in a path segment, so that no walked path can spell a reserved module. Ruby has no overloading — a name is one method per type — so a method carries no arity; def self.call and def call coexist in one class, and the singleton method alone carries the disambiguator self, the keyword the source writes. A method written twice in one file is one entity, anchored at the first. A parameter is keyed #param:<name> below its method, by 1-based ordinal when it has no name or repeats one.
  • signature is the method as it would be called: total(currency, [precision], *rest, tax:, [rounding:], **opts, &blk) — brackets mark a parameter with a default, whose value is not part of the signature; a singleton method is prefixed self.. The default itself rides as the parameter’s value when it is a constant expression: a string, a number, true/false/nil, an array of those, a frozen literal ("EUR".freeze); a symbol, a hash, a range and a regexp as unevaluated with the text written; a constant naming a type as a type literal. A default or an initializer that calls carries no value at all.
  • A field is an instance variable, and the attr_reader / attr_writer / attr_accessor that exposes it is the SAME entity, named @total and anchored at the attr_* when there is one, else where the variable is first written. @total in a body and a call of the accessor (total, self.total = x, order.total on a typed receiver) are access edges — the C# property rule: the accessor is not an entity. An instance variable belongs to the object, so a subclass reading @total reads the field of the root-most corpus ancestor that has it. @x in a class body or a singleton method is the class’s own variable and carries the self disambiguator; @@x is a field of the type. A Struct member is a field named like the variable it stands for (@cents).
  • Corpus membership is a whitelist of the constant paths the roots declare or assign, never a name-prefix test. Every other type is a stub below one of two reserved modules: <ruby> for a constant the VM defines before any file runs (the core classes and modules, from a table distilled from RBS core/ at a pinned version and embedded in the extractor — the header names that version), and <gems> for a constant the roots do not declare and the VM does not define: a library, whichever one. The standard library is in <gems> on purpose — nearly all of it is default or bundled gems, the set moves with every Ruby release, and a model’s keys must not. A <gems> stub is a class when a site inherits from it, instantiates it or raises it, and a module otherwise — every class is one; it may also be a value constant of that library, which nothing here can tell. That kind is the evidence ONE corpus holds, so two corpora can disagree on a stub (Logger instantiated here, only named there): validating their union reports it as a duplicate-id conflict. A stub has no members: a call on a library’s type lands on the stub.
  • require, require_relative, load and autoload with a literal argument are import edges from the file, wherever they are written. require_relative names a path: the file it resolves to, or a stub module keyed by that path when nothing is there; the Ruby 1.8 spellings fold to it (File.expand_path("../x", __FILE__), File.join(File.dirname(__FILE__), "x"), "#{__dir__}/x"). require "acme/order" is searched on the load roots the layout implies — every directory named lib, spec or test, then the root — nearest to the requiring file first, and that resolution is a fact about the repository rather than the language, counted on its own. A feature no corpus file answers is a stub module keyed by the feature as written below <gems> (<gems>%2Fnokogiri, <gems>%2Fnet%2Fhttp). Any other computed argument is dropped and counted.
  • A Zeitwerk application writes no require: the constant reference IS the import, turned into a load at run time. So a constant that resolves to a declaration in another file — a type or a value, in a header or in a body — yields ONE import edge per (file, target file), provenance derived, anchored at the first such reference, and none when the file already imports that file explicitly. Without it the import layer, the one layer comparable across languages, is empty for every Rails application; with it, a consumer that needs written facts only filters on declared.
  • A constant is resolved as Ruby resolves it: each enclosing scope from the innermost out (Module.nesting — a compact class A::B opens ONE scope, A::B, and does not see A), then the ancestors of the innermost scope (a constant of the superclass or of an included module), then the top level; ::X is absolute; A::B resolves A that way, then B in A and in what A inherits from (Child::LIMIT is Base::LIMIT when Base assigns it) — never at the top level. A compact declaration READS what it is written under: class Admin::Users is a reference from Admin::Users to Admin where Admin is a type, since the name must exist for the class to open; a wrapper (module Admin; class Users) creates what it opens and reads nothing. The header of a declaration (< Base, include M) is read by the lexical scopes alone. A constant read is a reference to the type it names, or an access to the value constant it names; A::B::C is one read, not three. A read of a VM value (ARGV, ENV) is the language: resolved, counted, no edge.
  • class A < B is an inheritance edge when B is a constant; < Class.new(Base), < ActiveRecord::Migration[7.0] and every other computed superclass is dropped and counted — the constants it names are still referenced, from the class. include M, extend M and prepend M written in a type’s body (or include M inside its class << self) are traitUsage edges from the type, one per argument, and the edge does not say which of the three was written; extend self is not a relation. What an included hook gives whatever includes the module is read where the idiom is literal: base.extend(M), base.include(M) and their send spellings in def self.included(base); and, in a module that writes extend ActiveSupport::Concern, its ClassMethods module, class_methods do … end — the BODY of that module, an entity anchored on the block — and the mixins written directly in included do … end. No traitUsage is written for it, since the includer writes none: the lookup goes through the hook, and what it finds there is an inference (below). A mixin applied any other way — a computed argument, a hook that calls a helper — is invisible to the parser; --trace adds it.
  • A call is an invocation when what it is sent to has a type the source states, looked up as Ruby would: prepended modules, the type, its mixins last-included first, its superclass’s — and on the class side its singleton methods, what it extends, its superclass’s. declared: an implicit or explicit self, self.class.x, Foo.x on a constant, Foo.new(…).x, super, and new(…).x where self is the class. Foo.new — and a bare new in a class method — is an invocation of initialize. module_function and extend self make an instance method answer on the module; an alias_method or alias answers with the method it names; send(:x) with a literal name is a call like any other, and method(:x) a reference to the method. A top-level def is a method of every object. A block is assumed to keep self, as each and map do: an instance_eval-style DSL block is not detected.
  • derived says the receiver’s type was inferred: a local every assignment of which in its body is Foo.new(…) (flow-insensitively — one other assignment anywhere untypes it, and a parameter types nothing), and an instance variable every assignment of which in the type is Foo.new(…), the @repo ||= Repo.new idiom included. An accessor on such a receiver is a derived access. derived also says the METHOD was reached through an included hook: audited :total in a class that includes a concern whose class_methods define audited — the receiver is written, the link from the class to that module is not.
  • What every object answers (puts, raise, freeze — Object, Kernel, BasicObject), what every class answers in a class-side call (new, attr_accessor, private, include — Class, Module), and any method of a literal ("x".upcase, [1].map) are the LANGUAGE: resolved, counted, and never an edge. A method the corpus inherits from a core class it names is a dependency on it — upcase in a subclass of String is an invocation of <ruby>/String, between? of <ruby>/Comparable — and a call on a core class is an invocation of that class as written (File.read lands on <ruby>/File, though read is IO’s; so does Time.zone, which some library added).
  • A call nothing the corpus or the VM defines answers, in a type with exactly ONE library that could answer, is an invocation of that library’s stub with provenance derived — an inference by elimination, where has_many and where land in an ActiveRecord model. A library that could answer is a library superclass, a library module mixed in — and, on the class side, a library module merely INCLUDED, whose hook may have extended the class: validates in a model that includes ActiveModel::Validations is that module’s, not ActiveRecord::Base’s. Two of them is dropped and counted as ambiguous-ancestor; none, as unbound-self-call (unbound-method on a named receiver). A type that defines methods no def writes proves nothing by not declaring one — on the side they are defined for: def method_missing and a define_method with a computed name in a class body cloud its INSTANCES, def self.method_missing and define_singleton_method the class itself; a miss on that side is unbound-dynamic.
  • A receiver no parser can type — a parameter, a block argument, the result of a call — is the stated ceiling: dropped and counted as untyped-receiver, never guessed, candidate generation being the analyzer’s. An operator on such a receiver (a + b, x[0], !done) is counted apart and kept out of the resolution rate: it is the language far more often than not. --explain-dropped lists every dropped site with its reason, file and line, and what decided it where something did: the type that defines methods at run time for an unbound-dynamic, the libraries that could answer for an ambiguous-ancestor.
  • raise Foo, raise Foo, "msg" and raise Foo.new(…) are throws edges to Foo, beside the reference the constant read also is; raise "text" throws <ruby>/RuntimeError. raise error re-raises a value whose class the source does not say: dropped and counted.
  • A def belongs to the type whose body it is written in — a def inside a block (included do … end) too, since a block does not change where a constant or a method is written, a concern’s class_methods do … end alone excepted (it is the body of ClassMethods); at a file’s top level it is a child of the file. def self.x and a def inside class << self are singleton methods of the enclosing type. define_method(:x) { … } with a literal name is a method like a def, its block the body. def obj.x, a def inside class << obj, and a type whose name is computed (class self::Foo) or written inside a singleton class have no key to carry them: dropped and counted, their constants still read, their calls not.
  • An edge comes from the narrowest entity that encloses its site: a method, else the constant whose initializer it is in, else the class or module body, else the file. A class body is code (has_many :lines, attr_reader), a file’s top level too (a Rakefile, a gemspec) — hence the optional TWithInvocations / TWithAccesses on those kinds, declared when the entity is the source of such an edge. A call is anchored at the line its name is written on, not across the block it is given.
  • Blind spots, stated: methods no def writes (define_method with a computed name, method_missing, a class_eval of a string in a loop, the accessors delegate, def_delegators and ActiveRecord generate); mixins applied at run time other than through a literal included hook; a test framework’s DSL (describe, it, expect, let — a spec directory is the bulk of what a Rails application leaves unbound); send, public_send, const_get, constantize with a computed name; class_eval and instance_eval; ERB, Haml and Slim templates, which are not Ruby files; a file’s top-level DSL (gem in a Gemfile, task in a Rakefile, run in a config.ru) — evaluated by a library against its own object, so unbound here. Visibility (private, protected) is not modelled.
  • Walked: *.rb, *.rake, *.gemspec, *.ru, the extensionless Rakefile, Gemfile, Guardfile, Capfile, Vagrantfile, Brewfile, Dangerfile, Thorfile, and any other extensionless file whose first line is a ruby shebang — under the roots and outside vendor, node_modules, tmp and dot-directories (a root named on the command line is walked whatever it is called). Prism recovers from syntax errors and honours encoding magic comments, so a file with errors is still extracted from the tree the parser recovered, and named on stderr; a name that is not valid UTF-8 is written with U+FFFD and counted.
  • Comments (TComment): the # block written directly above a class, a module, a method or a constant, one string, magic comments (frozen_string_literal) and tool directives (rubocop:) looked through. Measures (TMetrics, METAMODEL.md §3.8): sloc on the file, every type and every method — lines of its own span that are neither blank nor comment-only, a heredoc’s text being code and what follows __END__ data; cyclomatic on methods: 1 + if/elsif/unless/ternary + while/until/for + each when/in + each rescue clause and modifier + &&/||/and/or. A block contributes to its enclosing method: it is not an entity.
  • The metrics pass (codegraph-metrics, PLAN.md §18): big-code-analysis’s Ruby grammar opens a space per class, module and def, so every file, type and method joins exactly and the join’s witness holds — sloc equals its ploc on each (106 of 106 on the fixture). Point = Struct.new(…) do … end, Handler = Class.new(Base) do … end and a concern’s class_methods do … end are blocks to the grammar: no class space of their own, and none of the pass’s keys.
  • --trace <file> merges what extractors/ruby/trace/codegraph_trace.rb wrote from INSIDE the running program — the one thing the extractor never does is run corpus code, so the trace is a separate step, the user’s, in a project that still boots. It names constants, files and lines, never a key: no entity is added and no edge of the source changes. A mixin the VM holds and no line writes is a traitUsage with provenance generated, anchored on the declaration of the type that has it. A call the program made at a site the baseline left unresolved is ONE invocation with provenance dynamic-candidate, candidates listing every method that site was seen to reach and to the first of them — what ran once, not what must run. A method the VM holds on a line that writes no def has no entity and gets none: it is counted.
  • Audited (M18c) on rake 13.2.1, rubygems 3.6.2, Mastodon 4.3.0 and rails 8.0.2 — 100, 794, 2 541 and 3 334 files, every model validate-clean, rails in 4.7 s. Resolution rate 77.8 %, 54.4 %, 31.2 % and 57.0 % of the sites; Mastodon without spec/ is 62.1 % (55 000 of its unbound calls are RSpec’s DSL), rails’ own lib and app 59.4 %. Four fifths of what stays unresolved in application code is untyped-receiver — the ceiling of any reader that does not run the code. ORACLES: Packwerk 3.3.1’s reference extraction and ConstantResolver over Mastodon’s autoload paths resolve 4 335 cross-file constant references in app/ and lib/; the model has the same constant in the same file for 4 289 of them (98.9 %) — the 45 others are a module Admin WRAPPER, which Packwerk counts as a reference and the wrapper rule does not, and one an inherited constant Packwerk places by file name. The model names 346 more that Packwerk cannot: declarations outside the autoload paths. Run under its own tests in a container, rake’s VM holds a method on the same line for 1 006 of the 1 012 methods the model declares in the 83 files it loaded (the six others: definitions under a platform or capability if, a def inside a method that never ran, methods of anonymous classes), and 200 methods the model has none for — 199 written by the metaprogramming loops of three files (class_eval of a string, define_method over a list), one the else branch of a platform test. Of the 2 829 calls observed during that run, 1 533 are edges the baseline wrote and 879 land on sites it had left untyped. The core-table misreading, measured on rubygems: 39 corpus bodies of 20 VM types (Gem alone 12), and 3 802 of 71 197 edges landing on the VM’s stub of a type that corpus itself implements.

Rust — rust

Edge kinds: import · interfaceImplementation · invocation · access · reference

KindRequired traitsOptional traits
constTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
crateTNamed, TModule, TWithChildrenTComment
enumTNamed, TType, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment
fieldTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment
functionTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
implTWithChildren, TAttachedTo, TSourceAnchorTChildOf, TComment
localVariableTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor, TComment
methodTNamed, TInvocable, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TTypedEntity, TChildOf, TSourceAnchorTComment
moduleTNamed, TModule, TWithChildren, TChildOfTSourceAnchor, TComment
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor
structTNamed, TType, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment
traitTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTComment
typeAliasTNamed, TType, TTypedEntity, TChildOf, TSourceAnchorTComment

Lexicon — what the vocabulary reads of this language

  • Reserved words: as · async · await · break · const · continue · crate · dyn · else · enum · extern · false · fn · for · if · impl · in · let · loop · match · mod · move · mut · pub · ref · return · self · Self · static · struct · super · trait · true · type · unsafe · use · where · while · abstract · become · box · do · final · gen · macro · override · priv · try · typeof · unsized · virtual · yield
  • Conventional names: main · fmt · drop · clone · clone_from · eq · ne · partial_cmp · cmp · deref · deref_mut · into_iter · from_str · try_from · try_into · as_ref · as_mut · borrow_mut · index_mut · default · poll
  • Technical words: arc · buf · cell · ctx · err · ffi · iter · mutex · ok · opt · rc · refcell · rs · rx · sys · tx · vec

Notes

  • Rust has no inheritance: TWithInheritances and the inheritance edge kind are absent from this profile by design, not by omission. Supertrait bounds (trait A: B), generic bounds and where-clauses are reference edges.
  • The impl block is reified (METAMODEL.md §7): an anonymous entity with [TWithChildren, TAttachedTo, TSourceAnchor] and no TNamed, attachedTo the implementing type, its methods as children. The interfaceImplementation edge Type -> Trait takes that block as its anchor. Inherent impl Type blocks carry no such edge but still own their methods.
  • Imports exist at two levels: use paths inside the module tree and crate dependencies declared in Cargo.toml. Both are import edges; the level is read from the endpoints’ kinds (module vs crate), never from the id string.
  • Modules are N-N with files: TModule.definedIn lists every file contributing to a module — inline mod x { } blocks put several modules in one file, while mod.rs plus sibling files (and #[path] attributes) spread one module tree over many.
  • Blanket impls (impl<T: Bound> Trait for T) and derived trait impls yield interfaceImplementation edges with provenance ‘derived’: the concrete implementing types are computed by the analysis, not written in the source.
  • #[derive(…)], macro_rules! and proc-macro expansion produce entities and edges with provenance ‘generated’. A pre-expansion extraction sees none of them; a post-expansion one must anchor them at the invocation site.
  • dyn Trait objects, generic calls and function pointers resolve to provenance ‘dynamic-candidate’ with the known impls as candidates.
  • cfg-gated code (#[cfg(...)], feature flags, target attributes) may be absent from any single extraction: a model reflects exactly one configuration, so two extractions of the same crate can legitimately differ in entity set.
  • Trait default method bodies live in the trait, not in the impl blocks that inherit them; their invocation and access edges originate from the trait’s method, and an impl that does not override the method has no child for it.
  • Enum variants are not reified as a kind of their own: the enum is the type-level unit, variant payload types appear as reference edges from it, and named variant fields are field children.
  • static items map to the const kind. Tuple-struct fields are named by their position (“0”, “1”, …). Shadowed let bindings produce several localVariable entities disambiguated by (file, startLine).
  • The self receiver is not emitted as a parameter; the receiver relation is the impl block’s attachment.

TypeScript — ts

Declaration spaces: abstractClass, arrowFunction, class, enum, function, interface, method, module, namespace, parameter, property, typeAlias, variable.

Edge kinds: import · inheritance · interfaceImplementation · invocation · access · reference · annotationUse · throws

KindRequired traitsOptional traits
abstractClassTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics, TWithInvocations, TWithAccesses
arrowFunctionTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity, TComment, TMetrics
classTNamed, TType, TWithInheritances, TWithImplements, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics, TWithInvocations, TWithAccesses
constructorTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTComment, TMetrics
enumTNamed, TType, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
functionTInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTNamed, TTypedEntity, TComment, TMetrics
interfaceTNamed, TType, TWithInheritances, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics
methodTNamed, TInvocable, TWithChildren, TWithParameters, TWithLocalVariables, TWithInvocations, TWithAccesses, TChildOf, TSourceAnchorTTypedEntity, TComment, TMetrics
moduleTNamed, TModule, TWithChildrenTSourceAnchor, TComment, TMetrics, TWithInvocations, TWithAccesses, TWithLocalVariables
namespaceTNamed, TWithChildren, TChildOf, TSourceAnchorTComment, TMetrics, TWithInvocations, TWithAccesses, TWithLocalVariables
parameterTNamed, TStructural, TTypedEntity, TChildOfTSourceAnchor, TWithValue
propertyTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment, TWithChildren, TWithValue, TWithInvocations, TWithAccesses
typeAliasTNamed, TType, TChildOf, TSourceAnchorTTypedEntity, TComment, TMetrics
variableTNamed, TStructural, TTypedEntity, TChildOf, TSourceAnchorTComment, TWithChildren, TWithValue, TWithInvocations, TWithAccesses

Lexicon — what the vocabulary reads of this language

  • Reserved words: await · break · case · catch · class · const · continue · debugger · default · delete · do · else · enum · export · extends · false · finally · for · function · if · import · in · instanceof · new · null · return · super · switch · this · throw · true · try · typeof · var · void · while · with · yield · implements · interface · let · package · private · protected · public · static
  • Conventional names: constructor · toString · toLocaleString · valueOf · toJSON · [Symbol.iterator] · [Symbol.asyncIterator] · [Symbol.hasInstance] · [Symbol.toPrimitive] · [Symbol.toStringTag] · [Symbol.dispose] · [Symbol.asyncDispose]
  • Technical words: args · async · callback · cb · fn · json · opts · promise · prototype
  • File kinds: module

Notes

  • This is the only profile that populates Entity.space (METAMODEL.md §1.4). Type-space only (space: ["type"]): interface, typeAlias, a const enum (its members are inlined at emit, leaving no runtime entity), and a namespace that declares only types. Both spaces (["type", "value"]): class, abstractClass, a plain enum, and a namespace that declares at least one value. Value-space only (["value"]): module, function, arrowFunction, constructor, variable, parameter. A method or property is ["type"] when written in an interface body — it exists only for the checker, and an access to it is a dependency on the interface’s SHAPE — and ["value"] when written in a class or an object literal.
  • A dependency whose target is type-space only is ERASED at runtime: it exists for the type checker and leaves nothing in the emitted JavaScript. Runtime, bundling and deployment analyses should therefore filter edges whose to resolves to a space: ["type"] entity; architectural coupling and design analyses should keep them, since the design dependency is real. Because the distinction is per-analysis, the model always stores both and never pre-filters.
  • import type { X } and inline type specifiers produce ordinary import edges; they are erased at runtime and are recognized by the space of their target, not by a separate edge kind.
  • The module is the file: TModule.definedIn always has exactly one entry, and every entity’s module is the file it is written in — a script file’s global declarations included (containment is where a thing is written, CLAUDE.md invariant 5; there is no <global> module). A namespace block is a child entity of its file or enclosing namespace, never a module. An ambient declare module "pkg" block in a corpus .d.ts IS a module entity, keyed by the quoted name, with isStub: false — the corpus declares it, and an import of pkg resolves to it.
  • Key components are percent-encoded: the file path that names a module is made of /, which renderId reserves, so every path segment and every non-identifier name entering a key encodes exactly / as %2F, # as %23, % as %25 and — in names only, where . separates nesting — . as %2E. Identifiers contain none of the four and are written as-is, so the encoding is injective and reversible; the module entity’s name is the unescaped path. A #private member is therefore Foo.%23secret, and a string-literal member "a.b" is Foo.a%2Eb.
  • Declaration merging yields one entity PER DECLARING FILE (the module is the file): interface Order in two files is two entities, and two declarations in one file are one entity anchored at the first. An edge to a merged symbol targets the declaration that owns the referenced member; an edge to the merged container itself targets its first declaration in canonical file order — a deterministic choice, not a claim that the others do not exist. A module augmentation of an external type (declare module "express" { interface Request { user: User } }) makes its members declared entities parented by the stub type.
  • Structural (non-nominal) typing: a class conforms to an interface without any implements clause. interfaceImplementation therefore carries provenance declared only for an explicit implements; conformance computed by shape comparison is derived, is never computed by the extractor, and must never be merged with the declared edges. Any analysis wanting facts filters on declared.
  • Corpus membership is a whitelist of the symbols DECLARED in the analyzed sources, never a path-prefix test. What the checker resolves outside the roots is still external: a type declared under node_modules/<pkg> is a stub in the module named by the package (the nearest package.json name, never the resolved .d.ts path, so keys do not depend on what is installed); a type from the compiler’s own lib.*.d.ts is a stub in the reserved module <lib> (Array, Promise); a name that binds to nothing is a stub in the reserved module <unresolved>, named as written — never a guess from the file’s imports. A module specifier that resolves to nothing becomes a stub module keyed by the specifier as written, so the import edge survives and import fan-out stays honest. Node built-ins are normalised to the node: form.
  • A bare specifier that names a package declared UNDER the roots (a monorepo’s own @acme/pricing) resolves by package.json name to that package’s source entry when standard resolution fails — with nothing installed and nothing built — because a workspace’s packages are corpus, not dependencies. Every such resolution is counted separately on stderr; a subpath below such a package resolves the same way below its source root, else stubs.
  • any erases resolution completely: a call or member access through an any-typed (or unknown, or error-typed, or index-signature) receiver has no target. Such edges are dropped and COUNTED in the stderr summary — never emitted with a guessed candidates list, since candidate generation is the analyzer’s, which alone has whole-corpus implementor knowledge. The proportion of any-typed receivers is the honest ceiling on this profile’s resolution rate.
  • A nameless invocable (arrow function, function expression) is identified by #line:column of its first token below the nearest NAMED ancestor’s symbol, and below that ancestor’s own disambiguator when it has one — a column is a source fact, and two can start on one line. At module top level the symbol is empty and the disambiguator alone identifies it (ts:src%2Fa.ts#3:15). const f = () => {} yields two entities: the variable f and its child arrowFunction; call sites resolve through the variable to the arrow, and invocation edges target the arrowFunction. A class expression with no binding is not an entity (dropped and counted).
  • Overloads are ONE entity: TypeScript has no overloading by parameter type at the declaration level, so a function’s overload signatures and its implementation share one key with no parameter-list component; the entity is anchored at the implementation (or the sole ambient signature). Members TypeScript lets coexist under one name carry a disambiguator: #static for a static member beside an instance one, #get/#set for an accessor pair; a member without a twin renders with no suffix.
  • Ambient declarations (.d.ts, declare module, declare global) under the roots describe entities with no implementation in the corpus. They are emitted as real entities anchored in the .d.ts; the implementation they describe, when outside the corpus, is a stub.
  • declaredType is populated from the checker’s type where it names ONE declaration — annotated or inferred alike; a union, intersection, literal, primitive, type-parameter or anonymous type leaves it absent rather than guessed, which is why TTypedEntity.declaredType is optional even where the trait is required.
  • Generic type parameters are not reified as entities; a use of Array<Order> yields a reference edge to Order and one to <lib>/Array, and none to Array’s parameter slot. A type parameter’s constraint and default are reference edges from the declaring entity.
  • Decorators are annotationUse edges carrying their arguments as written values, under both the legacy (experimentalDecorators) and the TC39 syntax; the decorator itself is an ordinary function or variable entity. emitDecoratorMetadata synthesizes nothing the model shows.
  • A JSX element whose tag names a component (<OrderTable/>) is an invocation of that component — an element is a call by the language definition, so the edge is declared; an intrinsic tag (<div>) yields nothing.
  • A throws edge is emitted per written throw statement whose static type names a declaration, anchored at the throw site; a rethrow targets the caught binding’s static type; a thrown string or any is dropped and counted.
  • Measures (TMetrics, METAMODEL.md §3.8): sloc on every type and invocable — lines of its own span that are neither blank nor comment-only, counted with the compiler’s own scanner, so template literals, regular expressions and JSX text cannot swallow a comment marker. cyclomatic on invocables only: 1 + if / for / for-in / for-of / while / do / non-default case / catch / ternary / && / || / ??. A nested arrow or function does NOT contribute to its enclosing invocable: it is its own invocable and carries its own count.
  • Path resolution depends on tsconfig paths, baseUrl, rootDirs and package.json fields; the extractor reads a tsconfig.json for those RESOLUTION options only — never for files/include (the roots define the corpus) and never for project references — and creates one program over every source file under the roots. Nothing is built and node_modules is never required.
  • Dynamic import(expr) and require(expr) with a computed, template-literal or variable path are unresolvable: no import edge is emitted, and the site is counted. Only statically literal specifiers produce edges. CommonJS module.exports reassignment shapes beyond a literal require, monkey patching (Obj.prototype.m = fn, Object.assign), this rebinding through call/apply/bind, and computed member access o[k] with a non-literal key follow the JavaScript profile’s rules: no entity is invented and the site yields at most access/reference edges at the patch site, or nothing.
  • Resolution rate and the any-receiver ceiling, measured (M13c audit, every corpus extracted WITHOUT node_modules): 97.4 % on microsoft/TypeScript 4.9.5 src/compiler (69 files, 52 341 entities, 2 684 any-typed receivers, 3 unresolved names — node types with no @types/node); 88.6 % on nestjs/nest packages (902 files, 29 992 entities, 20 745 any-typed receivers, 78 unresolved names, all from uninstalled fastify/express/class-transformer); 94.4 % on excalidraw (628 files, 43 706 entities, 24 954 any-typed receivers, 41 unresolved names from uninstalled react/mermaid/vitest, 303 stub modules mostly font and asset imports); 96.5 % on codegraph itself (24 887 entities, zero unresolved, 168 imports resolved to workspace packages by name). The rate is a property of the corpus’s DEPENDENCY SURFACE and of whether its dependencies are installed: with no install, every call into a package is a call through any and is dropped and counted — the honest lower bound. Install the dependencies (or point --src at them) to fold those calls to their packages’ stubs.
  • Members of an object literal are entities only when the literal is BOUND to a name (const Ops = { … }, static x = { … }): array elements, call arguments and return values are unbound, so their parts are not entities and a method written in one is keyed positionally like an arrow. A class expression bound to a variable or a property IS that binding (one entity, the class’s); a class expression bound to nothing is no entity (dropped and counted), nor is anything written inside it. Two declarations of one name in sibling blocks of one scope (if (a) { class X {} } else { class X {} }) are re-keyed by position and named on stderr.
  • An edge whose endpoint no entity declares is dropped and counted as unclosable on stderr, never written dangling (schemas/README.md §5): zero on every audited corpus, and a non-zero count names an id-scheme gap.
  • Getters and setters are method entities; a property access that runs an accessor is an access edge, not an invocation, so a read that runs code is visible as access — a write lands on the setter, a read on the getter. Class fields and object-literal members share the property kind; a parameter property (constructor(readonly x: T)) is both a parameter and a property, and an access to it lands on the property.
  • Locals are entities keyed #local:name:line:column below their invocable (two let x in sibling blocks are two entities) and listed in its localVariables; a function declared inside an invocable is #fn:name, a type declared there #type:name. Locals and parameters are never edge targets: an access to a local is not a dependency, and a call through a parameter of function type names no declaration (dropped and counted as an indirect call). A member of an anonymous type literal ({ order: Order }) is not an entity — the literal is none.
  • A written value (TWithValue) is emitted for a const variable, a readonly property, an enum member and a parameter default whose initializer is CONSTANT-SHAPED — a literal, -N, an enum member, a type used as a value, arrays and operator expressions over those. TypeScript folds nothing at the declaration, so 4 * 25 rides unevaluated with its source text where Java would fold it; an enum member’s value is the checker’s computed constant. An initializer that is code — a call, a new, a function, an object literal — carries no value at all: absence is a claim. Such an initializer’s calls and accesses are edges FROM the variable or property (the narrowest declared owner), which then carries TWithInvocations/TWithAccesses.
  • The metrics pass (codegraph-metrics, PLAN.md §18): namespaces, enums, type aliases, interface members without a body and ambient declarations have no big-code-analysis space and carry none of the pass’s keys. An exported class’s decorators precede tree-sitter’s class node; the pass joins across the decorator lines. In a .tsx file big-code-analysis’s ploc can count a line or two fewer than this extractor’s sloc (a JSX text token counts every line it spans here), so the join’s witness differs there by that much.
Last updated on