[compiler] Resolve members through template parameters.#9868
[compiler] Resolve members through template parameters.#9868witemple-msft wants to merge 11 commits intomicrosoft:mainfrom
Conversation
…ateparameter-metaproperties
commit: |
|
All changed packages have been documented.
Show changes
|
|
You can try these changes here
|
|
Solid illustration of functionality here (see |
…ateparameter-metaproperties
There was a problem hiding this comment.
Pull request overview
This PR introduces a new TemplateParameterAccess type to the TypeSpec compiler, enabling member access (.) and meta-member access (::) through template parameters based on their constraints. For instance, if R extends Resource and Resource guarantees a property id, then R.id can be used as a type in template declarations.
Changes:
- Adds a new
TemplateParameterAccessinternal type and comprehensive resolution logic inchecker.tsandname-resolver.tsfor resolving member/meta-member access through template parameter constraints - Updates all subsystems that handle the
Typeunion (semantic walker, type relation checker, string template utils, type name utils, completions, hover, document highlight) to recognize the new type - Updates dependent packages (
html-program-viewer,http-server-csharp,tspd) and adds tests for completions, hover, document highlighting, and reference resolution
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/compiler/src/core/types.ts |
Adds TemplateParameterAccess interface and includes it in the Type union |
packages/compiler/src/core/checker.ts |
Core logic for resolving template parameter access (member & meta-member), caching, and completions |
packages/compiler/src/core/name-resolver.ts |
Adds getMetaMemberNames API to expose available meta-member names for completion and validation |
packages/compiler/src/core/semantic-walker.ts |
Adds navigation support for TemplateParameterAccess in the semantic walker |
packages/compiler/src/core/type-relation-checker.ts |
Treats TemplateParameterAccess like TemplateParameter (uses constraint for assignability) |
packages/compiler/src/core/helpers/type-name-utils.ts |
Returns path as the type name for TemplateParameterAccess |
packages/compiler/src/core/helpers/string-template-utils.ts |
Handles TemplateParameterAccess in string template serialization check |
packages/compiler/src/server/type-signature.ts |
Renders hover signature for TemplateParameterAccess as (template access) |
packages/compiler/test/checker/references.test.ts |
Tests for resolving references through template parameter constraints |
packages/compiler/test/checker/operations.test.ts |
Tests that operation parameters resolve correctly with template member types |
packages/compiler/test/server/completion.test.ts |
Tests for IDE completions through constrained template parameters |
packages/compiler/test/server/get-hover.test.ts |
Tests for hover information on template parameter access expressions |
packages/compiler/test/server/document-highlight.test.ts |
Tests for document highlighting of template access references |
packages/html-program-viewer/src/react/type-config.ts |
Registers TemplateParameterAccess in the program viewer UI |
packages/http-server-csharp/src/lib/service.ts |
Handles TemplateParameterAccess as an unsupported type (returns undefined) |
packages/tspd/src/ref-doc/utils/type-signature.ts |
Renders TemplateParameterAccess signature in documentation generation |
.chronus/changes/*.md |
Two changeset entries for the compiler (feature) and dependent packages (internal) |
| export interface TemplateParameterAccess extends BaseType { | ||
| kind: "TemplateParameterAccess"; | ||
| /** @internal */ | ||
| node: MemberExpressionNode; | ||
| /** @internal */ | ||
| base: TemplateParameter | TemplateParameterAccess; | ||
| /** @internal */ | ||
| path: string; | ||
| /** @internal */ | ||
| cacheKey: string; | ||
| /** @internal */ | ||
| constraint?: MixedParameterConstraint; | ||
| } |
There was a problem hiding this comment.
The TemplateParameterAccess interface is missing the JSDoc documentation that TemplateParameter has. TemplateParameter is documented with a description of when it can appear, a warning about what you might be missing if you see it, and an @experimental tag. The new TemplateParameterAccess should have analogous documentation explaining that this type represents a member access rooted in a template parameter within template declarations, and should similarly carry an @experimental tag (since it's closely related to the experimental TemplateParameter type).
| /** Emit semantic walker events for template parameter access nodes. */ | ||
| function navigateTemplateParameterAccess( | ||
| type: TemplateParameterAccess, | ||
| context: NavigationContext, | ||
| ) { | ||
| if (checkVisited(context.visited, type)) { | ||
| return; | ||
| } | ||
| if (context.emit("templateParameterAccess", type) === ListenerFlow.NoRecursion) return; | ||
| } |
There was a problem hiding this comment.
The navigateTemplateParameterAccess function does not emit the exitTemplateParameterAccess event, even though exitTemplateParameterAccess is included in the eventNames array (line 518). This inconsistency means that any listener registered for exitTemplateParameterAccess will never be called. Note that navigateTemplateParameter has the same issue (it also doesn't emit its exit event), but the new TemplateParameterAccess follows this same pattern, perpetuating a pre-existing inconsistency in the codebase. If this is intentional (because these types don't have children to navigate), the exitTemplateParameterAccess entry in eventNames should be removed to avoid confusing users who register exit listeners.
| /** Get the available meta-member names for a symbol's meta-type prototype. */ | ||
| function getMetaMemberNames(baseSym: Sym): readonly string[] { | ||
| const baseNode = getSymNode(baseSym); | ||
| const prototype = getMetaTypePrototypeForSymbol(baseSym, baseNode); | ||
| return prototype ? [...prototype.keys()] : []; | ||
| } |
There was a problem hiding this comment.
In resolveMetaMemberByName (name-resolver.ts line 718), the lookup only uses metaTypePrototypes.get(baseNode.kind) without the Reflection model fallback logic. However, getMetaMemberNames (added in this PR) uses getMetaTypePrototypeForSymbol which includes the Reflection fallback. This creates an inconsistency: getMetaMemberNames can return meta-member names for Reflection.ModelProperty and Reflection.Operation symbols, but resolveMetaMemberByName called with the same symbols would return NotFound. Fortunately, resolveMetaTypeFromConstraint in checker.ts handles this by returning unknownType early for Reflection meta projection symbols (line 4012-4016), so the resolveMetaMemberByName call at line 4018 is never reached for those cases. However, this asymmetry is fragile and could cause bugs if the guard at line 4012 is ever relaxed or if resolveMetaMemberByName is called directly on Reflection symbols from other code paths.
| if (typeOrValue !== null) { | ||
| if (isValue(typeOrValue)) { | ||
| hasValue = true; | ||
| } else if ("kind" in typeOrValue && typeOrValue.kind === "TemplateParameter") { | ||
| } else if ( | ||
| "kind" in typeOrValue && | ||
| (typeOrValue.kind === "TemplateParameter" || | ||
| typeOrValue.kind === "TemplateParameterAccess") | ||
| ) { | ||
| if (typeOrValue.constraint) { | ||
| if (typeOrValue.constraint.valueType) { | ||
| hasValue = true; |
There was a problem hiding this comment.
In the checkStringTemplateExpresion function (checker.ts), the detection phase at lines 4486-4490 was updated to handle TemplateParameterAccess. However, the value-building loop at line 4522 still only checks typeOrValue.kind !== "TemplateParameter" and does not also exclude "TemplateParameterAccess". This means that if a TemplateParameterAccess with only a valueType constraint (no type constraint) appears as a span in a value-mode string template, the code will incorrectly attempt to treat it as a value (reaching the compilerAssert(isValue(typeOrValue), "Expected value.") at line 4524), causing a runtime crash. The condition on line 4522 should also exclude "TemplateParameterAccess" to be consistent with the detection phase above.
| - "@typespec/compiler" | ||
| --- | ||
|
|
||
| Enabled resolution of member properties and metaproperties through template parameters based on constraints. No newline at end of file |
There was a problem hiding this comment.
Could we show some code examples?
|
|
||
| const diagnostics = await testHost.diagnose("./main.tsp"); | ||
|
|
||
| expectDiagnostics(diagnostics, [ |
There was a problem hiding this comment.
nit: don't need the array wrap
| const idParam = myGet.parameters.properties.get("id"); | ||
| ok(idParam); | ||
| strictEqual(idParam.type.kind, "Scalar"); | ||
| strictEqual((idParam.type as any).name, "uuid"); |
There was a problem hiding this comment.
is the as any necessary after the strictEqual above?
| /** @internal */ | ||
| base: TemplateParameter | TemplateParameterAccess; | ||
| /** @internal */ | ||
| path: string; |
There was a problem hiding this comment.
what does path corespond to? and cacheKey?
| * @param baseEntity Template parameter or prior template access chain. | ||
| * @returns The resolved member/meta-member type, or `errorType` when not guaranteed. | ||
| */ | ||
| function resolveTemplateAccessType( |
There was a problem hiding this comment.
would that work for values?
This PR enables resolving member symbols (properties, metaproperties) through template parameters based on constraints.
It accomplishes this by adding a new Type node,
TemplateParameterAccess, which is a semantic equivalent of a member expression where the base of the member access is a template parameter or another template parameter access expression. These are only visible inside the context of template declarations. Otherwise, like template parameters, they become "resolved" to concrete types when the template is instantiated.This allows you to invoke metaproperties such as
::returnTypewhen a template item is constrained to anOperation. This is recursive, soI.o::returnTypeis allowed ifIis an interface that is proven to have an operation namedo. proven is the keyword. Members only resolve if the constraint guarantees their presence.This feature enables much more powerful templates. For example: