Skip to content
Draft
39 changes: 28 additions & 11 deletions internal/checker/emitresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1270,29 +1270,46 @@ func (r *EmitResolver) TryJSTypeNodeToTypeNode(emitContext *printer.EmitContext,
return requestNodeBuilder.TryJSTypeNodeToTypeNode(typeNode, enclosingDeclaration, flags, internalFlags, tracker)
}

func (r *EmitResolver) GetBaseDeclarationsForPropertyDeclaration(node *ast.Node) []*ast.Node {
// IsThisPropertyAssignmentDeclarationRedundant reports whether a JS `this.<name> = ...` expando
// assignment should be omitted from declaration emit because the member it would synthesize is
// already provided by an `extends` base type. This mirrors the skip condition in the checker's
// serializePropertySymbol: an inherited member is redundant when it is identical to the assigned
// one (same readonly-ness, optionality and type). Inherited accessors and methods are always
// treated as redundant here, since accessors merge oddly with value assignments (and run via the
// accessor at runtime), and `this`-expando props carry the ReplaceableByMethod contract, so a
// rebind such as `this.method = this.method.bind(this)` must not override the base method.
//
// Only `extends` base types are considered. Members coming from `implements` clauses are not
// inherited, so the class must redeclare them, and they are always emitted.
func (r *EmitResolver) IsThisPropertyAssignmentDeclarationRedundant(node *ast.Node) bool {
if node == nil {
return nil
return false
}

r.checkerMu.Lock()
defer r.checkerMu.Unlock()

s := r.checker.getSymbolOfDeclaration(node)
if s == nil || s.Parent == nil {
return nil
return false
}
parentType := r.checker.getDeclaredTypeOfSymbol(s.Parent)
if parentType == nil {
return nil
return false
}
bases := r.checker.getBaseTypes(parentType)
for _, b := range bases {
baseProp := r.checker.getPropertyOfObjectType(b, s.Name)
if baseProp != nil {
return baseProp.Declarations
// TODO: return base declarations from all base types if any callers actually look at the list
for _, base := range r.checker.getBaseTypes(parentType) {
baseProp := r.checker.getPropertyOfType(base, s.Name)
if baseProp == nil {
continue
}
if baseProp.Flags&(ast.SymbolFlagsAccessor|ast.SymbolFlagsMethod|ast.SymbolFlagsFunction) != 0 {
return true
}
if r.checker.isReadonlySymbol(baseProp) == r.checker.isReadonlySymbol(s) &&
(s.Flags&ast.SymbolFlagsOptional) == (baseProp.Flags&ast.SymbolFlagsOptional) &&
r.checker.isTypeIdenticalTo(r.checker.getTypeOfSymbol(s), r.checker.getTypeOfSymbol(baseProp)) {
return true
}
}
return nil
return false
}
2 changes: 1 addition & 1 deletion internal/printer/emitresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ type EmitResolver interface {
GetEnumMemberValue(node *ast.Node) evaluator.Result
IsLateBound(node *ast.Node) bool
IsOptionalParameter(node *ast.Node) bool
GetBaseDeclarationsForPropertyDeclaration(node *ast.Node) []*ast.Node
IsThisPropertyAssignmentDeclarationRedundant(node *ast.Node) bool

// isolatedDeclarations-specific declaration emit
GetPropertiesOfContainerFunction(node *ast.Node) []*ast.Symbol
Expand Down
5 changes: 2 additions & 3 deletions internal/transformers/declarations/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2066,9 +2066,8 @@ caseBlock:
if thisTarget.ClassLikeData().HeritageClauses != nil && len(thisTarget.ClassLikeData().HeritageClauses.Nodes) > 0 && !isClassExtendingNull(thisTarget) {
// there is a base type any assignments might be "from"
tx.tracker.ReportInferenceFallback(thisTarget) // Add an isolated declarations error on this class - we can't know how to transform this prop into an assignment without referring to type information
decls := tx.resolver.GetBaseDeclarationsForPropertyDeclaration(node)
if len(decls) > 0 {
break caseBlock // property lightly overrides a property in a base type - skip it
if tx.resolver.IsThisPropertyAssignmentDeclarationRedundant(node) {
break caseBlock // skip assignments whose member is already provided by an `extends` base type (an inherited accessor/method, or an identical inherited property)
// TODO: If the property has an explicit `@type` annotation, we should probably emit it (maybe with an `override` modifier) instead of skipping it
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//// [tests/cases/compiler/jsDeclarationEmitThisAssignmentWithBaseProperty.ts] ////

//// [component.d.ts]
export class Component {
state: any;
constructor(props?: any);
}

export class WithAccessor {
get value(): number;
set value(v: number);
}

export class WithMethod {
method(): void;
}

//// [main.js]
import { Component, WithAccessor, WithMethod } from "./component";

export class C1 extends Component {
state = { count: 0 };
}

export class C2 extends Component {
constructor() {
super({});
this.state = { count: 0 };
}
}

export class C3 extends Component {
update() {
this.state = { count: 1 };
}
}

export class C4 extends WithAccessor {
constructor() {
super();
this.value = 1;
}
}

/** @implements {WithAccessor} */
export class C5 {
constructor() {
this.value = 1;
}
}

export class C6 extends WithMethod {
constructor() {
super();
this.method = this.method.bind(this);
}
}

//// [mainTs.ts]
import { Component } from "./component";

export class C1 extends Component {
state = { count: 0 };
}

export class C2 extends Component {
constructor() {
super({});
this.state = { count: 0 };
}
}




//// [main.d.ts]
import { Component, WithAccessor, WithMethod } from "./component";
export declare class C1 extends Component {
state: {
count: number;
};
}
export declare class C2 extends Component {
state: {
count: number;
};
constructor();
}
export declare class C3 extends Component {
update(): void;
}
export declare class C4 extends WithAccessor {
constructor();
}
/** @implements {WithAccessor} */
export declare class C5 implements WithAccessor {
value: number;
constructor();
}
export declare class C6 extends WithMethod {
constructor();
}
//// [mainTs.d.ts]
import { Component } from "./component";
export declare class C1 extends Component {
state: {
count: number;
};
}
export declare class C2 extends Component {
constructor();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//// [tests/cases/compiler/jsDeclarationEmitThisAssignmentWithBaseProperty.ts] ////

=== component.d.ts ===
export class Component {
>Component : Symbol(Component, Decl(component.d.ts, 0, 0))

state: any;
>state : Symbol(Component.state, Decl(component.d.ts, 0, 24))

constructor(props?: any);
>props : Symbol(props, Decl(component.d.ts, 2, 16))
}

export class WithAccessor {
>WithAccessor : Symbol(WithAccessor, Decl(component.d.ts, 3, 1))

get value(): number;
>value : Symbol(WithAccessor.value, Decl(component.d.ts, 5, 27), Decl(component.d.ts, 6, 24))

set value(v: number);
>value : Symbol(WithAccessor.value, Decl(component.d.ts, 5, 27), Decl(component.d.ts, 6, 24))
>v : Symbol(v, Decl(component.d.ts, 7, 14))
}

export class WithMethod {
>WithMethod : Symbol(WithMethod, Decl(component.d.ts, 8, 1))

method(): void;
>method : Symbol(WithMethod.method, Decl(component.d.ts, 10, 25))
}

=== main.js ===
import { Component, WithAccessor, WithMethod } from "./component";
>Component : Symbol(Component, Decl(main.js, 0, 8))
>WithAccessor : Symbol(WithAccessor, Decl(main.js, 0, 19))
>WithMethod : Symbol(WithMethod, Decl(main.js, 0, 33))

export class C1 extends Component {
>C1 : Symbol(C1, Decl(main.js, 0, 66))
>Component : Symbol(Component, Decl(main.js, 0, 8))

state = { count: 0 };
>state : Symbol(C1.state, Decl(main.js, 2, 35))
>count : Symbol(count, Decl(main.js, 3, 13))
}

export class C2 extends Component {
>C2 : Symbol(C2, Decl(main.js, 4, 1))
>Component : Symbol(Component, Decl(main.js, 0, 8))

constructor() {
super({});
>super : Symbol(Component, Decl(component.d.ts, 0, 0))

this.state = { count: 0 };
>this.state : Symbol(C2.state, Decl(main.js, 8, 18))
>this : Symbol(C2, Decl(main.js, 4, 1))
>state : Symbol(C2.state, Decl(main.js, 8, 18))
>count : Symbol(count, Decl(main.js, 9, 22))
}
}

export class C3 extends Component {
>C3 : Symbol(C3, Decl(main.js, 11, 1))
>Component : Symbol(Component, Decl(main.js, 0, 8))

update() {
>update : Symbol(C3.update, Decl(main.js, 13, 35))

this.state = { count: 1 };
>this.state : Symbol(C3.state, Decl(main.js, 14, 14))
>this : Symbol(C3, Decl(main.js, 11, 1))
>state : Symbol(C3.state, Decl(main.js, 14, 14))
>count : Symbol(count, Decl(main.js, 15, 22))
}
}

export class C4 extends WithAccessor {
>C4 : Symbol(C4, Decl(main.js, 17, 1))
>WithAccessor : Symbol(WithAccessor, Decl(main.js, 0, 19))

constructor() {
super();
>super : Symbol(WithAccessor, Decl(component.d.ts, 3, 1))

this.value = 1;
>this.value : Symbol(C4.value, Decl(main.js, 21, 16))
>this : Symbol(C4, Decl(main.js, 17, 1))
>value : Symbol(C4.value, Decl(main.js, 21, 16))
}
}

/** @implements {WithAccessor} */
export class C5 {
>C5 : Symbol(C5, Decl(main.js, 24, 1))

constructor() {
this.value = 1;
>this.value : Symbol(C5.value, Decl(main.js, 28, 19))
>this : Symbol(C5, Decl(main.js, 24, 1))
>value : Symbol(C5.value, Decl(main.js, 28, 19))
}
}

export class C6 extends WithMethod {
>C6 : Symbol(C6, Decl(main.js, 31, 1))
>WithMethod : Symbol(WithMethod, Decl(main.js, 0, 33))

constructor() {
super();
>super : Symbol(WithMethod, Decl(component.d.ts, 8, 1))

this.method = this.method.bind(this);
>this.method : Symbol(C6.method, Decl(main.js, 35, 16))
>this : Symbol(C6, Decl(main.js, 31, 1))
>method : Symbol(C6.method, Decl(main.js, 35, 16))
>this.method.bind : Symbol(CallableFunction.bind, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>this.method : Symbol(C6.method, Decl(main.js, 35, 16))
>this : Symbol(C6, Decl(main.js, 31, 1))
>method : Symbol(C6.method, Decl(main.js, 35, 16))
>bind : Symbol(CallableFunction.bind, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>this : Symbol(C6, Decl(main.js, 31, 1))
}
}

=== mainTs.ts ===
import { Component } from "./component";
>Component : Symbol(Component, Decl(mainTs.ts, 0, 8))

export class C1 extends Component {
>C1 : Symbol(C1, Decl(mainTs.ts, 0, 40))
>Component : Symbol(Component, Decl(mainTs.ts, 0, 8))

state = { count: 0 };
>state : Symbol(C1.state, Decl(mainTs.ts, 2, 35))
>count : Symbol(count, Decl(mainTs.ts, 3, 13))
}

export class C2 extends Component {
>C2 : Symbol(C2, Decl(mainTs.ts, 4, 1))
>Component : Symbol(Component, Decl(mainTs.ts, 0, 8))

constructor() {
super({});
>super : Symbol(Component, Decl(component.d.ts, 0, 0))

this.state = { count: 0 };
>this.state : Symbol(Component.state, Decl(component.d.ts, 0, 24))
>this : Symbol(C2, Decl(mainTs.ts, 4, 1))
>state : Symbol(Component.state, Decl(component.d.ts, 0, 24))
>count : Symbol(count, Decl(mainTs.ts, 9, 22))
}
}

Loading