Skip to content

Commit

Permalink
cherry pick "No this type arguments in base constraints (#54536)" t…
Browse files Browse the repository at this point in the history
…o release-5.1 (#54814)

Co-authored-by: Anders Hejlsberg <andersh@microsoft.com>
  • Loading branch information
jakebailey and ahejlsberg committed Jun 28, 2023
1 parent e7f1caf commit ac5884a
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 27 deletions.
34 changes: 15 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12499,10 +12499,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (getObjectFlags(type) & ObjectFlags.Reference) {
const target = (type as TypeReference).target;
const typeArguments = getTypeArguments(type as TypeReference);
if (length(target.typeParameters) === length(typeArguments)) {
const ref = createTypeReference(target, concatenate(typeArguments, [thisArgument || target.thisType!]));
return needApparentType ? getApparentType(ref) : ref;
}
return length(target.typeParameters) === length(typeArguments) ? createTypeReference(target, concatenate(typeArguments, [thisArgument || target.thisType!])) : type;
}
else if (type.flags & TypeFlags.Intersection) {
const types = sameMap((type as IntersectionType).types, t => getTypeWithThisArgument(t, thisArgument, needApparentType));
Expand All @@ -12511,10 +12508,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return needApparentType ? getApparentType(type) : type;
}

function getThisArgument(type: Type) {
return getObjectFlags(type) & ObjectFlags.Reference && length(getTypeArguments(type as TypeReference)) > getTypeReferenceArity(type as TypeReference) ? last(getTypeArguments(type as TypeReference)) : type;
}

function resolveObjectTypeMembers(type: ObjectType, source: InterfaceTypeWithDeclaredMembers, typeParameters: readonly TypeParameter[], typeArguments: readonly Type[]) {
let mapper: TypeMapper | undefined;
let members: SymbolTable;
Expand Down Expand Up @@ -13696,7 +13689,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type.resolvedBaseConstraint;
}
const stack: object[] = [];
return type.resolvedBaseConstraint = getTypeWithThisArgument(getImmediateBaseConstraint(type), getThisArgument(type));
return type.resolvedBaseConstraint = getImmediateBaseConstraint(type);

function getImmediateBaseConstraint(t: Type): Type {
if (!t.immediateBaseConstraint) {
Expand Down Expand Up @@ -13802,17 +13795,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// We substitute constraints for variadic elements only when the constraints are array types or
// non-variadic tuple types as we want to avoid further (possibly unbounded) recursion.
const newElements = map(getElementTypes(t), (v, i) => {
const constraint = t.target.elementFlags[i] & ElementFlags.Variadic && getBaseConstraint(v) || v;
return constraint && everyType(constraint, c => isArrayOrTupleType(c) && !isGenericTupleType(c)) ? constraint : v;
const constraint = v.flags & TypeFlags.TypeParameter && t.target.elementFlags[i] & ElementFlags.Variadic && getBaseConstraint(v) || v;
return constraint !== v && everyType(constraint, c => isArrayOrTupleType(c) && !isGenericTupleType(c)) ? constraint : v;
});
return createTupleType(newElements, t.target.elementFlags, t.target.readonly, t.target.labeledElementDeclarations);
}
return t;
}
}

function getApparentTypeOfIntersectionType(type: IntersectionType) {
return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, type, /*needApparentType*/ true));
function getApparentTypeOfIntersectionType(type: IntersectionType, thisArgument: Type) {
return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, thisArgument, /*needApparentType*/ true));
}

function getResolvedTypeParameterDefault(typeParameter: TypeParameter): Type | undefined {
Expand Down Expand Up @@ -13890,9 +13883,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* type itself.
*/
function getApparentType(type: Type): Type {
const t = !(type.flags & TypeFlags.Instantiable) ? type : getBaseConstraintOfType(type) || unknownType;
return getObjectFlags(t) & ObjectFlags.Mapped ? getApparentTypeOfMappedType(t as MappedType) :
t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t as IntersectionType) :
const t = type.flags & TypeFlags.Instantiable ? getBaseConstraintOfType(type) || unknownType : type;
const objectFlags = getObjectFlags(t);
return objectFlags & ObjectFlags.Mapped ? getApparentTypeOfMappedType(t as MappedType) :
objectFlags & ObjectFlags.Reference && t !== type ? getTypeWithThisArgument(t, type) :
t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t as IntersectionType, type) :
t.flags & TypeFlags.StringLike ? globalStringType :
t.flags & TypeFlags.NumberLike ? globalNumberType :
t.flags & TypeFlags.BigIntLike ? getGlobalBigIntType() :
Expand Down Expand Up @@ -21570,9 +21565,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
const c = target as ConditionalType;
// We check for a relationship to a conditional type target only when the conditional type has no
// 'infer' positions and is not distributive or is distributive but doesn't reference the check type
// parameter in either of the result types.
if (!c.root.inferTypeParameters && !isDistributionDependent(c.root)) {
// 'infer' positions, is not distributive or is distributive but doesn't reference the check type
// parameter in either of the result types, and the source isn't an instantiation of the same
// conditional type (as happens when computing variance).
if (!c.root.inferTypeParameters && !isDistributionDependent(c.root) && !(source.flags & TypeFlags.Conditional && (source as ConditionalType).root === c.root)) {
// Check if the conditional is always true or always false but still deferred for distribution purposes.
const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType));
const skipFalse = !skipTrue && isTypeAssignableTo(getRestrictiveInstantiation(c.checkType), getRestrictiveInstantiation(c.extendsType));
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/complexRecursiveCollections.errors.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
tests/cases/compiler/immutable.ts(341,22): error TS2430: Interface 'Keyed<K, V>' incorrectly extends interface 'Collection<K, V>'.
The types returned by 'toSeq()' are incompatible between these types.
Type 'Keyed<K, V>' is not assignable to type 'this'.
'this' could be instantiated with an arbitrary type which could be unrelated to 'Keyed<K, V>'.
'Keyed<K, V>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Keyed<K, V>'.
tests/cases/compiler/immutable.ts(359,22): error TS2430: Interface 'Indexed<T>' incorrectly extends interface 'Collection<number, T>'.
The types returned by 'toSeq()' are incompatible between these types.
Type 'Indexed<T>' is not assignable to type 'this'.
'this' could be instantiated with an arbitrary type which could be unrelated to 'Indexed<T>'.
'Indexed<T>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Indexed<T>'.
tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' incorrectly extends interface 'Collection<never, T>'.
The types returned by 'toSeq()' are incompatible between these types.
Type 'Set<T>' is not assignable to type 'this'.
'this' could be instantiated with an arbitrary type which could be unrelated to 'Set<T>'.
'Set<T>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Set<T>'.


==== tests/cases/compiler/complex.ts (0 errors) ====
Expand Down Expand Up @@ -379,7 +379,7 @@ tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' inco
!!! error TS2430: Interface 'Keyed<K, V>' incorrectly extends interface 'Collection<K, V>'.
!!! error TS2430: The types returned by 'toSeq()' are incompatible between these types.
!!! error TS2430: Type 'Keyed<K, V>' is not assignable to type 'this'.
!!! error TS2430: 'this' could be instantiated with an arbitrary type which could be unrelated to 'Keyed<K, V>'.
!!! error TS2430: 'Keyed<K, V>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Keyed<K, V>'.
toJS(): Object;
toJSON(): { [key: string]: V };
toSeq(): Seq.Keyed<K, V>;
Expand All @@ -402,7 +402,7 @@ tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' inco
!!! error TS2430: Interface 'Indexed<T>' incorrectly extends interface 'Collection<number, T>'.
!!! error TS2430: The types returned by 'toSeq()' are incompatible between these types.
!!! error TS2430: Type 'Indexed<T>' is not assignable to type 'this'.
!!! error TS2430: 'this' could be instantiated with an arbitrary type which could be unrelated to 'Indexed<T>'.
!!! error TS2430: 'Indexed<T>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Indexed<T>'.
toJS(): Array<any>;
toJSON(): Array<T>;
// Reading values
Expand Down Expand Up @@ -439,7 +439,7 @@ tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' inco
!!! error TS2430: Interface 'Set<T>' incorrectly extends interface 'Collection<never, T>'.
!!! error TS2430: The types returned by 'toSeq()' are incompatible between these types.
!!! error TS2430: Type 'Set<T>' is not assignable to type 'this'.
!!! error TS2430: 'this' could be instantiated with an arbitrary type which could be unrelated to 'Set<T>'.
!!! error TS2430: 'Set<T>' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Set<T>'.
toJS(): Array<any>;
toJSON(): Array<T>;
toSeq(): Seq.Set<T>;
Expand Down
118 changes: 118 additions & 0 deletions tests/baselines/reference/largeTupleTypes.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
=== tests/cases/compiler/largeTupleTypes.ts ===
// Repro from #54491

type UnshiftTuple<T extends [...any[]]> = T extends [T[0], ...infer Tail] ? Tail : never;
>UnshiftTuple : Symbol(UnshiftTuple, Decl(largeTupleTypes.ts, 0, 0))
>T : Symbol(T, Decl(largeTupleTypes.ts, 2, 18))
>T : Symbol(T, Decl(largeTupleTypes.ts, 2, 18))
>T : Symbol(T, Decl(largeTupleTypes.ts, 2, 18))
>Tail : Symbol(Tail, Decl(largeTupleTypes.ts, 2, 67))
>Tail : Symbol(Tail, Decl(largeTupleTypes.ts, 2, 67))

type ExpandSmallerTuples<T extends [...any[]]> = T extends [T[0], ...infer Tail] ? T | ExpandSmallerTuples<Tail> : [];
>ExpandSmallerTuples : Symbol(ExpandSmallerTuples, Decl(largeTupleTypes.ts, 2, 89))
>T : Symbol(T, Decl(largeTupleTypes.ts, 3, 25))
>T : Symbol(T, Decl(largeTupleTypes.ts, 3, 25))
>T : Symbol(T, Decl(largeTupleTypes.ts, 3, 25))
>Tail : Symbol(Tail, Decl(largeTupleTypes.ts, 3, 74))
>T : Symbol(T, Decl(largeTupleTypes.ts, 3, 25))
>ExpandSmallerTuples : Symbol(ExpandSmallerTuples, Decl(largeTupleTypes.ts, 2, 89))
>Tail : Symbol(Tail, Decl(largeTupleTypes.ts, 3, 74))

type Shift<A extends Array<any>> = ((...args: A) => void) extends (...args: [A[0], ...infer R]) => void ? R : never;
>Shift : Symbol(Shift, Decl(largeTupleTypes.ts, 3, 118))
>A : Symbol(A, Decl(largeTupleTypes.ts, 4, 11))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>args : Symbol(args, Decl(largeTupleTypes.ts, 4, 37))
>A : Symbol(A, Decl(largeTupleTypes.ts, 4, 11))
>args : Symbol(args, Decl(largeTupleTypes.ts, 4, 67))
>A : Symbol(A, Decl(largeTupleTypes.ts, 4, 11))
>R : Symbol(R, Decl(largeTupleTypes.ts, 4, 91))
>R : Symbol(R, Decl(largeTupleTypes.ts, 4, 91))

type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : GrowExpRev<[...A, ...P[0]][N] extends undefined ? [...A, ...P[0]] : A, N, Shift<P>>;
>GrowExpRev : Symbol(GrowExpRev, Decl(largeTupleTypes.ts, 4, 116))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>N : Symbol(N, Decl(largeTupleTypes.ts, 5, 37))
>P : Symbol(P, Decl(largeTupleTypes.ts, 5, 55))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>N : Symbol(N, Decl(largeTupleTypes.ts, 5, 37))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>GrowExpRev : Symbol(GrowExpRev, Decl(largeTupleTypes.ts, 4, 116))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>P : Symbol(P, Decl(largeTupleTypes.ts, 5, 55))
>N : Symbol(N, Decl(largeTupleTypes.ts, 5, 37))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>P : Symbol(P, Decl(largeTupleTypes.ts, 5, 55))
>A : Symbol(A, Decl(largeTupleTypes.ts, 5, 16))
>N : Symbol(N, Decl(largeTupleTypes.ts, 5, 37))
>Shift : Symbol(Shift, Decl(largeTupleTypes.ts, 3, 118))
>P : Symbol(P, Decl(largeTupleTypes.ts, 5, 55))

type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>;
>GrowExp : Symbol(GrowExp, Decl(largeTupleTypes.ts, 5, 199))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>N : Symbol(N, Decl(largeTupleTypes.ts, 6, 34))
>P : Symbol(P, Decl(largeTupleTypes.ts, 6, 52))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>N : Symbol(N, Decl(largeTupleTypes.ts, 6, 34))
>GrowExp : Symbol(GrowExp, Decl(largeTupleTypes.ts, 5, 199))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>N : Symbol(N, Decl(largeTupleTypes.ts, 6, 34))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>P : Symbol(P, Decl(largeTupleTypes.ts, 6, 52))
>GrowExpRev : Symbol(GrowExpRev, Decl(largeTupleTypes.ts, 4, 116))
>A : Symbol(A, Decl(largeTupleTypes.ts, 6, 13))
>N : Symbol(N, Decl(largeTupleTypes.ts, 6, 34))
>P : Symbol(P, Decl(largeTupleTypes.ts, 6, 52))

type Tuple<T, N extends number> = number extends N ? Array<T> : N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T], N, [[]]>;
>Tuple : Symbol(Tuple, Decl(largeTupleTypes.ts, 6, 178))
>T : Symbol(T, Decl(largeTupleTypes.ts, 7, 11))
>N : Symbol(N, Decl(largeTupleTypes.ts, 7, 13))
>N : Symbol(N, Decl(largeTupleTypes.ts, 7, 13))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(largeTupleTypes.ts, 7, 11))
>N : Symbol(N, Decl(largeTupleTypes.ts, 7, 13))
>N : Symbol(N, Decl(largeTupleTypes.ts, 7, 13))
>T : Symbol(T, Decl(largeTupleTypes.ts, 7, 11))
>GrowExp : Symbol(GrowExp, Decl(largeTupleTypes.ts, 5, 199))
>T : Symbol(T, Decl(largeTupleTypes.ts, 7, 11))
>N : Symbol(N, Decl(largeTupleTypes.ts, 7, 13))

declare class ArrayValidator<T extends unknown[], I = T[number]> {
>ArrayValidator : Symbol(ArrayValidator, Decl(largeTupleTypes.ts, 7, 125))
>T : Symbol(T, Decl(largeTupleTypes.ts, 9, 29))
>I : Symbol(I, Decl(largeTupleTypes.ts, 9, 49))
>T : Symbol(T, Decl(largeTupleTypes.ts, 9, 29))

lengthRange<S extends number, E extends number>(start: S, endBefore: E): ArrayValidator<Exclude<ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, E>]>>, ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, S>]>>>>;
>lengthRange : Symbol(ArrayValidator.lengthRange, Decl(largeTupleTypes.ts, 9, 66))
>S : Symbol(S, Decl(largeTupleTypes.ts, 10, 16))
>E : Symbol(E, Decl(largeTupleTypes.ts, 10, 33))
>start : Symbol(start, Decl(largeTupleTypes.ts, 10, 52))
>S : Symbol(S, Decl(largeTupleTypes.ts, 10, 16))
>endBefore : Symbol(endBefore, Decl(largeTupleTypes.ts, 10, 61))
>E : Symbol(E, Decl(largeTupleTypes.ts, 10, 33))
>ArrayValidator : Symbol(ArrayValidator, Decl(largeTupleTypes.ts, 7, 125))
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
>ExpandSmallerTuples : Symbol(ExpandSmallerTuples, Decl(largeTupleTypes.ts, 2, 89))
>UnshiftTuple : Symbol(UnshiftTuple, Decl(largeTupleTypes.ts, 0, 0))
>Tuple : Symbol(Tuple, Decl(largeTupleTypes.ts, 6, 178))
>I : Symbol(I, Decl(largeTupleTypes.ts, 9, 49))
>E : Symbol(E, Decl(largeTupleTypes.ts, 10, 33))
>ExpandSmallerTuples : Symbol(ExpandSmallerTuples, Decl(largeTupleTypes.ts, 2, 89))
>UnshiftTuple : Symbol(UnshiftTuple, Decl(largeTupleTypes.ts, 0, 0))
>Tuple : Symbol(Tuple, Decl(largeTupleTypes.ts, 6, 178))
>I : Symbol(I, Decl(largeTupleTypes.ts, 9, 49))
>S : Symbol(S, Decl(largeTupleTypes.ts, 10, 16))
}

32 changes: 32 additions & 0 deletions tests/baselines/reference/largeTupleTypes.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
=== tests/cases/compiler/largeTupleTypes.ts ===
// Repro from #54491

type UnshiftTuple<T extends [...any[]]> = T extends [T[0], ...infer Tail] ? Tail : never;
>UnshiftTuple : UnshiftTuple<T>

type ExpandSmallerTuples<T extends [...any[]]> = T extends [T[0], ...infer Tail] ? T | ExpandSmallerTuples<Tail> : [];
>ExpandSmallerTuples : ExpandSmallerTuples<T>

type Shift<A extends Array<any>> = ((...args: A) => void) extends (...args: [A[0], ...infer R]) => void ? R : never;
>Shift : Shift<A>
>args : A
>args : [A[0], ...R]

type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : GrowExpRev<[...A, ...P[0]][N] extends undefined ? [...A, ...P[0]] : A, N, Shift<P>>;
>GrowExpRev : GrowExpRev<A, N, P>

type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>;
>GrowExp : GrowExp<A, N, P>

type Tuple<T, N extends number> = number extends N ? Array<T> : N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T], N, [[]]>;
>Tuple : Tuple<T, N>

declare class ArrayValidator<T extends unknown[], I = T[number]> {
>ArrayValidator : ArrayValidator<T, I>

lengthRange<S extends number, E extends number>(start: S, endBefore: E): ArrayValidator<Exclude<ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, E>]>>, ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, S>]>>>>;
>lengthRange : <S extends number, E extends number>(start: S, endBefore: E) => ArrayValidator<Exclude<ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, E>]>>, ExpandSmallerTuples<UnshiftTuple<[...Tuple<I, S>]>>>>
>start : S
>endBefore : E
}

4 changes: 2 additions & 2 deletions tests/baselines/reference/mergedDeclarations7.errors.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
tests/cases/compiler/test.ts(4,5): error TS2322: Type 'PassportStatic' is not assignable to type 'Passport'.
The types returned by 'use()' are incompatible between these types.
Type 'PassportStatic' is not assignable to type 'this'.
'this' could be instantiated with an arbitrary type which could be unrelated to 'PassportStatic'.
'PassportStatic' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Passport'.


==== tests/cases/compiler/passport.d.ts (0 errors) ====
Expand Down Expand Up @@ -29,4 +29,4 @@ tests/cases/compiler/test.ts(4,5): error TS2322: Type 'PassportStatic' is not as
!!! error TS2322: Type 'PassportStatic' is not assignable to type 'Passport'.
!!! error TS2322: The types returned by 'use()' are incompatible between these types.
!!! error TS2322: Type 'PassportStatic' is not assignable to type 'this'.
!!! error TS2322: 'this' could be instantiated with an arbitrary type which could be unrelated to 'PassportStatic'.
!!! error TS2322: 'PassportStatic' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Passport'.

0 comments on commit ac5884a

Please sign in to comment.