Implement pre-projection (#1791)
- Add support for `pre to` and `pre from` projections that can run before children are projected and return never to delete or a different type. - It is an error to mutate anything pre-projections but this is not currently enforced by the compiler. We had a note to document this, but projections are still experimental and undocumented, so documeting it here. - Add support for `#enummember`, `#unionvariant`, and `#modelproperty` projections. - Use these features in versioning library to fix issues that led to emitter crashes with versioned resource types. - Fix issues with incompatible versioniing verification: - Bogus error asking to annotate a template definition when passing a versioned template argument. - Failure to validate versioned types in tuples or unions. - Failure to validate template arguments/array element type in operation return type. - Add access to the extends list of an interface via `Interface.sourceInterfaces`. This was a requested feature for next sprint, but ended up helping to get the validation fixed so it is done here.
This commit is contained in:
Родитель
a99db17d47
Коммит
dfa63a26d6
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/compiler",
|
||||
"comment": "Add pre-projection support.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/compiler"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/compiler",
|
||||
"comment": "Provide access to extended interfaces in type graph.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/compiler"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/html-program-viewer",
|
||||
"comment": "",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/html-program-viewer"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/openapi3",
|
||||
"comment": "",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/openapi3"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/versioning",
|
||||
"comment": "Use pre-projections to fix issues with versioned resources.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/versioning"
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { compilerAssert } from "./diagnostics.js";
|
||||
import { visitChildren } from "./parser.js";
|
||||
import { Program } from "./program.js";
|
||||
import {
|
||||
|
@ -347,16 +348,37 @@ export function createBinder(program: Program): Binder {
|
|||
node.selector.kind !== SyntaxKind.Identifier &&
|
||||
node.selector.kind !== SyntaxKind.MemberExpression
|
||||
) {
|
||||
const selectorString =
|
||||
node.selector.kind === SyntaxKind.ProjectionModelSelector
|
||||
? "model"
|
||||
: node.selector.kind === SyntaxKind.ProjectionOperationSelector
|
||||
? "op"
|
||||
: node.selector.kind === SyntaxKind.ProjectionUnionSelector
|
||||
? "union"
|
||||
: node.selector.kind === SyntaxKind.ProjectionEnumSelector
|
||||
? "enum"
|
||||
: "interface";
|
||||
let selectorString: string;
|
||||
switch (node.selector.kind) {
|
||||
case SyntaxKind.ProjectionModelSelector:
|
||||
selectorString = "model";
|
||||
break;
|
||||
case SyntaxKind.ProjectionModelPropertySelector:
|
||||
selectorString = "modelproperty";
|
||||
break;
|
||||
case SyntaxKind.ProjectionOperationSelector:
|
||||
selectorString = "op";
|
||||
break;
|
||||
case SyntaxKind.ProjectionUnionSelector:
|
||||
selectorString = "union";
|
||||
break;
|
||||
case SyntaxKind.ProjectionUnionVariantSelector:
|
||||
selectorString = "unionvariant";
|
||||
break;
|
||||
case SyntaxKind.ProjectionEnumSelector:
|
||||
selectorString = "enum";
|
||||
break;
|
||||
case SyntaxKind.ProjectionEnumMemberSelector:
|
||||
selectorString = "enummember";
|
||||
break;
|
||||
case SyntaxKind.ProjectionInterfaceSelector:
|
||||
selectorString = "interface";
|
||||
break;
|
||||
default:
|
||||
const _never: never = node.selector;
|
||||
compilerAssert(false, "Unreachable");
|
||||
}
|
||||
|
||||
let existingSelectors = projectionSymbolSelectors.get(sym);
|
||||
if (!existingSelectors) {
|
||||
existingSelectors = new Set();
|
||||
|
|
|
@ -290,10 +290,13 @@ export function createChecker(program: Program): Checker {
|
|||
|
||||
const projectionsByTypeKind = new Map<Type["kind"], ProjectionStatementNode[]>([
|
||||
["Model", []],
|
||||
["ModelProperty", []],
|
||||
["Union", []],
|
||||
["UnionVariant", []],
|
||||
["Operation", []],
|
||||
["Interface", []],
|
||||
["Enum", []],
|
||||
["EnumMember", []],
|
||||
]);
|
||||
const projectionsByType = new Map<Type, ProjectionStatementNode[]>();
|
||||
// whether we've checked this specific projection statement before
|
||||
|
@ -301,7 +304,7 @@ export function createChecker(program: Program): Checker {
|
|||
const processedProjections = new Set<ProjectionStatementNode>();
|
||||
|
||||
// interpreter state
|
||||
let currentProjectionDirection: "to" | "from" | undefined;
|
||||
let currentProjectionDirection: "to" | "from" | "pre_to" | "pre_from" | undefined;
|
||||
/**
|
||||
* Set keeping track of node pending type resolution.
|
||||
* Key is the SymId of a node. It can be retrieved with getNodeSymId(node)
|
||||
|
@ -349,7 +352,7 @@ export function createChecker(program: Program): Checker {
|
|||
cloneType,
|
||||
resolveIdentifier,
|
||||
resolveCompletions,
|
||||
evalProjection: evalProjectionStatement,
|
||||
evalProjection,
|
||||
project,
|
||||
neverType,
|
||||
errorType,
|
||||
|
@ -3347,6 +3350,7 @@ export function createChecker(program: Program): Checker {
|
|||
decorators: [],
|
||||
node,
|
||||
namespace: getParentNamespaceType(node),
|
||||
sourceInterfaces: [],
|
||||
operations: createRekeyableMap(),
|
||||
name: node.id.sv,
|
||||
});
|
||||
|
@ -3384,6 +3388,7 @@ export function createChecker(program: Program): Checker {
|
|||
|
||||
interfaceType.operations.set(newMember.name, newMember);
|
||||
}
|
||||
interfaceType.sourceInterfaces.push(extendsType);
|
||||
}
|
||||
|
||||
for (const [key, value] of ownMembers) {
|
||||
|
@ -3915,6 +3920,10 @@ export function createChecker(program: Program): Checker {
|
|||
projectionsByTypeKind.get("Model")!.push(node);
|
||||
type.nodeByKind.set("Model", node);
|
||||
break;
|
||||
case SyntaxKind.ProjectionModelPropertySelector:
|
||||
projectionsByTypeKind.get("ModelProperty")!.push(node);
|
||||
type.nodeByKind.set("ModelProperty", node);
|
||||
break;
|
||||
case SyntaxKind.ProjectionOperationSelector:
|
||||
projectionsByTypeKind.get("Operation")!.push(node);
|
||||
type.nodeByKind.set("Operation", node);
|
||||
|
@ -3923,6 +3932,10 @@ export function createChecker(program: Program): Checker {
|
|||
projectionsByTypeKind.get("Union")!.push(node);
|
||||
type.nodeByKind.set("Union", node);
|
||||
break;
|
||||
case SyntaxKind.ProjectionUnionVariantSelector:
|
||||
projectionsByTypeKind.get("UnionVariant")!.push(node);
|
||||
type.nodeByKind.set("UnionVariant", node);
|
||||
break;
|
||||
case SyntaxKind.ProjectionInterfaceSelector:
|
||||
projectionsByTypeKind.get("Interface")!.push(node);
|
||||
type.nodeByKind.set("Interface", node);
|
||||
|
@ -3931,6 +3944,10 @@ export function createChecker(program: Program): Checker {
|
|||
projectionsByTypeKind.get("Enum")!.push(node);
|
||||
type.nodeByKind.set("Enum", node);
|
||||
break;
|
||||
case SyntaxKind.ProjectionEnumMemberSelector:
|
||||
projectionsByTypeKind.get("EnumMember")!.push(node);
|
||||
type.nodeByKind.set("EnumMember", node);
|
||||
break;
|
||||
default:
|
||||
const projected = checkTypeReference(node.selector, undefined);
|
||||
let current = projectionsByType.get(projected);
|
||||
|
@ -4247,7 +4264,11 @@ export function createChecker(program: Program): Checker {
|
|||
}
|
||||
}
|
||||
|
||||
function evalProjectionStatement(node: ProjectionNode, target: Type, args: Type[]): Type {
|
||||
function evalProjection(node: ProjectionNode, target: Type, args: Type[]): Type {
|
||||
if (node.direction === "<error>") {
|
||||
throw new ProjectionError("Cannot evaluate projection with invalid direction.");
|
||||
}
|
||||
|
||||
let topLevelProjection = false;
|
||||
if (!currentProjectionDirection) {
|
||||
topLevelProjection = true;
|
||||
|
@ -4550,7 +4571,7 @@ export function createChecker(program: Program): Checker {
|
|||
projection: ProjectionNode,
|
||||
args: (Type | boolean | string | number)[] = []
|
||||
) {
|
||||
return evalProjectionStatement(
|
||||
return evalProjection(
|
||||
projection,
|
||||
target,
|
||||
args.map((x) => marshalProjectionReturn(x))
|
||||
|
|
|
@ -62,6 +62,7 @@ import {
|
|||
OperationStatementNode,
|
||||
ParseOptions,
|
||||
ProjectionBlockExpressionNode,
|
||||
ProjectionEnumMemberSelectorNode,
|
||||
ProjectionEnumSelectorNode,
|
||||
ProjectionExpression,
|
||||
ProjectionExpressionStatementNode,
|
||||
|
@ -71,6 +72,7 @@ import {
|
|||
ProjectionLambdaParameterDeclarationNode,
|
||||
ProjectionModelExpressionNode,
|
||||
ProjectionModelPropertyNode,
|
||||
ProjectionModelPropertySelectorNode,
|
||||
ProjectionModelSelectorNode,
|
||||
ProjectionModelSpreadPropertyNode,
|
||||
ProjectionNode,
|
||||
|
@ -80,6 +82,7 @@ import {
|
|||
ProjectionStatementNode,
|
||||
ProjectionTupleExpressionNode,
|
||||
ProjectionUnionSelectorNode,
|
||||
ProjectionUnionVariantSelectorNode,
|
||||
ScalarStatementNode,
|
||||
SourceFile,
|
||||
Statement,
|
||||
|
@ -1560,29 +1563,31 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
|||
const id = parseIdentifier();
|
||||
|
||||
parseExpected(Token.OpenBrace);
|
||||
let from, to;
|
||||
let proj1, proj2;
|
||||
if (token() === Token.Identifier) {
|
||||
proj1 = parseProjection();
|
||||
|
||||
if (token() === Token.Identifier) {
|
||||
proj2 = parseProjection();
|
||||
const projectionMap = new Map<string, ProjectionNode>();
|
||||
const projections: ProjectionNode[] = [];
|
||||
while (token() === Token.Identifier) {
|
||||
const projection = parseProjection();
|
||||
if (projection.direction !== "<error>") {
|
||||
if (projectionMap.has(projection.direction)) {
|
||||
error({ code: "duplicate-symbol", target: projection, format: { name: "projection" } });
|
||||
} else {
|
||||
projectionMap.set(projection.direction, projection);
|
||||
}
|
||||
}
|
||||
// NOTE: Don't drop projections with error in direction definition from the AST.
|
||||
projections.push(projection);
|
||||
}
|
||||
|
||||
if (proj1 && proj2 && proj1.direction === proj2.direction) {
|
||||
error({ code: "duplicate-symbol", target: proj2, format: { name: "projection" } });
|
||||
} else if (proj1) {
|
||||
[to, from] = proj1.direction === "to" ? [proj1, proj2] : [proj2, proj1];
|
||||
}
|
||||
|
||||
parseExpected(Token.CloseBrace);
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.ProjectionStatement,
|
||||
selector,
|
||||
from,
|
||||
to,
|
||||
projections,
|
||||
preTo: projectionMap.get("pre_to"),
|
||||
preFrom: projectionMap.get("pre_from"),
|
||||
from: projectionMap.get("from"),
|
||||
to: projectionMap.get("to"),
|
||||
id,
|
||||
...finishNode(pos),
|
||||
};
|
||||
|
@ -1590,20 +1595,32 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
|||
|
||||
function parseProjection(): ProjectionNode {
|
||||
const pos = tokenPos();
|
||||
const directionId = parseIdentifier({ message: "projectionDirection" });
|
||||
let direction: "to" | "from";
|
||||
if (directionId.sv !== "from" && directionId.sv !== "to") {
|
||||
let directionId = parseIdentifier({ message: "projectionDirection" });
|
||||
let direction: "to" | "from" | "pre_to" | "pre_from" | "<error>";
|
||||
const modifierIds: IdentifierNode[] = [];
|
||||
let isPre = false;
|
||||
|
||||
if (directionId.sv === "pre") {
|
||||
isPre = true;
|
||||
modifierIds.push(directionId);
|
||||
directionId = parseIdentifier({ message: "projectionDirection" });
|
||||
}
|
||||
if (directionId.sv !== "to" && directionId.sv !== "from") {
|
||||
error({ code: "token-expected", messageId: "projectionDirection" });
|
||||
direction = "from";
|
||||
direction = "<error>";
|
||||
} else if (isPre) {
|
||||
direction = directionId.sv === "to" ? "pre_to" : "pre_from";
|
||||
} else {
|
||||
direction = directionId.sv;
|
||||
}
|
||||
|
||||
let parameters: ProjectionParameterDeclarationNode[];
|
||||
if (token() === Token.OpenParen) {
|
||||
parameters = parseList(ListKind.ProjectionParameter, parseProjectionParameter);
|
||||
} else {
|
||||
parameters = [];
|
||||
}
|
||||
|
||||
parseExpected(Token.OpenBrace);
|
||||
const body: ProjectionStatementItem[] = parseProjectionStatementList();
|
||||
parseExpected(Token.CloseBrace);
|
||||
|
@ -1613,6 +1630,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
|||
body,
|
||||
direction,
|
||||
directionId,
|
||||
modifierIds,
|
||||
parameters,
|
||||
...finishNode(pos),
|
||||
};
|
||||
|
@ -2101,9 +2119,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
|||
| MemberExpressionNode
|
||||
| ProjectionInterfaceSelectorNode
|
||||
| ProjectionModelSelectorNode
|
||||
| ProjectionModelPropertySelectorNode
|
||||
| ProjectionOperationSelectorNode
|
||||
| ProjectionUnionSelectorNode
|
||||
| ProjectionEnumSelectorNode {
|
||||
| ProjectionUnionVariantSelectorNode
|
||||
| ProjectionEnumSelectorNode
|
||||
| ProjectionEnumMemberSelectorNode {
|
||||
const pos = tokenPos();
|
||||
const selectorTok = expectTokenIsOneOf(
|
||||
Token.Identifier,
|
||||
|
@ -2116,7 +2137,27 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
|||
|
||||
switch (selectorTok) {
|
||||
case Token.Identifier:
|
||||
return parseIdentifierOrMemberExpression(undefined, true);
|
||||
const id = parseIdentifierOrMemberExpression(undefined, true);
|
||||
if (id.kind === SyntaxKind.Identifier) {
|
||||
switch (id.sv) {
|
||||
case "modelproperty":
|
||||
return {
|
||||
kind: SyntaxKind.ProjectionModelPropertySelector,
|
||||
...finishNode(pos),
|
||||
};
|
||||
case "unionvariant":
|
||||
return {
|
||||
kind: SyntaxKind.ProjectionUnionVariantSelector,
|
||||
...finishNode(pos),
|
||||
};
|
||||
case "enummember":
|
||||
return {
|
||||
kind: SyntaxKind.ProjectionEnumMemberSelector,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
}
|
||||
return id;
|
||||
case Token.ModelKeyword:
|
||||
nextToken();
|
||||
return {
|
||||
|
@ -2929,6 +2970,7 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
|
|||
case SyntaxKind.Projection:
|
||||
return (
|
||||
visitNode(cb, node.directionId) ||
|
||||
visitEach(cb, node.modifierIds) ||
|
||||
visitEach(cb, node.parameters) ||
|
||||
visitEach(cb, node.body)
|
||||
);
|
||||
|
@ -2969,10 +3011,7 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
|
|||
return visitEach(cb, node.parameters) || visitNode(cb, node.body);
|
||||
case SyntaxKind.ProjectionStatement:
|
||||
return (
|
||||
visitNode(cb, node.id) ||
|
||||
visitNode(cb, node.selector) ||
|
||||
visitNode(cb, node.from) ||
|
||||
visitNode(cb, node.to)
|
||||
visitNode(cb, node.id) || visitNode(cb, node.selector) || visitEach(cb, node.projections)
|
||||
);
|
||||
case SyntaxKind.ProjectionDecoratorReferenceExpression:
|
||||
return visitNode(cb, node.target);
|
||||
|
@ -3006,10 +3045,13 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
|
|||
case SyntaxKind.Identifier:
|
||||
case SyntaxKind.EmptyStatement:
|
||||
case SyntaxKind.ProjectionModelSelector:
|
||||
case SyntaxKind.ProjectionModelPropertySelector:
|
||||
case SyntaxKind.ProjectionUnionSelector:
|
||||
case SyntaxKind.ProjectionUnionVariantSelector:
|
||||
case SyntaxKind.ProjectionInterfaceSelector:
|
||||
case SyntaxKind.ProjectionOperationSelector:
|
||||
case SyntaxKind.ProjectionEnumSelector:
|
||||
case SyntaxKind.ProjectionEnumMemberSelector:
|
||||
case SyntaxKind.VoidKeyword:
|
||||
case SyntaxKind.NeverKeyword:
|
||||
case SyntaxKind.ExternKeyword:
|
||||
|
|
|
@ -116,6 +116,7 @@ export function createProjectionMembers(checker: Checker): {
|
|||
},
|
||||
},
|
||||
ModelProperty: {
|
||||
...createBaseMembers(),
|
||||
name(base) {
|
||||
return createLiteralType(base.name);
|
||||
},
|
||||
|
@ -195,6 +196,7 @@ export function createProjectionMembers(checker: Checker): {
|
|||
},
|
||||
},
|
||||
UnionVariant: {
|
||||
...createBaseMembers(),
|
||||
name(base) {
|
||||
if (typeof base.name === "string") {
|
||||
return createLiteralType(base.name);
|
||||
|
@ -300,12 +302,7 @@ export function createProjectionMembers(checker: Checker): {
|
|||
},
|
||||
},
|
||||
Enum: {
|
||||
projectionSource(base) {
|
||||
return base.projectionSource ?? voidType;
|
||||
},
|
||||
projectionBase(base) {
|
||||
return base.projectionBase || voidType;
|
||||
},
|
||||
...createBaseMembers(),
|
||||
...createNameableMembers(),
|
||||
members(base) {
|
||||
return createType({
|
||||
|
@ -382,6 +379,7 @@ export function createProjectionMembers(checker: Checker): {
|
|||
},
|
||||
},
|
||||
EnumMember: {
|
||||
...createBaseMembers(),
|
||||
name(base) {
|
||||
return createLiteralType(base.name);
|
||||
},
|
||||
|
|
|
@ -100,6 +100,14 @@ export function createProjector(
|
|||
}
|
||||
|
||||
scope.push(type);
|
||||
|
||||
const preProjected = applyPreProjection(type);
|
||||
if (preProjected !== type) {
|
||||
projectedTypes.set(type, preProjected);
|
||||
scope.pop();
|
||||
return preProjected;
|
||||
}
|
||||
|
||||
let projected;
|
||||
switch (type.kind) {
|
||||
case "Namespace":
|
||||
|
@ -144,7 +152,6 @@ export function createProjector(
|
|||
}
|
||||
|
||||
scope.pop();
|
||||
|
||||
return projected;
|
||||
}
|
||||
|
||||
|
@ -281,10 +288,10 @@ export function createProjector(
|
|||
|
||||
projectedTypes.set(model, projectedModel);
|
||||
|
||||
for (const [key, prop] of model.properties) {
|
||||
for (const prop of model.properties.values()) {
|
||||
const projectedProp = projectType(prop);
|
||||
if (projectedProp.kind === "ModelProperty") {
|
||||
properties.set(key, projectedProp);
|
||||
properties.set(projectedProp.name, projectedProp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,7 +387,7 @@ export function createProjector(
|
|||
if (prop.model) {
|
||||
projectedProp.model = projectType(prop.model) as Model;
|
||||
}
|
||||
return projectedProp;
|
||||
return applyProjection(prop, projectedProp);
|
||||
}
|
||||
|
||||
function projectOperation(op: Operation): Type {
|
||||
|
@ -412,10 +419,12 @@ export function createProjector(
|
|||
|
||||
function projectInterface(iface: Interface): Type {
|
||||
const operations = createRekeyableMap<string, Operation>();
|
||||
const sourceInterfaces: Interface[] = [];
|
||||
const decorators = projectDecorators(iface.decorators);
|
||||
const projectedIface = shallowClone(iface, {
|
||||
decorators,
|
||||
operations,
|
||||
sourceInterfaces,
|
||||
});
|
||||
|
||||
if (iface.templateMapper) {
|
||||
|
@ -430,6 +439,10 @@ export function createProjector(
|
|||
}
|
||||
}
|
||||
|
||||
for (const source of iface.sourceInterfaces) {
|
||||
sourceInterfaces.push(projectType(source) as Interface);
|
||||
}
|
||||
|
||||
if (shouldFinishType(iface)) {
|
||||
finishTypeForProgram(projectedProgram, projectedIface);
|
||||
}
|
||||
|
@ -451,10 +464,10 @@ export function createProjector(
|
|||
projectedUnion.templateArguments = mutate(projectedUnion.templateMapper.args);
|
||||
}
|
||||
|
||||
for (const [key, variant] of union.variants) {
|
||||
for (const variant of union.variants.values()) {
|
||||
const projectedVariant = projectType(variant);
|
||||
if (projectedVariant.kind === "UnionVariant" && projectedVariant.type !== neverType) {
|
||||
variants.set(key, projectedVariant);
|
||||
variants.set(projectedVariant.name, projectedVariant);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,7 +489,7 @@ export function createProjector(
|
|||
|
||||
finishTypeForProgram(projectedProgram, projectedVariant);
|
||||
projectedVariant.union = projectType(variant.union) as Union;
|
||||
return projectedVariant;
|
||||
return applyProjection(variant, projectedVariant);
|
||||
}
|
||||
|
||||
function projectTuple(tuple: Tuple) {
|
||||
|
@ -513,14 +526,14 @@ export function createProjector(
|
|||
return applyProjection(e, projectedEnum);
|
||||
}
|
||||
|
||||
function projectEnumMember(e: EnumMember, projectingEnum?: Enum) {
|
||||
function projectEnumMember(e: EnumMember) {
|
||||
const decorators = projectDecorators(e.decorators);
|
||||
const projectedMember = shallowClone(e, {
|
||||
decorators,
|
||||
});
|
||||
finishTypeForProgram(projectedProgram, projectedMember);
|
||||
projectedMember.enum = projectType(e.enum) as Enum;
|
||||
return projectedMember;
|
||||
return applyProjection(e, projectedMember);
|
||||
}
|
||||
|
||||
function projectDecorators(decs: DecoratorApplication[]) {
|
||||
|
@ -589,19 +602,44 @@ export function createProjector(
|
|||
if (projectionsByName.length === 0) continue;
|
||||
const targetNode =
|
||||
projectionApplication.direction === "from"
|
||||
? projectionsByName[0].from!
|
||||
: projectionsByName[0].to!;
|
||||
const projected = checker.project(projectedType, targetNode, projectionApplication.arguments);
|
||||
if (projected !== projectedType) {
|
||||
// override the projected type cache with the returned type
|
||||
projectedTypes.set(baseType, projected);
|
||||
return projected;
|
||||
? projectionsByName[0].from
|
||||
: projectionsByName[0].to;
|
||||
|
||||
if (targetNode) {
|
||||
const projected = checker.project(
|
||||
projectedType,
|
||||
targetNode,
|
||||
projectionApplication.arguments
|
||||
);
|
||||
if (projected !== projectedType) {
|
||||
// override the projected type cache with the returned type
|
||||
projectedTypes.set(baseType, projected);
|
||||
return projected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return projectedType;
|
||||
}
|
||||
|
||||
function applyPreProjection(type: Type): Type {
|
||||
const inScopeProjections = getInScopeProjections();
|
||||
for (const projectionApplication of inScopeProjections) {
|
||||
const projectionsByName = type.projectionsByName(projectionApplication.projectionName);
|
||||
if (projectionsByName.length === 0) continue;
|
||||
const targetNode =
|
||||
projectionApplication.direction === "from"
|
||||
? projectionsByName[0].preFrom
|
||||
: projectionsByName[0].preTo;
|
||||
|
||||
if (targetNode) {
|
||||
return checker.project(type, targetNode, projectionApplication.arguments);
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
function shallowClone<T extends Type>(type: T, additionalProps: Partial<T>): T {
|
||||
const scopeProps: any = {};
|
||||
if ("namespace" in type && type.namespace !== undefined) {
|
||||
|
|
|
@ -282,6 +282,16 @@ export interface Interface extends BaseType, DecoratedType, TemplatedTypeBase {
|
|||
node: InterfaceStatementNode;
|
||||
namespace?: Namespace;
|
||||
|
||||
/**
|
||||
* The interfaces that provide additional operations via `interface extends`.
|
||||
*
|
||||
* Note that despite the same `extends` keyword in source form, this is a
|
||||
* different semantic relationship than the one from {@link Model} to
|
||||
* {@link Model.baseModel}. Operations from extended interfaces are copied
|
||||
* into {@link Interface.operations}.
|
||||
*/
|
||||
sourceInterfaces: Interface[];
|
||||
|
||||
/**
|
||||
* The operations of the interface.
|
||||
*
|
||||
|
@ -700,10 +710,13 @@ export enum SyntaxKind {
|
|||
Projection,
|
||||
ProjectionParameterDeclaration,
|
||||
ProjectionModelSelector,
|
||||
ProjectionModelPropertySelector,
|
||||
ProjectionOperationSelector,
|
||||
ProjectionUnionSelector,
|
||||
ProjectionUnionVariantSelector,
|
||||
ProjectionInterfaceSelector,
|
||||
ProjectionEnumSelector,
|
||||
ProjectionEnumMemberSelector,
|
||||
ProjectionExpressionStatement,
|
||||
ProjectionIfExpression,
|
||||
ProjectionBlockExpression,
|
||||
|
@ -802,10 +815,13 @@ export type Node =
|
|||
| ProjectionStatementItem
|
||||
| ProjectionExpression
|
||||
| ProjectionModelSelectorNode
|
||||
| ProjectionModelPropertySelectorNode
|
||||
| ProjectionInterfaceSelectorNode
|
||||
| ProjectionOperationSelectorNode
|
||||
| ProjectionEnumSelectorNode
|
||||
| ProjectionEnumMemberSelectorNode
|
||||
| ProjectionUnionSelectorNode
|
||||
| ProjectionUnionVariantSelectorNode
|
||||
| ProjectionModelPropertyNode
|
||||
| ProjectionModelSpreadPropertyNode
|
||||
| ProjectionStatementNode
|
||||
|
@ -1287,6 +1303,10 @@ export interface ProjectionModelSelectorNode extends BaseNode {
|
|||
readonly kind: SyntaxKind.ProjectionModelSelector;
|
||||
}
|
||||
|
||||
export interface ProjectionModelPropertySelectorNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionModelPropertySelector;
|
||||
}
|
||||
|
||||
export interface ProjectionInterfaceSelectorNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionInterfaceSelector;
|
||||
}
|
||||
|
@ -1299,10 +1319,18 @@ export interface ProjectionUnionSelectorNode extends BaseNode {
|
|||
readonly kind: SyntaxKind.ProjectionUnionSelector;
|
||||
}
|
||||
|
||||
export interface ProjectionUnionVariantSelectorNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionUnionVariantSelector;
|
||||
}
|
||||
|
||||
export interface ProjectionEnumSelectorNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionEnumSelector;
|
||||
}
|
||||
|
||||
export interface ProjectionEnumMemberSelectorNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionEnumMemberSelector;
|
||||
}
|
||||
|
||||
export type ProjectionStatementItem = ProjectionExpressionStatementNode;
|
||||
|
||||
export interface ProjectionParameterDeclarationNode extends DeclarationNode, BaseNode {
|
||||
|
@ -1400,7 +1428,7 @@ export interface ProjectionBlockExpressionNode extends BaseNode {
|
|||
|
||||
export interface ProjectionLambdaExpressionNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.ProjectionLambdaExpression;
|
||||
readonly parameters: ProjectionLambdaParameterDeclarationNode[];
|
||||
readonly parameters: readonly ProjectionLambdaParameterDeclarationNode[];
|
||||
readonly locals?: SymbolTable;
|
||||
readonly body: ProjectionBlockExpressionNode;
|
||||
}
|
||||
|
@ -1411,10 +1439,11 @@ export interface ProjectionLambdaParameterDeclarationNode extends DeclarationNod
|
|||
|
||||
export interface ProjectionNode extends BaseNode {
|
||||
readonly kind: SyntaxKind.Projection;
|
||||
readonly direction: "to" | "from";
|
||||
readonly direction: "to" | "from" | "pre_to" | "pre_from" | "<error>";
|
||||
readonly directionId: IdentifierNode;
|
||||
readonly modifierIds: readonly IdentifierNode[];
|
||||
readonly parameters: ProjectionParameterDeclarationNode[];
|
||||
readonly body: ProjectionStatementItem[];
|
||||
readonly body: readonly ProjectionStatementItem[];
|
||||
readonly locals?: SymbolTable;
|
||||
}
|
||||
|
||||
|
@ -1422,14 +1451,20 @@ export interface ProjectionStatementNode extends BaseNode, DeclarationNode {
|
|||
readonly kind: SyntaxKind.ProjectionStatement;
|
||||
readonly selector:
|
||||
| ProjectionModelSelectorNode
|
||||
| ProjectionModelPropertySelectorNode
|
||||
| ProjectionInterfaceSelectorNode
|
||||
| ProjectionOperationSelectorNode
|
||||
| ProjectionUnionSelectorNode
|
||||
| ProjectionUnionVariantSelectorNode
|
||||
| ProjectionEnumSelectorNode
|
||||
| ProjectionEnumMemberSelectorNode
|
||||
| MemberExpressionNode
|
||||
| IdentifierNode;
|
||||
readonly to?: ProjectionNode;
|
||||
readonly from?: ProjectionNode;
|
||||
readonly preTo?: ProjectionNode;
|
||||
readonly preFrom?: ProjectionNode;
|
||||
readonly projections: readonly ProjectionNode[];
|
||||
readonly parent?: TypeSpecScriptNode | NamespaceStatementNode;
|
||||
}
|
||||
|
||||
|
|
|
@ -231,14 +231,20 @@ export function printNode(
|
|||
return printProjectionStatement(path as AstPath<ProjectionStatementNode>, options, print);
|
||||
case SyntaxKind.ProjectionModelSelector:
|
||||
return "model";
|
||||
case SyntaxKind.ProjectionModelPropertySelector:
|
||||
return "modelproperty";
|
||||
case SyntaxKind.ProjectionOperationSelector:
|
||||
return "op";
|
||||
case SyntaxKind.ProjectionUnionSelector:
|
||||
return "union";
|
||||
case SyntaxKind.ProjectionUnionVariantSelector:
|
||||
return "unionvariant";
|
||||
case SyntaxKind.ProjectionInterfaceSelector:
|
||||
return "interface";
|
||||
case SyntaxKind.ProjectionEnumSelector:
|
||||
return "enum";
|
||||
case SyntaxKind.ProjectionEnumMemberSelector:
|
||||
return "enummember";
|
||||
case SyntaxKind.Projection:
|
||||
return printProjection(path as AstPath<ProjectionNode>, options, print);
|
||||
case SyntaxKind.ProjectionParameterDeclaration:
|
||||
|
@ -1348,20 +1354,17 @@ function printProjectionStatement(
|
|||
options: TypeSpecPrettierOptions,
|
||||
print: PrettierChildPrint
|
||||
) {
|
||||
const node = path.getValue();
|
||||
const selector = path.call(print, "selector");
|
||||
const id = path.call(print, "id");
|
||||
const to = node.to ? [hardline, path.call(print, "to")] : "";
|
||||
const from = node.from ? [hardline, path.call(print, "from")] : "";
|
||||
const body = [to, from];
|
||||
const projections = path.map(print, "projections").flatMap((x) => [hardline, x]);
|
||||
return [
|
||||
"projection ",
|
||||
selector,
|
||||
"#",
|
||||
id,
|
||||
" {",
|
||||
indent(body),
|
||||
node.to || node.from ? hardline : "",
|
||||
indent(projections),
|
||||
projections.length > 0 ? hardline : "",
|
||||
"}",
|
||||
];
|
||||
}
|
||||
|
@ -1374,7 +1377,15 @@ function printProjection(
|
|||
const node = path.getValue();
|
||||
const params = printProjectionParameters(path, options, print);
|
||||
const body = printProjectionExpressionStatements(path, options, print, "body");
|
||||
return [node.direction, params, " {", indent(body), hardline, "}"];
|
||||
return [
|
||||
...node.modifierIds.flatMap((i) => [i.sv, " "]),
|
||||
node.directionId.sv,
|
||||
params,
|
||||
" {",
|
||||
indent(body),
|
||||
hardline,
|
||||
"}",
|
||||
];
|
||||
}
|
||||
|
||||
function printProjectionParameters(
|
||||
|
|
|
@ -953,6 +953,9 @@ export function createServer(host: ServerHost): Server {
|
|||
break;
|
||||
case SyntaxKind.Projection:
|
||||
classify(node.directionId, SemanticTokenKind.Keyword);
|
||||
for (const modifierId of node.modifierIds) {
|
||||
classify(modifierId, SemanticTokenKind.Keyword);
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.ProjectionParameterDeclaration:
|
||||
classifyReference(node.id, SemanticTokenKind.Parameter);
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
SyntaxKind,
|
||||
UnionStatementNode,
|
||||
} from "../core/types.js";
|
||||
import { expectDiagnosticEmpty } from "../testing/expect.js";
|
||||
|
||||
describe("compiler: binder", () => {
|
||||
let binder: Binder;
|
||||
|
@ -90,7 +91,7 @@ describe("compiler: binder", () => {
|
|||
|
||||
}
|
||||
|
||||
op get1() { }
|
||||
op get1(): void;
|
||||
}
|
||||
|
||||
namespace A {
|
||||
|
@ -98,7 +99,7 @@ describe("compiler: binder", () => {
|
|||
|
||||
}
|
||||
|
||||
op get2() { }
|
||||
op get2(): void;
|
||||
}
|
||||
`;
|
||||
const script = bindTypeSpec(code);
|
||||
|
@ -195,7 +196,7 @@ describe("compiler: binder", () => {
|
|||
op Foo(): void;
|
||||
}
|
||||
|
||||
op Foo(): void
|
||||
op Foo(): void;
|
||||
`;
|
||||
const script = bindTypeSpec(code);
|
||||
strictEqual(script.namespaces.length, 1);
|
||||
|
@ -325,11 +326,11 @@ describe("compiler: binder", () => {
|
|||
to(a) { }
|
||||
}
|
||||
projection model#proj {
|
||||
to(a) { },
|
||||
to(a) { }
|
||||
from(a) { }
|
||||
}
|
||||
projection op#proj {
|
||||
to(a) { },
|
||||
to(a) { }
|
||||
}
|
||||
`;
|
||||
const script = bindTypeSpec(code);
|
||||
|
@ -354,7 +355,7 @@ describe("compiler: binder", () => {
|
|||
const code = `
|
||||
projection model#proj {
|
||||
to() {
|
||||
(a) => 1;
|
||||
(a) => { 1; };
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -420,6 +421,7 @@ describe("compiler: binder", () => {
|
|||
|
||||
function bindTypeSpec(code: string) {
|
||||
const sourceFile = parse(code);
|
||||
expectDiagnosticEmpty(sourceFile.parseDiagnostics);
|
||||
binder.bindSourceFile(sourceFile);
|
||||
return sourceFile;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ok, strictEqual } from "assert";
|
||||
import { deepStrictEqual, ok, strictEqual } from "assert";
|
||||
import { isTemplateDeclaration } from "../../core/type-utils.js";
|
||||
import { Interface, Model, Operation, Type } from "../../core/types.js";
|
||||
import {
|
||||
|
@ -106,6 +106,10 @@ describe("compiler: interfaces", () => {
|
|||
const { Foo } = (await testHost.compile("./")) as {
|
||||
Foo: Interface;
|
||||
};
|
||||
deepStrictEqual(
|
||||
Foo.sourceInterfaces.map((i) => i.name),
|
||||
["Bar"]
|
||||
);
|
||||
strictEqual(Foo.operations.size, 2);
|
||||
ok(Foo.operations.get("foo"));
|
||||
ok(Foo.operations.get("bar"));
|
||||
|
@ -129,6 +133,10 @@ describe("compiler: interfaces", () => {
|
|||
const { Foo } = (await testHost.compile("./")) as {
|
||||
Foo: Interface;
|
||||
};
|
||||
deepStrictEqual(
|
||||
Foo.sourceInterfaces.map((i) => i.name),
|
||||
["Bar", "Baz"]
|
||||
);
|
||||
strictEqual(Foo.operations.size, 3);
|
||||
ok(Foo.operations.get("foo"));
|
||||
ok(Foo.operations.get("bar"));
|
||||
|
|
|
@ -1631,16 +1631,22 @@ model Foo {
|
|||
});
|
||||
|
||||
describe("projections", () => {
|
||||
it("format to and from", () => {
|
||||
it("format projections", () => {
|
||||
assertFormat({
|
||||
code: `
|
||||
projection model#proj
|
||||
{to{} from {}}
|
||||
{pre to{} to{} pre from {} from {}}
|
||||
`,
|
||||
expected: `
|
||||
projection model#proj {
|
||||
pre to {
|
||||
|
||||
}
|
||||
to {
|
||||
|
||||
}
|
||||
pre from {
|
||||
|
||||
}
|
||||
from {
|
||||
|
||||
|
@ -1650,18 +1656,35 @@ projection model#proj {
|
|||
});
|
||||
});
|
||||
|
||||
it("format to and from with args", () => {
|
||||
it("format empty projection on single line", () => {
|
||||
assertFormat({
|
||||
code: `
|
||||
projection model#proj {
|
||||
|
||||
}`,
|
||||
expected: `
|
||||
projection model#proj {}`,
|
||||
});
|
||||
});
|
||||
|
||||
it("format projections with args", () => {
|
||||
assertFormat({
|
||||
code: `
|
||||
projection model#proj
|
||||
{to( val) {} from(
|
||||
{pre to ( val ) {} to( val) {} pre from(
|
||||
|
||||
val) {}}
|
||||
val) {} from (val ){}
|
||||
`,
|
||||
expected: `
|
||||
projection model#proj {
|
||||
pre to(val) {
|
||||
|
||||
}
|
||||
to(val) {
|
||||
|
||||
}
|
||||
pre from(val) {
|
||||
|
||||
}
|
||||
from(val) {
|
||||
|
||||
|
|
|
@ -686,6 +686,8 @@ describe("compiler: parser", () => {
|
|||
parseEach([
|
||||
`projection model#v { to(version) { } }`,
|
||||
`projection model#foo{ from(bar, baz) { } }`,
|
||||
`projection model#v { pre to(version) { } }`,
|
||||
`projection model#foo{ pre from(bar, baz) { } }`,
|
||||
]);
|
||||
});
|
||||
describe("projection expressions", () => {
|
||||
|
@ -745,10 +747,15 @@ describe("compiler: parser", () => {
|
|||
[`projection x#f`, [/'{' expected/]],
|
||||
[`projection x#f {`, [/'}' expected/]],
|
||||
[`projection x#f { asdf`, [/from or to expected/]],
|
||||
[`projection x#f { pre asdf`, [/from or to expected/]],
|
||||
[`projection x#f { to (`, [/'\)' expected/]],
|
||||
[`projection x#f { to @`, [/'{' expected/]],
|
||||
[`projection x#f { to {`, [/} expected/]],
|
||||
[`projection x#f { to {}`, [/'}' expected/]],
|
||||
[`projection x#f { pre to (`, [/'\)' expected/]],
|
||||
[`projection x#f { pre to @`, [/'{' expected/]],
|
||||
[`projection x#f { pre to {`, [/} expected/]],
|
||||
[`projection x#f { pre to {}`, [/'}' expected/]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -211,6 +211,7 @@ const InterfaceUI: FunctionComponent<{ type: Interface }> = ({ type }) => {
|
|||
type={type}
|
||||
properties={{
|
||||
operations: "nested",
|
||||
sourceInterfaces: "ref",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DecoratorContext, getNamespaceFullName, Namespace } from "@typespec/compiler";
|
||||
import { createTestWrapper } from "@typespec/compiler/testing";
|
||||
import { createTestWrapper, expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { deepStrictEqual, strictEqual } from "assert";
|
||||
import { createOpenAPITestHost, createOpenAPITestRunner, openApiFor } from "./test-host.js";
|
||||
|
||||
|
@ -175,4 +175,78 @@ describe("openapi3: versioning", () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe("versioned resource", () => {
|
||||
it("reports diagnostic without crashing for mismatched versions", async () => {
|
||||
const runner = await createOpenAPITestRunner({ withVersioning: true });
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@versioned(Versions)
|
||||
@service
|
||||
namespace DemoService;
|
||||
|
||||
enum Versions {
|
||||
v1,
|
||||
v2
|
||||
}
|
||||
|
||||
model Toy {
|
||||
@key id: string;
|
||||
}
|
||||
|
||||
@added(Versions.v2)
|
||||
model Widget {
|
||||
@key id: string;
|
||||
}
|
||||
|
||||
@error
|
||||
model Error {
|
||||
message: string;
|
||||
}
|
||||
|
||||
@route("/toys")
|
||||
interface Toys extends Resource.ResourceOperations<Toy, Error> {}
|
||||
|
||||
@route("/widgets")
|
||||
interface Widgets extends Resource.ResourceOperations<Widget, Error> {}
|
||||
`);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
});
|
||||
});
|
||||
|
||||
it("succeeds for aligned versions", async () => {
|
||||
const runner = await createOpenAPITestRunner({ withVersioning: true });
|
||||
await runner.compile(`
|
||||
@versioned(Versions)
|
||||
@service
|
||||
namespace DemoService;
|
||||
|
||||
enum Versions {
|
||||
v1,
|
||||
v2
|
||||
}
|
||||
|
||||
model Toy {
|
||||
@key id: string;
|
||||
}
|
||||
|
||||
@added(Versions.v2)
|
||||
model Widget {
|
||||
@key id: string;
|
||||
}
|
||||
|
||||
@error
|
||||
model Error {
|
||||
message: string;
|
||||
}
|
||||
|
||||
@route("/toys")
|
||||
interface Toys extends Resource.ResourceOperations<Toy, Error> {}
|
||||
|
||||
@added(Versions.v2)
|
||||
@route("/widgets")
|
||||
interface Widgets extends Resource.ResourceOperations<Widget, Error> {}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -532,11 +532,14 @@ ProjectionSelector :
|
|||
`interface`
|
||||
`op`
|
||||
`union`
|
||||
`modelproperty`
|
||||
`enummember`
|
||||
`unionvariant`
|
||||
ReferenceExpression
|
||||
|
||||
ProjectionDirection :
|
||||
`to`
|
||||
`from`
|
||||
`pre`? `to`
|
||||
`pre`? `from`
|
||||
|
||||
ProjectionTag :
|
||||
`#` Identifier
|
||||
|
|
|
@ -5,10 +5,12 @@ using TypeSpec.Versioning;
|
|||
|
||||
#suppress "projections-are-experimental"
|
||||
projection op#v {
|
||||
to(version) {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
to(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
|
@ -16,10 +18,12 @@ projection op#v {
|
|||
self::changeReturnType(getReturnTypeBeforeVersion(self, version));
|
||||
};
|
||||
}
|
||||
from(version) {
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
from(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
@ -28,10 +32,12 @@ projection op#v {
|
|||
|
||||
#suppress "projections-are-experimental"
|
||||
projection interface#v {
|
||||
to(version) {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
to(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
|
@ -44,157 +50,190 @@ projection interface#v {
|
|||
};
|
||||
});
|
||||
}
|
||||
from(version) {
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::operations::forEach((operation) => {
|
||||
if !existsAtVersion(operation, version) {
|
||||
self::addOperation(operation::name, operation::parameters, operation::returnType);
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
from(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::operations::forEach((operation) => {
|
||||
if !existsAtVersion(operation, version) {
|
||||
self::addOperation(operation::name, operation::parameters, operation::returnType);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#suppress "projections-are-experimental"
|
||||
projection union#v {
|
||||
to(version) {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
|
||||
self::variants::forEach((variant) => {
|
||||
if !existsAtVersion(variant, version) {
|
||||
self::deleteVariant(variant::name);
|
||||
} else if hasDifferentNameAtVersion(variant, version) {
|
||||
self::renameVariant(variant::name, getNameAtVersion(variant, version));
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
to(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
|
||||
self::variants::forEach((variant) => {
|
||||
if hasDifferentNameAtVersion(variant, version) {
|
||||
self::renameVariant(variant::name, getNameAtVersion(variant, version));
|
||||
};
|
||||
});
|
||||
}
|
||||
from(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::variants::forEach((variant) => {
|
||||
if !existsAtVersion(variant, version) {
|
||||
self::addVariant(variant::name, variant::type);
|
||||
} else if hasDifferentNameAtVersion(variant, version) {
|
||||
self::renameVariant(getNameAtVersion(variant, version), variant::name);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#suppress "projections-are-experimental"
|
||||
projection unionvariant#v {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::variants::forEach((variant) => {
|
||||
if !existsAtVersion(variant, version) {
|
||||
self::addVariant(variant::name, variant::type);
|
||||
} else if hasDifferentNameAtVersion(variant, version) {
|
||||
self::renameVariant(getNameAtVersion(variant, version), variant::name);
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#suppress "projections-are-experimental"
|
||||
projection model#v {
|
||||
to(version) {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
}
|
||||
to(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
self::properties::forEach((p) => {
|
||||
if hasDifferentNameAtVersion(p, version) {
|
||||
self::renameProperty(p::name, getNameAtVersion(p, version));
|
||||
};
|
||||
|
||||
self::properties::forEach((p) => {
|
||||
if !existsAtVersion(p, version) {
|
||||
self::deleteProperty(p::name);
|
||||
};
|
||||
if madeOptionalAfter(p, version) {
|
||||
p::setOptional(false);
|
||||
};
|
||||
|
||||
if hasDifferentNameAtVersion(p, version) {
|
||||
self::renameProperty(p::name, getNameAtVersion(p, version));
|
||||
};
|
||||
|
||||
if madeOptionalAfter(p, version) {
|
||||
p::setOptional(false);
|
||||
};
|
||||
|
||||
if hasDifferentTypeAtVersion(p, version) {
|
||||
self::changePropertyType(p::name, getTypeBeforeVersion(p, version));
|
||||
};
|
||||
});
|
||||
if hasDifferentTypeAtVersion(p, version) {
|
||||
self::changePropertyType(p::name, getTypeBeforeVersion(p, version));
|
||||
};
|
||||
});
|
||||
}
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::properties::forEach((p) => {
|
||||
if !existsAtVersion(p, version) {
|
||||
self::addProperty(p::name, p::type);
|
||||
};
|
||||
|
||||
self::projectionBase::properties::forEach((p) => {
|
||||
if !existsAtVersion(p, version) {
|
||||
self::addProperty(p::name, p::type);
|
||||
};
|
||||
if hasDifferentNameAtVersion(p, version) {
|
||||
self::renameProperty(getNameAtVersion(p, version), p::name);
|
||||
};
|
||||
|
||||
if hasDifferentNameAtVersion(p, version) {
|
||||
self::renameProperty(getNameAtVersion(p, version), p::name);
|
||||
};
|
||||
if madeOptionalAfter(p, version) {
|
||||
p::setOptional(true);
|
||||
};
|
||||
|
||||
if madeOptionalAfter(p, version) {
|
||||
p::setOptional(true);
|
||||
};
|
||||
if hasDifferentTypeAtVersion(p, version) {
|
||||
self::changePropertyType(p::name, p::type);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if hasDifferentTypeAtVersion(p, version) {
|
||||
self::changePropertyType(p::name, p::type);
|
||||
};
|
||||
});
|
||||
#suppress "projections-are-experimental"
|
||||
projection modelproperty#v {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#suppress "projections-are-experimental"
|
||||
projection enum#v {
|
||||
to(version) {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
}
|
||||
to(version) {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(getNameAtVersion(self, version));
|
||||
};
|
||||
|
||||
self::members::forEach((m) => {
|
||||
if hasDifferentNameAtVersion(m, version) {
|
||||
self::renameMember(m::name, getNameAtVersion(m, version));
|
||||
};
|
||||
|
||||
self::members::forEach((m) => {
|
||||
if !existsAtVersion(m, version) {
|
||||
self::deleteMember(m::name);
|
||||
};
|
||||
|
||||
if hasDifferentNameAtVersion(m, version) {
|
||||
self::renameMember(m::name, getNameAtVersion(m, version));
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
} else {
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
if hasDifferentNameAtVersion(self, version) {
|
||||
self::rename(self::projectionBase::name);
|
||||
};
|
||||
|
||||
self::projectionBase::members::forEach((m) => {
|
||||
if !existsAtVersion(m, version, self::projectionBase) {
|
||||
self::addMember(m::name, m::type);
|
||||
};
|
||||
|
||||
self::projectionBase::members::forEach((m) => {
|
||||
if !existsAtVersion(m, version, self::projectionBase) {
|
||||
self::addMember(m::name, m::type);
|
||||
};
|
||||
if hasDifferentNameAtVersion(m, version, self::projectionBase) {
|
||||
self::renameMember(getNameAtVersion(m, version), m::name);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if hasDifferentNameAtVersion(m, version, self::projectionBase) {
|
||||
self::renameMember(getNameAtVersion(m, version), m::name);
|
||||
};
|
||||
});
|
||||
#suppress "projections-are-experimental"
|
||||
projection enummember#v {
|
||||
pre to(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
pre from(version) {
|
||||
if !existsAtVersion(self, version) {
|
||||
return never;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,14 +57,23 @@ export function $onValidate(program: Program) {
|
|||
}
|
||||
},
|
||||
union: (union) => {
|
||||
// If this is an instantiated type we don't want to keep the mapping.
|
||||
if (isTemplateInstance(union)) {
|
||||
return;
|
||||
}
|
||||
if (union.namespace === undefined) {
|
||||
return;
|
||||
}
|
||||
for (const option of union.options.values()) {
|
||||
addDependency(union.namespace, option);
|
||||
for (const variant of union.variants.values()) {
|
||||
addDependency(union.namespace, variant.type);
|
||||
}
|
||||
},
|
||||
operation: (op) => {
|
||||
// If this is an instantiated type we don't want to keep the mapping.
|
||||
if (isTemplateInstance(op)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = op.namespace ?? op.interface?.namespace;
|
||||
addDependency(namespace, op.parameters);
|
||||
addDependency(namespace, op.returnType);
|
||||
|
@ -73,7 +82,13 @@ export function $onValidate(program: Program) {
|
|||
// Validate model -> property have correct versioning
|
||||
validateTargetVersionCompatible(program, op.interface, op, { isTargetADependent: true });
|
||||
}
|
||||
validateTargetVersionCompatible(program, op, op.returnType);
|
||||
|
||||
validateReference(program, op, op.returnType);
|
||||
},
|
||||
interface: (iface) => {
|
||||
for (const source of iface.sourceInterfaces) {
|
||||
validateReference(program, iface, source);
|
||||
}
|
||||
},
|
||||
namespace: (namespace) => {
|
||||
const versionedNamespace = findVersionedNamespace(program, namespace);
|
||||
|
@ -206,11 +221,24 @@ interface IncompatibleVersionValidateOptions {
|
|||
function validateReference(program: Program, source: Type, target: Type) {
|
||||
validateTargetVersionCompatible(program, source, target);
|
||||
|
||||
if (target.kind === "Model" && target.templateArguments) {
|
||||
for (const param of target.templateArguments) {
|
||||
if ("templateMapper" in target) {
|
||||
for (const param of target.templateMapper?.args ?? []) {
|
||||
validateTargetVersionCompatible(program, source, param);
|
||||
}
|
||||
}
|
||||
|
||||
switch (target.kind) {
|
||||
case "Union":
|
||||
for (const variant of target.variants.values()) {
|
||||
validateTargetVersionCompatible(program, source, variant.type);
|
||||
}
|
||||
break;
|
||||
case "Tuple":
|
||||
for (const value of target.values) {
|
||||
validateTargetVersionCompatible(program, source, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailabilityMapWithParentInfo(
|
||||
|
|
|
@ -3,20 +3,23 @@ import {
|
|||
createTestWrapper,
|
||||
expectDiagnosticEmpty,
|
||||
expectDiagnostics,
|
||||
TestHost,
|
||||
} from "@typespec/compiler/testing";
|
||||
import { createVersioningTestHost, createVersioningTestRunner } from "./test-host.js";
|
||||
|
||||
describe("versioning: validate incompatible references", () => {
|
||||
let runner: BasicTestRunner;
|
||||
let host: TestHost;
|
||||
const imports: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
const host = await createVersioningTestHost();
|
||||
host = await createVersioningTestHost();
|
||||
runner = createTestWrapper(host, {
|
||||
wrapper: (code) => `
|
||||
import "@typespec/versioning";
|
||||
|
||||
${imports.map((i) => `import "${i}";`).join("\n")}
|
||||
using TypeSpec.Versioning;
|
||||
|
||||
|
||||
@versioned(Versions)
|
||||
namespace TestService {
|
||||
enum Versions {v1, v2, v3, v4}
|
||||
|
@ -262,7 +265,7 @@ describe("versioning: validate incompatible references", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("model template arguments", () => {
|
||||
describe("complex type references", () => {
|
||||
it("emit diagnostic when using versioned model as template argument in non versioned property", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@added(Versions.v2)
|
||||
|
@ -280,17 +283,55 @@ describe("versioning: validate incompatible references", () => {
|
|||
"'TestService.Bar.foo' is referencing versioned type 'TestService.Versioned' but is not versioned itself.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("interface operations", () => {
|
||||
it("succeed when unversioned interface has versioned operation", async () => {
|
||||
it("emit diagnostic when using versioned union variant in non versioned operation return type", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@added(Versions.v2)
|
||||
model Versioned {}
|
||||
op test(): Versioned | string;
|
||||
`);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
message:
|
||||
"'TestService.test' is referencing versioned type 'TestService.Versioned' but is not versioned itself.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when using versioned array element in non versioned operation return type", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@added(Versions.v2)
|
||||
model Versioned {}
|
||||
op test(): Versioned[];
|
||||
`);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
message:
|
||||
"'TestService.test' is referencing versioned type 'TestService.Versioned' but is not versioned itself.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when using versioned tuple element in non versioned operation return type", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@added(Versions.v2)
|
||||
model Versioned {}
|
||||
op test(): [Versioned, string];
|
||||
`);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
message:
|
||||
"'TestService.test' is referencing versioned type 'TestService.Versioned' but is not versioned itself.",
|
||||
});
|
||||
});
|
||||
|
||||
describe("interface operations", () => {
|
||||
it("succeed when unversioned interface has versioned operation", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
interface Bar {
|
||||
@added(Versions.v2)
|
||||
foo(): string;
|
||||
}
|
||||
`);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when operation was added before interface itself", async () => {
|
||||
|
@ -337,6 +378,71 @@ describe("versioning: validate incompatible references", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("interface templates", () => {
|
||||
beforeEach(() => {
|
||||
imports.push("./lib.tsp");
|
||||
host.addTypeSpecFile(
|
||||
"lib.tsp",
|
||||
`
|
||||
namespace Lib;
|
||||
interface Ops<T extends object> {
|
||||
get(): T[];
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
it("emit diagnostic when extending interface with versioned type argument from unversioned interface", async () => {
|
||||
const diagnostics = await runner.diagnose(
|
||||
`
|
||||
@added(Versions.v2)
|
||||
model Widget {
|
||||
id: string;
|
||||
}
|
||||
interface WidgetService extends Lib.Ops<Widget> {}
|
||||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
message:
|
||||
"'TestService.WidgetService' is referencing versioned type 'TestService.Widget' but is not versioned itself.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when extending interface with versioned type argument added after interface", async () => {
|
||||
const diagnostics = await runner.diagnose(
|
||||
`
|
||||
@added(Versions.v2)
|
||||
model Widget {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@added(Versions.v1)
|
||||
interface WidgetService extends Lib.Ops<Widget> {}
|
||||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/versioning/incompatible-versioned-reference",
|
||||
message:
|
||||
"'TestService.WidgetService' was added on version 'v1' but referencing type 'TestService.Widget' added in version 'v2'.",
|
||||
});
|
||||
});
|
||||
|
||||
it("succeed when extending interface with versioned type argument added before interface", async () => {
|
||||
const diagnostics = await runner.diagnose(
|
||||
`
|
||||
@added(Versions.v2)
|
||||
model Widget {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@added(Versions.v2)
|
||||
interface WidgetService extends Lib.Ops<Widget> {}
|
||||
`
|
||||
);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with @useDependency", () => {
|
||||
let runner: BasicTestRunner;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче