Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---

Add support for inline named model expressions in property type positions
25 changes: 23 additions & 2 deletions grammars/typespec.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@
{
"include": "#tuple-expression"
},
{
"include": "#inline-named-model-expression"
},
{
"include": "#model-expression"
},
Expand Down Expand Up @@ -432,6 +435,24 @@
}
]
},
"inline-named-model-expression": {
"name": "meta.inline-named-model-expression.typespec",
"begin": "\\b(model)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*",
"beginCaptures": {
"1": {
"name": "keyword.other.tsp"
},
"2": {
"name": "entity.name.type.tsp"
}
},
"end": "(?<=\\})",
"patterns": [
{
"include": "#model-expression"
}
]
},
"interface-body": {
"name": "meta.interface-body.typespec",
"begin": "\\{",
Expand Down Expand Up @@ -598,7 +619,7 @@
"name": "string.quoted.double.tsp"
}
},
"end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)",
"end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|op|using|import|enum|alias|union|interface|dec|fn)\\b)",
"patterns": [
{
"include": "#token"
Expand Down Expand Up @@ -1165,7 +1186,7 @@
"name": "keyword.operator.type.annotation.tsp"
}
},
"end": "(?=,|;|@|\\)|\\}|=|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)",
"end": "(?=,|;|@|\\)|\\}|=|\\b(?:namespace|op|using|import|enum|alias|union|interface|dec|fn)\\b)",
"patterns": [
{
"include": "#expression"
Expand Down
25 changes: 24 additions & 1 deletion packages/compiler/src/core/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,30 @@ export function createBinder(program: Program): Binder {
}

function bindModelExpression(node: ModelExpressionNode) {
bindSymbol(node, SymbolFlags.Model);
if (node.id) {
// When the model expression has a name, declare it in the enclosing namespace/script scope
// so it can be referenced by name (e.g. from augment decorators).
const prevScope = scope;
scope = getEnclosingDeclarationScope();
declareSymbol(node as unknown as Declaration, SymbolFlags.Model | SymbolFlags.Declaration);
scope = prevScope;
} else {
bindSymbol(node, SymbolFlags.Model);
}
}

function getEnclosingDeclarationScope(): ScopeNode {
let current: Node | undefined = parentNode;
while (current) {
if (
current.kind === SyntaxKind.TypeSpecScript ||
current.kind === SyntaxKind.NamespaceStatement
) {
return current as ScopeNode;
}
current = current.parent;
}
return scope;
}

function bindModelProperty(node: ModelPropertyNode) {
Expand Down
25 changes: 24 additions & 1 deletion packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3909,7 +3909,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return links.declaredType as any;
}

const type = initModel(node);
const decorators: DecoratorApplication[] = [];
const type: Model = createType({
kind: "Model",
node,
name: node.id ? node.id.sv : "",
namespace: getParentNamespaceType(node),
properties: createRekeyableMap<string, ModelProperty>(),
decorators,
derivedModels: [],
sourceModels: [],
});
const properties = type.properties;
linkType(ctx, links, type);
linkMapper(type, ctx.mapper);
Expand All @@ -3921,6 +3931,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
type,
() => {
checkModelProperties(ctx, node, properties, type);

if (node.id) {
// Named inline models support augment decorators
const sym = getMergedSymbol(node.symbol);
const augmentDecoratorNodes = resolver.getAugmentDecoratorsForSym(sym);
for (const decNode of augmentDecoratorNodes) {
const decorator = checkDecoratorApplication(ctx, type, decNode);
if (decorator) {
decorators.unshift(decorator);
}
}
}

finishType(type, { skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration) });
},
);
Expand Down
21 changes: 20 additions & 1 deletion packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
return parseNumericLiteral();
case Token.OpenBrace:
return parseModelExpression();
case Token.ModelKeyword:
return parseInlineNamedModelExpression();
case Token.OpenBracket:
return parseTupleExpression();
case Token.OpenParen:
Expand Down Expand Up @@ -1775,6 +1777,23 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
};
}

function parseInlineNamedModelExpression(): ModelExpressionNode {
const pos = tokenPos();
parseExpected(Token.ModelKeyword);
const id = parseIdentifier();
const { items: properties, range: bodyRange } = parseList(
ListKind.ModelProperties,
parseModelPropertyOrSpread,
);
return {
kind: SyntaxKind.ModelExpression,
id,
properties,
bodyRange,
...finishNode(pos),
};
}

