Skip to content
This repository was archived by the owner on Apr 30, 2025. It is now read-only.

Commit e286698

Browse files
committed
Fix adding duplicate fields when they are comming from an extended class
1 parent 27925f8 commit e286698

File tree

2 files changed

+115
-66
lines changed

2 files changed

+115
-66
lines changed

generator.go

Lines changed: 72 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -462,70 +462,8 @@ func (g *generator) genInterface(name string, v cue.Value) []ts.Decl {
462462
return nil
463463
}
464464

465-
// Create an empty value, onto which we'll unify fields that need not be
466-
// generated as literals.
467-
nolit := v.Context().CompileString("{...}")
468-
469-
var extends []ts.Expr
470-
var some bool
471-
472-
// Recursively walk down Values returned from Expr() and separate
473-
// unified/embedded structs from a struct literal, so that we can make the
474-
// former (if they are also marked with @cuetsy(kind="interface")) show up
475-
// as "extends" instead of writing out their fields directly.
476-
var walkExpr func(wv cue.Value) error
477-
walkExpr = func(wv cue.Value) error {
478-
op, dvals := wv.Expr()
479-
switch op {
480-
case cue.NoOp:
481-
// Simple path - when the field is a plain struct literal decl, the walk function
482-
// will take this branch and return immediately.
483-
484-
// FIXME this does the struct literal path correctly, but it also
485-
// catches this case, for some reason:
486-
//
487-
// Thing: {
488-
// other.Thing
489-
// }
490-
//
491-
// The saner form - `Thing: other.Thing` - does not go through this path.
492-
return nil
493-
case cue.OrOp:
494-
return valError(wv, "typescript interfaces cannot be constructed from disjunctions")
495-
case cue.SelectorOp:
496-
expr, err := refAsInterface(wv)
497-
if err != nil {
498-
return err
499-
}
500-
501-
// If we have a string to add to the list of "extends", then also
502-
// add the ref to the list of fields to exclude if subsumed.
503-
if expr != nil {
504-
some = true
505-
extends = append(extends, expr)
506-
nolit = nolit.Unify(cue.Dereference(wv))
507-
}
508-
return nil
509-
case cue.AndOp:
510-
// First, search the dvals for StructLits. Having more than one is possible,
511-
// but weird, as writing >1 literal and unifying them is the same as just writing
512-
// one containing the unified result - more complicated with no obvious benefit.
513-
for _, dv := range dvals {
514-
if dv.IncompleteKind() != cue.StructKind && dv.IncompleteKind() != cue.TopKind {
515-
panic("impossible? seems like it should be. if this pops, clearly not!")
516-
}
517-
518-
if err := walkExpr(dv); err != nil {
519-
return err
520-
}
521-
}
522-
return nil
523-
default:
524-
panic(fmt.Sprintf("unhandled op type %s", op.String()))
525-
}
526-
}
527-
528-
if err := walkExpr(v); err != nil {
465+
extends, nolit, err := findExtends(v)
466+
if err != nil {
529467
g.addErr(err)
530468
return nil
531469
}
@@ -559,15 +497,15 @@ func (g *generator) genInterface(name string, v cue.Value) []ts.Decl {
559497
//
560498
// There's _probably_ a way around this, especially when we move to an
561499
// AST rather than dumb string templates. But i'm tired of looking.
562-
if some {
500+
if len(extends) > 0 {
563501
// Look up the path of the current field within the nolit value,
564502
// then check it for subsumption.
565503
sel := iter.Selector()
566504
if iter.IsOptional() {
567505
sel = sel.Optional()
568506
}
569-
sub := nolit.LookupPath(cue.MakePath(sel))
570507

508+
sub := nolit.LookupPath(cue.MakePath(sel))
571509
// Theoretically, lattice equality can be defined as bijective
572510
// subsumption. In practice, Subsume() seems to ignore optional
573511
// fields, and Equals() doesn't. So, use Equals().
@@ -636,6 +574,74 @@ func (g *generator) genInterface(name string, v cue.Value) []ts.Decl {
636574
return ret
637575
}
638576

577+
// Recursively walk down Values returned from Expr() and separate
578+
// unified/embedded structs from a struct literal, so that we can make the
579+
// former (if they are also marked with @cuetsy(kind="interface")) show up
580+
// as "extends" instead of writing out their fields directly.
581+
func findExtends(v cue.Value) ([]ts.Expr, cue.Value, error) {
582+
var extends []ts.Expr
583+
// Create an empty value, onto which we'll unify fields that need not be
584+
// generated as literals.
585+
baseNolit := v.Context().CompileString("")
586+
nolit := v.Context().CompileString("")
587+
var walkExpr func(v cue.Value) error
588+
walkExpr = func(v cue.Value) error {
589+
op, dvals := v.Expr()
590+
switch op {
591+
case cue.NoOp:
592+
// Simple path - when the field is a plain struct literal decl, the walk function
593+
// will take this branch and return immediately.
594+
595+
// FIXME this does the struct literal path correctly, but it also
596+
// catches this case, for some reason:
597+
//
598+
// Thing: {
599+
// other.Thing
600+
// }
601+
//
602+
// The saner form - `Thing: other.Thing` - does not go through this path.
603+
return nil
604+
case cue.OrOp:
605+
return valError(v, "typescript interfaces cannot be constructed from disjunctions")
606+
case cue.SelectorOp:
607+
expr, err := refAsInterface(v)
608+
if err != nil {
609+
return err
610+
}
611+
612+
// If we have a string to add to the list of "extends", then also
613+
// add the ref to the list of fields to exclude if subsumed.
614+
if expr != nil {
615+
extends = append(extends, expr)
616+
nolit = baseNolit.Unify(nolit.Unify(cue.Dereference(v)))
617+
}
618+
return nil
619+
case cue.AndOp:
620+
// First, search the dvals for StructLits. Having more than one is possible,
621+
// but weird, as writing >1 literal and unifying them is the same as just writing
622+
// one containing the unified result - more complicated with no obvious benefit.
623+
for _, dv := range dvals {
624+
if dv.IncompleteKind() != cue.StructKind && dv.IncompleteKind() != cue.TopKind {
625+
panic("impossible? seems like it should be. if this pops, clearly not!")
626+
}
627+
628+
if err := walkExpr(dv); err != nil {
629+
return err
630+
}
631+
}
632+
return nil
633+
default:
634+
panic(fmt.Sprintf("unhandled op type %s", op.String()))
635+
}
636+
}
637+
638+
if err := walkExpr(v); err != nil {
639+
return nil, nolit, err
640+
}
641+
642+
return extends, nolit, nil
643+
}
644+
639645
// Generate a typeRef for the cue.Value
640646
func (g *generator) genInterfaceField(v cue.Value) (*typeRef, error) {
641647
tref := &typeRef{}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
-- cue.mod/module.cue --
2+
module: "example.com"
3+
4+
-- one.cue --
5+
package test
6+
7+
import "example.com/dep"
8+
9+
#Extended: {
10+
dep.#Base
11+
#AStruct
12+
#BStruct
13+
field: string
14+
} @cuetsy(kind="interface")
15+
16+
#AStruct: {
17+
anotherField: string
18+
} @cuetsy(kind="interface")
19+
20+
#BStruct: {
21+
moreField: string
22+
} @cuetsy(kind="interface")
23+
24+
-- dep/file.cue --
25+
package dep
26+
27+
#Base: {
28+
baseField: string
29+
} @cuetsy(kind="interface")
30+
31+
-- out/gen --
32+
33+
export interface Extended extends dep.Base, AStruct, BStruct {
34+
field: string;
35+
}
36+
37+
export interface AStruct {
38+
anotherField: string;
39+
}
40+
41+
export interface BStruct {
42+
moreField: string;
43+
}

0 commit comments

Comments
 (0)