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.
@@ -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
+ }
@@ -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 the nodes for a statement list — the file's top
191
- // level, or the body of a namespace it recurses into. A member's id is built
192
- // from its symbol, which already carries the enclosing namespace in its parent
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 (method, constructor, accessor) and
537
- // property members of a class or interface declaration, keyed by their
538
- // class-qualified names so resolved member references land on the same node.
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 the member nodes of a class or interface declaration, or
654
- // nil for anything else.
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
- return decl.Members.Nodes
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
- return decl.Members.Nodes
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
- return nil
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 == nil || parent.Name == "" {
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
+ }
@@ -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
- for _, declaration := range member.Symbol().Declarations {
336
- declarationFile := shimast.GetSourceFileOfNode(declaration)
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
- // handedOffValues records the workspace functions a call is handed, as uses of
711
- // them.
712
- //
713
- // `React.memo(ExcalidrawBase)`, `forwardRef(Impl)`, `app.use(handler)`,
714
- // `pipe(map(project))` — the argument is not called here and is not a property
715
- // read, so neither the call nor the access collector would record it, and the
716
- // wrapper would be a dead end: `export const Excalidraw =
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
- for _, argument := range call.Arguments.Nodes {
728
- switch argument.Kind {
729
- case shimast.KindIdentifier, shimast.KindPropertyAccessExpression:
730
- g.valueUseEdge(checker, from, argument, EdgeValueAccess, "")
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 _, exists := g.lookupNode(id); exists {
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 closure Build recorded is keyed by its scoped name
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 GetSymbolAtLocation API, then requests an
71
- // aliased symbol when one is reported. That API deliberately returns a contextual
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 The supported external symbol API and alias API supply a contextual endpoint; representative declaration selection prefers non-declaration-file bodies without certifying physical ownership or every expression's type-checking binding.
77
- // @evidence contracts/common.md#clear-and-simple-design Symbol lookup, alias unwrapping and declaration classification form one endpoint adapter shared by graph relation passes.
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 := checker.GetSymbolAtLocation(ref)
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.0",
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.0",
65
- "@ttsc/linux-arm": "0.33.0",
66
- "@ttsc/linux-arm64": "0.33.0",
67
- "@ttsc/darwin-x64": "0.33.0",
68
- "@ttsc/darwin-arm64": "0.33.0",
69
- "@ttsc/win32-x64": "0.33.0",
70
- "@ttsc/win32-arm64": "0.33.0"
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",