crossbind 2.0.0-beta.74 → 2.0.0-beta.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,25 @@
1
+ // A const char* followed by an integer may be a byte span, with embedded NULs or no terminator.
2
+ // Preserve its address; JavaScript can read exactly the reported length with readBuffer. The argument keeps its const for
3
+ // callbackArgument, which takes it by reference: a callback's const std::vector<std::string> & binds to no other.
4
+ const CALLBACK_ARGUMENT_AT = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
5
+ using Arg = std::remove_reference_t<decltype(std::get<I>(values))>;
6
+ using A = std::remove_cv_t<Arg>;
7
+ if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
8
+ using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
9
+ if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
10
+ else return callbackArgument<Arg>(std::get<I>(values));
11
+ } else return callbackArgument<Arg>(std::get<I>(values));
12
+ }`;
13
+ // The form a bridge cached before the argument kept its const still holds.
14
+ const CALLBACK_ARGUMENT_AT_WITHOUT_CONST = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
15
+ using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
16
+ if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
17
+ using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
18
+ if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
19
+ else return callbackArgument<A>(std::get<I>(values));
20
+ } else return callbackArgument<A>(std::get<I>(values));
21
+ }`;
22
+
1
23
  // Adapt the runtime emitted by the pinned SWIG fork. Kept in the generator so cache rebuilds
2
24
  // produce the same runtime in every bridge, including builds on a remote runner.
3
25
  export default function fixBridgeRuntime(text) {
@@ -22,16 +44,7 @@ template<typename T> constexpr bool isPassedByAddress =`);
22
44
  }
23
45
  if (!text.includes('auto callbackArgumentAt(')) {
24
46
  text = '#include <tuple>\n' + text;
25
- text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {', `// A const char* followed by an integer may be a byte span, with embedded NULs or no terminator.
26
- // Preserve its address; JavaScript can read exactly the reported length with readBuffer.
27
- template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
28
- using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
29
- if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
30
- using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
31
- if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
32
- else return callbackArgument<A>(std::get<I>(values));
33
- } else return callbackArgument<A>(std::get<I>(values));
34
- }
47
+ text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {', `${CALLBACK_ARGUMENT_AT}
35
48
  #ifndef __EMSCRIPTEN__
36
49
  struct RuntimeCallbacks {`);
37
50
  text = text.replaceAll('template<typename R, typename... A> R invokeCallback(size_t slot, A... args) {',
@@ -44,5 +57,5 @@ struct RuntimeCallbacks {`);
44
57
  }
45
58
  template<typename F> struct Callback {`);
46
59
  }
47
- return text;
60
+ return text.replace(CALLBACK_ARGUMENT_AT_WITHOUT_CONST, CALLBACK_ARGUMENT_AT);
48
61
  }
@@ -0,0 +1,42 @@
1
+ // A pointer a member function or property hands out can point into the object it was called on, as a getter of a
2
+ // member behind a unique_ptr does, or name that object, as a fluent call does. JavaScript frees the object once
3
+ // nothing reaches its handle (utils/embindObjectLifetime.js), and the pointer then reads freed memory. The handle of
4
+ // such a pointer holds a clone of the object's handle, on the count the pointer's clones share, so the object goes
5
+ // with the last of them. Only a raw pointer, a NativePointer and an emscripten::val can borrow: a copy and a smart
6
+ // pointer hold what they point at. Static methods and free functions have no object to hold.
7
+
8
+ const HELPERS = 'function crossbindMayBorrow(type){if(!type)return false;if(type.name==="emscripten::val")return true;'
9
+ + 'var cls=type.registeredClass;return!!cls&&!type.isReference&&(!type.isSmartPointer||cls.name==="NativePointer")}'
10
+ + 'function crossbindKeepOwner(value,owner){var $$=value&&value.$$;var held=owner&&owner.$$;'
11
+ + 'if(!$$||!$$.count||$$.count.owner||!held||!held.ptr||held.preservePointerOnDelete||$$.count===held.count)return value;'
12
+ + 'if($$.smartPtr?$$.ptrType.registeredClass.name==="NativePointer":!$$.count.owned)$$.count.owner=owner.clone();return value}'
13
+ + 'function crossbindKeepsOwner(fn,type){if(!crossbindMayBorrow(type))return fn;'
14
+ + 'var kept=function(...args){var owner=this;var value=fn.apply(owner,args);'
15
+ + 'return value instanceof Promise?value.then(resolved=>crossbindKeepOwner(resolved,owner)):crossbindKeepOwner(value,owner)};'
16
+ + 'Object.defineProperty(kept,"name",{value:fn.name});return kept}';
17
+
18
+ // The release glue spells each on one line, the debug glue with spaces and quoted property names. A release glue
19
+ // names embind's registrations by the minified keys of the wasm imports, so a glue that registers class methods is
20
+ // known by the pure virtual check of the method registration, and one that registers properties by the getter's
21
+ // `this` check.
22
+ const CRAFT = /function craftInvokerFunction\s*\(/;
23
+ const MEMBER = /(var memberFunction\s*=\s*)(craftInvokerFunction\(\s*humanName,\s*argTypes,\s*classType,\s*rawInvoker,\s*context(?:,\s*isAsync)?\s*\))(\s*;)/g;
24
+ const GETTER = /return (getterReturnType(?:\.fromWireType|\[['"]fromWireType['"]\])\(\s*getter\(\s*getterContext,\s*ptr\s*\)\s*\))/g;
25
+ const METHODS = /pureVirtualFunctions\.push\(\s*methodName\s*\)/;
26
+ const PROPERTIES = /humanName\s*\+\s*['"] getter['"]/;
27
+
28
+ const count = (text, pattern) => text.match(pattern)?.length ?? 0;
29
+
30
+ // Returns the rewritten glue and whether it binds class methods or properties this rewrite does not recognise.
31
+ export function keepPointerOwners(glue) {
32
+ const hasMethods = METHODS.test(glue);
33
+ const hasProperties = PROPERTIES.test(glue);
34
+ if (!hasMethods && !hasProperties) return { text: glue, missed: false };
35
+ const recognised = CRAFT.test(glue) && (!hasMethods || count(glue, MEMBER) === 1) && (!hasProperties || count(glue, GETTER) === 1);
36
+ if (!recognised) return { text: glue, missed: true };
37
+ const text = glue
38
+ .replace(CRAFT, (craft) => `${HELPERS}${craft}`)
39
+ .replace(MEMBER, (_, assign, call, end) => `${assign}crossbindKeepsOwner(${call},argTypes[0])${end}`)
40
+ .replace(GETTER, (_, read) => `return crossbindKeepOwner(${read},this)`);
41
+ return { text, missed: false };
42
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { getContentHash } from './hash.js';
3
3
  import { recipePatchesFingerprint } from './recipePatches.js';
4
+ import { ANDROID_STL } from './androidToolchain.js';
4
5
 
5
6
  // The prebuilt-lib cache in buildLib is existence-only, so a nativeVersion or recipe-sha256 bump
6
7
  // would otherwise be served from the previous build - shipping the old upstream under the new
@@ -10,8 +11,10 @@ export const SOURCE_FINGERPRINT_FILE = 'crossbind-source.fingerprint';
10
11
  export function getSourceFingerprint(config, target) {
11
12
  const nativeVersion = config?.package?.nativeVersion || null;
12
13
  const sha256 = config?.build?.sha256 || null;
14
+ // An Android library built while it linked libc++ in builds again against the shared runtime, of local sources too.
15
+ const androidRuntime = target?.platform === 'android' ? ANDROID_STL : null;
13
16
  // Local-source packages pin neither; buildLib checks their native source inventory instead.
14
- if (!nativeVersion && !sha256) return null;
17
+ if (!nativeVersion && !sha256 && !androidRuntime) return null;
15
18
  const sourceReplacements = target ? config?.build?.sourceReplaceList?.(target, config.allDependencyPaths?.[target.path] ?? {}) : undefined;
16
19
  const patches = recipePatchesFingerprint(config?.build?.patches);
17
20
  return getContentHash(JSON.stringify({
@@ -19,6 +22,7 @@ export function getSourceFingerprint(config, target) {
19
22
  sha256,
20
23
  ...(sourceReplacements?.length ? { sourceReplacements } : {}),
21
24
  ...(patches ? { patches } : {}),
25
+ ...(androidRuntime ? { androidRuntime } : {}),
22
26
  }));
23
27
  }
24
28
 
@@ -65,7 +65,7 @@ export function interfaceIncludes(headerPath, prelude = []) {
65
65
  // wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
66
66
  export function buildInterfaceContent({
67
67
  moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [], preamble = [], functions = ALL_NAMES,
68
- extras = [],
68
+ classes = ALL_NAMES, extras = [],
69
69
  }) {
70
70
  const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
71
71
  .map((header) => `#include "${header}"`)
@@ -89,7 +89,7 @@ ${extraBlock}%{
89
89
  ${includes}
90
90
  %}
91
91
 
92
- ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
92
+ ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}${classRequests(classes)}%feature("shared_ptr");
93
93
  %feature("polymorphic_shared_ptr");
