-
Notifications
You must be signed in to change notification settings - Fork 13k
Fix unassignable properties by adding undefined with exactOptionalPropertyTypes #45032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
664d749
d1008eb
671096b
a728852
715f235
cfea6e1
d058191
334ca65
41f0525
09c916f
cdd40d3
f018b8e
960e657
cdbe969
22b9e7a
e9607f1
2c1982f
3ffe9db
3e70798
4b7b3fe
d535509
e027d1e
cc51d23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -633,6 +633,8 @@ namespace ts { | |
isTupleType, | ||
isArrayLikeType, | ||
isTypeInvalidDueToUnionDiscriminant, | ||
getExactOptionalUnassignableProperties, | ||
isExactOptionalPropertyMismatch, | ||
getAllPossiblePropertiesOfTypes, | ||
getSuggestedSymbolForNonexistentProperty, | ||
getSuggestionForNonexistentProperty, | ||
|
@@ -16753,24 +16755,29 @@ namespace ts { | |
let sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType); | ||
if (!sourcePropType) continue; | ||
const propName = getPropertyNameFromIndex(nameType, /*accessNode*/ undefined); | ||
const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & SymbolFlags.Optional); | ||
const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & SymbolFlags.Optional); | ||
targetPropType = removeMissingType(targetPropType, targetIsOptional); | ||
sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional); | ||
if (!checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) { | ||
const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); | ||
if (elaborated) { | ||
reportedError = true; | ||
} | ||
else { | ||
reportedError = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. driveby fix; reportedError was always set to true in this block, regardless of whether |
||
if (!elaborated) { | ||
// Issue error on the prop itself, since the prop couldn't elaborate the error | ||
const resultObj: { errors?: Diagnostic[] } = errorOutputContainer || {}; | ||
// Use the expression type, if available | ||
const specificSource = next ? checkExpressionForMutableLocationWithContextualType(next, sourcePropType) : sourcePropType; | ||
const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); | ||
if (result && specificSource !== sourcePropType) { | ||
// If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType | ||
checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); | ||
if (exactOptionalPropertyTypes && isExactOptionalPropertyMismatch(specificSource, targetPropType)) { | ||
const diag = createDiagnosticForNode(prop, Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, typeToString(specificSource), typeToString(targetPropType)); | ||
diagnostics.add(diag); | ||
resultObj.errors = [diag]; | ||
} | ||
else { | ||
const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & SymbolFlags.Optional); | ||
const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & SymbolFlags.Optional); | ||
targetPropType = removeMissingType(targetPropType, targetIsOptional); | ||
sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional); | ||
const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); | ||
if (result && specificSource !== sourcePropType) { | ||
// If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType | ||
checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); | ||
} | ||
} | ||
if (resultObj.errors) { | ||
const reportedDiag = resultObj.errors[resultObj.errors.length - 1]; | ||
|
@@ -16798,7 +16805,6 @@ namespace ts { | |
} | ||
} | ||
} | ||
reportedError = true; | ||
} | ||
} | ||
} | ||
|
@@ -17664,10 +17670,18 @@ namespace ts { | |
else if (sourceType === targetType) { | ||
message = Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated; | ||
} | ||
else if (exactOptionalPropertyTypes && getExactOptionalUnassignableProperties(source, target).length) { | ||
message = Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; | ||
} | ||
else { | ||
message = Diagnostics.Type_0_is_not_assignable_to_type_1; | ||
} | ||
} | ||
else if (message === Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1 | ||
&& exactOptionalPropertyTypes | ||
&& getExactOptionalUnassignableProperties(source, target).length) { | ||
message = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; | ||
} | ||
|
||
reportError(message, generalizedSourceType, targetType); | ||
} | ||
|
@@ -19583,6 +19597,15 @@ namespace ts { | |
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral); | ||
} | ||
|
||
function getExactOptionalUnassignableProperties(source: Type, target: Type) { | ||
return getPropertiesOfType(target) | ||
.filter(targetProp => isExactOptionalPropertyMismatch(getTypeOfPropertyOfType(source, targetProp.escapedName), getTypeOfSymbol(targetProp))); | ||
} | ||
|
||
function isExactOptionalPropertyMismatch(source: Type | undefined, target: Type | undefined) { | ||
return !!source && !!target && maybeTypeOfKind(source, TypeFlags.Undefined) && !!containsMissingType(target); | ||
andrewbranch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) { | ||
return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) || | ||
findMatchingTypeReferenceOrTypeAliasReference(source, target) || | ||
|
@@ -32444,8 +32467,16 @@ namespace ts { | |
Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, | ||
Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) | ||
&& (!isIdentifier(left) || unescapeLeadingUnderscores(left.escapedText) !== "exports")) { | ||
|
||
let headMessage: DiagnosticMessage | undefined; | ||
if (exactOptionalPropertyTypes && isPropertyAccessExpression(left) && maybeTypeOfKind(valueType, TypeFlags.Undefined)) { | ||
const target = getTypeOfPropertyOfType(getTypeOfExpression(left.expression), left.name.escapedText); | ||
if (isExactOptionalPropertyMismatch(valueType, target)) { | ||
headMessage = Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target; | ||
} | ||
} | ||
// to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported | ||
checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right); | ||
checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right, headMessage); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* @internal */ | ||
namespace ts.codefix { | ||
const addOptionalPropertyUndefined = "addOptionalPropertyUndefined"; | ||
|
||
const errorCodes = [ | ||
Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target.code, | ||
Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties.code, | ||
Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties.code, | ||
]; | ||
|
||
registerCodeFix({ | ||
errorCodes, | ||
getCodeActions(context) { | ||
const typeChecker = context.program.getTypeChecker(); | ||
const toAdd = getPropertiesToAdd(context.sourceFile, context.span.start, typeChecker); | ||
if (!toAdd.length) { | ||
return undefined; | ||
} | ||
const changes = textChanges.ChangeTracker.with(context, t => addUndefinedToOptionalProperty(t, toAdd)); | ||
return [createCodeFixAction(addOptionalPropertyUndefined, changes, Diagnostics.Add_undefined_to_optional_property_type, addOptionalPropertyUndefined, Diagnostics.Add_undefined_to_all_optional_properties)]; | ||
}, | ||
fixIds: [addOptionalPropertyUndefined], | ||
getAllCodeActions: context => { | ||
const { program } = context; | ||
const checker = program.getTypeChecker(); | ||
const seen = new Map<number, true>(); | ||
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { | ||
eachDiagnostic(context, errorCodes, diag => { | ||
const toAdd = getPropertiesToAdd(diag.file, diag.start, checker); | ||
if (!toAdd.length) { | ||
return; | ||
} | ||
let untouched = true; | ||
for (const add of toAdd) { | ||
if (!addToSeen(seen, getSymbolId(add))) { | ||
untouched = false; | ||
} | ||
} | ||
if (untouched) { | ||
addUndefinedToOptionalProperty(changes, toAdd); | ||
} | ||
}); | ||
})); | ||
}, | ||
}); | ||
|
||
function getPropertiesToAdd(file: SourceFile, pos: number, checker: TypeChecker): Symbol[] { | ||
const sourceTarget = getSourceTarget(getErrorNode(file, pos), checker); | ||
if (!sourceTarget) { | ||
return emptyArray; | ||
} | ||
const { source: sourceNode, target: targetNode } = sourceTarget; | ||
const target = checker.getTypeAtLocation(targetNode); | ||
const source = checker.getTypeAtLocation(sourceNode); | ||
if (target.symbol?.declarations?.some(d => getSourceFileOfNode(d).fileName.match(/\.d\.ts$/))) { | ||
return emptyArray; | ||
} | ||
const targetPropertyType = getTargetPropertyType(checker, targetNode); | ||
if (targetPropertyType && checker.isExactOptionalPropertyMismatch(source, targetPropertyType)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe find parent type and fix all, like the other cases |
||
const s = checker.getSymbolAtLocation((targetNode as PropertyAccessExpression).name); | ||
return s ? [s] : emptyArray; | ||
} | ||
return checker.getExactOptionalUnassignableProperties(source, target); | ||
} | ||
|
||
function getTargetPropertyType(checker: TypeChecker, targetNode: Node) { | ||
if (isPropertySignature(targetNode)) { | ||
return checker.getTypeAtLocation(targetNode.name); | ||
} | ||
else if (isPropertyAccessExpression(targetNode)) { | ||
return checker.getTypeOfPropertyOfType(checker.getTypeAtLocation(targetNode.expression), targetNode.name.text); | ||
} | ||
return undefined; | ||
} | ||
/** | ||
* Get the part of the incorrect assignment that is useful for type-checking | ||
* eg | ||
* this.definite = 1; ---> `this.definite` | ||
* ^^^^ | ||
* definite = source ----> `definite` | ||
* ^^^^^^^^ | ||
*/ | ||
function getErrorNode(file: SourceFile, pos: number): MemberName | PropertyAccessExpression | undefined { | ||
const start = getTokenAtPosition(file, pos); | ||
return isPropertyAccessExpression(start.parent) && start.parent.expression === start ? start.parent | ||
: isIdentifier(start) || isPrivateIdentifier(start) ? start | ||
: undefined; | ||
} | ||
|
||
/** | ||
* Find the source and target of the incorrect assignment. | ||
* The call is recursive for property assignments. | ||
*/ | ||
function getSourceTarget(errorNode: Node | undefined, checker: TypeChecker): { source: Node, target: Node } | undefined { | ||
if (!errorNode) { | ||
return undefined; | ||
} | ||
else if (isBinaryExpression(errorNode.parent) && errorNode.parent.operatorToken.kind === SyntaxKind.EqualsToken) { | ||
return { source: errorNode.parent.right, target: errorNode.parent.left }; | ||
} | ||
else if (isVariableDeclaration(errorNode.parent) && errorNode.parent.initializer) { | ||
return { source: errorNode.parent.initializer, target: errorNode.parent.name }; | ||
} | ||
else if (isCallExpression(errorNode.parent)) { | ||
const n = checker.getSymbolAtLocation(errorNode.parent.expression); | ||
if (!n?.valueDeclaration) return undefined; | ||
if (!isExpression(errorNode)) return undefined; | ||
const i = errorNode.parent.arguments.indexOf(errorNode); | ||
const name = (n.valueDeclaration as any as SignatureDeclaration).parameters[i].name; | ||
if (isIdentifier(name)) return { source: errorNode, target: name }; | ||
} | ||
else if (isPropertyAssignment(errorNode.parent) && isIdentifier(errorNode.parent.name) || | ||
isShorthandPropertyAssignment(errorNode.parent)) { | ||
const parentTarget = getSourceTarget(errorNode.parent.parent, checker); | ||
if (!parentTarget) return undefined; | ||
const prop = checker.getPropertyOfType(checker.getTypeAtLocation(parentTarget.target), (errorNode.parent.name as Identifier).text); | ||
const declaration = prop?.declarations?.[0]; | ||
if (!declaration) return undefined; | ||
return { | ||
source: isPropertyAssignment(errorNode.parent) ? errorNode.parent.initializer : errorNode.parent.name, | ||
target: declaration | ||
}; | ||
} | ||
return undefined; | ||
} | ||
|
||
function addUndefinedToOptionalProperty(changes: textChanges.ChangeTracker, toAdd: Symbol[]) { | ||
for (const add of toAdd) { | ||
const d = add.valueDeclaration; | ||
if (d && (isPropertySignature(d) || isPropertyDeclaration(d)) && d.type) { | ||
const t = factory.createUnionTypeNode([ | ||
...d.type.kind === SyntaxKind.UnionType ? (d.type as UnionTypeNode).types : [d.type], | ||
factory.createTypeReferenceNode("undefined") | ||
]); | ||
changes.replaceNode(d.getSourceFile(), d.type, t); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved this verbatim inside the new
else