Introduce flattened error reporting for properties, call signatures, and construct signatures (#33473)

* Introduce flattened error reporting for properties, call signatures, and construct signatures

* Update message, specialize output for argument-less signatures

* Skip leading signature incompatability flattening

* Add return type specialized message
This commit is contained in:
Wesley Wigham
2019-09-23 16:08:44 -07:00
committed by GitHub
parent 6c2ae12559
commit 26caa3793e
62 changed files with 1023 additions and 735 deletions

View File

@@ -12329,7 +12329,7 @@ namespace ts {
target: Signature,
ignoreReturnTypes: boolean): boolean {
return compareSignaturesRelated(source, target, CallbackCheck.None, ignoreReturnTypes, /*reportErrors*/ false,
/*errorReporter*/ undefined, compareTypesAssignable) !== Ternary.False;
/*errorReporter*/ undefined, /*errorReporter*/ undefined, compareTypesAssignable) !== Ternary.False;
}
type ErrorReporter = (message: DiagnosticMessage, arg0?: string, arg1?: string) => void;
@@ -12352,6 +12352,7 @@ namespace ts {
ignoreReturnTypes: boolean,
reportErrors: boolean,
errorReporter: ErrorReporter | undefined,
incompatibleErrorReporter: ((source: Type, target: Type) => void) | undefined,
compareTypes: TypeComparer): Ternary {
// TODO (drosen): De-duplicate code between related functions.
if (source === target) {
@@ -12422,7 +12423,7 @@ namespace ts {
(getFalsyFlags(sourceType) & TypeFlags.Nullable) === (getFalsyFlags(targetType) & TypeFlags.Nullable);
const related = callbacks ?
// TODO: GH#18217 It will work if they're both `undefined`, but not if only one is
compareSignaturesRelated(targetSig!, sourceSig!, strictVariance ? CallbackCheck.Strict : CallbackCheck.Bivariant, /*ignoreReturnTypes*/ false, reportErrors, errorReporter, compareTypes) :
compareSignaturesRelated(targetSig!, sourceSig!, strictVariance ? CallbackCheck.Strict : CallbackCheck.Bivariant, /*ignoreReturnTypes*/ false, reportErrors, errorReporter, incompatibleErrorReporter, compareTypes) :
!callbackCheck && !strictVariance && compareTypes(sourceType, targetType, /*reportErrors*/ false) || compareTypes(targetType, sourceType, reportErrors);
if (!related) {
if (reportErrors) {
@@ -12468,6 +12469,9 @@ namespace ts {
// wouldn't be co-variant for T without this rule.
result &= callbackCheck === CallbackCheck.Bivariant && compareTypes(targetReturnType, sourceReturnType, /*reportErrors*/ false) ||
compareTypes(sourceReturnType, targetReturnType, reportErrors);
if (!result && reportErrors && incompatibleErrorReporter) {
incompatibleErrorReporter(sourceReturnType, targetReturnType);
}
}
}
@@ -12677,11 +12681,16 @@ namespace ts {
let depth = 0;
let expandingFlags = ExpandingFlags.None;
let overflow = false;
let overrideNextErrorInfo: DiagnosticMessageChain | undefined;
let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: [DiagnosticMessage, (string | number)?, (string | number)?, (string | number)?, (string | number)?][] = [];
Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");
const result = isRelatedTo(source, target, /*reportErrors*/ !!errorNode, headMessage);
if (incompatibleStack.length) {
reportIncompatibleStack();
}
if (overflow) {
const diag = error(errorNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target));
if (errorOutputContainer) {
@@ -12726,8 +12735,134 @@ namespace ts {
}
return result !== Ternary.False;
function resetErrorInfo(saved: ReturnType<typeof captureErrorCalculationState>) {
errorInfo = saved.errorInfo;
lastSkippedInfo = saved.lastSkippedInfo;
incompatibleStack = saved.incompatibleStack;
overrideNextErrorInfo = saved.overrideNextErrorInfo;
relatedInfo = saved.relatedInfo;
}
function captureErrorCalculationState() {
return {
errorInfo,
lastSkippedInfo,
incompatibleStack: incompatibleStack.slice(),
overrideNextErrorInfo,
relatedInfo: !relatedInfo ? undefined : relatedInfo.slice() as ([DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined)
};
}
function reportIncompatibleError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number) {
overrideNextErrorInfo++; // Suppress the next relation error
lastSkippedInfo = undefined; // Reset skipped info cache
incompatibleStack.push([message, arg0, arg1, arg2, arg3]);
}
function reportIncompatibleStack() {
const stack = incompatibleStack;
incompatibleStack = [];
const info = lastSkippedInfo;
lastSkippedInfo = undefined;
if (stack.length === 1) {
reportError(...stack[0]);
if (info) {
// Actually do the last relation error
reportRelationError(/*headMessage*/ undefined, ...info);
}
return;
}
// The first error will be the innermost, while the last will be the outermost - so by popping off the end,
// we can build from left to right
let path = "";
const secondaryRootErrors: typeof incompatibleStack = [];
while (stack.length) {
const [msg, ...args] = stack.pop()!;
switch (msg.code) {
case Diagnostics.Types_of_property_0_are_incompatible.code: {
// Parenthesize a `new` if there is one
if (path.indexOf("new ") === 0) {
path = `(${path})`;
}
const str = "" + args[0];
// If leading, just print back the arg (irrespective of if it's a valid identifier)
if (path.length === 0) {
path = `${str}`;
}
// Otherwise write a dotted name if possible
else if (isIdentifierText(str, compilerOptions.target)) {
path = `${path}.${str}`;
}
// Failing that, check if the name is already a computed name
else if (str[0] === "[" && str[str.length - 1] === "]") {
path = `${path}${str}`;
}
// And finally write out a computed name as a last resort
else {
path = `${path}[${str}]`;
}
break;
}
case Diagnostics.Call_signature_return_types_0_and_1_are_incompatible.code:
case Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code:
case Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code:
case Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: {
if (path.length === 0) {
// Don't flatten signature compatability errors at the start of a chain - instead prefer
// to unify (the with no arguments bit is excessive for printback) and print them back
let mappedMsg = msg;
if (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) {
mappedMsg = Diagnostics.Call_signature_return_types_0_and_1_are_incompatible;
}
else if (msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) {
mappedMsg = Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible;
}
secondaryRootErrors.unshift([mappedMsg, args[0], args[1]]);
}
else {
const prefix = (msg.code === Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code ||
msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code)
? "new "
: "";
const params = (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code ||
msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code)
? ""
: "...";
path = `${prefix}${path}(${params})`;
}
break;
}
default:
return Debug.fail(`Unhandled Diagnostic: ${msg.code}`);
}
}
if (path) {
reportError(path[path.length - 1] === ")"
? Diagnostics.The_types_returned_by_0_are_incompatible_between_these_types
: Diagnostics.The_types_of_0_are_incompatible_between_these_types,
path
);
}
else {
// Remove the innermost secondary error as it will duplicate the error already reported by `reportRelationError` on entry
secondaryRootErrors.shift();
}
for (const [msg, ...args] of secondaryRootErrors) {
const originalValue = msg.elidedInCompatabilityPyramid;
msg.elidedInCompatabilityPyramid = false; // Teporarily override elision to ensure error is reported
reportError(msg, ...args);
msg.elidedInCompatabilityPyramid = originalValue;
}
if (info) {
// Actually do the last relation error
reportRelationError(/*headMessage*/ undefined, ...info);
}
}
function reportError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void {
Debug.assert(!!errorNode);
if (incompatibleStack.length) reportIncompatibleStack();
if (message.elidedInCompatabilityPyramid) return;
errorInfo = chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3);
}
@@ -12742,6 +12877,7 @@ namespace ts {
}
function reportRelationError(message: DiagnosticMessage | undefined, source: Type, target: Type) {
if (incompatibleStack.length) reportIncompatibleStack();
const [sourceType, targetType] = getTypeNamesForErrorDisplay(source, target);
if (target.flags & TypeFlags.TypeParameter && target.immediateBaseConstraint !== undefined && isTypeAssignableTo(source, target.immediateBaseConstraint)) {
@@ -12899,7 +13035,7 @@ namespace ts {
}
let result = Ternary.False;
const saveErrorInfo = errorInfo;
const saveErrorInfo = captureErrorCalculationState();
let isIntersectionConstituent = !!isApparentIntersectionConstituent;
// Note that these checks are specifically ordered to produce correct results. In particular,
@@ -12949,7 +13085,7 @@ namespace ts {
}
if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) {
if (result = recursiveTypeRelatedTo(source, target, reportErrors, isIntersectionConstituent)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
}
}
}
@@ -12966,19 +13102,21 @@ namespace ts {
const constraint = getUnionConstraintOfIntersection(<IntersectionType>source, !!(target.flags & TypeFlags.Union));
if (constraint) {
if (result = isRelatedTo(constraint, target, reportErrors, /*headMessage*/ undefined, isIntersectionConstituent)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
}
}
}
if (!result && reportErrors) {
let maybeSuppress = overrideNextErrorInfo;
overrideNextErrorInfo = undefined;
let maybeSuppress = overrideNextErrorInfo > 0;
if (maybeSuppress) {
overrideNextErrorInfo--;
}
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) {
const currentError = errorInfo;
tryElaborateArrayLikeErrors(source, target, reportErrors);
if (errorInfo !== currentError) {
maybeSuppress = errorInfo;
maybeSuppress = !!errorInfo;
}
}
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Primitive) {
@@ -12998,6 +13136,7 @@ namespace ts {
}
}
if (!headMessage && maybeSuppress) {
lastSkippedInfo = [source, target];
// Used by, eg, missing property checking to replace the top-level message with a more informative one
return result;
}
@@ -13439,7 +13578,7 @@ namespace ts {
let result: Ternary;
let originalErrorInfo: DiagnosticMessageChain | undefined;
let varianceCheckFailed = false;
const saveErrorInfo = errorInfo;
const saveErrorInfo = captureErrorCalculationState();
// We limit alias variance probing to only object and conditional types since their alias behavior
// is more predictable than other, interned types, which may or may not have an alias depending on
@@ -13531,7 +13670,7 @@ namespace ts {
}
}
originalErrorInfo = errorInfo;
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
}
}
}
@@ -13543,7 +13682,7 @@ namespace ts {
result &= isRelatedTo((<IndexedAccessType>source).indexType, (<IndexedAccessType>target).indexType, reportErrors);
}
if (result) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
@@ -13552,25 +13691,25 @@ namespace ts {
if (!constraint || (source.flags & TypeFlags.TypeParameter && constraint.flags & TypeFlags.Any)) {
// A type variable with no constraint is not related to the non-primitive object type.
if (result = isRelatedTo(emptyObjectType, extractTypesOfKind(target, ~TypeFlags.NonPrimitive))) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
// hi-speed no-this-instantiation check (less accurate, but avoids costly `this`-instantiation when the constraint will suffice), see #28231 for report on why this is needed
else if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, isIntersectionConstituent)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
// slower, fuller, this-instantiated check (necessary when comparing raw `this` types from base classes), see `subclassWithPolymorphicThisIsAssignable.ts` test for example
else if (result = isRelatedTo(getTypeWithThisArgument(constraint, source), target, reportErrors, /*headMessage*/ undefined, isIntersectionConstituent)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
else if (source.flags & TypeFlags.Index) {
if (result = isRelatedTo(keyofConstraintType, target, reportErrors)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
@@ -13595,7 +13734,7 @@ namespace ts {
result &= isRelatedTo(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target), reportErrors);
}
if (result) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
@@ -13604,14 +13743,14 @@ namespace ts {
const distributiveConstraint = getConstraintOfDistributiveConditionalType(<ConditionalType>source);
if (distributiveConstraint) {
if (result = isRelatedTo(distributiveConstraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
const defaultConstraint = getDefaultConstraintOfConditionalType(<ConditionalType>source);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
@@ -13625,7 +13764,7 @@ namespace ts {
if (isGenericMappedType(target)) {
if (isGenericMappedType(source)) {
if (result = mappedTypeRelatedTo(source, target, reportErrors)) {
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return result;
}
}
@@ -13671,7 +13810,7 @@ namespace ts {
// relates to X. Thus, we include intersection types on the source side here.
if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Object) {
// Report structural errors only if we haven't reported any errors yet
const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo && !sourceIsPrimitive;
const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo.errorInfo && !sourceIsPrimitive;
result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, isIntersectionConstituent);
if (result) {
result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors);
@@ -13686,7 +13825,7 @@ namespace ts {
}
}
if (varianceCheckFailed && result) {
errorInfo = originalErrorInfo || errorInfo || saveErrorInfo; // Use variance error (there is no structural one) and return false
errorInfo = originalErrorInfo || errorInfo || saveErrorInfo.errorInfo; // Use variance error (there is no structural one) and return false
}
else if (result) {
return result;
@@ -13718,7 +13857,7 @@ namespace ts {
// We elide the variance-based error elaborations, since those might not be too helpful, since we'll potentially
// be assuming identity of the type parameter.
originalErrorInfo = undefined;
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
return undefined;
}
const allowStructuralFallback = targetTypeArguments && hasCovariantVoidArgument(targetTypeArguments, variances);
@@ -13746,7 +13885,7 @@ namespace ts {
// comparison unexpectedly succeeds. This can happen when the structural comparison result
// is a Ternary.Maybe for example caused by the recursion depth limiter.
originalErrorInfo = errorInfo;
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
}
}
}
@@ -13980,7 +14119,7 @@ namespace ts {
const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, isIntersectionConstituent);
if (!related) {
if (reportErrors) {
reportError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp));
reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp));
}
return Ternary.False;
}
@@ -14022,8 +14161,8 @@ namespace ts {
if (length(unmatchedProperty.declarations)) {
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
}
if (shouldSkipElaboration) {
overrideNextErrorInfo = errorInfo;
if (shouldSkipElaboration && errorInfo) {
overrideNextErrorInfo++;
}
}
else if (tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ false)) {
@@ -14033,8 +14172,8 @@ namespace ts {
else {
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
}
if (shouldSkipElaboration) {
overrideNextErrorInfo = errorInfo;
if (shouldSkipElaboration && errorInfo) {
overrideNextErrorInfo++;
}
}
// ELSE: No array like or unmatched property error - just issue top level error (errorInfo = undefined)
@@ -14157,7 +14296,8 @@ namespace ts {
}
let result = Ternary.True;
const saveErrorInfo = errorInfo;
const saveErrorInfo = captureErrorCalculationState();
const incompatibleReporter = kind === SignatureKind.Construct ? reportIncompatibleConstructSignatureReturn : reportIncompatibleCallSignatureReturn;
if (getObjectFlags(source) & ObjectFlags.Instantiated && getObjectFlags(target) & ObjectFlags.Instantiated && source.symbol === target.symbol) {
// We have instantiations of the same anonymous type (which typically will be the type of a
@@ -14165,7 +14305,7 @@ namespace ts {
// of the much more expensive N * M comparison matrix we explore below. We erase type parameters
// as they are known to always be the same.
for (let i = 0; i < targetSignatures.length; i++) {
const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors);
const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors, incompatibleReporter(sourceSignatures[i], targetSignatures[i]));
if (!related) {
return Ternary.False;
}
@@ -14179,17 +14319,17 @@ namespace ts {
// this regardless of the number of signatures, but the potential costs are prohibitive due
// to the quadratic nature of the logic below.
const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks;
result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors);
result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors, incompatibleReporter(sourceSignatures[0], targetSignatures[0]));
}
else {
outer: for (const t of targetSignatures) {
// Only elaborate errors from the first failure
let shouldElaborateErrors = reportErrors;
for (const s of sourceSignatures) {
const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors);
const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors, incompatibleReporter(s, t));
if (related) {
result &= related;
errorInfo = saveErrorInfo;
resetErrorInfo(saveErrorInfo);
continue outer;
}
shouldElaborateErrors = false;
@@ -14206,12 +14346,26 @@ namespace ts {
return result;
}
function reportIncompatibleCallSignatureReturn(siga: Signature, sigb: Signature) {
if (siga.parameters.length === 0 && sigb.parameters.length === 0) {
return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target));
}
return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Call_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target));
}
function reportIncompatibleConstructSignatureReturn(siga: Signature, sigb: Signature) {
if (siga.parameters.length === 0 && sigb.parameters.length === 0) {
return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target));
}
return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target));
}
/**
* See signatureAssignableTo, compareSignaturesIdentical
*/
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean): Ternary {
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: Type, target: Type) => void): Ternary {
return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target,
CallbackCheck.None, /*ignoreReturnTypes*/ false, reportErrors, reportError, isRelatedTo);
CallbackCheck.None, /*ignoreReturnTypes*/ false, reportErrors, reportError, incompatibleReporter, isRelatedTo);
}
function signaturesIdenticalTo(source: Type, target: Type, kind: SignatureKind): Ternary {

View File

@@ -1040,6 +1040,35 @@
"code": 1357
},
"The types of '{0}' are incompatible between these types.": {
"category": "Error",
"code": 2200
},
"The types returned by '{0}' are incompatible between these types.": {
"category": "Error",
"code": 2201
},
"Call signature return types '{0}' and '{1}' are incompatible.": {
"category": "Error",
"code": 2202,
"elidedInCompatabilityPyramid": true
},
"Construct signature return types '{0}' and '{1}' are incompatible.": {
"category": "Error",
"code": 2203,
"elidedInCompatabilityPyramid": true
},
"Call signatures with no arguments have incompatible return types '{0}' and '{1}'.": {
"category": "Error",
"code": 2204,
"elidedInCompatabilityPyramid": true
},
"Construct signatures with no arguments have incompatible return types '{0}' and '{1}'.": {
"category": "Error",
"code": 2205,
"elidedInCompatabilityPyramid": true
},
"Duplicate identifier '{0}'.": {
"category": "Error",
"code": 2300

View File

@@ -4631,6 +4631,8 @@ namespace ts {
code: number;
message: string;
reportsUnnecessary?: {};
/* @internal */
elidedInCompatabilityPyramid?: boolean;
}
/**