Skip to content

Commit 6b61b3c

Browse files
Show full definition of model/interface when it's 'extends' or 'is' other model/interfaces. (#7530)
Show full definition of model and interface when it's 'extends' or 'is' other model/interfaces for the pain point when people working with project with complex model/interface relationship like in our specs repo. fixes #6991 --------- Co-authored-by: Timothee Guerin <timothee.guerin@outlook.com>
1 parent cbef5a5 commit 6b61b3c

6 files changed

Lines changed: 230 additions & 27 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/compiler"
5+
---
6+
7+
Show the full definition of model and interface when it has 'extends' and 'is' relationship in the hover text

packages/compiler/src/core/helpers/type-name-utils.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { printIdentifier } from "./syntax-utils.js";
2020
export interface TypeNameOptions {
2121
namespaceFilter?: (ns: Namespace) => boolean;
2222
printable?: boolean;
23+
nameOnly?: boolean;
2324
}
2425

2526
export function getTypeName(type: Type, options?: TypeNameOptions): string {
@@ -135,7 +136,7 @@ export function getNamespaceFullName(type: Namespace, options?: TypeNameOptions)
135136
}
136137

137138
function getNamespacePrefix(type: Namespace | undefined, options?: TypeNameOptions) {
138-
if (type === undefined || isStdNamespace(type)) {
139+
if (type === undefined || isStdNamespace(type) || options?.nameOnly === true) {
139140
return "";
140141
}
141142
const namespaceFullName = getNamespaceFullName(type, options);
@@ -212,6 +213,9 @@ function isInTypeSpecNamespace(type: Type & { namespace?: Namespace }): boolean
212213
}
213214

214215
function getModelPropertyName(prop: ModelProperty, options: TypeNameOptions | undefined) {
216+
if (options?.nameOnly === true) {
217+
return prop.name;
218+
}
215219
const modelName = prop.model ? getModelName(prop.model, options) : undefined;
216220

217221
return `${modelName ?? "(anonymous model)"}.${prop.name}`;
@@ -234,10 +238,14 @@ function getOperationName(op: Operation, options: TypeNameOptions | undefined) {
234238
const params = op.node.templateParameters.map((t) => getIdentifierName(t.id.sv, options));
235239
opName += `<${params.join(", ")}>`;
236240
}
237-
const prefix = op.interface
238-
? getInterfaceName(op.interface, options) + "."
239-
: getNamespacePrefix(op.namespace, options);
240-
return `${prefix}${opName}`;
241+
if (options?.nameOnly === true) {
242+
return opName;
243+
} else {
244+
const prefix = op.interface
245+
? getInterfaceName(op.interface, options) + "."
246+
: getNamespacePrefix(op.namespace, options);
247+
return `${prefix}${opName}`;
248+
}
241249
}
242250

243251
function getIdentifierName(name: string, options: TypeNameOptions | undefined) {

packages/compiler/src/server/serverlib.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
WorkspaceEdit,
4949
WorkspaceFoldersChangeEvent,
5050
} from "vscode-languageserver/node.js";
51+
import { getSymNode } from "../core/binder.js";
5152
import { CharCode } from "../core/charcode.js";
5253
import { resolveCodeFix } from "../core/code-fixes.js";
5354
import { compilerAssert, getSourceLocation } from "../core/diagnostics.js";
@@ -717,13 +718,42 @@ export function createServer(host: ServerHost): Server {
717718
const sym =
718719
id?.kind === SyntaxKind.Identifier ? program.checker.resolveRelatedSymbols(id) : undefined;
719720

720-
const markdown: MarkupContent = {
721-
kind: MarkupKind.Markdown,
722-
value: sym && sym.length > 0 ? getSymbolDetails(program, sym[0]) : "",
723-
};
724-
return {
725-
contents: markdown,
726-
};
721+
if (!sym || sym.length === 0) {
722+
return { contents: { kind: MarkupKind.Markdown, value: "" } };
723+
} else {
724+
// Only show full definition if the symbol is a model or interface that has extends or is clauses.
725+
// Avoid showing full definition in other cases which can be long and not useful
726+
let includeExpandedDefinition = false;
727+
const sn = getSymNode(sym[0]);
728+
if (sn.kind !== SyntaxKind.AliasStatement) {
729+
const type = sym[0].type ?? program.checker.getTypeOrValueForNode(sn);
730+
if (type && "kind" in type) {
731+
const modelHasExtendOrIs: boolean =
732+
type.kind === "Model" &&
733+
(type.baseModel !== undefined ||
734+
type.sourceModel !== undefined ||
735+
type.sourceModels.length > 0);
736+
const interfaceHasExtend: boolean =
737+
type.kind === "Interface" && type.sourceInterfaces.length > 0;
738+
includeExpandedDefinition = modelHasExtendOrIs || interfaceHasExtend;
739+
}
740+
}
741+
742+
const markdown: MarkupContent = {
743+
kind: MarkupKind.Markdown,
744+
value:
745+
sym && sym.length > 0
746+
? getSymbolDetails(program, sym[0], {
747+
includeSignature: true,
748+
includeParameterTags: true,
749+
includeExpandedDefinition,
750+
})
751+
: "",
752+
};
753+
return {
754+
contents: markdown,
755+
};
756+
}
727757
}
728758

729759
async function getSignatureHelp(params: SignatureHelpParams): Promise<SignatureHelp | undefined> {

packages/compiler/src/server/type-details.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import { isType } from "../core/type-utils.js";
66
import { DocContent, Node, Sym, SyntaxKind, TemplateDeclarationNode, Type } from "../core/types.js";
77
import { getSymbolSignature } from "./type-signature.js";
88

9+
interface GetSymbolDetailsOptions {
10+
includeSignature: boolean;
11+
includeParameterTags: boolean;
12+
/**
13+
* Whether to include the final expended definition of the symbol
14+
* For Model and Interface, it's body with expended members will be included. Otherwise, it will be the same as signature. (Support for other type may be added in the future as needed)
15+
* This is useful for models and interfaces with complex 'extends' and 'is' relationship when user wants to know the final expended definition.
16+
*/
17+
includeExpandedDefinition?: boolean;
18+
}
19+
920
/**
1021
* Get the detailed documentation for a symbol.
1122
* @param program The program
@@ -14,9 +25,10 @@ import { getSymbolSignature } from "./type-signature.js";
1425
export function getSymbolDetails(
1526
program: Program,
1627
symbol: Sym,
17-
options = {
28+
options: GetSymbolDetailsOptions = {
1829
includeSignature: true,
1930
includeParameterTags: true,
31+
includeExpandedDefinition: false,
2032
},
2133
): string {
2234
const lines = [];
@@ -43,6 +55,15 @@ export function getSymbolDetails(
4355
}
4456
}
4557
}
58+
if (options.includeExpandedDefinition) {
59+
lines.push(`*Full Definition:*`);
60+
lines.push(
61+
getSymbolSignature(program, symbol, {
62+
includeBody: true,
63+
}),
64+
);
65+
}
66+
4667
return lines.join("\n\n");
4768
}
4869

packages/compiler/src/server/type-signature.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
Decorator,
1010
EnumMember,
1111
FunctionParameter,
12+
Interface,
13+
Model,
1214
ModelProperty,
1315
Operation,
1416
StringTemplate,
@@ -18,40 +20,60 @@ import {
1820
UnionVariant,
1921
Value,
2022
} from "../core/types.js";
23+
import { walkPropertiesInherited } from "../index.js";
24+
25+
interface GetSymbolSignatureOptions {
26+
/**
27+
* Whether to include the body in the signature. Only support Model and Interface type now
28+
*/
29+
includeBody: boolean;
30+
}
2131

2232
/** @internal */
23-
export function getSymbolSignature(program: Program, sym: Sym): string {
33+
export function getSymbolSignature(
34+
program: Program,
35+
sym: Sym,
36+
options: GetSymbolSignatureOptions = {
37+
includeBody: false,
38+
},
39+
): string {
2440
const decl = getSymNode(sym);
2541
switch (decl?.kind) {
2642
case SyntaxKind.AliasStatement:
2743
return fence(`alias ${getAliasSignature(decl)}`);
2844
}
2945
const entity = sym.type ?? program.checker.getTypeOrValueForNode(decl);
30-
return getEntitySignature(sym, entity);
46+
return getEntitySignature(sym, entity, options);
3147
}
3248

33-
function getEntitySignature(sym: Sym, entity: Type | Value | null): string {
49+
function getEntitySignature(
50+
sym: Sym,
51+
entity: Type | Value | null,
52+
options: GetSymbolSignatureOptions,
53+
): string {
3454
if (entity === null) {
3555
return "(error)";
3656
}
3757
if ("valueKind" in entity) {
3858
return fence(`const ${sym.name}: ${getTypeName(entity.type)}`);
3959
}
4060

41-
return getTypeSignature(entity);
61+
return getTypeSignature(entity, options);
4262
}
4363

44-
function getTypeSignature(type: Type): string {
64+
function getTypeSignature(type: Type, options: GetSymbolSignatureOptions): string {
4565
switch (type.kind) {
4666
case "Scalar":
4767
case "Enum":
4868
case "Union":
49-
case "Interface":
50-
case "Model":
5169
case "Namespace":
5270
return fence(`${type.kind.toLowerCase()} ${getPrintableTypeName(type)}`);
71+
case "Interface":
72+
return fence(getInterfaceSignature(type, options.includeBody));
73+
case "Model":
74+
return fence(getModelSignature(type, options.includeBody));
5375
case "ScalarConstructor":
54-
return fence(`init ${getTypeSignature(type.scalar)}.${type.name}`);
76+
return fence(`init ${getTypeSignature(type.scalar, options)}.${type.name}`);
5577
case "Decorator":
5678
return fence(getDecoratorSignature(type));
5779
case "Operation":
@@ -80,7 +102,7 @@ function getTypeSignature(type: Type): string {
80102
case "UnionVariant":
81103
return `(union variant)\n${fence(getUnionVariantSignature(type))}`;
82104
case "Tuple":
83-
return `(tuple)\n[${fence(type.values.map(getTypeSignature).join(", "))}]`;
105+
return `(tuple)\n[${fence(type.values.map((v) => getTypeSignature(v, options)).join(", "))}]`;
84106
default:
85107
const _assertNever: never = type;
86108
compilerAssert(false, "Unexpected type kind");
@@ -94,9 +116,41 @@ function getDecoratorSignature(type: Decorator) {
94116
return `dec ${ns}${name}(${parameters.join(", ")})`;
95117
}
96118

97-
function getOperationSignature(type: Operation) {
98-
const parameters = [...type.parameters.properties.values()].map(getModelPropertySignature);
99-
return `op ${getTypeName(type)}(${parameters.join(", ")}): ${getPrintableTypeName(type.returnType)}`;
119+
function getOperationSignature(type: Operation, includeQualifier: boolean = true) {
120+
const parameters = [...type.parameters.properties.values()].map((p) =>
121+
getModelPropertySignature(p, false /* includeQualifier */),
122+
);
123+
return `op ${getTypeName(type, {
124+
nameOnly: !includeQualifier,
125+
})}(${parameters.join(", ")}): ${getPrintableTypeName(type.returnType)}`;
126+
}
127+
128+
function getInterfaceSignature(type: Interface, includeBody: boolean) {
129+
if (includeBody) {
130+
const INDENT = " ";
131+
const opDesc = Array.from(type.operations).map(
132+
([name, op]) => INDENT + getOperationSignature(op, false /* includeQualifier */) + ";",
133+
);
134+
return `${type.kind.toLowerCase()} ${getPrintableTypeName(type)} {\n${opDesc.join("\n")}\n}`;
135+
} else {
136+
return `${type.kind.toLowerCase()} ${getPrintableTypeName(type)}`;
137+
}
138+
}
139+
140+
/**
141+
* All properties from 'extends' and 'is' will be included if includeBody is true.
142+
*/
143+
function getModelSignature(type: Model, includeBody: boolean) {
144+
if (includeBody) {
145+
const propDesc = [];
146+
const INDENT = " ";
147+
for (const prop of walkPropertiesInherited(type)) {
148+
propDesc.push(INDENT + getModelPropertySignature(prop, false /*includeQualifier*/));
149+
}
150+
return `${type.kind.toLowerCase()} ${getPrintableTypeName(type)}{\n${propDesc.map((d) => `${d};`).join("\n")}\n}`;
151+
} else {
152+
return `${type.kind.toLowerCase()} ${getPrintableTypeName(type)}`;
153+
}
100154
}
101155

102156
function getFunctionParameterSignature(parameter: FunctionParameter) {
@@ -117,8 +171,8 @@ function getStringTemplateSignature(stringTemplate: StringTemplate) {
117171
);
118172
}
119173

120-
function getModelPropertySignature(property: ModelProperty) {
121-
const ns = getQualifier(property.model);
174+
function getModelPropertySignature(property: ModelProperty, includeQualifier: boolean = true) {
175+
const ns = includeQualifier ? getQualifier(property.model) : "";
122176
return `${ns}${printIdentifier(property.name, "allow-reserved")}: ${getPrintableTypeName(property.type)}`;
123177
}
124178

packages/compiler/test/server/get-hover.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,56 @@ describe("compiler: server: on hover", () => {
394394
},
395395
});
396396
});
397+
398+
it("model with extends and is (full definition expected)", async () => {
399+
const hover = await getHoverAtCursor(
400+
`
401+
namespace TestNs;
402+
403+
model Do┆g is Animal<string, DogProperties> {
404+
barkVolume: int32;
405+
}
406+
407+
model Animal<T, P> extends AnimalBase<P>{
408+
name: string;
409+
age: int16;
410+
tTag: T;
411+
}
412+
413+
model AnimalBase<P> {
414+
id: string;
415+
properties: P;
416+
}
417+
418+
419+
model DogProperties {
420+
breed: string;
421+
color: string;
422+
}
423+
`,
424+
);
425+
deepStrictEqual(hover, {
426+
contents: {
427+
kind: MarkupKind.Markdown,
428+
value: `\`\`\`typespec
429+
model TestNs.Dog
430+
\`\`\`
431+
432+
*Full Definition:*
433+
434+
\`\`\`typespec
435+
model TestNs.Dog{
436+
name: string;
437+
age: int16;
438+
tTag: string;
439+
barkVolume: int32;
440+
id: string;
441+
properties: TestNs.DogProperties;
442+
}
443+
\`\`\``,
444+
},
445+
});
446+
});
397447
});
398448

399449
describe("interface", () => {
@@ -449,6 +499,39 @@ describe("compiler: server: on hover", () => {
449499
},
450500
});
451501
});
502+
503+
it("interface with extends", async () => {
504+
const hover = await getHoverAtCursor(
505+
`
506+
namespace TestNs;
507+
508+
interface IActions{
509+
fly(): void;
510+
}
511+
512+
interface Bi┆rd extends IActions {
513+
eat(): void;
514+
}
515+
`,
516+
);
517+
deepStrictEqual(hover, {
518+
contents: {
519+
kind: MarkupKind.Markdown,
520+
value: `\`\`\`typespec
521+
interface TestNs.Bird
522+
\`\`\`
523+
524+
*Full Definition:*
525+
526+
\`\`\`typespec
527+
interface TestNs.Bird {
528+
op fly(): void;
529+
op eat(): void;
530+
}
531+
\`\`\``,
532+
},
533+
});
534+
});
452535
});
453536

454537
describe("operation", () => {

0 commit comments

Comments
 (0)