94
94
 
95
95
  ${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
@@ -104,17 +104,32 @@ function constantRequests(constants) {
104
104
  return constants.length ? `${constants.map((name) => `%feature("embind:constant") ${name};`).join('\n')}\n\n` : '';
105
105
  }
106
106
 
107
- // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. The
108
- // package's own ignores come after, since SWIG applies the later of two renames of a name.
107
+ // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. A
108
+ // function declared through a function typedef (OpenSSL's `OSSL_provider_init_fn OSSL_provider_init;`) reads as a
109
+ // variable, so global variables are ignored too, as rule 10 binds a mutable one only when imported. The package's own
110
+ // ignores come after, since SWIG applies the later of two renames of a name.
109
111
  function functionRequests(functions) {
110
112
  if (functions === ALL_NAMES) return '';
111
113
  const names = [...new Set(functions)].sort();
112
- return `%rename($ignore, %$isfunction, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
114
+ return `%rename($ignore, %$isfunction, %$isglobal) "";\n%rename($ignore, %$isvariable, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
113
115
  }
114
116
 
115
- // Editor declarations need the whole header even when the compiled bridge selects functions by import.
117
+ // SWIG ignores every class, then wraps the classes the app's imports reach (see typeClosure.js) again. A class outside
118
+ // any namespace is wrapped as a class only, so a C function sharing its name (struct stat and stat()) stays out; SWIG
119
+ // matches a namespace's class by its full name only without that condition.
120
+ function classRequests(classes) {
121
+ if (classes === ALL_NAMES) return '';
122
+ const lines = [...new Set(classes)].sort()
123
+ .map((name) => (name.includes('::') ? `%rename("%s") ${name};\n` : `%rename("%s", %$isclass) ${name};\n`));
124
+ return `%rename($ignore, %$isclass) "";\n${lines.join('')}\n`;
125
+ }
126
+
127
+ // Editor declarations and SWIG's parse for the bindings plan need the whole header even when the compiled bridge
128
+ // selects functions and classes by import.
116
129
  export function interfaceWithAllFunctions(content) {
117
- return content.replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '');
130
+ return content
131
+ .replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\(\$ignore, %\$isvariable, %\$isglobal\) "";\n)?(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '')
132
+ .replace(/%rename\(\$ignore, %\$isclass\) "";\n(?:%rename\("%s"(?:, %\$isclass)?\) [A-Za-z_][\w:]*;\n)*\n/g, '');
118
133
  }
119
134
 
