From a0a3f7765eb7745af9763c52db9ebc0164f7e18e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Sep 2024 15:20:01 -0400 Subject: [PATCH 01/11] Add TypeRelationError abstraction --- packages/compiler/src/core/checker.ts | 12 +- packages/compiler/src/core/messages.ts | 3 +- .../src/core/type-relation-checker.ts | 222 +++++++++++------- packages/compiler/src/lib/service.ts | 4 +- 4 files changed, 147 insertions(+), 94 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a8921b6af5..4ba4013550 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5649,7 +5649,7 @@ export function createChecker(program: Program): Checker { function checkArgumentAssignable( argumentType: Type | Value | IndeterminateEntity, parameterType: Entity, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): boolean { const [valid] = relation.isTypeAssignableTo(argumentType, parameterType, diagnosticTarget); if (!valid) { @@ -7420,7 +7420,7 @@ export function createChecker(program: Program): Checker { function checkTypeOfValueMatchConstraint( source: Entity, constraint: CheckValueConstraint, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): boolean { const [related, diagnostics] = relation.isTypeAssignableTo( source, @@ -7455,7 +7455,7 @@ export function createChecker(program: Program): Checker { function checkTypeAssignable( source: Entity | IndeterminateEntity, target: Entity, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): boolean { const [related, diagnostics] = relation.isTypeAssignableTo(source, target, diagnosticTarget); if (!related) { @@ -7464,11 +7464,7 @@ export function createChecker(program: Program): Checker { return related; } - function checkValueOfType( - source: Value, - target: Type, - diagnosticTarget: DiagnosticTarget - ): boolean { + function checkValueOfType(source: Value, target: Type, diagnosticTarget: Entity | Node): boolean { const [related, diagnostics] = relation.isValueOfType(source, target, diagnosticTarget); if (!related) { reportCheckerDiagnostics(diagnostics); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 8f8b7c5c34..97422f8d60 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -444,8 +444,7 @@ const diagnostics = { unassignable: { severity: "error", messages: { - default: paramMessage`Type '${"value"}' is not assignable to type '${"targetType"}'`, - withDetails: paramMessage`Type '${"sourceType"}' is not assignable to type '${"targetType"}'\n ${"details"}`, + default: paramMessage`Type '${"sourceType"}' is not assignable to type '${"targetType"}'`, }, }, "property-required": { diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index e49d898936..7f236553f3 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -12,7 +12,7 @@ import { getMinValueAsNumeric, getMinValueExclusiveAsNumeric, } from "./intrinsic-type-state.js"; -import { createDiagnostic } from "./messages.js"; +import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; import { Program } from "./program.js"; @@ -21,7 +21,7 @@ import { ArrayModelType, ArrayValue, Diagnostic, - DiagnosticTarget, + DiagnosticReport, Entity, Enum, IndeterminateEntity, @@ -30,6 +30,8 @@ import { ModelIndexer, ModelProperty, Namespace, + Node, + NoTarget, NumericLiteral, Scalar, StringLiteral, @@ -47,6 +49,18 @@ enum Related { maybe = 2, } +interface TypeRelationError { + code: + | "unassignable" + | "missing-index" + | "property-required" + | "missing-property" + | "unexpected-property"; + message: string; + child?: TypeRelationError; + target: Entity | Node; +} + /** * Mapping from the reflection models to Type["kind"] value */ @@ -73,13 +87,13 @@ export interface TypeRelation { isTypeAssignableTo( source: Entity | IndeterminateEntity, target: Entity, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): [boolean, readonly Diagnostic[]]; isValueOfType( source: Value, target: Type, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): [boolean, readonly Diagnostic[]]; isReflectionType(type: Type): type is Model & { name: ReflectionTypeName }; @@ -104,15 +118,39 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isTypeAssignableTo( source: Entity | IndeterminateEntity, target: Entity, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): [boolean, readonly Diagnostic[]] { - const [related, diagnostics] = isTypeAssignableToInternal( + const [related, errors] = isTypeAssignableToInternal( source, target, diagnosticTarget, new MultiKeyMap<[Entity, Entity], Related>() ); - return [related === Related.true, diagnostics]; + return [related === Related.true, convertErrorsToDiagnostics(errors)]; + } + + function convertErrorsToDiagnostics(errors: readonly TypeRelationError[]): readonly Diagnostic[] { + return errors.map(convertErrorToDiagnostic); + } + + function convertErrorToDiagnostic(error: TypeRelationError): Diagnostic { + let message = error.message; + let current = error.child; + let indent = " "; + while (current !== undefined) { + message += current.message + .split("\n") + .map((line) => `\n${indent}${line}`) + .join(""); + indent += " "; + current = error.child; + } + return { + severity: "error", + code: error.code, + message: message, + target: error.target, + }; } /** @@ -124,23 +162,23 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isValueOfType( source: Value, target: Type, - diagnosticTarget: DiagnosticTarget + diagnosticTarget: Entity | Node ): [boolean, readonly Diagnostic[]] { - const [related, diagnostics] = isValueOfTypeInternal( + const [related, errors] = isValueOfTypeInternal( source, target, diagnosticTarget, new MultiKeyMap<[Entity, Entity], Related>() ); - return [related === Related.true, diagnostics]; + return [related === Related.true, convertErrorsToDiagnostics(errors)]; } function isTypeAssignableToInternal( source: Entity | IndeterminateEntity, target: Entity, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity | IndeterminateEntity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { const cached = relationCache.get([source, target]); if (cached !== undefined) { return [cached, []]; @@ -158,9 +196,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isTypeAssignableToWorker( source: Entity | IndeterminateEntity, target: Entity, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { // BACKCOMPAT: Allow certain type to be accepted as values if ( "kind" in source && @@ -255,13 +293,13 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return [ Related.false, [ - createDiagnostic({ + createTypeRelationError({ code: "missing-index", format: { indexType: getTypeName(source.indexer.key), sourceType: getTypeName(target), }, - target: diagnosticTarget, + diagnosticTarget, }), ], ]; @@ -303,9 +341,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isIndeterminateEntityAssignableTo( indeterminate: IndeterminateEntity, target: Type | MixedParameterConstraint, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { const [typeRelated, typeDiagnostics] = isTypeAssignableToInternal( indeterminate.type, target, @@ -335,9 +373,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isAssignableToValueType( source: Entity, target: Type, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { if (!isValue(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } @@ -348,9 +386,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isAssignableToMixedParameterConstraint( source: Entity, target: MixedParameterConstraint, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { if ("entityKind" in source && source.entityKind === "MixedParameterConstraint") { if (source.type && target.type) { const [variantAssignable, diagnostics] = isTypeAssignableToInternal( @@ -408,9 +446,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isValueOfTypeInternal( source: Value, target: Type, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { return isTypeAssignableToInternal(source.type, target, diagnosticTarget, relationCache); } @@ -566,11 +604,11 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isModelRelatedTo( source: Model, target: Model, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, Diagnostic[]] { + ): [Related, TypeRelationError[]] { relationCache.set([source, target], Related.maybe); - const diagnostics: Diagnostic[] = []; + const diagnostics: TypeRelationError[] = []; const remainingProperties = new Map(source.properties); for (const prop of walkPropertiesInherited(target)) { @@ -578,14 +616,14 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T if (sourceProperty === undefined) { if (!prop.optional) { diagnostics.push( - createDiagnostic({ + createTypeRelationError({ code: "missing-property", format: { propertyName: prop.name, sourceType: getTypeName(source), targetType: getTypeName(target), }, - target: source, + diagnosticTarget: source, }) ); } @@ -594,13 +632,13 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T if (sourceProperty.optional && !prop.optional) { diagnostics.push( - createDiagnostic({ + createTypeRelationError({ code: "property-required", format: { propName: prop.name, targetType: getTypeName(target), }, - target: diagnosticTarget, + diagnosticTarget, }) ); } @@ -641,13 +679,13 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T for (const [propName, prop] of remainingProperties) { if (shouldCheckExcessProperty(prop)) { diagnostics.push( - createDiagnostic({ + createTypeRelationError({ code: "unexpected-property", format: { propertyName: propName, type: getEntityName(target), }, - target: prop, + diagnosticTarget: prop, }) ); } @@ -678,9 +716,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function arePropertiesAssignableToIndexer( properties: Map, indexerConstaint: Type, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Type, Type], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { for (const prop of properties.values()) { const [related, diagnostics] = isTypeAssignableToInternal( prop.type, @@ -700,20 +738,20 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function hasIndexAndIsAssignableTo( source: Model, target: Model & { indexer: ModelIndexer }, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { if (source.indexer === undefined || source.indexer.key !== target.indexer.key) { return [ Related.false, [ - createDiagnostic({ + createTypeRelationError({ code: "missing-index", format: { indexType: getTypeName(target.indexer.key), sourceType: getTypeName(source), }, - target: diagnosticTarget, + diagnosticTarget, }), ], ]; @@ -729,25 +767,21 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isTupleAssignableToArray( source: Tuple, target: ArrayModelType, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { const minItems = getMinItems(program, target); const maxItems = getMaxItems(program, target); if (minItems !== undefined && source.values.length < minItems) { return [ Related.false, [ - createDiagnostic({ - code: "unassignable", - messageId: "withDetails", - format: { - sourceType: getEntityName(source), - targetType: getTypeName(target), - details: `Source has ${source.values.length} element(s) but target requires ${minItems}.`, - }, - target: diagnosticTarget, - }), + createUnassignableDiagnostic( + source, + target, + diagnosticTarget, + `Source has ${source.values.length} element(s) but target requires ${minItems}.` + ), ], ]; } @@ -755,16 +789,12 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return [ Related.false, [ - createDiagnostic({ - code: "unassignable", - messageId: "withDetails", - format: { - sourceType: getEntityName(source), - targetType: getTypeName(target), - details: `Source has ${source.values.length} element(s) but target only allows ${maxItems}.`, - }, - target: diagnosticTarget, - }), + createUnassignableDiagnostic( + source, + target, + diagnosticTarget, + `Source has ${source.values.length} element(s) but target only allows ${maxItems}.` + ), ], ]; } @@ -785,23 +815,19 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isTupleAssignableToTuple( source: Tuple | ArrayValue, target: Tuple, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { + ): [Related, readonly TypeRelationError[]] { if (source.values.length !== target.values.length) { return [ Related.false, [ - createDiagnostic({ - code: "unassignable", - messageId: "withDetails", - format: { - sourceType: getEntityName(source), - targetType: getTypeName(target), - details: `Source has ${source.values.length} element(s) but target requires ${target.values.length}.`, - }, - target: diagnosticTarget, - }), + createUnassignableDiagnostic( + source, + target, + diagnosticTarget, + `Source has ${source.values.length} element(s) but target requires ${target.values.length}.` + ), ], ]; } @@ -823,9 +849,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isAssignableToUnion( source: Type, target: Union, - diagnosticTarget: DiagnosticTarget, + diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, Diagnostic[]] { + ): [Related, TypeRelationError[]] { if (source.kind === "UnionVariant" && source.union === target) { return [Related.true, []]; } @@ -846,8 +872,8 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isAssignableToEnum( source: Type, target: Enum, - diagnosticTarget: DiagnosticTarget - ): [Related, Diagnostic[]] { + diagnosticTarget: Entity | Node + ): [Related, TypeRelationError[]] { switch (source.kind) { case "Enum": if (source === target) { @@ -866,17 +892,49 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } } + interface TypeRelationeErrorInit { + code: C; + diagnosticTarget: Entity | Node; + format: DiagnosticReport["format"]; + details?: string; + } + + function createTypeRelationError({ + code, + format, + details, + diagnosticTarget, + }: TypeRelationeErrorInit): TypeRelationError { + const diag = createDiagnostic({ + code: code as any, + format: format, + target: NoTarget, + }); + + return { + code: code, + message: details ? `${diag.message}\n ${details}` : diag.message, + target: diagnosticTarget, + }; + } + function createUnassignableDiagnostic( source: Entity, target: Entity, - diagnosticTarget: DiagnosticTarget - ) { - return createDiagnostic({ + diagnosticTarget: Entity | Node, + details?: string + ): TypeRelationError { + return createTypeRelationError({ code: "unassignable", - format: { targetType: getEntityName(target), value: getEntityName(source) }, - target: diagnosticTarget, + format: { + sourceType: getEntityName(source), + targetType: getEntityName(target), + }, + diagnosticTarget, + details, }); } + function isTypeSpecNamespace( namespace: Namespace ): namespace is Namespace & { name: "TypeSpec"; namespace: Namespace } { diff --git a/packages/compiler/src/lib/service.ts b/packages/compiler/src/lib/service.ts index abca2d6009..299d20f658 100644 --- a/packages/compiler/src/lib/service.ts +++ b/packages/compiler/src/lib/service.ts @@ -89,7 +89,7 @@ export const $service: ServiceDecorator = ( } else { reportDiagnostic(context.program, { code: "unassignable", - format: { value: getTypeName(title), targetType: "String" }, + format: { sourceType: getTypeName(title), targetType: "String" }, target: context.getArgumentTarget(0)!, }); } @@ -107,7 +107,7 @@ export const $service: ServiceDecorator = ( } else { reportDiagnostic(context.program, { code: "unassignable", - format: { value: getTypeName(version), targetType: "String" }, + format: { sourceType: getTypeName(version), targetType: "String" }, target: context.getArgumentTarget(0)!, }); } From eaedce5caaff49d3efa5de64fd0fd112fb9d4f67 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Sep 2024 15:39:20 -0400 Subject: [PATCH 02/11] Cascade error up --- .../src/core/type-relation-checker.ts | 35 +++++++++++++++---- packages/compiler/src/lib/service.ts | 4 +++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index 7f236553f3..b020529560 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -126,14 +126,20 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T diagnosticTarget, new MultiKeyMap<[Entity, Entity], Related>() ); - return [related === Related.true, convertErrorsToDiagnostics(errors)]; + return [related === Related.true, convertErrorsToDiagnostics(errors, diagnosticTarget)]; } - function convertErrorsToDiagnostics(errors: readonly TypeRelationError[]): readonly Diagnostic[] { - return errors.map(convertErrorToDiagnostic); + function convertErrorsToDiagnostics( + errors: readonly TypeRelationError[], + diagnosticBase: Entity | Node + ): readonly Diagnostic[] { + return errors.map((x) => convertErrorToDiagnostic(x, diagnosticBase)); } - function convertErrorToDiagnostic(error: TypeRelationError): Diagnostic { + function convertErrorToDiagnostic( + error: TypeRelationError, + diagnosticBase: Entity | Node + ): Diagnostic { let message = error.message; let current = error.child; let indent = " "; @@ -145,11 +151,28 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T indent += " "; current = error.child; } + + const errorNode: Node = + "kind" in error.target && typeof error.target.kind === "number" + ? error.target + : (error.target as any).node; + const baseNode: Node = + "kind" in diagnosticBase && typeof diagnosticBase.kind === "number" + ? diagnosticBase + : (diagnosticBase as any).node; + let target = diagnosticBase; + let currentNode: Node | undefined = errorNode; + while (currentNode) { + if (current === baseNode) { + target = errorNode; + } + currentNode = currentNode.parent; + } return { severity: "error", code: error.code, message: message, - target: error.target, + target, }; } @@ -170,7 +193,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T diagnosticTarget, new MultiKeyMap<[Entity, Entity], Related>() ); - return [related === Related.true, convertErrorsToDiagnostics(errors)]; + return [related === Related.true, convertErrorsToDiagnostics(errors, diagnosticTarget)]; } function isTypeAssignableToInternal( diff --git a/packages/compiler/src/lib/service.ts b/packages/compiler/src/lib/service.ts index 299d20f658..3d4bc830b4 100644 --- a/packages/compiler/src/lib/service.ts +++ b/packages/compiler/src/lib/service.ts @@ -115,3 +115,7 @@ export const $service: ServiceDecorator = ( addService(context.program, target, serviceDetails); }; + +// const empty = {}; +// const emptyDeep = { a: {} }; +// const _a: { a: { b: string } } = emptyDeep; From 6ea157cdec706120ad8558affeec196d2445c12c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Sep 2024 17:00:33 -0400 Subject: [PATCH 03/11] better target --- .../src/core/type-relation-checker.ts | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index b020529560..819741cba2 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -129,6 +129,21 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return [related === Related.true, convertErrorsToDiagnostics(errors, diagnosticTarget)]; } + function isTargetChildOf(target: Entity | Node, base: Entity | Node) { + const errorNode: Node = + "kind" in target && typeof target.kind === "number" ? target : (target as any).node; + const baseNode: Node = + "kind" in base && typeof base.kind === "number" ? base : (base as any).node; + let currentNode: Node | undefined = errorNode; + while (currentNode) { + if (currentNode === baseNode) { + return true; + } + currentNode = currentNode.parent; + } + return false; + } + function convertErrorsToDiagnostics( errors: readonly TypeRelationError[], diagnosticBase: Entity | Node @@ -136,10 +151,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return errors.map((x) => convertErrorToDiagnostic(x, diagnosticBase)); } - function convertErrorToDiagnostic( - error: TypeRelationError, - diagnosticBase: Entity | Node - ): Diagnostic { + function combineErrorMessage(error: TypeRelationError): string { let message = error.message; let current = error.child; let indent = " "; @@ -149,28 +161,33 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T .map((line) => `\n${indent}${line}`) .join(""); indent += " "; - current = error.child; + current = current.child; } + return message; + } - const errorNode: Node = - "kind" in error.target && typeof error.target.kind === "number" - ? error.target - : (error.target as any).node; - const baseNode: Node = - "kind" in diagnosticBase && typeof diagnosticBase.kind === "number" - ? diagnosticBase - : (diagnosticBase as any).node; + function convertErrorToDiagnostic( + error: TypeRelationError, + diagnosticBase: Entity | Node + ): Diagnostic { + let current = error; let target = diagnosticBase; - let currentNode: Node | undefined = errorNode; - while (currentNode) { - if (current === baseNode) { - target = errorNode; + let base = error; + while (true) { + if (isTargetChildOf(current.target, diagnosticBase)) { + base = current; + target = current.target; } - currentNode = currentNode.parent; + if (current.child === undefined) { + break; + } + current = current.child; } + const message = combineErrorMessage(base); + return { severity: "error", - code: error.code, + code: base.code, message: message, target, }; @@ -343,7 +360,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } } else if (target.kind === "Model" && source.kind === "Model") { - return isModelRelatedTo(source, target, diagnosticTarget, relationCache); + return areModelsRelated(source, target, diagnosticTarget, relationCache); } else if ( target.kind === "Model" && isArrayModelType(program, target) && @@ -624,21 +641,21 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T return true; } - function isModelRelatedTo( + function areModelsRelated( source: Model, target: Model, diagnosticTarget: Entity | Node, relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, TypeRelationError[]] { + ): [Related, readonly TypeRelationError[]] { relationCache.set([source, target], Related.maybe); - const diagnostics: TypeRelationError[] = []; + const errors: TypeRelationError[] = []; const remainingProperties = new Map(source.properties); for (const prop of walkPropertiesInherited(target)) { const sourceProperty = getProperty(source, prop.name); if (sourceProperty === undefined) { if (!prop.optional) { - diagnostics.push( + errors.push( createTypeRelationError({ code: "missing-property", format: { @@ -654,7 +671,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T remainingProperties.delete(prop.name); if (sourceProperty.optional && !prop.optional) { - diagnostics.push( + errors.push( createTypeRelationError({ code: "property-required", format: { @@ -672,7 +689,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T relationCache ); if (!related) { - diagnostics.push(...propDiagnostics); + errors.push(...propDiagnostics); } } } @@ -684,7 +701,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T diagnosticTarget, relationCache ); - diagnostics.push(...indexerDiagnostics); + errors.push(...indexerDiagnostics); // For anonymous models we don't need an indexer if (source.name !== "" && target.indexer.key.name !== "integer") { @@ -695,13 +712,13 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T relationCache ); if (!related) { - diagnostics.push(...indexDiagnostics); + errors.push(...indexDiagnostics); } } } else if (shouldCheckExcessProperties(source)) { for (const [propName, prop] of remainingProperties) { if (shouldCheckExcessProperty(prop)) { - diagnostics.push( + errors.push( createTypeRelationError({ code: "unexpected-property", format: { @@ -715,7 +732,10 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } } - return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; + return [ + errors.length === 0 ? Related.true : Related.false, + wrapUnassignableErrors(source, target, errors), + ]; } /** If we should check for excess properties on the given model. */ @@ -958,6 +978,16 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T }); } + function wrapUnassignableErrors( + source: Entity, + target: Entity, + errors: readonly TypeRelationError[] + ): readonly TypeRelationError[] { + const error = createUnassignableDiagnostic(source, target, source); + error.child = errors[0]; + return [error]; + } + function isTypeSpecNamespace( namespace: Namespace ): namespace is Namespace & { name: "TypeSpec"; namespace: Namespace } { From 871b49c5e7d5abf3d89bed62a22129178d9e8473 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Sep 2024 17:19:56 -0400 Subject: [PATCH 04/11] add tests --- .../compiler/test/checker/relation.test.ts | 73 +++++++++++++++++-- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 36cf0c2284..8cf62cc6e9 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -16,6 +16,7 @@ import { expectDiagnosticEmpty, expectDiagnostics, extractCursor, + extractSquiggles, } from "../../src/testing/index.js"; interface RelatedTypeOptions { @@ -24,14 +25,14 @@ interface RelatedTypeOptions { commonCode?: string; } -describe("compiler: checker: type relations", () => { - let runner: BasicTestRunner; - let host: TestHost; - beforeEach(async () => { - host = await createTestHost(); - runner = createTestWrapper(host); - }); +let runner: BasicTestRunner; +let host: TestHost; +beforeEach(async () => { + host = await createTestHost(); + runner = createTestWrapper(host); +}); +describe("compiler: checker: type relations", () => { async function checkTypeAssignable({ source, target, commonCode }: RelatedTypeOptions): Promise<{ related: boolean; diagnostics: readonly Diagnostic[]; @@ -1716,3 +1717,61 @@ describe("compiler: checker: type relations", () => { }); }); }); + +describe.only("relation error target and messages", () => { + async function expectRelationDiagnostics(code: string, expected: DiagnosticMatch) { + const { pos, end, source } = extractSquiggles(code, "┆"); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + pos, + end, + ...expected, + }); + } + + it("report missing property at assignment right on the object literal", async () => { + await expectRelationDiagnostics(`const a: {a: string} = ┆#{}┆;`, { + code: "missing-property", + message: "Property 'a' is missing on type '{}' but required in '{ a: string }'", + }); + }); + + it("report missing property at assignment right on the object literal (nested)", async () => { + await expectRelationDiagnostics(`const a: {prop: {a: string}} = #{prop: ┆#{}┆};`, { + code: "missing-property", + message: "Property 'a' is missing on type '{}' but required in '{ a: string }'", + }); + }); + + it("report extra property at assignment right on the property literal", async () => { + await expectRelationDiagnostics(`const a: {} = #{┆a: "abc"┆};`, { + code: "unexpected-property", + message: + "Object value may only specify known properties, and 'a' does not exist in type '{}'.", + }); + }); + + it("report extra property at assignment right on the property literal (nested)", async () => { + await expectRelationDiagnostics(`const a: {prop: {}} = #{ prop: #{┆a: "abc"┆}};`, { + code: "unexpected-property", + message: + "Object value may only specify known properties, and 'a' does not exist in type '{}'.", + }); + }); + + it("report with full stack if originate from another declaration", async () => { + await expectRelationDiagnostics( + ` + const b = #{ prop: #{a: "abc"}}; + const ┆a┆: {prop: {}} = b;`, + { + code: "unassignable", + message: [ + `Type '{ prop: { a: "abc" } }' is not assignable to type '{ prop: {} }'`, + ` Type '{ a: "abc" }' is not assignable to type '{}'`, + ` Object value may only specify known properties, and 'a' does not exist in type '{}'.`, + ].join("\n"), + } + ); + }); +}); From 19ef27c336212ccc4984088286cabce80e8c97a5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 09:10:16 -0400 Subject: [PATCH 05/11] better errors when traverse in the middle --- packages/compiler/src/core/messages.ts | 6 ++++ .../src/core/type-relation-checker.ts | 30 +++++++++++++++++-- .../compiler/test/checker/relation.test.ts | 22 ++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 97422f8d60..0c02fe9a87 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -447,6 +447,12 @@ const diagnostics = { default: paramMessage`Type '${"sourceType"}' is not assignable to type '${"targetType"}'`, }, }, + "property-unassignable": { + severity: "error", + messages: { + default: paramMessage`Types of property '${"propName"}' are incompatible`, + }, + }, "property-required": { severity: "error", messages: { diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index 819741cba2..16e6685942 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -52,6 +52,7 @@ enum Related { interface TypeRelationError { code: | "unassignable" + | "property-unassignable" | "missing-index" | "property-required" | "missing-property" @@ -59,6 +60,8 @@ interface TypeRelationError { message: string; child?: TypeRelationError; target: Entity | Node; + /** If the first error and it has a child show the child error at this target instead */ + skipIfFirst?: boolean; } /** @@ -183,7 +186,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } current = current.child; } - const message = combineErrorMessage(base); + + const messageBase = base.skipIfFirst && base.child ? base.child : base; + const message = combineErrorMessage(messageBase); return { severity: "error", @@ -682,14 +687,14 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T }) ); } - const [related, propDiagnostics] = isTypeAssignableToInternal( + const [related, propErrors] = isTypeAssignableToInternal( sourceProperty.type, prop.type, diagnosticTarget, relationCache ); if (!related) { - errors.push(...propDiagnostics); + errors.push(...wrapUnassignablePropertyErrors(sourceProperty, prop, propErrors)); } } } @@ -940,6 +945,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T diagnosticTarget: Entity | Node; format: DiagnosticReport["format"]; details?: string; + skipIfFirst?: boolean; } function createTypeRelationError({ @@ -947,6 +953,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T format, details, diagnosticTarget, + skipIfFirst, }: TypeRelationeErrorInit): TypeRelationError { const diag = createDiagnostic({ code: code as any, @@ -958,6 +965,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T code: code, message: details ? `${diag.message}\n ${details}` : diag.message, target: diagnosticTarget, + skipIfFirst, }; } @@ -987,6 +995,22 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T error.child = errors[0]; return [error]; } + function wrapUnassignablePropertyErrors( + source: ModelProperty, + target: ModelProperty, + errors: readonly TypeRelationError[] + ): readonly TypeRelationError[] { + const error = createTypeRelationError({ + code: "property-unassignable", + diagnosticTarget: source, + format: { + propName: source.name, + }, + skipIfFirst: true, + }); + error.child = errors[0]; + return [error]; + } function isTypeSpecNamespace( namespace: Namespace diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 8cf62cc6e9..8d3976d691 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1718,7 +1718,7 @@ describe("compiler: checker: type relations", () => { }); }); -describe.only("relation error target and messages", () => { +describe("relation error target and messages", () => { async function expectRelationDiagnostics(code: string, expected: DiagnosticMatch) { const { pos, end, source } = extractSquiggles(code, "┆"); const diagnostics = await runner.diagnose(source); @@ -1768,8 +1768,24 @@ describe.only("relation error target and messages", () => { code: "unassignable", message: [ `Type '{ prop: { a: "abc" } }' is not assignable to type '{ prop: {} }'`, - ` Type '{ a: "abc" }' is not assignable to type '{}'`, - ` Object value may only specify known properties, and 'a' does not exist in type '{}'.`, + ` Types of property 'prop' are incompatible`, + ` Type '{ a: "abc" }' is not assignable to type '{}'`, + ` Object value may only specify known properties, and 'a' does not exist in type '{}'.`, + ].join("\n"), + } + ); + }); + + it("show up error in the further node without leaving the base", async () => { + await expectRelationDiagnostics( + ` + const b = #{a: "abc"}; + const a: { prop: { b: string } } = #{ ┆prop: b┆ };`, + { + code: "property-unassignable", + message: [ + `Type '{ a: "abc" }' is not assignable to type '{ b: string }'`, + ` Property 'b' is missing on type '{ a: "abc" }' but required in '{ b: string }'`, ].join("\n"), } ); From e450e80e6e51c8090523b12bbe1b698f66515489 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 06:16:30 -0700 Subject: [PATCH 06/11] Create type-relation-improvements-2024-8-6-13-13-15.md --- .../type-relation-improvements-2024-8-6-13-13-15.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/type-relation-improvements-2024-8-6-13-13-15.md diff --git a/.chronus/changes/type-relation-improvements-2024-8-6-13-13-15.md b/.chronus/changes/type-relation-improvements-2024-8-6-13-13-15.md new file mode 100644 index 0000000000..f1937efc80 --- /dev/null +++ b/.chronus/changes/type-relation-improvements-2024-8-6-13-13-15.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Improvements to type relation errors: Show stack when it happens in a nested property otherwise show up in the correct location. From ba750a616ce4435e891b1353a10976b2982cfb3c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 09:23:18 -0400 Subject: [PATCH 07/11] cleanup --- .../src/core/type-relation-checker.ts | 182 +++++++++--------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index 16e6685942..f34f521167 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -43,6 +43,24 @@ import { Value, } from "./types.js"; +export interface TypeRelation { + isTypeAssignableTo( + source: Entity | IndeterminateEntity, + target: Entity, + diagnosticTarget: Entity | Node + ): [boolean, readonly Diagnostic[]]; + + isValueOfType( + source: Value, + target: Type, + diagnosticTarget: Entity | Node + ): [boolean, readonly Diagnostic[]]; + + isReflectionType(type: Type): type is Model & { name: ReflectionTypeName }; + + areScalarsRelated(source: Scalar, target: Scalar): boolean; +} + enum Related { false = 0, true = 1, @@ -86,24 +104,6 @@ const _assertReflectionNameToKind: Record = ReflectionName type ReflectionTypeName = keyof typeof ReflectionNameToKind; -export interface TypeRelation { - isTypeAssignableTo( - source: Entity | IndeterminateEntity, - target: Entity, - diagnosticTarget: Entity | Node - ): [boolean, readonly Diagnostic[]]; - - isValueOfType( - source: Value, - target: Type, - diagnosticTarget: Entity | Node - ): [boolean, readonly Diagnostic[]]; - - isReflectionType(type: Type): type is Model & { name: ReflectionTypeName }; - - areScalarsRelated(source: Scalar, target: Scalar): boolean; -} - export function createTypeRelationChecker(program: Program, checker: Checker): TypeRelation { return { isTypeAssignableTo, @@ -694,7 +694,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T relationCache ); if (!related) { - errors.push(...wrapUnassignablePropertyErrors(sourceProperty, prop, propErrors)); + errors.push(...wrapUnassignablePropertyErrors(sourceProperty, propErrors)); } } } @@ -940,78 +940,6 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } } - interface TypeRelationeErrorInit { - code: C; - diagnosticTarget: Entity | Node; - format: DiagnosticReport["format"]; - details?: string; - skipIfFirst?: boolean; - } - - function createTypeRelationError({ - code, - format, - details, - diagnosticTarget, - skipIfFirst, - }: TypeRelationeErrorInit): TypeRelationError { - const diag = createDiagnostic({ - code: code as any, - format: format, - target: NoTarget, - }); - - return { - code: code, - message: details ? `${diag.message}\n ${details}` : diag.message, - target: diagnosticTarget, - skipIfFirst, - }; - } - - function createUnassignableDiagnostic( - source: Entity, - target: Entity, - diagnosticTarget: Entity | Node, - details?: string - ): TypeRelationError { - return createTypeRelationError({ - code: "unassignable", - format: { - sourceType: getEntityName(source), - targetType: getEntityName(target), - }, - diagnosticTarget, - details, - }); - } - - function wrapUnassignableErrors( - source: Entity, - target: Entity, - errors: readonly TypeRelationError[] - ): readonly TypeRelationError[] { - const error = createUnassignableDiagnostic(source, target, source); - error.child = errors[0]; - return [error]; - } - function wrapUnassignablePropertyErrors( - source: ModelProperty, - target: ModelProperty, - errors: readonly TypeRelationError[] - ): readonly TypeRelationError[] { - const error = createTypeRelationError({ - code: "property-unassignable", - diagnosticTarget: source, - format: { - propName: source.name, - }, - skipIfFirst: true, - }); - error.child = errors[0]; - return [error]; - } - function isTypeSpecNamespace( namespace: Namespace ): namespace is Namespace & { name: "TypeSpec"; namespace: Namespace } { @@ -1022,3 +950,75 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T ); } } + +// #region Helpers +interface TypeRelationeErrorInit { + code: C; + diagnosticTarget: Entity | Node; + format: DiagnosticReport["format"]; + details?: string; + skipIfFirst?: boolean; +} + +function wrapUnassignableErrors( + source: Entity, + target: Entity, + errors: readonly TypeRelationError[] +): readonly TypeRelationError[] { + const error = createUnassignableDiagnostic(source, target, source); + error.child = errors[0]; + return [error]; +} +function wrapUnassignablePropertyErrors( + source: ModelProperty, + errors: readonly TypeRelationError[] +): readonly TypeRelationError[] { + const error = createTypeRelationError({ + code: "property-unassignable", + diagnosticTarget: source, + format: { + propName: source.name, + }, + skipIfFirst: true, + }); + error.child = errors[0]; + return [error]; +} +function createTypeRelationError({ + code, + format, + details, + diagnosticTarget, + skipIfFirst, +}: TypeRelationeErrorInit): TypeRelationError { + const diag = createDiagnostic({ + code: code as any, + format: format, + target: NoTarget, + }); + + return { + code: code, + message: details ? `${diag.message}\n ${details}` : diag.message, + target: diagnosticTarget, + skipIfFirst, + }; +} + +function createUnassignableDiagnostic( + source: Entity, + target: Entity, + diagnosticTarget: Entity | Node, + details?: string +): TypeRelationError { + return createTypeRelationError({ + code: "unassignable", + format: { + sourceType: getEntityName(source), + targetType: getEntityName(target), + }, + diagnosticTarget, + details, + }); +} +// #endregion From 4a9444f28ac411fa2a7c20a5d4734ffb78b2aff1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 10:13:25 -0400 Subject: [PATCH 08/11] fix test --- packages/compiler/test/checker/relation.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 8d3976d691..83b4638396 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -964,8 +964,11 @@ describe("compiler: checker: type relations", () => { }); ok(!related); expectDiagnostics(diagnostics, { - code: "missing-property", - message: "Property 'b' is missing on type 'A' but required in 'B'", + code: "unassignable", + message: [ + `Type 'A' is not assignable to type 'B'`, + " Property 'b' is missing on type 'A' but required in 'B'", + ].join("\n"), }); }); }); From a0f2d5c0e382e3bc62dbda15f34e2d47c4e54767 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 14:53:32 -0400 Subject: [PATCH 09/11] Keep multi errors --- .../src/core/type-relation-checker.ts | 63 ++++++++++--------- .../compiler/test/checker/relation.test.ts | 14 +++-- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index f34f521167..773e646eac 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -76,7 +76,7 @@ interface TypeRelationError { | "missing-property" | "unexpected-property"; message: string; - child?: TypeRelationError; + children: readonly TypeRelationError[]; target: Entity | Node; /** If the first error and it has a child show the child error at this target instead */ skipIfFirst?: boolean; @@ -151,12 +151,12 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T errors: readonly TypeRelationError[], diagnosticBase: Entity | Node ): readonly Diagnostic[] { - return errors.map((x) => convertErrorToDiagnostic(x, diagnosticBase)); + return errors.flatMap((x) => convertErrorToDiagnostic(x, diagnosticBase)); } function combineErrorMessage(error: TypeRelationError): string { let message = error.message; - let current = error.child; + let current = error.children[0]; let indent = " "; while (current !== undefined) { message += current.message @@ -164,38 +164,40 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T .map((line) => `\n${indent}${line}`) .join(""); indent += " "; - current = current.child; + current = current.children[0]; } return message; } - function convertErrorToDiagnostic( + function flattenErrors( error: TypeRelationError, diagnosticBase: Entity | Node - ): Diagnostic { - let current = error; - let target = diagnosticBase; - let base = error; - while (true) { - if (isTargetChildOf(current.target, diagnosticBase)) { - base = current; - target = current.target; - } - if (current.child === undefined) { - break; - } - current = current.child; + ): TypeRelationError[] { + if (!isTargetChildOf(error.target, diagnosticBase)) { + return [{ ...error, target: diagnosticBase }]; } - - const messageBase = base.skipIfFirst && base.child ? base.child : base; - const message = combineErrorMessage(messageBase); - - return { - severity: "error", - code: base.code, - message: message, - target, - }; + if (error.children.length === 0) { + return [error]; + } + return error.children.flatMap((x) => flattenErrors(x, error.target)); + } + function convertErrorToDiagnostic( + error: TypeRelationError, + diagnosticBase: Entity | Node + ): Diagnostic[] { + const flattened = flattenErrors(error, diagnosticBase); + return flattened.map((error) => { + const messageBase = + error.skipIfFirst && error.children.length > 0 ? error.children[0] : error; + const message = combineErrorMessage(messageBase); + + return { + severity: "error", + code: error.code, + message: message, + target: error.target, + }; + }); } /** @@ -966,7 +968,7 @@ function wrapUnassignableErrors( errors: readonly TypeRelationError[] ): readonly TypeRelationError[] { const error = createUnassignableDiagnostic(source, target, source); - error.child = errors[0]; + error.children = errors; return [error]; } function wrapUnassignablePropertyErrors( @@ -981,7 +983,7 @@ function wrapUnassignablePropertyErrors( }, skipIfFirst: true, }); - error.child = errors[0]; + error.children = errors; return [error]; } function createTypeRelationError({ @@ -1002,6 +1004,7 @@ function createTypeRelationError({ message: details ? `${diag.message}\n ${details}` : diag.message, target: diagnosticTarget, skipIfFirst, + children: [], }; } diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 83b4638396..4a546eb6a6 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -842,7 +842,10 @@ describe("compiler: checker: type relations", () => { { source: `Record`, target: "Record" }, { code: "unassignable", - message: "Type 'int32' is not assignable to type 'string'", + message: [ + `Type 'Record' is not assignable to type 'Record'`, + " Type 'int32' is not assignable to type 'string'", + ].join("\n"), } ); }); @@ -1783,12 +1786,13 @@ describe("relation error target and messages", () => { await expectRelationDiagnostics( ` const b = #{a: "abc"}; - const a: { prop: { b: string } } = #{ ┆prop: b┆ };`, + const a: { prop: { a: int32 } } = #{ ┆prop: b┆ };`, { - code: "property-unassignable", + code: "unassignable", message: [ - `Type '{ a: "abc" }' is not assignable to type '{ b: string }'`, - ` Property 'b' is missing on type '{ a: "abc" }' but required in '{ b: string }'`, + `Type '{ a: "abc" }' is not assignable to type '{ a: int32 }'`, + ` Types of property 'a' are incompatible`, + ` Type '"abc"' is not assignable to type 'int32'`, ].join("\n"), } ); From 0e11b62b99107e11faae08c01748ad533a88792a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 14:58:17 -0400 Subject: [PATCH 10/11] update --- .../compiler/test/checker/relation.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 4a546eb6a6..b62b3ff03a 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1757,6 +1757,29 @@ describe("relation error target and messages", () => { }); }); + it("report multiple extra property at assignment right on the property literal", async () => { + const { source: sourceTmp, ...pos1 } = extractSquiggles( + `const a: {} = #{┆a: "abc"┆, ┆b: "abc"┆};`, + "┆" + ); + const { source, ...pos2 } = extractSquiggles(sourceTmp, "┆"); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, [ + { + code: "unexpected-property", + message: + "Object value may only specify known properties, and 'a' does not exist in type '{}'.", + ...pos1, + }, + { + code: "unexpected-property", + message: + "Object value may only specify known properties, and 'b' does not exist in type '{}'.", + ...pos2, + }, + ]); + }); + it("report extra property at assignment right on the property literal (nested)", async () => { await expectRelationDiagnostics(`const a: {prop: {}} = #{ prop: #{┆a: "abc"┆}};`, { code: "unexpected-property", From d27bfc8359c79acc1190a81bae9f62fbc218184e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 15:33:20 -0400 Subject: [PATCH 11/11] remove test code --- packages/compiler/src/lib/service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/compiler/src/lib/service.ts b/packages/compiler/src/lib/service.ts index 3d4bc830b4..299d20f658 100644 --- a/packages/compiler/src/lib/service.ts +++ b/packages/compiler/src/lib/service.ts @@ -115,7 +115,3 @@ export const $service: ServiceDecorator = ( addService(context.program, target, serviceDetails); }; - -// const empty = {}; -// const emptyDeep = { a: {} }; -// const _a: { a: { b: string } } = emptyDeep;