Skip to content

Commit

Permalink
feat: Parse jsdoc comments where available and include them in AWST
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel committed Nov 27, 2024
1 parent dfad42c commit 7845ca1
Show file tree
Hide file tree
Showing 22 changed files with 1,936 additions and 29 deletions.
23 changes: 18 additions & 5 deletions examples/hello-world-abi/contract.algo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { arc4, log } from '@algorandfoundation/algorand-typescript'
import { arc4 } from '@algorandfoundation/algorand-typescript'

/**
* An abstract base class for a simple example contract
*/
abstract class Intermediate extends arc4.Contract {
/**
* sayBananas method
* @returns The string "Bananas"
*/
@arc4.abimethod({ allowActions: ['NoOp'], readonly: true })
public sayBananas(): string {
const result = `Bananas`
log(result)
return result
return `Bananas`
}
}

/**
* A simple hello world example contract
*/
export default class HelloWorldContract extends Intermediate {
/**
* sayHello method
* @param firstName The first name of the person to greet
* @param lastName THe last name of the person to greet
* @returns The string "Hello {firstName} {lastName"}
*/
public sayHello(firstName: string, lastName: string): string {
const result = `Hello ${firstName} ${lastName}`
log(result)
return result
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev:examples": "tsx src/cli.ts build examples --output-awst --output-awst-json",
"dev:approvals": "rimraf tests/approvals/out && tsx src/cli.ts build tests/approvals --dry-run",
"dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run",
"dev:testing": "tsx src/cli.ts build tests/approvals/arc4-types.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level debug --out-dir out/[name]",
"dev:testing": "tsx src/cli.ts build tests/approvals/jsdoc.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level info --out-dir out/[name]",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
4 changes: 4 additions & 0 deletions src/awst/wtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,18 @@ export namespace wtypes {
fields: Record<string, ARC4Type>
sourceLocation: SourceLocation | null
frozen: boolean
desc: string | null

constructor({
fields,
sourceLocation,
name,
desc,
frozen,
}: {
frozen: boolean
name: string
desc: string | null
fields: Record<string, ARC4Type>
sourceLocation?: SourceLocation
}) {
Expand All @@ -273,6 +276,7 @@ export namespace wtypes {
this.sourceLocation = sourceLocation ?? null
this.fields = fields
this.frozen = frozen
this.desc = desc
}
}
export class ARC4Tuple extends ARC4Type {
Expand Down
1 change: 1 addition & 0 deletions src/awst_build/arc4-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocat
return new ARC4StructType({
name: ptype.name,
module: ptype.module,
description: ptype.description,
fields: Object.fromEntries(ptype.orderedProperties().map(([p, pt]) => [p, ptypeToArc4EncodedType(pt, sourceLocation)])),
})

Expand Down
38 changes: 37 additions & 1 deletion src/awst_build/ast-visitors/base-visitor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ts from 'typescript'
import { nodeFactory } from '../../awst/node-factory'
import type { Expression, LValue, Statement } from '../../awst/nodes'
import type { Expression, LValue, MethodDocumentation, Statement } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { CodeError, NotSupported, TodoError } from '../../errors'
import { logger } from '../../logger'
Expand Down Expand Up @@ -554,6 +554,7 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
return new ObjectPType({
name: targetType.name,
module: targetType.module,
description: targetType.description,
properties: Object.fromEntries(
sourceType
.orderedProperties()
Expand Down Expand Up @@ -670,4 +671,39 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
isPublic,
}
}

protected getNodeDescription(node: ts.Node): string | null {
const docs = ts.getJSDocCommentsAndTags(node)
for (const doc of docs) {
if (ts.isJSDoc(doc)) {
return ts.getTextOfJSDocComment(doc.comment) ?? null
}
}
return null
}

protected getMethodDocumentation(node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration): MethodDocumentation {
const docs = Array.from(ts.getJSDocCommentsAndTags(node))
let description: string | null = null
const args = new Map<string, string>()
let returns: string | null = null
for (const doc of docs) {
if (ts.isJSDoc(doc)) {
description = ts.getTextOfJSDocComment(doc.comment) ?? null
if (doc.tags) docs.push(...doc.tags)
} else if (ts.isJSDocParameterTag(doc)) {
const paramName = this.textVisitor.accept(doc.name)
const paramComment = ts.getTextOfJSDocComment(doc.comment)

args.set(paramName, paramComment ?? '')
} else if (ts.isJSDocReturnTag(doc)) {
returns = ts.getTextOfJSDocComment(doc.comment) ?? null
}
}
return nodeFactory.methodDocumentation({
description,
args,
returns,
})
}
}
16 changes: 13 additions & 3 deletions src/awst_build/ast-visitors/contract-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
ctor: this._ctor ?? this.makeDefaultConstructor(sourceLocation),
methods: this._methods,
bases: this._contractPType.baseTypes.map((bt) => ContractReference.fromPType(bt)),
description: null,
description: this.getNodeDescription(classDec),
approvalProgram: this._contractPType.isARC4 ? null : this._approvalProgram,
clearProgram: this._clearStateProgram,
reservedScratchSpace: new Set(),
Expand Down Expand Up @@ -165,7 +165,12 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
const initializer = this.accept(node.initializer)

if (initializer instanceof GlobalStateFunctionResultBuilder) {
const storageDeclaration = initializer.buildStorageDeclaration(propertyName, this.sourceLocation(node.name), this._contractPType)
const storageDeclaration = initializer.buildStorageDeclaration(
propertyName,
this.sourceLocation(node.name),
this.getNodeDescription(node),
this._contractPType,
)
this.context.addStorageDeclaration(storageDeclaration)
if (initializer.initialValue) {
this._propertyInitialization.push(
Expand All @@ -183,7 +188,12 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
}
} else if (initializer instanceof BoxProxyExpressionBuilder || initializer instanceof LocalStateFunctionResultBuilder) {
this.context.addStorageDeclaration(
initializer.buildStorageDeclaration(propertyName, this.sourceLocation(node.name), this._contractPType),
initializer.buildStorageDeclaration(
propertyName,
this.sourceLocation(node.name),
this.getNodeDescription(node),
this._contractPType,
),
)
} else {
logger.error(initializer.sourceLocation, `Unsupported property type ${initializer.typeDescription}`)
Expand Down
8 changes: 2 additions & 6 deletions src/awst_build/ast-visitors/function-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,11 @@ export class FunctionVisitor
const body = assignDestructuredParams.length
? nodeFactory.block({ sourceLocation }, assignDestructuredParams, this.accept(node.body))
: this.accept(node.body)
const documentation = nodeFactory.methodDocumentation({
args: new Map(),
description: null,
returns: null,
})

return {
args,
body,
documentation,
documentation: this.getMethodDocumentation(node),
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/awst_build/eb/storage/box/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { ValueProxy } from '../value-proxy'
export abstract class BoxProxyExpressionBuilder<
TProxyType extends BoxMapPType | BoxRefPType | BoxPType,
> extends InstanceExpressionBuilder<TProxyType> {
buildStorageDeclaration(memberName: string, memberLocation: SourceLocation, contractType: ContractClassPType): AppStorageDeclaration {
buildStorageDeclaration(
memberName: string,
memberLocation: SourceLocation,
memberDescription: string | null,
contractType: ContractClassPType,
): AppStorageDeclaration {
codeInvariant(
this._expr instanceof BytesConstant,
`key${this.ptype instanceof BoxMapPType ? ' prefix' : ''} must be a compile time constant value if ${this.typeDescription} is assigned to a contract member`,
Expand All @@ -21,7 +26,7 @@ export abstract class BoxProxyExpressionBuilder<
ptype: this.ptype,
memberName: memberName,
keyOverride: this._expr ?? null,
description: null,
description: memberDescription,
definedIn: contractType,
})
}
Expand Down
9 changes: 7 additions & 2 deletions src/awst_build/eb/storage/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export class GlobalStateFunctionResultBuilder extends InstanceBuilder<GlobalStat
return this._ptype
}

buildStorageDeclaration(memberName: string, memberLocation: SourceLocation, contractType: ContractClassPType): AppStorageDeclaration {
buildStorageDeclaration(
memberName: string,
memberLocation: SourceLocation,
memberDescription: string | null,
contractType: ContractClassPType,
): AppStorageDeclaration {
if (this._expr)
codeInvariant(
this._expr instanceof BytesConstant,
Expand All @@ -119,7 +124,7 @@ export class GlobalStateFunctionResultBuilder extends InstanceBuilder<GlobalStat
ptype: this._ptype,
memberName: memberName,
keyOverride: this._expr ?? null,
description: null,
description: memberDescription,
definedIn: contractType,
})
}
Expand Down
9 changes: 7 additions & 2 deletions src/awst_build/eb/storage/local-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ export class LocalStateFunctionResultBuilder extends InstanceBuilder<LocalStateT
}
readonly ptype: LocalStateType

buildStorageDeclaration(memberName: string, memberLocation: SourceLocation, contractType: ContractClassPType): AppStorageDeclaration {
buildStorageDeclaration(
memberName: string,
memberLocation: SourceLocation,
memberDescription: string | null,
contractType: ContractClassPType,
): AppStorageDeclaration {
if (this._expr)
codeInvariant(
this._expr instanceof BytesConstant,
Expand All @@ -171,7 +176,7 @@ export class LocalStateFunctionResultBuilder extends InstanceBuilder<LocalStateT
ptype: this.ptype,
memberName: memberName,
keyOverride: this._expr ?? null,
description: null,
description: memberDescription,
definedIn: contractType,
})
}
Expand Down
4 changes: 2 additions & 2 deletions src/awst_build/models/app-storage-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { AppStorageKind, BytesEncoding } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { CodeError } from '../../errors'
import { invariant, utf8ToUint8Array } from '../../util'
import type { ContractClassPType, StorageProxyPType } from './../ptypes'
import { BoxMapPType, BoxPType, BoxRefPType, GlobalStateType, LocalStateType } from './../ptypes'
import type { ContractClassPType, StorageProxyPType } from '../ptypes'
import { BoxMapPType, BoxPType, BoxRefPType, GlobalStateType, LocalStateType } from '../ptypes'

export class AppStorageDeclaration {
readonly memberName: string
Expand Down
6 changes: 6 additions & 0 deletions src/awst_build/ptypes/arc4-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,28 @@ export class ARC4StructClass extends PType {
export class ARC4StructType extends ARC4EncodedType {
readonly name: string
readonly module: string
readonly description: string | undefined
readonly singleton = false
readonly fields: Record<string, ARC4EncodedType>
readonly sourceLocation: SourceLocation | undefined
constructor({
name,
module,
fields,
description,
sourceLocation,
}: {
name: string
module: string
description: string | undefined
fields: Record<string, ARC4EncodedType>
sourceLocation?: SourceLocation
}) {
super()
this.name = name
this.module = module
this.fields = fields
this.description = description
this.sourceLocation = sourceLocation
}

Expand All @@ -132,6 +136,7 @@ export class ARC4StructType extends ARC4EncodedType {
name: this.name,
fields: Object.fromEntries(Object.entries(this.fields).map(([f, t]) => [f, t.wtype])),
sourceLocation: this.sourceLocation,
desc: this.description ?? null,
frozen: false,
})
}
Expand All @@ -154,6 +159,7 @@ export const arc4StructBaseType = new ARC4StructType({
name: 'StructBase',
module: Constants.arc4EncodedTypesModuleName,
fields: {},
description: undefined,
})

export const Arc4TupleClass = new LibClassType({
Expand Down
7 changes: 5 additions & 2 deletions src/awst_build/ptypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ export class FunctionPType extends PType {
name: `${props.name}Result`,
module: props.module,
properties: props.returnType.properties,
description: props.returnType.description,
})
} else {
this.returnType = props.returnType
Expand Down Expand Up @@ -617,12 +618,13 @@ export class ArrayPType extends TransientType {
}

type ObjectPTypeArgs =
| { module: string; name: string; properties: Record<string, PType>; isAnonymous?: false }
| { module?: undefined; name?: undefined; properties: Record<string, PType>; isAnonymous: true }
| { module: string; name: string; description: string | undefined; properties: Record<string, PType>; isAnonymous?: false }
| { module?: undefined; name?: undefined; properties: Record<string, PType>; isAnonymous: true; description?: undefined }

export class ObjectPType extends PType {
readonly name: string
readonly module: string
readonly description: string | undefined
readonly properties: Record<string, PType>
readonly singleton = false
readonly isAnonymous: boolean
Expand All @@ -633,6 +635,7 @@ export class ObjectPType extends PType {
this.module = props.module ?? ''
this.properties = props.properties
this.isAnonymous = props.isAnonymous ?? false
this.description = props.description
}

static anonymous(props: Record<string, PType> | Array<[string, PType]>) {
Expand Down
Loading

0 comments on commit 7845ca1

Please sign in to comment.