Skip to content

Commit

Permalink
initial support for decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious committed Oct 11, 2024
1 parent 00d64e2 commit 09e4efd
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-coins-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renoun': minor
---

Adds initial support for analyzing class member decorators.
151 changes: 151 additions & 0 deletions packages/renoun/src/utils/resolve-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": "undefined",
"element": {
"filePath": "test.ts",
Expand Down Expand Up @@ -1973,6 +1974,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": "undefined",
"filePath": "test.ts",
"isReadonly": false,
Expand Down Expand Up @@ -3761,6 +3763,7 @@ describe('processProperties', () => {
"kind": "Class",
"methods": [
{
"decorators": [],
"kind": "ClassMethod",
"name": "setValue",
"scope": undefined,
Expand Down Expand Up @@ -3812,6 +3815,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": undefined,
"filePath": "test.ts",
"isReadonly": false,
Expand Down Expand Up @@ -3891,6 +3895,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": "#666",
"filePath": "test.ts",
"isReadonly": false,
Expand Down Expand Up @@ -6515,6 +6520,7 @@ describe('processProperties', () => {
{
"accessors": [
{
"decorators": [],
"description": "Sets the count.",
"kind": "ClassSetAccessor",
"modifier": undefined,
Expand Down Expand Up @@ -6549,6 +6555,7 @@ describe('processProperties', () => {
"visibility": undefined,
},
{
"decorators": [],
"description": "Returns the current count.",
"kind": "ClassGetAccessor",
"name": "accessorCount",
Expand Down Expand Up @@ -6593,6 +6600,7 @@ describe('processProperties', () => {
"kind": "Class",
"methods": [
{
"decorators": [],
"description": "Increments the count.",
"kind": "ClassMethod",
"name": "increment",
Expand All @@ -6611,6 +6619,7 @@ describe('processProperties', () => {
"visibility": undefined,
},
{
"decorators": [],
"description": "Decrements the count.",
"kind": "ClassMethod",
"name": "decrement",
Expand All @@ -6629,6 +6638,7 @@ describe('processProperties', () => {
"visibility": undefined,
},
{
"decorators": [],
"description": "Returns the current count.",
"kind": "ClassMethod",
"name": "getCount",
Expand Down Expand Up @@ -6668,6 +6678,7 @@ describe('processProperties', () => {
"visibility": "public",
},
{
"decorators": [],
"kind": "ClassMethod",
"name": "getStaticCount",
"scope": "static",
Expand Down Expand Up @@ -6697,6 +6708,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": 0,
"filePath": "test.ts",
"isReadonly": false,
Expand All @@ -6718,6 +6730,7 @@ describe('processProperties', () => {
"visibility": undefined,
},
{
"decorators": [],
"defaultValue": 0,
"filePath": "test.ts",
"isReadonly": false,
Expand Down Expand Up @@ -8475,6 +8488,7 @@ describe('processProperties', () => {
"kind": "Class",
"methods": [
{
"decorators": [],
"kind": "ClassMethod",
"name": "increment",
"scope": undefined,
Expand Down Expand Up @@ -8504,6 +8518,7 @@ describe('processProperties', () => {
},
"properties": [
{
"decorators": [],
"defaultValue": 0,
"filePath": "test.ts",
"isReadonly": false,
Expand Down Expand Up @@ -8757,4 +8772,140 @@ describe('processProperties', () => {
}
`)
})

test('class decorators', () => {
const sourceFile = project.createSourceFile(
'test.ts',
dedent`
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log("LOG: Entering method.")
const result = target.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet(): void {
console.log("Hello, " + this.name);
}
}`,
{ overwrite: true }
)
const classDeclaration = sourceFile.getClassOrThrow('Person')
const types = resolveType(classDeclaration.getType(), classDeclaration)

expect(types).toMatchInlineSnapshot(`
{
"constructors": [
{
"kind": "FunctionSignature",
"modifier": undefined,
"parameters": [
{
"context": "parameter",
"defaultValue": undefined,
"description": undefined,
"filePath": "test.ts",
"isOptional": false,
"kind": "String",
"name": "name",
"position": {
"end": {
"column": 27,
"line": 20,
},
"start": {
"column": 15,
"line": 20,
},
},
"text": "string",
"value": undefined,
},
],
"returnType": "Person",
"text": "(name: string) => Person",
},
],
"filePath": "test.ts",
"kind": "Class",
"methods": [
{
"decorators": [
{
"arguments": [],
"name": "loggedMethod",
},
],
"kind": "ClassMethod",
"name": "greet",
"scope": undefined,
"signatures": [
{
"kind": "FunctionSignature",
"modifier": undefined,
"parameters": [],
"returnType": "void",
"text": "() => void",
},
],
"text": "() => void",
"visibility": undefined,
},
],
"name": "Person",
"position": {
"end": {
"column": 2,
"line": 28,
},
"start": {
"column": 1,
"line": 17,
},
},
"properties": [
{
"decorators": [],
"defaultValue": undefined,
"filePath": "test.ts",
"isReadonly": false,
"kind": "String",
"name": "name",
"position": {
"end": {
"column": 16,
"line": 18,
},
"start": {
"column": 3,
"line": 18,
},
},
"scope": undefined,
"text": "string",
"value": undefined,
"visibility": undefined,
},
],
"text": "Person",
}
`)
})
})
18 changes: 18 additions & 0 deletions packages/renoun/src/utils/resolve-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
ClassDeclaration,
Decorator,
FunctionDeclaration,
GetAccessorDeclaration,
MethodDeclaration,
Expand Down Expand Up @@ -151,6 +152,10 @@ export interface ClassType extends BaseType {
export interface SharedClassMemberType extends BaseType {
scope?: 'abstract' | 'static'
visibility?: 'private' | 'protected' | 'public'
decorators: {
name: string
arguments?: string[]
}[]
}

export interface ClassGetAccessorType extends SharedClassMemberType {
Expand Down Expand Up @@ -1456,6 +1461,7 @@ function resolveClassAccessor(
scope: getScope(accessor),
visibility: getVisibility(accessor),
text: accessor.getType().getText(accessor, TYPE_FORMAT_FLAGS),
decorators: resolveDecorators(accessor.getDecorators()),
...getJsDocMetadata(accessor),
}

Expand Down Expand Up @@ -1500,6 +1506,7 @@ function resolveClassMethod(
visibility: getVisibility(method),
signatures: resolveCallSignatures(callSignatures, method, filter),
text: method.getType().getText(method, TYPE_FORMAT_FLAGS),
decorators: resolveDecorators(method.getDecorators()),
...getJsDocMetadata(method),
} satisfies ClassMethodType
}
Expand All @@ -1521,6 +1528,7 @@ function resolveClassProperty(
scope: getScope(property),
visibility: getVisibility(property),
isReadonly: property.isReadonly(),
decorators: resolveDecorators(property.getDecorators()),
} satisfies ClassPropertyType
}

Expand All @@ -1529,6 +1537,16 @@ function resolveClassProperty(
)
}

/** Resolve the decorators of a class member. */
function resolveDecorators(
decorators: Decorator[]
): { name: string; arguments: string[] }[] {
return decorators.map((decorator) => ({
name: decorator.getName(),
arguments: decorator.getArguments().map((argument) => argument.getText()),
}))
}

/** Get the primary declaration of a symbol preferred by type hierarchy. */
function getPrimaryDeclaration(symbol: Symbol | undefined): Node | undefined {
if (!symbol) return undefined
Expand Down

0 comments on commit 09e4efd

Please sign in to comment.