cg-code-graph 0.10.1__py3-none-any.whl

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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,2652 @@
1
+ // TypeScript / Vue SFC fact extractor for code-graph.
2
+ // Uses the TypeScript compiler API (exact symbol resolution with the project's tsconfig, including
3
+ // Nuxt's generated .nuxt types for auto-imports) and @vue/compiler-sfc for .vue files.
4
+ //
5
+ // .vue files become virtual "<file>.vue.ts" sources: script blocks are kept at their original
6
+ // offsets (everything else blanked, so line numbers stay 1:1), and template expressions /
7
+ // component tags are appended as stub functions with a line map back to the template.
8
+ //
9
+ // Usage: node extract.mjs --config cfg.json (cfg: {root, tsconfig, components_dts, kinds, out})
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import ts from 'typescript'
13
+ import { parse as parseSFC } from '@vue/compiler-sfc'
14
+ import { collectFrameworkFacts } from './fw.mjs'
15
+
16
+ const t0 = Date.now()
17
+ const cfg = JSON.parse(fs.readFileSync(process.argv[process.argv.indexOf('--config') + 1], 'utf8'))
18
+ const ROOT = path.resolve(cfg.root)
19
+ const rel = p => path.relative(ROOT, p).split(path.sep).join('/')
20
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options'])
21
+ const FETCH_FNS = new Set(['$fetch', 'useFetch', 'useLazyFetch', 'ofetch', 'fetch'])
22
+ const I18N_FNS = new Set(['t', '$t', 'te', '$te', 'tm', 'rt'])
23
+ const SKIP_FILE = /(\.test|\.spec)\.(ts|js|mts)$/
24
+ // directory rules from the presets and .cg.yaml (codegraph/core/paths.py PathRules.extractor_cfg): names the walks
25
+ // skip, names kept (hidden directories are skipped unless kept), `include` directories walked although a skip rule
26
+ // covers them, and the directories whose files the tsconfig pulls in as resolution input only (node_modules, .nuxt)
27
+ const SKIP_NAMES = new Set(cfg.skip_names || [])
28
+ const KEEP_NAMES = new Set(cfg.keep_names || [])
29
+ const TEST_SKIP_NAMES = new Set([...SKIP_NAMES, ...(cfg.test_skip_names || [])])
30
+ const INCLUDE = (cfg.include || []).map(i => i.replace(/^\/+|\/+$/g, '')).filter(Boolean)
31
+ const included = r => INCLUDE.some(i => r === i || r.startsWith(i + '/'))
32
+ const onIncludePath = r => included(r) || INCLUDE.some(i => i.startsWith(r + '/'))
33
+ const esc = x => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
34
+ const SOURCE_SKIP_RE = (cfg.source_skip_names || []).length ? new RegExp('(^|/)(' + cfg.source_skip_names.map(esc).join('|') + ')/') : null
35
+ // a file below a directory the walks skip by name (dist/, coverage/, node_modules/, .nuxt/ ...) is not project
36
+ // source, also when the tsconfig lists it, unless it is inside an `include` directory
37
+ const belowSkipped = r => !included(r) && r.split('/').slice(0, -1).some(x => SKIP_NAMES.has(x))
38
+ const sourceSkipped = r => belowSkipped(r) || (!!(SOURCE_SKIP_RE && SOURCE_SKIP_RE.test(r)) && !included(r))
39
+ // a directory entry of a walk: skipped by name / as hidden, unless it is on the way to an `include` directory;
40
+ // `forced`: the parent is skipped by itself and only walked to reach an include directory
41
+ const skipDir = (name, r, names, forced) => !onIncludePath(r) && (forced || names.has(name) || (name.startsWith('.') && !KEEP_NAMES.has(name)))
42
+ const forcedBelow = (name, r, names, forced) => !included(r) && (forced || names.has(name) || (name.startsWith('.') && !KEEP_NAMES.has(name)))
43
+ // framework plugins may exclude more (tests, build output) by a regex over the repo-relative path
44
+ const SKIP_REL = cfg.skip_re ? new RegExp(cfg.skip_re) : null
45
+ // the project's .cg.yaml exclude globs and skip_dirs.add names: never indexed, test code included
46
+ const EXCLUDE_REL = cfg.exclude_re ? new RegExp(cfg.exclude_re) : null
47
+ // generated / copied / vendored files the Python-side scan classified (codegraph/core/generated.py)
48
+ const EXCLUDE_FILES = new Set(cfg.exclude_files || [])
49
+ // directories the generated-file classifier excludes as a whole (not inside an `include` directory)
50
+ const GENERATED_REL = cfg.generated_re ? new RegExp(cfg.generated_re) : null
51
+ const excludedRel = r => !!(EXCLUDE_REL && EXCLUDE_REL.test(r)) || (!included(r.replace(/\/$/, '')) && (!!(GENERATED_REL && GENERATED_REL.test(r)) || EXCLUDE_FILES.has(r)))
52
+ const SRC_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/
53
+ // test code (Vitest / Jest / Playwright / Cypress): indexed as test nodes unless cfg.index_tests === false; kept out of
54
+ // the application graph by the indexer (TEST_* edges)
55
+ const INDEX_TESTS = cfg.index_tests !== false
56
+ const TEST_FILE_RE = /\.(test|spec|e2e-spec|e2e|cy)\.(t|j)sx?$/
57
+ const TEST_DIR_RE = /(^|\/)(__tests__|e2e|tests?|cypress|playwright)\//
58
+ const TEST_ROOTS = ['e2e', 'test', 'tests', 'cypress', 'playwright', 'src/test', 'src/tests', 'src/e2e']
59
+ const isTestRel = r => TEST_FILE_RE.test(r) || /(^|\/)__tests__\//.test(r) || TEST_ROOTS.some(d => r.startsWith(d + '/'))
60
+
61
+ // ---------- file kinds (from the framework plugin: [[prefix, kind], ...]) ----------
62
+ function fileKind(r) {
63
+ for (const [prefix, kind] of cfg.kinds || []) if (r.startsWith(prefix)) return kind
64
+ return r.endsWith('.vue') ? 'component' : 'module'
65
+ }
66
+
67
+ // ---------- global components (Nuxt .nuxt/types/components.d.ts) ----------
68
+ const globalComponents = {}
69
+ if (cfg.components_dts && fs.existsSync(cfg.components_dts)) {
70
+ const txt = fs.readFileSync(cfg.components_dts, 'utf8')
71
+ const re = /^\s*'?(\w+)'?: (?:LazyComponent<)?typeof import\("([^"]+)"\)/gm
72
+ let m
73
+ while ((m = re.exec(txt))) {
74
+ if (m[1].startsWith('Lazy') && globalComponents[m[1].slice(4)]) { globalComponents[m[1]] = globalComponents[m[1].slice(4)]; continue }
75
+ globalComponents[m[1]] = path.resolve(path.dirname(cfg.components_dts), m[2])
76
+ }
77
+ }
78
+ const pascal = s => s.split('-').map(x => x ? x[0].toUpperCase() + x.slice(1) : '').join('')
79
+
80
+ // ---------- tsconfig ----------
81
+ // tsconfig.json, else jsconfig.json; a solution-style config (`files: []` + references) uses its referenced
82
+ // configs; with no config at all (plain JS projects) the source dirs are walked with allowJs defaults.
83
+ function parseConfig(p) {
84
+ try { return ts.parseJsonConfigFileContent(ts.readConfigFile(p, ts.sys.readFile).config || {}, ts.sys, path.dirname(p), undefined, p) } catch { return null }
85
+ }
86
+ let tsconfigPath = path.resolve(ROOT, cfg.tsconfig || 'tsconfig.json')
87
+ if (!fs.existsSync(tsconfigPath) && fs.existsSync(path.join(ROOT, 'jsconfig.json'))) tsconfigPath = path.join(ROOT, 'jsconfig.json')
88
+ let parsed = fs.existsSync(tsconfigPath) ? parseConfig(tsconfigPath) : null
89
+ if (parsed && !parsed.fileNames.length && parsed.projectReferences && parsed.projectReferences.length) {
90
+ for (const ref of parsed.projectReferences) {
91
+ let rp = ref.path
92
+ if (fs.existsSync(rp) && fs.statSync(rp).isDirectory()) rp = path.join(rp, 'tsconfig.json')
93
+ const sub = fs.existsSync(rp) ? parseConfig(rp) : null
94
+ if (sub && sub.fileNames.length) { parsed = { ...sub, fileNames: [...new Set([...parsed.fileNames, ...sub.fileNames])], options: { ...sub.options, ...parsed.options, paths: parsed.options.paths || sub.options.paths, baseUrl: parsed.options.baseUrl || sub.options.baseUrl, pathsBasePath: parsed.options.pathsBasePath || sub.options.pathsBasePath } } }
95
+ }
96
+ }
97
+ // no root config, but per-package tsconfigs (a lerna / nx / workspaces monorepo; the Python side lists them): one
98
+ // program over all their files, each package's `paths` kept with absolute targets
99
+ const pkgDirs = []
100
+ if (!parsed && Array.isArray(cfg.package_tsconfigs) && cfg.package_tsconfigs.length) {
101
+ const names = new Set(), paths = {}
102
+ let base = null
103
+ for (const r of cfg.package_tsconfigs) {
104
+ const p = path.resolve(ROOT, r)
105
+ const sub = fs.existsSync(p) ? parseConfig(p) : null
106
+ if (!sub || !sub.fileNames.length) continue
107
+ pkgDirs.push(path.dirname(p))
108
+ for (const f of sub.fileNames) names.add(f)
109
+ const pb = sub.options.pathsBasePath || sub.options.baseUrl || path.dirname(p)
110
+ for (const [k, v] of Object.entries(sub.options.paths || {})) if (!paths[k]) paths[k] = v.map(x => path.resolve(pb, x))
111
+ if (!base) base = sub
112
+ }
113
+ if (base) {
114
+ const o = { ...base.options }
115
+ for (const k of ['rootDir', 'outDir', 'declarationDir', 'composite', 'baseUrl', 'paths', 'pathsBasePath', 'tsBuildInfoFile']) delete o[k]
116
+ if (Object.keys(paths).length) { o.paths = paths; o.pathsBasePath = ROOT }
117
+ parsed = { options: o, fileNames: [...names], packageConfigs: cfg.package_tsconfigs.length }
118
+ tsconfigPath = path.join(ROOT, 'tsconfig.json')
119
+ }
120
+ }
121
+ const noConfig = !parsed
122
+ if (!parsed) parsed = { options: {}, fileNames: [] }
123
+ const options = { ...parsed.options, noEmit: true, skipLibCheck: true }
124
+ if (noConfig || cfg.allow_js) Object.assign(options, { allowJs: true, checkJs: false, maxNodeModuleJsDepth: 0 })
125
+ if (noConfig) Object.assign(options, { jsx: ts.JsxEmit.Preserve, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
126
+ target: ts.ScriptTarget.ESNext, resolveJsonModule: true, esModuleInterop: true, allowSyntheticDefaultImports: true })
127
+ if (options.moduleResolution === ts.ModuleResolutionKind.Classic) options.moduleResolution = ts.ModuleResolutionKind.Bundler
128
+ // React Native / Expo platform files (storage.ios.ts, storage.android.ts): `import './storage'` resolves the way Metro
129
+ // does (the project's own tsconfig moduleSuffixes win); the Python side links the sibling variants
130
+ if (cfg.platform_suffixes && !options.moduleSuffixes) options.moduleSuffixes = cfg.platform_suffixes
131
+ const pathsBase = options.pathsBasePath || options.baseUrl || path.dirname(tsconfigPath)
132
+ // local packages (package.json `"x": "file:./libraries/x"` / `link:` dependencies, workspace packages) with no installed
133
+ // node_modules link: imports of them resolve to the package source, as the package manager's symlink would
134
+ const localPkgs = []
135
+ {
136
+ const readJson = f => { try { return JSON.parse(fs.readFileSync(f, 'utf8')) } catch { return null } }
137
+ const pj = readJson(path.join(ROOT, 'package.json'))
138
+ const dirs = []
139
+ if (pj) {
140
+ for (const [name, spec] of Object.entries({ ...(pj.dependencies || {}), ...(pj.devDependencies || {}) })) {
141
+ const m = typeof spec === 'string' && spec.match(/^(?:file|link):(.+)$/)
142
+ if (m) dirs.push([name, path.resolve(ROOT, m[1])])
143
+ }
144
+ const ws = Array.isArray(pj.workspaces) ? pj.workspaces : ((pj.workspaces || {}).packages || [])
145
+ for (const w of ws) {
146
+ if (typeof w !== 'string' || !/^[\w@./-]+(\/\*)?$/.test(w)) continue
147
+ const base = path.resolve(ROOT, w.replace(/\/\*$/, ''))
148
+ let subs = [base]
149
+ if (w.endsWith('/*')) { try { subs = fs.readdirSync(base, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => path.join(base, e.name)) } catch { subs = [] } }
150
+ for (const d of subs) { const p = readJson(path.join(d, 'package.json')); if (p && p.name) dirs.push([p.name, d]) }
151
+ }
152
+ }
153
+ for (const [name, dir] of dirs) {
154
+ if (fs.existsSync(path.join(ROOT, 'node_modules', name)) || !dir.startsWith(ROOT + path.sep)) continue
155
+ const p = readJson(path.join(dir, 'package.json'))
156
+ if (!p) continue
157
+ const cands = [p['react-native'], p.source, p.main, 'src/index', 'index'].filter(e => typeof e === 'string')
158
+ .map(e => path.resolve(dir, e).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, ''))
159
+ const hit = cands.find(c => ['.ts', '.tsx', '.js', '.jsx'].some(x => fs.existsSync(c + x)))
160
+ if (!hit) continue
161
+ options.paths = { ...(options.paths || {}) }
162
+ if (!options.baseUrl && !options.pathsBasePath) options.pathsBasePath = ROOT
163
+ if (!options.paths[name]) options.paths[name] = [hit]
164
+ if (!options.paths[name + '/*']) options.paths[name + '/*'] = [dir + '/*']
165
+ localPkgs.push(name)
166
+ }
167
+ }
168
+
169
+ // symlinks: a link to a file is indexed, a link to a directory is not followed, a dangling link (e.g. to a file
170
+ // outside the checkout) is skipped with a warning; an unreadable directory is skipped, never fatal
171
+ const skippedLinks = []
172
+ function fileEntry(e, p) {
173
+ if (e.isFile()) return true
174
+ if (!e.isSymbolicLink()) return false
175
+ try { return fs.statSync(p).isFile() } catch (err) {
176
+ skippedLinks.push(rel(p)); process.stderr.write(`codegraph: skipping ${rel(p)}: dangling symlink\n`); return false
177
+ }
178
+ }
179
+ function walkDir(d, out, forced = false) {
180
+ let ents = []
181
+ try { ents = fs.readdirSync(d, { withFileTypes: true }) } catch (err) { process.stderr.write(`codegraph: skipping ${d}: ${err.code || err}\n`); return out }
182
+ for (const e of ents) {
183
+ const p = path.join(d, e.name), r = rel(p)
184
+ if (e.isDirectory() ? skipDir(e.name, r, SKIP_NAMES, forced) : (forced && !included(r))) continue
185
+ if ((SKIP_REL && SKIP_REL.test(r)) || excludedRel(r) || (e.isDirectory() && excludedRel(r + '/'))) continue
186
+ if (e.isDirectory()) walkDir(p, out, forcedBelow(e.name, r, SKIP_NAMES, forced))
187
+ else if (fileEntry(e, p)) out.push(p)
188
+ }
189
+ return out
190
+ }
191
+ let srcDirs = (cfg.src_dirs || ['app']).map(d => path.resolve(ROOT, d)).filter(fs.existsSync)
192
+ // none of the conventional dirs holds code the tsconfig includes: use the top dirs of the tsconfig's own files
193
+ {
194
+ const inside = f => srcDirs.some(d => f.startsWith(d + path.sep))
195
+ const own = (parsed.fileNames || []).filter(f => !sourceSkipped(path.relative(ROOT, f).split(path.sep).join('/')) && !f.endsWith('.d.ts'))
196
+ if (parsed.packageConfigs && pkgDirs.length) {
197
+ // each package directory is a source dir: files next to its tsconfig that `files` / `include` leave out (a
198
+ // bridge script bundled by its own rollup config) are code of the package too
199
+ srcDirs = pkgDirs
200
+ process.stderr.write(`codegraph: source dirs from package tsconfigs: ${pkgDirs.map(rel).join(', ')}\n`)
201
+ } else if (own.length && !own.some(inside)) {
202
+ const tops = new Set(own.map(f => { const r = path.relative(ROOT, f).split(path.sep); return r.length > 1 ? r.slice(0, Math.min(r.length - 1, 3)).join(path.sep) : '.' }))
203
+ const dirs = [...tops].sort((a, b) => a.length - b.length).filter((d, i, all) => !all.slice(0, i).some(p => p === '.' || d.startsWith(p + path.sep)))
204
+ srcDirs = dirs.map(d => path.resolve(ROOT, d))
205
+ process.stderr.write(`codegraph: source dirs from tsconfig: ${dirs.join(', ')}\n`)
206
+ }
207
+ }
208
+ // package.json entry points: with no conventional source dir and no config files to take them from (a plain JS
209
+ // package: lib/ + bin/, no tsconfig), main / module / bin / exports and the `files` entries name the source (an entry
210
+ // in a sub directory makes that top directory a source dir, one at the root is a source file of its own); next to
211
+ // source dirs, a `bin` script outside them (bin/cli.js beside src/) is a source file, so tests that run it have a
212
+ // node to link to (#94)
213
+ const srcFiles = new Set()
214
+ let pkgSrc = false
215
+ {
216
+ let pkg = null
217
+ try { pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')) } catch { pkg = null }
218
+ if (!pkg || typeof pkg !== 'object') pkg = {}
219
+ const strings = v => typeof v === 'string' ? [v] : Array.isArray(v) ? v.flatMap(strings) : v && typeof v === 'object' ? Object.values(v).flatMap(strings) : []
220
+ const usable = e => {
221
+ const r = rel(path.resolve(ROOT, e.split(/[*?{[]/)[0])).replace(/\/$/, '')
222
+ return r && !r.startsWith('..') && !path.isAbsolute(r) && !sourceSkipped(r) && !excludedRel(r) && fs.existsSync(path.resolve(ROOT, r)) ? r : null
223
+ }
224
+ const isSrc = r => SRC_EXT.test(r) && !r.endsWith('.d.ts') && fs.statSync(path.resolve(ROOT, r)).isFile()
225
+ if (!srcDirs.length) {
226
+ const dirs = new Set()
227
+ for (const e of strings([pkg.main, pkg.module, pkg.bin, pkg.exports, pkg.files])) {
228
+ const r = usable(e)
229
+ if (!r) continue
230
+ const segs = r.split('/'), top = path.resolve(ROOT, segs[0])
231
+ if (segs.length > 1 || fs.statSync(top).isDirectory()) {
232
+ if (fs.statSync(top).isDirectory() && !skipDir(segs[0], segs[0], SKIP_NAMES, false)) dirs.add(top)
233
+ } else if (isSrc(r)) srcFiles.add(path.resolve(ROOT, r))
234
+ }
235
+ srcDirs = [...dirs].sort()
236
+ pkgSrc = srcDirs.length > 0
237
+ } else {
238
+ for (const e of strings(pkg.bin)) {
239
+ const r = usable(e), p = r && path.resolve(ROOT, r)
240
+ if (r && isSrc(r) && !srcDirs.some(d => p.startsWith(d + path.sep)) && !belowSkipped(r)) srcFiles.add(p)
241
+ }
242
+ }
243
+ if (pkgSrc || srcFiles.size) {
244
+ process.stderr.write(`codegraph: source from package.json: ${[...srcDirs, ...srcFiles].map(rel).join(', ')}\n`)
245
+ }
246
+ }
247
+ // .cg.yaml include directories are source dirs too (generated sources a project wants in the graph)
248
+ for (const i of INCLUDE) {
249
+ const p = path.resolve(ROOT, i)
250
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !srcDirs.some(d => p === d || p.startsWith(d + path.sep))) srcDirs.push(p)
251
+ }
252
+ const allFiles = [...srcDirs.flatMap(d => walkDir(d, [])), ...srcFiles]
253
+ // test files: spec/test files anywhere in the source dirs, plus top-level test trees (e2e/, tests/, ...)
254
+ const testFiles = new Set()
255
+ function walkTests(d, all, forced = false) {
256
+ let ents = []
257
+ try { ents = fs.readdirSync(d, { withFileTypes: true }) } catch { return }
258
+ for (const e of ents) {
259
+ const p = path.join(d, e.name), r = rel(p)
260
+ if (e.isDirectory() ? skipDir(e.name, r, TEST_SKIP_NAMES, forced) : (forced && !included(r))) continue
261
+ if (excludedRel(r) || (e.isDirectory() && excludedRel(r + '/'))) continue
262
+ if (e.isDirectory()) walkTests(p, all, forcedBelow(e.name, r, TEST_SKIP_NAMES, forced))
263
+ else if (SRC_EXT.test(e.name) && (e.isFile() || (e.isSymbolicLink() && !skippedLinks.includes(rel(p)) && fileEntry(e, p))) && !e.name.endsWith('.d.ts') && (all || isTestRel(rel(p)))) testFiles.add(p)
264
+ }
265
+ }
266
+ if (INDEX_TESTS) {
267
+ for (const d of srcDirs) walkTests(d, false)
268
+ for (const d of TEST_ROOTS) { const p = path.resolve(ROOT, d); if (fs.existsSync(p) && fs.statSync(p).isDirectory()) walkTests(p, true) }
269
+ }
270
+ const vueFiles = allFiles.filter(f => f.endsWith('.vue'))
271
+ const vueSet = new Set(vueFiles)
272
+
273
+ // ---------- virtual sources for .vue ----------
274
+ const virtual = new Map() // virtual path -> text
275
+ const lineMaps = new Map() // virtual path -> {virtualLine(1-based): origLine}
276
+ const sfcInfo = new Map()
277
+ const sfcI18n = {} // rel vue path -> keys defined in <i18n> blocks // vue abs path -> {templateTags:[...], pageMeta}
278
+ const stats = { vue_files: vueFiles.length, ts_files: 0, template_stubs: 0, template_component_tags: 0,
279
+ sfc_errors: 0, components_global: 0, components_imported: 0, components_external: 0, components_unknown: 0, unknown_tags: {} }
280
+
281
+ function blank(src, keep) {
282
+ // keep: [[start,end]] offsets to preserve; everything else -> spaces (newlines kept)
283
+ const out = src.split('')
284
+ let k = 0
285
+ keep.sort((a, b) => a[0] - b[0])
286
+ for (let i = 0; i < out.length; i++) {
287
+ while (k < keep.length && i >= keep[k][1]) k++
288
+ const inKeep = k < keep.length && i >= keep[k][0] && i < keep[k][1]
289
+ if (!inKeep && out[i] !== '\n' && out[i] !== '\r') out[i] = ' '
290
+ }
291
+ return out.join('')
292
+ }
293
+
294
+ function buildVirtualVue(file) {
295
+ let src
296
+ try { src = fs.readFileSync(file, 'utf8') } catch (e) { process.stderr.write(`codegraph: skipping ${rel(file)}: ${e.code || e}\n`); return '' }
297
+ let d
298
+ try { d = parseSFC(src, { filename: file }).descriptor } catch (e) { stats.sfc_errors++; return '' }
299
+ const keep = []
300
+ for (const b of [d.script, d.scriptSetup]) if (b) keep.push([b.loc.start.offset, b.loc.end.offset])
301
+ let text = blank(src, keep)
302
+ const baseLines = text.split('\n').length
303
+ const stubs = [] // [code, origLine]
304
+ const tags = [] // {tag, line, stubIndex}
305
+ let n = 0
306
+ const T = d.template && d.template.ast
307
+ const scopeStack = []
308
+ const pushStub = (code, line) => { stubs.push([code, line]); stats.template_stubs++ }
309
+ const params = () => scopeStack.flat().map(p => `${p}: any`).join(', ')
310
+ function exprStub(exp, line, isHandler) {
311
+ if (!exp || !exp.trim()) return
312
+ const ps = params()
313
+ if (isHandler) pushStub(`function __tpl_${n++}(${ps}${ps ? ', ' : ''}$event: any) { ${exp} }`, line)
314
+ else pushStub(`function __tpl_${n++}(${ps}) { return (${exp}) }`, line)
315
+ }
316
+ function visit(node) {
317
+ if (!node) return
318
+ if (node.type === 5) exprStub(node.content && node.content.content, node.loc.start.line, false) // {{ }}
319
+ if (node.type !== 1 && node.type !== 0) return
320
+ let pushed = 0
321
+ if (node.type === 1) {
322
+ const isComp = node.tagType === 1 || (/[A-Z]/.test(node.tag) || node.tag.includes('-'))
323
+ if (isComp && node.tagType !== 2 && node.tagType !== 3) tags.push({ tag: node.tag, line: node.loc.start.line })
324
+ for (const p of node.props) {
325
+ if (p.type !== 7) continue
326
+ const line = (p.exp && p.exp.loc && p.exp.loc.start.line) || p.loc.start.line
327
+ if (p.name === 'for') {
328
+ const fr = p.forParseResult
329
+ if (fr) {
330
+ exprStub(fr.source && fr.source.content, line, false)
331
+ const vars = [fr.value, fr.key, fr.index].filter(Boolean).map(x => x.content)
332
+ scopeStack.push(vars); pushed++
333
+ }
334
+ } else if (p.name === 'slot') {
335
+ if (p.exp && p.exp.content) { scopeStack.push([p.exp.content]); pushed++ }
336
+ } else if (p.name === 'on') {
337
+ exprStub(p.exp && p.exp.content, line, true)
338
+ } else if (p.exp) {
339
+ exprStub(p.exp.content, line, false)
340
+ }
341
+ if (p.arg && p.arg.type === 4 && !p.arg.isStatic) exprStub(p.arg.content, line, false)
342
+ }
343
+ }
344
+ for (const c of node.children || []) visit(c)
345
+ while (pushed--) scopeStack.pop()
346
+ }
347
+ if (T) visit(T)
348
+ // component tags: emit a reference stub so the checker resolves explicit imports exactly
349
+ for (const tg of tags) {
350
+ const name = pascal(tg.tag)
351
+ tg.name = name
352
+ tg.stub = stubs.length
353
+ pushStub(`function __tplc_${n++}() { return typeof ${/^[A-Za-z_$][\w$]*$/.test(name) ? name : 'undefined'} }`, tg.line)
354
+ stats.template_component_tags++
355
+ }
356
+ const lm = {}
357
+ let out = text
358
+ stubs.forEach(([code, line], i) => { lm[baseLines + 1 + i] = line; out += '\n' + code.replace(/\n/g, ' ') })
359
+ const i18nKeys = []
360
+ for (const cb of d.customBlocks || []) {
361
+ if (cb.type !== 'i18n') continue
362
+ try {
363
+ let data = JSON.parse(cb.content)
364
+ const ks = Object.keys(data)
365
+ if (ks.length && ks.every(k => /^[a-z]{2}(-[A-Z]{2})?$/.test(k))) data = Object.assign({}, ...ks.map(k => data[k]))
366
+ const flat = (o, p) => { for (const [k, v] of Object.entries(o)) { const q = p ? p + '.' + k : k; if (v && typeof v === 'object') flat(v, q); else i18nKeys.push(q) } }
367
+ flat(data, '')
368
+ } catch { stats.sfc_i18n_unparsed = (stats.sfc_i18n_unparsed || 0) + 1 }
369
+ }
370
+ if (i18nKeys.length) sfcI18n[rel(file)] = [...new Set(i18nKeys)]
371
+ sfcInfo.set(file, { tags, baseLines, hasSetup: !!d.scriptSetup })
372
+ const vp = file + '.ts'
373
+ lineMaps.set(vp, lm)
374
+ return out
375
+ }
376
+ for (const f of vueFiles) virtual.set(f + '.ts', buildVirtualVue(f))
377
+
378
+ // ---------- compiler host ----------
379
+ const host = ts.createCompilerHost(options, true)
380
+ const origGetSourceFile = host.getSourceFile.bind(host)
381
+ const origFileExists = host.fileExists.bind(host)
382
+ const origReadFile = host.readFile.bind(host)
383
+ host.getSourceFile = (fn, lang, onErr, should) => virtual.has(fn)
384
+ ? ts.createSourceFile(fn, virtual.get(fn), lang, true, ts.ScriptKind.TS)
385
+ : origGetSourceFile(fn, lang, onErr, should)
386
+ host.fileExists = fn => virtual.has(fn) || origFileExists(fn)
387
+ host.readFile = fn => virtual.has(fn) ? virtual.get(fn) : origReadFile(fn)
388
+
389
+ function resolveVuePath(spec, containing) {
390
+ let cands = []
391
+ if (spec.startsWith('.')) cands.push(path.resolve(path.dirname(containing), spec))
392
+ else for (const [pat, targets] of Object.entries(options.paths || {})) {
393
+ const star = pat.indexOf('*')
394
+ if (star < 0 ? spec === pat : spec.startsWith(pat.slice(0, star))) {
395
+ const rest = star < 0 ? '' : spec.slice(star)
396
+ for (const t of targets) cands.push(path.resolve(pathsBase, t.replace('*', rest)))
397
+ }
398
+ }
399
+ return cands.find(c => vueSet.has(c)) || null
400
+ }
401
+ host.resolveModuleNameLiterals = (lits, containing, redirected, opts) => lits.map(l => {
402
+ const spec = l.text
403
+ const containingReal = containing.endsWith('.vue.ts') ? containing.slice(0, -3) : containing
404
+ if (spec.endsWith('.vue')) {
405
+ const p = resolveVuePath(spec, containingReal)
406
+ if (p) return { resolvedModule: { resolvedFileName: p + '.ts', extension: ts.Extension.Ts, isExternalLibraryImport: false } }
407
+ }
408
+ return ts.resolveModuleName(spec, containingReal, opts, host)
409
+ })
410
+
411
+ const SKIP_ROOT = /(\.test|\.spec)\.(ts|tsx|js|jsx|mts)$/
412
+ let configFiles = parsed.fileNames
413
+ // files outside the config's include list but inside the source dirs (JS projects, partial includes)
414
+ if (noConfig || cfg.walk_src || pkgSrc) configFiles = [...new Set([...configFiles, ...allFiles.filter(f => SRC_EXT.test(f) && !f.endsWith('.d.ts'))])]
415
+ if (srcFiles.size) configFiles = [...new Set([...configFiles, ...srcFiles])]
416
+ // .cg.yaml include directories and kept hidden directories (skip_dirs.keep: [.storybook]): their files are roots
417
+ // even when the tsconfig leaves them out (node_modules, build/, and dot-directories its wildcards never match)
418
+ const keptHidden = r => r.split('/').slice(0, -1).some(x => x.startsWith('.') && KEEP_NAMES.has(x))
419
+ if (INCLUDE.length || KEEP_NAMES.size) configFiles = [...new Set([...configFiles, ...allFiles.filter(f => SRC_EXT.test(f) && !f.endsWith('.d.ts') && (included(rel(f)) || keptHidden(rel(f))))])]
420
+ configFiles = configFiles.filter(f => f.endsWith('.d.ts') || !belowSkipped(rel(f))) // declarations stay resolution input
421
+ const extraFiles = (cfg.extra_files || []).map(f => path.resolve(ROOT, f)).filter(f => fs.existsSync(f))
422
+ if (extraFiles.some(f => /\.(c|m)?jsx?$/.test(f))) options.allowJs = true
423
+ const rootNames = [...new Set([...configFiles.filter(f => !SKIP_ROOT.test(f) && !(SKIP_REL && SKIP_REL.test(rel(f))) && !excludedRel(rel(f))), ...extraFiles, ...testFiles]), ...virtual.keys()]
424
+ if ([...testFiles].some(f => /\.(c|m)?jsx?$/.test(f))) options.allowJs = true
425
+ const program = ts.createProgram({ rootNames, options, host })
426
+ const checker = program.getTypeChecker()
427
+ const tProgram = Date.now()
428
+
429
+ // ---------- helpers ----------
430
+ const projectSfMemo = new Map()
431
+ const projectSf = sf => {
432
+ const fn = sf.fileName
433
+ let v = projectSfMemo.get(fn)
434
+ if (v !== undefined) return v
435
+ const real = fn.endsWith('.vue.ts') ? fn.slice(0, -3) : fn
436
+ v = testFiles.has(real) || (!sourceSkipped(rel(real)) && (srcFiles.has(real) || srcDirs.some(d => real.startsWith(d + path.sep))) && !SKIP_FILE.test(real) && !real.endsWith('.d.ts') && !(SKIP_REL && SKIP_REL.test(rel(real))) && !excludedRel(rel(real)))
437
+ projectSfMemo.set(fn, v)
438
+ return v
439
+ }
440
+ const realFile = sf => sf.fileName.endsWith('.vue.ts') ? sf.fileName.slice(0, -3) : sf.fileName
441
+ function lineOf(node, sf = node.getSourceFile()) {
442
+ const l = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1
443
+ const lm = lineMaps.get(sf.fileName)
444
+ return (lm && lm[l]) || l
445
+ }
446
+ const isTemplateLine = (node, sf = node.getSourceFile()) => {
447
+ const lm = lineMaps.get(sf.fileName); if (!lm) return false
448
+ return !!lm[sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1]
449
+ }
450
+ function unwrap(e) {
451
+ while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression?.(e) || ts.isNonNullExpression(e) || ts.isTypeAssertionExpression?.(e))) e = e.expression
452
+ return e
453
+ }
454
+ const isFn = e => e && (ts.isArrowFunction(e) || ts.isFunctionExpression(e))
455
+ function docOf(node) {
456
+ let n = node
457
+ if (ts.isVariableDeclaration(n) && n.parent && n.parent.parent && ts.isVariableStatement(n.parent.parent)) n = n.parent.parent
458
+ const sf = n.getSourceFile()
459
+ const ranges = ts.getLeadingCommentRanges(sf.text, n.getFullStart()) || []
460
+ const docs = ranges.filter(r => sf.text.slice(r.pos, r.pos + 3) === '/**').map(r => sf.text.slice(r.pos, r.end))
461
+ if (!docs.length) return null
462
+ return docs[docs.length - 1].replace(/^\/\*\*|\*\/$/g, '').split('\n').map(l => l.replace(/^\s*\* ?/, '')).join('\n').trim() || null
463
+ }
464
+
465
+ // ---------- pass 1: declarations -> nodes ----------
466
+ // router registrations with inline handlers: router.get('/x', mw, (req, res) => {...}) / app.route('/x').post(fn)
467
+ const ROUTE_VERBS = new Set(['get', 'post', 'put', 'patch', 'delete', 'del', 'head', 'options', 'all'])
468
+ const isStrArg = a => a && (ts.isStringLiteralLike(a) || ts.isTemplateExpression(a) || ts.isRegularExpressionLiteral(a) || (ts.isArrayLiteralExpression(a) && a.elements.length && a.elements.every(x => ts.isStringLiteralLike(x))))
469
+ function routeCallInfo(node) {
470
+ if (!ts.isCallExpression(node)) return null
471
+ const c = unwrap(node.expression)
472
+ if (!ts.isPropertyAccessExpression(c) || !ROUTE_VERBS.has(c.name.text)) return null
473
+ const a0 = unwrap(node.arguments[0])
474
+ let pathText = null
475
+ if (isStrArg(a0)) {
476
+ if (c.name.text === 'get' && node.arguments.length === 2 && ts.isStringLiteralLike(a0) && !/^\/|^\*$|^:/.test(a0.text)) return null
477
+ pathText = a0.getText().replace(/\s+/g, ' ').slice(0, 80)
478
+ } else {
479
+ const inner = unwrap(c.expression)
480
+ const ic = inner && ts.isCallExpression(inner) && unwrap(inner.expression)
481
+ if (!(ic && ts.isPropertyAccessExpression(ic) && (ic.name.text === 'route' || ROUTE_VERBS.has(ic.name.text)))) return null
482
+ pathText = ic.name.text === 'route' && inner.arguments[0] ? inner.arguments[0].getText().slice(0, 80) : ''
483
+ }
484
+ let base = unwrap(c.expression)
485
+ while (ts.isCallExpression(base) && ts.isPropertyAccessExpression(unwrap(base.expression))) base = unwrap(unwrap(base.expression).expression)
486
+ return { label: `${base.getText().replace(/\s+/g, ' ').slice(0, 40)}.${c.name.text}(${pathText})` }
487
+ }
488
+ // Electron IPC / context bridge registrations whose inline handlers become function nodes:
489
+ // ipcMain.handle('ch', fn) / handleOnce / on / once, ipcRenderer.on('ch', fn) / once -> label `ipcMain.handle('ch')`
490
+ // contextBridge.exposeInMainWorld('api', { ping: () => ... }) -> one node per member `api.ping`
491
+ const IPC_RECV = new Map([['ipcMain', new Set(['handle', 'handleOnce', 'on', 'once'])], ['ipcRenderer', new Set(['on', 'once'])]])
492
+ function ipcCallInfo(node) {
493
+ if (!ts.isCallExpression(node)) return null
494
+ const c = unwrap(node.expression)
495
+ if (!ts.isPropertyAccessExpression(c)) return null
496
+ const obj = unwrap(c.expression)
497
+ const on0 = obj && (ts.isIdentifier(obj) ? obj.text : ts.isPropertyAccessExpression(obj) ? obj.name.text : null)
498
+ // ipcMain / ipcRenderer, or a project wrapper named after them (ipcMainManager.handle(IpcEvents.X, fn))
499
+ const on = on0 && (IPC_RECV.has(on0) ? on0 : /^ipcMain[A-Z_]\w*$/.test(on0) ? 'ipcMain' : /^ipcRenderer[A-Z_]\w*$/.test(on0) ? 'ipcRenderer' : on0)
500
+ if (!node.arguments[0]) return null
501
+ const ch = ipcChan(node.arguments[0]) // a literal, a const, an enum member (IpcEvents.OPEN = 'open')
502
+ if (on && IPC_RECV.has(on) && IPC_RECV.get(on).has(c.name.text) && node.arguments.length >= 2) {
503
+ // a channel variable typed as a union of literals (`channel: IpcEvents` from a lookup table): every member
504
+ const chans = ch != null ? [ch] : ipcChanUnion(node.arguments[0])
505
+ if (!chans) return null
506
+ const lab = ch != null ? `'${ch}'` : unwrap(node.arguments[0]).getText().slice(0, 40)
507
+ return { kind: 'ipc', process: on === 'ipcMain' ? 'main' : 'renderer', channels: chans, label: `${on0}.${c.name.text}(${lab})`, wrapper: on !== on0, union: ch == null }
508
+ }
509
+ if (ch == null) return null
510
+ if (on === 'contextBridge' && c.name.text === 'exposeInMainWorld' && node.arguments.length >= 2) return { kind: 'expose', key: ch }
511
+ return null
512
+ }
513
+ function ipcChan(a) {
514
+ a = unwrap(a)
515
+ if (!a) return null
516
+ if (ts.isStringLiteralLike(a)) return a.text
517
+ if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a) && !ts.isElementAccessExpression(a)) return null
518
+ try {
519
+ if (ts.isPropertyAccessExpression(a) || ts.isElementAccessExpression(a)) {
520
+ const v = checker.getConstantValue(a)
521
+ if (typeof v === 'string') return v
522
+ }
523
+ const t = checker.getTypeAtLocation(a)
524
+ if (t && t.isStringLiteral && t.isStringLiteral()) return t.value
525
+ } catch { }
526
+ return null
527
+ }
528
+ function ipcChanUnion(a) {
529
+ a = unwrap(a)
530
+ if (!a || !ts.isIdentifier(a)) return null
531
+ try {
532
+ const t = checker.getTypeAtLocation(a)
533
+ const ts_ = t && t.isUnion && t.isUnion() ? t.types : null
534
+ if (ts_ && ts_.length <= 500 && ts_.every(x => x.isStringLiteral && x.isStringLiteral())) return [...new Set(ts_.map(x => x.value))].sort()
535
+ } catch { }
536
+ return null
537
+ }
538
+ const exposedKeys = new Set() // contextBridge.exposeInMainWorld keys (`window.api`)
539
+ // `wrap(async () => {...}, opts)` / `withA(withB(fn))`: the wrapped function literal (HOF wrappers: route handler
540
+ // builders, asyncHandler, withAuth, ...)
541
+ function wrappedFn(init, depth = 0) {
542
+ init = unwrap(init)
543
+ if (!init || depth > 3 || !ts.isCallExpression(init)) return null
544
+ const c = unwrap(init.expression)
545
+ if (ts.isIdentifier(c) && (c.text === 'require' || c.text === 'defineStore')) return null
546
+ for (const a of init.arguments) {
547
+ const u = unwrap(a)
548
+ if (isFn(u)) return u
549
+ const w = wrappedFn(u, depth + 1)
550
+ if (w) return w
551
+ }
552
+ return null
553
+ }
554
+ const isModuleExports = e => { e = unwrap(e); return !!e && ts.isPropertyAccessExpression(e) && e.getText() === 'module.exports' }
555
+ const nodes = [] // {id, kind, name, file, line, end_line, doc, parent, attrs}
556
+ const declId = new Map() // ts.Node (declaration) -> node id
557
+ const valueIds = new Map() // enum member / constant declaration -> enum_case / constant node id (#84)
558
+ const valueNames = new Set()
559
+ // a constant's initializer: a literal, a literal array / object / template, `-1`, `as const`; not a call or `new`
560
+ function constInit(e) {
561
+ e = unwrap(e)
562
+ if (!e) return false
563
+ if (ts.isStringLiteralLike(e) || ts.isNumericLiteral(e) || ts.isBigIntLiteral?.(e) || ts.isRegularExpressionLiteral(e)
564
+ || e.kind === ts.SyntaxKind.TrueKeyword || e.kind === ts.SyntaxKind.FalseKeyword || e.kind === ts.SyntaxKind.NullKeyword) return true
565
+ if (ts.isPrefixUnaryExpression(e)) return constInit(e.operand)
566
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return constInit(e.left) && constInit(e.right) // 1000 * 60
567
+ if (ts.isTemplateExpression(e)) return true
568
+ if (ts.isArrayLiteralExpression(e)) return true
569
+ if (ts.isObjectLiteralExpression(e)) return !e.properties.some(p => ts.isMethodDeclaration(p) || ts.isGetAccessor(p) || ts.isSetAccessor(p) || (ts.isPropertyAssignment(p) && isFn(unwrap(p.initializer))))
570
+ return false
571
+ }
572
+ // stored class fields (#88): `count = 0`, `private items: Item[]`, constructor parameter properties; accesses that the
573
+ // checker resolves to one are READS_PROP / WRITES_PROP
574
+ const fieldIds = new Map() // declaration -> field node id
575
+ const fieldNames = new Set()
576
+ const TS_MUTATING = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin',
577
+ 'set', 'delete', 'clear', 'add'])
578
+ function addField(cls, q, m, sf, r, parentId, how) {
579
+ const nm = m.name.text
580
+ const id = `field:${r}#${q}.${nm}`
581
+ if (usedIds.has(id)) { if (how === 'this') fieldIds.set(m, id); return }
582
+ usedIds.add(id)
583
+ const fl = how === 'this' ? 0 : ts.getCombinedModifierFlags(m)
584
+ nodes.push({ id, kind: 'field', name: `${q}.${nm}`, file: r, line: lineOf(m, sf), end_line: sf.getLineAndCharacterOfPosition(m.end).line + 1,
585
+ doc: null, parent: parentId, attrs: { property: 'stored', declared: how, ...((fl & ts.ModifierFlags.Readonly) ? { readonly: true } : {}) } })
586
+ fieldIds.set(m, id)
587
+ fieldNames.add(nm)
588
+ stats.stored_field_nodes = (stats.stored_field_nodes || 0) + 1
589
+ }
590
+ // component / composable state (#88): React `const [x, setX] = useState()` and Vue `const x = ref()` /
591
+ // `shallowRef()` / `reactive()` are field nodes (`property: state`, `hook`); a reference the checker resolves to the
592
+ // variable is READS_PROP, `setX(...)` / `x.value = ...` / `x.value++` / `state.p = ...` WRITES_PROP
593
+ const stateIds = new Map() // declaration (VariableDeclaration / BindingElement) -> [field id, 'value' | 'setter' | 'reactive']
594
+ const stateNames = new Set()
595
+ const STATE_HOOKS = new Set(['useState', 'useReducer', 'ref', 'shallowRef', 'reactive', 'shallowReactive'])
596
+ // Pinia options stores (`defineStore('cart', { state: () => ({ items: [] }), actions: {...} })`) and the Vue Options
597
+ // API (`data() { return { count: 0 } }`): each state / data key is a `property: state` field (#88). `this.x` inside
598
+ // that options object, and `store.x` / `useCart().x` on a store, are its reads / writes
599
+ const optFields = new Map() // options ObjectLiteralExpression -> Map(name -> field id)
600
+ const storeFields = new Map() // store VariableDeclaration -> Map(name -> field id)
601
+ const optNames = new Set()
602
+ function returnedObject(f) {
603
+ if (!f) return null
604
+ f = unwrap(f)
605
+ if (ts.isArrowFunction(f) && !ts.isBlock(f.body)) { const b = unwrap(f.body); return ts.isObjectLiteralExpression(b) ? b : null }
606
+ const body = f.body
607
+ if (!body || !ts.isBlock(body)) return null
608
+ for (const st of body.statements) if (ts.isReturnStatement(st) && st.expression && ts.isObjectLiteralExpression(unwrap(st.expression))) return unwrap(st.expression)
609
+ return null
610
+ }
611
+ function optionsFields(sf, r, fid) {
612
+ const reg = (obj, owner, ownerName, keyProp, hook, storeDecl) => {
613
+ const p = obj.properties.find(x => x.name && ts.isIdentifier(x.name) && x.name.text === keyProp)
614
+ if (!p) return
615
+ const ret = returnedObject(ts.isPropertyAssignment(p) ? p.initializer : (ts.isMethodDeclaration(p) ? p : null))
616
+ if (!ret) return
617
+ const m = new Map()
618
+ for (const q of ret.properties) {
619
+ if (!q.name || !(ts.isIdentifier(q.name) || ts.isStringLiteral(q.name))) continue
620
+ const nm = q.name.text
621
+ const id = `field:${r}#${ownerName ? ownerName + '.' : ''}${nm}`
622
+ if (!usedIds.has(id)) {
623
+ usedIds.add(id)
624
+ nodes.push({ id, kind: 'field', name: id.split('#').pop(), file: r, line: lineOf(q, sf), end_line: lineOf(q, sf), doc: null,
625
+ parent: owner, attrs: { property: 'state', hook } })
626
+ stats.state_field_nodes = (stats.state_field_nodes || 0) + 1
627
+ }
628
+ m.set(nm, id); optNames.add(nm)
629
+ }
630
+ optFields.set(obj, m)
631
+ if (storeDecl) storeFields.set(storeDecl, m)
632
+ }
633
+ const scan = (n) => {
634
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'defineStore') {
635
+ const obj = n.arguments.map(unwrap).find(a => a && ts.isObjectLiteralExpression(a))
636
+ const vd = ts.isVariableDeclaration(n.parent) ? n.parent : null
637
+ const sid = vd && declId.get(n)
638
+ if (obj && vd && ts.isIdentifier(vd.name)) reg(obj, sid || fid, vd.name.text, 'state', 'pinia', vd)
639
+ } else if (ts.isObjectLiteralExpression(n) && (ts.isExportAssignment(n.parent)
640
+ || (ts.isCallExpression(n.parent) && ts.isIdentifier(n.parent.expression) && n.parent.expression.text === 'defineComponent'))) {
641
+ reg(n, fid, '', 'data', 'data', null)
642
+ }
643
+ ts.forEachChild(n, scan)
644
+ }
645
+ scan(sf)
646
+ }
647
+ function storeOfExpr(e) {
648
+ e = unwrap(e)
649
+ let call = null
650
+ if (ts.isCallExpression(e)) call = e
651
+ else if (ts.isIdentifier(e)) {
652
+ let s = null
653
+ try { s = checker.getSymbolAtLocation(e) } catch { }
654
+ const vd = s && s.valueDeclaration
655
+ if (vd && ts.isVariableDeclaration(vd) && vd.initializer && ts.isCallExpression(unwrap(vd.initializer))) call = unwrap(vd.initializer)
656
+ }
657
+ if (!call || !ts.isIdentifier(call.expression)) return null
658
+ let s = null
659
+ try { s = checker.getSymbolAtLocation(call.expression) } catch { }
660
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s) } catch { s = null } }
661
+ return (s && s.valueDeclaration && storeFields.get(s.valueDeclaration)) || null
662
+ }
663
+ function writeOf(node) {
664
+ const p = node.parent
665
+ const isAssign = (e) => e.parent && ts.isBinaryExpression(e.parent) && e.parent.left === e && e.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && e.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
666
+ const isIncr = (e) => e.parent && (ts.isPrefixUnaryExpression(e.parent) || ts.isPostfixUnaryExpression(e.parent)) && (e.parent.operator === ts.SyntaxKind.PlusPlusToken || e.parent.operator === ts.SyntaxKind.MinusMinusToken)
667
+ if (isAssign(node) || isIncr(node) || (p && ts.isDeleteExpression(p))) return [true, undefined]
668
+ if (p && ts.isElementAccessExpression(p) && p.expression === node && (isAssign(p) || (p.parent && ts.isDeleteExpression(p.parent)))) return [true, 'item']
669
+ if (p && ts.isPropertyAccessExpression(p) && p.expression === node && TS_MUTATING.has(p.name.text) && p.parent && ts.isCallExpression(p.parent) && p.parent.expression === p) return [true, 'mutating']
670
+ return [false, undefined]
671
+ }
672
+ function optionsRef(node, cur, sf, r) {
673
+ let m = null
674
+ if (node.expression.kind === ts.SyntaxKind.ThisKeyword) {
675
+ for (let x = node.parent; x && !ts.isSourceFile(x); x = x.parent) if (optFields.has(x)) { m = optFields.get(x); break }
676
+ } else m = storeOfExpr(node.expression)
677
+ const fid = m && m.get(node.name.text)
678
+ if (!fid || fid === cur) return
679
+ const [write, via] = writeOf(node)
680
+ const recv = node.expression.kind === ts.SyntaxKind.ThisKeyword ? 'this' : node.expression.getText(sf).slice(0, 40)
681
+ addEdge(cur, fid, write ? 'WRITES_PROP' : 'READS_PROP', r, lineOf(node, sf), m === null ? 'resolved' : (recv === 'this' ? 'exact' : 'resolved'), { receiver: recv, ...(via ? { via } : {}) })
682
+ stats[write ? 'state_field_writes' : 'state_field_reads'] = (stats[write ? 'state_field_writes' : 'state_field_reads'] || 0) + 1
683
+ }
684
+ function stateDecl(d, cur, sf, r) {
685
+ const init = unwrap(d.initializer)
686
+ if (!init || !ts.isCallExpression(init)) return
687
+ const callee = unwrap(init.expression)
688
+ const hook = ts.isIdentifier(callee) ? callee.text : (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === 'React' ? callee.name.text : null)
689
+ if (!hook || !STATE_HOOKS.has(hook)) return
690
+ const react = hook.startsWith('use')
691
+ const owner = cur.split(':').slice(1).join(':')
692
+ const add = (nameNode, decl, mode) => {
693
+ const nm = nameNode.text
694
+ const id = `field:${owner.includes('#') ? owner : owner + '#'}${owner.includes('#') ? '.' : ''}${nm}`
695
+ if (!usedIds.has(id)) {
696
+ usedIds.add(id)
697
+ nodes.push({ id, kind: 'field', name: id.split('#').pop(), file: r, line: lineOf(decl, sf), end_line: lineOf(decl, sf), doc: null,
698
+ parent: cur, attrs: { property: 'state', hook } })
699
+ stats.state_field_nodes = (stats.state_field_nodes || 0) + 1
700
+ }
701
+ stateIds.set(decl, [id, mode])
702
+ stateNames.add(nm)
703
+ }
704
+ if (react && ts.isArrayBindingPattern(d.name)) {
705
+ const [v, set] = d.name.elements
706
+ if (v && ts.isBindingElement(v) && ts.isIdentifier(v.name)) {
707
+ add(v.name, v, 'read')
708
+ if (set && ts.isBindingElement(set) && ts.isIdentifier(set.name)) {
709
+ stateIds.set(set, [stateIds.get(v)[0], 'setter']); stateNames.add(set.name.text)
710
+ }
711
+ }
712
+ } else if (!react && ts.isIdentifier(d.name)) add(d.name, d, hook.endsWith('eactive') ? 'reactive' : 'value')
713
+ }
714
+ function stateRef(node, cur, sf, r) {
715
+ const p = node.parent
716
+ if (p && (ts.isVariableDeclaration(p) || ts.isBindingElement(p)) && p.name === node) return
717
+ let s = null
718
+ try { s = checker.getSymbolAtLocation(node) } catch { }
719
+ if (s && !s.valueDeclaration && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s) } catch { s = null } }
720
+ const hit = s && stateIds.get(s.valueDeclaration || (s.declarations || [])[0])
721
+ if (!hit || hit[0] === cur) return
722
+ const [fid, mode] = hit
723
+ const isAssign = (e) => e.parent && ts.isBinaryExpression(e.parent) && e.parent.left === e && e.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && e.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
724
+ const isIncr = (e) => e.parent && (ts.isPrefixUnaryExpression(e.parent) || ts.isPostfixUnaryExpression(e.parent)) && (e.parent.operator === ts.SyntaxKind.PlusPlusToken || e.parent.operator === ts.SyntaxKind.MinusMinusToken)
725
+ let write = false, via
726
+ if (mode === 'setter') {
727
+ if (!(ts.isCallExpression(p) && p.expression === node)) return // `onChange={setX}`: handed out, not a write here
728
+ write = true; via = 'setter'
729
+ } else if (p && ts.isPropertyAccessExpression(p) && p.expression === node && (isAssign(p) || isIncr(p) || ts.isDeleteExpression(p.parent))
730
+ && (mode === 'reactive' || p.name.text === 'value')) { write = true; via = mode === 'reactive' ? 'property' : 'value' }
731
+ else if (isAssign(node) || isIncr(node)) { write = true }
732
+ else if (mode === 'reactive' && p && ts.isPropertyAccessExpression(p) && p.expression === node) {
733
+ const q = p.parent // `state.items.push(x)` / `state.items[0] = x`
734
+ if (q && ts.isPropertyAccessExpression(q) && q.expression === p && TS_MUTATING.has(q.name.text) && q.parent && ts.isCallExpression(q.parent) && q.parent.expression === q) { write = true; via = 'mutating' }
735
+ }
736
+ addEdge(cur, fid, write ? 'WRITES_PROP' : 'READS_PROP', r, lineOf(node, sf), 'exact', via ? { via } : undefined)
737
+ stats[write ? 'state_field_writes' : 'state_field_reads'] = (stats[write ? 'state_field_writes' : 'state_field_reads'] || 0) + 1
738
+ }
739
+ function addValue(kind, key, name, d, sf, r, parentId) {
740
+ const id = `${kind}:${key}`
741
+ if (usedIds.has(id)) return
742
+ usedIds.add(id)
743
+ nodes.push({ id, kind, name, file: r, line: lineOf(d, sf), end_line: sf.getLineAndCharacterOfPosition(d.end).line + 1, doc: null, parent: parentId, attrs: {} })
744
+ valueIds.set(d, id)
745
+ valueNames.add(d.name.text)
746
+ }
747
+ const fileNode = new Map() // abs real path -> node id
748
+ const modNode = new Map() // module node id -> node object (re-export attrs)
749
+ const usedIds = new Set()
750
+ function mkId(kind, key) { let id = `${kind}:${key}`, i = 2; while (usedIds.has(id)) id = `${kind}:${key}~${i++}`; usedIds.add(id); return id }
751
+
752
+ // describe / it / test (Vitest, Jest, Mocha, Playwright test, Cypress): {name, describe, framework}
753
+ const TEST_FNS = new Set(['it', 'test', 'describe', 'suite', 'context', 'specify'])
754
+ const TEST_MODS = new Set(['only', 'skip', 'todo', 'concurrent', 'serial', 'parallel', 'fixme', 'fail', 'slow', 'sequential', 'describe', 'each', 'skipIf', 'runIf'])
755
+ let testFramework = 'test'
756
+ function testCallInfo(node) {
757
+ let c = unwrap(node.expression)
758
+ if (ts.isCallExpression(c)) c = unwrap(c.expression) // it.each([...])('name', fn) / test.skipIf(x)('name', fn)
759
+ let base = c, mods = []
760
+ while (ts.isPropertyAccessExpression(base)) { mods.unshift(base.name.text); base = unwrap(base.expression) }
761
+ if (!ts.isIdentifier(base) || !TEST_FNS.has(base.text) || !mods.every(m => TEST_MODS.has(m))) return null
762
+ const a0 = node.arguments[0] && unwrap(node.arguments[0])
763
+ if (!a0 || !(ts.isStringLiteralLike(a0) || ts.isTemplateExpression(a0))) return null
764
+ if (!node.arguments.some(a => isFn(unwrap(a)))) return null
765
+ const describe = base.text === 'describe' || base.text === 'suite' || base.text === 'context' || mods.includes('describe')
766
+ const name = ts.isStringLiteralLike(a0) ? a0.text : a0.getText().slice(1, -1)
767
+ return { name: name.replace(/\s+/g, ' ').slice(0, 160), describe, framework: testFramework }
768
+ }
769
+ const sourceFiles = program.getSourceFiles().filter(projectSf)
770
+ // interfaces some project class `implements` (and the interfaces they extend): their function-typed properties
771
+ // (`fetch: (o: Opts) => Promise<R>`) become member nodes too; method signatures always do
772
+ const aliasTarget = sym => { for (let i = 0; sym && (sym.flags & ts.SymbolFlags.Alias) && i < 6; i++) { try { sym = checker.getAliasedSymbol(sym) } catch { return null } } return sym }
773
+ const ifaceDecls = sym => ((sym && sym.declarations) || []).filter(d => ts.isInterfaceDeclaration(d) || (ts.isTypeAliasDeclaration(d) && ts.isTypeLiteralNode(d.type)))
774
+ // project interfaces / object type aliases expected where `n` stands (a typed variable, a return value, an argument,
775
+ // `satisfies`): one type, `| undefined` / `| null` aside
776
+ const ctxType = n => {
777
+ let ct = null
778
+ try { ct = checker.getContextualType(n) } catch { }
779
+ if (!ct) return null
780
+ const ts0 = ct.isUnion() ? ct.types.filter(t => !(t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null))) : [ct]
781
+ return ts0.length === 1 ? ts0[0] : null
782
+ }
783
+ const projIfaces = t => t ? ifaceDecls(t.aliasSymbol || t.getSymbol()).filter(d => projectSf(d.getSourceFile())) : []
784
+ const ctxIfaces = n => projIfaces(ctxType(n))
785
+ // a class passed as a value where a constructor of an interface is expected (`register(FollowingFeedAPI)` with
786
+ // `register(c: new () => FeedAPI)`): those interfaces
787
+ const ctorIfaces = n => {
788
+ const t = ctxType(n)
789
+ if (!t) return []
790
+ const out = []
791
+ for (const sig of t.getConstructSignatures()) { let rt = null; try { rt = checker.getReturnTypeOfSignature(sig) } catch { } for (const d of projIfaces(rt)) if (!out.includes(d)) out.push(d) }
792
+ return out
793
+ }
794
+ const fnMembers = o => o.properties.filter(p => ts.isMethodDeclaration(p) || (ts.isPropertyAssignment(p) && isFn(unwrap(p.initializer))))
795
+ const VALUE_SLOT = n => n.parent && ((ts.isCallExpression(n.parent) || ts.isNewExpression(n.parent)) && (n.parent.arguments || []).includes(n)
796
+ || ts.isArrayLiteralExpression(n.parent) || (ts.isPropertyAssignment(n.parent) && n.parent.initializer === n)
797
+ || ts.isShorthandPropertyAssignment(n.parent) || (ts.isVariableDeclaration(n.parent) && n.parent.initializer === n)
798
+ || ts.isReturnStatement(n.parent))
799
+ const classOfValue = n => {
800
+ if (!ts.isIdentifier(n) || !VALUE_SLOT(n)) return null
801
+ let sym = null
802
+ try { sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n) } catch { }
803
+ sym = aliasTarget(sym)
804
+ return ((sym && sym.declarations) || []).find(x => ts.isClassDeclaration(x) && projectSf(x.getSourceFile())) || null
805
+ }
806
+ // mixins: `class Client extends mix(Base).with(Users, Posts)` (or `Users(Posts(Base))`) where `const Users = (b) =>
807
+ // class extends b {..}`, merged with `interface Client extends Base, UsersMix, PostsMix`: the merged interfaces, and
808
+ // the mixin class expressions applied in the `extends` call
809
+ const mergedIfaces = cls => {
810
+ let sym = null
811
+ try { sym = cls.name && checker.getSymbolAtLocation(cls.name) } catch { }
812
+ return ((sym && sym.declarations) || []).filter(d => ts.isInterfaceDeclaration(d) && projectSf(d.getSourceFile()))
813
+ }
814
+ const mixinClasses = cls => {
815
+ const h = (cls.heritageClauses || []).find(h => h.token === ts.SyntaxKind.ExtendsKeyword)
816
+ const e = h && h.types[0] && unwrap(h.types[0].expression)
817
+ if (!e || !ts.isCallExpression(e)) return []
818
+ const out = []
819
+ const walk = n => {
820
+ if (ts.isIdentifier(n) && n.parent && ts.isCallExpression(n.parent) && n.parent.arguments.includes(n)) {
821
+ let sym = null
822
+ try { sym = aliasTarget(checker.getSymbolAtLocation(n)) } catch { }
823
+ for (const d of (sym && sym.declarations) || []) {
824
+ const f = ts.isVariableDeclaration(d) && d.initializer ? unwrap(d.initializer) : ts.isFunctionDeclaration(d) ? d : null
825
+ if (!f || !(isFn(f) || ts.isFunctionDeclaration(f)) || !f.body) continue
826
+ let ce = ts.isBlock(f.body) ? null : unwrap(f.body)
827
+ if (ts.isBlock(f.body)) for (const st of f.body.statements) if (ts.isReturnStatement(st) && st.expression) ce = unwrap(st.expression)
828
+ if (ce && ts.isClassExpression(ce) && projectSf(ce.getSourceFile()) && !out.includes(ce)) out.push(ce)
829
+ }
830
+ }
831
+ ts.forEachChild(n, walk)
832
+ }
833
+ walk(e)
834
+ return out
835
+ }
836
+ const implementedIfaces = new Set()
837
+ {
838
+ const addIface = (d, depth = 0) => {
839
+ if (implementedIfaces.has(d) || depth > 8) return
840
+ implementedIfaces.add(d)
841
+ for (const h of d.heritageClauses || []) for (const t of h.types) {
842
+ let sym = null
843
+ try { sym = aliasTarget(checker.getSymbolAtLocation(t.expression)) } catch { }
844
+ for (const x of ifaceDecls(sym)) addIface(x, depth + 1)
845
+ }
846
+ }
847
+ const walk = n => {
848
+ if ((ts.isClassDeclaration(n) || ts.isClassExpression(n)) && n.heritageClauses) {
849
+ for (const h of n.heritageClauses) if (h.token === ts.SyntaxKind.ImplementsKeyword) for (const t of h.types) {
850
+ let sym = null
851
+ try { sym = aliasTarget(checker.getSymbolAtLocation(t.expression)) } catch { }
852
+ for (const d of ifaceDecls(sym)) addIface(d)
853
+ }
854
+ if (ts.isClassDeclaration(n) && mixinClasses(n).length) for (const d of mergedIfaces(n)) addIface(d)
855
+ } else if (ts.isObjectLiteralExpression(n) && fnMembers(n).length) for (const d of ctxIfaces(n)) addIface(d) // `const api: FeedAPI = { fetch() {..} }`
856
+ else if (classOfValue(n)) for (const d of ctorIfaces(n)) addIface(d)
857
+ ts.forEachChild(n, walk)
858
+ }
859
+ for (const sf of sourceFiles) if (!realFile(sf).endsWith('.vue')) walk(sf)
860
+ }
861
+ const isFnType = t => t && (ts.isFunctionTypeNode(t) || (ts.isParenthesizedTypeNode(t) && isFnType(t.type)))
862
+ // members of an interface / object type alias: `method:<file>#Iface.member` (attrs.signature; no body). A call on an
863
+ // interface-typed value resolves to it; IMPLEMENTED_BY links it to the class members implementing it
864
+ function ifaceMembers(decl, q, parentId, r, sf) {
865
+ const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.type.members
866
+ const seen = new Map()
867
+ for (const m of members || []) {
868
+ if (!m.name || !(ts.isIdentifier(m.name) || ts.isStringLiteralLike(m.name))) continue
869
+ const isM = ts.isMethodSignature(m), isP = ts.isPropertySignature(m) && isFnType(m.type) && implementedIfaces.has(decl)
870
+ if (!isM && !isP) continue
871
+ const mq = `${q}.${m.name.text}`
872
+ let id = seen.get(mq) // overloads share one node
873
+ if (!id) {
874
+ id = mkId('method', `${r}#${mq}`)
875
+ seen.set(mq, id)
876
+ nodes.push({ id, kind: 'method', name: mq, file: r, line: lineOf(m, sf), end_line: sf.getLineAndCharacterOfPosition(m.end).line + 1,
877
+ doc: docOf(m), parent: parentId, attrs: { signature: true, interface_member: true } })
878
+ stats.interface_members = (stats.interface_members || 0) + 1
879
+ }
880
+ declId.set(m, id)
881
+ }
882
+ }
883
+ for (const sf of sourceFiles) {
884
+ const real = realFile(sf), r = rel(real), fk = fileKind(r)
885
+ const isTestSf = testFiles.has(real)
886
+ if (isTestSf) {
887
+ const txt = sf.text
888
+ testFramework = /@playwright\/test/.test(txt) ? 'playwright' : /from ['"]vitest['"]/.test(txt) ? 'vitest' : /\bcy\./.test(txt) ? 'cypress' : /@jest\/globals|jest\./.test(txt) ? 'jest' : 'test'
889
+ }
890
+ if (!real.endsWith('.vue')) stats.ts_files++
891
+ if (sf.parseDiagnostics && sf.parseDiagnostics.length) { // the parser recovers; count files that needed recovery
892
+ stats.syntax_error_files = (stats.syntax_error_files || 0) + 1
893
+ if ((stats.syntax_error_samples ||= []).length < 5) stats.syntax_error_samples.push(r)
894
+ // error lines per file for `cg coverage` (#73); in a .vue file only those of its <script> blocks (the template
895
+ // stubs are appended after the file's own lines)
896
+ let maxLine = Infinity
897
+ if (real.endsWith('.vue')) { try { maxLine = fs.readFileSync(real, 'utf8').split('\n').length } catch (e) { maxLine = 0 } }
898
+ const lines = [...new Set(sf.parseDiagnostics.map(d => sf.getLineAndCharacterOfPosition(d.start || 0).line + 1))]
899
+ .filter(l => l <= maxLine).slice(0, 20)
900
+ const se = (stats.syntax_errors ||= {})
901
+ if (lines.length && Object.keys(se).length < 500) se[r] = lines.map(l => [l, l])
902
+ }
903
+ const fid = real.endsWith('.vue') ? `${fk}:${r}` : `module:${r}`
904
+ usedIds.add(fid)
905
+ fileNode.set(real, fid)
906
+ nodes.push({ id: fid, kind: real.endsWith('.vue') ? fk : 'module', name: r, file: r, line: 1, end_line: sf.getLineAndCharacterOfPosition(sf.end).line + 1, attrs: { file_kind: fk, ...(isTestSf ? { test: true } : {}) } })
907
+ modNode.set(fid, nodes[nodes.length - 1])
908
+ if (real.endsWith('.vue')) continue // SFC: references are attributed to the component node
909
+ const visit = (node, qual, parentId, inObj) => {
910
+ let name = null, kind = null, body = null, wrapped = null
911
+ if (!qual && !inObj && ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer
912
+ && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const)
913
+ && ts.isVariableStatement(node.parent.parent) && node.parent.parent.parent === sf && constInit(node.initializer))
914
+ addValue('constant', `${r}#${node.name.text}`, node.name.text, node, sf, r, fid) // `export const MAX = 3` (#84)
915
+ if (ts.isFunctionDeclaration(node) && node.name) { name = node.name.text; kind = 'function'; body = node }
916
+ else if (ts.isFunctionDeclaration(node) && !node.name && !qual && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Default)) { name = 'default'; kind = 'function'; body = node }
917
+ else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
918
+ const init = unwrap(node.initializer)
919
+ if (isFn(init)) { name = node.name.text; kind = 'function'; body = init }
920
+ else if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && init.expression.text === 'defineStore') {
921
+ name = node.name.text; kind = 'store'; body = init
922
+ } else if (!qual && (wrapped = wrappedFn(init))) { name = node.name.text; kind = 'function'; body = wrapped }
923
+ else if (!qual && ts.isObjectLiteralExpression(init)) { ts.forEachChild(init, c => visit(c, node.name.text, parentId, true)); return }
924
+ } else if (ts.isPropertyAssignment(node) && inObj && node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) && ts.isObjectLiteralExpression(unwrap(node.initializer))) {
925
+ ts.forEachChild(node, c => visit(c, qual ? `${qual}.${node.name.text}` : node.name.text, parentId, true)); return
926
+ } else if ((ts.isMethodDeclaration(node) || ts.isPropertyAssignment(node)) && node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) {
927
+ const init = ts.isPropertyAssignment(node) ? unwrap(node.initializer) : node
928
+ if (ts.isMethodDeclaration(node) || isFn(init)) { name = node.name.text; kind = ts.isMethodDeclaration(node) && ts.isClassLike(node.parent) ? 'method' : 'function'; body = init }
929
+ } else if (ts.isPropertyDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer && ts.isClassLike(node.parent)) {
930
+ const init = unwrap(node.initializer)
931
+ const f = isFn(init) ? init : wrappedFn(init)
932
+ if (f) { name = node.name.text; kind = 'method'; body = f }
933
+ } else if (ts.isExportAssignment(node) && !qual) {
934
+ const ex = unwrap(node.expression)
935
+ const f = isFn(ex) ? ex : wrappedFn(ex)
936
+ if (f) { name = 'default'; kind = 'function'; body = f }
937
+ else if (ts.isObjectLiteralExpression(ex)) { ts.forEachChild(ex, c => visit(c, '', parentId, true)); return }
938
+ } else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && !qual && ts.isExpressionStatement(node.parent)) {
939
+ // CommonJS: module.exports = fn | {..}; exports.x = fn; module.exports.x = fn
940
+ const l = unwrap(node.left), rgt = unwrap(node.right)
941
+ const f = isFn(rgt) ? rgt : wrappedFn(rgt)
942
+ if (isModuleExports(l)) {
943
+ if (f) { name = f.name ? f.name.text : 'default'; kind = 'function'; body = f }
944
+ else if (ts.isObjectLiteralExpression(rgt)) { ts.forEachChild(rgt, c => visit(c, '', parentId, true)); return }
945
+ } else if (ts.isPropertyAccessExpression(l) && (unwrap(l.expression).getText() === 'exports' || isModuleExports(l.expression)) && f) {
946
+ name = l.name.text; kind = 'function'; body = f
947
+ }
948
+ } else if (ts.isCallExpression(node) && isTestSf && testCallInfo(node)) {
949
+ const tc = testCallInfo(node)
950
+ const cb = [...node.arguments].reverse().map(unwrap).find(isFn)
951
+ if (tc.describe) {
952
+ if (cb) ts.forEachChild(cb, c => visit(c, qual ? `${qual} > ${tc.name}` : tc.name, parentId))
953
+ for (const a of node.arguments) if (unwrap(a) !== cb) visit(a, qual, parentId)
954
+ return
955
+ }
956
+ const q = qual ? `${qual} > ${tc.name}` : tc.name
957
+ const id = mkId('test', `${r}#${q}`)
958
+ nodes.push({ id, kind: 'test', name: q, file: r, line: lineOf(node, sf), end_line: sf.getLineAndCharacterOfPosition(node.end).line + 1,
959
+ doc: null, parent: parentId, attrs: { framework: tc.framework, test: true } })
960
+ stats.test_cases = (stats.test_cases || 0) + 1
961
+ if (cb) { declId.set(cb, id); declId.set(node, id); ts.forEachChild(cb, c => visit(c, q, id)) }
962
+ return
963
+ } else if (ts.isCallExpression(node)) {
964
+ const ic = ipcCallInfo(node)
965
+ if (ic) {
966
+ const fns = []
967
+ if (ic.kind === 'ipc') {
968
+ const h = unwrap(node.arguments[1])
969
+ if (isFn(h)) fns.push([h, ic.label])
970
+ } else {
971
+ exposedKeys.add(ic.key)
972
+ const o = unwrap(node.arguments[1])
973
+ if (o && ts.isObjectLiteralExpression(o)) for (const p of o.properties) {
974
+ const pn = p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)) ? p.name.text : null
975
+ if (!pn) continue
976
+ if (ts.isMethodDeclaration(p)) fns.push([p, `${ic.key}.${pn}`])
977
+ else if (ts.isPropertyAssignment(p) && isFn(unwrap(p.initializer))) fns.push([unwrap(p.initializer), `${ic.key}.${pn}`])
978
+ }
979
+ }
980
+ if (fns.length) {
981
+ const handled = new Set()
982
+ for (const [f, lab] of fns) {
983
+ const q = qual ? `${qual}.${lab}` : lab
984
+ const id = mkId('function', `${r}#${q}`)
985
+ nodes.push({ id, kind: 'function', name: q, file: r, line: lineOf(f, sf), end_line: sf.getLineAndCharacterOfPosition(f.end).line + 1,
986
+ doc: null, parent: parentId, attrs: { inline_handler: true, ipc: ic.kind === 'ipc' && !ic.union ? ic.channels[0] : undefined, exposed: ic.kind === 'expose' ? ic.key : undefined } })
987
+ declId.set(f, id)
988
+ ts.forEachChild(f, c => visit(c, q, id))
989
+ handled.add(f)
990
+ }
991
+ ts.forEachChild(node, c => { if (!handled.has(c) && !handled.has(unwrap(c))) visit(c, qual, parentId) })
992
+ return
993
+ }
994
+ }
995
+ const rc = routeCallInfo(node)
996
+ if (rc) {
997
+ const handled = new Set()
998
+ for (const a of node.arguments) {
999
+ const u = unwrap(a)
1000
+ const fns = isFn(u) ? [u] : ts.isArrayLiteralExpression(u) ? u.elements.map(unwrap).filter(isFn) : [wrappedFn(u)].filter(Boolean)
1001
+ for (const f of fns) {
1002
+ const q = qual ? `${qual}.${rc.label}` : rc.label
1003
+ const id = mkId('function', `${r}#${q}`)
1004
+ nodes.push({ id, kind: 'function', name: q, file: r, line: lineOf(f, sf), end_line: sf.getLineAndCharacterOfPosition(f.end).line + 1,
1005
+ doc: null, parent: parentId, attrs: { inline_handler: true } })
1006
+ declId.set(f, id)
1007
+ ts.forEachChild(f, c => visit(c, q, id))
1008
+ handled.add(f)
1009
+ }
1010
+ }
1011
+ const v2 = c => { if (handled.has(c)) return; if (handled.has(unwrap(c))) return; visit(c, qual, parentId) }
1012
+ ts.forEachChild(node, v2)
1013
+ return
1014
+ }
1015
+ } else if (ts.isClassDeclaration(node) && node.name) { name = node.name.text; kind = 'class'; body = node }
1016
+ else if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) && node.name) {
1017
+ name = node.name.text; kind = 'type'
1018
+ }
1019
+ if (name) {
1020
+ const q = qual ? `${qual}.${name}` : name
1021
+ if (kind === 'function' && !qual && fk === 'composable' && /^use[A-Z0-9]/.test(name)) kind = 'composable'
1022
+ const attrs = {}
1023
+ if (wrapped || (body && body !== node && !isFn(unwrap(node.initializer || node)) && !ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) && kind !== 'store' && kind !== 'class')) {
1024
+ const w = node.initializer || node.expression || node.right
1025
+ if (w && ts.isCallExpression(unwrap(w))) attrs.wrapped_by = unwrap(unwrap(w).expression).getText().slice(0, 60)
1026
+ }
1027
+ if (kind === 'store') { const a0 = body.arguments[0]; if (a0 && ts.isStringLiteralLike(a0)) attrs.store_id = a0.text }
1028
+ let isExported = false
1029
+ try { isExported = !qual && (name === 'default' || ts.isExportAssignment(node) || ts.isBinaryExpression(node) || (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0) } catch { }
1030
+ if (isExported) attrs.exported = true
1031
+ const id = mkId(kind, `${r}#${q}`)
1032
+ nodes.push({ id, kind, name: q, file: r, line: lineOf(node, sf), end_line: sf.getLineAndCharacterOfPosition(node.end).line + 1,
1033
+ doc: docOf(node), parent: parentId, attrs })
1034
+ declId.set(node, id)
1035
+ if (body && body !== node) declId.set(body, id)
1036
+ if (kind === 'type' && (ts.isInterfaceDeclaration(node) || (ts.isTypeAliasDeclaration(node) && ts.isTypeLiteralNode(node.type)))) ifaceMembers(node, q, id, r, sf)
1037
+ if (ts.isEnumDeclaration(node)) for (const m of node.members) {
1038
+ if (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) addValue('enum_case', `${r}#${q}.${m.name.text}`, `${q}.${m.name.text}`, m, sf, r, id)
1039
+ }
1040
+ if (ts.isClassDeclaration(node)) for (const m of node.members) { // `static readonly MAX = 3`
1041
+ const fl = ts.getCombinedModifierFlags(m)
1042
+ if (ts.isPropertyDeclaration(m) && ts.isIdentifier(m.name) && (fl & ts.ModifierFlags.Static) && (fl & ts.ModifierFlags.Readonly) && constInit(m.initializer))
1043
+ addValue('constant', `${r}#${q}.${m.name.text}`, `${q}.${m.name.text}`, m, sf, r, id)
1044
+ else if (ts.isPropertyDeclaration(m) && m.name && (ts.isIdentifier(m.name) || ts.isPrivateIdentifier(m.name)) && !(fl & ts.ModifierFlags.Static)
1045
+ && !(fl & ts.ModifierFlags.Abstract) && !(m.initializer && (isFn(unwrap(m.initializer)) || wrappedFn(unwrap(m.initializer)))))
1046
+ addField(node, q, m, sf, r, id, 'class')
1047
+ else if (ts.isConstructorDeclaration(m)) for (const p of m.parameters) { // constructor(private api: Api)
1048
+ if (ts.isIdentifier(p.name) && (ts.getCombinedModifierFlags(p) & (ts.ModifierFlags.ParameterPropertyModifier)))
1049
+ addField(node, q, p, sf, r, id, 'constructor')
1050
+ }
1051
+ }
1052
+ // plain JavaScript: `this.x = ...` in the constructor or a method declares the field (#88); in a .ts file the
1053
+ // checker only accepts declared properties, so this only adds fields a JS class never declares
1054
+ if (ts.isClassDeclaration(node) && /\.(c|m)?jsx?$/.test(r)) {
1055
+ const declared = new Set(node.members.filter(m => m.name && (ts.isIdentifier(m.name) || ts.isPrivateIdentifier(m.name))).map(m => m.name.text))
1056
+ const scan = (n) => {
1057
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(n.left)
1058
+ && n.left.expression.kind === ts.SyntaxKind.ThisKeyword && !declared.has(n.left.name.text)) {
1059
+ addField(node, q, n.left, sf, r, id, 'this'); fieldIds.set(n, fieldIds.get(n.left))
1060
+ }
1061
+ if (!ts.isClassLike(n) && !(ts.isFunctionLike(n) && !ts.isArrowFunction(n) && !ts.isMethodDeclaration(n) && !ts.isConstructorDeclaration(n) && !ts.isAccessor(n))) ts.forEachChild(n, scan)
1062
+ }
1063
+ for (const m of node.members) if ((ts.isConstructorDeclaration(m) || ts.isMethodDeclaration(m) || ts.isAccessor(m)) && m.body) ts.forEachChild(m.body, scan)
1064
+ }
1065
+ if (kind === 'type') return
1066
+ ts.forEachChild(node, c => visit(c, q, id))
1067
+ return
1068
+ }
1069
+ ts.forEachChild(node, c => visit(c, qual, parentId))
1070
+ }
1071
+ ts.forEachChild(sf, c => visit(c, '', fid))
1072
+ }
1073
+ for (const n of nodes) if (testFiles.has(path.resolve(ROOT, n.file)) && n.kind !== 'test') n.attrs = { ...(n.attrs || {}), test: true }
1074
+ stats.test_files = testFiles.size
1075
+ const nodeById = new Map(nodes.map(n => [n.id, n]))
1076
+
1077
+ // ---------- symbol -> target node ----------
1078
+ const projectDecl = d => d && projectSf(d.getSourceFile())
1079
+ function declToNode(d) {
1080
+ if (!d) return null
1081
+ if (declId.has(d)) return declId.get(d)
1082
+ if ((ts.isArrowFunction(d) || ts.isFunctionExpression(d)) && declId.has(d.parent)) return declId.get(d.parent)
1083
+ // CommonJS `exports.x = fn`: the symbol's declaration is the left-hand property access
1084
+ if (ts.isPropertyAccessExpression(d) && d.parent && ts.isBinaryExpression(d.parent) && d.parent.left === d && declId.has(d.parent)) return declId.get(d.parent)
1085
+ if (ts.isSourceFile(d)) return fileNode.get(realFile(d)) || null
1086
+ return null
1087
+ }
1088
+ function importTypeTarget(d) {
1089
+ // .nuxt/types/imports.d.ts: const useApi: typeof import('../../app/composables/useApi').useApi
1090
+ const tn = d.type
1091
+ if (!tn || !ts.isImportTypeNode(tn) || !tn.qualifier) return null
1092
+ const spec = tn.argument && tn.argument.literal && tn.argument.literal.text
1093
+ if (!spec) return null
1094
+ const res = host.resolveModuleNameLiterals([{ text: spec }], d.getSourceFile().fileName, undefined, options)[0]
1095
+ const fn = res && res.resolvedModule && res.resolvedModule.resolvedFileName
1096
+ const sf = fn && program.getSourceFile(fn)
1097
+ if (!sf) return null
1098
+ const msym = sf.symbol && checker.getMergedSymbol(sf.symbol)
1099
+ if (!msym) return null
1100
+ const name = ts.isIdentifier(tn.qualifier) ? tn.qualifier.text : tn.qualifier.right.text
1101
+ return checker.getExportsOfModule(msym).find(s => s.name === name) || null
1102
+ }
1103
+ // a Nuxt auto-import global (`const x: typeof import('…').x`), if that is all the symbol is
1104
+ function autoImportDecl(sym) {
1105
+ const ds = sym.declarations || []
1106
+ return ds.length === 1 && ts.isVariableDeclaration(ds[0]) && ds[0].type && ts.isImportTypeNode(ds[0].type) ? ds[0] : null
1107
+ }
1108
+ // returns {id, conf, via} or null
1109
+ function resolveSymbol(sym, conf = 'exact', via = []) {
1110
+ for (let i = 0; sym && i < 10; i++) {
1111
+ if (sym.flags & ts.SymbolFlags.Alias) {
1112
+ const ad = (sym.declarations || [])[0]
1113
+ const imp = ad && (ts.isImportClause(ad) ? ad.parent : (ts.isImportSpecifier(ad) ? ad.parent.parent.parent : null))
1114
+ if (imp && ts.isImportDeclaration(imp) && imp.moduleSpecifier.text.endsWith('.vue')) {
1115
+ const ms = checker.getSymbolAtLocation(imp.moduleSpecifier); const sd = ms && (ms.declarations || [])[0]
1116
+ if (sd && ts.isSourceFile(sd) && fileNode.has(realFile(sd))) return { id: fileNode.get(realFile(sd)), conf, via: [...via, 'import'] }
1117
+ }
1118
+ try { sym = checker.getAliasedSymbol(sym) } catch { return null } via.push('import'); continue
1119
+ }
1120
+ const decls = sym.declarations || []
1121
+ const d = decls.find(projectDecl) || decls[0]
1122
+ if (!d) return null
1123
+ const id = projectDecl(d) ? declToNode(d) : null
1124
+ if (id) return { id, conf, via }
1125
+ if (ts.isShorthandPropertyAssignment(d)) { sym = checker.getShorthandAssignmentValueSymbol(d); conf = 'resolved'; via.push('shorthand'); continue }
1126
+ if (ts.isPropertyAssignment(d) && d.initializer) { sym = checker.getSymbolAtLocation(unwrap(d.initializer)); conf = 'resolved'; via.push('property'); continue }
1127
+ if (ts.isBindingElement(d) && ts.isObjectBindingPattern(d.parent)) {
1128
+ const pname = (d.propertyName || d.name).getText()
1129
+ const t = checker.getTypeAtLocation(d.parent)
1130
+ const p = t && t.getProperty(pname)
1131
+ if (!p || p === sym) return null
1132
+ sym = p; conf = 'resolved'; via.push('destructure'); continue
1133
+ }
1134
+ if (ts.isVariableDeclaration(d)) {
1135
+ if (d.type && ts.isImportTypeNode(d.type)) { sym = importTypeTarget(d); conf = 'resolved'; via.push('nuxt-auto-import'); continue }
1136
+ const init = d.initializer && unwrap(d.initializer)
1137
+ if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) && projectDecl(d)) { sym = checker.getSymbolAtLocation(init); conf = 'resolved'; via.push('alias'); continue }
1138
+ }
1139
+ if (ts.isExportSpecifier(d)) { try { sym = checker.getExportSpecifierLocalTargetSymbol(d) || null } catch { return null } continue }
1140
+ if (ts.isExportAssignment(d)) { const ex = unwrap(d.expression); const id = declToNode(ex) || (ts.isIdentifier(ex) ? null : null); if (id) return { id, conf, via }; sym = ts.isIdentifier(ex) ? checker.getSymbolAtLocation(ex) : null; continue }
1141
+ return null
1142
+ }
1143
+ return null
1144
+ }
1145
+
1146
+ // ---------- string evaluation (URL templates) ----------
1147
+ const PH = (n) => `\u0001${n}\u0002`
1148
+ const MAXV = 16
1149
+ function cross(a, b) { const o = []; for (const x of a) for (const y of b) { o.push(x + y); if (o.length >= MAXV) return o } return o }
1150
+ function literalUnion(expr) {
1151
+ try {
1152
+ const t = checker.getTypeAtLocation(expr)
1153
+ const parts = t.isUnion() ? t.types : [t]
1154
+ if (parts.length > 8) return null
1155
+ const vals = []
1156
+ for (const p of parts) { if (p.isStringLiteral() || p.isNumberLiteral()) vals.push(String(p.value)); else return null }
1157
+ return vals.length ? vals : null
1158
+ } catch { return null }
1159
+ }
1160
+ function returnExprs(fn) {
1161
+ if (!fn) return []
1162
+ if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return [fn.body]
1163
+ const out = []
1164
+ const v = n => { if (ts.isReturnStatement(n) && n.expression) out.push(n.expression); if (!isFn(n) && !ts.isFunctionDeclaration(n)) ts.forEachChild(n, v) }
1165
+ if (fn.body) ts.forEachChild(fn.body, v)
1166
+ return out
1167
+ }
1168
+ function fnDeclOf(sym) {
1169
+ for (let i = 0; sym && i < 8; i++) {
1170
+ if (sym.flags & ts.SymbolFlags.Alias) { sym = checker.getAliasedSymbol(sym); continue }
1171
+ const d = (sym.declarations || [])[0]
1172
+ if (!d) return null
1173
+ if (ts.isFunctionDeclaration(d) || ts.isMethodDeclaration(d)) return d
1174
+ if (ts.isVariableDeclaration(d)) {
1175
+ if (d.type && ts.isImportTypeNode(d.type)) { sym = importTypeTarget(d); continue }
1176
+ const init = d.initializer && unwrap(d.initializer)
1177
+ if (isFn(init)) return init
1178
+ if (init && ts.isIdentifier(init)) { sym = checker.getSymbolAtLocation(init); continue }
1179
+ return null
1180
+ }
1181
+ if (ts.isPropertyAssignment(d) && isFn(unwrap(d.initializer))) return unwrap(d.initializer)
1182
+ if (ts.isShorthandPropertyAssignment(d)) { sym = checker.getShorthandAssignmentValueSymbol(d); continue }
1183
+ if (ts.isBindingElement(d) && ts.isObjectBindingPattern(d.parent)) {
1184
+ const t = checker.getTypeAtLocation(d.parent); const p = t && t.getProperty((d.propertyName || d.name).getText())
1185
+ if (!p || p === sym) return null; sym = p; continue
1186
+ }
1187
+ return null
1188
+ }
1189
+ return null
1190
+ }
1191
+ // configured values: runtime config (useRuntimeConfig().public.X / config.public.X) and env (process.env.X,
1192
+ // import.meta.env.X) become {runtimeConfig.X} / {env.X} placeholders; the Python side folds in their values
1193
+ // (nuxt.config runtimeConfig defaults, .env files). `X || 'default'` records the in-code default.
1194
+ const CONF_PH = /^\u0001(runtimeConfig|env)\.[^\u0002]*\u0002$/
1195
+ const configDefaults = {}
1196
+ function envKey(e) {
1197
+ e = unwrap(e)
1198
+ if (!(ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) return null
1199
+ const name = ts.isPropertyAccessExpression(e) ? e.name.text : (ts.isStringLiteralLike(unwrap(e.argumentExpression)) ? unwrap(e.argumentExpression).text : null)
1200
+ if (!name) return null
1201
+ const o = unwrap(e.expression).getText().replace(/\s+/g, '')
1202
+ if (o === 'process.env' || o === 'import.meta.env' || o === 'Bun.env' || o === 'Deno.env') return 'env.' + name
1203
+ if (/(^|\.)public$|useRuntimeConfig\(\)$|^\$config$|runtimeConfig$/.test(o)) return 'runtimeConfig.' + name
1204
+ return null
1205
+ }
1206
+ // destructured config: const { apiBase } = useRuntimeConfig().public / const { public: { apiBase } } = useRuntimeConfig()
1207
+ function bindingConfigKey(d) {
1208
+ if (!ts.isBindingElement(d)) return null
1209
+ const name = (d.propertyName || d.name).getText()
1210
+ let pat = d.parent, outer = []
1211
+ while (pat && ts.isBindingElement(pat.parent)) { outer.unshift((pat.parent.propertyName || pat.parent.name).getText()); pat = pat.parent.parent }
1212
+ const decl = pat && pat.parent
1213
+ if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return null
1214
+ const init = unwrap(decl.initializer).getText().replace(/\s+/g, '')
1215
+ const full = [init, ...outer].join('.')
1216
+ if (/^(process\.env|import\.meta\.env)$/.test(full)) return 'env.' + name
1217
+ if (/(useRuntimeConfig\(\)|\$config|runtimeConfig)(\.public)?$/.test(full) || /(^|\.)public$/.test(full)) {
1218
+ if (/(useRuntimeConfig\(\)|\$config|runtimeConfig)$/.test(full) && name === 'public') return null
1219
+ return 'runtimeConfig.' + name
1220
+ }
1221
+ if (/^(config|runtimeConfig)$/.test(init) && outer[0] === 'public') return 'runtimeConfig.' + name
1222
+ return null
1223
+ }
1224
+ // ref / computed unwrapping: x.value where x = computed(() => expr) | ref(expr) | useX() returning one of them
1225
+ function refValue(e, depth, ctx) {
1226
+ e = unwrap(e)
1227
+ if (!e || depth > 8) return null
1228
+ if (ts.isIdentifier(e)) {
1229
+ const sym = checker.getSymbolAtLocation(e)
1230
+ const d = sym && (sym.declarations || [])[0]
1231
+ if (d && ts.isVariableDeclaration(d) && d.initializer) return refValue(d.initializer, depth + 1, ctx)
1232
+ return null
1233
+ }
1234
+ if (!ts.isCallExpression(e)) return null
1235
+ const callee = unwrap(e.expression)
1236
+ const cname = ts.isIdentifier(callee) ? callee.text : (ts.isPropertyAccessExpression(callee) ? callee.name.text : '')
1237
+ // computed(() => expr) only: a ref's initial value (ref(null), ref('')) is not what it holds at request time
1238
+ if (cname === 'computed') {
1239
+ const a = unwrap(e.arguments[0])
1240
+ if (!a) return null
1241
+ if (isFn(a)) {
1242
+ const rs = returnExprs(a)
1243
+ if (!rs.length || rs.length > 4) return null
1244
+ let vals = []
1245
+ for (const r of rs) vals.push(...evalStr(r, depth + 1, ctx).vals)
1246
+ return { vals: [...new Set(vals)].slice(0, MAXV), conf: 'resolved' }
1247
+ }
1248
+ return null
1249
+ }
1250
+ const fn = fnDeclOf(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(callee) ? callee.name : callee))
1251
+ if (fn && projectDecl(fn)) {
1252
+ const rs = returnExprs(fn)
1253
+ if (!rs.length || rs.length > 4) return null
1254
+ let vals = []
1255
+ for (const r of rs) { const v = refValue(r, depth + 1, { fn, args: e.arguments, outer: ctx }); if (v) vals.push(...v.vals) }
1256
+ return vals.length ? { vals: [...new Set(vals)].slice(0, MAXV), conf: 'resolved' } : null
1257
+ }
1258
+ return null
1259
+ }
1260
+ // evalStr -> {vals:[...], conf:'exact'|'resolved', params:Set}
1261
+ function evalStr(e, depth = 0, ctx = {}) {
1262
+ e = unwrap(e)
1263
+ const R = (vals, conf = 'exact') => ({ vals, conf })
1264
+ if (!e || depth > 12) return R([PH('?')], 'resolved')
1265
+ if (ts.isStringLiteralLike(e)) return R([e.text])
1266
+ if (ts.isNumericLiteral(e)) return R([e.text])
1267
+ if (ts.isTemplateExpression(e)) {
1268
+ let vals = [e.head.text], conf = 'exact'
1269
+ for (const sp of e.templateSpans) {
1270
+ const v = evalStr(sp.expression, depth + 1, ctx)
1271
+ if (v.conf !== 'exact') conf = 'resolved'
1272
+ vals = cross(cross(vals, v.vals), [sp.literal.text])
1273
+ }
1274
+ return R(vals, conf)
1275
+ }
1276
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.PlusToken) {
1277
+ const a = evalStr(e.left, depth + 1, ctx), b = evalStr(e.right, depth + 1, ctx)
1278
+ return R(cross(a.vals, b.vals), a.conf === 'exact' && b.conf === 'exact' ? 'exact' : 'resolved')
1279
+ }
1280
+ if (ts.isConditionalExpression(e)) {
1281
+ const a = evalStr(e.whenTrue, depth + 1, ctx), b = evalStr(e.whenFalse, depth + 1, ctx)
1282
+ return R([...new Set([...a.vals, ...b.vals])].slice(0, MAXV), 'resolved')
1283
+ }
1284
+ if (ts.isIdentifier(e)) {
1285
+ const sym = e.parent && ts.isShorthandPropertyAssignment(e.parent) && e.parent.name === e
1286
+ ? checker.getShorthandAssignmentValueSymbol(e.parent) : checker.getSymbolAtLocation(e)
1287
+ const d = sym && (sym.declarations || [])[0]
1288
+ if (d && ts.isParameter(d)) {
1289
+ if (ctx.args && ctx.fn && d.parent === ctx.fn) {
1290
+ const idx = ctx.fn.parameters.indexOf(d)
1291
+ const a = ctx.args[idx]
1292
+ if (a && ts.isNumericLiteral(unwrap(a))) return R([PH(e.text)], 'resolved') // ids stay parameters
1293
+ if (a) { const v = evalStr(a, depth + 1, ctx.outer || {}); return R(v.vals, 'resolved') }
1294
+ }
1295
+ const lu = literalUnion(e); if (lu) return R(lu, 'resolved')
1296
+ ctx.params && ctx.params.add(e.text)
1297
+ return R([PH(e.text)])
1298
+ }
1299
+ if (d && ts.isBindingElement(d)) { const k = bindingConfigKey(d); if (k) return R([PH(k)]) }
1300
+ if (d && ts.isVariableDeclaration(d) && d.initializer && (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)) {
1301
+ const v = evalStr(d.initializer, depth + 1, ctx)
1302
+ if (v.vals.length === 1 && CONF_PH.test(v.vals[0])) return R(v.vals, v.conf)
1303
+ if (v.vals.length === 1 && /^\u0001[^\u0002]*\u0002$/.test(v.vals[0])) return R([PH(e.text)], v.conf)
1304
+ return R(v.vals, v.conf === 'exact' && !(d.getSourceFile() !== e.getSourceFile()) ? 'exact' : 'resolved')
1305
+ }
1306
+ if (d && ts.isVariableDeclaration(d) && d.initializer && (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Let)) {
1307
+ const v = evalStr(d.initializer, depth + 1, ctx)
1308
+ if (v.vals.length === 1 && /^\u0001[^\u0002]*\u0002$/.test(v.vals[0])) return R(v.vals, 'resolved')
1309
+ }
1310
+ const lu = literalUnion(e); if (lu) return R(lu, 'resolved')
1311
+ return R([PH(e.text)])
1312
+ }
1313
+ const ek = envKey(e)
1314
+ if (ek) return R([PH(ek)])
1315
+ if (ts.isPropertyAccessExpression(e) && e.name.text === 'value') {
1316
+ const rv = refValue(e.expression, depth + 1, ctx)
1317
+ if (rv && rv.vals.length && !rv.vals.some(v => v.includes(PH('?')))) return R(rv.vals, 'resolved')
1318
+ }
1319
+ if (ts.isElementAccessExpression(e) || ts.isPropertyAccessExpression(e)) {
1320
+ const objSym = checker.getSymbolAtLocation(unwrap(e.expression))
1321
+ const od = objSym && (objSym.declarations || [])[0]
1322
+ const obj = od && ts.isVariableDeclaration(od) && od.initializer && unwrap(od.initializer)
1323
+ if (obj && ts.isObjectLiteralExpression(obj)) {
1324
+ const key = ts.isPropertyAccessExpression(e) ? e.name.text : (ts.isStringLiteralLike(unwrap(e.argumentExpression)) ? unwrap(e.argumentExpression).text : null)
1325
+ const props = obj.properties.filter(p => ts.isPropertyAssignment(p) && (key === null || p.name.getText().replace(/['"]/g, '') === key))
1326
+ if (props.length) {
1327
+ let vals = []
1328
+ for (const p of props) vals.push(...evalStr(p.initializer, depth + 1, ctx).vals)
1329
+ return R([...new Set(vals)].slice(0, MAXV), 'resolved')
1330
+ }
1331
+ }
1332
+ const lu = literalUnion(e); if (lu) return R(lu, 'resolved')
1333
+ return R([PH(ts.isPropertyAccessExpression(e) ? e.name.text : '?')])
1334
+ }
1335
+ if (ts.isCallExpression(e)) {
1336
+ const callee = unwrap(e.expression)
1337
+ const cname = ts.isIdentifier(callee) ? callee.text : (ts.isPropertyAccessExpression(callee) ? callee.name.text : '')
1338
+ // value-preserving wrappers do not count against the depth budget
1339
+ if (['encodeURIComponent', 'encodeURI', 'String', 'Number'].includes(cname) && e.arguments[0]) {
1340
+ const v = evalStr(e.arguments[0], depth, ctx)
1341
+ return R(v.vals, 'resolved')
1342
+ }
1343
+ if (['toString', 'trim', 'replace', 'replaceAll', 'toLowerCase', 'toUpperCase'].includes(cname) && ts.isPropertyAccessExpression(callee)) {
1344
+ const v = evalStr(callee.expression, depth, ctx)
1345
+ return R(v.vals, cname === 'toString' ? v.conf : 'resolved')
1346
+ }
1347
+ const fn = fnDeclOf(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(callee) ? callee.name : callee))
1348
+ if (fn && projectDecl(fn)) {
1349
+ const rs = returnExprs(fn)
1350
+ if (rs.length && rs.length <= 4) {
1351
+ let vals = []
1352
+ for (const r of rs) vals.push(...evalStr(r, depth + 1, { fn, args: e.arguments, outer: ctx }).vals)
1353
+ return R([...new Set(vals)].slice(0, MAXV), 'resolved')
1354
+ }
1355
+ }
1356
+ const lu = literalUnion(e); if (lu) return R(lu, 'resolved')
1357
+ return R([PH(cname ? cname + '()' : '?')])
1358
+ }
1359
+ if (ts.isBinaryExpression(e) && (e.operatorToken.kind === ts.SyntaxKind.BarBarToken || e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) {
1360
+ const l = evalStr(e.left, depth + 1, ctx)
1361
+ if (l.vals.length === 1 && CONF_PH.test(l.vals[0])) {
1362
+ const r = evalStr(e.right, depth + 1, ctx)
1363
+ const lits = r.vals.filter(v => v && !v.includes('\u0001'))
1364
+ if (lits.length) configDefaults[l.vals[0].slice(1, -1)] = lits[0]
1365
+ return R(l.vals, 'resolved')
1366
+ }
1367
+ if (l.vals.length === 1 && /^\u0001[^\u0002]*\u0002$/.test(l.vals[0])) return R(l.vals, 'resolved')
1368
+ return R([PH(e.left.getText().split('.').pop().replace(/\W/g, '') || '?')])
1369
+ }
1370
+ const lu = literalUnion(e); if (lu) return R(lu, 'resolved')
1371
+ return R([PH('?')])
1372
+ }
1373
+ const render = s => s.replace(/\u0001([^\u0002]*)\u0002/g, (_, n) => `{${n}}`)
1374
+ function shapeDedupe(vals) {
1375
+ const seen = new Map()
1376
+ for (const v of vals) {
1377
+ const shape = v.replace(/\u0001[^\u0002]*\u0002/g, '\u0001\u0002')
1378
+ const prev = seen.get(shape)
1379
+ if (prev === undefined || (/\u0001\?\u0002/.test(prev) && !/\u0001\?\u0002/.test(v))) seen.set(shape, v)
1380
+ }
1381
+ return [...seen.values()]
1382
+ }
1383
+
1384
+ // axios instance origin: follow the receiver back to an axios.create({...}) call, return its config object
1385
+ function axiosCreateOf(expr, depth = 0) {
1386
+ expr = unwrap(expr)
1387
+ if (!expr || depth > 8) return null
1388
+ if (ts.isCallExpression(expr)) {
1389
+ const c = unwrap(expr.expression)
1390
+ if (ts.isPropertyAccessExpression(c) && c.name.text === 'create' && isAxiosType(c.expression)) return { cfg: expr.arguments[0], via: ['axios.create'] }
1391
+ if (ts.isPropertyAccessExpression(c) && c.name.text === 'create' && FW.importSource && FW.importSource(c.expression) === 'axios') return { cfg: expr.arguments[0], via: ['axios.create'] }
1392
+ // call returning an instance
1393
+ const fn = fnDeclOf(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(c) ? c.name : c))
1394
+ for (const r of returnExprs(fn)) { const o = axiosCreateOf(r, depth + 1); if (o) return o }
1395
+ return null
1396
+ }
1397
+ let sym = ts.isPropertyAccessExpression(expr) ? checker.getSymbolAtLocation(expr.name) : checker.getSymbolAtLocation(expr)
1398
+ for (let i = 0; sym && i < 10; i++) {
1399
+ if (sym.flags & ts.SymbolFlags.Alias) { sym = checker.getAliasedSymbol(sym); continue }
1400
+ const ai = autoImportDecl(sym)
1401
+ if (ai) { sym = importTypeTarget(ai); continue }
1402
+ const d = (sym.declarations || []).find(projectDecl)
1403
+ if (!d) return null
1404
+ if ((ts.isVariableDeclaration(d) || ts.isPropertyDeclaration(d)) && d.initializer) { const o = axiosCreateOf(d.initializer, depth + 1); return o && { cfg: o.cfg, via: [...o.via, 'var'] } }
1405
+ if (ts.isShorthandPropertyAssignment(d)) { sym = checker.getShorthandAssignmentValueSymbol(d); continue }
1406
+ if (ts.isPropertyAssignment(d)) return axiosCreateOf(d.initializer, depth + 1)
1407
+ if (ts.isBindingElement(d) && ts.isObjectBindingPattern(d.parent)) {
1408
+ const t = checker.getTypeAtLocation(d.parent); const p = t && t.getProperty((d.propertyName || d.name).getText())
1409
+ if (!p || p === sym) return null; sym = p; continue
1410
+ }
1411
+ return null
1412
+ }
1413
+ return null
1414
+ }
1415
+ function isAxiosType(expr) {
1416
+ try {
1417
+ const t = checker.getTypeAtLocation(expr)
1418
+ const s = checker.typeToString(t)
1419
+ if (/\bAxios(Instance|Static)\b/.test(s) || (t.symbol && /^Axios(Instance|Static)$/.test(t.symbol.name))) return true
1420
+ // axios types not installed: the receiver is the axios import itself
1421
+ return (s === 'any' || /^typeof /.test(s) || s === 'error') && FW.importSource && FW.importSource(expr) === 'axios' && (ts.isIdentifier(unwrap(expr)))
1422
+ } catch { return false }
1423
+ }
1424
+ const FW = {} // filled with fw.mjs helpers (importSource) once the program is built
1425
+ // instance created by <lib>.create/extend({...}) (ky, ofetch, $fetch): {lib, cfg}
1426
+ function clientCreateOf(expr, depth = 0) {
1427
+ expr = unwrap(expr)
1428
+ if (!expr || depth > 6) return null
1429
+ if (ts.isCallExpression(expr)) {
1430
+ const c = unwrap(expr.expression)
1431
+ if (ts.isPropertyAccessExpression(c) && ['create', 'extend'].includes(c.name.text)) {
1432
+ const lib = FW.importSource ? FW.importSource(c.expression) : null
1433
+ const base = ts.isIdentifier(unwrap(c.expression)) ? unwrap(c.expression).text : ''
1434
+ if (lib === 'ky' || lib === 'ofetch' || base === '$fetch' || base === 'ofetch') return { lib: lib || base, cfg: expr.arguments[0] }
1435
+ const inner = clientCreateOf(c.expression, depth + 1) // api.extend({...}) on an instance
1436
+ if (inner) return { lib: inner.lib, cfg: expr.arguments[0] || inner.cfg, parent: inner }
1437
+ }
1438
+ // factory: useApiClient() returning $fetch.create({...}) / ky.create({...})
1439
+ const fn = fnDeclOf(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(c) ? c.name : c))
1440
+ if (fn && projectDecl(fn)) {
1441
+ for (const r of returnExprs(fn)) { const o = clientCreateOf(r, depth + 1); if (o) return o }
1442
+ }
1443
+ return null
1444
+ }
1445
+ let sym = ts.isPropertyAccessExpression(expr) ? checker.getSymbolAtLocation(expr.name) : checker.getSymbolAtLocation(expr)
1446
+ for (let i = 0; sym && i < 8; i++) {
1447
+ if (sym.flags & ts.SymbolFlags.Alias) { try { sym = checker.getAliasedSymbol(sym) } catch { return null } continue }
1448
+ const ai = autoImportDecl(sym)
1449
+ if (ai) { sym = importTypeTarget(ai); continue }
1450
+ const d = (sym.declarations || []).find(projectDecl)
1451
+ if (!d) return null
1452
+ if (ts.isVariableDeclaration(d) && d.initializer) return clientCreateOf(d.initializer, depth + 1)
1453
+ if (ts.isPropertyDeclaration(d) && d.initializer) return clientCreateOf(d.initializer, depth + 1)
1454
+ return null
1455
+ }
1456
+ return null
1457
+ }
1458
+ function objProp(obj, name) {
1459
+ obj = unwrap(obj)
1460
+ if (!obj || !ts.isObjectLiteralExpression(obj)) return null
1461
+ for (const p of obj.properties) {
1462
+ if (ts.isPropertyAssignment(p) && p.name.getText().replace(/['"]/g, '') === name) return p.initializer
1463
+ if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) return p.name
1464
+ }
1465
+ return null
1466
+ }
1467
+
1468
+ // ---------- request keys (query params / body) ----------
1469
+ function requestKeys(e, depth = 0) {
1470
+ e = unwrap(e)
1471
+ const res = { keys: new Set(), conditional: new Set(), opaque: false }
1472
+ if (!e || depth > 4) { res.opaque = true; return res }
1473
+ const merge = (o, cond) => { for (const k of o.keys) (cond ? res.conditional : res.keys).add(k); for (const k of o.conditional) res.conditional.add(k); if (o.opaque) res.opaque = true; if (o.forwarded) { res.forwarded = o.forwarded; res.forwarded_index = o.forwarded_index } }
1474
+ if (ts.isObjectLiteralExpression(e)) {
1475
+ for (const p of e.properties) {
1476
+ if (ts.isSpreadAssignment(p)) merge(requestKeys(p.expression, depth + 1), false)
1477
+ else if (p.name) res.keys.add(p.name.getText().replace(/['"]/g, ''))
1478
+ }
1479
+ return res
1480
+ }
1481
+ if (ts.isIdentifier(e)) {
1482
+ const sym = e.parent && ts.isShorthandPropertyAssignment(e.parent) && e.parent.name === e
1483
+ ? checker.getShorthandAssignmentValueSymbol(e.parent) : checker.getSymbolAtLocation(e)
1484
+ const d = sym && (sym.declarations || [])[0]
1485
+ if (d && ts.isVariableDeclaration(d) && d.initializer) {
1486
+ merge(requestKeys(d.initializer, depth + 1), false)
1487
+ // later `x.key = ...` assignments in the same function; conditional when nested in an if
1488
+ const scope = d.parent && d.parent.parent && d.parent.parent.parent
1489
+ const v = n => {
1490
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(n.left)
1491
+ && ts.isIdentifier(n.left.expression) && n.left.expression.text === e.text) {
1492
+ let c = n.parent, cond = false
1493
+ while (c && c !== scope) { if (ts.isIfStatement(c) || ts.isConditionalExpression(c)) cond = true; c = c.parent }
1494
+ (cond ? res.conditional : res.keys).add(n.left.name.text)
1495
+ }
1496
+ ts.forEachChild(n, v)
1497
+ }
1498
+ if (scope) v(scope)
1499
+ return res
1500
+ }
1501
+ if (d && ts.isParameter(d)) {
1502
+ // forwarded parameter: keys come from its declared type (optional props -> conditional); the call
1503
+ // sites' object keys (CALLS.arg_keys) say what is actually passed
1504
+ const ty = checker.getNonNullableType(checker.getTypeAtLocation(d))
1505
+ // only plain object shapes carry request keys: not primitives / literal unions (String's methods), arrays,
1506
+ // functions, URLSearchParams…
1507
+ const plainObj = t => t.isUnion() ? t.types.every(plainObj) : t.isIntersection() ? t.types.some(plainObj)
1508
+ : !!(t.flags & ts.TypeFlags.Object) && !checker.isArrayLikeType(t) && !t.getCallSignatures().length &&
1509
+ !(t.getSymbol() && /^(URLSearchParams|FormData|Map|Set|Headers|Date|Blob|File)$/.test(t.getSymbol().name))
1510
+ const props = ty && plainObj(ty) ? checker.getPropertiesOfType(ty).filter(p => !p.name.startsWith('__@')) : []
1511
+ if (props.length && props.length <= 80) {
1512
+ for (const p of props) ((p.flags & ts.SymbolFlags.Optional) ? res.conditional : res.keys).add(p.name)
1513
+ res.forwarded = d.name && ts.isIdentifier(d.name) ? d.name.text : '?'
1514
+ res.forwarded_index = d.parent && d.parent.parameters ? d.parent.parameters.indexOf(d) : undefined
1515
+ } else res.opaque = true
1516
+ return res
1517
+ }
1518
+ }
1519
+ if (ts.isCallExpression(e)) {
1520
+ const c = unwrap(e.expression)
1521
+ const fn = fnDeclOf(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(c) ? c.name : c))
1522
+ if (fn && projectDecl(fn)) { for (const r of returnExprs(fn)) merge(requestKeys(r, depth + 1), false); return res }
1523
+ }
1524
+ res.opaque = true
1525
+ return res
1526
+ }
1527
+ const keysOut = r => ({ keys: [...r.keys].sort(), conditional: [...r.conditional].filter(k => !r.keys.has(k)).sort(), opaque: r.opaque || undefined, forwarded: r.forwarded || undefined, forwarded_index: r.forwarded ? r.forwarded_index : undefined })
1528
+
1529
+ // ---------- framework facts helpers (import sources etc.) ----------
1530
+ // test code (spec files, test trees) stays in the graph as tests, but its routers, controllers and handlers are test
1531
+ // setups, never application routes: framework facts come from application files only
1532
+ const appSourceFiles = sourceFiles.filter(sf => !testFiles.has(realFile(sf)))
1533
+ const FWX = { ts, checker, program, sourceFiles: appSourceFiles, rel, realFile, lineOf, declId, declToNode, resolveSymbol, evalStr, render, unwrap, projectSf, fileNode }
1534
+ let fwFacts = null
1535
+ const tFw0 = Date.now()
1536
+ fwFacts = collectFrameworkFacts(FWX)
1537
+ FW.importSource = FWX.importSource
1538
+ const tFw = Date.now() - tFw0
1539
+
1540
+ // ---------- pass 2: references ----------
1541
+ const edges = []
1542
+ const apiCalls = []
1543
+ const i18nUses = []
1544
+ const fallbacks = []
1545
+ const FALLBACK_OPS = new Set([ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken])
1546
+ const pageMeta = {}
1547
+ const edgeSeen = new Set()
1548
+ function addEdge(src, dst, kind, file, line, conf, attrs) {
1549
+ if (!src || !dst || (src === dst && kind === 'CALLS')) return
1550
+ const k = `${src}|${dst}|${kind}|${line}`
1551
+ if (edgeSeen.has(k)) return
1552
+ edgeSeen.add(k)
1553
+ edges.push({ src, dst, kind, file, line, confidence: conf, attrs: attrs || undefined })
1554
+ }
1555
+ const callSites = []
1556
+
1557
+ // ---------- class hierarchy: EXTENDS / IMPLEMENTS between project classes, OVERRIDDEN_BY base method -> override ----------
1558
+ // (a call on a base-typed value resolves to the base declaration; the override edge lets impact / tests reach the
1559
+ // callers of the base from an override and the callers of the overrides from the base)
1560
+ {
1561
+ const memberNode = (cls, name) => {
1562
+ for (const m of cls.members || []) {
1563
+ if (m.name && (ts.isIdentifier(m.name) || ts.isPrivateIdentifier(m.name) || ts.isStringLiteralLike(m.name)) && m.name.text === name
1564
+ && (ts.isMethodDeclaration(m) || ts.isGetAccessor(m) || ts.isSetAccessor(m) || ts.isPropertyDeclaration(m)) && declId.has(m)) return declId.get(m)
1565
+ }
1566
+ return null
1567
+ }
1568
+ const baseClassOf = cls => {
1569
+ for (const h of cls.heritageClauses || []) {
1570
+ if (h.token !== ts.SyntaxKind.ExtendsKeyword || !h.types.length) continue
1571
+ let sym = null
1572
+ try { sym = checker.getSymbolAtLocation(h.types[0].expression) } catch { }
1573
+ for (let i = 0; sym && (sym.flags & ts.SymbolFlags.Alias) && i < 6; i++) { try { sym = checker.getAliasedSymbol(sym) } catch { sym = null } }
1574
+ const d = sym && (sym.declarations || []).find(x => ts.isClassDeclaration(x) || ts.isClassExpression(x))
1575
+ return d && projectDecl(d) ? d : null
1576
+ }
1577
+ return null
1578
+ }
1579
+ const implementedClasses = cls => {
1580
+ const out = []
1581
+ for (const h of cls.heritageClauses || []) {
1582
+ if (h.token !== ts.SyntaxKind.ImplementsKeyword) continue
1583
+ for (const t of h.types) {
1584
+ let sym = null
1585
+ try { sym = checker.getSymbolAtLocation(t.expression) } catch { }
1586
+ for (let i = 0; sym && (sym.flags & ts.SymbolFlags.Alias) && i < 6; i++) { try { sym = checker.getAliasedSymbol(sym) } catch { sym = null } }
1587
+ const d = sym && (sym.declarations || []).find(x => ts.isClassDeclaration(x))
1588
+ if (d && projectDecl(d)) out.push(d)
1589
+ }
1590
+ }
1591
+ return out
1592
+ }
1593
+ // interface members: `class X implements I` (also through X's base classes and I's own `extends`)
1594
+ const ifaceMember = (d, name, depth = 0) => {
1595
+ const members = ts.isInterfaceDeclaration(d) ? d.members : (d.type && d.type.members) || []
1596
+ for (const m of members) if (m.name && (ts.isIdentifier(m.name) || ts.isStringLiteralLike(m.name)) && m.name.text === name && declId.has(m)) return declId.get(m)
1597
+ if (depth < 8) for (const h of d.heritageClauses || []) for (const t of h.types) {
1598
+ let sym = null
1599
+ try { sym = aliasTarget(checker.getSymbolAtLocation(t.expression)) } catch { }
1600
+ for (const x of ifaceDecls(sym)) { const hit = ifaceMember(x, name, depth + 1); if (hit) return hit }
1601
+ }
1602
+ return null
1603
+ }
1604
+ const ownIfaces = cls => {
1605
+ const out = []
1606
+ for (const h of cls.heritageClauses || []) {
1607
+ if (h.token !== ts.SyntaxKind.ImplementsKeyword) continue
1608
+ for (const t of h.types) {
1609
+ let sym = null
1610
+ try { sym = aliasTarget(checker.getSymbolAtLocation(t.expression)) } catch { }
1611
+ for (const d of ifaceDecls(sym)) if (projectDecl(d)) out.push(d)
1612
+ }
1613
+ }
1614
+ return out
1615
+ }
1616
+ const memberName = m => m.name && (ts.isIdentifier(m.name) || ts.isStringLiteralLike(m.name)) ? m.name.text : null
1617
+ const implementsIfaces = (decl, r, sf, ifaces, conf, attrs, inherited) => {
1618
+ let n = 0
1619
+ for (const m of decl.members || []) {
1620
+ if (!(ts.isMethodDeclaration(m) || ts.isGetAccessor(m) || ts.isSetAccessor(m) || ts.isPropertyDeclaration(m)) || !declId.has(m)) continue
1621
+ if (!declId.get(m).startsWith('method:') || (m.modifiers && m.modifiers.some(x => x.kind === ts.SyntaxKind.StaticKeyword))) continue
1622
+ const nm = memberName(m)
1623
+ if (!nm) continue
1624
+ for (const [d, inh] of ifaces) {
1625
+ if (inh && inherited(nm)) continue // the base class's own member implements it
1626
+ const im = ifaceMember(d, nm)
1627
+ if (im && im !== declId.get(m)) { addEdge(im, declId.get(m), 'IMPLEMENTED_BY', r, lineOf(m, sf), conf, attrs); n++ }
1628
+ }
1629
+ }
1630
+ return n
1631
+ }
1632
+ let nh = 0, no = 0
1633
+ for (const [decl, id] of declId) {
1634
+ if (!ts.isClassDeclaration(decl) || !id.startsWith('class:')) continue
1635
+ const sf = decl.getSourceFile()
1636
+ if (!projectSf(sf)) continue
1637
+ const r = rel(realFile(sf))
1638
+ for (const h of decl.heritageClauses || []) {
1639
+ const kind = h.token === ts.SyntaxKind.ExtendsKeyword ? 'EXTENDS' : 'IMPLEMENTS'
1640
+ for (const t of h.types) {
1641
+ let sym = null
1642
+ try { sym = checker.getSymbolAtLocation(t.expression) } catch { }
1643
+ const tgt = sym && resolveSymbol(sym)
1644
+ if (tgt && /^(class|type):/.test(tgt.id) && tgt.id !== id) { addEdge(id, tgt.id, kind, r, lineOf(t, sf), tgt.conf); nh++ }
1645
+ }
1646
+ }
1647
+ // each method overrides the nearest base class declaring a member of that name
1648
+ for (const m of decl.members || []) {
1649
+ if (!(ts.isMethodDeclaration(m) || ts.isGetAccessor(m) || ts.isSetAccessor(m)) || !declId.has(m) || !m.name || !ts.isIdentifier(m.name)) continue
1650
+ if (m.modifiers && m.modifiers.some(x => x.kind === ts.SyntaxKind.StaticKeyword)) continue
1651
+ let b = baseClassOf(decl), hit = null
1652
+ for (let i = 0; b && !hit && i < 12; i++) { hit = memberNode(b, m.name.text); if (!hit) b = baseClassOf(b) }
1653
+ if (hit && hit !== declId.get(m)) { addEdge(hit, declId.get(m), 'OVERRIDDEN_BY', r, lineOf(m, sf), 'exact'); no++ }
1654
+ // `implements AbstractRepo` (a class used as an interface): its declared methods are implemented here
1655
+ for (const ic of implementedClasses(decl)) {
1656
+ const im = memberNode(ic, m.name.text)
1657
+ if (im && im !== declId.get(m) && im !== hit) { addEdge(im, declId.get(m), 'IMPLEMENTED_BY', r, lineOf(m, sf), 'exact'); no++ }
1658
+ }
1659
+ }
1660
+ // interfaces this class or a base class implements -> their members
1661
+ const ifaces = ownIfaces(decl).map(d => [d, false])
1662
+ for (let b = baseClassOf(decl), i = 0; b && i < 12; b = baseClassOf(b), i++) for (const d of ownIfaces(b)) ifaces.push([d, true])
1663
+ if (ifaces.length) {
1664
+ const inherited = nm => { for (let b = baseClassOf(decl), i = 0; b && i < 12; b = baseClassOf(b), i++) if (memberNode(b, nm)) return true; return false }
1665
+ no += implementsIfaces(decl, r, sf, ifaces, 'exact', undefined, inherited)
1666
+ }
1667
+ }
1668
+ // structural: `new X()` where an interface type is expected (`const api: FeedAPI = new X()`, an argument, a return
1669
+ // value) and X does not declare `implements` it
1670
+ for (const sf of sourceFiles) {
1671
+ if (realFile(sf).endsWith('.vue') || !sf.text.includes('new ')) continue
1672
+ const r = rel(realFile(sf))
1673
+ const done = new Set()
1674
+ const walk = n => {
1675
+ if (ts.isNewExpression(n)) {
1676
+ let ct = null, sym = null
1677
+ try { ct = checker.getContextualType(n) } catch { }
1678
+ const ts0 = ct && (ct.aliasSymbol || ct.getSymbol())
1679
+ const ids = ts0 ? ifaceDecls(ts0).filter(projectDecl) : []
1680
+ if (ids.length) {
1681
+ try { sym = aliasTarget(checker.getSymbolAtLocation(n.expression)) } catch { }
1682
+ const cd = sym && (sym.declarations || []).find(x => ts.isClassDeclaration(x) && projectDecl(x) && declId.has(x))
1683
+ if (cd) for (const d of ids) {
1684
+ const key = `${declId.get(cd)}|${declId.get(d)}`
1685
+ if (done.has(key) || ownIfaces(cd).includes(d)) continue
1686
+ done.add(key)
1687
+ const csf = cd.getSourceFile()
1688
+ no += implementsIfaces(cd, rel(realFile(csf)), csf, [[d, false]], 'resolved', { via: ['structural'], at: `${r}:${lineOf(n, sf)}` }, () => false)
1689
+ }
1690
+ }
1691
+ }
1692
+ ts.forEachChild(n, walk)
1693
+ }
1694
+ walk(sf)
1695
+ }
1696
+ // object literals where an interface is expected (`const api: FeedAPI = { fetch() {..} }`, a factory returning one,
1697
+ // an argument, `satisfies`): their function members implement the interface's; a class passed as a value where a
1698
+ // constructor of an interface is expected (`register(FollowingFeedAPI)`): structurally, as for `new X()`
1699
+ let nol = 0
1700
+ for (const sf of sourceFiles) {
1701
+ if (realFile(sf).endsWith('.vue')) continue
1702
+ const r = rel(realFile(sf))
1703
+ const done = new Set()
1704
+ const walk = n => {
1705
+ if (ts.isObjectLiteralExpression(n)) {
1706
+ const fm = fnMembers(n).filter(p => declId.has(p))
1707
+ if (fm.length) for (const d of ctxIfaces(n)) for (const p of fm) {
1708
+ const nm = memberName(p), im = nm && ifaceMember(d, nm)
1709
+ if (im && im !== declId.get(p)) { addEdge(im, declId.get(p), 'IMPLEMENTED_BY', r, lineOf(p, sf), 'exact', { via: ['object_literal'] }); nol++ }
1710
+ }
1711
+ } else {
1712
+ const cd = classOfValue(n)
1713
+ if (cd && declId.has(cd)) for (const d of ctorIfaces(n)) {
1714
+ const key = `${declId.get(cd)}|${declId.get(d)}`
1715
+ if (done.has(key) || ownIfaces(cd).includes(d)) continue
1716
+ done.add(key)
1717
+ const csf = cd.getSourceFile()
1718
+ no += implementsIfaces(cd, rel(realFile(csf)), csf, [[d, false]], 'resolved', { via: ['structural'], at: `${r}:${lineOf(n, sf)}` }, () => false)
1719
+ }
1720
+ }
1721
+ ts.forEachChild(n, walk)
1722
+ }
1723
+ walk(sf)
1724
+ }
1725
+ // mixin members implement the merged interface's members (`interface Client extends UsersMix` + `class Client
1726
+ // extends mix(Base).with(Users)`)
1727
+ let nmx = 0
1728
+ for (const [decl, id] of declId) {
1729
+ if (!ts.isClassDeclaration(decl) || !id.startsWith('class:') || !projectSf(decl.getSourceFile())) continue
1730
+ const mix = mixinClasses(decl)
1731
+ if (!mix.length) continue
1732
+ const ifs = mergedIfaces(decl)
1733
+ if (!ifs.length) continue
1734
+ const at = `${rel(realFile(decl.getSourceFile()))}:${lineOf(decl, decl.getSourceFile())}`
1735
+ for (const ce of mix) {
1736
+ const csf = ce.getSourceFile(), cr = rel(realFile(csf))
1737
+ for (const m of ce.members || []) {
1738
+ if (!declId.has(m) || !declId.get(m).startsWith('method:') || (m.modifiers && m.modifiers.some(x => x.kind === ts.SyntaxKind.StaticKeyword))) continue
1739
+ const nm = memberName(m)
1740
+ if (!nm) continue
1741
+ for (const d of ifs) {
1742
+ const im = ifaceMember(d, nm)
1743
+ if (im && im !== declId.get(m)) { addEdge(im, declId.get(m), 'IMPLEMENTED_BY', cr, lineOf(m, csf), 'resolved', { via: ['mixin'], at }); nmx++; break }
1744
+ }
1745
+ }
1746
+ }
1747
+ }
1748
+ if (nmx) { stats.mixin_impl_edges = nmx; no += nmx }
1749
+ if (nol) { stats.object_literal_impl_edges = nol; no += nol }
1750
+ if (nh) stats.class_heritage_edges = nh
1751
+ if (no) stats.override_edges = no
1752
+ }
1753
+ const deferredParamCalls = [] // api calls whose URL depends on an enclosing-function parameter
1754
+ const subscriptions = [] // realtime channel subscriptions (laravel-echo, pusher-js, useEcho hooks)
1755
+ const visits = [] // browser tests opening a page: page.goto('/x'), cy.visit('/x')
1756
+
1757
+ // ---------- realtime: laravel-echo / pusher-js ----------
1758
+ const ECHO_SUB = { private: 'private', channel: 'public', join: 'presence', encryptedPrivate: 'private-encrypted' }
1759
+ const ECHO_HOOKS = { useEcho: 'private', useEchoPublic: 'public', useEchoPresence: 'presence', useEchoNotification: 'private', useEchoModel: 'private' }
1760
+ function declOfExpr(e) {
1761
+ try {
1762
+ const sym = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(e) ? e.name : e)
1763
+ return sym && (sym.declarations || [])[0]
1764
+ } catch { return null }
1765
+ }
1766
+ // how sure we are that `expr` is a laravel-echo (lib='echo') / pusher-js (lib='pusher') client: 'exact' | 'resolved' | 'heuristic' | null
1767
+ function clientKind(expr, lib) {
1768
+ const e = unwrap(expr)
1769
+ if (!e) return null
1770
+ const tre = lib === 'echo' ? /\bEcho\b/ : /\bPusher\b/
1771
+ const pkg = lib === 'echo' ? 'laravel-echo' : 'pusher-js'
1772
+ try { const s = checker.typeToString(checker.getNonNullableType(checker.getTypeAtLocation(e))); if (tre.test(s)) return 'exact' } catch { }
1773
+ if (FW.importSource && FW.importSource(e) === pkg) return 'exact'
1774
+ const d = declOfExpr(e)
1775
+ if (d && (ts.isVariableDeclaration(d) || ts.isPropertyDeclaration(d) || ts.isParameter(d) || ts.isPropertySignature(d))) {
1776
+ if (d.type && tre.test(d.type.getText())) return 'exact'
1777
+ if (d.initializer && new RegExp('new\\s+' + (lib === 'echo' ? 'Echo' : 'Pusher') + '\\b').test(d.initializer.getText())) return 'exact'
1778
+ }
1779
+ const txt = e.getText()
1780
+ if (lib === 'echo' && /^(window\.|globalThis\.)?Echo$/.test(txt)) return 'resolved'
1781
+ if (new RegExp('(^|\\.)\\$?' + lib + '(Instance|Client)?$', 'i').test(txt)) return 'heuristic'
1782
+ return null
1783
+ }
1784
+ function strVals(e) {
1785
+ if (!e) return []
1786
+ const v = evalStr(e)
1787
+ return shapeDedupe(v.vals).map(render)
1788
+ }
1789
+ // .listen('X') / .listenToAll / .notification / .listenForWhisper chained on (or called on a const holding) a subscription
1790
+ function listenedEvents(call) {
1791
+ const ev = new Set()
1792
+ const add = (m, c) => {
1793
+ if (m === 'listen' && c.arguments[0]) strVals(c.arguments[0]).forEach(x => ev.add(x))
1794
+ else if (m === 'listenToAll') ev.add('*')
1795
+ else if (m === 'notification') ev.add('(notification)')
1796
+ else if (m === 'listenForWhisper' && c.arguments[0]) strVals(c.arguments[0]).forEach(x => ev.add('whisper:' + x))
1797
+ }
1798
+ let n = call
1799
+ for (let i = 0; i < 20; i++) {
1800
+ const pa = n.parent
1801
+ if (!(pa && ts.isPropertyAccessExpression(pa) && pa.expression === n && pa.parent && ts.isCallExpression(pa.parent))) break
1802
+ add(pa.name.text, pa.parent); n = pa.parent
1803
+ }
1804
+ let holder = n.parent
1805
+ while (holder && (ts.isAwaitExpression(holder) || ts.isParenthesizedExpression(holder) || ts.isAsExpression(holder))) holder = holder.parent
1806
+ if (holder && ts.isVariableDeclaration(holder) && ts.isIdentifier(holder.name)) {
1807
+ const sym = checker.getSymbolAtLocation(holder.name)
1808
+ let scope = holder
1809
+ while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent
1810
+ const v = x => {
1811
+ if (ts.isCallExpression(x) && ts.isPropertyAccessExpression(x.expression) && ts.isIdentifier(unwrap(x.expression.expression))
1812
+ && checker.getSymbolAtLocation(unwrap(x.expression.expression)) === sym) add(x.expression.name.text, x)
1813
+ ts.forEachChild(x, v)
1814
+ }
1815
+ if (scope && sym) v(scope)
1816
+ }
1817
+ // this.channel = Echo.private(...) / ctx.channel = Echo.join(...): listeners chained on `<any>.channel` in the same
1818
+ // file (matched by property name)
1819
+ if (holder && ts.isBinaryExpression(holder) && holder.operatorToken.kind === ts.SyntaxKind.EqualsToken && holder.right === n
1820
+ && ts.isPropertyAccessExpression(holder.left)) {
1821
+ const prop = holder.left.name.text
1822
+ const v = x => {
1823
+ if (ts.isPropertyAccessExpression(x) && x.name.text === prop && x !== holder.left) {
1824
+ let c = x
1825
+ for (let i = 0; i < 20; i++) {
1826
+ const pa = c.parent
1827
+ if (!(pa && ts.isPropertyAccessExpression(pa) && pa.expression === c && pa.parent && ts.isCallExpression(pa.parent))) break
1828
+ add(pa.name.text, pa.parent); c = pa.parent
1829
+ }
1830
+ }
1831
+ ts.forEachChild(x, v)
1832
+ }
1833
+ v(holder.getSourceFile())
1834
+ }
1835
+ return [...ev]
1836
+ }
1837
+ function realtimeSub(node, callee) {
1838
+ if (!ts.isCallExpression(node)) return null
1839
+ if (ts.isIdentifier(callee) && ECHO_HOOKS[callee.text] && node.arguments[0]) {
1840
+ let names
1841
+ if (callee.text === 'useEchoModel') {
1842
+ const m = strVals(node.arguments[0]), id = node.arguments[1] ? strVals(node.arguments[1]) : ['{id}']
1843
+ names = m.flatMap(a => id.map(b => `${a}.${b}`))
1844
+ } else names = strVals(node.arguments[0])
1845
+ const evs = callee.text === 'useEchoNotification' ? ['(notification)'] : (node.arguments[1] && callee.text !== 'useEchoModel' ? strVals(node.arguments[1]) : [])
1846
+ const lib = FW.importSource ? FW.importSource(callee) : null
1847
+ return { client: callee.text, names, visibility: ECHO_HOOKS[callee.text], events: evs, conf: lib && /echo/.test(lib) ? 'exact' : 'resolved' }
1848
+ }
1849
+ if (!ts.isPropertyAccessExpression(callee) || !node.arguments[0]) return null
1850
+ const m = callee.name.text
1851
+ if (ECHO_SUB[m]) {
1852
+ const k = clientKind(callee.expression, 'echo')
1853
+ if (!k) return null
1854
+ return { client: 'echo', names: strVals(node.arguments[0]), visibility: ECHO_SUB[m], events: listenedEvents(node), conf: k }
1855
+ }
1856
+ if (m === 'subscribe') {
1857
+ const k = clientKind(callee.expression, 'pusher')
1858
+ if (!k) return null
1859
+ const out = { client: 'pusher', names: [], visibility: 'public', events: [], conf: k }
1860
+ for (const n of strVals(node.arguments[0])) {
1861
+ const pm = n.match(/^(private-encrypted-|private-|presence-)(.*)$/)
1862
+ if (pm) { out.visibility = pm[1] === 'presence-' ? 'presence' : pm[1] === 'private-' ? 'private' : 'private-encrypted'; out.names.push(pm[2]) } else out.names.push(n)
1863
+ }
1864
+ // channel.bind('event', cb) on the returned channel
1865
+ const evs = new Set()
1866
+ let holder = node.parent
1867
+ if (holder && ts.isPropertyAccessExpression(holder) && holder.name.text === 'bind' && holder.parent && ts.isCallExpression(holder.parent) && holder.parent.arguments[0]) strVals(holder.parent.arguments[0]).forEach(x => evs.add(x))
1868
+ if (holder && ts.isVariableDeclaration(holder) && ts.isIdentifier(holder.name)) {
1869
+ const sym = checker.getSymbolAtLocation(holder.name)
1870
+ let scope = holder; while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent
1871
+ const v = x => { if (ts.isCallExpression(x) && ts.isPropertyAccessExpression(x.expression) && x.expression.name.text === 'bind' && ts.isIdentifier(unwrap(x.expression.expression)) && checker.getSymbolAtLocation(unwrap(x.expression.expression)) === sym && x.arguments[0]) strVals(x.arguments[0]).forEach(e => evs.add(e)); ts.forEachChild(x, v) }
1872
+ if (scope && sym) v(scope)
1873
+ }
1874
+ out.events = [...evs]
1875
+ return out
1876
+ }
1877
+ return null
1878
+ }
1879
+
1880
+ // ---------- web / native bridges: Capacitor plugins, React Native native modules, Expo modules ----------
1881
+ // A call `X.m()` whose receiver X evaluates to a native module handle (registerPlugin('Name'), Plugins.Name,
1882
+ // NativeModules.Name, TurboModuleRegistry.get('Name'), requireNativeModule('Name'), an @capacitor/* plugin export) is
1883
+ // a bridge send: {protocol, module, method}. codegraph/bridges.py links it to the Kotlin / Java / Swift / ObjC side.
1884
+ const bridges = []
1885
+ const bridgeReceivers = []
1886
+ const ELECTRON = ['electron']
1887
+ const TAURI_PKGS = ['@tauri-apps/api']
1888
+ function handlerNode(a) {
1889
+ const u = unwrap(a)
1890
+ if (!u) return null
1891
+ if (declId.has(u)) return { id: declId.get(u), conf: 'exact' }
1892
+ if (ts.isIdentifier(u) || ts.isPropertyAccessExpression(u)) {
1893
+ let sym = null
1894
+ try { sym = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(u) ? u.name : u) } catch { }
1895
+ const t = sym && resolveSymbol(sym)
1896
+ if (t) return { id: t.id, conf: t.conf }
1897
+ }
1898
+ return null
1899
+ }
1900
+ function ipcFacts(node, callee, cur, r, line) {
1901
+ const testSf = testFiles.has(realFile(node.getSourceFile())) || undefined
1902
+ if (ts.isPropertyAccessExpression(callee)) {
1903
+ const m = callee.name.text
1904
+ const obj = unwrap(callee.expression)
1905
+ // renderer / preload -> main: ipcRenderer.invoke('ch') / send / sendSync / postMessage
1906
+ // project wrappers named after the Electron objects: ipcRendererManager.invoke / ipcMainManager.send(ch, ...)
1907
+ const wn = ts.isIdentifier(obj) ? obj.text : ts.isPropertyAccessExpression(obj) ? obj.name.text : ''
1908
+ if (['invoke', 'send', 'sendSync', 'postMessage'].includes(m) && /^ipc(Main|Renderer)[A-Z_]\w*$/.test(wn)) {
1909
+ const ch = ipcChan(node.arguments[0])
1910
+ if (ch) { bridges.push({ src: cur, file: r, line, protocol: 'electron-ipc', module: ch, method: null, conf: 'heuristic', via: [`${wn}.${m}`], process: wn.startsWith('ipcMain') ? 'main' : 'renderer', test: testSf }); return }
1911
+ }
1912
+ if (['invoke', 'send', 'sendSync', 'postMessage'].includes(m)) {
1913
+ const k = bridgeLib(obj, ['ipcRenderer'], ELECTRON)
1914
+ const ch = k && (strArg(node) ?? ipcChan(node.arguments[0]))
1915
+ if (ch) { bridges.push({ src: cur, file: r, line, protocol: 'electron-ipc', module: ch, method: null, conf: k, via: [`ipcRenderer.${m}`], process: 'renderer', test: testSf }); return }
1916
+ // main -> renderer: win.webContents.send('ch') / webContents.send
1917
+ // main -> renderer: win.webContents.send('ch'), event.sender.send (reply), a WebFrameMain (frame.send)
1918
+ if (m === 'send' || m === 'postMessage') {
1919
+ let tn = null
1920
+ try { const t = checker.getTypeAtLocation(obj); tn = t && t.symbol && t.symbol.name } catch { }
1921
+ const typed = tn === 'WebContents' || tn === 'WebFrameMain'
1922
+ const named = ['webContents', 'sender', 'mainFrame', 'frame'].includes(wn)
1923
+ const c2 = (typed || named) && (strArg(node) ?? ipcChan(node.arguments[0]))
1924
+ if (c2) { bridges.push({ src: cur, file: r, line, protocol: 'electron-ipc', module: c2, method: null, conf: typed || wn === 'webContents' ? 'resolved' : 'heuristic', via: [`${tn || wn}.${m}`], process: 'main', test: testSf }); return }
1925
+ }
1926
+ }
1927
+ // receivers: ipcMain.handle('ch', fn) / on / once, ipcRenderer.on('ch', fn)
1928
+ const info = ipcCallInfo(node)
1929
+ if (info && info.kind === 'ipc') {
1930
+ const k = info.wrapper ? 'heuristic' : bridgeLib(obj, [info.process === 'main' ? 'ipcMain' : 'ipcRenderer'], ELECTRON)
1931
+ if (testSf) return // a test registering a handler is not the app's receiver
1932
+ // a handler outside the project (`ipcMain.on('quit', app.quit)`): the function registering it receives
1933
+ const h = k && (handlerNode(node.arguments[1]) || (cur && !cur.startsWith('module:') ? { id: cur, conf: 'heuristic' } : null))
1934
+ if (h) for (const ch of info.channels)
1935
+ bridgeReceivers.push({ protocol: 'electron-ipc', module: ch, method: null, handler: h.id, file: r, line, conf: info.union ? 'heuristic' : k === 'exact' ? h.conf : k, via: info.label.replace(/\(.*/, ''), process: info.process, union: info.union || undefined })
1936
+ return
1937
+ }
1938
+ if (info && info.kind === 'expose') {
1939
+ if (testSf) return
1940
+ const o = unwrap(node.arguments[1])
1941
+ if (o && ts.isObjectLiteralExpression(o)) for (const p of o.properties) {
1942
+ const pn = p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)) ? p.name.text : null
1943
+ const h = pn && handlerNode(ts.isPropertyAssignment(p) ? p.initializer : ts.isShorthandPropertyAssignment(p) ? p.name : p)
1944
+ if (h) bridgeReceivers.push({ protocol: 'electron-preload', module: info.key, method: pn, handler: h.id, file: r, line: lineOf(p), conf: h.conf, via: 'contextBridge.exposeInMainWorld', process: 'preload' })
1945
+ }
1946
+ return
1947
+ }
1948
+ // renderer -> preload: window.api.ping() / globalThis.api.ping() for an exposed key
1949
+ let ob = obj
1950
+ if (ts.isIdentifier(obj) && exposedKeys.size) { // const api = window.api; api.ping()
1951
+ try {
1952
+ const d = (checker.getSymbolAtLocation(obj) || {}).valueDeclaration
1953
+ const init = d && ts.isVariableDeclaration(d) && unwrap(d.initializer)
1954
+ if (init && ts.isPropertyAccessExpression(init)) ob = init
1955
+ } catch { }
1956
+ }
1957
+ if (ts.isPropertyAccessExpression(ob) && exposedKeys.has(ob.name.text)) {
1958
+ const obj = ob
1959
+ const root = unwrap(obj.expression)
1960
+ if (root && ts.isIdentifier(root) && ['window', 'globalThis', 'self'].includes(root.text)) {
1961
+ bridges.push({ src: cur, file: r, line, protocol: 'electron-preload', module: obj.name.text, method: m, conf: 'resolved', via: [`${root.text}.${obj.name.text}`], process: 'renderer', test: testSf })
1962
+ return
1963
+ }
1964
+ }
1965
+ }
1966
+ // Tauri: invoke('cmd', args) from @tauri-apps/api/core (v2) or /tauri (v1), window.__TAURI__.core.invoke
1967
+ const isInvoke = (ts.isIdentifier(callee) && callee.text === 'invoke') || (ts.isPropertyAccessExpression(callee) && callee.name.text === 'invoke')
1968
+ if (isInvoke) {
1969
+ let k = null
1970
+ if (ts.isIdentifier(callee)) {
1971
+ const lib = FW.importSource ? FW.importSource(callee) : null
1972
+ if (lib && TAURI_PKGS.some(p => lib === p || lib.startsWith(p + '/'))) k = 'exact'
1973
+ } else if (/__TAURI__|__TAURI_INTERNALS__/.test(callee.expression.getText())) k = 'resolved'
1974
+ else {
1975
+ const o = unwrap(callee.expression)
1976
+ const lib = o && ts.isIdentifier(o) && FW.importSource ? FW.importSource(o) : null
1977
+ if (lib && TAURI_PKGS.some(p => lib === p || lib.startsWith(p + '/'))) k = 'exact'
1978
+ }
1979
+ const cmd = k && strArg(node)
1980
+ if (cmd) {
1981
+ const plugin = cmd.match(/^plugin:([^|]+)\|/)
1982
+ bridges.push({ src: cur, file: r, line, protocol: 'tauri', module: cmd, method: null, conf: k, via: ['invoke'], process: 'webview', test: testSf,
1983
+ external: plugin ? `tauri-plugin-${plugin[1]}` : undefined })
1984
+ }
1985
+ }
1986
+ }
1987
+ const BRIDGE_SKIP = new Set(['then', 'catch', 'finally', 'bind', 'call', 'apply', 'toString', 'hasOwnProperty'])
1988
+ // bridge calls whose module / method / event name is not a literal cg can evaluate (cg bridges lists them, #61)
1989
+ const bridgeDynamic = []
1990
+ // events React Native itself emits through DeviceEventEmitter / NativeEventEmitter (no project native sender)
1991
+ const RN_CORE_EVENTS = new Set(['keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide', 'keyboardDidHide', 'keyboardWillChangeFrame',
1992
+ 'keyboardDidChangeFrame', 'hardwareBackPress', 'appStateDidChange', 'memoryWarning', 'change', 'url', 'didUpdateDimensions',
1993
+ 'accessibilityServiceChanged', 'screenReaderChanged', 'reduceMotionChanged', 'appearanceChanged', 'websocketMessage', 'websocketOpen',
1994
+ 'websocketClosed', 'websocketFailed', 'didReceiveNetworkResponse', 'didReceiveNetworkData', 'didCompleteNetworkResponse',
1995
+ 'remoteNotificationReceived', 'localNotificationReceived', 'remoteNotificationsRegistered', 'remoteNotificationRegistrationError'])
1996
+ const EXPO_EMITTER_PKGS = ['expo-modules-core', 'expo']
1997
+ // a React Native / Expo event emitter: new NativeEventEmitter(Module), DeviceEventEmitter, NativeAppEventEmitter,
1998
+ // new EventEmitter(ExpoModule) (expo-modules-core), an Expo native module handle itself -> {module, conf, via}
1999
+ function rnEmitterOf(e, depth) {
2000
+ e = unwrap(e)
2001
+ if (!e || depth > 4) return null
2002
+ const k = bridgeLib(e, ['DeviceEventEmitter', 'NativeAppEventEmitter'], RN_PKGS)
2003
+ if (k) return { module: null, conf: k, via: e.text }
2004
+ if (ts.isNewExpression(e) && ts.isIdentifier(e.expression)) {
2005
+ const nm = e.expression.text
2006
+ const lib = FW.importSource ? FW.importSource(e.expression) : null
2007
+ if ((nm === 'NativeEventEmitter' && (!lib || RN_PKGS.includes(lib))) || (nm === 'EventEmitter' && lib && EXPO_EMITTER_PKGS.includes(lib))) {
2008
+ const bm = e.arguments && e.arguments[0] ? bridgeModuleOf(e.arguments[0], 0) : null
2009
+ return { module: bm ? bm.module : null, conf: lib ? 'exact' : 'resolved', via: `new ${nm}` }
2010
+ }
2011
+ return null
2012
+ }
2013
+ if (ts.isIdentifier(e) || ts.isPropertyAccessExpression(e)) {
2014
+ const bm = bridgeModuleOf(e, 0)
2015
+ if (bm && bm.protocol === 'react-native' && bm.api === 'expo-modules') return { module: bm.module, conf: bm.conf, via: 'expo module' }
2016
+ let sym
2017
+ try { sym = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(e) ? e.name : e) } catch { return null }
2018
+ for (const d of (sym && sym.declarations) || []) {
2019
+ const init = (ts.isVariableDeclaration(d) || ts.isPropertyDeclaration(d) || ts.isPropertyAssignment(d)) ? d.initializer : null
2020
+ const r = init && rnEmitterOf(init, depth + 1)
2021
+ if (r) return r
2022
+ }
2023
+ }
2024
+ return null
2025
+ }
2026
+ // JS side of native -> app events: Capacitor Plugin.addListener('evt', cb), RN emitter.addListener('evt', cb)
2027
+ // events the JS side emits itself (DeviceEventEmitter.emit('evt') as an in-app event bus): not a native bridge
2028
+ const jsEmitted = new Set()
2029
+ function listenerFacts(node, callee, cur, r, line, testSf) {
2030
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'emit') {
2031
+ const evt = strArg(node)
2032
+ if (evt && rnEmitterOf(callee.expression, 0)) jsEmitted.add(evt)
2033
+ return
2034
+ }
2035
+ if (testSf || !ts.isPropertyAccessExpression(callee) || !['addListener', 'addEventListener'].includes(callee.name.text)) return
2036
+ const h = handlerNode(node.arguments[1]) || (cur && !cur.startsWith('module:') ? { id: cur, conf: 'heuristic' } : null)
2037
+ const bm = bridgeModuleOf(callee.expression, 0)
2038
+ const evt = strArg(node)
2039
+ if (bm && bm.protocol === 'capacitor') {
2040
+ if (!evt) { bridgeDynamic.push({ file: r, line, protocol: 'capacitor-event', what: `${bm.module}.addListener(<dynamic>)` }); return }
2041
+ if (h) bridgeReceivers.push({ protocol: 'capacitor-event', module: bm.module, method: evt, handler: h.id, file: r, line, conf: bm.conf === 'exact' ? h.conf : bm.conf,
2042
+ via: `${bm.module}.addListener`, external: bm.external })
2043
+ return
2044
+ }
2045
+ const em = rnEmitterOf(callee.expression, 0)
2046
+ if (!em) return
2047
+ if (!evt) { bridgeDynamic.push({ file: r, line, protocol: 'react-native-event', what: `${em.via}.addListener(<dynamic>)` }); return }
2048
+ if (!em.module && RN_CORE_EVENTS.has(evt)) return
2049
+ if (h) bridgeReceivers.push({ protocol: 'react-native-event', module: evt, method: null, handler: h.id, file: r, line, conf: em.conf === 'exact' ? h.conf : em.conf,
2050
+ via: `${em.via}.addListener`, emitter_module: em.module || undefined })
2051
+ }
2052
+ // Cordova: cordova.exec(ok, fail, 'Service', 'action', args) / exec(...) from 'cordova/exec'
2053
+ function cordovaFacts(node, callee, cur, r, line, testSf) {
2054
+ let ok = false
2055
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'exec' && /(^|\.)cordova$/.test(unwrap(callee.expression).getText())) ok = true
2056
+ else if (ts.isIdentifier(callee) && callee.text === 'exec') {
2057
+ const lib = FW.importSource ? FW.importSource(callee) : null
2058
+ let req = false
2059
+ try {
2060
+ const d = ((checker.getSymbolAtLocation(callee) || {}).declarations || [])[0]
2061
+ const init = d && ts.isVariableDeclaration(d) && unwrap(d.initializer)
2062
+ req = !!(init && ts.isCallExpression(init) && ts.isIdentifier(init.expression) && init.expression.text === 'require' && init.arguments[0]
2063
+ && ts.isStringLiteralLike(init.arguments[0]) && init.arguments[0].text === 'cordova/exec')
2064
+ } catch { }
2065
+ ok = lib === 'cordova/exec' || req
2066
+ }
2067
+ if (!ok || node.arguments.length < 4) return
2068
+ const sv = a => { const u = unwrap(a); if (u && ts.isStringLiteralLike(u)) return u.text; const v = u ? strVals(u) : []; return v.length === 1 && !/[\u0001{]/.test(v[0]) ? v[0] : null }
2069
+ const svc = sv(node.arguments[2]), act = sv(node.arguments[3])
2070
+ if (!svc || !act) { bridgeDynamic.push({ file: r, line, protocol: 'cordova', what: `cordova.exec(${svc || '<dynamic>'}, ${act || '<dynamic>'})` }); return }
2071
+ bridges.push({ src: cur, file: r, line, protocol: 'cordova', module: svc, method: act, conf: 'resolved', via: ['cordova.exec'], test: testSf || undefined })
2072
+ }
2073
+ const CAP_BUILTIN = new Set(['addListener', 'removeAllListeners', 'removeListener', 'notifyListeners']) // listener plumbing of the bridge
2074
+ const CAP_CORE_EXPORTS = new Set(['Capacitor', 'registerPlugin', 'Plugins', 'WebPlugin', 'CapacitorHttp', 'CapacitorCookies', 'WebView', 'SplashScreen'])
2075
+ const bridgeCache = new Map()
2076
+ const atOf = e => { const sf = e.getSourceFile(); return rel(realFile(sf)) + ':' + lineOf(e, sf) }
2077
+ function bridgeLib(e, names, pkgs) {
2078
+ const id = unwrap(e)
2079
+ if (!id || !ts.isIdentifier(id) || !names.includes(id.text)) return null
2080
+ const lib = FW.importSource ? FW.importSource(id) : null
2081
+ if (lib && pkgs.some(p => lib === p || lib.startsWith(p + '/'))) return 'exact'
2082
+ if (lib) return null // a local NativeModules / Plugins of another library
2083
+ try { const s = checker.getSymbolAtLocation(id); if (s && (s.declarations || []).some(d => projectSf(d.getSourceFile()))) return null } catch { }
2084
+ return 'resolved'
2085
+ }
2086
+ const RN_PKGS = ['react-native', 'react-native-web', 'react-native-windows', 'react-native-macos']
2087
+ const CAP_PKGS = ['@capacitor/core']
2088
+ const EXPO_PKGS = ['expo-modules-core', 'expo']
2089
+ function strArg(c) {
2090
+ const a = c.arguments && unwrap(c.arguments[0])
2091
+ if (!a) return null
2092
+ if (ts.isStringLiteralLike(a)) return a.text
2093
+ const v = strVals(a)
2094
+ return v.length === 1 && !/[\u0001{]/.test(v[0]) ? v[0] : null
2095
+ }
2096
+ function moduleFile(spec, fromSf) {
2097
+ if (!spec.startsWith('.')) {
2098
+ let f = null
2099
+ try { f = (ts.resolveModuleName(spec, fromSf.fileName, options, host).resolvedModule || {}).resolvedFileName } catch { }
2100
+ return f ? program.getSourceFile(f) || null : null
2101
+ }
2102
+ const base = path.resolve(path.dirname(fromSf.fileName), spec)
2103
+ for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']) {
2104
+ const sf = program.getSourceFile(base + ext)
2105
+ if (sf) return sf
2106
+ }
2107
+ return null
2108
+ }
2109
+ function defaultExportOf(sf) {
2110
+ for (const st of sf.statements) if (ts.isExportAssignment(st) && !st.isExportEquals) return st.expression
2111
+ return null
2112
+ }
2113
+ // {protocol, module, conf, via, api?, external?} for a native module handle expression, else null
2114
+ function bridgeModuleOf(e, depth) {
2115
+ e = unwrap(e)
2116
+ if (!e || depth > 8) return null
2117
+ if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) {
2118
+ const name = ts.isPropertyAccessExpression(e) ? e.name.text : (e.argumentExpression && ts.isStringLiteralLike(e.argumentExpression) ? e.argumentExpression.text : null)
2119
+ if (name) {
2120
+ let k = bridgeLib(e.expression, ['NativeModules'], RN_PKGS)
2121
+ if (k) return { protocol: 'react-native', module: name, conf: k, via: ['NativeModules'], at: atOf(e) }
2122
+ k = bridgeLib(e.expression, ['Plugins'], CAP_PKGS)
2123
+ if (k) return { protocol: 'capacitor', module: name, conf: k, via: ['Plugins'], at: atOf(e) }
2124
+ // Capacitor.Plugins.Name / window.Capacitor.Plugins.Name
2125
+ const pe = unwrap(e.expression)
2126
+ if (ts.isPropertyAccessExpression(pe) && pe.name.text === 'Plugins' && /(^|\.)(Capacitor|cap)$/.test(unwrap(pe.expression).getText()))
2127
+ return { protocol: 'capacitor', module: name, conf: 'resolved', via: ['Capacitor.Plugins'], at: atOf(e) }
2128
+ }
2129
+ // require('./NativeFoo').default
2130
+ if (ts.isPropertyAccessExpression(e) && e.name.text === 'default') {
2131
+ const c = unwrap(e.expression)
2132
+ if (c && ts.isCallExpression(c) && ts.isIdentifier(c.expression) && c.expression.text === 'require' && c.arguments[0] && ts.isStringLiteralLike(c.arguments[0])) {
2133
+ const sf = moduleFile(c.arguments[0].text, e.getSourceFile())
2134
+ const x = sf && defaultExportOf(sf)
2135
+ const r = x && bridgeModuleOf(x, depth + 1)
2136
+ if (r) return { ...r, via: [...r.via, 'require'] }
2137
+ return null
2138
+ }
2139
+ }
2140
+ if (!ts.isPropertyAccessExpression(e)) return null
2141
+ }
2142
+ if (ts.isCallExpression(e)) {
2143
+ const c = unwrap(e.expression)
2144
+ // registerPlugin('Name') (imported from @capacitor/core, or a local re-export of it) / Capacitor.registerPlugin('Name')
2145
+ if ((ts.isIdentifier(c) && c.text === 'registerPlugin') || (ts.isPropertyAccessExpression(c) && c.name.text === 'registerPlugin'
2146
+ && /(^|\.)(Capacitor|cap)$/.test(unwrap(c.expression).getText()))) {
2147
+ const lib = ts.isIdentifier(c) && FW.importSource ? FW.importSource(c) : null
2148
+ const n = strArg(e)
2149
+ if (n && (!lib || lib === '@capacitor/core' || lib.startsWith('.'))) return { protocol: 'capacitor', module: n, conf: lib === '@capacitor/core' ? 'exact' : 'resolved', via: ['registerPlugin'], at: atOf(e) }
2150
+ }
2151
+ if (ts.isPropertyAccessExpression(c) && ['get', 'getEnforcing'].includes(c.name.text)) {
2152
+ const k = bridgeLib(c.expression, ['TurboModuleRegistry'], RN_PKGS)
2153
+ const n = k && strArg(e)
2154
+ if (n) return { protocol: 'react-native', module: n, conf: k, via: ['TurboModuleRegistry'], at: atOf(e) }
2155
+ }
2156
+ // Object.assign(NativeModule, { helpers }) is still the native module
2157
+ // (the members the other arguments add are JS helpers, not native methods)
2158
+ if (ts.isPropertyAccessExpression(c) && c.name.text === 'assign' && ts.isIdentifier(c.expression) && c.expression.text === 'Object' && e.arguments.length) {
2159
+ const r = bridgeModuleOf(e.arguments[0], depth + 1)
2160
+ if (!r) return null
2161
+ const js = [...(r.jsOnly || [])]
2162
+ for (const a of e.arguments.slice(1)) {
2163
+ const o = unwrap(a)
2164
+ if (o && ts.isObjectLiteralExpression(o)) for (const p of o.properties) if (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name))) js.push(p.name.text)
2165
+ }
2166
+ return js.length ? { ...r, jsOnly: js } : r
2167
+ }
2168
+ if (ts.isIdentifier(c) && ['requireNativeModule', 'requireOptionalNativeModule'].includes(c.text)) {
2169
+ const lib = FW.importSource ? FW.importSource(c) : null
2170
+ const n = strArg(e)
2171
+ if (n && (!lib || EXPO_PKGS.includes(lib))) return { protocol: 'react-native', module: n, conf: lib ? 'exact' : 'resolved', via: [c.text], api: 'expo-modules', at: atOf(e) }
2172
+ }
2173
+ return null
2174
+ }
2175
+ if (ts.isConditionalExpression(e)) return bridgeModuleOf(e.whenFalse, depth + 1) || bridgeModuleOf(e.whenTrue, depth + 1)
2176
+ if (ts.isBinaryExpression(e) && [ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(e.operatorToken.kind))
2177
+ return bridgeModuleOf(e.left, depth + 1) || bridgeModuleOf(e.right, depth + 1)
2178
+ if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return null
2179
+ let sym
2180
+ try { sym = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(e) ? e.name : e) } catch { return null }
2181
+ if (!sym) return null
2182
+ if (bridgeCache.has(sym)) return bridgeCache.get(sym)
2183
+ bridgeCache.set(sym, null)
2184
+ let out = null
2185
+ const first = (sym.declarations || [])[0]
2186
+ // an import from an @capacitor/* plugin package (node_modules is not indexed): the export name is the plugin name
2187
+ if (first && ts.isImportSpecifier(first)) {
2188
+ let p = first; while (p && !ts.isImportDeclaration(p)) p = p.parent
2189
+ const lib = p && ts.isStringLiteral(p.moduleSpecifier) ? p.moduleSpecifier.text : ''
2190
+ const nm = (first.propertyName || first.name).text
2191
+ if (/^@capacitor(-community)?\/|^@capawesome(-team)?\//.test(lib) && lib !== '@capacitor/core' && /^[A-Z]/.test(nm) && !CAP_CORE_EXPORTS.has(nm)) {
2192
+ let target = null
2193
+ try { target = checker.getAliasedSymbol(sym) } catch { }
2194
+ const td = target && (target.declarations || [])[0]
2195
+ if (!td || !projectSf(td.getSourceFile())) out = { protocol: 'capacitor', module: nm, conf: 'heuristic', via: ['package-export'], external: lib }
2196
+ }
2197
+ }
2198
+ if (!out) {
2199
+ let s = sym
2200
+ for (let i = 0; i < 6 && s && (s.flags & ts.SymbolFlags.Alias); i++) { try { s = checker.getAliasedSymbol(s) } catch { s = null } }
2201
+ for (const d of (s && s.declarations) || []) {
2202
+ if (ts.isVariableDeclaration(d) && d.initializer) out = bridgeModuleOf(d.initializer, depth + 1)
2203
+ else if ((ts.isPropertyDeclaration(d) || ts.isPropertyAssignment(d)) && d.initializer) out = bridgeModuleOf(d.initializer, depth + 1)
2204
+ else if (ts.isExportAssignment(d)) out = bridgeModuleOf(d.expression, depth + 1)
2205
+ else if (ts.isBindingElement(d) && ts.isObjectBindingPattern(d.parent) && ts.isVariableDeclaration(d.parent.parent) && d.parent.parent.initializer) {
2206
+ const nm = d.propertyName && ts.isIdentifier(d.propertyName) ? d.propertyName.text : (ts.isIdentifier(d.name) ? d.name.text : null)
2207
+ const init = d.parent.parent.initializer
2208
+ if (nm) {
2209
+ let k = bridgeLib(init, ['NativeModules'], RN_PKGS)
2210
+ if (k) out = { protocol: 'react-native', module: nm, conf: k, via: ['NativeModules'], at: atOf(d) }
2211
+ else if ((k = bridgeLib(init, ['Plugins'], CAP_PKGS))) out = { protocol: 'capacitor', module: nm, conf: k, via: ['Plugins'], at: atOf(d) }
2212
+ }
2213
+ }
2214
+ if (out) break
2215
+ }
2216
+ }
2217
+ bridgeCache.set(sym, out)
2218
+ return out
2219
+ }
2220
+
2221
+ // the innermost if / else / ternary / `&&` / switch case around a construction or a JSX element (#88): attrs.branch
2222
+ // (`if (editing)`, `else of open ?`, `case 'settings'`, `user &&`) and attrs.branch_line, so the parent that swaps a
2223
+ // component or object under a condition shows on its INSTANTIATES / RENDERS edge
2224
+ function branchOf(node, sf) {
2225
+ let prev = node, x = node.parent
2226
+ const lab = (t) => t.replace(/\s+/g, ' ').slice(0, 80)
2227
+ const at = (n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1
2228
+ while (x && !ts.isFunctionLike(x) && !ts.isSourceFile(x) && !ts.isClassLike(x)) {
2229
+ if (ts.isIfStatement(x) && prev !== x.expression) {
2230
+ const c = `if (${x.expression.getText(sf)})`
2231
+ return { branch: lab(prev === x.elseStatement ? `else of ${c}` : c), branch_line: at(x) }
2232
+ }
2233
+ if (ts.isConditionalExpression(x) && prev !== x.condition) {
2234
+ const c = `${x.condition.getText(sf)} ?`
2235
+ return { branch: lab(prev === x.whenFalse ? `else of ${c}` : c), branch_line: at(x) }
2236
+ }
2237
+ if (ts.isBinaryExpression(x) && prev === x.right && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(x.operatorToken.kind))
2238
+ return { branch: lab(`${x.left.getText(sf)} ${x.operatorToken.getText(sf)}`), branch_line: at(x) }
2239
+ if (ts.isCaseClause(x) && prev !== x.expression) return { branch: lab(`case ${x.expression.getText(sf)}`), branch_line: at(x) }
2240
+ if (ts.isDefaultClause(x)) return { branch: 'default', branch_line: at(x) }
2241
+ prev = x; x = x.parent
2242
+ }
2243
+ return null
2244
+ }
2245
+ function edgeKindFor(targetId, isCall) {
2246
+ const k = targetId.split(':')[0]
2247
+ if (k === 'composable') return 'USES_COMPOSABLE'
2248
+ if (k === 'store') return 'USES_STORE'
2249
+ if (k === 'class') return 'INSTANTIATES'
2250
+ if (k === 'type') return 'REFERENCES_TYPE'
2251
+ return 'CALLS'
2252
+ }
2253
+
2254
+ for (const sf of sourceFiles) { const real = realFile(sf); optionsFields(sf, rel(real), fileNode.get(real)) }
2255
+ for (const sf of sourceFiles) {
2256
+ const real = realFile(sf), r = rel(real), fid = fileNode.get(real)
2257
+ const isVue = real.endsWith('.vue')
2258
+ const stack = [fid]
2259
+ const fnStack = []
2260
+ const visit = node => {
2261
+ const own = !isVue && declId.get(node)
2262
+ if (own) stack.push(own)
2263
+ const isFnNode = ts.isFunctionLike(node)
2264
+ if (isFnNode) fnStack.push(node)
2265
+ const cur = stack[stack.length - 1]
2266
+ if (ts.isVariableDeclaration(node) && node.initializer && cur) stateDecl(node, cur, sf, r)
2267
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && cur) stateRef(node, cur, sf, r)
2268
+ if (ts.isPropertyAccessExpression(node) && optNames.has(node.name.text) && cur) optionsRef(node, cur, sf, r)
2269
+ // imports
2270
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
2271
+ const ms = checker.getSymbolAtLocation(node.moduleSpecifier)
2272
+ const d = ms && (ms.declarations || [])[0]
2273
+ if (d && ts.isSourceFile(d) && projectSf(d)) addEdge(fid, fileNode.get(realFile(d)), 'IMPORTS', r, lineOf(node, sf), 'exact')
2274
+ }
2275
+ // re-exports (`export {a as b} from './m'`, `export {x}`, `export * from './m'`): the module defines those names
2276
+ // too (attrs.reexports: exported name -> node id; reexports_all: modules re-exported whole), and depends on './m'
2277
+ if (!isVue && ts.isExportDeclaration(node)) {
2278
+ let target = null
2279
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
2280
+ const ms = checker.getSymbolAtLocation(node.moduleSpecifier)
2281
+ const d = ms && (ms.declarations || [])[0]
2282
+ if (d && ts.isSourceFile(d) && projectSf(d)) {
2283
+ target = fileNode.get(realFile(d))
2284
+ addEdge(fid, target, 'IMPORTS', r, lineOf(node, sf), 'exact', { reexport: true })
2285
+ }
2286
+ }
2287
+ const mo = modNode.get(fid)
2288
+ if (mo && node.exportClause && ts.isNamedExports(node.exportClause)) {
2289
+ for (const el of node.exportClause.elements) {
2290
+ let s = null
2291
+ try { s = node.moduleSpecifier ? checker.getSymbolAtLocation(el.propertyName || el.name) : checker.getExportSpecifierLocalTargetSymbol(el) } catch { }
2292
+ const res = s && resolveSymbol(s)
2293
+ if (res && res.id && res.id !== fid) (mo.attrs.reexports ||= {})[el.name.text] = res.id
2294
+ else if (!res && !(node.isTypeOnly || el.isTypeOnly)) (mo.attrs.reexports_external ||= []).push(el.name.text) // from a package
2295
+ }
2296
+ } else if (mo && target && !node.exportClause) (mo.attrs.reexports_all ||= []).push(target)
2297
+ }
2298
+ // `export const X = Imported` / `= ns.member`: an alias the module defines (re-export of a project or package symbol)
2299
+ if (!isVue && ts.isVariableStatement(node) && node.parent === sf && (node.modifiers || []).some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
2300
+ const mo = modNode.get(fid)
2301
+ for (const d of node.declarationList.declarations) {
2302
+ const init = d.initializer && unwrap(d.initializer)
2303
+ if (!mo || !ts.isIdentifier(d.name) || !init || !(ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) || declId.get(d)) continue
2304
+ let s = null
2305
+ try { s = checker.getSymbolAtLocation(init) } catch { }
2306
+ const res = s && resolveSymbol(s)
2307
+ if (res && res.id && res.id !== fid) (mo.attrs.reexports ||= {})[d.name.text] = res.id
2308
+ else if (!res) (mo.attrs.reexports_external ||= []).push(d.name.text)
2309
+ }
2310
+ }
2311
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) handleCall(node, cur, sf, r, fnStack[fnStack.length - 1])
2312
+ // literal fallbacks: `a ?? b ?? 'X'` / `a || 'X'` (outermost operator only)
2313
+ if (ts.isBinaryExpression(node) && FALLBACK_OPS.has(node.operatorToken.kind)
2314
+ && !(ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === node.operatorToken.kind)) {
2315
+ const ops = []
2316
+ let x = node
2317
+ while (ts.isBinaryExpression(x) && x.operatorToken.kind === node.operatorToken.kind) { ops.unshift(x.right); x = x.left }
2318
+ ops.unshift(x)
2319
+ const last = unwrap(ops[ops.length - 1])
2320
+ if (ts.isStringLiteralLike(last) || ts.isNumericLiteral(last)) {
2321
+ fallbacks.push({ owner: cur, file: r, line: lineOf(node, sf), op: node.operatorToken.getText(sf),
2322
+ chain: ops.map(o => o.getText(sf).replace(/\s+/g, ' ').slice(0, 120)), literal: ts.isNumericLiteral(last) ? Number(last.text) : last.text,
2323
+ template: isVue && isTemplateLine(node, sf) || undefined })
2324
+ }
2325
+ }
2326
+ // identifier references to project functions/stores/composables that are not direct callees (callbacks, template handlers)
2327
+ if (ts.isIdentifier(node) && !isDeclName(node) && !(node.parent && (ts.isCallExpression(node.parent) || ts.isNewExpression(node.parent)) && node.parent.expression === node)
2328
+ && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node && ts.isCallExpression(node.parent.parent) && node.parent.parent.expression === node.parent)
2329
+ && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent) && !ts.isExportSpecifier(node.parent)
2330
+ && !ts.isBindingElement(node.parent) && !ts.isShorthandPropertyAssignment(node.parent) && !ts.isTypeReferenceNode(node.parent) && !ts.isQualifiedName(node.parent)) {
2331
+ const sym = checker.getSymbolAtLocation(node)
2332
+ if (sym) {
2333
+ const t = resolveSymbol(sym)
2334
+ if (t && t.id !== cur) {
2335
+ const k = t.id.split(':')[0]
2336
+ if (['function', 'composable', 'method', 'store', 'component', 'page', 'layout'].includes(k)) {
2337
+ const tplTag = isVue && isTemplateLine(node, sf) && /^__tplc_/.test(enclosingFnName(node))
2338
+ const jsxTag = node.parent && (ts.isJsxOpeningElement(node.parent) || ts.isJsxSelfClosingElement(node.parent)) && node.parent.tagName === node
2339
+ if (jsxTag) addEdge(cur, t.id, 'RENDERS', r, lineOf(node, sf), t.conf === 'exact' ? 'exact' : 'resolved', { via: ['jsx', ...t.via], ...(branchOf(node.parent, sf) || {}) })
2340
+ else if (tplTag) addEdge(cur, t.id, 'RENDERS', r, lineOf(node, sf), t.conf === 'exact' ? 'exact' : 'resolved', { via: ['template', ...t.via] })
2341
+ else if (!['component', 'page', 'layout'].includes(k)) addEdge(cur, t.id, edgeKindFor(t.id), r, lineOf(node, sf), t.conf, { ref: true, via: t.via.length ? t.via : undefined, template: isVue && isTemplateLine(node, sf) || undefined })
2342
+ }
2343
+ }
2344
+ }
2345
+ }
2346
+ // stored class fields (#88): `this.count`, `cart.items`, `this.#secret`, resolved by the checker
2347
+ if (ts.isPropertyAccessExpression(node) && fieldNames.has(node.name.text)) {
2348
+ let s = null
2349
+ try { s = checker.getSymbolAtLocation(node.name) } catch { }
2350
+ const fid = s && fieldIds.get(s.valueDeclaration || (s.declarations || [])[0])
2351
+ if (fid && fid !== cur && cur) {
2352
+ const p = node.parent; let via
2353
+ let write = (ts.isBinaryExpression(p) && p.left === node && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
2354
+ || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken))
2355
+ || ts.isDeleteExpression(p)
2356
+ if (!write && ts.isElementAccessExpression(p) && p.expression === node && p.parent && ((ts.isBinaryExpression(p.parent) && p.parent.left === p
2357
+ && p.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment) || ts.isDeleteExpression(p.parent))) { write = true; via = 'item' }
2358
+ if (!write && ts.isPropertyAccessExpression(p) && p.expression === node && TS_MUTATING.has(p.name.text) && p.parent && ts.isCallExpression(p.parent) && p.parent.expression === p) { write = true; via = 'mutating' }
2359
+ const recv = node.expression.kind === ts.SyntaxKind.ThisKeyword ? 'this' : node.expression.getText(sf).slice(0, 40)
2360
+ addEdge(cur, fid, write ? 'WRITES_PROP' : 'READS_PROP', r, lineOf(node, sf), 'exact', { receiver: recv, ...(via ? { via } : {}) })
2361
+ stats[write ? 'stored_field_writes' : 'stored_field_reads'] = (stats[write ? 'stored_field_writes' : 'stored_field_reads'] || 0) + 1
2362
+ }
2363
+ }
2364
+ // enum members and constants (#84): `Color.Red`, `MAX`, an imported `MAX`, `ns.MAX`; resolved by the checker
2365
+ if (ts.isImportSpecifier(node) && node.propertyName && valueNames.has(node.propertyName.text)) valueNames.add(node.name.text)
2366
+ if (ts.isIdentifier(node) && valueNames.has(node.text) && !(node.parent && node.parent.name === node
2367
+ && (ts.isVariableDeclaration(node.parent) || ts.isEnumMember(node.parent) || ts.isPropertyDeclaration(node.parent)))
2368
+ && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent) && !ts.isExportSpecifier(node.parent)
2369
+ && !ts.isQualifiedName(node.parent)) {
2370
+ let s = null
2371
+ try { s = checker.getSymbolAtLocation(node) } catch { }
2372
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s) } catch { s = null } }
2373
+ const vd = s && (s.valueDeclaration || (s.declarations || [])[0])
2374
+ const vid = vd && valueIds.get(vd)
2375
+ if (vid && vid !== cur) addEdge(cur, vid, 'USES_VALUE', r, lineOf(node, sf), 'exact', { how: 'checker' })
2376
+ }
2377
+ if (ts.isTypeReferenceNode(node)) {
2378
+ const sym = checker.getSymbolAtLocation(ts.isQualifiedName(node.typeName) ? node.typeName.right : node.typeName)
2379
+ const t = sym && resolveSymbol(sym)
2380
+ if (t && t.id.startsWith('type:') || t && t.id.startsWith('class:')) addEdge(cur, t.id, 'REFERENCES_TYPE', r, lineOf(node, sf), t.conf)
2381
+ }
2382
+ ts.forEachChild(node, visit)
2383
+ if (own) stack.pop()
2384
+ if (isFnNode) fnStack.pop()
2385
+ }
2386
+ ts.forEachChild(sf, visit)
2387
+ }
2388
+ function enclosingFnName(n) { while (n && !ts.isFunctionDeclaration(n)) n = n.parent; return n && n.name ? n.name.text : '' }
2389
+ function isDeclName(n) {
2390
+ const p = n.parent
2391
+ return p && (p.name === n) && (ts.isVariableDeclaration(p) || ts.isFunctionDeclaration(p) || ts.isParameter(p) || ts.isPropertyAssignment(p)
2392
+ || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p) || ts.isClassDeclaration(p) || ts.isInterfaceDeclaration(p) || ts.isTypeAliasDeclaration(p)
2393
+ || ts.isPropertySignature(p) || ts.isEnumDeclaration(p) || ts.isPropertyAccessExpression(p) || ts.isEnumMember(p))
2394
+ }
2395
+
2396
+ function handleCall(node, cur, sf, r, encFn) {
2397
+ const callee = unwrap(node.expression)
2398
+ const line = lineOf(node, sf)
2399
+ const inTpl = isTemplateLine(node, sf)
2400
+ // i18n keys
2401
+ const cname = ts.isIdentifier(callee) ? callee.text : (ts.isPropertyAccessExpression(callee) ? callee.name.text : '')
2402
+ if (I18N_FNS.has(cname) && node.arguments && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) {
2403
+ i18nUses.push({ src: cur, key: node.arguments[0].text, file: r, line })
2404
+ }
2405
+ if (cname === 'definePageMeta' && node.arguments[0]) {
2406
+ const o = unwrap(node.arguments[0]); const m = {}
2407
+ const lay = objProp(o, 'layout'); if (lay && ts.isStringLiteralLike(unwrap(lay))) m.layout = unwrap(lay).text
2408
+ const mw = objProp(o, 'middleware')
2409
+ if (mw) { const u = unwrap(mw); m.middleware = ts.isArrayLiteralExpression(u) ? u.elements.filter(ts.isStringLiteralLike).map(x => x.text) : (ts.isStringLiteralLike(u) ? [u.text] : []) }
2410
+ pageMeta[r] = m
2411
+ }
2412
+ // HTTP
2413
+ let http = null
2414
+ let axiosInst = null
2415
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(callee) && (HTTP_METHODS.has(callee.name.text) || callee.name.text === 'request')
2416
+ && (isAxiosType(callee.expression) || (FW.importSource && (axiosInst = axiosCreateOf(callee.expression))))) {
2417
+ const recvType = checker.typeToString(checker.getTypeAtLocation(callee.expression))
2418
+ const isInstance = /AxiosInstance/.test(recvType) || !!axiosInst
2419
+ let method = callee.name.text.toUpperCase(), urlExpr = node.arguments[0], cfgObj = null
2420
+ if (callee.name.text === 'request') { cfgObj = node.arguments[0]; urlExpr = objProp(cfgObj, 'url'); const m = objProp(cfgObj, 'method'); method = m && ts.isStringLiteralLike(unwrap(m)) ? unwrap(m).text.toUpperCase() : 'GET' }
2421
+ let base = null, baseConf = 'exact', baseVia = []
2422
+ if (isInstance) {
2423
+ const o = axiosCreateOf(callee.expression)
2424
+ if (o) { const b = objProp(o.cfg, 'baseURL'); if (b) { const v = evalStr(b); base = v.vals; baseConf = v.conf; baseVia = o.via } }
2425
+ else { baseConf = 'heuristic'; baseVia = ['instance-origin-unknown'] }
2426
+ }
2427
+ const lower = callee.name.text
2428
+ const cfgArg = ['get', 'delete', 'head', 'options'].includes(lower) ? node.arguments[1] : node.arguments[2]
2429
+ const qp = objProp(cfgArg, 'params')
2430
+ const body = ['post', 'put', 'patch'].includes(lower) ? node.arguments[1] : null
2431
+ http = { client: isInstance ? 'axios-instance' : 'axios', method, urlExpr, base, baseConf, baseVia,
2432
+ query: qp ? keysOut(requestKeys(qp)) : undefined, body: body ? keysOut(requestKeys(body)) : undefined }
2433
+ } else if (ts.isCallExpression(node) && (http = generatedClientCall(node, callee))) {
2434
+ // OpenAPI-generated clients: this.request({ path, method }) / __request(OpenAPI, { method, url })
2435
+ } else if (ts.isCallExpression(node) && (http = kyOrInstanceCall(node, callee))) {
2436
+ // ky / ofetch.create / $fetch.create instances
2437
+ } else if (ts.isCallExpression(node) && ts.isIdentifier(callee) && /^useSWR(Immutable|Infinite)?$|^useQuery$/.test(callee.text) && callee.text.startsWith('useSWR') && node.arguments[0]) {
2438
+ // SWR: the key is the URL fetched by the (global or passed) fetcher; `cond ? url : null` keys keep the URL branch
2439
+ let k = unwrap(node.arguments[0])
2440
+ if (ts.isConditionalExpression(k)) k = [k.whenTrue, k.whenFalse].map(unwrap).find(x => x.kind !== ts.SyntaxKind.NullKeyword && !(ts.isIdentifier(x) && x.text === 'undefined')) || k
2441
+ if (ts.isBinaryExpression(k) && k.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) k = unwrap(k.right)
2442
+ if (ts.isArrayLiteralExpression(k)) k = unwrap(k.elements[0])
2443
+ if (k && (ts.isStringLiteralLike(k) || ts.isTemplateExpression(k) || ts.isBinaryExpression(k) || ts.isIdentifier(k) || ts.isCallExpression(k)))
2444
+ http = { client: 'swr', method: 'GET', urlExpr: k, base: null, baseConf: 'exact', baseVia: [] }
2445
+ } else if (ts.isCallExpression(node) && ts.isIdentifier(callee) && FETCH_FNS.has(callee.text)) {
2446
+ const o = node.arguments[1]; const m = objProp(o, 'method')
2447
+ const method = m && ts.isStringLiteralLike(unwrap(m)) ? unwrap(m).text.toUpperCase() : 'GET'
2448
+ const b = objProp(o, 'baseURL')
2449
+ const bv = b ? evalStr(b) : null
2450
+ const fq = objProp(o, 'query') || objProp(o, 'params'), fb = objProp(o, 'body')
2451
+ http = { client: callee.text, method, urlExpr: node.arguments[0], base: bv ? bv.vals : null, baseConf: bv ? bv.conf : 'exact', baseVia: [],
2452
+ query: fq ? keysOut(requestKeys(fq)) : undefined, body: fb ? keysOut(requestKeys(fb)) : undefined }
2453
+ }
2454
+ // browser / API tests: page.goto('/x'), cy.visit('/x'); Playwright request.get('/api/x'), cy.request('/api/x')
2455
+ const testSf = testFiles.has(realFile(sf))
2456
+ if (testSf && !http && ts.isCallExpression(node) && node.arguments[0]) {
2457
+ const recvTxt = ts.isPropertyAccessExpression(callee) ? unwrap(callee.expression).getText() : ''
2458
+ if (ts.isPropertyAccessExpression(callee) && ((callee.name.text === 'goto' && /(^|\.)page$|Page$/i.test(recvTxt)) || (callee.name.text === 'visit' && recvTxt === 'cy'))) {
2459
+ for (const u of strVals(node.arguments[0])) visits.push({ src: cur, file: r, line, url: u, via: `${recvTxt}.${callee.name.text}` })
2460
+ } else if (ts.isPropertyAccessExpression(callee) && (HTTP_METHODS.has(callee.name.text) || callee.name.text === 'fetch') && /(^|\.)request$|apiContext|requestContext/i.test(recvTxt)) {
2461
+ const o = node.arguments[1]; const mo = objProp(o, 'method')
2462
+ const method = callee.name.text === 'fetch' ? (mo && ts.isStringLiteralLike(unwrap(mo)) ? unwrap(mo).text.toUpperCase() : 'GET') : callee.name.text.toUpperCase()
2463
+ http = { client: 'playwright-request', method, urlExpr: node.arguments[0], base: null, baseConf: 'exact', baseVia: [] }
2464
+ } else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'request' && recvTxt === 'cy') {
2465
+ const a0 = unwrap(node.arguments[0])
2466
+ if (ts.isObjectLiteralExpression(a0)) { const mo = objProp(a0, 'method'), uo = objProp(a0, 'url'); if (uo) http = { client: 'cypress', method: mo && ts.isStringLiteralLike(unwrap(mo)) ? unwrap(mo).text.toUpperCase() : 'GET', urlExpr: uo, base: null, baseConf: 'exact', baseVia: [] } }
2467
+ else if (node.arguments.length >= 2 && ts.isStringLiteralLike(a0) && /^[A-Z]+$/.test(a0.text)) http = { client: 'cypress', method: a0.text, urlExpr: node.arguments[1], base: null, baseConf: 'exact', baseVia: [] }
2468
+ else http = { client: 'cypress', method: 'GET', urlExpr: node.arguments[0], base: null, baseConf: 'exact', baseVia: [] }
2469
+ }
2470
+ }
2471
+ const sub = realtimeSub(node, callee)
2472
+ if (sub && sub.names.length) subscriptions.push({ src: cur, file: r, line, ...sub, test: testSf || undefined })
2473
+ if (http) {
2474
+ const params = new Set()
2475
+ const v = http.urlExpr ? evalStr(http.urlExpr, 0, { params }) : { vals: [PH('?')], conf: 'resolved' }
2476
+ const rec = { src: cur, file: r, line, client: http.client, method: http.method, template: inTpl || undefined, test: testSf || undefined,
2477
+ urls: shapeDedupe(v.vals).map(render), url_conf: v.conf, base: http.base && shapeDedupe(http.base).map(render), base_conf: http.baseConf, base_via: http.baseVia,
2478
+ expr: http.urlExpr ? http.urlExpr.getText().slice(0, 160) : null, query: http.query, body: http.body }
2479
+ // URL built from a parameter of the enclosing function: expand at its call sites (1 level)
2480
+ const depParams = [...params].filter(p => encFn && encFn.parameters && encFn.parameters.some(x => ts.isIdentifier(x.name) && x.name.text === p))
2481
+ if (depParams.length && encFn) deferredParamCalls.push({ rec, encFn, urlExpr: http.urlExpr })
2482
+ apiCalls.push(rec)
2483
+ }
2484
+ // Electron IPC / context bridge / Tauri commands -> endpoint:<protocol>:<name> (codegraph/bridges.py)
2485
+ if (ts.isCallExpression(node)) ipcFacts(node, callee, cur, r, line)
2486
+ if (ts.isCallExpression(node)) { listenerFacts(node, callee, cur, r, line, testSf); cordovaFacts(node, callee, cur, r, line, testSf) }
2487
+ // web / native bridge sends: Capacitor.nativePromise('Plugin', 'method', ...) / nativeCallback (the low-level bridge)
2488
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(callee) && ['nativePromise', 'nativeCallback'].includes(callee.name.text)
2489
+ && node.arguments.length >= 2 && /(^|\.)(Capacitor|cap)$/.test(unwrap(callee.expression).getText())) {
2490
+ const pn = strArg(node), mn = ts.isStringLiteralLike(unwrap(node.arguments[1])) ? unwrap(node.arguments[1]).text : null
2491
+ if (pn && mn) bridges.push({ src: cur, file: r, line, method: mn, protocol: 'capacitor', module: pn, conf: 'exact', via: [callee.name.text], test: testSf || undefined })
2492
+ } else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(callee) && !BRIDGE_SKIP.has(callee.name.text)) {
2493
+ const ce = unwrap(callee.expression)
2494
+ if (ce && ts.isElementAccessExpression(ce) && !(ce.argumentExpression && ts.isStringLiteralLike(ce.argumentExpression))) {
2495
+ if (bridgeLib(ce.expression, ['NativeModules'], RN_PKGS))
2496
+ bridgeDynamic.push({ file: r, line, protocol: 'react-native', what: `NativeModules[${ce.argumentExpression ? ce.argumentExpression.getText().slice(0, 40) : '?'}].${callee.name.text}` })
2497
+ else if (bridgeLib(ce.expression, ['Plugins'], CAP_PKGS))
2498
+ bridgeDynamic.push({ file: r, line, protocol: 'capacitor', what: `Plugins[${ce.argumentExpression ? ce.argumentExpression.getText().slice(0, 40) : '?'}].${callee.name.text}` })
2499
+ }
2500
+ const bm = bridgeModuleOf(callee.expression, 0)
2501
+ if (bm && !(bm.protocol === 'capacitor' && CAP_BUILTIN.has(callee.name.text)) && !(bm.jsOnly && bm.jsOnly.includes(callee.name.text))) {
2502
+ const { jsOnly, ...fact } = bm
2503
+ bridges.push({ src: cur, file: r, line, method: callee.name.text, ...fact, test: testSf || undefined })
2504
+ }
2505
+ }
2506
+ // call edges
2507
+ let sym = null
2508
+ if (ts.isIdentifier(callee)) sym = checker.getSymbolAtLocation(callee)
2509
+ else if (ts.isPropertyAccessExpression(callee)) sym = checker.getSymbolAtLocation(callee.name)
2510
+ if (!sym) return
2511
+ const t = resolveSymbol(sym, ts.isIdentifier(callee) ? 'exact' : 'resolved')
2512
+ if (!t) return
2513
+ let kind = edgeKindFor(t.id, true)
2514
+ if (kind === 'REFERENCES_TYPE') return
2515
+ const recv = kind === 'CALLS' && ts.isPropertyAccessExpression(callee) ? receiverClasses(callee.expression, t.id) : null
2516
+ const br = kind === 'INSTANTIATES' && !inTpl ? branchOf(node, sf) : null
2517
+ addEdge(cur, t.id, kind, r, line, t.conf, { via: t.via.length ? t.via : undefined, template: inTpl || undefined, recv: recv || undefined, ...(br || {}) })
2518
+ callSites.push({ target: t.id, node, cur, r, line })
2519
+ }
2520
+
2521
+ // `b.run()` landing on `Base.run` (B inherits it): the receiver's project classes (attrs.recv), so `impact B.run` can
2522
+ // keep only the calls whose receiver can be a B. Only for methods of classes / interfaces something extends or implements
2523
+ var baseTypeIds = null // var: handleCall runs before this line is reached
2524
+ function receiverClasses(expr, targetId) {
2525
+ if (!targetId.startsWith('method:')) return null
2526
+ if (!baseTypeIds) baseTypeIds = new Set(edges.filter(e => e.kind === 'EXTENDS' || e.kind === 'IMPLEMENTS').map(e => e.dst))
2527
+ const tn = nodeById.get(targetId)
2528
+ if (!tn || !tn.parent || !baseTypeIds.has(tn.parent)) return null
2529
+ let ty = null
2530
+ try { ty = checker.getTypeAtLocation(expr) } catch { return null }
2531
+ if (!ty) return null
2532
+ const out = []
2533
+ for (const m of ty.isUnion() ? ty.types : [ty]) {
2534
+ const sym = m.getSymbol && m.getSymbol()
2535
+ const ds = (sym && sym.declarations) || [] // a merged `interface X` + `class X`: the class
2536
+ const d = ds.find(x => ts.isClassDeclaration(x) && declId.has(x)) || ds.find(x => ts.isInterfaceDeclaration(x) && declId.has(x))
2537
+ if (!d) return null // a receiver type outside the project classes: unknown
2538
+ const id = declId.get(d)
2539
+ if (!out.includes(id)) out.push(id)
2540
+ }
2541
+ return out.length && !(out.length === 1 && out[0] === tn.parent) ? out : null
2542
+ }
2543
+
2544
+ function generatedClientCall(node, callee) {
2545
+ // swagger-typescript-api: this.request({ path: `/users/${id}`, method: 'GET', query, body })
2546
+ // openapi-typescript-codegen: __request(OpenAPI, { method: 'GET', url: '/users/{id}', path: { id }, query, body })
2547
+ let o = null
2548
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'request' && node.arguments.length === 1) o = unwrap(node.arguments[0])
2549
+ else if (ts.isIdentifier(callee) && /^_*request$/.test(callee.text) && node.arguments.length === 2) o = unwrap(node.arguments[1])
2550
+ if (!o || !ts.isObjectLiteralExpression(o)) return null
2551
+ const m = objProp(o, 'method')
2552
+ if (!m || !ts.isStringLiteralLike(unwrap(m))) return null
2553
+ const pathProp = objProp(o, 'path'), urlProp = objProp(o, 'url')
2554
+ const strish = e => !!e && (ts.isStringLiteralLike(e) || ts.isTemplateExpression(e) || ts.isBinaryExpression(e))
2555
+ const urlExpr = pathProp && strish(unwrap(pathProp)) ? pathProp : urlProp && strish(unwrap(urlProp)) ? urlProp : null
2556
+ if (!urlExpr) return null
2557
+ const q = objProp(o, 'query'), b = objProp(o, 'body')
2558
+ // the base URL is client configuration (new Api({ baseUrl }) / OpenAPI.BASE): origin unknown
2559
+ return { client: 'openapi-client', method: unwrap(m).text.toUpperCase(), urlExpr, base: [PH('baseUrl')], baseConf: 'resolved', baseVia: ['generated-client'],
2560
+ query: q ? keysOut(requestKeys(q)) : undefined, body: b ? keysOut(requestKeys(b)) : undefined }
2561
+ }
2562
+ function kyOrInstanceCall(node, callee) {
2563
+ if (!FW.importSource) return null
2564
+ let recv = null, method = null, optsArg = null
2565
+ if (ts.isIdentifier(callee) || ts.isCallExpression(callee)) { recv = callee; optsArg = node.arguments[1] } // api(...) / useApi()(...)
2566
+ else if (ts.isPropertyAccessExpression(callee) && HTTP_METHODS.has(callee.name.text)) { recv = callee.expression; method = callee.name.text.toUpperCase(); optsArg = node.arguments[1] }
2567
+ else return null
2568
+ let lib = FW.importSource(recv), inst = null
2569
+ if (lib === 'ky' && ts.isIdentifier(unwrap(recv)) && unwrap(recv).text !== 'ky' && !ts.isIdentifier(callee)) lib = 'ky'
2570
+ if (lib !== 'ky') { inst = clientCreateOf(recv); if (!inst) return null; lib = inst.lib }
2571
+ if (lib === 'ofetch' || lib === '$fetch') { if (method) return null } // ofetch instances are called directly
2572
+ const m = objProp(optsArg, 'method')
2573
+ method = method || (m && ts.isStringLiteralLike(unwrap(m)) ? unwrap(m).text.toUpperCase() : 'GET')
2574
+ let base = null, baseConf = 'exact'
2575
+ for (let i = inst; i; i = i.parent) {
2576
+ const b = objProp(i.cfg, 'prefixUrl') || objProp(i.cfg, 'prefix') || objProp(i.cfg, 'baseURL') || objProp(i.cfg, 'baseUrl')
2577
+ if (b) { const v = evalStr(b); base = v.vals; baseConf = v.conf; break }
2578
+ }
2579
+ const q = objProp(optsArg, 'searchParams') || objProp(optsArg, 'query') || objProp(optsArg, 'params')
2580
+ const b = objProp(optsArg, 'json') || objProp(optsArg, 'body')
2581
+ return { client: lib === 'ky' ? (inst ? 'ky-instance' : 'ky') : 'ofetch-instance', method, urlExpr: node.arguments[0], base, baseConf, baseVia: inst ? [`${lib}.create`] : [],
2582
+ query: q ? keysOut(requestKeys(q)) : undefined, body: b ? keysOut(requestKeys(b)) : undefined }
2583
+ }
2584
+
2585
+ // 1-level expansion of parameter-dependent URLs at the enclosing function's call sites
2586
+ let expanded = 0
2587
+ for (const d of deferredParamCalls) {
2588
+ const fid = declToNode(d.encFn)
2589
+ if (!fid) continue
2590
+ const sites = callSites.filter(c => c.target === fid)
2591
+ for (const s of sites) {
2592
+ if (!ts.isCallExpression(s.node)) continue
2593
+ const v = evalStr(d.urlExpr, 0, { fn: d.encFn, args: s.node.arguments })
2594
+ if (v.vals.every(x => /^\u0001[^\u0002]*\u0002$/.test(x))) continue
2595
+ apiCalls.push({ ...d.rec, src: s.cur, file: s.r, line: s.line, urls: shapeDedupe(v.vals).map(render), url_conf: 'resolved',
2596
+ via_helper: { fn: fid, at: `${d.rec.file}:${d.rec.line}` } })
2597
+ expanded++
2598
+ }
2599
+ }
2600
+
2601
+ const httpFns = new Set(apiCalls.map(a => a.src))
2602
+ for (const c of callSites) {
2603
+ if (!httpFns.has(c.target) || !ts.isCallExpression(c.node)) continue
2604
+ // object keys passed per argument: literals, variables, and builder calls (requestKeys follows initialisers
2605
+ // and project-function returns, up to 4 levels); conditional keys (set under an if) are kept apart
2606
+ const rks = c.node.arguments.map(a => {
2607
+ const r = keysOut(requestKeys(a))
2608
+ return (r.keys.length || r.conditional.length) && !(r.opaque && !r.keys.length) ? r : null
2609
+ })
2610
+ if (!rks.some(Boolean)) continue
2611
+ const e = edges.find(x => x.src === c.cur && x.dst === c.target && x.line === c.line)
2612
+ if (e) e.attrs = { ...(e.attrs || {}), arg_keys: rks.map(r => r ? r.keys : null),
2613
+ arg_keys_conditional: rks.some(r => r && r.conditional.length) ? rks.map(r => r ? r.conditional : null) : undefined,
2614
+ arg_keys_partial: rks.some(r => r && (r.opaque || r.forwarded)) ? rks.map(r => !!(r && (r.opaque || r.forwarded))) : undefined }
2615
+ }
2616
+
2617
+ // ---------- component tags not resolved by imports: Nuxt global components ----------
2618
+ for (const [file, info] of sfcInfo) {
2619
+ const src = fileNode.get(file); const r = rel(file)
2620
+ if (!src) continue
2621
+ const resolvedTags = new Set(edges.filter(e => e.src === src && e.kind === 'RENDERS').map(e => e.line + ':' + e.dst))
2622
+ for (const tg of info.tags) {
2623
+ const already = edges.some(e => e.src === src && e.kind === 'RENDERS' && e.line === tg.line)
2624
+ if (already) { stats.components_imported++; continue }
2625
+ const target = globalComponents[tg.name]
2626
+ if (target && fileNode.has(target)) { addEdge(src, fileNode.get(target), 'RENDERS', r, tg.line, 'resolved', { via: ['nuxt-global-component'], tag: tg.tag }); stats.components_global++ }
2627
+ else if (target) stats.components_external++
2628
+ else { stats.components_unknown++; stats.unknown_tags[tg.name] = (stats.unknown_tags[tg.name] || 0) + 1 }
2629
+ }
2630
+ }
2631
+
2632
+ stats.nodes = nodes.length
2633
+ stats.edges = edges.length
2634
+ stats.api_calls = apiCalls.length
2635
+ stats.api_calls_param_expanded = expanded
2636
+ stats.subscriptions = subscriptions.length
2637
+ stats.bridge_sends = bridges.length
2638
+ stats.local_packages = localPkgs.length ? localPkgs : undefined
2639
+ stats.test_visits = visits.length
2640
+ stats.program_files = program.getSourceFiles().length
2641
+ stats.seconds_program = (tProgram - t0) / 1000
2642
+ stats.seconds_total = (Date.now() - t0) / 1000
2643
+ const top = Object.entries(stats.unknown_tags).sort((a, b) => b[1] - a[1]).slice(0, 15)
2644
+ stats.unknown_tags = Object.fromEntries(top)
2645
+ stats.seconds_fw_facts = tFw / 1000
2646
+ stats.fw_facts = fwFacts ? { classes: fwFacts.classes.length, calls: fwFacts.calls.length, member_calls: fwFacts.member_calls.length, env: fwFacts.env.length, budget_left: fwFacts.budget_left } : null
2647
+ stats.config = { tsconfig: noConfig ? null : (parsed.packageConfigs ? null : rel(tsconfigPath)), root_files: rootNames.length,
2648
+ ...(parsed.packageConfigs ? { package_tsconfigs: parsed.packageConfigs } : {}) }
2649
+ fs.writeFileSync(cfg.out, JSON.stringify({ nodes, edges, api_calls: apiCalls, i18n: i18nUses, fallbacks, sfc_i18n: sfcI18n, page_meta: pageMeta, fw: fwFacts,
2650
+ subscriptions, bridges, bridge_receivers: bridgeReceivers.filter(b => !(b.protocol === 'react-native-event' && jsEmitted.has(b.module))), bridge_dynamic: bridgeDynamic, visits, test_files: [...testFiles].map(rel).sort(), config_defaults: configDefaults,
2651
+ skipped_links: [...new Set(skippedLinks)].sort(), stats }))
2652
+ console.log(JSON.stringify(stats))