From dc3925e86d27d4f7360d0b7d398a251f60042265 Mon Sep 17 00:00:00 2001 From: Ryan Waskiewicz Date: Tue, 13 Jun 2023 06:31:35 -0700 Subject: [PATCH] fix(compiler): handle static members with stencil decorators (#4463) this commit fixes an edge case to the fix introduced in 6dbe9a5 (#4447). the mechanism for checking if there is a statically intiialized class member has been updated in order to exclude such members that are decorated with Stencil's `@Prop()` and `@State()` decorators. --- .../convert-static-members.ts | 26 +++++++- .../test/convert-static-members.spec.ts | 59 +++++++++++++++++++ test/karma/test-app/components.d.ts | 13 ++++ test/karma/test-app/static-members/index.html | 1 + .../test-app/static-members/karma.spec.ts | 6 ++ .../static-decorated-members.tsx | 15 +++++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 test/karma/test-app/static-members/static-decorated-members.tsx diff --git a/src/compiler/transformers/decorators-to-static/convert-static-members.ts b/src/compiler/transformers/decorators-to-static/convert-static-members.ts index ee701a51c5c..86cdb7938f2 100644 --- a/src/compiler/transformers/decorators-to-static/convert-static-members.ts +++ b/src/compiler/transformers/decorators-to-static/convert-static-members.ts @@ -1,10 +1,14 @@ +import { readOnlyArrayHasStringMember } from '@utils'; import ts from 'typescript'; +import { CONSTRUCTOR_DEFINED_MEMBER_DECORATORS } from './decorators-constants'; + /** * Helper function to detect if a class element fits the following criteria: * - It is a property declaration (e.g. `foo`) * - It has an initializer (e.g. `foo *=1*`) * - The property declaration has the `static` modifier on it (e.g. `*static* foo =1`) + * - The property declaration does not include a Stencil @Prop or @State decorator * @param classElm the class member to test * @returns true if the class member fits the above criteria, false otherwise */ @@ -13,6 +17,26 @@ export const hasStaticInitializerInClass = (classElm: ts.ClassElement): boolean ts.isPropertyDeclaration(classElm) && classElm.initializer !== undefined && Array.isArray(classElm.modifiers) && - classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) && + !classElm.modifiers!.some(isStencilStateOrPropDecorator) ); }; + +/** + * Determines if a Modifier-like node is a Stencil `@Prop()` or `@State()` decorator + * @param modifier the AST node to evaluate + * @returns true if the node is a decorator with the name 'Prop' or 'State', false otherwise + */ +const isStencilStateOrPropDecorator = (modifier: ts.ModifierLike): boolean => { + if (ts.isDecorator(modifier)) { + const decoratorName = + ts.isCallExpression(modifier.expression) && + ts.isIdentifier(modifier.expression.expression) && + modifier.expression.expression.text; + return ( + typeof decoratorName !== 'boolean' && + readOnlyArrayHasStringMember(CONSTRUCTOR_DEFINED_MEMBER_DECORATORS, decoratorName) + ); + } + return false; +}; diff --git a/src/compiler/transformers/test/convert-static-members.spec.ts b/src/compiler/transformers/test/convert-static-members.spec.ts index 4f8344ef84b..130605c5abe 100644 --- a/src/compiler/transformers/test/convert-static-members.spec.ts +++ b/src/compiler/transformers/test/convert-static-members.spec.ts @@ -42,6 +42,65 @@ describe('convert-static-members', () => { expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); }); + it('returns true for a decorated (non-Stencil) static property with an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ + ts.factory.createDecorator( + ts.factory.createCallExpression( + ts.factory.createIdentifier('SomeDecorator'), // Imaginary decorator + undefined, + [] + ) + ), + ts.factory.createToken(ts.SyntaxKind.StaticKeyword), + ], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); + }); + + it.each(['Prop', 'State'])( + 'returns false for a static property decorated with @%s with an initializer', + (decoratorName) => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ + ts.factory.createDecorator( + ts.factory.createCallExpression( + ts.factory.createIdentifier(decoratorName), // Stencil decorator + undefined, + [] + ) + ), + ts.factory.createToken(ts.SyntaxKind.StaticKeyword), + ], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + } + ); + it('returns true for a static property with an initializer with multiple members', () => { const classWithStaticMembers = ts.factory.createClassDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index d70fbbfacb4..7732f5200b4 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -318,6 +318,8 @@ export namespace Components { } interface SlottedCss { } + interface StaticDecoratedMembers { + } interface StaticMembers { } interface StaticMembersSeparateExport { @@ -1104,6 +1106,12 @@ declare global { prototype: HTMLSlottedCssElement; new (): HTMLSlottedCssElement; }; + interface HTMLStaticDecoratedMembersElement extends Components.StaticDecoratedMembers, HTMLStencilElement { + } + var HTMLStaticDecoratedMembersElement: { + prototype: HTMLStaticDecoratedMembersElement; + new (): HTMLStaticDecoratedMembersElement; + }; interface HTMLStaticMembersElement extends Components.StaticMembers, HTMLStencilElement { } var HTMLStaticMembersElement: { @@ -1282,6 +1290,7 @@ declare global { "slot-replace-wrapper": HTMLSlotReplaceWrapperElement; "slot-replace-wrapper-root": HTMLSlotReplaceWrapperRootElement; "slotted-css": HTMLSlottedCssElement; + "static-decorated-members": HTMLStaticDecoratedMembersElement; "static-members": HTMLStaticMembersElement; "static-members-separate-export": HTMLStaticMembersSeparateExportElement; "static-members-separate-initializer": HTMLStaticMembersSeparateInitializerElement; @@ -1609,6 +1618,8 @@ declare namespace LocalJSX { } interface SlottedCss { } + interface StaticDecoratedMembers { + } interface StaticMembers { } interface StaticMembersSeparateExport { @@ -1751,6 +1762,7 @@ declare namespace LocalJSX { "slot-replace-wrapper": SlotReplaceWrapper; "slot-replace-wrapper-root": SlotReplaceWrapperRoot; "slotted-css": SlottedCss; + "static-decorated-members": StaticDecoratedMembers; "static-members": StaticMembers; "static-members-separate-export": StaticMembersSeparateExport; "static-members-separate-initializer": StaticMembersSeparateInitializer; @@ -1889,6 +1901,7 @@ declare module "@stencil/core" { "slot-replace-wrapper": LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes; "slot-replace-wrapper-root": LocalJSX.SlotReplaceWrapperRoot & JSXBase.HTMLAttributes; "slotted-css": LocalJSX.SlottedCss & JSXBase.HTMLAttributes; + "static-decorated-members": LocalJSX.StaticDecoratedMembers & JSXBase.HTMLAttributes; "static-members": LocalJSX.StaticMembers & JSXBase.HTMLAttributes; "static-members-separate-export": LocalJSX.StaticMembersSeparateExport & JSXBase.HTMLAttributes; "static-members-separate-initializer": LocalJSX.StaticMembersSeparateInitializer & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/static-members/index.html b/test/karma/test-app/static-members/index.html index 1438519ea4d..9b7837ac956 100644 --- a/test/karma/test-app/static-members/index.html +++ b/test/karma/test-app/static-members/index.html @@ -4,5 +4,6 @@ + diff --git a/test/karma/test-app/static-members/karma.spec.ts b/test/karma/test-app/static-members/karma.spec.ts index 8b4273aa4e9..f24be91da94 100644 --- a/test/karma/test-app/static-members/karma.spec.ts +++ b/test/karma/test-app/static-members/karma.spec.ts @@ -15,6 +15,12 @@ describe('static-members', function () { expect(cmp.textContent.trim()).toBe('This is a component with static public and private members'); }); + it('renders properly with initialized, decorated static members', async () => { + const cmp = app.querySelector('static-decorated-members'); + + expect(cmp.textContent.trim()).toBe('This is a component with a static Stencil decorated member'); + }); + it('renders properly with a separate export', async () => { const cmp = app.querySelector('static-members-separate-export'); diff --git a/test/karma/test-app/static-members/static-decorated-members.tsx b/test/karma/test-app/static-members/static-decorated-members.tsx new file mode 100644 index 00000000000..29632e628f7 --- /dev/null +++ b/test/karma/test-app/static-members/static-decorated-members.tsx @@ -0,0 +1,15 @@ +import { Component, h, State } from '@stencil/core'; + +@Component({ + tag: 'static-decorated-members', +}) +export class StaticDecoratedMembers { + /** + * See the spec file associated with this file for the motivation for this test + */ + @State() static property = '@State-ful'; + + render() { + return
This is a component with a static Stencil decorated member
; + } +}