Skip to content

Commit fc61077

Browse files
Allow passing values parameters (#7137)
fix #7118 (Cannot use template parameter in object/array value) also fix #5905 (Cannot use template parameter in property default)
1 parent 9f69544 commit fc61077

File tree

6 files changed

+146
-17
lines changed

6 files changed

+146
-17
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/compiler"
6+
---
7+
8+
Allow passing template parameters as property defaults
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: fix
4+
packages:
5+
- "@typespec/compiler"
6+
---
7+
8+
Allow passing tempalate parameter values in object and array values used inside the template

packages/compiler/src/core/checker.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ import {
156156
UnknownType,
157157
UsingStatementNode,
158158
Value,
159+
ValueWithTemplate,
159160
VoidType,
160161
} from "./types.js";
161162

@@ -315,7 +316,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
315316
TypeReferenceNode | MemberExpressionNode | IdentifierNode,
316317
Sym | undefined
317318
>();
318-
const valueExactTypes = new WeakMap<Value, Type>();
319+
const valueExactTypes = new WeakMap<ValueWithTemplate, Type>();
319320
let onCheckerDiagnostic: (diagnostic: Diagnostic) => void = (x: Diagnostic) => {
320321
program.reportDiagnostic(x);
321322
};
@@ -561,7 +562,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
561562
node: Node,
562563
mapper?: TypeMapper,
563564
constraint?: CheckValueConstraint,
564-
options: { legacyTupleAndModelCast?: boolean } = {},
565565
): Value | null {
566566
const initial = checkNode(node, mapper, constraint);
567567
if (initial === null) {
@@ -573,15 +573,28 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
573573
} else {
574574
entity = initial;
575575
}
576-
// if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) {
577-
// entity = legacy_tryTypeToValueCast(entity, constraint, node);
578-
// }
579576
if (entity === null) {
580577
return null;
581578
}
582579
if (isValue(entity)) {
583580
return constraint ? inferScalarsFromConstraints(entity, constraint.type) : entity;
584581
}
582+
// If a template parameter that can be a value is used in a template declaration then we allow it but we return null because we don't have an actual value.
583+
if (
584+
entity.kind === "TemplateParameter" &&
585+
entity.constraint?.valueType &&
586+
entity.constraint.type === undefined &&
587+
mapper === undefined
588+
) {
589+
return createValue(
590+
{
591+
entityKind: "Value",
592+
valueKind: "TemplateValue",
593+
type: entity.constraint.valueType,
594+
},
595+
entity.constraint.valueType,
596+
) as any;
597+
}
585598
reportExpectedValue(node, entity);
586599
return null;
587600
}
@@ -3841,7 +3854,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
38413854
);
38423855
}
38433856

3844-
function createValue<T extends Value>(value: T, preciseType: Type): T {
3857+
function createValue<T extends ValueWithTemplate>(value: T, preciseType: Type): T {
38453858
valueExactTypes.set(value, preciseType);
38463859
return value;
38473860
}
@@ -4650,7 +4663,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
46504663
pendingResolutions.start(sym, ResolutionKind.Type);
46514664
type.type = getTypeForNode(prop.value, mapper);
46524665
if (prop.default) {
4653-
const defaultValue = checkDefaultValue(prop.default, type.type);
4666+
const defaultValue = checkDefaultValue(prop.default, type.type, mapper);
46544667
if (defaultValue !== null) {
46554668
type.defaultValue = defaultValue;
46564669
}
@@ -4687,27 +4700,30 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
46874700
};
46884701
}
46894702

