Conversation
…ocs output When a @prop default value is a reference to a const variable, an object property access, or an indexed object access, follow the reference to its underlying primitive literal so generated docs JSON shows the real value instead of the source-text expression. Falls back to the original getText() output for any expression that cannot be safely resolved to a primitive literal, so existing behavior is preserved for every case the previous code already handled. Closes #6727
There was a problem hiding this comment.
Pull request overview
This PR improves Stencil’s generated docs metadata for @Prop defaults by resolving certain safe initializer expressions (const identifiers and object property/index access) down to their underlying primitive literal text, so docs show the actual default value instead of a variable expression.
Changes:
- Updated
@Propdefault value extraction to attempt literal resolution before falling back toinitializer.getText(). - Added a bounded resolver that follows
constinitializers and object literal member lookups for simpleOBJ.key/OBJ['key']/OBJ[0]forms. - Added unit tests covering the newly supported resolution cases and the fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/compiler/transformers/decorators-to-static/prop-decorator.ts |
Routes defaultValue through a new resolver that tries to reduce supported initializer shapes to primitive literal text. |
src/compiler/transformers/test/parse-props.spec.ts |
Adds tests for resolving defaults from const identifiers and object member access, plus fallback cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address review feedback on #6728: - `getConstVariableInitializer` now unwraps alias symbols via `getAliasedSymbol`, so a `@Prop` initialized from an imported `const` is correctly resolved to its underlying literal (previously the alias symbol's declarations didn't include the `VariableDeclaration`). - Move the `undefined` identifier check into `isPrimitiveLiteral` so the inline comment / branch documentation matches the actual accepted set. - Extend the test `transpileModule` helper with an optional `extraFiles` map plus a `resolveModuleNames` short-circuit, enabling multi-file cross-module symbol resolution in unit tests. - Add a multi-file unit test that exercises the imported-const path through to the emitted `static get properties()` block. Also reformats the helper block to match the repository's Prettier configuration (resolves the failing `prettier.dry-run` CI step).
…defaults" This reverts commit 05a9cd2.
…ndling - Apply Prettier formatting (resolves the failing `prettier.dry-run` CI step). - Fold the special-case `undefined` identifier branch into `isPrimitiveLiteral`, so the inline documentation accurately describes the accepted set. No behavior change.
|
hey @gnbm thanks for raising! Can you work through / resolve the points raised by copilot - than I'll do a proper review :) Also nit - but can we not refer to internal tickets ( |
…literal Address Copilot review feedback on #6728: `isPrimitiveLiteral` previously matched every Identifier named `undefined` solely by node text, which mis-handled the (legal) case of a user shadowing `undefined` with a local `const`. Drop the special case and let `getConstVariableInitializer` walk the chain like any other identifier. The global `undefined` (declared in `lib.es5.d.ts` without an initializer) naturally falls through to the `getText()` fallback, so `@Prop() x = undefined;` still emits the string `"undefined"` — verified by a new regression test. No behavior change for unshadowed code; correctness fix for the shadowed case.
…d tighten recursion guard Address remaining Copilot review feedback on #6728: - Unwrap alias symbols (`ImportSpecifier` / `NamespaceImport`) before searching declarations, so `@Prop` initializers that reference an imported `const` correctly resolve to the imported binding's literal value (e.g. `import { QUERY } from './q'; @prop() when = QUERY['lg'];`). - Match `ShorthandPropertyAssignment` in `findObjectLiteralMember` and use `getShorthandAssignmentValueSymbol` to reach the in-scope binding, so defaults like `CONFIG.label` where `CONFIG = { label }` (shorthand) resolve correctly instead of falling back to raw text. - Tighten the recursion guard from `depth > MAX_RESOLVE_DEPTH` to `depth >= MAX_RESOLVE_DEPTH` so the documented "max depth 5" matches actual behavior (5 levels, not 6). - Extract `resolveConstSymbolInitializer` so symbol-to-initializer resolution (with alias unwrap + const guard) is shared between the identifier and shorthand paths. Adds two new tests: - Cross-file imported const resolution (self-contained 2-file `ts.Program` inline in the spec — does not touch the shared `transpileModule` helper). - Object shorthand property resolution. All existing tests still pass. Full transformers suite: 319/319.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/compiler/transformers/decorators-to-static/prop-decorator.ts:533
findObjectLiteralMemberreturns the first matching property assignment it encounters. In JavaScript object literals, later properties with the same key override earlier ones, so this can resolve an incorrect default value in the presence of duplicate keys. Consider iterating through all members and keeping the last matching initializer (while still ignoring spreads/computed names as you do now).
for (const member of obj.properties) {
if (ts.isPropertyAssignment(member)) {
const memberName = getPropertyNameText(member.name);
if (memberName === name) {
return member.initializer;
}
continue;
}
// Shorthand: `{ label }` — equivalent to `{ label: label }`. The shorthand
// name carries the property symbol, not the symbol of the in-scope binding,
// so use `getShorthandAssignmentValueSymbol` to reach the original binding
// and then walk to its `const` initializer.
if (ts.isShorthandPropertyAssignment(member) && member.name.text === name) {
return resolveConstSymbolInitializer(typeChecker.getShorthandAssignmentValueSymbol(member), typeChecker);
}
Address remaining Copilot review feedback on #6728: - `resolveLiteralText` now unwraps value-preserving wrapper expressions before pattern-matching: `as` (`'x' as const`), `<T>x` type assertions, parenthesized expressions, `satisfies`, and non-null `!`. Common patterns like `const DEFAULT = 'x' as const; @prop() v = DEFAULT;` now resolve to the literal instead of falling back to the source. - When an identifier chain terminates at a bare `undefined` global (e.g. `const DEFAULT = undefined; @prop() v = DEFAULT;`), emit the literal `'undefined'` so docs show the same value the runtime would observe. User-shadowed `const undefined = 'foo'` still resolves through the regular const path. Adds three new tests covering `as const`, parenthesized + non-null wrappers, and `const X = undefined; @prop() v = X;`. All existing tests still pass. Full transformers suite: 322/322.
…const aliases Address remaining Copilot review feedback on #6728: - Treat signed numeric literals (`-1`, `+2`) as primitive literals. TypeScript represents these as `PrefixUnaryExpression` wrapping a `NumericLiteral`, so `isPrimitiveLiteral` needs to recognise the shape via the new `isSignedNumericLiteral` helper. - Extend `resolveObjectLiteral` to unwrap value-preserving wrappers (`as const`, `(...)`, `<T>`, `satisfies`, `!`) and to follow chained `const`-to-`const` identifier references when locating the underlying object literal. Bounded by `MAX_RESOLVE_DEPTH` so cyclic chains cannot blow the stack. - Extract `unwrapValuePreservingWrappers` so both `resolveLiteralText` and `resolveObjectLiteral` use the same iterative helper. Adds three new tests covering: negative numeric const, `const CONFIG = ({...} as const)` property access, and `const A = CONFIG; @prop() v = A.label;` chain. All existing tests still pass. Full transformers suite: 325/325.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/compiler/transformers/decorators-to-static/prop-decorator.ts:549
- Same off-by-one depth guard as
resolveLiteralText:resolveObjectLiteralbails out whendepth >= MAX_RESOLVE_DEPTH, but callers passdepth + 1before following an identifier. This means an alias chain can hitdepth === MAX_RESOLVE_DEPTHon the final object literal and still fail to resolve. Adjust the depth check so up-to-MAX_RESOLVE_DEPTHtraversals actually succeed.
): ts.ObjectLiteralExpression | undefined => {
if (depth >= MAX_RESOLVE_DEPTH) {
return undefined;
}
… docs Address remaining Copilot review feedback on #6728: - Element-access key expressions are now unwrapped before the string/numeric literal check, so safe shapes like `OBJ[('lg')]` or `OBJ['lg' as const]` resolve instead of falling back to raw text. - Depth guard reverted to `depth > MAX_RESOLVE_DEPTH` so a 5-hop const chain still reaches its primitive leaf (the previous `>=` was off-by-one in the other direction). The constant doc comment is rewritten to spell out the exact semantics — "up to N hops" — so the guard, constant, and comment all agree. - Doc comment for `resolveConstSymbolInitializer` corrected: it returns the initializer expression as-is (not only when it is "literal-bearing") and leaves the literal-ness decision to the caller. Adds one test for the wrapped element-access key. All existing tests still pass. Full transformers suite: 326/326.
Hey @johnjenkins |
johnjenkins
left a comment
There was a problem hiding this comment.
just some tiny things. Don't worry about them if you don't have time :)
Introduce getPropertyNameText in decorators-to-static/prop-decorator.ts to return static text for object-literal property names and deliberately return undefined for computed names (avoiding compile-time evaluation of arbitrary expressions). Add a unit test in parse-props.spec.ts that ensures the resolver falls back to the original source text when a value chain exceeds MAX_RESOLVE_DEPTH (verifying fallback to getText()).
|
Hi @gnbm, this works for ion-split-pane's |
When a prop's default is a const that references an object/array literal (including cases using `satisfies`), emit the literal's source text so docs show the actual default instead of the identifier. Add a guard in resolveLiteralText to return node.getText() for object and array literals. Include tests for object literal resolved via `satisfies` and for array literal consts (parse-props.spec.ts).
Closes #6727
What is the current behavior?
Stencil emits the source text of a
@Propinitializer verbatim into the generated docs JSON (defaultValuefield). When the initializer is a reference to a const variable or an object-property access, the docs end up showing the variable expression rather than the actual default value.Example from
ion-split-pane(Ionic Framework):Today the generated docs JSON contains:
…which is what users see in https://ionicframework.com/docs/api/split-pane#prop-when.
GitHub Issue Number: #6727
What is the new behavior?
The
@Propdecorator parser now follows the initializer expression to its underlying primitive literal when it is safe to do so. With this fix, the same component produces:Shapes that are now resolved:
IDENTwhereIDENTis aconstvariable whose initializer chain ends in a primitive literal.OBJ.keyandOBJ['key'](orOBJ[0]) againstconstobject literals.const label = 'x'; const CONFIG = { label }; @Prop() v = CONFIG.label;) — resolved viagetShorthandAssignmentValueSymbol.consts (import { QUERY } from './queries';) — alias symbols are unwrapped viagetAliasedSymbolbefore walking declarations.as('x' as const),<T>xtype assertions, parenthesized expressions,satisfies, and non-null!.consts (const CONFIG = ({ label: 'x' } as const)) — the object side ofOBJ.keyis also unwrapped.const-to-constobject aliases (const A = CONFIG; @Prop() v = A.label;) — bounded byMAX_RESOLVE_DEPTH.-1,+2) — TypeScript represents these asPrefixUnaryExpression, now recognised as primitives.const X = undefined; @Prop() v = X;resolves to the literalundefined.Shapes that intentionally still fall back to the original
getText()output (preserving prior behavior):OBJ.a.b).OBJ[varKey]).SomeEnum.VALUE) — out of scope to avoid silently changing existing docs strings.let/varbindings — never followed because they may be reassigned.Safety properties:
string/number/ signed number /true/false/null/undefined) are emitted. Object and array literals are never serialized intodefaultValue.MAX_RESOLVE_DEPTH = 5) for both the literal walk and the object-literal walk.getText()at the leaf).getText()output via??, so no existing case regresses.Documentation
N/A — internal compiler change. The Gallery component's hardcoded
columns/gapdescription defaults inionic-team/ionic-docscan be dropped once Ionic picks up this Stencil release; that follow-up belongs in the docs repo.Does this introduce a breaking change?
The change only widens the set of expressions that are resolved into a literal. Every expression shape the previous code already produced output for produces the same output now.
Testing
Fifteen new unit tests in
src/compiler/transformers/test/parse-props.spec.tscovering:conststring / number variable resolution.OBJ.keyandOBJ['key']resolution — the latter uses the verbatimion-split-paneshape (FW-7298 reproduction).{ label }).constresolution — self-contained 2-filets.Programinline in the spec.as const, parenthesized + non-null wrapper unwrapping.constaccessed via property access.const-to-constobject alias resolution.-1).const X = undefined; @Prop() v = X;chain.@Prop() x = undefined;— preserved as raw text.Full transformers suite: 325 passed, zero regressions.
Other information
src/compiler/transformers/decorators-to-static/prop-decorator.tsand the corresponding test file; no other files are touched.