ttsc 0.33.0 → 0.33.1
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/cmd/ttscgraph/serve_alternative_members_and_reads_refresh_test.go +95 -0
- package/internal/graph/alternative_member_declarations_resolve_uses_test.go +145 -0
- package/internal/graph/build.go +60 -18
- package/internal/graph/declaration_members.go +143 -0
- package/internal/graph/edges.go +108 -36
- package/internal/graph/namespace_private_declarations_keep_lexical_identity_test.go +85 -0
- package/internal/graph/resolve.go +30 -5
- package/internal/graph/value_reads_cover_expression_positions_test.go +88 -0
- package/package.json +8 -8
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Verifies reported graph facts survive resident edits and barrel imports.
|
|
9
|
+
//
|
|
10
|
+
// 1. Load namespace-private functions, a nested type member and a parameter property through a barrel.
|
|
11
|
+
// 2. Rename the nested member and replace a class expression in the same resident session.
|
|
12
|
+
// 3. Require current serialized nodes and uses, reject stale members and false namespace calls, and compare every generation with a full build.
|
|
13
|
+
//
|
|
14
|
+
// @evidence contracts/testing.md#behavioral-verification Resident shard publication must preserve lexical identities and actual value/member uses, then remove obsolete members after edits. Literal serialized endpoints test semantics independently of full-build equivalence.
|
|
15
|
+
// @evidence contracts/testing.md#independent-expectations Source-authored deep/changed and render/replaced names prescribe current and stale nodes. Only B.hidden calls target, reader accesses num, change accesses the current Props.nested member, cost accesses Sale.price and draw calls the current Widget member.
|
|
16
|
+
// @evidence contracts/testing.md#distinguishing-cases Initial and renamed generations cover cross-file barrel aliases, namespace-private declarations, value returns, nested type members, parameter properties and named/anonymous class expressions. Old member names and A.hidden-to-target provide negative controls; direct syntax cases belong to the graph package's source units.
|
|
17
|
+
// @evidence contracts/testing.md#execution-ownership This Go source unit owns one temporary project and in-process graphSession, closes it and invokes native preparation/projection directly without building or launching a product process.
|
|
18
|
+
func TestServeAlternativeMembersAndReadsRefresh(t *testing.T) {
|
|
19
|
+
root := t.TempDir()
|
|
20
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{"compilerOptions":{"target":"ES2022","module":"commonjs","strict":true},"include":["src"]}`)
|
|
21
|
+
writeGraphFile(t, filepath.Join(root, "src", "barrel.ts"), "export * from './api';\n")
|
|
22
|
+
var session *graphSession
|
|
23
|
+
for _, state := range []struct{ field, method, className string }{{"deep", "render", "Internal"}, {"changed", "replaced", ""}} {
|
|
24
|
+
writeGraphFile(t, filepath.Join(root, "src", "api.ts"), `export const num = 1;
|
|
25
|
+
export function target() { return 0; }
|
|
26
|
+
export namespace A { function hidden() { return 1; } export const a = hidden(); }
|
|
27
|
+
export namespace B { function hidden() { return target(); } export const b = hidden(); }
|
|
28
|
+
export type Props = { nested: { `+state.field+`: number } };
|
|
29
|
+
export class Sale { constructor(public readonly price: number) {} }
|
|
30
|
+
export const Widget = class `+state.className+` { `+state.method+`() { return num; } };
|
|
31
|
+
`)
|
|
32
|
+
writeGraphFile(t, filepath.Join(root, "src", "use.ts"), `import { num, Sale, Widget, type Props } from './barrel';
|
|
33
|
+
export function reader() { return num; }
|
|
34
|
+
export function change(props: Props) { return props.nested.`+state.field+`; }
|
|
35
|
+
export function cost(sale: Sale) { return sale.price; }
|
|
36
|
+
export function draw() { return new Widget().`+state.method+`(); }
|
|
37
|
+
`)
|
|
38
|
+
if session == nil {
|
|
39
|
+
var err error
|
|
40
|
+
session, err = newGraphSession(root, "tsconfig.json")
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
defer session.Close()
|
|
45
|
+
}
|
|
46
|
+
snapshot, _, _, err := snapshotGraphShardState(session)
|
|
47
|
+
if err != nil || snapshot == nil {
|
|
48
|
+
t.Fatalf("snapshot: %v", err)
|
|
49
|
+
}
|
|
50
|
+
assertServeShardFactsMatchFullDump(t, session)
|
|
51
|
+
dump, err := projectGraphDump(session)
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatal(err)
|
|
54
|
+
}
|
|
55
|
+
ids := map[string]string{}
|
|
56
|
+
for _, node := range dump.Nodes {
|
|
57
|
+
name := node.QualifiedName
|
|
58
|
+
if name == "" {
|
|
59
|
+
name = node.Name
|
|
60
|
+
}
|
|
61
|
+
ids[name] = node.ID
|
|
62
|
+
if (name == "Props.nested.deep" && state.field != "deep") || (name == "Widget.render" && state.method != "render") {
|
|
63
|
+
t.Errorf("stale declaration %s", name)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for _, pair := range []struct {
|
|
67
|
+
from, to, kind string
|
|
68
|
+
want bool
|
|
69
|
+
}{
|
|
70
|
+
{"reader", "num", "accesses", true},
|
|
71
|
+
{"change", "Props.nested." + state.field, "accesses", true},
|
|
72
|
+
{"cost", "Sale.price", "accesses", true},
|
|
73
|
+
{"draw", "Widget." + state.method, "calls", true},
|
|
74
|
+
{"Widget." + state.method, "num", "accesses", true},
|
|
75
|
+
{"A.a", "A.hidden", "calls", true},
|
|
76
|
+
{"B.b", "B.hidden", "calls", true},
|
|
77
|
+
{"B.hidden", "target", "calls", true},
|
|
78
|
+
{"A.hidden", "target", "calls", false},
|
|
79
|
+
} {
|
|
80
|
+
if ids[pair.from] == "" || ids[pair.to] == "" {
|
|
81
|
+
t.Errorf("missing endpoint %s or %s", pair.from, pair.to)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
found := false
|
|
85
|
+
for _, edge := range dump.Edges {
|
|
86
|
+
if edge.From == ids[pair.from] && edge.To == ids[pair.to] && edge.Kind == pair.kind {
|
|
87
|
+
found = true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if found != pair.want {
|
|
91
|
+
t.Errorf("%s -> %s (%s) = %t, want %t", pair.from, pair.to, pair.kind, found, pair.want)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Verifies equivalent member declarations produce usable endpoints.
|
|
12
|
+
//
|
|
13
|
+
// 1. Load type-literal intersections, nested properties, parameter properties and variable-bound class expressions.
|
|
14
|
+
// 2. Build their nodes and checker-resolved calls and accesses alongside interface and class-body controls.
|
|
15
|
+
// 3. Require precise members and reject unbound class identities and ordinary constructor parameters.
|
|
16
|
+
//
|
|
17
|
+
// @evidence contracts/testing.md#behavioral-verification Supported named literal members must have their own nodes, source spans and incoming uses. Class-expression bodies retain outgoing dependencies and internal constructor identity, while unindexed shadowing locals cannot borrow global endpoints.
|
|
18
|
+
// @evidence contracts/testing.md#independent-expectations The authored declaration hierarchy supplies literal names Props.onChange, Props.nested.deep, Props.extra, Sale.price and Widget.render, with source-authored caller/target pairs and separate negative controls.
|
|
19
|
+
// @evidence contracts/testing.md#distinguishing-cases Intersections, parenthesized/nested literals, quoted/computed-literal names, parameter-property modifiers, named class self-references, heritage and namespace owners contrast dynamic keys, normal parameters, unbound returned classes, shadowing local classes/objects and cross-owner calls. Existing tests own broader dispatch and object literals.
|
|
20
|
+
// @evidence contracts/testing.md#execution-ownership This Go unit writes a temporary project, owns and closes an in-process compiler Program and calls Build directly without a product process or installed consumer.
|
|
21
|
+
func TestAlternativeMemberDeclarationsResolveUses(t *testing.T) {
|
|
22
|
+
t.Setenv(driver.LinkedPluginsEnv, "")
|
|
23
|
+
root := t.TempDir()
|
|
24
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
25
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper() { return 1; }
|
|
26
|
+
const key = Symbol("key");
|
|
27
|
+
export type Props = ({ onChange(): void; nested: { deep: number }; "quoted.key": number; [key]: number; ["computed"]: number }) & { extra: number };
|
|
28
|
+
export class Sale {
|
|
29
|
+
constructor(public readonly price: number, ordinary: number, private secret = helper(), protected hidden = 1) {}
|
|
30
|
+
read() { return this.secret + this.hidden; }
|
|
31
|
+
}
|
|
32
|
+
export class Base { render() { return 0; } }
|
|
33
|
+
export interface Drawable { render(): number }
|
|
34
|
+
export const Widget = (class Internal extends Base implements Drawable {
|
|
35
|
+
render() { return helper(); }
|
|
36
|
+
field = helper();
|
|
37
|
+
static create() { return new Internal(); }
|
|
38
|
+
static identity() { return Internal; }
|
|
39
|
+
});
|
|
40
|
+
export const Other = class { render() { return 2; } };
|
|
41
|
+
export const api = { run() { return helper(); } };
|
|
42
|
+
export function localClass() {
|
|
43
|
+
const Widget = class Local { static create() { return new Local(); } };
|
|
44
|
+
return Widget.create();
|
|
45
|
+
}
|
|
46
|
+
export function localObject() {
|
|
47
|
+
const api = { run() { return 2; } };
|
|
48
|
+
return api.run();
|
|
49
|
+
}
|
|
50
|
+
export function change(props: Props) { props.onChange(); return props.nested.deep + props.extra + props["quoted.key"] + props.computed; }
|
|
51
|
+
export function cost(sale: Sale) { return sale.price; }
|
|
52
|
+
export function draw() { return new Widget().render(); }
|
|
53
|
+
export function other() { return new Other().render(); }
|
|
54
|
+
export function anonymous() { return class { render() { return helper(); } }; }
|
|
55
|
+
export interface Control { run(): void }
|
|
56
|
+
export class Body { price = 1; }
|
|
57
|
+
export function control(input: Control, body: Body) { input.run(); return body.price; }
|
|
58
|
+
export namespace Names {
|
|
59
|
+
export type Props = { nested: { deep: number } };
|
|
60
|
+
export const Widget = class { render() { return helper(); } };
|
|
61
|
+
export function read(props: Props) { new Widget().render(); return props.nested.deep; }
|
|
62
|
+
}
|
|
63
|
+
`)
|
|
64
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
defer func() { _ = prog.Close() }()
|
|
69
|
+
if len(diags) != 0 {
|
|
70
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
71
|
+
}
|
|
72
|
+
g := Build(prog)
|
|
73
|
+
file := sourceFile(t, prog, "main.ts").FileName()
|
|
74
|
+
for _, item := range []struct {
|
|
75
|
+
caller, member string
|
|
76
|
+
kind NodeKind
|
|
77
|
+
edge EdgeKind
|
|
78
|
+
}{
|
|
79
|
+
{"change", "Props.onChange", NodeMethod, EdgeValueCall},
|
|
80
|
+
{"change", "Props.nested.deep", NodeVariable, EdgeValueAccess},
|
|
81
|
+
{"change", "Props.extra", NodeVariable, EdgeValueAccess},
|
|
82
|
+
{"change", `Props["quoted.key"]`, NodeVariable, EdgeValueAccess},
|
|
83
|
+
{"change", "Props.computed", NodeVariable, EdgeValueAccess},
|
|
84
|
+
{"cost", "Sale.price", NodeVariable, EdgeValueAccess},
|
|
85
|
+
{"draw", "Widget.render", NodeMethod, EdgeValueCall},
|
|
86
|
+
{"other", "Other.render", NodeMethod, EdgeValueCall},
|
|
87
|
+
{"control", "Control.run", NodeMethod, EdgeValueCall},
|
|
88
|
+
{"control", "Body.price", NodeVariable, EdgeValueAccess},
|
|
89
|
+
{"Names.read", "Names.Props.nested.deep", NodeVariable, EdgeValueAccess},
|
|
90
|
+
{"Names.read", "Names.Widget.render", NodeMethod, EdgeValueCall},
|
|
91
|
+
} {
|
|
92
|
+
target := nodeID(file, item.member, item.kind)
|
|
93
|
+
if node := g.Nodes[target]; node == nil || node.End <= node.Pos || node.Name != item.member {
|
|
94
|
+
t.Errorf("missing member %s", item.member)
|
|
95
|
+
}
|
|
96
|
+
if !hasEdge(g, nodeID(file, item.caller, NodeFunction), target, item.edge) {
|
|
97
|
+
t.Errorf("missing %s -> %s", item.caller, item.member)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for member, kind := range map[string]NodeKind{"Widget.render": NodeMethod, "Widget.field": NodeVariable, "Sale.secret": NodeVariable} {
|
|
101
|
+
if !hasEdge(g, nodeID(file, member, kind), nodeID(file, "helper", NodeFunction), EdgeValueCall) {
|
|
102
|
+
t.Errorf("%s lost helper call", member)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for _, target := range []struct {
|
|
106
|
+
name string
|
|
107
|
+
kind NodeKind
|
|
108
|
+
}{{"Base", NodeClass}, {"Drawable", NodeInterface}} {
|
|
109
|
+
if !hasEdge(g, nodeID(file, "Widget", NodeVariable), nodeID(file, target.name, target.kind), EdgeHeritage) {
|
|
110
|
+
t.Errorf("Widget lost heritage to %s", target.name)
|
|
111
|
+
}
|
|
112
|
+
if !hasEdge(g, nodeID(file, "Widget.render", NodeMethod), nodeID(file, target.name+".render", NodeMethod), EdgeMemberRelation) {
|
|
113
|
+
t.Errorf("Widget.render lost relation to %s.render", target.name)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for _, item := range []struct {
|
|
117
|
+
member string
|
|
118
|
+
kind EdgeKind
|
|
119
|
+
}{{"Widget.create", EdgeValueCall}, {"Widget.identity", EdgeValueAccess}} {
|
|
120
|
+
if !hasEdge(g, nodeID(file, item.member, NodeMethod), nodeID(file, "Widget", NodeVariable), item.kind) {
|
|
121
|
+
t.Errorf("%s lost its bound class identity", item.member)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if hasEdge(g, nodeID(file, "draw", NodeFunction), nodeID(file, "Other.render", NodeMethod), EdgeValueCall) ||
|
|
125
|
+
hasEdge(g, nodeID(file, "other", NodeFunction), nodeID(file, "Widget.render", NodeMethod), EdgeValueCall) {
|
|
126
|
+
t.Error("class-expression calls crossed their lexical owners")
|
|
127
|
+
}
|
|
128
|
+
for _, item := range []struct {
|
|
129
|
+
caller, target string
|
|
130
|
+
kind NodeKind
|
|
131
|
+
}{{"localClass", "Widget", NodeVariable}, {"localClass", "Widget.create", NodeMethod}, {"localObject", "api.run", NodeMethod}} {
|
|
132
|
+
if hasEdge(g, nodeID(file, item.caller, NodeFunction), nodeID(file, item.target, item.kind), EdgeValueCall) {
|
|
133
|
+
t.Errorf("%s borrowed unrelated top-level target %s", item.caller, item.target)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
allowed := map[string]bool{"Props.onChange": true, "Props.nested": true, "Props.nested.deep": true, "Props.extra": true, `Props["quoted.key"]`: true, "Props.computed": true}
|
|
137
|
+
for _, node := range g.Nodes {
|
|
138
|
+
if node.Name == "Sale.ordinary" || node.Name == "__class.render" || node.Name == "Internal.render" {
|
|
139
|
+
t.Errorf("invented unowned identity %s", node.Name)
|
|
140
|
+
}
|
|
141
|
+
if (strings.HasPrefix(node.Name, "Props.") || strings.HasPrefix(node.Name, "Props[")) && !allowed[node.Name] {
|
|
142
|
+
t.Errorf("invented type member %s", node.Name)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
package/internal/graph/build.go
CHANGED
|
@@ -187,11 +187,9 @@ func collectDeclarations(g *Graph, file *shimast.SourceFile) {
|
|
|
187
187
|
collectStatements(g, file.FileName(), file.Statements.Nodes)
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// collectStatements records
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// chain, so a node recorded here and an edge target resolved later agree without
|
|
194
|
-
// the walk having to thread the namespace name through.
|
|
190
|
+
// collectStatements records a file or namespace statement list. qualifiedName
|
|
191
|
+
// combines compiler symbol ownership with lexical namespaces for private
|
|
192
|
+
// declarations, keeping indexed nodes and later resolved targets consistent.
|
|
195
193
|
func collectStatements(g *Graph, path string, statements []*shimast.Node) {
|
|
196
194
|
for _, statement := range statements {
|
|
197
195
|
switch statement.Kind {
|
|
@@ -214,6 +212,7 @@ func collectStatements(g *Graph, path string, statements []*shimast.Node) {
|
|
|
214
212
|
collectMembers(g, path, statement)
|
|
215
213
|
case shimast.KindTypeAliasDeclaration:
|
|
216
214
|
addNode(g, path, statement, NodeTypeAlias)
|
|
215
|
+
collectMembers(g, path, statement)
|
|
217
216
|
case shimast.KindEnumDeclaration:
|
|
218
217
|
addNode(g, path, statement, NodeEnum)
|
|
219
218
|
case shimast.KindVariableStatement:
|
|
@@ -242,6 +241,9 @@ func collectVariables(g *Graph, path string, statement *shimast.Node) {
|
|
|
242
241
|
for _, binding := range list.Declarations.Nodes {
|
|
243
242
|
addNode(g, path, binding, NodeVariable)
|
|
244
243
|
g.collectObjectMembers(path, binding)
|
|
244
|
+
if expression := boundClassExpression(binding); expression != nil {
|
|
245
|
+
collectMembers(g, path, expression)
|
|
246
|
+
}
|
|
245
247
|
collectClosures(g, path, binding)
|
|
246
248
|
}
|
|
247
249
|
}
|
|
@@ -533,9 +535,9 @@ func addNode(g *Graph, path string, node *shimast.Node, kind NodeKind) {
|
|
|
533
535
|
putDeclaredNode(g, path, qualifiedName(symbol), kind, node)
|
|
534
536
|
}
|
|
535
537
|
|
|
536
|
-
// collectMembers records callable members
|
|
537
|
-
//
|
|
538
|
-
//
|
|
538
|
+
// collectMembers records callable and property members of modeled type and
|
|
539
|
+
// class declarations. Structural and lexical owner names are shared with
|
|
540
|
+
// checker-resolved targets, including parameter properties and bound literals.
|
|
539
541
|
func collectMembers(g *Graph, path string, statement *shimast.Node) {
|
|
540
542
|
for _, member := range classMembers(statement) {
|
|
541
543
|
name := methodName(member.Symbol())
|
|
@@ -650,20 +652,44 @@ func declarationHasImplementation(declaration *shimast.Node, kind NodeKind) bool
|
|
|
650
652
|
}
|
|
651
653
|
}
|
|
652
654
|
|
|
653
|
-
// classMembers returns
|
|
654
|
-
//
|
|
655
|
+
// classMembers returns direct class/interface members, constructor parameter
|
|
656
|
+
// properties and named nested type-literal members. Type aliases and bound class
|
|
657
|
+
// expressions use the same member surface for indexing and edge attribution.
|
|
655
658
|
func classMembers(statement *shimast.Node) []*shimast.Node {
|
|
659
|
+
var members []*shimast.Node
|
|
656
660
|
switch statement.Kind {
|
|
657
661
|
case shimast.KindClassDeclaration:
|
|
658
662
|
if decl := statement.AsClassDeclaration(); decl != nil && decl.Members != nil {
|
|
659
|
-
|
|
663
|
+
members = decl.Members.Nodes
|
|
664
|
+
}
|
|
665
|
+
case shimast.KindClassExpression:
|
|
666
|
+
if decl := statement.AsClassExpression(); decl != nil && decl.Members != nil {
|
|
667
|
+
members = decl.Members.Nodes
|
|
660
668
|
}
|
|
661
669
|
case shimast.KindInterfaceDeclaration:
|
|
662
670
|
if decl := statement.AsInterfaceDeclaration(); decl != nil && decl.Members != nil {
|
|
663
|
-
|
|
671
|
+
members = decl.Members.Nodes
|
|
672
|
+
}
|
|
673
|
+
case shimast.KindTypeAliasDeclaration:
|
|
674
|
+
return typeLiteralMembers(statement.Type())
|
|
675
|
+
}
|
|
676
|
+
var extra []*shimast.Node
|
|
677
|
+
for _, member := range members {
|
|
678
|
+
if member.Kind == shimast.KindConstructor {
|
|
679
|
+
for _, parameter := range member.Parameters() {
|
|
680
|
+
if isParameterProperty(parameter) {
|
|
681
|
+
extra = append(extra, parameter)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
664
684
|
}
|
|
685
|
+
extra = append(extra, typeLiteralMembers(member.Type())...)
|
|
665
686
|
}
|
|
666
|
-
|
|
687
|
+
if len(extra) == 0 {
|
|
688
|
+
return members
|
|
689
|
+
}
|
|
690
|
+
result := make([]*shimast.Node, 0, len(members)+len(extra))
|
|
691
|
+
result = append(result, members...)
|
|
692
|
+
return append(result, extra...)
|
|
667
693
|
}
|
|
668
694
|
|
|
669
695
|
// isMethodMember reports whether a class/interface member kind is a callable the
|
|
@@ -680,7 +706,7 @@ func isMethodMember(kind shimast.Kind) bool {
|
|
|
680
706
|
|
|
681
707
|
func isPropertyMember(kind shimast.Kind) bool {
|
|
682
708
|
switch kind {
|
|
683
|
-
case shimast.KindPropertyDeclaration, shimast.KindPropertySignature:
|
|
709
|
+
case shimast.KindPropertyDeclaration, shimast.KindPropertySignature, shimast.KindParameter:
|
|
684
710
|
return true
|
|
685
711
|
default:
|
|
686
712
|
return false
|
|
@@ -693,6 +719,9 @@ func isPropertyMember(kind shimast.Kind) bool {
|
|
|
693
719
|
// symbol.Parent is the class/interface symbol, set by the binder for every
|
|
694
720
|
// member.
|
|
695
721
|
func methodName(symbol *shimast.Symbol) string {
|
|
722
|
+
if name, ok := structuralMemberName(symbol); ok {
|
|
723
|
+
return name
|
|
724
|
+
}
|
|
696
725
|
if symbol == nil || symbol.Parent == nil || symbol.Parent.Name == "" {
|
|
697
726
|
return ""
|
|
698
727
|
}
|
|
@@ -763,6 +792,9 @@ func qualifiedName(symbol *shimast.Symbol) string {
|
|
|
763
792
|
return name
|
|
764
793
|
}
|
|
765
794
|
}
|
|
795
|
+
if name, ok := structuralMemberName(symbol); ok {
|
|
796
|
+
return name
|
|
797
|
+
}
|
|
766
798
|
name := simpleName(symbol)
|
|
767
799
|
if prefix := containerPrefix(symbol); prefix != "" {
|
|
768
800
|
return prefix + "." + name
|
|
@@ -776,12 +808,22 @@ func qualifiedName(symbol *shimast.Symbol) string {
|
|
|
776
808
|
// `namespace` block — so a top-level declaration gets no prefix.
|
|
777
809
|
func containerPrefix(symbol *shimast.Symbol) string {
|
|
778
810
|
parent := symbol.Parent
|
|
779
|
-
if parent
|
|
780
|
-
return ""
|
|
781
|
-
}
|
|
782
|
-
if isNamespaceSymbol(parent) || isTypeContainerSymbol(parent) {
|
|
811
|
+
if parent != nil && parent.Name != "" && (isNamespaceSymbol(parent) || isTypeContainerSymbol(parent)) {
|
|
783
812
|
return qualifiedName(parent)
|
|
784
813
|
}
|
|
814
|
+
// The binder does not assign Symbol.Parent to private namespace locals.
|
|
815
|
+
// Their lexical namespace still owns their identity. Stop at a callable so
|
|
816
|
+
// ClosureName remains the owner of function-local qualification.
|
|
817
|
+
for _, declaration := range symbol.Declarations {
|
|
818
|
+
for owner := declaration.Parent; owner != nil; owner = owner.Parent {
|
|
819
|
+
if isFunctionLike(owner) {
|
|
820
|
+
break
|
|
821
|
+
}
|
|
822
|
+
if owner.Kind == shimast.KindModuleDeclaration && owner.Symbol() != nil && isNamespaceSymbol(owner.Symbol()) {
|
|
823
|
+
return qualifiedName(owner.Symbol())
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
785
827
|
return ""
|
|
786
828
|
}
|
|
787
829
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// structuralMemberName replaces anonymous type/class binder containers with
|
|
6
|
+
// the declaration that names them. An unbound literal has no stable owner.
|
|
7
|
+
func structuralMemberName(symbol *shimast.Symbol) (string, bool) {
|
|
8
|
+
if symbol == nil {
|
|
9
|
+
return "", false
|
|
10
|
+
}
|
|
11
|
+
for _, declaration := range symbol.Declarations {
|
|
12
|
+
parent := declaration.Parent
|
|
13
|
+
if parent == nil {
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
var owner string
|
|
17
|
+
switch {
|
|
18
|
+
case parent.Kind == shimast.KindTypeLiteral:
|
|
19
|
+
owner = typeLiteralOwner(parent)
|
|
20
|
+
case parent.Kind == shimast.KindClassExpression:
|
|
21
|
+
owner = classExpressionOwner(parent)
|
|
22
|
+
case isParameterProperty(declaration):
|
|
23
|
+
class := parent.Parent
|
|
24
|
+
if class != nil && class.Kind == shimast.KindClassExpression {
|
|
25
|
+
owner = classExpressionOwner(class)
|
|
26
|
+
} else if class != nil && class.Symbol() != nil {
|
|
27
|
+
owner = qualifiedName(class.Symbol())
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if owner != "" {
|
|
31
|
+
return appendObjectMemberName(owner, simpleName(symbol)), true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return "", false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// typeLiteralOwner follows only type structure with a named declaration owner;
|
|
38
|
+
// a function return or parameter type is not a property of that function.
|
|
39
|
+
func typeLiteralOwner(literal *shimast.Node) string {
|
|
40
|
+
for node := literal.Parent; node != nil; node = node.Parent {
|
|
41
|
+
switch node.Kind {
|
|
42
|
+
case shimast.KindUnionType, shimast.KindIntersectionType, shimast.KindParenthesizedType:
|
|
43
|
+
continue
|
|
44
|
+
case shimast.KindTypeAliasDeclaration, shimast.KindPropertySignature, shimast.KindPropertyDeclaration:
|
|
45
|
+
if node.Symbol() != nil {
|
|
46
|
+
return qualifiedName(node.Symbol())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
return ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// typeLiteralMembers enumerates statically owned literal members once in AST
|
|
55
|
+
// order, including literals nested under named properties and intersections.
|
|
56
|
+
func typeLiteralMembers(typeNode *shimast.Node) []*shimast.Node {
|
|
57
|
+
if typeNode == nil {
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
var members []*shimast.Node
|
|
61
|
+
var walk func(*shimast.Node)
|
|
62
|
+
walk = func(node *shimast.Node) {
|
|
63
|
+
if node.Kind == shimast.KindTypeLiteral {
|
|
64
|
+
literal := node.AsTypeLiteralNode()
|
|
65
|
+
if typeLiteralOwner(node) == "" || literal.Members == nil {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
for _, member := range literal.Members.Nodes {
|
|
69
|
+
if _, ok := staticObjectMemberName(member.Name()); !ok {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
members = append(members, member)
|
|
73
|
+
walkType := member.Type()
|
|
74
|
+
if walkType != nil {
|
|
75
|
+
walk(walkType)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
switch node.Kind {
|
|
81
|
+
case shimast.KindUnionType, shimast.KindIntersectionType, shimast.KindParenthesizedType:
|
|
82
|
+
node.ForEachChild(func(child *shimast.Node) bool { walk(child); return false })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
walk(typeNode)
|
|
86
|
+
return members
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// isParameterProperty admits constructor parameters with a property modifier,
|
|
90
|
+
// leaving ordinary parameters local to their constructor.
|
|
91
|
+
func isParameterProperty(node *shimast.Node) bool {
|
|
92
|
+
return node.Kind == shimast.KindParameter && node.Parent != nil && node.Parent.Kind == shimast.KindConstructor &&
|
|
93
|
+
shimast.GetCombinedModifierFlags(node)&shimast.ModifierFlagsParameterPropertyModifier != 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// boundClassExpression unwraps the transparent expression forms around a
|
|
97
|
+
// variable's class initializer, without guessing classes returned by factories.
|
|
98
|
+
func boundClassExpression(binding *shimast.Node) *shimast.Node {
|
|
99
|
+
variable := binding.AsVariableDeclaration()
|
|
100
|
+
if variable == nil {
|
|
101
|
+
return nil
|
|
102
|
+
}
|
|
103
|
+
node := variable.Initializer
|
|
104
|
+
for node != nil {
|
|
105
|
+
switch node.Kind {
|
|
106
|
+
case shimast.KindClassExpression:
|
|
107
|
+
return node
|
|
108
|
+
case shimast.KindParenthesizedExpression, shimast.KindAsExpression, shimast.KindSatisfiesExpression,
|
|
109
|
+
shimast.KindTypeAssertionExpression, shimast.KindNonNullExpression:
|
|
110
|
+
node = node.Expression()
|
|
111
|
+
default:
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// classExpressionOwner uses the outer binding even for a named expression;
|
|
119
|
+
// its internal class name is private to that expression's body.
|
|
120
|
+
func classExpressionOwner(expression *shimast.Node) string {
|
|
121
|
+
if binding := classExpressionBinding(expression); binding != nil {
|
|
122
|
+
return qualifiedName(binding.Symbol())
|
|
123
|
+
}
|
|
124
|
+
return ""
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// classExpressionBinding preserves the actual declaration behind an owner
|
|
128
|
+
// name, so a shadowing local cannot borrow a same-named indexed value.
|
|
129
|
+
func classExpressionBinding(expression *shimast.Node) *shimast.Node {
|
|
130
|
+
for parent := expression.Parent; parent != nil; parent = parent.Parent {
|
|
131
|
+
switch parent.Kind {
|
|
132
|
+
case shimast.KindParenthesizedExpression, shimast.KindAsExpression, shimast.KindSatisfiesExpression,
|
|
133
|
+
shimast.KindTypeAssertionExpression, shimast.KindNonNullExpression:
|
|
134
|
+
continue
|
|
135
|
+
case shimast.KindVariableDeclaration:
|
|
136
|
+
if parent.Symbol() != nil && boundClassExpression(parent) == expression {
|
|
137
|
+
return parent
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
}
|
package/internal/graph/edges.go
CHANGED
|
@@ -90,6 +90,17 @@ func (g *Graph) collectHeritageIn(checker *shimchecker.Checker, path string, sta
|
|
|
90
90
|
if decl != nil && decl.HeritageClauses != nil {
|
|
91
91
|
g.heritageEdges(checker, path, statement, NodeInterface, decl.HeritageClauses.Nodes)
|
|
92
92
|
}
|
|
93
|
+
case shimast.KindVariableStatement:
|
|
94
|
+
variables := statement.AsVariableStatement().DeclarationList
|
|
95
|
+
if variables != nil && variables.AsVariableDeclarationList().Declarations != nil {
|
|
96
|
+
for _, binding := range variables.AsVariableDeclarationList().Declarations.Nodes {
|
|
97
|
+
if expression := boundClassExpression(binding); expression != nil {
|
|
98
|
+
if clauses := expression.AsClassExpression().HeritageClauses; clauses != nil {
|
|
99
|
+
g.heritageEdges(checker, path, expression, NodeClass, clauses.Nodes)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
93
104
|
case shimast.KindModuleDeclaration:
|
|
94
105
|
g.collectHeritageIn(checker, path, moduleStatements(statement))
|
|
95
106
|
}
|
|
@@ -104,6 +115,13 @@ func (g *Graph) heritageEdges(checker *shimchecker.Checker, path string, node *s
|
|
|
104
115
|
return
|
|
105
116
|
}
|
|
106
117
|
from := nodeID(path, qualifiedName(symbol), kind)
|
|
118
|
+
if node.Kind == shimast.KindClassExpression {
|
|
119
|
+
owner := classExpressionOwner(node)
|
|
120
|
+
if owner == "" {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
from = nodeID(path, owner, NodeVariable)
|
|
124
|
+
}
|
|
107
125
|
for _, clauseNode := range clauses {
|
|
108
126
|
clause := clauseNode.AsHeritageClause()
|
|
109
127
|
if clause == nil || clause.Types == nil {
|
|
@@ -332,12 +350,8 @@ func graphMemberNodeID(g *Graph, member *shimast.Node) string {
|
|
|
332
350
|
if !ok {
|
|
333
351
|
return ""
|
|
334
352
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if declarationFile != nil && declarationFile.FileName() == stored.File &&
|
|
338
|
-
declaration.Pos() == stored.Pos && declaration.End() == stored.End {
|
|
339
|
-
return id
|
|
340
|
-
}
|
|
353
|
+
if nodeDeclaresSymbol(stored, member.Symbol()) {
|
|
354
|
+
return id
|
|
341
355
|
}
|
|
342
356
|
return ""
|
|
343
357
|
}
|
|
@@ -382,6 +396,7 @@ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string,
|
|
|
382
396
|
if id := topLevelID(path, statement, NodeTypeAlias); id != "" {
|
|
383
397
|
fn(id, statement)
|
|
384
398
|
}
|
|
399
|
+
forEachMember(path, statement, NodeTypeAlias, fn)
|
|
385
400
|
case shimast.KindEnumDeclaration:
|
|
386
401
|
// An enum is a recorded node (build.go), and a member initializer can call a
|
|
387
402
|
// function (`enum E { A = base() }`) or reference a type, so walk its body
|
|
@@ -433,6 +448,12 @@ func topLevelID(path string, statement *shimast.Node, kind NodeKind) string {
|
|
|
433
448
|
// lose dependency edges the owner node carries.
|
|
434
449
|
func forEachMember(path string, statement *shimast.Node, kind NodeKind, fn func(string, *shimast.Node)) {
|
|
435
450
|
containerID := topLevelID(path, statement, kind)
|
|
451
|
+
if statement.Kind == shimast.KindClassExpression {
|
|
452
|
+
containerID = ""
|
|
453
|
+
if owner := classExpressionOwner(statement); owner != "" {
|
|
454
|
+
containerID = nodeID(path, owner, NodeVariable)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
436
457
|
for _, member := range classMembers(statement) {
|
|
437
458
|
forEachClosureIn(path, member, fn)
|
|
438
459
|
if isMethodMember(member.Kind) {
|
|
@@ -479,6 +500,10 @@ func heritageClauses(statement *shimast.Node) []*shimast.Node {
|
|
|
479
500
|
if decl := statement.AsClassDeclaration(); decl != nil && decl.HeritageClauses != nil {
|
|
480
501
|
return decl.HeritageClauses.Nodes
|
|
481
502
|
}
|
|
503
|
+
case shimast.KindClassExpression:
|
|
504
|
+
if decl := statement.AsClassExpression(); decl != nil && decl.HeritageClauses != nil {
|
|
505
|
+
return decl.HeritageClauses.Nodes
|
|
506
|
+
}
|
|
482
507
|
case shimast.KindInterfaceDeclaration:
|
|
483
508
|
if decl := statement.AsInterfaceDeclaration(); decl != nil && decl.HeritageClauses != nil {
|
|
484
509
|
return decl.HeritageClauses.Nodes
|
|
@@ -505,6 +530,9 @@ func forEachVariable(path string, statement *shimast.Node, fn func(string, *shim
|
|
|
505
530
|
continue
|
|
506
531
|
}
|
|
507
532
|
fn(nodeID(path, qualifiedName(symbol), NodeVariable), binding)
|
|
533
|
+
if expression := boundClassExpression(binding); expression != nil {
|
|
534
|
+
forEachMember(path, expression, NodeVariable, fn)
|
|
535
|
+
}
|
|
508
536
|
if variable := binding.AsVariableDeclaration(); variable != nil {
|
|
509
537
|
forEachObjectDeclaration(path, variable.Initializer, fn)
|
|
510
538
|
}
|
|
@@ -542,6 +570,11 @@ func closureID(path string, closure *shimast.Node) string {
|
|
|
542
570
|
// value-call edges, and property or element access as value-access edges.
|
|
543
571
|
func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
|
|
544
572
|
node.ForEachChild(func(child *shimast.Node) bool {
|
|
573
|
+
// Computed names and typeof inside a type surface are type dependencies,
|
|
574
|
+
// even when the language-service expression predicate accepts their names.
|
|
575
|
+
if shimast.IsPartOfTypeNode(child) || child.Kind == shimast.KindTypeQuery {
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
545
578
|
// The owning callable keeps aggregate body relationships across closures.
|
|
546
579
|
// forEachClosureIn also records each named closure's own relationships.
|
|
547
580
|
switch child.Kind {
|
|
@@ -557,7 +590,6 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
|
|
|
557
590
|
}
|
|
558
591
|
if call := child.AsCallExpression(); call != nil && call.Expression != nil {
|
|
559
592
|
g.callEdge(checker, from, call.Expression, "call")
|
|
560
|
-
g.handedOffValues(checker, from, call)
|
|
561
593
|
}
|
|
562
594
|
case shimast.KindNewExpression:
|
|
563
595
|
if newExpr := child.AsNewExpression(); newExpr != nil && newExpr.Expression != nil {
|
|
@@ -582,6 +614,12 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
|
|
|
582
614
|
if !isInvokedAccess(child) {
|
|
583
615
|
g.accessEdge(checker, from, child)
|
|
584
616
|
}
|
|
617
|
+
case shimast.KindIdentifier:
|
|
618
|
+
if isValueIdentifier(child) {
|
|
619
|
+
g.accessEdge(checker, from, child)
|
|
620
|
+
}
|
|
621
|
+
case shimast.KindShorthandPropertyAssignment, shimast.KindBindingElement, shimast.KindJsxAttribute:
|
|
622
|
+
g.accessEdge(checker, from, child)
|
|
585
623
|
case shimast.KindBinaryExpression:
|
|
586
624
|
if target := g.assignedFunctionTarget(checker, from, child); target != "" {
|
|
587
625
|
file := g.recordImplementation(target, child)
|
|
@@ -707,29 +745,22 @@ func (g *Graph) callEdge(checker *shimchecker.Checker, from string, callee *shim
|
|
|
707
745
|
g.valueUseEdge(checker, from, callee, EdgeValueCall, origin)
|
|
708
746
|
}
|
|
709
747
|
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// React.memo(ExcalidrawBase)` would reach nothing of the component behind it.
|
|
718
|
-
//
|
|
719
|
-
// A function handed to another function is a use of it, and it is the one the
|
|
720
|
-
// runtime will run. Bounded to argument position: this is the callback, the
|
|
721
|
-
// wrapped component, the registered handler — not every identifier in every
|
|
722
|
-
// expression.
|
|
723
|
-
func (g *Graph) handedOffValues(checker *shimchecker.Checker, from string, call *shimast.CallExpression) {
|
|
724
|
-
if call.Arguments == nil {
|
|
725
|
-
return
|
|
748
|
+
// isValueIdentifier uses the compiler's expression classification and excludes
|
|
749
|
+
// names already represented by their enclosing access, call or JSX expression.
|
|
750
|
+
// Type queries are expressions to the language service but remain type edges here.
|
|
751
|
+
func isValueIdentifier(node *shimast.Node) bool {
|
|
752
|
+
parent := node.Parent
|
|
753
|
+
if parent == nil || !shimast.IsExpressionNode(node) || shimast.IsDeclarationNameOrImportPropertyName(node) || isInvokedAccess(node) {
|
|
754
|
+
return false
|
|
726
755
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
756
|
+
switch parent.Kind {
|
|
757
|
+
case shimast.KindPropertyAccessExpression:
|
|
758
|
+
return parent.AsPropertyAccessExpression().Name() != node
|
|
759
|
+
case shimast.KindJsxOpeningElement, shimast.KindJsxSelfClosingElement, shimast.KindJsxClosingElement,
|
|
760
|
+
shimast.KindTypeQuery, shimast.KindQualifiedName:
|
|
761
|
+
return false
|
|
732
762
|
}
|
|
763
|
+
return true
|
|
733
764
|
}
|
|
734
765
|
|
|
735
766
|
// accessEdge resolves a property or element access to its declaration and
|
|
@@ -746,6 +777,17 @@ func (g *Graph) valueUseEdge(checker *shimchecker.Checker, from string, targetEx
|
|
|
746
777
|
if target == nil || target.Symbol == nil {
|
|
747
778
|
return
|
|
748
779
|
}
|
|
780
|
+
// Enum members remain outline facts on their enum rather than separate nodes.
|
|
781
|
+
// Their reads therefore depend on that enum's existing identity.
|
|
782
|
+
if kind == EdgeValueAccess && target.Symbol.Flags&shimast.SymbolFlagsEnumMember != 0 {
|
|
783
|
+
if parent := target.Symbol.Parent; parent != nil {
|
|
784
|
+
if declaration := declarationNode(parent); declaration != nil {
|
|
785
|
+
if file := declarationFile(parent); file != nil {
|
|
786
|
+
target = &Target{Symbol: parent, File: file.FileName(), External: !IsWorkspaceSourceFile(file), Pos: declaration.Pos(), End: declaration.End()}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
749
791
|
to := g.ensureTargetNode(target)
|
|
750
792
|
if to == "" || to == from {
|
|
751
793
|
return
|
|
@@ -838,6 +880,21 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
838
880
|
if target.File == "" {
|
|
839
881
|
return ""
|
|
840
882
|
}
|
|
883
|
+
if kind == NodeClass && !target.External {
|
|
884
|
+
// A named class expression's internal name denotes the same constructor
|
|
885
|
+
// its variable binds. Keep those self-references on the existing value node.
|
|
886
|
+
for _, declaration := range target.Symbol.Declarations {
|
|
887
|
+
if declaration.Kind != shimast.KindClassExpression {
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
if binding := classExpressionBinding(declaration); binding != nil {
|
|
891
|
+
id := nodeID(target.File, qualifiedName(binding.Symbol()), NodeVariable)
|
|
892
|
+
if stored, exists := g.lookupNode(id); exists && nodeDeclaresSymbol(stored, binding.Symbol()) {
|
|
893
|
+
return id
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
841
898
|
if kind == NodeMethod {
|
|
842
899
|
// A method node is class-qualified and only modeled when it belongs to the
|
|
843
900
|
// workspace (Build recorded it). A call into a dependency's method stops at
|
|
@@ -848,7 +905,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
848
905
|
return ""
|
|
849
906
|
}
|
|
850
907
|
id := nodeID(target.File, name, NodeMethod)
|
|
851
|
-
if
|
|
908
|
+
if stored, exists := g.lookupNode(id); exists && nodeDeclaresSymbol(stored, target.Symbol) {
|
|
852
909
|
return id
|
|
853
910
|
}
|
|
854
911
|
return ""
|
|
@@ -857,17 +914,19 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
857
914
|
return ""
|
|
858
915
|
}
|
|
859
916
|
name := qualifiedName(target.Symbol)
|
|
860
|
-
id := nodeID(target.File, name, kind)
|
|
861
|
-
if _, exists := g.lookupNode(id); exists {
|
|
862
|
-
return id
|
|
863
|
-
}
|
|
864
917
|
if !target.External {
|
|
865
|
-
// A
|
|
866
|
-
// (`baseCreateRenderer.patch`), which the symbol alone does not spell, so
|
|
867
|
-
// resolve it from the declaration the checker pointed at.
|
|
918
|
+
// A scoped callable must resolve before any same-named top-level node.
|
|
868
919
|
if id := g.closureTargetID(target); id != "" {
|
|
869
920
|
return id
|
|
870
921
|
}
|
|
922
|
+
}
|
|
923
|
+
id := nodeID(target.File, name, kind)
|
|
924
|
+
if stored, exists := g.lookupNode(id); exists {
|
|
925
|
+
if nodeDeclaresSymbol(stored, target.Symbol) {
|
|
926
|
+
return id
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if !target.External {
|
|
871
930
|
// Any other workspace target Build did not record is body-scoped and
|
|
872
931
|
// unnameable without collisions (a local of an anonymous callback), so its
|
|
873
932
|
// name would key the same id as a same-named local of another scope and
|
|
@@ -889,6 +948,19 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
889
948
|
return id
|
|
890
949
|
}
|
|
891
950
|
|
|
951
|
+
// nodeDeclaresSymbol verifies the indexed display declaration belongs to the
|
|
952
|
+
// resolved symbol. Names alone cannot distinguish unindexed shadowing locals;
|
|
953
|
+
// overloads and merged symbols may legitimately carry several declarations.
|
|
954
|
+
func nodeDeclaresSymbol(node *Node, symbol *shimast.Symbol) bool {
|
|
955
|
+
for _, declaration := range symbol.Declarations {
|
|
956
|
+
file := shimast.GetSourceFileOfNode(declaration)
|
|
957
|
+
if file != nil && file.FileName() == node.File && declaration.Pos() == node.Pos && declaration.End() == node.End {
|
|
958
|
+
return true
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return false
|
|
962
|
+
}
|
|
963
|
+
|
|
892
964
|
// closureTargetID returns the node id of a closure the checker resolved to, or ""
|
|
893
965
|
// when the target is not one Build recorded.
|
|
894
966
|
func (g *Graph) closureTargetID(target *Target) string {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// Verifies private namespace declarations retain their lexical owners.
|
|
11
|
+
//
|
|
12
|
+
// 1. Load repeated private functions, classes, aliases and variables in two namespaces alongside top-level controls.
|
|
13
|
+
// 2. Build the graph and inspect declaration spans and resolved calls.
|
|
14
|
+
// 3. Require separate identities and reject calls borrowed from another scope.
|
|
15
|
+
//
|
|
16
|
+
// @evidence contracts/testing.md#behavioral-verification Build must retain each lexical declaration and connect only its actual callers and callees, exposing the false union of dependencies caused by a shared private name.
|
|
17
|
+
// @evidence contracts/testing.md#independent-expectations Literal A.hidden, B.hidden and hidden names follow the source's three distinct lexical scopes; only B.hidden calls target. Selected declarations must retain nonempty spans, without an independent byte-offset oracle.
|
|
18
|
+
// @evidence contracts/testing.md#distinguishing-cases Private functions, variables, classes, interfaces, aliases, nested namespaces and named closures contrast exported members and a top-level function of the same name. Moving source offsets is covered by the existing position-invariance tests.
|
|
19
|
+
// @evidence contracts/testing.md#execution-ownership This Go unit owns a temporary project and in-process compiler Program, closes it and calls Build directly without installing or launching a product host.
|
|
20
|
+
func TestNamespacePrivateDeclarationsKeepLexicalIdentity(t *testing.T) {
|
|
21
|
+
t.Setenv(driver.LinkedPluginsEnv, "")
|
|
22
|
+
root := t.TempDir()
|
|
23
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
24
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function target() { return 0; }
|
|
25
|
+
export namespace A {
|
|
26
|
+
function hidden() { return 1; }
|
|
27
|
+
class Local { method() { return hidden(); } }
|
|
28
|
+
interface Shape { value: number }
|
|
29
|
+
type Alias = Shape;
|
|
30
|
+
const local = hidden();
|
|
31
|
+
export const a = hidden();
|
|
32
|
+
export namespace Deep { function hidden() { return 4; } export const d = hidden(); }
|
|
33
|
+
export function factory() { function hidden() { return 5; } return hidden(); }
|
|
34
|
+
}
|
|
35
|
+
export namespace B {
|
|
36
|
+
function hidden() { return target(); }
|
|
37
|
+
class Local { method() { return hidden(); } }
|
|
38
|
+
interface Shape { value: number }
|
|
39
|
+
type Alias = Shape;
|
|
40
|
+
const local = hidden();
|
|
41
|
+
export const b = hidden();
|
|
42
|
+
}
|
|
43
|
+
function hidden() { return 3; }
|
|
44
|
+
export const c = hidden();
|
|
45
|
+
`)
|
|
46
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
defer func() { _ = prog.Close() }()
|
|
51
|
+
if len(diags) != 0 {
|
|
52
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
53
|
+
}
|
|
54
|
+
g := Build(prog)
|
|
55
|
+
file := sourceFile(t, prog, "main.ts").FileName()
|
|
56
|
+
for _, prefix := range []string{"A", "B"} {
|
|
57
|
+
for name, kind := range map[string]NodeKind{
|
|
58
|
+
"hidden": NodeFunction, "Local": NodeClass, "Local.method": NodeMethod,
|
|
59
|
+
"Shape": NodeInterface, "Shape.value": NodeVariable, "Alias": NodeTypeAlias, "local": NodeVariable,
|
|
60
|
+
} {
|
|
61
|
+
if node := g.Nodes[nodeID(file, prefix+"."+name, kind)]; node == nil || node.Name != prefix+"."+name || node.End <= node.Pos {
|
|
62
|
+
t.Errorf("missing lexical declaration %s.%s", prefix, name)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for _, name := range []string{"A.hidden", "B.hidden", "hidden", "A.Deep.hidden", "A.factory.hidden"} {
|
|
67
|
+
id := nodeID(file, name, NodeFunction)
|
|
68
|
+
if g.Nodes[id] == nil {
|
|
69
|
+
t.Errorf("missing %s", name)
|
|
70
|
+
}
|
|
71
|
+
if got := hasEdge(g, id, nodeID(file, "target", NodeFunction), EdgeValueCall); got != (name == "B.hidden") {
|
|
72
|
+
t.Errorf("%s calls target = %t, want %t", name, got, name == "B.hidden")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for caller, target := range map[string]string{"A.a": "A.hidden", "B.b": "B.hidden", "c": "hidden", "A.Deep.d": "A.Deep.hidden"} {
|
|
76
|
+
for _, candidate := range []string{"A.hidden", "B.hidden", "hidden", "A.Deep.hidden", "A.factory.hidden"} {
|
|
77
|
+
if got := hasEdge(g, nodeID(file, caller, NodeVariable), nodeID(file, candidate, NodeFunction), EdgeValueCall); got != (candidate == target) {
|
|
78
|
+
t.Errorf("%s -> %s = %t, want %t", caller, candidate, got, candidate == target)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if !hasEdge(g, nodeID(file, "A.factory", NodeFunction), nodeID(file, "A.factory.hidden", NodeFunction), EdgeValueCall) {
|
|
83
|
+
t.Error("factory lost its own hidden closure")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -67,14 +67,15 @@ func (g *Graph) resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target
|
|
|
67
67
|
return target
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Resolve uses the checker's external
|
|
71
|
-
//
|
|
70
|
+
// Resolve uses the checker's external symbol APIs, including source-value lookup
|
|
71
|
+
// for shorthand properties and contextual property lookup for destructuring and
|
|
72
|
+
// JSX attributes, then unwraps aliases. These APIs return a contextual
|
|
72
73
|
// symbol, not a universal type-checking binding guarantee. A valid checker and
|
|
73
74
|
// parented source-tree ref are required. Nil means no reported symbol; a symbol
|
|
74
75
|
// without a source declaration can instead return a target with empty File.
|
|
75
76
|
//
|
|
76
|
-
// @evidence contracts/common.md#principled-implementation
|
|
77
|
-
// @evidence contracts/common.md#clear-and-simple-design
|
|
77
|
+
// @evidence contracts/common.md#principled-implementation Supported upstream symbol, shorthand-value and contextual-type/property queries supply endpoints for their respective syntax. Alias unwrapping and representative declaration selection prefer workspace bodies without certifying physical ownership or every expression's type-checking binding.
|
|
78
|
+
// @evidence contracts/common.md#clear-and-simple-design Syntax-specific supported checker queries feed shared alias unwrapping and declaration classification in one endpoint adapter.
|
|
78
79
|
// @evidence contracts/common.md#prohibited-implementation-shortcuts Barrel resolution uses the supported checker API rather than guessing export files or rewriting foreign state.
|
|
79
80
|
// @evidence contracts/common.md#meaningful-documentation Native prose states alias behavior, unresolved outcomes and source ownership, with documentation-skill tag spacing.
|
|
80
81
|
// @evidence contracts/performance.md#efficient-algorithms Delegated contextual lookup/alias resolution can perform lazy semantic work beyond the returned symbol. Up to three declaration passes, source ancestry and filename classification add declaration-count/depth/string costs; this adapter itself performs no repository scan.
|
|
@@ -97,7 +98,31 @@ func Resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
|
|
|
97
98
|
return nil
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
symbol
|
|
101
|
+
var symbol *shimast.Symbol
|
|
102
|
+
switch ref.Kind {
|
|
103
|
+
case shimast.KindShorthandPropertyAssignment:
|
|
104
|
+
symbol = checker.GetShorthandAssignmentValueSymbol(ref)
|
|
105
|
+
case shimast.KindBindingElement:
|
|
106
|
+
binding := ref.AsBindingElement()
|
|
107
|
+
if ref.Parent != nil && ref.Parent.Kind == shimast.KindObjectBindingPattern && binding.DotDotDotToken == nil {
|
|
108
|
+
name := binding.PropertyName
|
|
109
|
+
if name == nil {
|
|
110
|
+
name = binding.Name()
|
|
111
|
+
}
|
|
112
|
+
if property, ok := staticObjectMemberName(name); ok {
|
|
113
|
+
symbol = checker.GetPropertyOfType(checker.GetTypeAtLocation(ref.Parent), property)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
case shimast.KindJsxAttribute:
|
|
117
|
+
if property, ok := staticObjectMemberName(ref.Name()); ok && ref.Parent != nil {
|
|
118
|
+
// Zero requests the upstream API's ordinary contextual type, with no flags.
|
|
119
|
+
if contextual := checker.GetContextualType(ref.Parent, 0); contextual != nil {
|
|
120
|
+
symbol = checker.GetPropertyOfType(contextual, property)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
symbol = checker.GetSymbolAtLocation(ref)
|
|
125
|
+
}
|
|
101
126
|
if symbol == nil {
|
|
102
127
|
return nil
|
|
103
128
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// Verifies named value uses survive their expression position.
|
|
11
|
+
//
|
|
12
|
+
// 1. Load isolated readers, destructuring and JSX properties with invocation and type-only controls.
|
|
13
|
+
// 2. Build checker-resolved relations from one TSX Program.
|
|
14
|
+
// 3. Require each actual reader and reject type-only, label and declaration-name dependencies.
|
|
15
|
+
//
|
|
16
|
+
// @evidence contracts/testing.md#behavioral-verification Build must record accesses to num, Props.label and Color from every authored reader, while preserving call edges without duplicate accesses and excluding type-only syntax.
|
|
17
|
+
// @evidence contracts/testing.md#independent-expectations Each literal function body independently determines whether it reads the named module value or property. Expected relationships are authored pairs rather than a snapshot of the extractor's output.
|
|
18
|
+
// @evidence contracts/testing.md#distinguishing-cases Return, arithmetic, condition, array, object, shorthand, template, assignment, anonymous closure, destructured aliases/defaults/parameters, JSX and enum uses contrast declaration names, labels, shadowed locals, type queries and direct calls. Existing tests own ordinary dotted and bracket access, dispatch and external leaves.
|
|
19
|
+
// @evidence contracts/testing.md#execution-ownership This Go unit owns a temporary TSX project, loads and closes its compiler Program and calls Build directly; no native binary, installed consumer or agent query is launched.
|
|
20
|
+
func TestValueReadsCoverExpressionPositions(t *testing.T) {
|
|
21
|
+
t.Setenv(driver.LinkedPluginsEnv, "")
|
|
22
|
+
root := t.TempDir()
|
|
23
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{"compilerOptions":{"strict":true,"target":"ES2022","module":"commonjs","jsx":"preserve"},"include":["src"]}`)
|
|
24
|
+
writeFile(t, filepath.Join(root, "src", "main.tsx"), "declare global { namespace JSX { type Element = unknown; interface IntrinsicElements {} } }\n"+`export const num = 1;
|
|
25
|
+
export enum Color { Red }
|
|
26
|
+
export interface Props { label: number }
|
|
27
|
+
export function Badge(props: Props) { return null; }
|
|
28
|
+
export function asArgument() { return String(num); }
|
|
29
|
+
export function returned() { return num; }
|
|
30
|
+
export function added() { return num + 1; }
|
|
31
|
+
export function tested() { if (num) return 1; return 0; }
|
|
32
|
+
export function inArray() { return [num]; }
|
|
33
|
+
export function inObject() { return { key: num }; }
|
|
34
|
+
export function shorthand() { return { num }; }
|
|
35
|
+
export function inTemplate() { return `+"`${num}`"+`; }
|
|
36
|
+
export function assigned() { const copy = num; return copy; }
|
|
37
|
+
export function closure() { return () => num; }
|
|
38
|
+
export function dotted(props: Props) { return props.label; }
|
|
39
|
+
export function destructured(props: Props) { const { label } = props; return label; }
|
|
40
|
+
export function renamed(props: Props) { const { label: renamed = num } = props; return renamed; }
|
|
41
|
+
export function parameter({ label }: Props) { return label; }
|
|
42
|
+
export function jsxAttribute() { return <Badge label={1} />; }
|
|
43
|
+
export function enumMember() { return Color.Red; }
|
|
44
|
+
export function enumBracket() { return Color["Red"]; }
|
|
45
|
+
export function invoked() { return Badge({ label: 1 }); }
|
|
46
|
+
export function onlyTypes(value: typeof num): Props { return { label: value }; }
|
|
47
|
+
export type OnlyType = { [num]: typeof num };
|
|
48
|
+
export function shadowed() { const num = 2; return num; }
|
|
49
|
+
export function labels() { num: for (;;) { break num; } }
|
|
50
|
+
`)
|
|
51
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatal(err)
|
|
54
|
+
}
|
|
55
|
+
defer func() { _ = prog.Close() }()
|
|
56
|
+
if len(diags) != 0 {
|
|
57
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
58
|
+
}
|
|
59
|
+
g := Build(prog)
|
|
60
|
+
file := sourceFile(t, prog, "main.tsx").FileName()
|
|
61
|
+
for _, reader := range []string{"asArgument", "returned", "added", "tested", "inArray", "inObject", "shorthand", "inTemplate", "assigned", "closure", "renamed"} {
|
|
62
|
+
if !hasEdge(g, nodeID(file, reader, NodeFunction), nodeID(file, "num", NodeVariable), EdgeValueAccess) {
|
|
63
|
+
t.Errorf("%s lost num read", reader)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for _, reader := range []string{"dotted", "destructured", "renamed", "parameter", "jsxAttribute"} {
|
|
67
|
+
if !hasEdge(g, nodeID(file, reader, NodeFunction), nodeID(file, "Props.label", NodeVariable), EdgeValueAccess) {
|
|
68
|
+
t.Errorf("%s lost Props.label read", reader)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for _, reader := range []string{"enumMember", "enumBracket"} {
|
|
72
|
+
if !hasEdge(g, nodeID(file, reader, NodeFunction), nodeID(file, "Color", NodeEnum), EdgeValueAccess) {
|
|
73
|
+
t.Errorf("%s lost enum read", reader)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for _, reader := range []string{"onlyTypes", "shadowed", "labels"} {
|
|
77
|
+
if hasEdge(g, nodeID(file, reader, NodeFunction), nodeID(file, "num", NodeVariable), EdgeValueAccess) {
|
|
78
|
+
t.Errorf("%s invented num read", reader)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if hasEdge(g, nodeID(file, "OnlyType", NodeTypeAlias), nodeID(file, "num", NodeVariable), EdgeValueAccess) {
|
|
82
|
+
t.Error("type-literal computed name invented a runtime read")
|
|
83
|
+
}
|
|
84
|
+
from, to := nodeID(file, "invoked", NodeFunction), nodeID(file, "Badge", NodeFunction)
|
|
85
|
+
if !hasEdge(g, from, to, EdgeValueCall) || hasEdge(g, from, to, EdgeValueAccess) {
|
|
86
|
+
t.Error("direct invocation must retain only its call edge")
|
|
87
|
+
}
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttsc",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
},
|
|
62
62
|
"optionalDependencies": {
|
|
63
63
|
"fsevents": "^2.3.3",
|
|
64
|
-
"@ttsc/linux-x64": "0.33.
|
|
65
|
-
"@ttsc/linux-arm": "0.33.
|
|
66
|
-
"@ttsc/linux-arm64": "0.33.
|
|
67
|
-
"@ttsc/darwin-
|
|
68
|
-
"@ttsc/
|
|
69
|
-
"@ttsc/
|
|
70
|
-
"@ttsc/win32-arm64": "0.33.
|
|
64
|
+
"@ttsc/linux-x64": "0.33.1",
|
|
65
|
+
"@ttsc/linux-arm": "0.33.1",
|
|
66
|
+
"@ttsc/linux-arm64": "0.33.1",
|
|
67
|
+
"@ttsc/darwin-arm64": "0.33.1",
|
|
68
|
+
"@ttsc/win32-x64": "0.33.1",
|
|
69
|
+
"@ttsc/darwin-x64": "0.33.1",
|
|
70
|
+
"@ttsc/win32-arm64": "0.33.1"
|
|
71
71
|
},
|
|
72
72
|
"sideEffects": [
|
|
73
73
|
"./lib/register.js",
|