ttsc 0.18.4 → 0.19.0
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 +5 -6
- package/cmd/graphbench/main.go +1 -1
- package/cmd/platform/main.go +1 -48
- package/cmd/ttsc/build_branches_test.go +5 -1
- package/cmd/ttsc/main.go +0 -50
- package/cmd/ttsc/main_alias_branches_test.go +6 -12
- package/cmd/ttscgraph/dump.go +13 -3
- package/cmd/ttscserver/main.go +5 -3
- package/driver/lsp.go +13 -1
- package/driver/plugins.go +35 -0
- package/driver/program.go +7 -8
- package/driver/rewrite.go +381 -21
- package/go.mod +1 -0
- package/internal/graph/build.go +297 -3
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
- package/internal/graph/diagnostics.go +5 -4
- package/internal/graph/dump.go +60 -4
- package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
- package/internal/graph/edges.go +122 -11
- package/internal/graph/exports.go +74 -17
- package/internal/graph/graph.go +33 -1
- package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
- package/internal/graph/resolve.go +24 -0
- package/internal/graphsymbols/provider.go +50 -14
- package/internal/lspserver/lsp_native_plugin_source.go +77 -23
- package/internal/lspserver/lsp_plugin.go +19 -2
- package/internal/lspserver/lsp_proxy.go +244 -52
- package/internal/lspserver/lsp_server.go +35 -27
- package/internal/lspserver/lsp_symbols.go +25 -16
- package/lib/TtscCompiler.d.ts +10 -0
- package/lib/TtscCompiler.js +24 -6
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.js +9 -7
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +1 -0
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -4
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
- package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
- package/lib/compiler/internal/residentTransformProcess.js +68 -20
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +11 -0
- package/lib/compiler/internal/runBuild.js +44 -8
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +7 -4
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
- package/lib/compiler/internal/sharedHostHelpers.js +23 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +18 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +34 -8
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +17 -0
- package/lib/flags/parser.js +11 -1
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.js +7 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +21 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +13 -53
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +11 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +27 -11
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
- package/lib/launcher/internal/runtimeHooks.js +632 -43
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
- package/lib/launcher/internal/servedSourceMap.js +164 -0
- package/lib/launcher/internal/servedSourceMap.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
- package/lib/plugin/internal/buildSourcePlugin.js +651 -113
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
- package/lib/plugin/internal/loadProjectPlugins.js +33 -10
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -2
- package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
- package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
- package/package.json +11 -8
- package/shim/ast/lint.go +10 -0
- package/shim/ast/shim.go +44 -0
- package/shim/checker/enums_gen.go +19 -0
- package/shim/checker/shim.go +130 -0
- package/shim/diagnosticwriter/lint.go +30 -3
- package/shim/scanner/regexp.go +27 -0
- package/src/TtscCompiler.ts +25 -6
- package/src/TtscService.ts +15 -7
- package/src/compiler/internal/compileProjectInMemory.ts +1 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
- package/src/compiler/internal/project/readProjectConfig.ts +61 -5
- package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
- package/src/compiler/internal/residentTransformProcess.ts +95 -21
- package/src/compiler/internal/runBuild.ts +76 -9
- package/src/compiler/internal/runSingleFileEmit.ts +7 -4
- package/src/compiler/internal/sharedHostHelpers.ts +24 -0
- package/src/compiler/internal/startResidentTransform.ts +21 -1
- package/src/compiler/internal/transformProjectInMemory.ts +49 -6
- package/src/flags/parser.ts +28 -1
- package/src/flags/schema.ts +8 -0
- package/src/launcher/internal/prepareExecution.ts +22 -2
- package/src/launcher/internal/runTtsc.ts +13 -61
- package/src/launcher/internal/runTtscserver.ts +14 -0
- package/src/launcher/internal/runTtsx.ts +28 -13
- package/src/launcher/internal/runtimeHooks.ts +771 -53
- package/src/launcher/internal/servedSourceMap.ts +183 -0
- package/src/plugin/internal/buildSourcePlugin.ts +859 -122
- package/src/plugin/internal/loadProjectPlugins.ts +36 -6
- package/src/structures/ITtscCompilerContext.ts +17 -2
- package/src/structures/ITtscPluginCapabilities.ts +9 -0
- package/src/structures/ITtscPluginFactoryContext.ts +15 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
- package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
- package/src/structures/internal/TtscCommonOptions.ts +8 -0
- package/tools/shim_audit/baseline.json +16 -7
- package/tools/shim_audit/main.go +777 -71
- package/tools/shim_audit/main_test.go +28 -1
- package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
- package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
- package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
- package/utility/host.go +46 -20
- package/utility/serve.go +1 -1
- package/shim/checker/signature_api_completeness.go +0 -58
package/internal/graph/edges.go
CHANGED
|
@@ -37,7 +37,12 @@ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, en
|
|
|
37
37
|
// target that surface as different relationships (a call and a `new`, an
|
|
38
38
|
// `extends` and an `implements` of the same base) are both kept, while
|
|
39
39
|
// repeated uses of the same form collapse to one edge.
|
|
40
|
-
|
|
40
|
+
//
|
|
41
|
+
// The key is a struct, not a concatenation. Concatenating allocated a fresh
|
|
42
|
+
// string per *candidate* edge — including the duplicates thrown away on the
|
|
43
|
+
// next line, which on a large program are most of them — and a comparable
|
|
44
|
+
// struct of the three fields hashes just as well for nothing.
|
|
45
|
+
key := edgeKey{from: from, to: to, kind: wireEdgeKind(kind, origin)}
|
|
41
46
|
if _, exists := g.seen[key]; exists {
|
|
42
47
|
return
|
|
43
48
|
}
|
|
@@ -101,7 +106,7 @@ func (g *Graph) heritageEdges(checker *shimchecker.Checker, path string, node *s
|
|
|
101
106
|
if base == nil || base.Expression == nil {
|
|
102
107
|
continue
|
|
103
108
|
}
|
|
104
|
-
target :=
|
|
109
|
+
target := g.resolve(checker, base.Expression)
|
|
105
110
|
if target == nil || target.Symbol == nil {
|
|
106
111
|
continue
|
|
107
112
|
}
|
|
@@ -147,6 +152,10 @@ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string,
|
|
|
147
152
|
if id := topLevelID(path, statement, NodeFunction); id != "" {
|
|
148
153
|
fn(id, statement)
|
|
149
154
|
}
|
|
155
|
+
// Closure nodes are not indexed (build.go): what a function body runs is
|
|
156
|
+
// implementation, and implementation is read from the file. The walker
|
|
157
|
+
// stays for the day the caller asks for them.
|
|
158
|
+
forEachClosureIn(path, statement, fn)
|
|
150
159
|
case shimast.KindTypeAliasDeclaration:
|
|
151
160
|
if id := topLevelID(path, statement, NodeTypeAlias); id != "" {
|
|
152
161
|
fn(id, statement)
|
|
@@ -166,10 +175,24 @@ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string,
|
|
|
166
175
|
forEachVariable(path, statement, fn)
|
|
167
176
|
case shimast.KindModuleDeclaration:
|
|
168
177
|
forEachContainerIn(path, moduleStatements(statement), fn)
|
|
178
|
+
default:
|
|
179
|
+
// A statement that declares nothing still runs: a test suite's
|
|
180
|
+
// `describe(() => it(() => schema.parse(x)))`, an entry file's
|
|
181
|
+
// `bootstrap()`, a registration side effect. No declaration owns those
|
|
182
|
+
// calls, so attributing edges to owners alone dropped every one of them,
|
|
183
|
+
// and with them every test the graph could have pointed at. The module
|
|
184
|
+
// runs them, so the module owns them.
|
|
185
|
+
fn(moduleID(path), statement)
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
|
|
190
|
+
// moduleID is the node id of the file's module — the owner of what its top-level
|
|
191
|
+
// statements do.
|
|
192
|
+
func moduleID(path string) string {
|
|
193
|
+
return nodeID(path, path, NodeModule)
|
|
194
|
+
}
|
|
195
|
+
|
|
173
196
|
// topLevelID returns the node id for a named declaration, or "". The name is
|
|
174
197
|
// namespace-qualified, so a namespaced declaration lands on the node the build
|
|
175
198
|
// pass recorded.
|
|
@@ -189,6 +212,7 @@ func topLevelID(path string, statement *shimast.Node, kind NodeKind) string {
|
|
|
189
212
|
func forEachMember(path string, statement *shimast.Node, kind NodeKind, fn func(string, *shimast.Node)) {
|
|
190
213
|
containerID := topLevelID(path, statement, kind)
|
|
191
214
|
for _, member := range classMembers(statement) {
|
|
215
|
+
forEachClosureIn(path, member, fn)
|
|
192
216
|
if isMethodMember(member.Kind) {
|
|
193
217
|
if name := methodName(member.Symbol()); name != "" {
|
|
194
218
|
fn(nodeID(path, name, NodeMethod), member)
|
|
@@ -259,14 +283,44 @@ func forEachVariable(path string, statement *shimast.Node, fn func(string, *shim
|
|
|
259
283
|
continue
|
|
260
284
|
}
|
|
261
285
|
fn(nodeID(path, qualifiedName(symbol), NodeVariable), binding)
|
|
286
|
+
forEachClosureIn(path, binding, fn)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// forEachClosureIn pairs each function a declaration's body declares with its
|
|
291
|
+
// own node, and recurses: the renderer's `patch` owns the calls it makes, not the
|
|
292
|
+
// factory it closes over. `callsWithin` stops at the same boundary, so no call is
|
|
293
|
+
// attributed twice.
|
|
294
|
+
func forEachClosureIn(path string, declaration *shimast.Node, fn func(string, *shimast.Node)) {
|
|
295
|
+
for _, closure := range ClosuresIn(declaration) {
|
|
296
|
+
if id := closureID(path, closure); id != "" {
|
|
297
|
+
fn(id, closure)
|
|
298
|
+
}
|
|
299
|
+
forEachClosureIn(path, closure, fn)
|
|
262
300
|
}
|
|
263
301
|
}
|
|
264
302
|
|
|
303
|
+
// closureID is the node id build.go recorded for a closure.
|
|
304
|
+
func closureID(path string, closure *shimast.Node) string {
|
|
305
|
+
name, ok := ClosureName(closure)
|
|
306
|
+
if !ok {
|
|
307
|
+
return ""
|
|
308
|
+
}
|
|
309
|
+
kind := NodeFunction
|
|
310
|
+
if closure.Kind == shimast.KindVariableDeclaration {
|
|
311
|
+
kind = NodeVariable
|
|
312
|
+
}
|
|
313
|
+
return nodeID(path, name, kind)
|
|
314
|
+
}
|
|
315
|
+
|
|
265
316
|
// callsWithin walks node's subtree and records runtime value-use edges from
|
|
266
317
|
// `from` to the resolved target: calls/new/tagged templates/JSX components as
|
|
267
318
|
// value-call edges, and property or element access as value-access edges.
|
|
268
319
|
func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
|
|
269
320
|
node.ForEachChild(func(child *shimast.Node) bool {
|
|
321
|
+
// With closures unindexed, a body's calls belong to the declaration that
|
|
322
|
+
// owns the body — the walk does not stop at one. (When they are indexed, it
|
|
323
|
+
// must: a closure that is its own node owns the calls it makes.)
|
|
270
324
|
switch child.Kind {
|
|
271
325
|
case shimast.KindCallExpression:
|
|
272
326
|
// A decorator's own factory call (`@Column()`, `@Entity()`) is metadata,
|
|
@@ -280,6 +334,7 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
|
|
|
280
334
|
}
|
|
281
335
|
if call := child.AsCallExpression(); call != nil && call.Expression != nil {
|
|
282
336
|
g.callEdge(checker, from, call.Expression, "call")
|
|
337
|
+
g.handedOffValues(checker, from, call)
|
|
283
338
|
}
|
|
284
339
|
case shimast.KindNewExpression:
|
|
285
340
|
if newExpr := child.AsNewExpression(); newExpr != nil && newExpr.Expression != nil {
|
|
@@ -334,7 +389,7 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
|
|
|
334
389
|
!shimast.IsFunctionLike(binary.Right) {
|
|
335
390
|
return ""
|
|
336
391
|
}
|
|
337
|
-
target :=
|
|
392
|
+
target := g.resolve(checker, binary.Left)
|
|
338
393
|
if target == nil || target.Symbol == nil {
|
|
339
394
|
return ""
|
|
340
395
|
}
|
|
@@ -387,6 +442,34 @@ func (g *Graph) callEdge(checker *shimchecker.Checker, from string, callee *shim
|
|
|
387
442
|
g.valueUseEdge(checker, from, callee, EdgeValueCall, origin)
|
|
388
443
|
}
|
|
389
444
|
|
|
445
|
+
// handedOffValues records the workspace functions a call is handed, as uses of
|
|
446
|
+
// them.
|
|
447
|
+
//
|
|
448
|
+
// `React.memo(ExcalidrawBase)`, `forwardRef(Impl)`, `app.use(handler)`,
|
|
449
|
+
// `pipe(map(project))` — the argument is not called here and is not a property
|
|
450
|
+
// read, so the collector saw neither a call nor an access and recorded nothing.
|
|
451
|
+
// The wrapper became a dead end: Excalidraw's public export, `export const
|
|
452
|
+
// Excalidraw = React.memo(ExcalidrawBase)`, reached zero files in the graph while
|
|
453
|
+
// the component behind it reached forty-four, so a tour of the public API opened
|
|
454
|
+
// on an app-level export dialog instead, and the model went looking for the real
|
|
455
|
+
// component by hand.
|
|
456
|
+
//
|
|
457
|
+
// A function handed to another function is a use of it, and it is the one the
|
|
458
|
+
// runtime will run. Bounded to argument position: this is the callback, the
|
|
459
|
+
// wrapped component, the registered handler — not every identifier in every
|
|
460
|
+
// expression.
|
|
461
|
+
func (g *Graph) handedOffValues(checker *shimchecker.Checker, from string, call *shimast.CallExpression) {
|
|
462
|
+
if call.Arguments == nil {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
for _, argument := range call.Arguments.Nodes {
|
|
466
|
+
switch argument.Kind {
|
|
467
|
+
case shimast.KindIdentifier, shimast.KindPropertyAccessExpression:
|
|
468
|
+
g.valueUseEdge(checker, from, argument, EdgeValueAccess, "")
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
390
473
|
// accessEdge resolves a property or element access to its declaration and
|
|
391
474
|
// records a value-access edge, skipping unresolved/external/self targets.
|
|
392
475
|
func (g *Graph) accessEdge(checker *shimchecker.Checker, from string, access *shimast.Node) {
|
|
@@ -397,7 +480,7 @@ func (g *Graph) valueUseEdge(checker *shimchecker.Checker, from string, targetEx
|
|
|
397
480
|
if targetExpr == nil {
|
|
398
481
|
return
|
|
399
482
|
}
|
|
400
|
-
target :=
|
|
483
|
+
target := g.resolve(checker, targetExpr)
|
|
401
484
|
if target == nil || target.Symbol == nil {
|
|
402
485
|
return
|
|
403
486
|
}
|
|
@@ -461,7 +544,7 @@ func (g *Graph) typeRefEdge(checker *shimchecker.Checker, from string, typeName
|
|
|
461
544
|
if typeName == nil {
|
|
462
545
|
return
|
|
463
546
|
}
|
|
464
|
-
target :=
|
|
547
|
+
target := g.resolve(checker, typeName)
|
|
465
548
|
if target == nil || target.Symbol == nil {
|
|
466
549
|
return
|
|
467
550
|
}
|
|
@@ -511,12 +594,17 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
511
594
|
return id
|
|
512
595
|
}
|
|
513
596
|
if !target.External {
|
|
514
|
-
// A
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
597
|
+
// A closure Build recorded is keyed by its scoped name
|
|
598
|
+
// (`baseCreateRenderer.patch`), which the symbol alone does not spell, so
|
|
599
|
+
// resolve it from the declaration the checker pointed at.
|
|
600
|
+
if id := g.closureTargetID(target); id != "" {
|
|
601
|
+
return id
|
|
602
|
+
}
|
|
603
|
+
// Any other workspace target Build did not record is body-scoped and
|
|
604
|
+
// unnameable without collisions (a local of an anonymous callback), so its
|
|
605
|
+
// name would key the same id as a same-named local of another scope and
|
|
606
|
+
// merge two nodes into one that is neither. Drop it — the same
|
|
607
|
+
// workspace-only discipline the NodeMethod branch already applies.
|
|
520
608
|
return ""
|
|
521
609
|
}
|
|
522
610
|
g.Nodes[id] = &Node{
|
|
@@ -532,6 +620,29 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
532
620
|
return id
|
|
533
621
|
}
|
|
534
622
|
|
|
623
|
+
// closureTargetID returns the node id of a closure the checker resolved to, or ""
|
|
624
|
+
// when the target is not one Build recorded.
|
|
625
|
+
func (g *Graph) closureTargetID(target *Target) string {
|
|
626
|
+
for _, declaration := range target.Symbol.Declarations {
|
|
627
|
+
if !IsClosure(declaration) {
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
name, ok := ClosureName(declaration)
|
|
631
|
+
if !ok {
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
kind := NodeFunction
|
|
635
|
+
if declaration.Kind == shimast.KindVariableDeclaration {
|
|
636
|
+
kind = NodeVariable
|
|
637
|
+
}
|
|
638
|
+
id := nodeID(target.File, name, kind)
|
|
639
|
+
if _, exists := g.Nodes[id]; exists {
|
|
640
|
+
return id
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return ""
|
|
644
|
+
}
|
|
645
|
+
|
|
535
646
|
// symbolNodeKind maps a resolved symbol's declarations/flags to a NodeKind, or
|
|
536
647
|
// "" when the symbol is not a kind the graph records as a node. Declaration kind
|
|
537
648
|
// wins over flags because property-like accessor symbols can otherwise be
|
|
@@ -5,18 +5,39 @@ import (
|
|
|
5
5
|
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
-
// markExports
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// markExports records a module's export surface: a node for the module itself
|
|
9
|
+
// and an `exports` edge to every declaration its export table resolves to. It
|
|
10
|
+
// runs on the checker, so it sees the real public surface — an inline
|
|
11
|
+
// `export class`, a separate `export { Foo }`, a re-export
|
|
11
12
|
// `export { Foo } from "./foo"`, and a barrel `export *` all land here, where a
|
|
12
13
|
// purely syntactic `export`-modifier scan would miss the re-export forms that
|
|
13
|
-
// carry most of a monorepo's public API.
|
|
14
|
+
// carry most of a monorepo's public API. Each resolved node is also flagged
|
|
15
|
+
// Exported.
|
|
16
|
+
//
|
|
17
|
+
// The edge is what the flag cannot say. A package's front door and its legacy
|
|
18
|
+
// subpath both re-export, so both surfaces end up Exported and a ranker that
|
|
19
|
+
// reads the flag alone cannot tell zod's `.` entry from its `./v3` one: it picks
|
|
20
|
+
// whichever name matches the query best, which is how a tour of the current API
|
|
21
|
+
// came back centered on the legacy implementation. The edge keeps the module the
|
|
22
|
+
// symbol came out of, so a caller holding a package.json — which names its entry
|
|
23
|
+
// file and nothing else — can ask the graph what that file, and only that file,
|
|
24
|
+
// puts on the wire.
|
|
25
|
+
//
|
|
26
|
+
// A barrel declares nothing of its own, so without a module node it is absent
|
|
27
|
+
// from the graph entirely: the file a package.json points at, the one thing that
|
|
28
|
+
// answers "what is this package's public API", is the one file the index never
|
|
29
|
+
// had. The module node is that anchor, and the export edges hang from it.
|
|
14
30
|
func (g *Graph) markExports(checker *shimchecker.Checker, file *shimast.SourceFile) {
|
|
15
31
|
moduleSymbol := checker.GetSymbolAtLocation(file.AsNode())
|
|
16
32
|
if moduleSymbol == nil {
|
|
17
33
|
return
|
|
18
34
|
}
|
|
19
|
-
|
|
35
|
+
exports := shimchecker.Checker_getExportsOfModule(checker, moduleSymbol)
|
|
36
|
+
if len(exports) == 0 {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
module := g.putModuleNode(file)
|
|
40
|
+
for _, export := range exports {
|
|
20
41
|
symbol := export
|
|
21
42
|
// A re-export is an alias; unwrap it to the declaration it points at so the
|
|
22
43
|
// exported flag lands on the real node, not a re-exporting index file's
|
|
@@ -26,35 +47,71 @@ func (g *Graph) markExports(checker *shimchecker.Checker, file *shimast.SourceFi
|
|
|
26
47
|
symbol = aliased
|
|
27
48
|
}
|
|
28
49
|
}
|
|
29
|
-
g.markExportedSymbol(symbol)
|
|
50
|
+
if id, ok := g.markExportedSymbol(symbol); ok {
|
|
51
|
+
g.addEdge(module.ID, id, EdgeExports)
|
|
52
|
+
}
|
|
30
53
|
}
|
|
31
54
|
}
|
|
32
55
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
56
|
+
// putModuleNode records the node for a source file. Its name is the file path,
|
|
57
|
+
// the only name a module has that a caller can hold.
|
|
58
|
+
//
|
|
59
|
+
// Every source file gets one, exporting or not, because a module is also what
|
|
60
|
+
// runs a file's top-level statements — and those statements are where a test
|
|
61
|
+
// file keeps everything it does. `describe(() => it(() => schema.parse(x)))` is
|
|
62
|
+
// a call the checker resolves and the graph used to drop on the floor: it sits
|
|
63
|
+
// inside a callback, no declaration owns it, and edges are attributed to owners.
|
|
64
|
+
// A whole test suite left no trace, which is why a tour that promised the tests
|
|
65
|
+
// to read next could name two of zod's and none of NestJS's, and why the model
|
|
66
|
+
// went off to glob for them.
|
|
67
|
+
func (g *Graph) putModuleNode(file *shimast.SourceFile) *Node {
|
|
68
|
+
path := file.FileName()
|
|
69
|
+
id := nodeID(path, path, NodeModule)
|
|
70
|
+
if existing, ok := g.Nodes[id]; ok {
|
|
71
|
+
return existing
|
|
72
|
+
}
|
|
73
|
+
node := &Node{
|
|
74
|
+
ID: id,
|
|
75
|
+
Name: path,
|
|
76
|
+
Simple: path,
|
|
77
|
+
Kind: NodeModule,
|
|
78
|
+
File: path,
|
|
79
|
+
// A module that exports is by definition part of some surface; whether it is
|
|
80
|
+
// the package's front door is the package.json's to say, not the graph's.
|
|
81
|
+
Exported: true,
|
|
82
|
+
}
|
|
83
|
+
g.Nodes[id] = node
|
|
84
|
+
return node
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// markExportedSymbol sets Exported on the node a resolved export symbol declares
|
|
88
|
+
// and returns that node's id. A symbol whose declaration the graph does not model
|
|
89
|
+
// as a node (a re-exported value from a dependency, a kind it does not track) is
|
|
90
|
+
// skipped rather than fabricated.
|
|
91
|
+
func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) (string, bool) {
|
|
38
92
|
declaration := declarationNode(symbol)
|
|
39
93
|
if declaration == nil {
|
|
40
|
-
return
|
|
94
|
+
return "", false
|
|
41
95
|
}
|
|
42
96
|
file := shimast.GetSourceFileOfNode(declaration)
|
|
43
97
|
if file == nil {
|
|
44
|
-
return
|
|
98
|
+
return "", false
|
|
45
99
|
}
|
|
46
100
|
kind := symbolNodeKind(symbol)
|
|
47
101
|
if kind == "" {
|
|
48
|
-
return
|
|
102
|
+
return "", false
|
|
49
103
|
}
|
|
50
104
|
name := qualifiedName(symbol)
|
|
51
105
|
if kind == NodeMethod {
|
|
52
106
|
name = methodName(symbol)
|
|
53
107
|
}
|
|
54
108
|
if name == "" {
|
|
55
|
-
return
|
|
109
|
+
return "", false
|
|
56
110
|
}
|
|
57
|
-
|
|
58
|
-
|
|
111
|
+
node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]
|
|
112
|
+
if !ok {
|
|
113
|
+
return "", false
|
|
59
114
|
}
|
|
115
|
+
node.Exported = true
|
|
116
|
+
return node.ID, true
|
|
60
117
|
}
|
package/internal/graph/graph.go
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
package graph
|
|
2
2
|
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
)
|
|
6
|
+
|
|
3
7
|
// NodeKind classifies a graph node by what its symbol declares.
|
|
4
8
|
type NodeKind string
|
|
5
9
|
|
|
@@ -14,6 +18,10 @@ const (
|
|
|
14
18
|
// accessor). Its id is class-qualified ("path#Class.method:method") so a
|
|
15
19
|
// resolved method call lands on the same node the build pass recorded.
|
|
16
20
|
NodeMethod NodeKind = "method"
|
|
21
|
+
// NodeModule is a source file with an export table — the surface a consumer
|
|
22
|
+
// imports from. A barrel declares nothing, so this is the only node it has,
|
|
23
|
+
// and it is the node a package.json entry path resolves to.
|
|
24
|
+
NodeModule NodeKind = "module"
|
|
17
25
|
)
|
|
18
26
|
|
|
19
27
|
// Node is one declared symbol. Its ID is position-invariant, built from the file
|
|
@@ -38,6 +46,12 @@ type Node struct {
|
|
|
38
46
|
// a barrel (`export *`) counts, not only an inline `export` modifier. It is
|
|
39
47
|
// the signal a public-API projection filters on.
|
|
40
48
|
Exported bool
|
|
49
|
+
// Closure marks a node declared inside another declaration's body — Vue's
|
|
50
|
+
// `baseCreateRenderer.patch`, a callback bound to a const inside a method.
|
|
51
|
+
// It is a name the runtime calls, and a model that asks for it by name gets
|
|
52
|
+
// it; but an orientation tour ranks and walks the surface, so the surface is
|
|
53
|
+
// what it sees. The flag is how a projection tells the two apart.
|
|
54
|
+
Closure bool
|
|
41
55
|
// Modifiers holds the declaration's syntactic modifiers as wire strings (a
|
|
42
56
|
// subset of the TtscGraphNodeModifier union: export/default/declare/abstract/
|
|
43
57
|
// static/readonly/async/const/public/private/protected). It is recorded from
|
|
@@ -74,6 +88,11 @@ const (
|
|
|
74
88
|
// type it mentions (a parameter, return, property, or alias type). It is not a
|
|
75
89
|
// runtime call, so an impact query can filter value edges from type edges.
|
|
76
90
|
EdgeTypeRef EdgeKind = "type-ref"
|
|
91
|
+
// EdgeExports runs from a module to a declaration its export table resolves
|
|
92
|
+
// to, through re-exports and barrels. It records which surface a symbol is
|
|
93
|
+
// public on, which the Exported flag cannot: a package's front door and its
|
|
94
|
+
// legacy subpath both export, and only the edge says which one did.
|
|
95
|
+
EdgeExports EdgeKind = "exports"
|
|
77
96
|
)
|
|
78
97
|
|
|
79
98
|
// Edge is a directed, checker-resolved relationship from one node to another,
|
|
@@ -112,7 +131,20 @@ type Graph struct {
|
|
|
112
131
|
bodyNodes map[string]bool
|
|
113
132
|
// seen deduplicates edges in O(1) during construction, so building a graph
|
|
114
133
|
// with N edges is O(N), not O(N²). Keyed by from\x00to\x00kind.
|
|
115
|
-
seen map[
|
|
134
|
+
seen map[edgeKey]struct{}
|
|
135
|
+
|
|
136
|
+
// resolved memoizes the checker resolution of an AST node for the length of a
|
|
137
|
+
// build: the edge pass visits the same node several times, and a resolution
|
|
138
|
+
// cannot change while the program is fixed.
|
|
139
|
+
resolved map[*shimast.Node]*Target
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// edgeKey identifies an edge by its two ends and the wire kind it will surface
|
|
143
|
+
// as: a comparable struct, so the dedup set costs no allocation per candidate.
|
|
144
|
+
type edgeKey struct {
|
|
145
|
+
from string
|
|
146
|
+
to string
|
|
147
|
+
kind string
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
// nodeID builds the position-invariant identity for a symbol named name,
|
|
@@ -41,8 +41,10 @@ export const value = 1;
|
|
|
41
41
|
if firstText == secondText {
|
|
42
42
|
t.Fatal("the reload returned identical source text; the probe is reading a stale cache, not the shifted file")
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// fn, value, and the module node for main.ts, whose id is the file path and so
|
|
45
|
+
// is position-invariant like the rest.
|
|
46
|
+
if len(firstIDs) != 3 {
|
|
47
|
+
t.Fatalf("expected 3 nodes, got %d: %v", len(firstIDs), firstIDs)
|
|
46
48
|
}
|
|
47
49
|
if !equalStringSets(firstIDs, secondIDs) {
|
|
48
50
|
t.Fatalf("node identity changed when positions shifted:\n first=%v\n second=%v", firstIDs, secondIDs)
|
|
@@ -35,6 +35,30 @@ type Target struct {
|
|
|
35
35
|
End int
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// resolve is Resolve with the graph's memo in front of it.
|
|
39
|
+
//
|
|
40
|
+
// The edge pass walks the same AST more than once by construction: `collectCalls`
|
|
41
|
+
// and `collectTypeRefs` each descend the whole container tree, and a closure's
|
|
42
|
+
// body is walked again for every container that encloses it. Every one of those
|
|
43
|
+
// visits asked the checker again, and for an identifier that is not a cheap
|
|
44
|
+
// lookup — typescript-go caches a resolved property access on the node, but a
|
|
45
|
+
// plain identifier goes back through `resolveEntityName` and a full scope walk
|
|
46
|
+
// each time.
|
|
47
|
+
//
|
|
48
|
+
// A node's resolution cannot change while the program is fixed, which it is for
|
|
49
|
+
// the length of a build, so the second answer is always the first one.
|
|
50
|
+
func (g *Graph) resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
|
|
51
|
+
if ref == nil {
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
if cached, hit := g.resolved[ref]; hit {
|
|
55
|
+
return cached
|
|
56
|
+
}
|
|
57
|
+
target := Resolve(checker, ref)
|
|
58
|
+
g.resolved[ref] = target
|
|
59
|
+
return target
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
// Resolve follows ref to the real declaration the checker binds it to. It
|
|
39
63
|
// unwraps import/export alias chains so a reference through a barrel re-export
|
|
40
64
|
// lands on the sibling source that declares the symbol, not the re-exporting
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Package graphsymbols answers the two graph-oriented LSP methods
|
|
2
2
|
// (textDocument/documentSymbol and textDocument/references) from ttsc's
|
|
3
3
|
// compiler-backed code graph. It is the lspserver.SymbolProvider implementation
|
|
4
|
-
// ttscserver wires in so a raw-LSP graph consumer such as @samchon/graph
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// ttscserver wires in so a raw-LSP graph consumer such as @samchon/graph can get
|
|
5
|
+
// declarations (graph nodes) and usages (graph edges) computed from that graph.
|
|
6
|
+
//
|
|
7
|
+
// tsgo's own LSP implements both methods with its compiler-exact language
|
|
8
|
+
// service, so the proxy forwards to tsgo by default and falls back to this
|
|
9
|
+
// provider only when tsgo does not advertise the capability, or when a consumer
|
|
10
|
+
// explicitly opts into graph-derived answers (see lspserver.Proxy).
|
|
7
11
|
//
|
|
8
12
|
// It lives in its own package rather than in internal/lspserver because it
|
|
9
13
|
// imports internal/graph and driver, and driver already imports lspserver;
|
|
@@ -31,9 +35,9 @@ import (
|
|
|
31
35
|
|
|
32
36
|
// Provider computes documentSymbol/references from a code graph built for one
|
|
33
37
|
// project (tsconfig). The graph is loaded lazily on the first request and
|
|
34
|
-
// cached
|
|
35
|
-
//
|
|
36
|
-
//
|
|
38
|
+
// cached until Invalidate is called; the proxy invalidates on didChange/didSave
|
|
39
|
+
// so a long-lived editor session reflects source edits instead of freezing at
|
|
40
|
+
// the first request's snapshot. It is safe for concurrent use.
|
|
37
41
|
type Provider struct {
|
|
38
42
|
cwd string
|
|
39
43
|
tsconfig string
|
|
@@ -88,6 +92,19 @@ func (pr *Provider) load() (*graph.Graph, map[string]string, error) {
|
|
|
88
92
|
return pr.graph, pr.sources, nil
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
// Invalidate discards the cached graph so the next DocumentSymbols/References
|
|
96
|
+
// call rebuilds it against the current on-disk sources. The proxy calls this on
|
|
97
|
+
// didChange/didSave; without it the first request's snapshot would answer every
|
|
98
|
+
// later request for the process lifetime.
|
|
99
|
+
func (pr *Provider) Invalidate() {
|
|
100
|
+
pr.mu.Lock()
|
|
101
|
+
defer pr.mu.Unlock()
|
|
102
|
+
pr.loaded = false
|
|
103
|
+
pr.graph = nil
|
|
104
|
+
pr.sources = nil
|
|
105
|
+
pr.loadErr = nil
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
// DocumentSymbols returns the declarations in uri as a hierarchy: top-level
|
|
92
109
|
// declarations at the root, class/interface members nested under their owner.
|
|
93
110
|
func (pr *Provider) DocumentSymbols(uri string) ([]lspserver.LSPDocumentSymbol, error) {
|
|
@@ -103,7 +120,7 @@ func (pr *Provider) DocumentSymbols(uri string) ([]lspserver.LSPDocumentSymbol,
|
|
|
103
120
|
|
|
104
121
|
var fileNodes []*graph.Node
|
|
105
122
|
for _, n := range g.Nodes {
|
|
106
|
-
if n
|
|
123
|
+
if !surfaceableNode(n) {
|
|
107
124
|
continue
|
|
108
125
|
}
|
|
109
126
|
if pathsEqual(n.File, file) {
|
|
@@ -162,8 +179,13 @@ func (pr *Provider) References(uri string, pos lspserver.LSPPosition, includeDec
|
|
|
162
179
|
// Edges are directed From a using declaration To the referenced symbol, so an
|
|
163
180
|
// edge whose To is the target node is one usage site; its span lives in the
|
|
164
181
|
// From node's file (graph's edge-evidence contract).
|
|
182
|
+
//
|
|
183
|
+
// A structural edge is not a usage. A module's `exports` edge says the symbol
|
|
184
|
+
// stands on that module's public surface — a fact about the declaration, not a
|
|
185
|
+
// place it is used — and it carries no span, so counting it as a usage puts a
|
|
186
|
+
// phantom reference at the top of the file.
|
|
165
187
|
for _, e := range g.Edges {
|
|
166
|
-
if e.To != target.ID {
|
|
188
|
+
if e.To != target.ID || isStructuralEdge(e.Kind) {
|
|
167
189
|
continue
|
|
168
190
|
}
|
|
169
191
|
f := nodeFile(e.From)
|
|
@@ -228,12 +250,10 @@ func nodeToSymbol(n *graph.Node, text string) lspserver.LSPDocumentSymbol {
|
|
|
228
250
|
Start: offsetToPosition(text, start),
|
|
229
251
|
End: offsetToPosition(text, end),
|
|
230
252
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
name = n.Name
|
|
234
|
-
}
|
|
253
|
+
// Callers pass only surfaceableNode candidates, so Simple is the declared
|
|
254
|
+
// identifier and never the file path a module node carries as its name.
|
|
235
255
|
return lspserver.LSPDocumentSymbol{
|
|
236
|
-
Name:
|
|
256
|
+
Name: n.Simple,
|
|
237
257
|
Kind: symbolKind(n.Kind),
|
|
238
258
|
Range: rng,
|
|
239
259
|
// The graph does not record the identifier's own span separately, so the
|
|
@@ -289,7 +309,7 @@ func symbolKind(k graph.NodeKind) lspserver.LSPSymbolKind {
|
|
|
289
309
|
func targetNodeAt(g *graph.Graph, file string, offset int) *graph.Node {
|
|
290
310
|
var best *graph.Node
|
|
291
311
|
for _, n := range g.Nodes {
|
|
292
|
-
if n
|
|
312
|
+
if !surfaceableNode(n) || !pathsEqual(n.File, file) {
|
|
293
313
|
continue
|
|
294
314
|
}
|
|
295
315
|
if offset >= n.Pos && offset < n.End {
|
|
@@ -314,6 +334,22 @@ func targetNodeAt(g *graph.Graph, file string, offset int) *graph.Node {
|
|
|
314
334
|
return nil
|
|
315
335
|
}
|
|
316
336
|
|
|
337
|
+
// isStructuralEdge reports an edge that records where a symbol stands rather than
|
|
338
|
+
// a place it is used.
|
|
339
|
+
func isStructuralEdge(kind graph.EdgeKind) bool {
|
|
340
|
+
return kind == graph.EdgeExports
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// surfaceableNode reports whether a graph node should appear as a user-facing
|
|
344
|
+
// LSP symbol or reference target. A per-file module node (NodeModule) carries
|
|
345
|
+
// the source file path as its name and spans the whole file, so surfacing it
|
|
346
|
+
// would put the absolute path in the outline and swallow whole-file reference
|
|
347
|
+
// queries. External boundary leaves and nameless nodes are likewise not
|
|
348
|
+
// addressable declarations.
|
|
349
|
+
func surfaceableNode(n *graph.Node) bool {
|
|
350
|
+
return !n.External && n.Kind != graph.NodeModule && n.Simple != ""
|
|
351
|
+
}
|
|
352
|
+
|
|
317
353
|
func sortNodes(nodes []*graph.Node) {
|
|
318
354
|
sort.Slice(nodes, func(i, j int) bool {
|
|
319
355
|
if nodes[i].Pos != nodes[j].Pos {
|