Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prop get set new #6050

Merged
merged 11 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/compiler/docs/generate-doc-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const getRealProperties = (properties: d.ComponentCompilerProperty[]): d.JsonDoc

optional: member.optional,
required: member.required,

getter: member.getter,
setter: member.setter,
}));
};

Expand All @@ -227,6 +230,9 @@ const getVirtualProperties = (virtualProps: d.ComponentCompilerVirtualProperty[]

optional: true,
required: false,

getter: undefined,
setter: undefined,
}));
};

Expand Down
10 changes: 10 additions & 0 deletions src/compiler/docs/test/markdown-props.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ describe('markdown props', () => {
reflectToAttr: false,
docsTags: [],
values: [],
getter: false,
setter: false,
},
{
name: 'hello',
Expand All @@ -28,6 +30,8 @@ describe('markdown props', () => {
reflectToAttr: false,
docsTags: [],
values: [],
getter: false,
setter: false,
},
]).join('\n');
expect(markdown).toEqual(`## Properties
Expand All @@ -54,6 +58,8 @@ describe('markdown props', () => {
reflectToAttr: false,
docsTags: [],
values: [],
getter: false,
setter: false,
},
]).join('\n');

Expand All @@ -80,6 +86,8 @@ describe('markdown props', () => {
reflectToAttr: false,
docsTags: [],
values: [],
getter: false,
setter: false,
},
]).join('\n');

Expand All @@ -106,6 +114,8 @@ describe('markdown props', () => {
reflectToAttr: false,
docsTags: [],
values: [],
getter: false,
setter: false,
},
]).join('\n');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,19 @@ const removeStencilMethodDecorators = (
member.type,
member.body,
);
} else if (ts.isGetAccessor(member)) {
return ts.factory.updateGetAccessorDeclaration(
member,
ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined,
member.name,
member.parameters,
member.type,
member.body,
);
} else if (ts.isSetAccessor(member)) {
const err = buildError(diagnostics);
err.messageText = 'A get accessor should be decorated before a set accessor';
augmentDiagnosticWithNode(err, member);
} else if (ts.isPropertyDeclaration(member)) {
if (shouldInitializeInConstructor(member, importAliasMap)) {
// if the current class member is decorated with either 'State' or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export const propDecoratorsToStatic = (
decoratorName: string,
): void => {
const properties = decoratedProps
.filter(ts.isPropertyDeclaration)
.map((prop) => parsePropDecorator(diagnostics, typeChecker, program, prop, decoratorName))
.filter((prop) => ts.isPropertyDeclaration(prop) || ts.isGetAccessor(prop))
.map((prop) => parsePropDecorator(diagnostics, typeChecker, program, prop, decoratorName, newMembers))
.filter((prop): prop is ts.PropertyAssignment => prop != null);

if (properties.length > 0) {
Expand All @@ -55,14 +55,16 @@ export const propDecoratorsToStatic = (
* @param program a {@link ts.Program} object
* @param prop the TypeScript `PropertyDeclaration` to parse
* @param decoratorName the name of the decorator to look for
* @param newMembers a collection of parsed `@Prop` annotated class members. Used for `get()` decorated props to find a corresponding `set()`
* @returns a property assignment expression to be added to the Stencil component's class
*/
const parsePropDecorator = (
diagnostics: d.Diagnostic[],
typeChecker: ts.TypeChecker,
program: ts.Program,
prop: ts.PropertyDeclaration,
prop: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
decoratorName: string,
newMembers: ts.ClassElement[],
): ts.PropertyAssignment | null => {
const propDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName));
if (propDecorator == null) {
Expand Down Expand Up @@ -92,6 +94,7 @@ const parsePropDecorator = (
const symbol = typeChecker.getSymbolAtLocation(prop.name);
const type = typeChecker.getTypeAtLocation(prop);
const typeStr = propTypeFromTSType(type);
const foundSetter = ts.isGetAccessor(prop) ? findSetter(propName, newMembers) : null;

const propMeta: d.ComponentCompilerStaticProperty = {
type: typeStr,
Expand All @@ -100,6 +103,8 @@ const parsePropDecorator = (
required: prop.exclamationToken !== undefined && propName !== 'mode',
optional: prop.questionToken !== undefined,
docs: serializeSymbol(typeChecker, symbol),
getter: ts.isGetAccessor(prop),
setter: !!foundSetter,
};

// prop can have an attribute if type is NOT "unknown"
Expand All @@ -109,9 +114,30 @@ const parsePropDecorator = (
}

// extract default value
const initializer = prop.initializer;
if (initializer) {
propMeta.defaultValue = initializer.getText();
if (ts.isPropertyDeclaration(prop) && prop.initializer) {
propMeta.defaultValue = prop.initializer.getText();
} else if (ts.isGetAccessorDeclaration(prop)) {
// shallow comb to find default value for a getter
const returnStatement = prop.body?.statements.find((st) => ts.isReturnStatement(st)) as ts.ReturnStatement;
const returnExpression = returnStatement.expression;

if (returnExpression && ts.isLiteralExpression(returnExpression)) {
// the getter has a literal return value
propMeta.defaultValue = returnExpression.getText();
} else if (returnExpression && ts.isPropertyAccessExpression(returnExpression)) {
const nameToFind = returnExpression.name.getText();
const foundProp = findGetProp(nameToFind, newMembers);

if (foundProp && foundProp.initializer) {
propMeta.defaultValue = foundProp.initializer.getText();

if (propMeta.type === 'unknown') {
const type = typeChecker.getTypeAtLocation(foundProp);
propMeta.type = propTypeFromTSType(type);
propMeta.complexType = getComplexType(typeChecker, foundProp, type, program);
}
}
}
}

const staticProp = ts.factory.createPropertyAssignment(
Expand Down Expand Up @@ -164,7 +190,7 @@ const getReflect = (diagnostics: d.Diagnostic[], propDecorator: ts.Decorator, pr

const getComplexType = (
typeChecker: ts.TypeChecker,
node: ts.PropertyDeclaration,
node: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
type: ts.Type,
program: ts.Program,
): d.ComponentCompilerPropertyComplexType => {
Expand Down Expand Up @@ -293,3 +319,26 @@ const isAny = (t: ts.Type): boolean => {
}
return false;
};

/**
* Attempts to find a `set` member of the class when there is a corresponding getter
* @param propName - the property name of the setter to find
* @param members - all the component class members
* @returns the found typescript AST setter node
*/
const findSetter = (propName: string, members: ts.ClassElement[]): ts.SetAccessorDeclaration | undefined => {
return members.find((m) => ts.isSetAccessor(m) && m.name.getText() === propName) as
| ts.SetAccessorDeclaration
| undefined;
};

/**
* When attempting to find the default value of a decorated `get` prop, if a member like `this.something`
* is returned, this method is used to comb the class members to attempt to get it's default value
* @param propName - the property name of the member to find
* @param members - all the component class members
* @returns the found typescript AST class member
*/
const findGetProp = (propName: string, members: ts.ClassElement[]): ts.PropertyDeclaration | undefined => {
return members.find((m) => ts.isPropertyDeclaration(m) && m.name.getText() === propName) as ts.PropertyDeclaration;
};
2 changes: 2 additions & 0 deletions src/compiler/transformers/static-to-meta/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const parseStaticProps = (staticMembers: ts.ClassElement[]): d.ComponentC
complexType: val.complexType,
docs: val.docs,
internal: isInternal(val.docs),
getter: !!val.getter,
setter: !!val.setter,
};
});
};
6 changes: 6 additions & 0 deletions src/compiler/transformers/test/convert-decorators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ describe('convert-decorators', () => {
"required": false,
"optional": false,
"docs": { "tags": [], "text": "" },
"getter": false,
"setter": false,
"attribute": "val",
"reflect": false,
"defaultValue": "\\"initial value\\""
Expand Down Expand Up @@ -84,6 +86,8 @@ describe('convert-decorators', () => {
complexType: { original: 'string', resolved: 'string', references: {} },
docs: { tags: [], text: '' },
internal: false,
getter: false,
setter: false,
},
]);
});
Expand All @@ -110,6 +114,8 @@ describe('convert-decorators', () => {
complexType: { original: 'string', resolved: 'string', references: {} },
docs: { tags: [], text: '' },
internal: false,
getter: false,
setter: false,
},
]);
});
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/transformers/test/parse-comments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ describe('parse comments', () => {
reflect: false,
required: false,
type: 'string',
getter: false,
setter: false,
});
expect(t.method).toEqual({
complexType: {
Expand Down
Loading