4690-
function checkDefaultValue(defaultNode: Node, type: Type): Value | null {
4703+
function checkDefaultValue(
4704+
defaultNode: Node,
4705+
type: Type,
4706+
mapper: TypeMapper | undefined,
4707+
): Value | null {
46914708
if (isErrorType(type)) {
46924709
// if the prop type is an error we don't need to validate again.
46934710
return null;
46944711
}
4695-
const defaultValue = getValueForNode(
4696-
defaultNode,
4697-
undefined,
4698-
{
4699-
kind: "assignment",
4700-
type,
4701-
},
4702-
{ legacyTupleAndModelCast: true },
4703-
);
4712+
const defaultValue = getValueForNode(defaultNode, mapper, {
4713+
kind: "assignment",
4714+
type,
4715+
});
47044716
if (defaultValue === null) {
47054717
return null;
47064718
}
47074719
const [related, diagnostics] = relation.isValueOfType(defaultValue, type, defaultNode);
47084720
if (!related) {
47094721
reportCheckerDiagnostics(diagnostics);
47104722
return null;
4723+
} else if ((defaultValue.valueKind as any) === "TemplateValue") {
4724+
// Right now we don't want to expose `TemplateValue` in the type graph.
4725+
// And as interating with the template declaration is not a supported feature we can just drop it.
4726+
return null;
47114727
} else {
47124728
return { ...defaultValue, type };
47134729
}

packages/compiler/src/core/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ export type Value =
317317
| EnumValue
318318
| NullValue;
319319

320+
/** @internal */
321+
export type ValueWithTemplate = Value | TemplateValue;
322+
320323
interface BaseValue {
321324
readonly entityKind: "Value";
322325
readonly valueKind: string;
@@ -380,6 +383,15 @@ export interface NullValue extends BaseValue {
380383
value: null;
381384
}
382385

386+
/**
387+
* This is an internal type that represent a value while in a template declaration.
388+
* This type should currently never be exposed on the type graph(unlike TemplateParameter).
389+
* @internal
390+
*/
391+
export interface TemplateValue extends BaseValue {
392+
valueKind: "TemplateValue";
393+
}
394+
383395
//#endregion Values
384396

385397
export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase {

packages/compiler/test/checker/model.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,34 @@ describe("compiler: models", () => {
174174
deepStrictEqual(foo.defaultValue?.value, "up-value");
175175
});
176176
});
177+
178+
describe("using a template parameter", () => {
179+
it(`set it with valid constraint`, async () => {
180+
testHost.addTypeSpecFile(
181+
"main.tsp",
182+
`
183+
model A<T extends valueof string> { @test foo?: string = T }
184+
alias Test = A<"Abc">;
185+
`,
186+
);
187+
const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty };
188+
strictEqual(foo.defaultValue?.valueKind, "StringValue");
189+
});
190+
191+
it(`error if constraint is not compatible with property type`, async () => {
192+
testHost.addTypeSpecFile(
193+
"main.tsp",
194+
`
195+
model A<T extends valueof int32> { @test foo?: string = T }
196+
`,
197+
);
198+
const diagnostics = await testHost.diagnose("main.tsp");
199+
expectDiagnostics(diagnostics, {
200+
code: "unassignable",
201+
message: "Type 'int32' is not assignable to type 'string'",
202+
});
203+
});
204+
});
177205
});
178206

179207
describe("doesn't allow a default of different type than the property type", () => {

packages/compiler/test/checker/templates.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,4 +1078,61 @@ describe("compiler: templates", () => {
10781078
strictEqual(members[1][1].name, "string");
10791079
});
10801080
});
1081+
1082+
describe("template declaration passing values", () => {
1083+
it("allows passing to a decorator expecting that value", async () => {
1084+
testHost.addJsFile("effect.js", {
1085+
$call: () => null,
1086+
});
1087+
1088+
testHost.addTypeSpecFile(
1089+
"main.tsp",
1090+
`
1091+
import "./effect.js";
1092+
extern dec call(target, arg: valueof string);
1093+
@call(T) model Dec<T extends valueof string> {}
1094+
`,
1095+
);
1096+
const diagnostics = await testHost.diagnose("main.tsp");
1097+
expectDiagnosticEmpty(diagnostics);
1098+
});
1099+
1100+
it("allows passing to a decorator expecting a composed value", async () => {
1101+
testHost.addJsFile("effect.js", {
1102+
$call: () => null,
1103+
});
1104+
1105+
testHost.addTypeSpecFile(
1106+
"main.tsp",
1107+
`
1108+
import "./effect.js";
1109+
extern dec call(target, arg: valueof unknown);
1110+
@call(#{foo: T}) model Dec<T extends valueof string> {}
1111+
`,
1112+
);
1113+
const diagnostics = await testHost.diagnose("main.tsp");
1114+
expectDiagnosticEmpty(diagnostics);
1115+
});
1116+
1117+
it("validate incompatible composed values", async () => {
1118+
testHost.addJsFile("effect.js", {
1119+
$call: () => null,
1120+
});
1121+
1122+
testHost.addTypeSpecFile(
1123+
"main.tsp",
1124+
`
1125+
import "./effect.js";
1126+
extern dec call(target, arg: valueof {foo: int32});
1127+
@call(#{foo: T}) model Dec<T extends valueof string> {}
1128+
`,
1129+
);
1130+
const diagnostics = await testHost.diagnose("main.tsp");
1131+
expectDiagnostics(diagnostics, {
1132+
code: "invalid-argument",
1133+
message:
1134+
"Argument of type '{ foo: string }' is not assignable to parameter of type '{ foo: int32 }'",
1135+
});
1136+
});
1137+
});
10811138
});

0 commit comments

Comments
 (0)