@weft-js/cli 0.1.0 → 0.1.2
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.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +174 -188
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +387 -0
- package/src/deploy/needs.mjs +149 -0
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -42
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +144 -116
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +194 -92
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/src/infer/engine.cjs
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
// Prop inference (research 44). Facts come from syntax; the types come from whichever checker the host wraps, asked
|
|
2
2
|
// about the call sites in the program's own terms. A round writes each component's annotation into its file's shadow;
|
|
3
3
|
// the next round sees the types that made possible (a prop forwarded to another inferred component, a component passed
|
|
4
|
-
// as a prop). Rounds stop when nothing changes
|
|
4
|
+
// as a prop). Rounds stop when nothing changes. A checker starts for inference or core box diagnostics.
|
|
5
5
|
|
|
6
|
-
const { fileFacts, hashOf
|
|
7
|
-
const
|
|
6
|
+
const { fileFacts, hashOf } = require('./facts.cjs')
|
|
7
|
+
const CORE_IMPORT = /['"]@weft-js\/core['"]/
|
|
8
|
+
const { shadow, toShadow } = require('./shadow.cjs')
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const quote = key => (/^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key))
|
|
13
|
-
const unique = list => [...new Set(list)]
|
|
14
|
-
const union = list => {
|
|
15
|
-
const parts = unique(list.filter(Boolean))
|
|
16
|
-
return parts.length ? parts.map(t => (/=>/.test(t) && !/^\(.*\)$/.test(t) ? `(${t})` : t)).join(' | ') : null
|
|
17
|
-
}
|
|
18
|
-
const sameFile = (a, b) => a === b || a.toLowerCase() === b.toLowerCase()
|
|
19
|
-
const overlaps = (a, b) => a[0] < b[1] && b[0] < a[1]
|
|
10
|
+
const { flows, collect, fromBody, infer } = require('./engine/flow.cjs')
|
|
11
|
+
const { conflicts, shapes, unprintable, errors, affected, run } = require('./engine/checks.cjs')
|
|
12
|
+
const { note: noteFinding, annotation, report, origin } = require('./engine/report.cjs')
|
|
20
13
|
|
|
21
14
|
class Engine {
|
|
22
15
|
/** @param {{ ts: any, read: (file: string) => string | undefined, routes?: (file: string) => boolean }} options */
|
|
23
16
|
constructor({ ts, read, routes }) {
|
|
24
17
|
this.ts = ts
|
|
25
18
|
this.read = read
|
|
26
|
-
this.routes = routes ?? (f => /\/src\/routes\//.test(f))
|
|
19
|
+
this.routes = routes ?? ((f) => /\/src\/routes\//.test(f))
|
|
27
20
|
this.facts = new Map()
|
|
28
21
|
this.texts = new Map()
|
|
29
22
|
this.maps = new Map() // file → shadow, for files that differ from their source
|
|
@@ -38,7 +31,14 @@ class Engine {
|
|
|
38
31
|
|
|
39
32
|
/** New text for a file. @returns whether anything about it changed */
|
|
40
33
|
set(file, text = this.read(file)) {
|
|
41
|
-
if (text === undefined) {
|
|
34
|
+
if (text === undefined) {
|
|
35
|
+
const had = this.facts.delete(file)
|
|
36
|
+
this.texts.delete(file)
|
|
37
|
+
this.maps.delete(file)
|
|
38
|
+
this.typed.delete(file)
|
|
39
|
+
this.flowCache.delete(file)
|
|
40
|
+
return had
|
|
41
|
+
}
|
|
42
42
|
const before = this.facts.get(file)
|
|
43
43
|
this.texts.set(file, text)
|
|
44
44
|
const hash = hashOf(text)
|
|
@@ -48,18 +48,52 @@ class Engine {
|
|
|
48
48
|
return true
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
comps() {
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
comps() {
|
|
52
|
+
return [...this.facts.values()].flatMap((f) => f.comps)
|
|
53
|
+
}
|
|
54
|
+
/** Facts that need a checker: inferred props, bindings or possible core box errors. */
|
|
55
|
+
needed() {
|
|
56
|
+
return (
|
|
57
|
+
this.typedChecksNeeded() ||
|
|
58
|
+
[...this.facts.values()].some(
|
|
59
|
+
(f) => f.comps.length || f.sites.some((s) => s.attrs.some((a) => a.bind)),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Syntax only gates startup; the checker proves every W008 finding. */
|
|
65
|
+
boxPropsNeeded() {
|
|
66
|
+
const sites = [...this.facts.values()].some((facts) =>
|
|
67
|
+
facts.sites.some((site) => site.attrs.length),
|
|
68
|
+
)
|
|
69
|
+
return sites && [...this.texts.values()].some((text) => CORE_IMPORT.test(text))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Syntax only gates startup; TS2540 and resolved core symbols prove readonly writes. */
|
|
73
|
+
derivedWritesNeeded() {
|
|
74
|
+
return (
|
|
75
|
+
[...this.texts.values()].some((text) => CORE_IMPORT.test(text)) &&
|
|
76
|
+
[...this.texts.values()].some((text) => /\bvalue\b/.test(text))
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether either core box diagnostic family needs the shared checker. */
|
|
81
|
+
typedChecksNeeded() {
|
|
82
|
+
return this.boxPropsNeeded() || this.derivedWritesNeeded()
|
|
83
|
+
}
|
|
54
84
|
|
|
55
85
|
/** Files with a component or a site of one that import `file`: what they pass may have changed type. */
|
|
56
86
|
dependents(file) {
|
|
57
87
|
const base = file.replace(/^.*[\\/]/, '').replace(/\.[^.]*$/, '')
|
|
58
|
-
const names = new Set(this.comps().map(c => c.name))
|
|
88
|
+
const names = new Set(this.comps().map((c) => c.name))
|
|
59
89
|
const out = new Set()
|
|
60
90
|
for (const f of this.facts.values()) {
|
|
61
|
-
if (!f.comps.length && !f.sites.some(s => names.has(s.tag))) continue
|
|
62
|
-
for (const { from } of f.imports.values())
|
|
91
|
+
if (!f.comps.length && !f.sites.some((s) => names.has(s.tag))) continue
|
|
92
|
+
for (const { from } of f.imports.values())
|
|
93
|
+
if (from === base || from.endsWith(`/${base}`)) {
|
|
94
|
+
out.add(f.file)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
63
97
|
}
|
|
64
98
|
return out
|
|
65
99
|
}
|
|
@@ -67,38 +101,55 @@ class Engine {
|
|
|
67
101
|
/** Whether a saved file can move anything: it holds a component or a site of one, or such a file imports it. */
|
|
68
102
|
relevant(file) {
|
|
69
103
|
if (!this.needed()) return false
|
|
104
|
+
if (this.typedChecksNeeded()) return true
|
|
70
105
|
const facts = this.facts.get(file)
|
|
71
|
-
if (!facts || facts.comps.length || facts.sites.some(s => s.attrs.some(a => a.bind)))
|
|
72
|
-
|
|
73
|
-
|
|
106
|
+
if (!facts || facts.comps.length || facts.sites.some((s) => s.attrs.some((a) => a.bind)))
|
|
107
|
+
return true
|
|
108
|
+
const names = new Set(this.comps().map((c) => c.name))
|
|
109
|
+
return (
|
|
110
|
+
facts.sites.some((s) => names.has(s.tag) || (this.flowing?.has(file) ?? false)) ||
|
|
111
|
+
this.dependents(file).size > 0
|
|
112
|
+
)
|
|
74
113
|
}
|
|
75
114
|
|
|
76
115
|
/** `comp` for a tag that needs no checker: a name in the same file, or a local wrap of one. */
|
|
77
116
|
direct(facts, site) {
|
|
78
|
-
const names = new Set(),
|
|
117
|
+
const names = new Set(),
|
|
118
|
+
todo = [site.tag.split('.')[0]]
|
|
79
119
|
while (todo.length) {
|
|
80
120
|
const n = todo.pop()
|
|
81
121
|
if (names.has(n)) continue
|
|
82
122
|
names.add(n)
|
|
83
123
|
todo.push(...(facts.wraps.get(n) ?? []))
|
|
84
124
|
}
|
|
85
|
-
return facts.comps.filter(c => names.has(c.name))
|
|
125
|
+
return facts.comps.filter((c) => names.has(c.name))
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
shadowOf(file) {
|
|
128
|
+
shadowOf(file) {
|
|
129
|
+
return this.maps.get(file)
|
|
130
|
+
}
|
|
89
131
|
|
|
90
132
|
edits(facts) {
|
|
91
133
|
const edits = []
|
|
92
|
-
for (const site of facts.sites)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
for (const site of facts.sites)
|
|
135
|
+
for (const a of site.attrs) {
|
|
136
|
+
if (!a.bind) continue
|
|
137
|
+
if (a.raw)
|
|
138
|
+
edits.push(
|
|
139
|
+
{ start: a.raw[0], end: a.raw[0] + 5, text: '' },
|
|
140
|
+
{ start: a.span[1], end: a.span[1], text: '.value' },
|
|
141
|
+
)
|
|
142
|
+
}
|
|
96
143
|
for (const c of facts.comps) {
|
|
97
144
|
const type = this.state.get(c.id)?.type ?? 'any'
|
|
98
145
|
const note = `: ${type}`
|
|
99
146
|
if (c.written) edits.push({ start: c.written[0], end: c.written[1], text: note })
|
|
100
147
|
else if (c.parens) edits.push({ start: c.nameEnd, end: c.nameEnd, text: note })
|
|
101
|
-
else
|
|
148
|
+
else
|
|
149
|
+
edits.push(
|
|
150
|
+
{ start: c.paramSpan[0], end: c.paramSpan[0], text: '(' },
|
|
151
|
+
{ start: c.nameEnd, end: c.nameEnd, text: `${note})` },
|
|
152
|
+
)
|
|
102
153
|
}
|
|
103
154
|
return edits
|
|
104
155
|
}
|
|
@@ -108,12 +159,15 @@ class Engine {
|
|
|
108
159
|
const changed = new Map()
|
|
109
160
|
for (const [file, facts] of this.facts) {
|
|
110
161
|
if (only && !only.has(file)) continue
|
|
111
|
-
if (!facts.comps.length && !facts.sites.some(s => s.attrs.some(a => a.bind))) {
|
|
162
|
+
if (!facts.comps.length && !facts.sites.some((s) => s.attrs.some((a) => a.bind))) {
|
|
112
163
|
if (this.maps.delete(file)) changed.set(file, this.texts.get(file))
|
|
113
164
|
continue
|
|
114
165
|
}
|
|
115
166
|
const map = shadow(this.texts.get(file), this.edits(facts))
|
|
116
|
-
if (this.maps.get(file)?.text !== map.text) {
|
|
167
|
+
if (this.maps.get(file)?.text !== map.text) {
|
|
168
|
+
this.maps.set(file, map)
|
|
169
|
+
changed.set(file, map.text)
|
|
170
|
+
}
|
|
117
171
|
}
|
|
118
172
|
if (changed.size) host.update(changed)
|
|
119
173
|
return changed
|
|
@@ -133,392 +187,78 @@ class Engine {
|
|
|
133
187
|
if (!reqs.length) return []
|
|
134
188
|
this.stats.queries += reqs.length
|
|
135
189
|
const map = this.maps.get(file)
|
|
136
|
-
const at = p => (map ? toShadow(map, p) : p)
|
|
137
|
-
return host.query(
|
|
190
|
+
const at = (p) => (map ? toShadow(map, p) : p)
|
|
191
|
+
return host.query(
|
|
192
|
+
file,
|
|
193
|
+
reqs.map((r) => ({ kind: r.kind, start: at(r.span[0]), end: at(r.span[1]) })),
|
|
194
|
+
)
|
|
138
195
|
}
|
|
139
196
|
|
|
140
197
|
/** Who a tag renders when its name doesn't say: a component held in a variable, a map, a prop, an import. */
|
|
141
198
|
flows(host, force) {
|
|
142
|
-
|
|
143
|
-
const comps = this.comps()
|
|
144
|
-
const open = comps.filter(c => c.exported || this.facts.get(c.file).escaped.has(c.name) || [...this.facts.values()].some(f => [...f.wraps.values()].some(l => l.includes(c.name))))
|
|
145
|
-
if (!open.length) return out
|
|
146
|
-
const names = new Set(open.map(c => c.name))
|
|
147
|
-
for (const [file, facts] of this.facts) {
|
|
148
|
-
const cross = s => {
|
|
149
|
-
const head = s.tag.split('.')[0]
|
|
150
|
-
const imported = facts.imports.get(head)
|
|
151
|
-
if (imported) return names.has(imported.name) || imported.name === 'default' || names.has(head)
|
|
152
|
-
return !facts.comps.some(c => c.name === s.tag)
|
|
153
|
-
}
|
|
154
|
-
const held = this.flowCache.get(file)
|
|
155
|
-
if (force && !force.has(file) && held?.facts === facts) {
|
|
156
|
-
for (const [k, v] of held.hits) out.set(k, v)
|
|
157
|
-
continue
|
|
158
|
-
}
|
|
159
|
-
const sites = facts.sites.filter(s => !this.direct(facts, s).length && cross(s))
|
|
160
|
-
const hits = new Map()
|
|
161
|
-
this.flowCache.set(file, { facts, hits })
|
|
162
|
-
if (!sites.length) continue
|
|
163
|
-
const res = this.ask(host, file, sites.map(s => ({ kind: 'decls', span: s.tagSpan })))
|
|
164
|
-
sites.forEach((s, i) => {
|
|
165
|
-
const found = []
|
|
166
|
-
for (const d of res[i].decls ?? []) {
|
|
167
|
-
for (const c of open) {
|
|
168
|
-
if (!sameFile(c.file, d.file)) continue
|
|
169
|
-
const map = this.maps.get(c.file)
|
|
170
|
-
if ((map ? toShadow(map, c.bodySpan[1]) : c.bodySpan[1]) === d.end && !found.includes(c)) found.push(c)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (found.length) { hits.set(`${file}:${s.span[0]}`, found); out.set(`${file}:${s.span[0]}`, found) }
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
return out
|
|
199
|
+
return flows.call(this, host, force)
|
|
177
200
|
}
|
|
178
201
|
|
|
179
202
|
/** Every call site of every component, with what the checker says each attribute is. */
|
|
180
203
|
collect(host, force) {
|
|
181
|
-
|
|
182
|
-
const sites = new Map(this.comps().map(c => [c.id, []]))
|
|
183
|
-
for (const [file, facts] of this.facts) {
|
|
184
|
-
const targets = facts.sites.map(s => { const d = this.direct(facts, s); return d.length ? d : flows.get(`${file}:${s.span[0]}`) ?? [] })
|
|
185
|
-
let held = this.typed.get(file)
|
|
186
|
-
if (!held || held.facts !== facts || !force || force.has(file)) {
|
|
187
|
-
const reqs = [], back = []
|
|
188
|
-
facts.sites.forEach((s, i) => {
|
|
189
|
-
if (!targets[i].length) return
|
|
190
|
-
for (const a of s.attrs) if (a.span) { back.push(a); reqs.push({ kind: 'type', span: a.span }) }
|
|
191
|
-
for (const sp of s.spreads) { back.push(sp); reqs.push({ kind: 'spread', span: sp.span }) }
|
|
192
|
-
if (s.children?.span) { back.push(s.children); reqs.push({ kind: 'type', span: s.children.span }) }
|
|
193
|
-
})
|
|
194
|
-
const res = this.ask(host, file, reqs)
|
|
195
|
-
held = { facts, typed: new Map(back.map((a, i) => [a, res[i]])) }
|
|
196
|
-
this.typed.set(file, held)
|
|
197
|
-
}
|
|
198
|
-
const { typed } = held
|
|
199
|
-
facts.sites.forEach((s, i) => { for (const c of targets[i]) sites.get(c.id).push({ file, site: s, typed }) })
|
|
200
|
-
}
|
|
201
|
-
return sites
|
|
204
|
+
return collect.call(this, host, force)
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
/** What the body alone says about `key`: what's left when no caller says, or a caller says `any`. */
|
|
205
208
|
fromBody(c, key, entry, cache) {
|
|
206
|
-
|
|
207
|
-
const typeOf = q('type'), ctx = q('context')
|
|
208
|
-
let member = null
|
|
209
|
-
for (const u of entry.uses) {
|
|
210
|
-
if (u.as === 'number') return 'number'
|
|
211
|
-
if (u.as === 'call') return `(${u.args.map((a, i) => `arg${i}: ${typeOf(a).text}`).join(', ')}) => any`
|
|
212
|
-
if (u.as === 'context') { const t = ctx(u.span); if (t && !t.any && t.text !== 'unknown') return t.text.replace(/ \| undefined$/, '') }
|
|
213
|
-
if (u.as === 'write') { const t = typeOf(u.value); if (!t.any) return t.text }
|
|
214
|
-
if (u.as === 'member') member = u.member
|
|
215
|
-
}
|
|
216
|
-
if (member) return /^(length|map|filter|forEach|slice|find|some|every|join|reduce)$/.test(member) ? 'any[]' : `{ ${member}: any }`
|
|
217
|
-
if (entry.fallback) { const t = typeOf(entry.fallback); if (!t.any) return t.text }
|
|
218
|
-
return null
|
|
209
|
+
return fromBody.call(this, c, key, entry, cache)
|
|
219
210
|
}
|
|
220
211
|
|
|
221
212
|
infer(c, sites, host) {
|
|
222
|
-
|
|
223
|
-
const prev = this.state.get(c.id) ?? {}
|
|
224
|
-
const st = { keys: new Map(), sites: sites.length, pins: prev.pins ?? new Map(), shape: prev.shape, anyFrom: [], noCallers: false }
|
|
225
|
-
if (this.routes(c.file) && c.isDefault) {
|
|
226
|
-
st.type = `import('@weft-js/start').RouteProps${facts.exportsLoad ? '<typeof load>' : ''}`
|
|
227
|
-
st.route = true
|
|
228
|
-
return st
|
|
229
|
-
}
|
|
230
|
-
const outer = sites.filter(s => !(s.site.in === c.name && s.file === c.file))
|
|
231
|
-
if (!outer.length) st.noCallers = true
|
|
232
|
-
|
|
233
|
-
const memo = new Map()
|
|
234
|
-
const cache = (kind, span) => {
|
|
235
|
-
const k = `${kind}:${span}`
|
|
236
|
-
if (!memo.has(k)) memo.set(k, this.ask(host, c.file, [{ kind, span }])[0])
|
|
237
|
-
return memo.get(k)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const seen = new Map() // key → [{ text, exact, any, untypedFn, at }]
|
|
241
|
-
const add = (key, v) => { if (!seen.has(key)) seen.set(key, []); seen.get(key).push(v) }
|
|
242
|
-
for (const { file, site, typed } of sites) {
|
|
243
|
-
const self = site.in === c.name && file === c.file
|
|
244
|
-
for (const a of site.attrs) {
|
|
245
|
-
const r = a.span ? typed.get(a) : null
|
|
246
|
-
const held = a.value && this.facts.get(file).comps.some(x => x.name === a.value)
|
|
247
|
-
const v = held ? { text: `typeof ${a.value}` } : a.literal === 'true' ? { text: 'boolean' } : a.literal === 'string' ? { text: 'string', exact: a.exact } : r ?? { text: 'any', any: true }
|
|
248
|
-
if (self) continue // a component rendering itself says nothing new about its props
|
|
249
|
-
add(a.name, { ...v, untypedFn: a.untypedFn, at: { file, span: a.span ?? a.whole ?? site.tagSpan, whole: a.whole ?? site.tagSpan, tag: site.tag, tagAt: site.tagSpan[0] }, self, literal: a.literal })
|
|
250
|
-
}
|
|
251
|
-
for (const sp of site.spreads) {
|
|
252
|
-
const r = typed.get(sp)
|
|
253
|
-
if (!self) for (const p of r?.props ?? []) add(p.name, { text: p.text, any: p.any, at: { file, span: sp.span, tag: site.tag }, self, optional: p.optional })
|
|
254
|
-
}
|
|
255
|
-
if (site.children && !self) {
|
|
256
|
-
const r = site.children.span ? typed.get(site.children) : null
|
|
257
|
-
add('children', { ...(r ?? { text: 'unknown' }), untypedFn: site.children.untypedFn, at: { file, span: site.tagSpan, tag: site.tag }, self })
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const fwd = c.forwards
|
|
262
|
-
const named = new Set(c.info.keys.keys())
|
|
263
|
-
const all = fwd.length ? named : new Set([...seen.keys(), ...named])
|
|
264
|
-
const parts = []
|
|
265
|
-
for (const key of all) {
|
|
266
|
-
const entry = c.info.keys.get(key) ?? { uses: [] }
|
|
267
|
-
const given = seen.get(key) ?? []
|
|
268
|
-
const real = given.filter(v => !v.any)
|
|
269
|
-
const body = (given.every(v => v.any || v.untypedFn) || !given.length) ? this.fromBody(c, key, entry, cache) : null
|
|
270
|
-
let text
|
|
271
|
-
const lits = real.filter(v => v.exact?.startsWith('"'))
|
|
272
|
-
if (real.length && lits.length === real.length && unique(lits.map(v => v.exact)).length > 1 && unique(lits.map(v => v.exact)).length <= MAX_LITERALS) text = union(lits.map(v => v.exact))
|
|
273
|
-
else text = body ?? union(real.map(v => v.text))
|
|
274
|
-
const distinct = unique(real.map(v => v.text))
|
|
275
|
-
const tally = new Map(distinct.map(t => [t, real.filter(v => v.text === t).length]))
|
|
276
|
-
const number = entry.uses.some(u => u.as === 'number') && !tally.has('number') && distinct.length ? 'number' : null
|
|
277
|
-
if (number) tally.set(number, 0)
|
|
278
|
-
if (st.pins.has(key)) text = st.pins.get(key)
|
|
279
|
-
else if (number) text = union([text, number])
|
|
280
|
-
if (given.some(v => v.any) && !real.length) st.anyFrom.push(...given.filter(v => v.any).map(v => ({ key, ...v.at, body: !!body })))
|
|
281
|
-
if (!text) {
|
|
282
|
-
if (given.length) text = 'any'
|
|
283
|
-
else if (entry.fallback) text = 'any'
|
|
284
|
-
else if (outer.length) text = 'undefined'
|
|
285
|
-
else text = 'any'
|
|
286
|
-
}
|
|
287
|
-
const passed = outer.filter(s => s.site.attrs.some(a => a.name === key) || (key === 'children' && s.site.children) || s.site.spreads.some(sp => s.typed.get(sp)?.props?.some(p => p.name === key && !p.optional)))
|
|
288
|
-
const opaque = outer.some(s => s.site.spreads.some(sp => s.typed.get(sp)?.any))
|
|
289
|
-
const required = outer.length && passed.length === outer.length && !entry.fallback && !opaque && text !== 'undefined'
|
|
290
|
-
const onlySelf = !outer.length
|
|
291
|
-
const optional = onlySelf ? !!entry.fallback : !required
|
|
292
|
-
st.keys.set(key, { text, optional, given, tally, uses: entry.uses })
|
|
293
|
-
parts.push(`${quote(key)}${optional ? '?' : ''}: ${text}`)
|
|
294
|
-
}
|
|
295
|
-
const own = parts.length ? `{ ${parts.join('; ')} }` : '{}'
|
|
296
|
-
if (fwd.length) {
|
|
297
|
-
const f = fwd[0]
|
|
298
|
-
const base = `Parameters<typeof ${f.tag}>[0]`
|
|
299
|
-
const names = unique([...named, ...f.given])
|
|
300
|
-
const taken = names.map(n => JSON.stringify(n)).join(' | ')
|
|
301
|
-
const left = names.length ? `Omit<${base}, ${taken}>` : base
|
|
302
|
-
const loose = f.given.filter(n => !named.has(n))
|
|
303
|
-
const given = loose.length ? ` & Partial<Pick<${base}, Extract<keyof ${base}, ${loose.map(n => JSON.stringify(n)).join(' | ')}>>>` : ''
|
|
304
|
-
st.type = parts.length ? `${left}${given} & ${own}` : `${left}${given}`
|
|
305
|
-
} else st.type = c.info.opaque ? 'any' : st.shape ?? own
|
|
306
|
-
return st
|
|
213
|
+
return infer.call(this, c, sites, host)
|
|
307
214
|
}
|
|
308
215
|
|
|
309
216
|
/** The body can't take the union: try each caller's type alone, and leave the rest as errors at those callers. */
|
|
310
217
|
conflicts(host, comps) {
|
|
311
|
-
|
|
312
|
-
const st = this.state.get(c.id)
|
|
313
|
-
if (!st?.keys) continue
|
|
314
|
-
for (const [key, k] of st.keys) {
|
|
315
|
-
if (k.tally.size < 2 || st.pins.has(key)) continue
|
|
316
|
-
const map = this.maps.get(c.file)
|
|
317
|
-
const uses = k.uses.map(u => u.span.map(p => toShadow(map, p)))
|
|
318
|
-
const hurt = () => this.diag(host, c.file).filter(d => uses.some(u => overlaps(u, [d.start, d.start + d.length])))
|
|
319
|
-
if (!hurt().length) continue
|
|
320
|
-
const shape = this.shapes(st)
|
|
321
|
-
if (shape) {
|
|
322
|
-
st.shape = shape
|
|
323
|
-
this.state.set(c.id, this.infer(c, this.lastSites.get(c.id), host))
|
|
324
|
-
this.publish(host, new Set([c.file]))
|
|
325
|
-
const m2 = this.maps.get(c.file)
|
|
326
|
-
const u2 = k.uses.map(u => u.span.map(p => toShadow(m2, p)))
|
|
327
|
-
if (!this.diag(host, c.file).some(d => u2.some(u => overlaps(u, [d.start, d.start + d.length])))) { st.shape = shape; break }
|
|
328
|
-
st.shape = undefined
|
|
329
|
-
this.state.set(c.id, this.infer(c, this.lastSites.get(c.id), host))
|
|
330
|
-
this.publish(host, new Set([c.file]))
|
|
331
|
-
}
|
|
332
|
-
const order = [...k.tally].sort((a, b) => b[1] - a[1]).map(([t]) => t)
|
|
333
|
-
let chosen = null
|
|
334
|
-
for (const t of order) {
|
|
335
|
-
st.pins.set(key, t)
|
|
336
|
-
this.state.set(c.id, this.infer(c, this.lastSites.get(c.id), host))
|
|
337
|
-
this.publish(host, new Set([c.file]))
|
|
338
|
-
const m2 = this.maps.get(c.file)
|
|
339
|
-
const u2 = k.uses.map(u => u.span.map(p => toShadow(m2, p)))
|
|
340
|
-
if (!this.diag(host, c.file).some(d => u2.some(u => overlaps(u, [d.start, d.start + d.length])))) { chosen = t; break }
|
|
341
|
-
}
|
|
342
|
-
if (chosen) {
|
|
343
|
-
st.pins.set(key, chosen)
|
|
344
|
-
const next = this.infer(c, this.lastSites.get(c.id), host)
|
|
345
|
-
this.state.set(c.id, next)
|
|
346
|
-
this.publish(host, new Set([c.file]))
|
|
347
|
-
next.odd = [...(next.odd ?? []), ...k.given.filter(v => !v.any && v.text !== chosen).map(v => ({ key, type: v.text, want: chosen, ...v.at, use: k.uses.find(u => u.as !== 'read') ?? k.uses[0] }))]
|
|
348
|
-
} else {
|
|
349
|
-
st.pins.delete(key)
|
|
350
|
-
this.state.set(c.id, this.infer(c, this.lastSites.get(c.id), host))
|
|
351
|
-
this.publish(host, new Set([c.file]))
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
218
|
+
return conflicts.call(this, host, comps)
|
|
355
219
|
}
|
|
356
220
|
|
|
357
221
|
/** Callers that differ by a string prop: one object type per value, so the body can narrow on it. */
|
|
358
222
|
shapes(st) {
|
|
359
|
-
|
|
360
|
-
const tag = keys.find(([, k]) => { const lits = k.given.map(v => v.exact); return lits.length && lits.every(l => l?.startsWith('"')) && new Set(lits).size > 1 })
|
|
361
|
-
if (!tag) return null
|
|
362
|
-
const groups = new Map()
|
|
363
|
-
for (const v of tag[1].given) {
|
|
364
|
-
if (!groups.has(v.exact)) groups.set(v.exact, new Set())
|
|
365
|
-
groups.get(v.exact).add(`${v.at.file}:${v.at.whole?.[0]}`)
|
|
366
|
-
}
|
|
367
|
-
return [...groups].map(([lit]) => {
|
|
368
|
-
const parts = [`${quote(tag[0])}: ${lit}`]
|
|
369
|
-
for (const [key, k] of keys) {
|
|
370
|
-
if (key === tag[0]) continue
|
|
371
|
-
const near = tag[1].given.filter(v => v.exact === lit).map(v => v.at)
|
|
372
|
-
const mine = k.given.filter(v => near.some(n => n.tagAt === v.at.tagAt && n.file === v.at.file))
|
|
373
|
-
if (mine.length) parts.push(`${quote(key)}${mine.length < near.length ? '?' : ''}: ${union(mine.map(v => v.text))}`)
|
|
374
|
-
}
|
|
375
|
-
return `{ ${parts.join('; ')} }`
|
|
376
|
-
}).join(' | ')
|
|
223
|
+
return shapes.call(this, st)
|
|
377
224
|
}
|
|
378
225
|
|
|
379
226
|
/** A type the checker printed in another scope may not be nameable here: then this component is `any`, and says so. */
|
|
380
227
|
unprintable(host, comps) {
|
|
381
|
-
|
|
382
|
-
const map = this.maps.get(c.file)
|
|
383
|
-
if (!map || !this.state.get(c.id)?.keys) continue
|
|
384
|
-
for (const d of this.diag(host, c.file)) {
|
|
385
|
-
const at = toSource(map, d.start)
|
|
386
|
-
if (!inGenerated(map, d.start, d.length) || at !== c.nameEnd) continue
|
|
387
|
-
const st = this.state.get(c.id)
|
|
388
|
-
st.unnamed = d.text
|
|
389
|
-
st.type = 'any'
|
|
390
|
-
}
|
|
391
|
-
}
|
|
228
|
+
return unprintable.call(this, host, comps)
|
|
392
229
|
}
|
|
393
230
|
|
|
394
231
|
/** The checker's errors in a file, in source positions, without what the engine already said better. */
|
|
395
232
|
errors(host, file) {
|
|
396
|
-
|
|
397
|
-
const said = (this.findings.get(file) ?? []).filter(f => f.code === 'W084')
|
|
398
|
-
return this.diag(host, file).flatMap(d => {
|
|
399
|
-
if (map && inGenerated(map, d.start, d.length)) return []
|
|
400
|
-
const start = map ? toSource(map, d.start) : d.start
|
|
401
|
-
const end = map ? toSource(map, d.start + d.length) : d.start + d.length
|
|
402
|
-
if (said.some(f => overlaps([f.start, f.end], [start, Math.max(end, start + 1)]))) return []
|
|
403
|
-
return [{ code: `TS${d.code}`, start, end, why: d.text, help: 'Fix the type error' }]
|
|
404
|
-
})
|
|
233
|
+
return errors.call(this, host, file)
|
|
405
234
|
}
|
|
406
235
|
|
|
407
236
|
/** Files whose types depend on inference: where a component is, and where it's rendered. */
|
|
408
237
|
affected() {
|
|
409
|
-
|
|
410
|
-
for (const sites of this.lastSites?.values() ?? []) for (const s of sites) files.add(s.file)
|
|
411
|
-
return files
|
|
238
|
+
return affected.call(this)
|
|
412
239
|
}
|
|
413
240
|
|
|
414
241
|
/** @returns {Map<string, {code: string, severity?: string, start: number, end: number, why: string, help?: string, edit?: object}[]>} */
|
|
415
242
|
run(host, changed) {
|
|
416
|
-
this
|
|
417
|
-
const comps = this.comps()
|
|
418
|
-
if (!this.needed()) { this.publish(host); return this.findings }
|
|
419
|
-
const first = this.publish(host)
|
|
420
|
-
let force = changed ? new Set(changed) : undefined
|
|
421
|
-
if (force) {
|
|
422
|
-
for (const f of changed) for (const d of this.dependents(f)) force.add(d)
|
|
423
|
-
for (const f of first.keys()) force.add(f)
|
|
424
|
-
}
|
|
425
|
-
const touched = new Set(force)
|
|
426
|
-
const sitesOf = id => (this.lastSites?.get(id) ?? []).map(s => s.file)
|
|
427
|
-
let dirty = comps
|
|
428
|
-
for (let round = 0; round < ROUNDS; round++) {
|
|
429
|
-
this.stats.rounds++
|
|
430
|
-
const sites = this.collect(host, force)
|
|
431
|
-
this.lastSites = sites
|
|
432
|
-
if (force) dirty = comps.filter(c => !this.state.has(c.id) || force.has(c.file) || sites.get(c.id).some(s => force.has(s.file)))
|
|
433
|
-
const moved = []
|
|
434
|
-
for (const c of dirty) {
|
|
435
|
-
const st = this.infer(c, sites.get(c.id), host)
|
|
436
|
-
if (st.type !== this.state.get(c.id)?.type) moved.push(c)
|
|
437
|
-
this.state.set(c.id, st)
|
|
438
|
-
}
|
|
439
|
-
if (!moved.length) break
|
|
440
|
-
const out = this.publish(host)
|
|
441
|
-
force = new Set([...out.keys(), ...moved.flatMap(c => [c.file, ...sitesOf(c.id)])])
|
|
442
|
-
for (const f of force) touched.add(f)
|
|
443
|
-
}
|
|
444
|
-
this.conflicts(host, dirty)
|
|
445
|
-
this.unprintable(host, dirty)
|
|
446
|
-
this.publish(host)
|
|
447
|
-
for (const c of comps) this.report(c)
|
|
448
|
-
this.touched = touched
|
|
449
|
-
return this.findings
|
|
243
|
+
return run.call(this, host, changed)
|
|
450
244
|
}
|
|
451
245
|
|
|
452
246
|
note(file, finding) {
|
|
453
|
-
|
|
454
|
-
this.findings.get(file).push(finding)
|
|
247
|
+
return noteFinding.call(this, file, finding)
|
|
455
248
|
}
|
|
456
249
|
|
|
457
250
|
/** The source edits that write a type down. */
|
|
458
251
|
annotation(c, type) {
|
|
459
|
-
|
|
460
|
-
if (c.written) return [{ start: c.written[0], end: c.written[1], text }]
|
|
461
|
-
if (c.parens) return [{ start: c.nameEnd, end: c.nameEnd, text }]
|
|
462
|
-
return [{ start: c.paramSpan[0], end: c.paramSpan[0], text: '(' }, { start: c.nameEnd, end: c.nameEnd, text: `${text})` }]
|
|
252
|
+
return annotation.call(this, c, type)
|
|
463
253
|
}
|
|
464
254
|
|
|
465
255
|
report(c) {
|
|
466
|
-
|
|
467
|
-
if (!st || st.route) return
|
|
468
|
-
const [start, end] = c.nameSpan
|
|
469
|
-
const line = (file, pos) => this.texts.get(file).slice(0, pos).split('\n').length
|
|
470
|
-
if (st.unnamed) this.note(c.file, { code: 'W085', severity: 'warning', start, end, why: `\`${c.name}\`'s inferred props name a type this scope can't see, so they fell back to \`any\`: ${st.unnamed}`, help: 'Move the type to module scope or export it, or annotate the parameter' })
|
|
471
|
-
const exposed = c.exported
|
|
472
|
-
if (st.noCallers && !exposed) {
|
|
473
|
-
const escaped = this.facts.get(c.file).escaped.has(c.name)
|
|
474
|
-
this.note(c.file, escaped
|
|
475
|
-
? { code: 'W083', severity: 'warning', start, end, why: `\`${c.name}\` is used as a value and no JSX site for it could be traced, so its props are \`any\``, help: `Annotate its parameter: function ${c.name}(props: { ... })` }
|
|
476
|
-
: { code: 'W080', start, end, why: `\`${c.name}\` has an untyped parameter and nothing renders it, so its props have nothing to be inferred from`, help: `Render it (\`<${c.name} ... />\`), delete it, or annotate the parameter: function ${c.name}(props: { ... })` })
|
|
477
|
-
}
|
|
478
|
-
if (exposed) {
|
|
479
|
-
const type = st.type
|
|
480
|
-
const edits = this.annotation(c, type)
|
|
481
|
-
const bad = st.noCallers
|
|
482
|
-
if (!c.written) this.note(c.file, { code: 'W081', start, end, why: `\`${c.name}\` is exported with no prop type${bad ? ' and no caller to infer one from' : ''}. An export's type is written down, so a caller's edit can't change a public API unseen`, help: `\`weft fix\` writes the inferred type: ${c.name}(props: ${type})`, edits: bad ? undefined : edits, safe: !bad })
|
|
483
|
-
else if (c.writtenType.replace(MARK, '').trim() !== type && !bad) this.note(c.file, { code: 'W082', start, end, why: `\`${c.name}\`'s written prop type no longer matches its callers: it says ${c.writtenType.replace(MARK, '').trim()}, they need ${type}`, help: '`weft fix` rewrites it (a diff to review), or delete the /*inferred*/ comment to keep the type you have', edits, safe: true })
|
|
484
|
-
}
|
|
485
|
-
for (const a of st.anyFrom) {
|
|
486
|
-
const origin = this.origin(a.file, a.span)
|
|
487
|
-
this.note(c.file, { code: 'W083', severity: 'warning', start, end, why: `\`${c.name}\`'s prop \`${a.key}\` is ${a.body ? 'typed from its use in the body, because' : '`any`, because'} the value at ${a.tag} (line ${line(a.file, a.span[0])}) is \`any\`${origin ? `: it comes from ${origin}` : ''}`, help: 'Give that source a type, or annotate the prop' })
|
|
488
|
-
}
|
|
489
|
-
for (const o of st.odd ?? []) {
|
|
490
|
-
const use = o.use?.span ? this.texts.get(c.file).slice(...o.use.span) : o.key
|
|
491
|
-
this.note(o.file, { code: 'W084', start: o.whole[0], end: o.whole[1], why: `\`${o.key}\` here is ${o.type}, but \`${c.name}\` uses it as ${o.want} (\`${use}\`, line ${line(c.file, o.use?.span?.[0] ?? c.nameSpan[0])}), and its other callers agree`, help: `Pass ${o.want} here, or change \`${c.name}\` to handle ${o.type} (a type parameter, if the two props must agree)` })
|
|
492
|
-
}
|
|
256
|
+
return report.call(this, c)
|
|
493
257
|
}
|
|
494
258
|
|
|
495
259
|
/** Where an `any` started, by following names in the file: `JSON.parse(raw)`, an untyped parameter, an import. */
|
|
496
260
|
origin(file, span) {
|
|
497
|
-
|
|
498
|
-
const text = this.texts.get(file)
|
|
499
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
500
|
-
const lineOf = n => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1
|
|
501
|
-
let node = null
|
|
502
|
-
const find = n => { if (n.getStart(sf) <= span[0] && n.end >= span[1]) { node = n; ts.forEachChild(n, find) } }
|
|
503
|
-
ts.forEachChild(sf, find)
|
|
504
|
-
for (let i = 0; node && i < 6; i++) {
|
|
505
|
-
if (ts.isPropertyAccessExpression(node)) node = node.expression
|
|
506
|
-
if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) { node = node.expression; continue }
|
|
507
|
-
if (ts.isCallExpression(node)) return `\`${node.getText(sf).slice(0, 48)}\` (line ${lineOf(node)})`
|
|
508
|
-
if (!ts.isIdentifier(node)) return `\`${node.getText(sf).slice(0, 48)}\` (line ${lineOf(node)})`
|
|
509
|
-
let decl = null
|
|
510
|
-
const look = n => {
|
|
511
|
-
if ((ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isImportSpecifier(n) || ts.isImportClause(n)) && n.name?.getText(sf) === node.text && n.pos < node.pos) decl = n
|
|
512
|
-
ts.forEachChild(n, look)
|
|
513
|
-
}
|
|
514
|
-
ts.forEachChild(sf, look)
|
|
515
|
-
if (!decl) return `\`${node.text}\``
|
|
516
|
-
if (ts.isParameter(decl)) return `the parameter \`${node.text}\`, which has no type (line ${lineOf(decl)})`
|
|
517
|
-
if (ts.isImportSpecifier(decl) || ts.isImportClause(decl)) return `\`${node.text}\`, imported at line ${lineOf(decl)}`
|
|
518
|
-
if (!decl.initializer) return `\`${node.text}\` (line ${lineOf(decl)})`
|
|
519
|
-
node = decl.initializer
|
|
520
|
-
}
|
|
521
|
-
return null
|
|
261
|
+
return origin.call(this, file, span)
|
|
522
262
|
}
|
|
523
263
|
}
|
|
524
264
|
|