Skip to content

[compiler] Resolve members through template parameters.#9868

Open
witemple-msft wants to merge 11 commits intomicrosoft:mainfrom
witemple-msft:witemple-msft/templateparameter-metaproperties
Open

[compiler] Resolve members through template parameters.#9868
witemple-msft wants to merge 11 commits intomicrosoft:mainfrom
witemple-msft:witemple-msft/templateparameter-metaproperties

Conversation

@witemple-msft
Copy link
Member

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 ::returnType when a template item is constrained to an Operation. This is recursive, so I.o::returnType is allowed if I is an interface that is proven to have an operation named o. proven is the keyword. Members only resolve if the constraint guarantees their presence.

This feature enables much more powerful templates. For example:

model Resource {
  @visibility(Lifecycle.Read)
  id: string;
}

@format("uuid")
scalar uuid extends string;

model MyResource extends Resource {
  @visibility(Lifecycle.Read)
  id: uuid;

  myProp: string;
}

@error
model Error {
  @minValue(400)
  @maxValue(599)
  @statusCode code: int32;
  message: string;
}

interface Ops<R extends Resource> {
  // Notice, we can actually use the type of the specific `id` property of this resource, enriched with all of its metadata,
  // because the constraint `Resource` guarantees it is present.
  get(@path id: R.id): Read<R> | Error;
  @post
  create(@bodyRoot resource: Create<R>): Read<R> | Error;
  // ...
}

interface MyOps extends Ops<MyResource> {}

⚠️ This PR was almost entirely generated by GitHub Copilot CLI.

@microsoft-github-policy-service microsoft-github-policy-service bot added compiler:core Issues for @typespec/compiler emitter:openapi3 Issues for @typespec/openapi3 emitter labels Mar 2, 2026
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@9868
npm i https://pkg.pr.new/@typespec/html-program-viewer@9868
npm i https://pkg.pr.new/@typespec/http-server-csharp@9868
npm i https://pkg.pr.new/@typespec/tspd@9868

commit: e582f35

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

All changed packages have been documented.

  • @typespec/compiler
  • @typespec/html-program-viewer
  • @typespec/http-server-csharp
  • @typespec/tspd
Show changes

@typespec/compiler - feature ✏️

Enabled resolution of member properties and metaproperties through template parameters based on constraints.

@typespec/html-program-viewer - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@typespec/http-server-csharp - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@typespec/tspd - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@witemple-msft witemple-msft added the int:azure-specs Run integration tests against azure-rest-api-specs label Mar 2, 2026
@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 2, 2026

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@witemple-msft
Copy link
Member Author

Solid illustration of functionality here (see ResourceOperations): Playground

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TemplateParameterAccess internal type and comprehensive resolution logic in checker.ts and name-resolver.ts for resolving member/meta-member access through template parameter constraints
  • Updates all subsystems that handle the Type union (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)

Comment on lines +717 to +729
export interface TemplateParameterAccess extends BaseType {
kind: "TemplateParameterAccess";
/** @internal */
node: MemberExpressionNode;
/** @internal */
base: TemplateParameter | TemplateParameterAccess;
/** @internal */
path: string;
/** @internal */
cacheKey: string;
/** @internal */
constraint?: MixedParameterConstraint;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +390 to +399
/** 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;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +736 to +741
/** 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()] : [];
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 4483 to 4493
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;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- "@typespec/compiler"
---

Enabled resolution of member properties and metaproperties through template parameters based on constraints. No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we show some code examples?


const diagnostics = await testHost.diagnose("./main.tsp");

expectDiagnostics(diagnostics, [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the as any necessary after the strictEqual above?

/** @internal */
base: TemplateParameter | TemplateParameterAccess;
/** @internal */
path: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would that work for values?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler emitter:openapi3 Issues for @typespec/openapi3 emitter int:azure-specs Run integration tests against azure-rest-api-specs tspd Issues for the tspd tool ui:type-graph-viewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants