@@ -778,6 +778,221 @@ class TestComponent {
778778 `TestComponent.html(1, 14): Property 'a' is private and only accessible within class 'Model'.` ,
779779 ] ) ;
780780 } ) ;
781+
782+ it ( 'disallows access to private members of a parent class while allowing own private and parent protected members' , ( ) => {
783+ const messages = diagnose (
784+ `<button (click)="parentMethod(); childMethod(); parentSetter = 'a'">{{ parentProp }} {{ parentParam }} {{ parentGetter }} {{ childProp }} {{ childParam }} {{ parentProtected }}</button>` ,
785+ `
786+ export class Parent {
787+ private parentProp = 'parent';
788+ private get parentGetter(): string { return 'getter'; }
789+ private set parentSetter(v: string) {}
790+ protected parentProtected = 'protected';
791+ constructor(private parentParam: string) {}
792+ private parentMethod(): void {}
793+ }
794+
795+ export class TestComponent extends Parent {
796+ private childProp = 'child';
797+ constructor(private childParam: string) {
798+ super(childParam);
799+ }
800+ private childMethod(): void {}
801+ }
802+ ` ,
803+ ) ;
804+
805+ expect ( messages ) . toEqual ( [
806+ `TestComponent.html(1, 18): Property 'parentMethod' is private and only accessible within class 'Parent'.` ,
807+ `TestComponent.html(1, 49): Property 'parentSetter' is private and only accessible within class 'Parent'.` ,
808+ `TestComponent.html(1, 72): Property 'parentProp' is private and only accessible within class 'Parent'.` ,
809+ `TestComponent.html(1, 89): Property 'parentParam' is private and only accessible within class 'Parent'.` ,
810+ `TestComponent.html(1, 107): Property 'parentGetter' is private and only accessible within class 'Parent'.` ,
811+ ] ) ;
812+ } ) ;
813+
814+ it ( 'disallows access to private members across multi-level inheritance' , ( ) => {
815+ const messages = diagnose (
816+ `{{ grandParentPrivate }} {{ parentPrivate }} {{ childPrivate }} {{ grandParentProtected }}` ,
817+ `
818+ export class GrandParent {
819+ private grandParentPrivate = 1;
820+ protected grandParentProtected = 2;
821+ }
822+
823+ export class Parent extends GrandParent {
824+ private parentPrivate = 3;
825+ }
826+
827+ export class TestComponent extends Parent {
828+ private childPrivate = 4;
829+ }
830+ ` ,
831+ ) ;
832+
833+ expect ( messages ) . toEqual ( [
834+ `TestComponent.html(1, 4): Property 'grandParentPrivate' is private and only accessible within class 'GrandParent'.` ,
835+ `TestComponent.html(1, 29): Property 'parentPrivate' is private and only accessible within class 'Parent'.` ,
836+ ] ) ;
837+ } ) ;
838+
839+ it ( 'handles private members on generic parent and generic child classes' , ( ) => {
840+ const messages = diagnose (
841+ `{{ parentPrivate }} {{ childPrivate }} {{ parentProtected }}` ,
842+ `
843+ export class Parent<T> {
844+ private parentPrivate!: T;
845+ protected parentProtected!: T;
846+ }
847+
848+ export class TestComponent<T> extends Parent<T> {
849+ private childPrivate!: T;
850+ }
851+ ` ,
852+ ) ;
853+
854+ expect ( messages ) . toEqual ( [
855+ `TestComponent.html(1, 4): Property 'parentPrivate' is private and only accessible within class 'Parent<T>'.` ,
856+ ] ) ;
857+ } ) ;
858+
859+ it ( 'disallows access to private members from a mixin base class' , ( ) => {
860+ const messages = diagnose (
861+ `{{ basePrivate }} {{ mixinPrivate }} {{ ownPrivate }}` ,
862+ `
863+ type Constructor<T = {}> = new (...args: any[]) => T;
864+
865+ export class Base {
866+ private basePrivate = 'base';
867+ }
868+
869+ function WithMixin<TBase extends Constructor>(Ctor: TBase) {
870+ return class MixinClass extends Ctor {
871+ private mixinPrivate = 'mixin';
872+ };
873+ }
874+
875+ export class TestComponent extends WithMixin(Base) {
876+ private ownPrivate = 'own';
877+ }
878+ ` ,
879+ ) ;
880+
881+ expect ( messages ) . toEqual ( [
882+ `TestComponent.html(1, 4): Property 'basePrivate' is private and only accessible within class 'Base'.` ,
883+ `TestComponent.html(1, 22): Property 'mixinPrivate' is private and only accessible within class 'MixinClass'.` ,
884+ ] ) ;
885+ } ) ;
886+
887+ it ( 'handles getter and setter with divergent visibility on parent and child classes' , ( ) => {
888+ const messages = diagnose (
889+ `<button (click)="parentProp = 'a'; ownProp = 'b'">{{ parentProp }} {{ ownProp }}</button>` ,
890+ `
891+ export class Parent {
892+ get parentProp(): string { return ''; }
893+ private set parentProp(v: string) {}
894+ }
895+
896+ export class TestComponent extends Parent {
897+ get ownProp(): string { return ''; }
898+ private set ownProp(v: string) {}
899+ }
900+ ` ,
901+ ) ;
902+
903+ expect ( messages ) . toEqual ( [
904+ `TestComponent.html(1, 18): Property 'parentProp' is private and only accessible within class 'Parent'.` ,
905+ ] ) ;
906+ } ) ;
907+
908+ it ( 'handles optional chaining, non-null assertion, and parentheses on explicit this for private members' , ( ) => {
909+ const messages = diagnose (
910+ `{{ this.own }} {{ this?.own }} {{ this!.own }} {{ (this).own }} {{ this.parent }} {{ this?.parent }} {{ this!.parent }} {{ (this).parent }}` ,
911+ `
912+ export class Parent {
913+ private parent = 'parent';
914+ }
915+
916+ export class TestComponent extends Parent {
917+ private own = 'own';
918+ }
919+ ` ,
920+ ) ;
921+
922+ expect ( messages ) . toEqual ( [
923+ `TestComponent.html(1, 73): Property 'parent' is private and only accessible within class 'Parent'.` ,
924+ `TestComponent.html(1, 92): Property 'parent' is private and only accessible within class 'Parent'.` ,
925+ `TestComponent.html(1, 111): Property 'parent' is private and only accessible within class 'Parent'.` ,
926+ `TestComponent.html(1, 131): Property 'parent' is private and only accessible within class 'Parent'.` ,
927+ ] ) ;
928+ } ) ;
929+
930+ it ( 'handles type narrowing of this with private members' , ( ) => {
931+ const messages = diagnose (
932+ `@if (isSub()) { {{ subPrivate }} {{ ownPrivate }} } @if (hasExtra()) { {{ extra }} {{ ownPrivate }} } @if (ownNullable !== null) { {{ ownNullable.value }} }` ,
933+ `
934+ export class TestComponent {
935+ private ownPrivate = 'own';
936+ private ownNullable: {value: string} | null = null;
937+
938+ isSub(): this is SubComponent {
939+ return true;
940+ }
941+
942+ hasExtra(): this is { extra: string } {
943+ return true;
944+ }
945+ }
946+
947+ export class SubComponent extends TestComponent {
948+ private subPrivate = 'sub';
949+ }
950+ ` ,
951+ ) ;
952+
953+ expect ( messages ) . toEqual ( [
954+ `TestComponent.html(1, 20): Property 'subPrivate' is private and only accessible within class 'SubComponent'.` ,
955+ ] ) ;
956+ } ) ;
957+
958+ it ( 'allows access to own private members when component class merges with an interface' , ( ) => {
959+ const messages = diagnose (
960+ `{{ ownPrivate }} {{ parentPrivate }}` ,
961+ `
962+ export class Parent {
963+ private parentPrivate = 'parent';
964+ }
965+
966+ export interface TestComponent {
967+ extraProp: string;
968+ }
969+
970+ export class TestComponent extends Parent {
971+ private ownPrivate = 'own';
972+ }
973+ ` ,
974+ ) ;
975+
976+ expect ( messages ) . toEqual ( [
977+ `TestComponent.html(1, 21): Property 'parentPrivate' is private and only accessible within class 'Parent'.` ,
978+ ] ) ;
979+ } ) ;
980+
981+ it ( 'disallows access to private members on another instance of the same component class' , ( ) => {
982+ const messages = diagnose (
983+ `{{ other.secret }}` ,
984+ `
985+ export class TestComponent {
986+ private secret = 'shh';
987+ other!: TestComponent;
988+ }
989+ ` ,
990+ ) ;
991+
992+ expect ( messages ) . toEqual ( [
993+ `TestComponent.html(1, 10): Property 'secret' is private and only accessible within class 'TestComponent'.` ,
994+ ] ) ;
995+ } ) ;
781996 } ) ;
782997
783998 describe ( 'method call spans' , ( ) => {
0 commit comments