mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
fix(js,ts): index the functions in every named object literal, not only exported ones (#2300) (#2363)
Issue #2300: a function written inside an object literal became a symbol
only when the object was an `export const`. A plain `const api = {...}`,
an object declared inside an IIFE or a function, and a namespace hung on
the page (`window.WS = {...}`, `ns.mod = {...}`) produced no member nodes,
and no node for `WS` either. Calls made inside those members were credited
to the enclosing constant or lost, and calls into them resolved to
nothing. Script-tag JavaScript, written almost entirely this way, was
mostly missing from callers and impact.
Cause: the TS/JS extractor, and the kernel's tsjs mirror of it, minted
members only for exported object-of-functions; any other literal was
walked as one opaque initializer, and an assignment to a member path was
never a declaration. Resolution had no way to reach a member through
`App.init()`, `window.App.init()` or `App.utils.pad()`.
Fix (extraction, TS and kernel byte-identical): a named object literal
owns its function members (method shorthand, `key: function`, `key: () =>`,
generators; static keys only), whether it is declared at module scope,
in a function body or IIFE, assigned to a path (`window.App = {...}`,
`App.utils = {...}`) or assigned to a plain name at module level
(`dw_page = {...}`). Members are `function` nodes qualified under the owner
(`api::load`, `window.App::init`, `App.utils::pad`), the exported case
included; a path owner is named by its last link and qualified by the
path. Calls in a member are the member's; other values are walked where
they were before. CommonJS exports, prototypes, `this.x = {...}`,
call-argument literals, a name reassigned inside a function, and
generated or minified files keep the old shape. Minified files are now
also recognised by content, in both extractors, and the kernel's
`.min.js` pattern matches the TS one.
Fix (resolution): a member is reached through its object only:
`App.init()`, `window.App.init()`, `App.utils.pad()`, a sibling's
`this.render()`, a `const { init } = App` binding; never a bare `init()`.
A same-file holder is chosen by lexical block (an IIFE's own `App` first);
otherwise a global one (`window.App = {...}`, or a classic script's
top-level `App`) when the caller neither imports nor binds the name.
Arrow members keep the `this` of the method around the literal, in the
`this.x` resolvers too. A dotted-path holder is never reached by its last
name alone, and Svelte's `$store` rule now applies only in `.svelte`
components, so the new local holders are not taken for `$n` in plain
scripts. The lookups read each file once (destructuring patterns only
when the raw text has a `} =`) and keep only the last 32 files' scans.
Contributor PR #2310 (@danusha2345): adopted its model (owner-qualified
members exported or not, path owners, IIFE/local owners, static keys,
no bare-name reach, `<script setup>` contains edges, SFC languages, test
scenarios), re-implemented on current main. Not taken: the
EXTRACTION_VERSION bump (already 28), the extraction-time binding oracle
with its ref/edge metadata and kernel ref patching (replaced by
resolution-time lookups), naming the owner `window.WS`, rewriting
`window.X.m()` ref names, flipping exported members' isExported, and
unrelated bare-call changes.
Verification: new js-object-literal-members suite (native + wasm, 18
tests); the issue's shapes fail on main and the guard test fails without
the guards. kernel-tsjs-parity passes with CODEGRAPH_KERNEL_EXPECT=1
(object-literal owners LF/CRLF, minified bundles); kernel parity sweeps
show 0 diffs on DokuWiki, vue-realworld, TodoMVC, excalidraw and this repo.
Validation (kernel loaded, before = origin/main 023fc311):
- DokuWiki: nodes +85, calls +105; 17 removed edges = 13 moved to the
member, 4 re-resolved from a wrong LinkWizard::init to the right init.
- TodoMVC: nodes +427, calls +148; 306 removed = 226 moved, 33
re-resolved (14 fixed `this.render()`/`this.save()`/`this.track()`, 19
wrong-to-wrong fallback guesses), 47 dropped wrong edges (43 to a
helper local to jQuery Mobile's scrollstart setup).
- excalidraw: nodes +86, calls +75; 34 removed = 33 moved, 1 fuzzy 0.3
guess dropped.
- vue-realworld: nodes +10, calls +11; 7 calls re-resolved from the
ApiService constant to its members.
New self-loops are real recursion, bar one lazily redefined method that
calls itself as written. 81 sampled added edges, 80 correct
(the other is main's own window.open guess, re-attributed). Whole-run
index time, median of 3 interleaved: vue-realworld 1.2s -> 1.2s,
DokuWiki 8.6s -> 9.1s, excalidraw 10.8s -> 10.7s, TodoMVC 16.9s -> 16.9s
(within run-to-run noise).
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ pub fn is_generated_file(file_path: &str) -> bool {
|
||||
r"\.pb\.[jt]s$",
|
||||
r"_pb\.[jt]s$",
|
||||
r"_grpc_pb\.[jt]s$",
|
||||
r"\.min\.m?js$",
|
||||
r"[.-]min\.m?js$",
|
||||
r"_pb2(_grpc)?\.py$",
|
||||
r"_pb2\.pyi$",
|
||||
r"\.pb\.(cc|h)$",
|
||||
@@ -78,6 +78,56 @@ pub fn is_generated_file(file_path: &str) -> bool {
|
||||
patterns.iter().any(|p| p.is_match(file_path))
|
||||
}
|
||||
|
||||
// isMinifiedContent's WEBPACK_RUNTIME (`\b` ASCII, as in JS).
|
||||
re!(webpack_runtime, r"(?-u:\b)function __webpack_require__\s*\(");
|
||||
|
||||
/// isMinifiedContent (src/extraction/generated-detection.ts): a `.js` / `.mjs`
|
||||
/// / `.cjs` file of at least 4000 UTF-16 units whose text sits mostly on lines
|
||||
/// of 1000 units or more, dense with `;{}(),` — or that carries a webpack
|
||||
/// runtime. Lengths are counted in UTF-16 units, as the JS string ops count
|
||||
/// them, so both extractors agree on every file.
|
||||
pub fn is_minified_content(file_path: &str, content: &str) -> bool {
|
||||
const LINE: usize = 1000;
|
||||
let lower = file_path.to_ascii_lowercase();
|
||||
if !(lower.ends_with(".js") || lower.ends_with(".mjs") || lower.ends_with(".cjs")) {
|
||||
return false;
|
||||
}
|
||||
// UTF-16 units never outnumber UTF-8 bytes: a short file is out at once.
|
||||
if content.len() < 4 * LINE {
|
||||
return false;
|
||||
}
|
||||
let total = content.encode_utf16().count();
|
||||
if total < 4 * LINE {
|
||||
return false;
|
||||
}
|
||||
if webpack_runtime().is_match(content) {
|
||||
return true;
|
||||
}
|
||||
let (mut long, mut punctuation) = (0usize, 0usize);
|
||||
let (mut line_len, mut line_punct) = (0usize, 0usize);
|
||||
for unit in content.encode_utf16() {
|
||||
if unit == 10 {
|
||||
if line_len >= LINE {
|
||||
long += line_len;
|
||||
punctuation += line_punct;
|
||||
}
|
||||
line_len = 0;
|
||||
line_punct = 0;
|
||||
continue;
|
||||
}
|
||||
line_len += 1;
|
||||
// ; { } ( ) ,
|
||||
if matches!(unit, 59 | 123 | 125 | 40 | 41 | 44) {
|
||||
line_punct += 1;
|
||||
}
|
||||
}
|
||||
if line_len >= LINE {
|
||||
long += line_len;
|
||||
punctuation += line_punct;
|
||||
}
|
||||
long as f64 >= total as f64 * 0.5 && punctuation as f64 >= long as f64 * 0.03
|
||||
}
|
||||
|
||||
/// Byte offsets of each line start, for UTF-16 column conversion.
|
||||
pub fn line_starts(src: &str) -> Vec<usize> {
|
||||
let mut out = vec![0usize];
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
use crate::textutil as util;
|
||||
use super::{
|
||||
body_of, is_builtin_type, is_literal_receiver, is_react_hoc, is_variable_type,
|
||||
is_vue_collection_name, Extra, Scope, Walker,
|
||||
body_of, is_builtin_type, is_host_global_root, is_literal_receiver, is_object_member_function,
|
||||
is_react_hoc, is_static_object_key, is_variable_type, is_vue_collection_name, Extra, Scope, Walker,
|
||||
};
|
||||
use crate::buffers::edge_kind_index;
|
||||
use tree_sitter::Node;
|
||||
@@ -449,6 +449,15 @@ impl<'t> Walker<'t> {
|
||||
self.extract_variable_type_annotation(child, row);
|
||||
}
|
||||
|
||||
// A named object literal owns its function members, exported or
|
||||
// not (#2300): `const api = { load() {…} }` gives `api::load`. The
|
||||
// rest of the literal is walked under the owner (#693). Mirrors
|
||||
// the ownedObject branch of TreeSitterExtractor.extractVariable.
|
||||
if let (Some(row), Some(obj)) = (var_row, self.owned_object_value(value)) {
|
||||
self.extract_owned_object_members(obj, row, kind, &name, true, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exported const object-of-functions / store shapes.
|
||||
let object_of_fns: Option<Node> = match value {
|
||||
Some(v) if matches!(v.kind(), "object" | "object_expression") => Some(v),
|
||||
@@ -552,6 +561,189 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- owned object literals (#2300) ------------------------------------------------
|
||||
|
||||
/// ownsObjectLiterals: a generated or minified bundle — named so or not —
|
||||
/// keeps the old shape. (Every tsjs variant is TS/JS, so only that check
|
||||
/// remains; it is computed once per file.)
|
||||
fn owns_object_literals(&self) -> bool {
|
||||
self.owns_objects
|
||||
}
|
||||
|
||||
/// ownedMemberFunction: the function an owned literal's member becomes,
|
||||
/// named by its static key — `load() {…}`, `load: () => {…}`,
|
||||
/// `load: function () {…}`, `load: function* () {…}`. None for every other
|
||||
/// member (a computed key, a value, a shorthand, a spread).
|
||||
fn owned_member_function(&self, member: Node<'t>) -> Option<(Node<'t>, String)> {
|
||||
if member.kind() == "method_definition" {
|
||||
let key = member.child_by_field_name("name")?;
|
||||
if !is_static_object_key(key.kind()) {
|
||||
return None;
|
||||
}
|
||||
return Some((member, util::object_key_name(self.text(key))));
|
||||
}
|
||||
if member.kind() != "pair" {
|
||||
return None;
|
||||
}
|
||||
let key = member.child_by_field_name("key")?;
|
||||
let value = member.child_by_field_name("value")?;
|
||||
if !is_static_object_key(key.kind()) || !is_object_member_function(value.kind()) {
|
||||
return None;
|
||||
}
|
||||
Some((value, util::object_key_name(self.text(key))))
|
||||
}
|
||||
|
||||
/// ownedObjectValue: `value` when it is an object literal owning at least
|
||||
/// one function member.
|
||||
pub(super) fn owned_object_value(&self, value: Option<Node<'t>>) -> Option<Node<'t>> {
|
||||
let value = value?;
|
||||
if !matches!(value.kind(), "object" | "object_expression") || !self.owns_object_literals() {
|
||||
return None;
|
||||
}
|
||||
for i in 0..value.named_child_count() {
|
||||
if let Some(member) = value.named_child(i) {
|
||||
if self.owned_member_function(member).is_some() {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Put an owner on the stack — on top of the file alone for one on the
|
||||
/// global object — returning what was set aside for leave_owner.
|
||||
fn enter_owner(&mut self, row: u32, kind: &'static str, name: &str, global: bool) -> Vec<Scope> {
|
||||
let rest = if global { self.stack.split_off(1) } else { Vec::new() };
|
||||
self.stack.push(Scope { row, kind, name: name.to_string() });
|
||||
rest
|
||||
}
|
||||
|
||||
fn leave_owner(&mut self, rest: Vec<Scope>) {
|
||||
self.stack.pop();
|
||||
self.stack.extend(rest);
|
||||
}
|
||||
|
||||
/// extractOwnedObjectMembers: each function member becomes its own node,
|
||||
/// qualified under the owner (at file scope for one on the global object).
|
||||
/// Every other member runs where the literal is written: under the owner
|
||||
/// for a module-scope declaration (`values_under_owner`, #693), under the
|
||||
/// enclosing scope for a local or an assignment. The literal's shorthand
|
||||
/// members go to the function-as-value capture the same way.
|
||||
fn extract_owned_object_members(&mut self, obj: Node<'t>, row: u32, kind: &'static str, name: &str, values_under_owner: bool, global: bool) {
|
||||
let held = values_under_owner.then(|| self.enter_owner(row, kind, name, global));
|
||||
self.maybe_capture_fn_refs(obj);
|
||||
for i in 0..obj.named_child_count() {
|
||||
let Some(member) = obj.named_child(i) else { continue };
|
||||
if let Some((func, member_name)) = self.owned_member_function(member) {
|
||||
if values_under_owner {
|
||||
self.extract_function(func, Some(member_name));
|
||||
} else {
|
||||
let rest = self.enter_owner(row, kind, name, global);
|
||||
self.extract_function(func, Some(member_name));
|
||||
self.leave_owner(rest);
|
||||
}
|
||||
} else {
|
||||
self.visit_function_body(member);
|
||||
}
|
||||
}
|
||||
if let Some(rest) = held {
|
||||
self.leave_owner(rest);
|
||||
}
|
||||
}
|
||||
|
||||
/// extractLocalObjectOwner: `const api = { load() {…} }` in a function body
|
||||
/// or an IIFE gets the node a module-scope declaration would, with its
|
||||
/// members under it. False when the declarator holds no owned literal.
|
||||
pub(super) fn extract_local_object_owner(&mut self, declarator: Node<'t>) -> bool {
|
||||
let Some(name_node) = declarator.child_by_field_name("name") else { return false };
|
||||
if name_node.kind() != "identifier" {
|
||||
return false;
|
||||
}
|
||||
let Some(obj) = self.owned_object_value(declarator.child_by_field_name("value")) else {
|
||||
return false;
|
||||
};
|
||||
let is_const = declarator.parent().map(|d| self.is_const_decl(d)).unwrap_or(false);
|
||||
let kind: &'static str = if is_const { "constant" } else { "variable" };
|
||||
let name = self.text(name_node).to_string();
|
||||
// A local: never the target of another scope's value read.
|
||||
let extra = Extra {
|
||||
docstring: crate::docstring::preceding_docstring(declarator, self.src),
|
||||
signature: Some(util::init_signature(self.text(obj))),
|
||||
is_exported: Some(false),
|
||||
not_value_target: true,
|
||||
..Extra::default()
|
||||
};
|
||||
let Some(row) = self.create_node(kind, &name, declarator, extra) else { return false };
|
||||
self.extract_variable_type_annotation(declarator, row);
|
||||
self.extract_owned_object_members(obj, row, kind, &name, false, false);
|
||||
true
|
||||
}
|
||||
|
||||
/// extractAssignedObjectOwner: `window.App = {…}` / `App.utils = {…}` /
|
||||
/// `dw_page = {…}`. A plain identifier target is qualified like a
|
||||
/// declaration. A path's owner is named by its last link and qualified by
|
||||
/// the path as written, and so are its members (`window.App::init`); a
|
||||
/// path on the global object is global wherever it is written, so it is
|
||||
/// contained by the file. False (nothing extracted) for any other assignment.
|
||||
pub(super) fn extract_assigned_object_owner(&mut self, node: Node<'t>, module_level: bool) -> bool {
|
||||
let Some(obj) = self.owned_object_value(node.child_by_field_name("right")) else { return false };
|
||||
let Some(path) = self.object_owner_path(node.child_by_field_name("left")) else { return false };
|
||||
// A plain name assigned at module level is a global (`dw_page = {…}`);
|
||||
// in a function it is a local being reassigned.
|
||||
if path.len() == 1 && !module_level {
|
||||
return false;
|
||||
}
|
||||
let qualified_name = (path.len() > 1).then(|| path.join("."));
|
||||
let global = path.len() > 1 && is_host_global_root(&path[0]);
|
||||
let rest = if global { self.stack.split_off(1) } else { Vec::new() };
|
||||
let statement = node.parent().filter(|p| p.kind() == "expression_statement").unwrap_or(node);
|
||||
let name = path.last().cloned().unwrap_or_default();
|
||||
// `App.utils` is read as `App.utils`, never as a bare `utils`.
|
||||
let extra = Extra {
|
||||
docstring: crate::docstring::preceding_docstring(statement, self.src),
|
||||
signature: Some(util::init_signature(self.text(obj))),
|
||||
is_exported: Some(false),
|
||||
qualified_name: qualified_name.clone(),
|
||||
not_value_target: !(path.len() == 1 || global),
|
||||
..Extra::default()
|
||||
};
|
||||
let row = self.create_node("variable", &name, node, extra);
|
||||
self.stack.extend(rest);
|
||||
let Some(row) = row else { return false };
|
||||
if let Some(qualified_name) = qualified_name {
|
||||
self.object_path_owners.insert(row, qualified_name);
|
||||
}
|
||||
self.extract_owned_object_members(obj, row, "variable", &name, false, global);
|
||||
true
|
||||
}
|
||||
|
||||
/// objectOwnerPath: an assignment target's links written as plain names
|
||||
/// (`dw_page` → [dw_page], `App.utils.dom` → [App, utils, dom]), or None
|
||||
/// for anything else — `this.x`, `a[k]`, a call, CommonJS's
|
||||
/// `module.exports` / `exports.x`, or a prototype.
|
||||
fn object_owner_path(&self, left: Option<Node<'t>>) -> Option<Vec<String>> {
|
||||
let mut path: Vec<String> = Vec::new();
|
||||
let mut cur = left?;
|
||||
while cur.kind() == "member_expression" {
|
||||
let property = cur.child_by_field_name("property")?;
|
||||
if property.kind() != "property_identifier" {
|
||||
return None;
|
||||
}
|
||||
path.push(self.text(property).to_string());
|
||||
cur = cur.child_by_field_name("object")?;
|
||||
}
|
||||
if cur.kind() != "identifier" {
|
||||
return None;
|
||||
}
|
||||
let root = self.text(cur);
|
||||
if root == "module" || root == "exports" || path.iter().any(|p| p == "prototype") {
|
||||
return None;
|
||||
}
|
||||
path.push(root.to_string());
|
||||
path.reverse();
|
||||
Some(path)
|
||||
}
|
||||
|
||||
// --- object-literal / store helpers -------------------------------------------------
|
||||
|
||||
pub(super) fn extract_object_literal_functions(&mut self, obj: Node<'t>) {
|
||||
|
||||
@@ -136,6 +136,24 @@ fn is_vue_collection_name(name: &str) -> bool {
|
||||
matches!(name, "actions" | "mutations" | "getters")
|
||||
}
|
||||
|
||||
/// OBJECT_MEMBER_FUNCTION_TYPES (tree-sitter.ts, #2300): the function values an
|
||||
/// owned object literal's member can hold.
|
||||
fn is_object_member_function(kind: &str) -> bool {
|
||||
matches!(kind, "arrow_function" | "function_expression" | "generator_function")
|
||||
}
|
||||
|
||||
/// STATIC_OBJECT_KEY_TYPES (tree-sitter.ts, #2300): keys that name a member — a
|
||||
/// computed `[expr]` key names nothing static.
|
||||
fn is_static_object_key(kind: &str) -> bool {
|
||||
matches!(kind, "property_identifier" | "string" | "number")
|
||||
}
|
||||
|
||||
/// HOST_GLOBAL_ROOTS (tree-sitter.ts, #2300): `window.App = {…}` defines the
|
||||
/// global `App`. (`self` is usually `var self = this` in page code, not the global.)
|
||||
fn is_host_global_root(name: &str) -> bool {
|
||||
matches!(name, "window" | "globalThis")
|
||||
}
|
||||
|
||||
/// One scope-stack entry (TS keeps node IDs; rows are our equivalent).
|
||||
struct Scope {
|
||||
row: u32,
|
||||
@@ -153,6 +171,9 @@ struct Extra {
|
||||
is_async: Option<bool>,
|
||||
is_static: Option<bool>,
|
||||
qualified_name: Option<String>,
|
||||
/// Never a value-read target: a local, or an object hung on a dotted path
|
||||
/// (createNode's `valueTarget = false`, #2300).
|
||||
not_value_target: bool,
|
||||
}
|
||||
|
||||
struct ValueScope<'t> {
|
||||
@@ -185,6 +206,12 @@ pub struct Walker<'t> {
|
||||
fs_value_counts: HashMap<String, u32>,
|
||||
value_scopes: Vec<ValueScope<'t>>,
|
||||
vue_store_file: Option<bool>,
|
||||
/// An object literal hung on a path (`window.App = {…}`) is qualified by
|
||||
/// the path, and so is what it holds (#2300): row → that qualified name.
|
||||
/// Mirrors TreeSitterExtractor.objectPathOwners.
|
||||
object_path_owners: HashMap<u32, String>,
|
||||
/// ownsObjectLiterals: false for a generated or minified bundle (#2300).
|
||||
owns_objects: bool,
|
||||
}
|
||||
|
||||
const MAX_VALUE_REF_NODES: usize = 20_000;
|
||||
@@ -234,6 +261,8 @@ pub fn extract(file_path: &str, source: &str, language: &str) -> Result<EmitOut,
|
||||
fs_value_counts: HashMap::new(),
|
||||
value_scopes: Vec::new(),
|
||||
vue_store_file: None,
|
||||
object_path_owners: HashMap::new(),
|
||||
owns_objects: !util::is_generated_file(file_path) && !util::is_minified_content(file_path, source),
|
||||
};
|
||||
|
||||
// File node (TreeSitterExtractor.extract): id `file:<path>`, endLine =
|
||||
@@ -352,6 +381,7 @@ impl<'t> Walker<'t> {
|
||||
let start_line = self.line_of(node);
|
||||
let column = self.col_of(node);
|
||||
let id = self.node_id_allocator.generate(self.file_path, kind, name, start_line, column);
|
||||
let value_target = !extra.not_value_target;
|
||||
|
||||
// endLine body extension: resolveBody only (TS/JS: function-valued
|
||||
// class fields whose body nests in the arrow / HOF-wrapped arrow).
|
||||
@@ -369,6 +399,13 @@ impl<'t> Walker<'t> {
|
||||
let qualified = extra.qualified_name.unwrap_or_else(|| {
|
||||
let mut parts: Vec<&str> = Vec::new();
|
||||
for s in &self.stack {
|
||||
// A path-hung object literal qualifies what it holds by its
|
||||
// path (`window.App::init`), which carries its own scope.
|
||||
if let Some(path) = self.object_path_owners.get(&s.row) {
|
||||
parts.clear();
|
||||
parts.push(path);
|
||||
continue;
|
||||
}
|
||||
if s.kind != "file" {
|
||||
parts.push(&s.name);
|
||||
}
|
||||
@@ -434,18 +471,19 @@ impl<'t> Walker<'t> {
|
||||
if kind == "function" || kind == "method" {
|
||||
self.defined_fn_names.insert(name.to_string());
|
||||
}
|
||||
self.capture_value_ref_scope(kind, name, row, node);
|
||||
self.capture_value_ref_scope(kind, name, row, node, value_target);
|
||||
Some(row)
|
||||
}
|
||||
|
||||
// --- value references (captureValueRefScope / flushValueRefs) --------------
|
||||
|
||||
fn capture_value_ref_scope(&mut self, kind: &'static str, name: &str, row: u32, node: Node<'t>) {
|
||||
fn capture_value_ref_scope(&mut self, kind: &'static str, name: &str, row: u32, node: Node<'t>, value_target: bool) {
|
||||
if !self.variant.value_refs() {
|
||||
return;
|
||||
}
|
||||
let target_kind_ok = kind == "constant" || kind == "variable";
|
||||
if target_kind_ok
|
||||
if value_target
|
||||
&& target_kind_ok
|
||||
&& util::utf16_len(name) >= 3
|
||||
&& util::has_upper_or_underscore().is_match(name)
|
||||
{
|
||||
@@ -640,6 +678,12 @@ impl<'t> Walker<'t> {
|
||||
// Function-as-value capture — independent of the dispatch ladder.
|
||||
self.maybe_capture_fn_refs(node);
|
||||
|
||||
// `window.App = {…}` / `App.utils = {…}`: the object's functions are
|
||||
// the path's members (#2300). Its whole subtree is handled there.
|
||||
if kind == "assignment_expression" && self.extract_assigned_object_owner(node, true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_function_type(kind) {
|
||||
// (the isInsideClassLike + methodTypes overlap is Python/Ruby-only)
|
||||
self.extract_function(node, None);
|
||||
@@ -729,6 +773,16 @@ impl<'t> Walker<'t> {
|
||||
let kind = node.kind();
|
||||
self.maybe_capture_fn_refs(node);
|
||||
|
||||
// A named object literal in a body (an IIFE's `const App = {…}`) owns
|
||||
// its function members as one at module scope does, and so does
|
||||
// `window.App = {…}` written in here (#2300). Each handles its subtree.
|
||||
if kind == "variable_declarator" && self.extract_local_object_owner(node) {
|
||||
return;
|
||||
}
|
||||
if kind == "assignment_expression" && self.extract_assigned_object_owner(node, false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if kind == "call_expression" {
|
||||
self.extract_call(node);
|
||||
} else if kind == "new_expression" {
|
||||
|
||||
Reference in New Issue
Block a user