function parseObjectLiteral(): ObjectLiteralNode {
const pos = tokenPos();
const { items: properties, range: bodyRange } = parseList(
Expand Down Expand Up @@ -2950,7 +2969,7 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
case SyntaxKind.MemberExpression:
return visitNode(cb, node.base) || visitNode(cb, node.id);
case SyntaxKind.ModelExpression:
return visitEach(cb, node.properties);
return visitNode(cb, node.id) || visitEach(cb, node.properties);
case SyntaxKind.ModelProperty:
return (
visitEach(cb, node.decorators) ||
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,7 @@ export interface EmptyStatementNode extends BaseNode {

export interface ModelExpressionNode extends BaseNode {
readonly kind: SyntaxKind.ModelExpression;
readonly id?: IdentifierNode;
readonly properties: (ModelPropertyNode | ModelSpreadPropertyNode)[];
readonly bodyRange: TextRange;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/src/formatter/print/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,8 +924,9 @@ export function printModelExpression(
) {
const inBlock = isModelExpressionInBlock(path);
const node = path.node;
if (inBlock) {
return group(printModelPropertiesBlock(path, options, print));
const prefix = node.id ? ["model ", path.call(print, "id"), " "] : [];
if (node.id || inBlock) {
return group([...prefix, printModelPropertiesBlock(path, options, print)]);
} else {
const properties =
node.properties.length === 0
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/server/classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] {
case SyntaxKind.ModelStatement:
classify(node.id, SemanticTokenKind.Struct);
break;
case SyntaxKind.ModelExpression:
if (node.id) {
classify(node.id, SemanticTokenKind.Struct);
}
break;
case SyntaxKind.ScalarStatement:
classify(node.id, SemanticTokenKind.Type);
break;
Expand Down
19 changes: 17 additions & 2 deletions packages/compiler/src/server/tmlanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ const qualifiedIdentifier = `\\b${identifierStart}(?:${identifierContinue}|\\.${
const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"';
const modifierKeyword = `\\b(?:extern)\\b`;
const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b`;
const statementKeywordExceptModel = `\\b(?:namespace|op|using|import|enum|alias|union|interface|dec|fn)\\b`;
const universalEnd = `(?=,|;|@|#[a-z]|\\)|\\}|${modifierKeyword}|${statementKeyword})`;
const universalEndExceptModel = `(?=,|;|@|#[a-z]|\\)|\\}|${modifierKeyword}|${statementKeywordExceptModel})`;
const universalEndExceptComma = `(?=;|@|\\)|\\}|${modifierKeyword}|${statementKeyword})`;

/**
* Universal end with extra end char: `=`
*/
const expressionEnd = `(?=,|;|@|\\)|\\}|=|${statementKeyword})`;
const expressionEnd = `(?=,|;|@|\\)|\\}|=|${statementKeywordExceptModel})`;
const hexNumber = "\\b(?<!\\$)0(?:x|X)[0-9a-fA-F][0-9a-fA-F_]*(n)?\\b(?!\\$)";
const binaryNumber = "\\b(?<!\\$)0(?:b|B)[01][01_]*(n)?\\b(?!\\$)";
const decimalNumber =
Expand Down Expand Up @@ -458,7 +460,7 @@ const modelProperty: BeginEndRule = {
"1": { scope: "variable.name.tsp" },
"2": { scope: "string.quoted.double.tsp" },
},
end: universalEnd,
end: universalEndExceptModel,
patterns: [token, typeAnnotation, operatorAssignment, expression],
};

Expand Down Expand Up @@ -509,6 +511,18 @@ const modelExpression: BeginEndRule = {
],
};

const inlineNamedModelExpression: BeginEndRule = {
key: "inline-named-model-expression",
scope: meta,
begin: `\\b(model)\\b\\s+(${identifier})\\s*`,
beginCaptures: {
"1": { scope: "keyword.other.tsp" },
"2": { scope: "entity.name.type.tsp" },
},
end: `(?<=\\})`,
patterns: [modelExpression],
};

const objectLiteralProperty: BeginEndRule = {
key: "object-literal-property",
scope: meta,
Expand Down Expand Up @@ -925,6 +939,7 @@ expression.patterns = [
objectLiteral,
tupleLiteral,
tupleExpression,
inlineNamedModelExpression,
modelExpression,
callExpression,
identifierExpression,
Expand Down
Loading
Loading