- 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:
Nick Guerrera 2023-04-07 13:12:54 -05:00 коммит произвёл GitHub
Родитель a99db17d47
Коммит dfa63a26d6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 716 добавлений и 205 удалений

Просмотреть файл

@ -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;