From 7049af5f4f872aed60a20a7276951c395ca212a1 Mon Sep 17 00:00:00 2001 From: "Oleksandr T." Date: Tue, 13 Aug 2024 01:42:09 +0300 Subject: [PATCH] fix(58166): Class parameter property with initializer before required property emits non-nullable parameter for declaration emit (#58177) --- src/compiler/checker.ts | 21 ++++++++------ src/compiler/expressionToTypeNode.ts | 2 +- src/compiler/transformers/declarations.ts | 2 +- .../transformers/declarations/diagnostics.ts | 2 +- src/compiler/types.ts | 6 ++-- .../fixMissingTypeAnnotationOnExports.ts | 5 ++-- .../parameterPropertyInConstructor4.js | 28 +++++++++++++++++++ .../parameterPropertyInConstructor4.symbols | 12 ++++++++ .../parameterPropertyInConstructor4.types | 17 +++++++++++ .../parameterPropertyInConstructor4.ts | 7 +++++ 10 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 tests/baselines/reference/parameterPropertyInConstructor4.js create mode 100644 tests/baselines/reference/parameterPropertyInConstructor4.symbols create mode 100644 tests/baselines/reference/parameterPropertyInConstructor4.types create mode 100644 tests/cases/compiler/parameterPropertyInConstructor4.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4b8d44e62fc..ea28b278026 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8316,7 +8316,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { * @param symbol - The symbol is used both to find an existing annotation if declaration is not provided, and to determine if `unique symbol` should be printed */ function serializeTypeForDeclaration(context: NodeBuilderContext, declaration: Declaration | undefined, type: Type, symbol: Symbol) { - const addUndefinedForParameter = declaration && (isParameter(declaration) || isJSDocParameterTag(declaration)) && requiresAddingImplicitUndefined(declaration); + const addUndefinedForParameter = declaration && (isParameter(declaration) || isJSDocParameterTag(declaration)) && requiresAddingImplicitUndefined(declaration, context.enclosingDeclaration); const enclosingDeclaration = context.enclosingDeclaration; const restoreFlags = saveRestoreFlags(context); if (declaration && hasInferredType(declaration) && !(context.internalFlags & InternalNodeBuilderFlags.NoSyntacticPrinter)) { @@ -49591,16 +49591,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const type = getTypeFromTypeNode(typeNode); return containsUndefinedType(type); } - function requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag) { - return (isRequiredInitializedParameter(parameter) || isOptionalUninitializedParameterProperty(parameter)) && !declaredParameterTypeContainsUndefined(parameter); + + function requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag, enclosingDeclaration: Node | undefined) { + return (isRequiredInitializedParameter(parameter, enclosingDeclaration) || isOptionalUninitializedParameterProperty(parameter)) && !declaredParameterTypeContainsUndefined(parameter); } - function isRequiredInitializedParameter(parameter: ParameterDeclaration | JSDocParameterTag): boolean { - return !!strictNullChecks && - !isOptionalParameter(parameter) && - !isJSDocParameterTag(parameter) && - !!parameter.initializer && - !hasSyntacticModifier(parameter, ModifierFlags.ParameterPropertyModifier); + function isRequiredInitializedParameter(parameter: ParameterDeclaration | JSDocParameterTag, enclosingDeclaration: Node | undefined): boolean { + if (!strictNullChecks || isOptionalParameter(parameter) || isJSDocParameterTag(parameter) || !parameter.initializer) { + return false; + } + if (hasSyntacticModifier(parameter, ModifierFlags.ParameterPropertyModifier)) { + return !!enclosingDeclaration && isFunctionLikeDeclaration(enclosingDeclaration); + } + return true; } function isOptionalUninitializedParameterProperty(parameter: ParameterDeclaration | JSDocParameterTag) { diff --git a/src/compiler/expressionToTypeNode.ts b/src/compiler/expressionToTypeNode.ts index 48f8434137b..047611a5d45 100644 --- a/src/compiler/expressionToTypeNode.ts +++ b/src/compiler/expressionToTypeNode.ts @@ -178,7 +178,7 @@ export function createSyntacticTypeNodeBuilder(options: CompilerOptions, resolve return typeFromAccessor(parent, context); } const declaredType = getEffectiveTypeAnnotationNode(node); - const addUndefined = resolver.requiresAddingImplicitUndefined(node); + const addUndefined = resolver.requiresAddingImplicitUndefined(node, context.enclosingDeclaration); let resultType; if (declaredType) { resultType = serializeExistingTypeAnnotation(declaredType, addUndefined); diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 536e0cf5d9c..b12c3acf84c 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -680,7 +680,7 @@ export function transformDeclarations(context: TransformationContext) { // Literal const declarations will have an initializer ensured rather than a type return; } - const shouldAddImplicitUndefined = node.kind === SyntaxKind.Parameter && resolver.requiresAddingImplicitUndefined(node); + const shouldAddImplicitUndefined = node.kind === SyntaxKind.Parameter && resolver.requiresAddingImplicitUndefined(node, enclosingDeclaration); if (type && !shouldAddImplicitUndefined) { return visitNode(type, visitDeclarationSubtree, isTypeNode); } diff --git a/src/compiler/transformers/declarations/diagnostics.ts b/src/compiler/transformers/declarations/diagnostics.ts index 22bb998ae60..d7a6c54f5ea 100644 --- a/src/compiler/transformers/declarations/diagnostics.ts +++ b/src/compiler/transformers/declarations/diagnostics.ts @@ -767,7 +767,7 @@ export function createGetIsolatedDeclarationErrors(resolver: EmitResolver) { if (isSetAccessor(node.parent)) { return createAccessorTypeError(node.parent); } - const addUndefined = resolver.requiresAddingImplicitUndefined(node); + const addUndefined = resolver.requiresAddingImplicitUndefined(node, /*enclosingDeclaration*/ undefined); if (!addUndefined && node.initializer) { return createExpressionError(node.initializer); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fb3022c5500..4f75397d59f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5293,7 +5293,7 @@ export interface TypeChecker { /** @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken, nodesToCheck?: Node[]): Diagnostic[]; /** @internal */ getGlobalDiagnostics(): Diagnostic[]; /** @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken, forceDts?: boolean): EmitResolver; - /** @internal */ requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag): boolean; + /** @internal */ requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag, enclosingDeclaration: Node | undefined): boolean; /** @internal */ getNodeCount(): number; /** @internal */ getIdentifierCount(): number; @@ -5814,7 +5814,7 @@ export interface EmitResolver { collectLinkedAliases(node: ModuleExportName, setVisibility?: boolean): Node[] | undefined; markLinkedReferences(node: Node): void; isImplementationOfOverload(node: SignatureDeclaration): boolean | undefined; - requiresAddingImplicitUndefined(node: ParameterDeclaration): boolean; + requiresAddingImplicitUndefined(node: ParameterDeclaration, enclosingDeclaration: Node | undefined): boolean; isExpandoFunctionDeclaration(node: FunctionDeclaration | VariableDeclaration): boolean; getPropertiesOfContainerFunction(node: Declaration): Symbol[]; createTypeOfDeclaration(declaration: AccessorDeclaration | VariableLikeDeclaration | PropertyAccessExpression | ElementAccessExpression | BinaryExpression, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; @@ -10378,6 +10378,6 @@ export interface SyntacticTypeNodeBuilderResolver { isExpandoFunctionDeclaration(name: FunctionDeclaration | VariableDeclaration): boolean; getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations; isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node, shouldComputeAliasToMakeVisible?: boolean): SymbolVisibilityResult; - requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag): boolean; + requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag, enclosingDeclaration: Node | undefined): boolean; isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean; } diff --git a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts index 0b845570782..4d961dfa979 100644 --- a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts +++ b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts @@ -914,11 +914,12 @@ function withContext( type = widenedType; } - if (isParameter(node) && typeChecker.requiresAddingImplicitUndefined(node)) { + const enclosingDeclaration = findAncestor(node, isDeclaration) ?? sourceFile; + if (isParameter(node) && typeChecker.requiresAddingImplicitUndefined(node, enclosingDeclaration)) { type = typeChecker.getUnionType([typeChecker.getUndefinedType(), type], UnionReduction.None); } return { - typeNode: typeToTypeNode(type, findAncestor(node, isDeclaration) ?? sourceFile, getFlags(type)), + typeNode: typeToTypeNode(type, enclosingDeclaration, getFlags(type)), mutatedTarget: false, }; diff --git a/tests/baselines/reference/parameterPropertyInConstructor4.js b/tests/baselines/reference/parameterPropertyInConstructor4.js new file mode 100644 index 00000000000..d4183c73937 --- /dev/null +++ b/tests/baselines/reference/parameterPropertyInConstructor4.js @@ -0,0 +1,28 @@ +//// [tests/cases/compiler/parameterPropertyInConstructor4.ts] //// + +//// [parameterPropertyInConstructor4.ts] +export class C { + constructor(public a: number[] = [], b: number) { + } +} + + +//// [parameterPropertyInConstructor4.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.C = void 0; +var C = /** @class */ (function () { + function C(a, b) { + if (a === void 0) { a = []; } + this.a = a; + } + return C; +}()); +exports.C = C; + + +//// [parameterPropertyInConstructor4.d.ts] +export declare class C { + a: number[]; + constructor(a: number[] | undefined, b: number); +} diff --git a/tests/baselines/reference/parameterPropertyInConstructor4.symbols b/tests/baselines/reference/parameterPropertyInConstructor4.symbols new file mode 100644 index 00000000000..b0517c309a2 --- /dev/null +++ b/tests/baselines/reference/parameterPropertyInConstructor4.symbols @@ -0,0 +1,12 @@ +//// [tests/cases/compiler/parameterPropertyInConstructor4.ts] //// + +=== parameterPropertyInConstructor4.ts === +export class C { +>C : Symbol(C, Decl(parameterPropertyInConstructor4.ts, 0, 0)) + + constructor(public a: number[] = [], b: number) { +>a : Symbol(C.a, Decl(parameterPropertyInConstructor4.ts, 1, 16)) +>b : Symbol(b, Decl(parameterPropertyInConstructor4.ts, 1, 40)) + } +} + diff --git a/tests/baselines/reference/parameterPropertyInConstructor4.types b/tests/baselines/reference/parameterPropertyInConstructor4.types new file mode 100644 index 00000000000..d193fc8d29e --- /dev/null +++ b/tests/baselines/reference/parameterPropertyInConstructor4.types @@ -0,0 +1,17 @@ +//// [tests/cases/compiler/parameterPropertyInConstructor4.ts] //// + +=== parameterPropertyInConstructor4.ts === +export class C { +>C : C +> : ^ + + constructor(public a: number[] = [], b: number) { +>a : number[] +> : ^^^^^^^^ +>[] : never[] +> : ^^^^^^^ +>b : number +> : ^^^^^^ + } +} + diff --git a/tests/cases/compiler/parameterPropertyInConstructor4.ts b/tests/cases/compiler/parameterPropertyInConstructor4.ts new file mode 100644 index 00000000000..8787f7ecf42 --- /dev/null +++ b/tests/cases/compiler/parameterPropertyInConstructor4.ts @@ -0,0 +1,7 @@ +// @declaration: true +// @strict: true + +export class C { + constructor(public a: number[] = [], b: number) { + } +}