Skip to content

Commit

Permalink
fix(compiler): handle static members with stencil decorators
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rwaskiewicz committed Jun 12, 2023
1 parent 87fb34a commit d8ae212
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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;
};
59 changes: 59 additions & 0 deletions src/compiler/transformers/test/convert-static-members.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand Down
13 changes: 13 additions & 0 deletions test/karma/test-app/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ export namespace Components {
}
interface SlottedCss {
}
interface StaticDecoratedMembers {
}
interface StaticMembers {
}
interface StaticMembersSeparateExport {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1609,6 +1618,8 @@ declare namespace LocalJSX {
}
interface SlottedCss {
}
interface StaticDecoratedMembers {
}
interface StaticMembers {
}
interface StaticMembersSeparateExport {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1889,6 +1901,7 @@ declare module "@stencil/core" {
"slot-replace-wrapper": LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes<HTMLSlotReplaceWrapperElement>;
"slot-replace-wrapper-root": LocalJSX.SlotReplaceWrapperRoot & JSXBase.HTMLAttributes<HTMLSlotReplaceWrapperRootElement>;
"slotted-css": LocalJSX.SlottedCss & JSXBase.HTMLAttributes<HTMLSlottedCssElement>;
"static-decorated-members": LocalJSX.StaticDecoratedMembers & JSXBase.HTMLAttributes<HTMLStaticDecoratedMembersElement>;
"static-members": LocalJSX.StaticMembers & JSXBase.HTMLAttributes<HTMLStaticMembersElement>;
"static-members-separate-export": LocalJSX.StaticMembersSeparateExport & JSXBase.HTMLAttributes<HTMLStaticMembersSeparateExportElement>;
"static-members-separate-initializer": LocalJSX.StaticMembersSeparateInitializer & JSXBase.HTMLAttributes<HTMLStaticMembersSeparateInitializerElement>;
Expand Down
1 change: 1 addition & 0 deletions test/karma/test-app/static-members/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<script src="./build/testapp.js" nomodule></script>

<static-members></static-members>
<static-decorated-members></static-decorated-members>
<static-members-separate-export></static-members-separate-export>
<static-members-separate-initializer></static-members-separate-initializer>
6 changes: 6 additions & 0 deletions test/karma/test-app/static-members/karma.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
15 changes: 15 additions & 0 deletions test/karma/test-app/static-members/static-decorated-members.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>This is a component with a static Stencil decorated member</div>;
}
}

0 comments on commit d8ae212

Please sign in to comment.