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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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))
|