Improvements to type relation errors (#4357)
fix https://github.com/microsoft/typespec/issues/3291 Changes: 1. Figure out the most accurate location for the diagnostic 2. If diagnostic target a child node of the base diagnostic target then emit diagnostic directly there 3. Otherwise emit back at the root(or closest child node) and build stack of error message Example the following would now emit the error on a ```ts const b = #{ prop: #{a: "abc"}}; const a: {prop: {}} = b; ``` ``` 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 '{}'. ``` Previously the error would have been in the complete wrong place <img width="271" alt="image" src="https://github.com/user-attachments/assets/c403d1ec-3611-4ad6-87b0-2e0a075dc1c5">
This commit is contained in:
Родитель
89e19ef521
Коммит
03d4fca5c0
|
@ -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.
|
|
@ -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);
|
||||
|
|
|
@ -444,8 +444,13 @@ 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-unassignable": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Types of property '${"propName"}' are incompatible`,
|
||||
},
|
||||
},
|
||||
"property-required": {
|
||||
|
|
|
@ -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,
|
||||
|
@ -41,12 +43,45 @@ 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,
|
||||
maybe = 2,
|
||||
}
|
||||
|
||||
interface TypeRelationError {
|
||||
code:
|
||||
| "unassignable"
|
||||
| "property-unassignable"
|
||||
| "missing-index"
|
||||
| "property-required"
|
||||
| "missing-property"
|
||||
| "unexpected-property";
|
||||
message: string;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping from the reflection models to Type["kind"] value
|
||||
*/
|
||||
|
@ -69,24 +104,6 @@ const _assertReflectionNameToKind: Record<string, Type["kind"]> = ReflectionName
|
|||
|
||||
type ReflectionTypeName = keyof typeof ReflectionNameToKind;
|
||||
|
||||
export interface TypeRelation {
|
||||
isTypeAssignableTo(
|
||||
source: Entity | IndeterminateEntity,
|
||||
target: Entity,
|
||||
diagnosticTarget: DiagnosticTarget
|
||||
): [boolean, readonly Diagnostic[]];
|
||||
|
||||
isValueOfType(
|
||||
source: Value,
|
||||
target: Type,
|
||||
diagnosticTarget: DiagnosticTarget
|
||||
): [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,
|
||||
|
@ -104,15 +121,83 @@ 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, 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
|
||||
): readonly Diagnostic[] {
|
||||
return errors.flatMap((x) => convertErrorToDiagnostic(x, diagnosticBase));
|
||||
}
|
||||
|
||||
function combineErrorMessage(error: TypeRelationError): string {
|
||||
let message = error.message;
|
||||
let current = error.children[0];
|
||||
let indent = " ";
|
||||
while (current !== undefined) {
|
||||
message += current.message
|
||||
.split("\n")
|
||||
.map((line) => `\n${indent}${line}`)
|
||||
.join("");
|
||||
indent += " ";
|
||||
current = current.children[0];
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function flattenErrors(
|
||||
error: TypeRelationError,
|
||||
diagnosticBase: Entity | Node
|
||||
): TypeRelationError[] {
|
||||
if (!isTargetChildOf(error.target, diagnosticBase)) {
|
||||
return [{ ...error, target: diagnosticBase }];
|
||||
}
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,23 +209,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, diagnosticTarget)];
|
||||
}
|
||||
|
||||
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 +243,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 +340,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,
|
||||
}),
|
||||
],
|
||||
];
|
||||
|
@ -282,7 +367,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) &&
|
||||
|
@ -303,9 +388,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 +420,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 +433,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 +493,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);
|
||||
}
|
||||
|
||||
|
@ -563,29 +648,29 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
|
|||
return true;
|
||||
}
|
||||
|
||||
function isModelRelatedTo(
|
||||
function areModelsRelated(
|
||||
source: Model,
|
||||
target: Model,
|
||||
diagnosticTarget: DiagnosticTarget,
|
||||
diagnosticTarget: Entity | Node,
|
||||
relationCache: MultiKeyMap<[Entity, Entity], Related>
|
||||
): [Related, Diagnostic[]] {
|
||||
): [Related, readonly TypeRelationError[]] {
|
||||
relationCache.set([source, target], Related.maybe);
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
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(
|
||||
createDiagnostic({
|
||||
errors.push(
|
||||
createTypeRelationError({
|
||||
code: "missing-property",
|
||||
format: {
|
||||
propertyName: prop.name,
|
||||
sourceType: getTypeName(source),
|
||||
targetType: getTypeName(target),
|
||||
},
|
||||
target: source,
|
||||
diagnosticTarget: source,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -593,25 +678,25 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
|
|||
remainingProperties.delete(prop.name);
|
||||
|
||||
if (sourceProperty.optional && !prop.optional) {
|
||||
diagnostics.push(
|
||||
createDiagnostic({
|
||||
errors.push(
|
||||
createTypeRelationError({
|
||||
code: "property-required",
|
||||
format: {
|
||||
propName: prop.name,
|
||||
targetType: getTypeName(target),
|
||||
},
|
||||
target: diagnosticTarget,
|
||||
diagnosticTarget,
|
||||
})
|
||||
);
|
||||
}
|
||||
const [related, propDiagnostics] = isTypeAssignableToInternal(
|
||||
const [related, propErrors] = isTypeAssignableToInternal(
|
||||
sourceProperty.type,
|
||||
prop.type,
|
||||
diagnosticTarget,
|
||||
relationCache
|
||||
);
|
||||
if (!related) {
|
||||
diagnostics.push(...propDiagnostics);
|
||||
errors.push(...wrapUnassignablePropertyErrors(sourceProperty, propErrors));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -623,7 +708,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") {
|
||||
|
@ -634,27 +719,30 @@ 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(
|
||||
createDiagnostic({
|
||||
errors.push(
|
||||
createTypeRelationError({
|
||||
code: "unexpected-property",
|
||||
format: {
|
||||
propertyName: propName,
|
||||
type: getEntityName(target),
|
||||
},
|
||||
target: prop,
|
||||
diagnosticTarget: prop,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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. */
|
||||
|
@ -678,9 +766,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
|
|||
function arePropertiesAssignableToIndexer(
|
||||
properties: Map<string, ModelProperty>,
|
||||
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 +788,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 +817,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 +839,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 +865,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 +899,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 +922,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 +942,6 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
|
|||
}
|
||||
}
|
||||
|
||||
function createUnassignableDiagnostic(
|
||||
source: Entity,
|
||||
target: Entity,
|
||||
diagnosticTarget: DiagnosticTarget
|
||||
) {
|
||||
return createDiagnostic({
|
||||
code: "unassignable",
|
||||
format: { targetType: getEntityName(target), value: getEntityName(source) },
|
||||
target: diagnosticTarget,
|
||||
});
|
||||
}
|
||||
function isTypeSpecNamespace(
|
||||
namespace: Namespace
|
||||
): namespace is Namespace & { name: "TypeSpec"; namespace: Namespace } {
|
||||
|
@ -887,3 +952,76 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// #region Helpers
|
||||
interface TypeRelationeErrorInit<C extends TypeRelationError["code"]> {
|
||||
code: C;
|
||||
diagnosticTarget: Entity | Node;
|
||||
format: DiagnosticReport<CompilerDiagnostics, C, "default">["format"];
|
||||
details?: string;
|
||||
skipIfFirst?: boolean;
|
||||
}
|
||||
|
||||
function wrapUnassignableErrors(
|
||||
source: Entity,
|
||||
target: Entity,
|
||||
errors: readonly TypeRelationError[]
|
||||
): readonly TypeRelationError[] {
|
||||
const error = createUnassignableDiagnostic(source, target, source);
|
||||
error.children = errors;
|
||||
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.children = errors;
|
||||
return [error];
|
||||
}
|
||||
function createTypeRelationError<const C extends TypeRelationError["code"]>({
|
||||
code,
|
||||
format,
|
||||
details,
|
||||
diagnosticTarget,
|
||||
skipIfFirst,
|
||||
}: TypeRelationeErrorInit<C>): 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,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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)!,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
@ -841,7 +842,10 @@ describe("compiler: checker: type relations", () => {
|
|||
{ source: `Record<int32>`, target: "Record<string>" },
|
||||
{
|
||||
code: "unassignable",
|
||||
message: "Type 'int32' is not assignable to type 'string'",
|
||||
message: [
|
||||
`Type 'Record<int32>' is not assignable to type 'Record<string>'`,
|
||||
" Type 'int32' is not assignable to type 'string'",
|
||||
].join("\n"),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -963,8 +967,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"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1716,3 +1723,101 @@ describe("compiler: checker: type relations", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
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 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",
|
||||
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: {} }'`,
|
||||
` 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: { a: int32 } } = #{ ┆prop: b┆ };`,
|
||||
{
|
||||
code: "unassignable",
|
||||
message: [
|
||||
`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"),
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче