Skip to content

Commit

Permalink
feat: prop get set new (#6050)
Browse files Browse the repository at this point in the history
* chore: updating things

* chore:

* chore: second pass. Basically working now

* chore: tidy up

* chore: fixed lazy initial setting

* chore: tidy

* auto add '@readonly' to jsdoc

* chore: more tests.. and ready?

* chore: make tests pass

* chore: one more test

---------

Co-authored-by: John Jenkins <[email protected]>
  • Loading branch information
johnjenkins and John Jenkins authored Nov 26, 2024
1 parent 7bdf128 commit 7ecb599
Show file tree
Hide file tree
Showing 33 changed files with 842 additions and 77 deletions.
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
63 changes: 56 additions & 7 deletions src/compiler/transformers/decorators-to-static/prop-decorator.ts
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

0 comments on commit 7ecb599

Please sign in to comment.