Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve completion and onhover in VSCode for decorator and extended model #3280

Merged
merged 24 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
08be7fe
add completion for decorator model -arg
RodgeFu May 2, 2024
731845e
update resolveIdentifier
RodgeFu May 3, 2024
f9468e1
add test
RodgeFu May 6, 2024
d5fff32
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 6, 2024
4b0bd81
add changelog
RodgeFu May 6, 2024
1493436
small fix
RodgeFu May 6, 2024
3948989
small fix
RodgeFu May 6, 2024
b5f1141
update to remove document dependency
RodgeFu May 7, 2024
2d9501b
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 7, 2024
f8b1225
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 9, 2024
7ebee85
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 10, 2024
cd726bc
support value and some refactor
RodgeFu May 12, 2024
af901e4
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 12, 2024
48252c9
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 14, 2024
6535b0f
update test and changelog
RodgeFu May 14, 2024
45d0239
update per comments
RodgeFu May 15, 2024
fb1ff43
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 15, 2024
4e66b7d
Update .chronus/changes/improve completion-support-in-ide-2024-4-6-16…
RodgeFu May 15, 2024
54c42af
update per comments
RodgeFu May 15, 2024
11d790c
support completion for model type/object as template parameter's defa…
RodgeFu May 15, 2024
4e8787b
update per comment
RodgeFu May 16, 2024
ae2fde9
update changelog per comments
RodgeFu May 17, 2024
28a6f76
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 17, 2024
a3d5acb
Merge remote-tracking branch 'upstream/main' into add-completion-for-…
RodgeFu May 17, 2024
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,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
Loading