Skip to main content

DECLARES

DECLARES is the first semantic edge on top of the AST. It links a node to each Identifier it introduces as a JavaScript binding. It's the base that REFERS_TO and value flow build on.

Where edges come from​

During the walk, each node is passed to declared(node), and an edge is pushed for every identifier it returns:

for (const t of declared(node)) push("DECLARES", node, t, { from: id, to: idOf(t) });

idOf(t) is the same identity-keyed id function SON uses, so the target is the Identifier already in the tree. It's never a copy. A test checks this: a VariableDeclarator's DECLARES target and its SON {key: 'id'} child have the same id.

declared(node)​

Node typeBindings
VariableDeclaratorbindingIds(id)
FunctionDeclaration, FunctionExpressionbindingIds of id and each param
ArrowFunctionExpression, ObjectMethod, ClassMethod, ClassPrivateMethodbindingIds of each param
ClassDeclaration, ClassExpressionbindingIds(id)
ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifierlocal
CatchClausebindingIds(param)
anything elsenone

bindingIds(pattern)​

This recursively collects the identifiers a binding pattern binds:

PatternRecurses into
Identifieritself, unless it's this (a TypeScript this parameter isn't a binding)
ObjectPatterneach property's value, or a RestElement's argument. Never the key.
ArrayPatterneach element (holes are null and yield nothing)
AssignmentPatternleft only. The default value is an expression, not a binding.
RestElementargument
TSParameterPropertyparameter (constructor(private x))

Choices and their reasons​

  • The declarator, not the declaration. const a = 1, b = 2 has one VariableDeclaration and two VariableDeclarators. The declarator owns both the name and its initializer (init), so it's the node that WRITES the binding and that its init flows through. See References.
  • Method keys aren't bindings. { method(a) {} } binds a but not method. The key is a property name. A test checks that no SON {key: 'key'} target is ever a DECLARES target.
  • Wrappers declare nothing. ExportNamedDeclaration, ExportDefaultDeclaration and VariableDeclaration only wrap the real declarer. export { x } and re-exports (export { y } from, export *) refer to bindings rather than creating them.
  • Assignments aren't declarations. ({ a } = o) writes to an existing binding. That's a REFERS_TO plus a WRITES, not DECLARES.
  • Scope is a separate edge. A FunctionDeclaration declares its own name and its params, even though the name binds in the enclosing scope and the params bind inside the function. DECLARES only answers "who introduces this name". IN_SCOPE answers "where does it live", and it's emitted from the same declared() call. See Scopes.

Coverage​

src/declares.test.ts holds 36 cases, each a snippet with its expected sorted Declarer -> name edges. They cover every declaration form, from using and await using through destructuring with computed keys and defaults, generators, class private methods, every import form and catch params. There are also negatives: export wrappers, re-exports, control flow and assignment patterns. Further tests check that only the producers in the table above ever emit DECLARES, that targets are always Identifiers that keep their hashes, and that the edges survive save() and reopening.