Skip to content

Commit

Permalink
improve completion and onhover in VSCode for decorator and extended m…
Browse files Browse the repository at this point in the history
…odel (#3280)

add following feature
1. support completion when user typing model expression as decorator
argument value
2. support completion for extended model
3. support showing decorator model argument prop information when
hovering on model expression as decorator argument value

closes #3130, closes #2228, closes #1956

---------

Co-authored-by: Timothee Guerin <timothee.guerin@outlook.com>
  • Loading branch information
RodgeFu and timotheeguerin committed May 17, 2024
1 parent c7a75ac commit b7f6948
Show file tree
Hide file tree
Showing 11 changed files with 1,664 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Support completion for Model with extended properties

Example
```tsp
model Device {
name: string;
description: string;
}
model Phone extends Device {
} | [name]
| [description]
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Support completion for object values and model expression properties.

Example
```tsp
model User {
name: string;
age: int32;
address: string;
}
const user: User = #{name: "Bob", ┆}
| [age]
| [address]
```

276 changes: 275 additions & 1 deletion packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { numericRanges } from "./numeric-ranges.js";
import { Numeric } from "./numeric.js";
import {
exprIsBareIdentifier,
getFirstAncestor,
getIdentifierContext,
hasParseError,
visitChildren,
Expand Down Expand Up @@ -130,6 +131,7 @@ import {
NumericLiteralNode,
NumericValue,
ObjectLiteralNode,
ObjectLiteralPropertyNode,
ObjectValue,
ObjectValuePropertyDescriptor,
Operation,
Expand Down Expand Up @@ -2711,6 +2713,16 @@ export function createChecker(program: Program): Checker {
const { node, kind } = getIdentifierContext(id);

switch (kind) {
case IdentifierKind.ModelExpressionProperty:
case IdentifierKind.ObjectLiteralProperty:
const model = getReferencedModel(node as ModelPropertyNode | ObjectLiteralPropertyNode);
if (model) {
sym = getMemberSymbol(model.node!.symbol, id.sv);
} else {
return undefined;
}
break;
case IdentifierKind.ModelStatementProperty:
case IdentifierKind.Declaration:
if (node.symbol && (!isTemplatedNode(node) || mapper === undefined)) {
sym = getMergedSymbol(node.symbol);
Expand Down Expand Up @@ -2792,6 +2804,219 @@ export function createChecker(program: Program): Checker {
return (resolved?.declarations.filter((n) => isTemplatedNode(n)) ?? []) as TemplateableNode[];
}

function getReferencedModel(
propertyNode: ObjectLiteralPropertyNode | ModelPropertyNode
): Model | undefined {
type ModelOrArrayValueNode = ArrayLiteralNode | ObjectLiteralNode;
type ModelOrArrayTypeNode = ModelExpressionNode | TupleExpressionNode;
type ModelOrArrayNode = ModelOrArrayValueNode | ModelOrArrayTypeNode;
type PathSeg = { propertyName?: string; tupleIndex?: number };
const isModelOrArrayValue = (n: Node | undefined) =>
n?.kind === SyntaxKind.ArrayLiteral || n?.kind === SyntaxKind.ObjectLiteral;
const isModelOrArrayType = (n: Node | undefined) =>
n?.kind === SyntaxKind.ModelExpression || n?.kind === SyntaxKind.TupleExpression;
const isModelOrArray = (n: Node | undefined) => isModelOrArrayValue(n) || isModelOrArrayType(n);

const path: PathSeg[] = [];
let preNode: Node | undefined;
const foundNode = getFirstAncestor(propertyNode, (n) => {
pushToModelPath(n, preNode, path);
preNode = n;
return (
(isModelOrArray(n) &&
(n.parent?.kind === SyntaxKind.TemplateParameterDeclaration ||
n.parent?.kind === SyntaxKind.DecoratorExpression)) ||
(isModelOrArrayValue(n) &&
(n.parent?.kind === SyntaxKind.CallExpression ||
n.parent?.kind === SyntaxKind.ConstStatement))
);
});

let refType: Type | undefined;
switch (foundNode?.parent?.kind) {
case SyntaxKind.TemplateParameterDeclaration:
refType = getReferencedTypeFromTemplateDeclaration(foundNode as ModelOrArrayNode);
break;
case SyntaxKind.DecoratorExpression:
refType = getReferencedTypeFromDecoratorArgument(foundNode as ModelOrArrayNode);
break;
case SyntaxKind.CallExpression:
refType = getReferencedTypeFromScalarConstructor(foundNode as ModelOrArrayValueNode);
break;
case SyntaxKind.ConstStatement:
refType = getReferencedTypeFromConstAssignment(foundNode as ModelOrArrayValueNode);
break;
}
return refType?.kind === "Model" || refType?.kind === "Tuple"
? getNestedModel(refType, path)
: undefined;

function pushToModelPath(node: Node, preNode: Node | undefined, path: PathSeg[]) {
if (node.kind === SyntaxKind.ArrayLiteral || node.kind === SyntaxKind.TupleExpression) {
const index = node.values.findIndex((n) => n === preNode);
if (index >= 0) {
path.unshift({ tupleIndex: index });
} else {
compilerAssert(false, "not expected, can't find child from the parent?");
}
}
if (
node.kind === SyntaxKind.ModelProperty ||
node.kind === SyntaxKind.ObjectLiteralProperty
) {
path.unshift({ propertyName: node.id.sv });
}
}

function getNestedModel(
modelOrTuple: Model | Tuple | undefined,
path: PathSeg[]
): Model | undefined {
let cur: Type | undefined = modelOrTuple;
for (const seg of path) {
switch (cur?.kind) {
case "Tuple":
if (
seg.tupleIndex !== undefined &&
seg.tupleIndex >= 0 &&
seg.tupleIndex < cur.values.length
) {
cur = cur.values[seg.tupleIndex];
} else {
return undefined;
}
break;
case "Model":
if (cur.name === "Array" && seg.tupleIndex !== undefined) {
cur = cur.templateMapper?.args[0] as Model;
} else if (cur.name !== "Array" && seg.propertyName) {
cur = cur.properties.get(seg.propertyName)?.type;
} else {
return undefined;
}
break;
default:
return undefined;
}
}
return cur?.kind === "Model" ? cur : undefined;
}

function getReferencedTypeFromTemplateDeclaration(dftNode: ModelOrArrayNode): Type | undefined {
const templateParmaeterDeclNode = dftNode?.parent;
if (
templateParmaeterDeclNode?.kind !== SyntaxKind.TemplateParameterDeclaration ||
!templateParmaeterDeclNode.constraint ||
!templateParmaeterDeclNode.default ||
templateParmaeterDeclNode.default !== dftNode
) {
return undefined;
}

let constraintType: Type | undefined;
if (
isModelOrArrayValue(dftNode) &&
templateParmaeterDeclNode.constraint.kind === SyntaxKind.ValueOfExpression
) {
constraintType = program.checker.getTypeForNode(
templateParmaeterDeclNode.constraint.target
);
} else if (
isModelOrArrayType(dftNode) &&
templateParmaeterDeclNode.constraint.kind !== SyntaxKind.ValueOfExpression
) {
constraintType = program.checker.getTypeForNode(templateParmaeterDeclNode.constraint);
}

return constraintType;
}

function getReferencedTypeFromScalarConstructor(
argNode: ModelOrArrayValueNode
): Type | undefined {
const callExpNode = argNode?.parent;
if (callExpNode?.kind !== SyntaxKind.CallExpression) {
return undefined;
}

const ctorType = checkCallExpressionTarget(callExpNode, undefined);

if (ctorType?.kind !== "ScalarConstructor") {
return undefined;
}

const argIndex = callExpNode.arguments.findIndex((n) => n === argNode);
if (argIndex < 0 || argIndex >= ctorType.parameters.length) {
return undefined;
}
const arg = ctorType.parameters[argIndex];

return arg.type;
}

function getReferencedTypeFromConstAssignment(
valueNode: ModelOrArrayValueNode
): Type | undefined {
const constNode = valueNode?.parent;
if (
!constNode ||
constNode.kind !== SyntaxKind.ConstStatement ||
!constNode.type ||
constNode.value !== valueNode
) {
return undefined;
}

return program.checker.getTypeForNode(constNode.type);
}

function getReferencedTypeFromDecoratorArgument(
decArgNode: ModelOrArrayNode
): Type | undefined {
const decNode = decArgNode?.parent;
if (decNode?.kind !== SyntaxKind.DecoratorExpression) {
return undefined;
}

const decSym = program.checker.resolveIdentifier(
decNode.target.kind === SyntaxKind.MemberExpression ? decNode.target.id : decNode.target
);
if (!decSym) {
return undefined;
}

const decDecl: DecoratorDeclarationStatementNode | undefined = decSym.declarations.find(
(x): x is DecoratorDeclarationStatementNode =>
x.kind === SyntaxKind.DecoratorDeclarationStatement
);
if (!decDecl) {
return undefined;
}

const decType = program.checker.getTypeForNode(decDecl);
compilerAssert(decType.kind === "Decorator", "Expected type to be a Decorator.");

const argIndex = decNode.arguments.findIndex((n) => n === decArgNode);
if (argIndex < 0 || argIndex >= decType.parameters.length) {
return undefined;
}
const decArg = decType.parameters[argIndex];

let type: Type | undefined;
if (isModelOrArrayValue(decArgNode)) {
type = decArg.type.valueType;
} else if (isModelOrArrayType(decArgNode)) {
type = decArg.type.type ?? decArg.type.valueType;
} else {
compilerAssert(
false,
"not expected node type to get reference model from decorator argument"
);
}
return type;
}
}

function resolveCompletions(identifier: IdentifierNode): Map<string, TypeSpecCompletionItem> {
const completions = new Map<string, TypeSpecCompletionItem>();
const { kind, node: ancestor } = getIdentifierContext(identifier);
Expand All @@ -2801,6 +3026,9 @@ export function createChecker(program: Program): Checker {
case IdentifierKind.Decorator:
case IdentifierKind.Function:
case IdentifierKind.TypeReference:
case IdentifierKind.ModelExpressionProperty:
case IdentifierKind.ModelStatementProperty:
case IdentifierKind.ObjectLiteralProperty:
break; // supported
case IdentifierKind.Other:
return completions; // not implemented
Expand All @@ -2825,7 +3053,49 @@ export function createChecker(program: Program): Checker {
compilerAssert(false, "Unreachable");
}

if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) {
if (kind === IdentifierKind.ModelStatementProperty) {
const model = ancestor.parent as ModelStatementNode;
const modelType = program.checker.getTypeForNode(model) as Model;
const baseType = modelType.baseModel;
const baseNode = baseType?.node;
if (!baseNode) {
return completions;
}
for (const prop of baseType.properties.values()) {
if (identifier.sv === prop.name || !modelType.properties.has(prop.name)) {
const sym = getMemberSymbol(baseNode.symbol, prop.name);
if (sym) {
addCompletion(prop.name, sym);
}
}
}
} else if (
kind === IdentifierKind.ModelExpressionProperty ||
kind === IdentifierKind.ObjectLiteralProperty
) {
const model = getReferencedModel(ancestor as ModelPropertyNode | ObjectLiteralPropertyNode);
if (!model) {
return completions;
}
const curModelNode = ancestor.parent as ModelExpressionNode | ObjectLiteralNode;

for (const prop of walkPropertiesInherited(model)) {
if (
identifier.sv === prop.name ||
!curModelNode.properties.find(
(p) =>
(p.kind === SyntaxKind.ModelProperty ||
p.kind === SyntaxKind.ObjectLiteralProperty) &&
p.id.sv === prop.name
)
) {
const sym = getMemberSymbol(model.node!.symbol, prop.name);
if (sym) {
addCompletion(prop.name, sym);
}
}
}
} else if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) {
let base = resolveTypeReferenceSym(identifier.parent.base, undefined, false);
if (base) {
if (base.flags & SymbolFlags.Alias) {
Expand Down Expand Up @@ -2930,6 +3200,10 @@ export function createChecker(program: Program): Checker {

function shouldAddCompletion(sym: Sym): boolean {
switch (kind) {
case IdentifierKind.ModelExpressionProperty:
case IdentifierKind.ModelStatementProperty:
case IdentifierKind.ObjectLiteralProperty:
return !!(sym.flags & SymbolFlags.ModelProperty);
case IdentifierKind.Decorator:
// Only return decorators and namespaces when completing decorator
return !!(sym.flags & (SymbolFlags.Decorator | SymbolFlags.Namespace));
Expand Down
Loading

0 comments on commit b7f6948

Please sign in to comment.