120
135
  export function withoutSwigMacros(content) {
@@ -3,3 +3,23 @@
3
3
  export default function toolchainNoticesDir(platform) {
4
4
  return `toolchain-licenses/${platform}`;
5
5
  }
6
+
7
+ // What a wasm build copies there (actions/wasmNotices.js).
8
+ export const WASM_NOTICES = {
9
+ emscriptenText: 'emscripten-LICENSE',
10
+ llvmText: 'llvm-LICENSE.TXT',
11
+ emscriptenVersion: 'emscripten-version.txt',
12
+ llvmVersion: 'clang-version.txt',
13
+ image: 'image.txt',
14
+ };
15
+
16
+ // What a Linux, Windows or Android link copies there (actions/nativeNotices.js), beside a Windows image's mingw-w64
17
+ // notices, which keep their names.
18
+ export const NATIVE_NOTICES = {
19
+ llvmText: 'llvm-LICENSE.TXT',
20
+ clangVersion: 'clang-version.txt',
21
+ libcxxVersion: 'libcxx-version.txt',
22
+ mingwVersion: 'mingw-w64-version.txt',
23
+ };
24
+
25
+ export const MINGW_NOTICE = /^COPYING/;
@@ -0,0 +1,170 @@
1
+ import { ALL_NAMES } from './headerImports.js';
2
+
3
+ // SWIG's own parse of a header (swig -xml), read for the classes a bridge has to register. embind registers each class
4
+ // once, in the bridge of the header defining it, and a binding naming a class no bridge registers fails only when it is
5
+ // called. So a header binds the classes its imported names reach: through their signatures, the bases of those classes
6
+ // and the signatures of their public members, in this header and in the headers defining them.
7
+
8
+ // SWIG leaves `>` unescaped inside attribute values (std::vector&lt;(T)>), so a tag ends only outside a quoted value.
9
+ const TAG = /<(\/?)([A-Za-z_][\w-]*)((?:\s+[\w:-]+="[^"]*")*)\s*(\/?)>/g;
10
+ const ATTRIBUTE = /([\w:-]+)="([^"]*)"/g;
11
+ const NAME = /[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*/g;
12
+ const ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
13
+ // SWIG's type strings spell pointers, references, qualifiers, functions and arrays as p. r. z. q(...). f(...). a(...).
14
+ const NOT_TYPES = new Set([
15
+ 'p', 'r', 'z', 'q', 'f', 'a', 'const', 'volatile', 'struct', 'class', 'union', 'enum', 'typename',
16
+ 'unsigned', 'signed', 'char', 'short', 'int', 'long', 'float', 'double', 'void', 'bool', 'wchar_t', 'size_t',
17
+ ]);
18
+ const EMPTY_TREE = { types: new Map(), values: new Map(), enumerators: new Map() };
19
+
20
+ const decode = (text) => text.replace(/&(lt|gt|amp|quot|apos);/g, (entity, name) => ENTITIES[name]);
21
+
22
+ function parse(xml) {
23
+ const root = { tag: 'root', attributes: {}, children: [] };
24
+ const open = [root];
25
+ for (const [, closing, tag, rest, selfClosing] of xml.matchAll(TAG)) {
26
+ if (closing) {
27
+ open.pop();
28
+ continue;
29
+ }
30
+ const attributes = Object.fromEntries([...rest.matchAll(ATTRIBUTE)].map(([, key, value]) => [key, decode(value)]));
31
+ const node = { tag, attributes, children: [] };
32
+ open.at(-1).children.push(node);
33
+ if (!selfClosing) open.push(node);
34
+ }
35
+ return root;
36
+ }
37
+
38
+ // A node's own attributes are the <attribute> elements of its first <attributelist>.
39
+ const listOf = (node) => node.children.find((child) => child.tag === 'attributelist')?.children ?? [];
40
+ const attributesOf = (node) => Object.fromEntries(listOf(node).filter((child) => child.tag === 'attribute')
41
+ .map((child) => [child.attributes.name, child.attributes.value]));
42
+ const basesOf = (node) => listOf(node).filter((child) => child.tag === 'baselist')
43
+ .flatMap((list) => list.children.filter((child) => child.tag === 'base').map((base) => base.attributes.name));
44
+ const parametersOf = (node) => listOf(node).filter((child) => child.tag === 'parmlist')
45
+ .flatMap((list) => list.children.filter((child) => child.tag === 'parm').map((parm) => attributesOf(parm).type ?? ''));
46
+
47
+ // What a header declares: its classes, enums and typedefs by qualified name, each with the type strings it uses, its free
48
+ // functions and variables by name, and the enum of each enumerator. SWIG binds no nested class and no member a class
49
+ // keeps private, so neither reaches anything.
50
+ export function readSwigTree(xml) {
51
+ const types = new Map();
52
+ const values = new Map();
53
+ const enumerators = new Map();
54
+ // C names a struct and its typedef alike (`typedef struct PJ_INFO PJ_INFO;` ahead of `struct PJ_INFO {...}`), and the
55
+ // name stands for the struct.
56
+ const declare = (name, entry) => {
57
+ if (!types.has(name) || (types.get(name).kind === 'typedef' && entry.kind !== 'typedef')) types.set(name, entry);
58
+ };
59
+ const visit = (node, namespace, owner) => {
60
+ // %$isclass leaves class templates alone, so the selection does too.
61
+ if (node.tag === 'template') return;
62
+ const own = node.tag === 'root' ? {} : attributesOf(node);
63
+ const isPublic = own.access !== 'private' && own.access !== 'protected';
64
+ // SWIG spells a class or enum by its full name; a typedef inside a namespace or class may be spelled without it.
65
+ const qualified = (name) => (name.includes('::') || !namespace.length ? name : [...namespace, name].join('::'));
66
+ if (node.tag === 'namespace') {
67
+ node.children.forEach((child) => visit(child, own.name ? [...namespace, own.name] : namespace, null));
68
+ } else if (node.tag === 'class') {
69
+ if (!own.name || !isPublic || own.nested === '1') return;
70
+ const name = qualified(own.name);
71
+ // The name SWIG binds it under: a C struct's typedef name (X509V3_CONF_METHOD for X509V3_CONF_METHOD_st).
72
+ const bound = own.sym_name ?? own['sym:name'] ?? own.tdname ?? name.split('::').at(-1);
73
+ const entry = { kind: 'class', scope: name.split('::'), bases: basesOf(node), uses: [], bound };
74
+ declare(name, entry);
75
+ node.children.forEach((child) => visit(child, namespace, entry));
76
+ } else if (node.tag === 'enum') {
77
+ if (!own.name || !isPublic) return;
78
+ const name = owner && !own.name.includes('::') ? [...owner.scope, own.name].join('::') : qualified(own.name);
79
+ declare(name, { kind: 'enum', scope: name.split('::'), bases: [], uses: [] });
80
+ node.children.filter((child) => child.tag === 'enumitem').forEach((item) => {
81
+ const enumerator = attributesOf(item).name;
82
+ if (enumerator && !enumerators.has(enumerator)) enumerators.set(enumerator, name);
83
+ });
84
+ } else if (node.tag === 'cdecl') {
85
+ if (!own.name || !isPublic || own.storage === 'friend') return;
86
+ const uses = [own.type ?? '', own.decl ?? '', ...parametersOf(node)];
87
+ if (own.storage === 'typedef') {
88
+ const name = owner ? [...owner.scope, own.name].join('::') : qualified(own.name);
89
+ declare(name, { kind: 'typedef', scope: name.split('::'), bases: [], uses });
90
+ } else if (owner) {
91
+ owner.uses.push(...uses);
92
+ } else {
93
+ // An import of an overloaded name binds every overload.
94
+ values.set(own.name, [...(values.get(own.name) ?? []), ...uses]);
95
+ }
96
+ } else if (node.tag === 'constructor') {
97
+ if (owner && isPublic) owner.uses.push(own.decl ?? '', ...parametersOf(node));
98
+ } else {
99
+ node.children.forEach((child) => visit(child, namespace, owner));
100
+ }
101
+ };
102
+ visit(parse(xml), [], null);
103
+ return { types, values, enumerators };
104
+ }
105
+
106
+ // The classes and enums `names` reach from `header`, by qualified name, each with its kind and the header defining it.
107
+ // `treeOf` reads a header's parse; a name no header read so far declares is looked up through `definingHeader`, whose
108
+ // header is read in turn. ALL_NAMES starts from everything the header declares, as a header bound whole does.
109
+ export function typeClosure(header, names, { treeOf, definingHeader }) {
110
+ const trees = new Map();
111
+ const read = (file) => {
112
+ if (!trees.has(file)) trees.set(file, treeOf(file) ?? EMPTY_TREE);
113
+ return trees.get(file);
114
+ };
115
+ const root = read(header);
116
+ const lookedUp = new Set();
117
+ const inTrees = (candidates, wanted) => {
118
+ for (const candidate of candidates) {
119
+ for (const [file, tree] of trees) {
120
+ const entry = tree.types.get(candidate);
121
+ if (entry && wanted(entry)) return { name: candidate, file, entry };
122
+ }
123
+ }
124
+ return null;
125
+ };
126
+ const isClassOrEnum = (entry) => entry.kind !== 'typedef';
127
+ // A class or enum wins over a typedef of the same name, even one another header declares first (a forward header's
128
+ // `typedef struct Foo Foo;`), so a header that may define the name is read before a typedef stands for it.
129
+ const find = (candidates) => {
130
+ const found = inTrees(candidates, isClassOrEnum);
131
+ if (found) return found;
132
+ for (const candidate of candidates) {
133
+ if (lookedUp.has(candidate)) continue;
134
+ lookedUp.add(candidate);
135
+ const file = definingHeader(candidate);
136
+ if (file && !trees.has(file)) read(file);
137
+ }
138
+ return inTrees(candidates, isClassOrEnum) ?? inTrees(candidates, () => true);
139
+ };
140
+ const queue = [];
141
+ const reach = (text, scope) => {
142
+ for (const [name] of text.matchAll(NAME)) {
143
+ if (!NOT_TYPES.has(name)) queue.push({ name, scope });
144
+ }
145
+ };
146
+ const start = names === ALL_NAMES ? [...root.values.keys(), ...root.types.keys()] : names;
147
+ for (const name of start) {
148
+ if (root.values.has(name)) root.values.get(name).forEach((use) => reach(use, []));
149
+ else queue.push({ name: root.enumerators.get(name) ?? name, scope: [] });
150
+ }
151
+ const reached = new Map();
152
+ const visited = new Set();
153
+ while (queue.length) {
154
+ const { name, scope } = queue.pop();
155
+ // As in C++ lookup, a name used inside a class or namespace is tried there first.
156
+ const candidates = [...scope.map((_, depth) => [...scope.slice(0, scope.length - depth), name].join('::')), name];
157
+ const found = find(candidates);
158
+ if (!found || visited.has(found.name)) continue;
159
+ visited.add(found.name);
160
+ const { entry } = found;
161
+ if (entry.kind === 'class') reached.set(found.name, { kind: entry.kind, header: found.file, bound: entry.bound });
162
+ else if (entry.kind === 'enum') reached.set(found.name, { kind: entry.kind, header: found.file });
163
+ const enclosing = entry.scope.slice(0, -1);
164
+ // SWIG binds an enum declared in a class only with that class.
165
+ if (entry.kind === 'enum' && enclosing.length) queue.push({ name: enclosing.join('::'), scope: [] });
166
+ entry.bases.forEach((base) => queue.push({ name: base, scope: enclosing }));
167
+ entry.uses.forEach((use) => reach(use, entry.kind === 'class' ? entry.scope : enclosing));
168
+ }
169
+ return reached;
170
+ }