docs-util: created docblock-generator tool (#6096)

This commit is contained in:
Shahed Nasser
2024-01-24 12:13:40 +02:00
committed by GitHub
parent d68089b2aa
commit f29948a6a8
37 changed files with 2684 additions and 115 deletions

View File

@@ -0,0 +1,145 @@
/* eslint-disable no-case-declarations */
import ts from "typescript"
import Formatter from "./formatter.js"
import KindsRegistry from "./kinds/registry.js"
import nodeHasComments from "../utils/node-has-comments.js"
export type Options = {
paths: string[]
dryRun?: boolean
}
/**
* A class used to generate docblock for one or multiple file paths.
*/
class DocblockGenerator {
protected options: Options
protected program?: ts.Program
protected checker?: ts.TypeChecker
protected formatter: Formatter
protected kindsRegistry?: KindsRegistry
constructor(options: Options) {
this.options = options
this.formatter = new Formatter()
}
/**
* Generate the docblock for the paths specified in the {@link options} class property.
*/
async run() {
this.program = ts.createProgram(this.options.paths, {})
this.checker = this.program.getTypeChecker()
this.kindsRegistry = new KindsRegistry(this.checker)
const printer = ts.createPrinter({
removeComments: false,
})
await Promise.all(
this.program.getSourceFiles().map(async (file) => {
// Ignore .d.ts files
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
return
}
console.log(`Generating for ${file.fileName}...`)
let fileContent = file.getFullText()
let fileComments: string = ""
const documentChild = (node: ts.Node, topLevel = false) => {
const isSourceFile = ts.isSourceFile(node)
const origNodeText = node.getFullText().trim()
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
let docComment: string | undefined
if (nodeKindGenerator && this.canDocumentNode(node)) {
docComment = nodeKindGenerator.getDocBlock(node)
if (docComment.length) {
if (isSourceFile) {
fileComments = docComment
} else {
ts.addSyntheticLeadingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
docComment,
true
)
}
}
}
ts.forEachChild(node, (childNode) =>
documentChild(childNode, isSourceFile)
)
if (!isSourceFile && topLevel) {
const newNodeText = printer.printNode(
ts.EmitHint.Unspecified,
node,
file
)
if (newNodeText !== origNodeText) {
fileContent = fileContent.replace(origNodeText, newNodeText)
}
}
}
documentChild(file, true)
if (!this.options.dryRun) {
ts.sys.writeFile(
file.fileName,
this.formatter.addCommentsToSourceFile(
fileComments,
await this.formatter.formatStr(fileContent, file.fileName)
)
)
}
console.log(`Finished generating docblock for ${file.fileName}.`)
})
)
this.reset()
}
/**
* Checks whether a file is included in the specified files.
*
* @param {string} fileName - The file to check for.
* @returns {boolean} Whether the file can have docblocks generated for it.
*/
isFileIncluded(fileName: string): boolean {
return this.options.paths.some((path) => path.includes(fileName))
}
/**
* Checks whether a node can be documented.
*
* @privateRemark
* I'm leaving this method in case other conditions arise for a node to be documented.
* Otherwise, we can directly use the {@link nodeHasComments} function.
*
* @param {ts.Node} node - The node to check for.
* @returns {boolean} Whether the node can be documented.
*/
canDocumentNode(node: ts.Node): boolean {
// check if node already has docblock
return !nodeHasComments(node)
}
/**
* Reset the generator's properties for new usage.
*/
reset() {
this.program = undefined
this.checker = undefined
}
}
export default DocblockGenerator

View File

@@ -0,0 +1,238 @@
import getMonorepoRoot from "../utils/get-monorepo-root.js"
import { ESLint, Linter } from "eslint"
import path from "path"
import dirname from "../utils/dirname.js"
import { minimatch } from "minimatch"
import { existsSync } from "fs"
import getRelativePaths from "../utils/get-relative-paths.js"
/**
* A class used to apply formatting to files using ESLint and other formatting options.
*/
class Formatter {
protected cwd: string
protected eslintConfig?: Linter.Config
protected generalESLintConfig?: Linter.ConfigOverride<Linter.RulesRecord>
protected configForFile: Map<
string,
Linter.ConfigOverride<Linter.RulesRecord>
>
constructor() {
this.cwd = getMonorepoRoot()
this.configForFile = new Map()
}
/**
* Adds new lines before and after a comment if it's preceeded/followed immediately by a word (not by an empty line).
*
* @param {string} content - The content to format.
* @returns {string} The returned formatted content.
*/
normalizeCommentNewLine(content: string): string {
return content
.replaceAll(/(.)\n(\s*)\/\*\*/g, "$1\n\n$2/**")
.replaceAll(/\*\/\s*(.)/g, "*/\n$1")
}
/**
* Normalizes an ESLint overrides configuration object. If a file name is specified, the configuration are normalized to
* include the `tsconfig` related to the file. If a file name isn't specified, the tsconfig file path names
* in the `parserConfig.project` array are normalized to have a full relative path (as that is required by ESLint).
*
* @param {Linter.ConfigOverride<Linter.RulesRecord>} config - The original configuration object.
* @param {string} fileName - The file name that
* @returns {Linter.ConfigOverride<Linter.RulesRecord>} The normalized and cloned configuration object.
*/
normalizeOverridesConfigObject(
config: Linter.ConfigOverride<Linter.RulesRecord>,
fileName?: string
): Linter.ConfigOverride<Linter.RulesRecord> {
// clone config
const newConfig = structuredClone(config)
if (!newConfig.parserOptions) {
return newConfig
}
if (fileName) {
const packagePattern = /^(?<packagePath>.*\/packages\/[^/]*).*$/
// try to manually set the project of the parser options
const matchFilePackage = packagePattern.exec(fileName)
if (matchFilePackage?.groups?.packagePath) {
const tsConfigPath = path.join(
matchFilePackage.groups.packagePath,
"tsconfig.json"
)
const tsConfigSpecPath = path.join(
matchFilePackage.groups.packagePath,
"tsconfig.spec.json"
)
newConfig.parserOptions.project = [
existsSync(tsConfigSpecPath)
? tsConfigSpecPath
: existsSync(tsConfigPath)
? tsConfigPath
: [
...getRelativePaths(
newConfig.parserOptions.project || [],
this.cwd
),
],
]
}
} else if (newConfig.parserOptions.project?.length) {
// fix parser projects paths to be relative to this script
newConfig.parserOptions.project = getRelativePaths(
newConfig.parserOptions.project as string[],
this.cwd
)
}
return newConfig
}
/**
* Retrieves the general ESLint configuration and sets it to the `eslintConfig` class property, if it's not already set.
* It also tries to set the `generalESLintConfig` class property to the override configuration in the `eslintConfig`
* whose `files` array includes `*.ts`.
*/
async getESLintConfig() {
if (this.eslintConfig) {
return
}
this.eslintConfig = (
await import(
path.relative(dirname(), path.join(this.cwd, ".eslintrc.js"))
)
).default as Linter.Config
this.generalESLintConfig = this.eslintConfig!.overrides?.find((item) =>
item.files.includes("*.ts")
)
if (this.generalESLintConfig) {
this.generalESLintConfig = this.normalizeOverridesConfigObject(
this.generalESLintConfig
)
}
}
/**
* Retrieves the normalized ESLint overrides configuration for a specific file.
*
* @param {string} filePath - The file's path.
* @returns {Promise<Linter.ConfigOverride<Linter.RulesRecord> | undefined>} The normalized configuration object or `undefined` if not found.
*/
async getESLintOverridesConfigForFile(
filePath: string
): Promise<Linter.ConfigOverride<Linter.RulesRecord> | undefined> {
await this.getESLintConfig()
if (this.configForFile.has(filePath)) {
return this.configForFile.get(filePath)!
}
let relevantConfig = this.eslintConfig!.overrides?.find((item) => {
if (typeof item.files === "string") {
return minimatch(filePath, item.files)
}
return item.files.some((file) => minimatch(filePath, file))
})
if (!relevantConfig && !this.generalESLintConfig) {
return undefined
}
relevantConfig = this.normalizeOverridesConfigObject(
structuredClone(relevantConfig || this.generalESLintConfig!),
filePath
)
relevantConfig!.files = [path.relative(this.cwd, filePath)]
this.configForFile.set(filePath, relevantConfig)
return relevantConfig
}
/**
* Formats a string with ESLint.
*
* @param {string} content - The content to format.
* @param {string} fileName - The path to the file that the content belongs to.
* @returns {Promise<string>} The formatted content.
*/
async formatStrWithEslint(
content: string,
fileName: string
): Promise<string> {
const relevantConfig = await this.getESLintOverridesConfigForFile(fileName)
const eslint = new ESLint({
overrideConfig: {
...this.eslintConfig,
overrides: relevantConfig ? [relevantConfig] : undefined,
},
cwd: this.cwd,
resolvePluginsRelativeTo: this.cwd,
fix: true,
ignore: false,
})
let newContent = content
const result = await eslint.lintText(content, {
filePath: fileName,
})
if (result.length) {
newContent = result[0].output || newContent
}
return newContent
}
/**
* Applies all formatting types to a string.
*
* @param {string} content - The content to format.
* @param {string} fileName - The path to the file that holds the content.
* @returns {Promise<string>} The formatted content.
*/
async formatStr(content: string, fileName: string): Promise<string> {
const newContent = await this.formatStrWithEslint(content, fileName)
let normalizedContent = this.normalizeCommentNewLine(newContent)
if (normalizedContent !== newContent) {
/**
* Since adding the new lines after comments as done in {@link normalizeCommentNewLine} method may lead to linting errors,
* we have to rerun the {@link formatStrWithEslint}. It's not possible to run {@link normalizeCommentNewLine} the first time
* and provide the expected result.
*/
normalizedContent = await this.formatStrWithEslint(
normalizedContent,
fileName
)
}
return normalizedContent
}
/**
* Adds comments of a source file to the top of the file's content. It should have additional extra line after the comment.
* If the comment's length is 0, the `content` is returned as is.
*
* @param {string} comment - The comments of the source file.
* @param {string} content - The source file's comments.
* @returns {string} The full content with the comments.
*/
addCommentsToSourceFile(comment: string, content: string): string {
return comment.length ? `/**\n ${comment}*/\n\n${content}` : content
}
}
export default Formatter

View File

@@ -0,0 +1,503 @@
import ts from "typescript"
import {
DOCBLOCK_START,
DOCBLOCK_END_LINE,
DOCBLOCK_DOUBLE_LINES,
DOCBLOCK_NEW_LINE,
} from "../../constants.js"
import getSymbol from "../../utils/get-symbol.js"
import KnowledgeBaseFactory, {
RetrieveOptions,
} from "../knowledge-base-factory.js"
import {
getCustomNamespaceTag,
shouldHaveCustomNamespace,
} from "../../utils/medusa-react-utils.js"
import {
camelToWords,
capitalize,
normalizeName,
} from "../../utils/str-formatting.js"
export type GeneratorOptions = {
checker: ts.TypeChecker
kinds?: ts.SyntaxKind[]
}
export type GetDocBlockOptions = {
addEnd?: boolean
summaryPrefix?: string
}
type CommonDocsOptions = {
addDefaultSummary?: boolean
prefixWithLineBreaks?: boolean
}
/**
* Class used to generate docblocks for basic kinds. It can be
* extended for kinds requiring more elaborate TSDocs.
*/
class DefaultKindGenerator<T extends ts.Node = ts.Node> {
static DEFAULT_ALLOWED_NODE_KINDS = [
ts.SyntaxKind.SourceFile,
ts.SyntaxKind.ClassDeclaration,
ts.SyntaxKind.EnumDeclaration,
ts.SyntaxKind.EnumMember,
ts.SyntaxKind.ModuleDeclaration,
ts.SyntaxKind.PropertyDeclaration,
ts.SyntaxKind.InterfaceDeclaration,
ts.SyntaxKind.TypeAliasDeclaration,
ts.SyntaxKind.PropertySignature,
]
protected allowedKinds: ts.SyntaxKind[]
protected checker: ts.TypeChecker
protected defaultSummary = "{summary}"
protected knowledgeBaseFactory: KnowledgeBaseFactory
constructor({ checker, kinds }: GeneratorOptions) {
this.allowedKinds = kinds || DefaultKindGenerator.DEFAULT_ALLOWED_NODE_KINDS
this.checker = checker
this.knowledgeBaseFactory = new KnowledgeBaseFactory()
}
/**
* @returns the kinds that are handled by this generator.
*/
getAllowedKinds(): ts.SyntaxKind[] {
return this.allowedKinds
}
/**
* Check whether this generator can be used for a node based on the node's kind.
*
* @param {ts.Node} node - The node to check for.
* @returns {boolean} Whether this generator can be used with the specified node.
*/
isAllowed(node: ts.Node): node is T {
return this.allowedKinds.includes(node.kind)
}
/**
* Retrieve the doc block for the passed node.
*
* @param {T | ts.Node} node - The node to retrieve the docblock for.
* @param {GetDocBlockOptions} options - Options useful for children classes of this class to specify the formatting of the docblock.
* @returns {string} The node's docblock.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getDocBlock(
node: T | ts.Node,
options: GetDocBlockOptions = { addEnd: true }
): string {
let str = DOCBLOCK_START
const summary = this.getNodeSummary({ node })
switch (node.kind) {
case ts.SyntaxKind.EnumDeclaration:
str += `@enum${DOCBLOCK_DOUBLE_LINES}${summary}`
break
case ts.SyntaxKind.TypeAliasDeclaration:
str += `@interface${DOCBLOCK_DOUBLE_LINES}${summary}`
break
default:
str += summary
}
str += this.getCommonDocs(node, {
prefixWithLineBreaks: true,
})
return `${str}${options.addEnd ? DOCBLOCK_END_LINE : ""}`
}
/**
* Retrieves the summary comment of a node. It gives precedense to the node's symbol if it's provided/retrieved and if it's available using the {@link getSymbolDocBlock}.
* Otherwise, it retrieves the comments of the type using the {@link getTypeDocBlock}
* @returns {string} The summary comment.
*/
getNodeSummary({
node,
symbol,
nodeType,
}: {
/**
* The node to retrieve the summary comment for.
*/
node: T | ts.Node
/**
* Optionally provide the node's symbol. If not provided, the
* method will try to retrieve it.
*/
symbol?: ts.Symbol
/**
* Optionally provide the node's type. If not provided, the method
* will try to retrieve it.
*/
nodeType?: ts.Type
}): string {
const knowledgeBaseOptions = this.getKnowledgeOptions(node)
if (!nodeType) {
nodeType =
"type" in node && node.type && ts.isTypeNode(node.type as ts.Node)
? this.checker.getTypeFromTypeNode(node.type as ts.TypeNode)
: symbol
? this.checker.getTypeOfSymbolAtLocation(symbol, node)
: this.checker.getTypeAtLocation(node)
}
if (!symbol) {
symbol = getSymbol(node, this.checker)
}
let summary = ""
if (symbol) {
summary = this.getSymbolDocBlock(symbol, knowledgeBaseOptions)
}
if (!summary.length) {
summary = this.getTypeDocBlock(nodeType, knowledgeBaseOptions)
}
return summary.length > 0 ? summary : this.defaultSummary
}
/**
* Retrieves the summary comment of a type. It tries to retrieve from the alias symbol, type arguments, or {@link KnowledgeBaseFactory}.
* If no summary comments are found, the {@link defaultSummary} is used.
*
* @param {ts.Type} nodeType - The type of a node.
* @returns {string} The summary comment.
*/
private getTypeDocBlock(
nodeType: ts.Type,
knowledgeBaseOptions?: Partial<RetrieveOptions>
): string {
if (nodeType.aliasSymbol || nodeType.symbol) {
const symbolDoc = this.getSymbolDocBlock(
nodeType.aliasSymbol || nodeType.symbol
)
if (symbolDoc.length) {
return symbolDoc
}
}
const typeArguments = this.checker.getTypeArguments(
nodeType as ts.TypeReference
)
if (typeArguments.length) {
// take only the first type argument to account
const typeArgumentDoc = this.getTypeDocBlock(typeArguments[0])
if (!typeArgumentDoc.length) {
const tryKnowledgeSummary = this.knowledgeBaseFactory.tryToGetSummary({
...knowledgeBaseOptions,
str: this.checker.typeToString(nodeType),
})
if (tryKnowledgeSummary?.length) {
return tryKnowledgeSummary
}
}
if (!this.checker.isArrayType(nodeType)) {
return typeArgumentDoc
}
// do some formatting if the encapsulating type is an array
return `The list of ${capitalize(typeArgumentDoc) || this.defaultSummary}`
}
return (
this.knowledgeBaseFactory.tryToGetSummary({
...knowledgeBaseOptions,
str: this.checker.typeToString(nodeType),
}) || ""
)
}
/**
* Retrieves the docblock of a symbol. It tries to retrieve it using the symbol's `getDocumentationComment` and `getJsDocTags`
* methods. If both methods don't return any comments, it tries to get the comments from the {@link KnowledgeBaseFactory}.
*
* @param {ts.Symbol} symbol - The symbol to retrieve its docblock.
* @returns {string} The symbol's docblock.
*/
private getSymbolDocBlock(
symbol: ts.Symbol,
knowledgeBaseOptions?: Partial<RetrieveOptions>
): string {
const commentDisplayParts = symbol.getDocumentationComment(this.checker)
if (!commentDisplayParts.length) {
// try to get description from the first JSDoc comment
const jsdocComments = symbol.getJsDocTags(this.checker)
if (jsdocComments.length) {
jsdocComments
.find((tag) => (tag.text?.length || 0) > 0)
?.text!.forEach((tagText) => {
commentDisplayParts.push(tagText)
})
}
}
if (!commentDisplayParts.length) {
return (
this.knowledgeBaseFactory.tryToGetSummary({
...knowledgeBaseOptions,
str: this.checker.typeToString(this.checker.getTypeOfSymbol(symbol)),
}) ||
this.knowledgeBaseFactory.tryToGetSummary({
...knowledgeBaseOptions,
str: symbol.name,
}) ||
""
)
}
return ts
.displayPartsToString(commentDisplayParts)
.replaceAll("\n", DOCBLOCK_NEW_LINE)
}
/**
* Retrieves docblocks based on decorators used on a symbol.
*
* @param {ts.Symbol} symbol - The symbol to retrieve its decorators docblock.
* @returns {string} The symbol's decorators docblock.
*/
getDecoratorDocs(symbol: ts.Symbol): string {
let str = ""
symbol.declarations?.forEach((declaration) => {
const modifiers =
"modifiers" in declaration && declaration.modifiers
? (declaration.modifiers as ts.NodeArray<ts.Modifier>)
: []
modifiers.forEach((modifier) => {
if (!ts.isDecorator(modifier)) {
return
}
// check for decorator text
;(modifier as ts.Decorator).forEachChild((childNode) => {
if (ts.isCallExpression(childNode)) {
const childNodeExpression = (childNode as ts.CallExpression)
.expression
if (ts.isIdentifier(childNodeExpression)) {
switch (childNodeExpression.escapedText) {
case "FeatureFlagEntity":
// add the `@featureFlag` tag.
str += `${DOCBLOCK_DOUBLE_LINES}@featureFlag [flag_name]`
break
case "BeforeInsert":
case "BeforeLoad":
case "AfterLoad":
// add `@apiIgnore` tag
str += `${DOCBLOCK_DOUBLE_LINES}@apiIgnore`
}
}
}
})
})
})
return str
}
/**
* Retrieve docblocks that are common to all nodes, despite their kind.
*
* @param {T | ts.Node} node - The node to retrieve its common doc blocks.
* @param {CommonDocsOptions} options - Formatting options.
* @returns {string} The common docblocks.
*/
getCommonDocs(
node: T | ts.Node,
options: CommonDocsOptions = { addDefaultSummary: false }
): string {
const tags = new Set<string>()
const symbol = getSymbol(node, this.checker)
if (!symbol) {
return ""
}
if (ts.isSourceFile(node)) {
// comments for source files must start with this tag
tags.add(`@packageDocumentation`)
}
if (options.addDefaultSummary) {
tags.add(this.defaultSummary)
}
// check for private or protected modifiers
// and if found, add the `@ignore` tag.
symbol.declarations?.some((declaration) => {
if (!("modifiers" in declaration) || !declaration.modifiers) {
return false
}
const hasPrivateOrProtected = (
declaration.modifiers as ts.NodeArray<ts.Modifier>
).find((modifier) => {
modifier.kind === ts.SyntaxKind.PrivateKeyword ||
modifier.kind === ts.SyntaxKind.ProtectedKeyword
})
if (!hasPrivateOrProtected) {
return false
}
tags.add("@ignore")
return true
})
// if a symbol's name starts with `_` then we
// should add the `@ignore` tag
if (symbol.getName().startsWith("_")) {
tags.add("@ignore")
}
// check if any docs can be added for the symbol's
// decorators
this.getDecoratorDocs(symbol)
.split(`${DOCBLOCK_DOUBLE_LINES}`)
.filter((docItem) => docItem.length > 0)
.forEach((docItem) => tags.add(docItem))
// add `@expandable` tag if the resource is
if (ts.isPropertyDeclaration(node)) {
const symbolType = this.checker.getTypeOfSymbol(symbol)
if (
symbolType.symbol?.declarations?.length &&
ts.isClassDeclaration(symbolType.symbol?.declarations[0]) &&
this.isEntity({
heritageClauses: (
symbolType.symbol?.declarations[0] as ts.ClassDeclaration
).heritageClauses,
node: symbolType.symbol?.declarations[0],
})
) {
tags.add(`@expandable`)
}
}
// check if custom namespace should be added
if (shouldHaveCustomNamespace(node)) {
tags.add(getCustomNamespaceTag(node))
}
// check for default value
if (
"initializer" in node &&
node.initializer &&
ts.isExpression(node.initializer as ts.Node)
) {
const initializer = node.initializer as ts.Expression
// retrieve default value only if the value is numeric, string, or boolean
const defaultValue =
ts.isNumericLiteral(initializer) || ts.isStringLiteral(initializer)
? initializer.getText()
: initializer.kind === ts.SyntaxKind.FalseKeyword
? "false"
: initializer.kind === ts.SyntaxKind.TrueKeyword
? "true"
: ""
if (defaultValue.length) {
tags.add(`@defaultValue ${defaultValue}`)
}
}
let str = ""
tags.forEach((tag) => {
if (str.length > 0) {
str += `${DOCBLOCK_DOUBLE_LINES}`
}
str += `${tag}`
})
if (str.length && options.prefixWithLineBreaks) {
str = `${DOCBLOCK_DOUBLE_LINES}${str}`
}
return str
}
/**
* Check if a node is a Medusa entity.
* @returns {boolean} Whether the node is a Medusa entity.
*/
isEntity({
/**
* The inherit/extend keywords of the node.
*/
heritageClauses,
/**
* Optionally provide the node to accurately retrieve its type name.
*/
node,
}: {
heritageClauses?: ts.NodeArray<ts.HeritageClause>
node?: ts.Node
}): boolean {
return (
heritageClauses?.some((heritageClause) => {
return heritageClause.types.some((heritageClauseType) => {
const symbolType = this.checker.getTypeAtLocation(
heritageClauseType.expression
)
if (
this.checker
.typeToString(symbolType, node, undefined)
.includes("BaseEntity")
) {
return true
}
if (
symbolType.symbol.valueDeclaration &&
"heritageClauses" in symbolType.symbol.valueDeclaration
) {
return this.isEntity({
heritageClauses: symbolType.symbol.valueDeclaration
.heritageClauses as ts.NodeArray<ts.HeritageClause>,
node,
})
}
return false
})
}) || false
)
}
getKnowledgeOptions(node: ts.Node): Partial<RetrieveOptions> {
const rawParentName =
"name" in node.parent &&
node.parent.name &&
ts.isIdentifier(node.parent.name as ts.Node)
? (node.parent.name as ts.Identifier).getText()
: undefined
return {
kind: node.kind,
templateOptions: {
rawParentName,
parentName: rawParentName
? camelToWords(normalizeName(rawParentName))
: undefined,
},
}
}
}
export default DefaultKindGenerator

View File

@@ -0,0 +1,104 @@
import ts from "typescript"
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js"
import {
camelToWords,
normalizeName,
snakeToWords,
} from "../../utils/str-formatting.js"
/**
* A class that generates doc blocks for properties in a DTO interface/type.
*/
class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.PropertySignature]
/**
* Check that the generator can handle generating for the node.
*
* @param {ts.Node} node - The node to check.
* @returns {boolean} Whether the generator can handle generating for the node.
*/
isAllowed(node: ts.Node): node is ts.PropertySignature {
if (!super.isAllowed(node)) {
return false
}
return (
this.getParentName((node as ts.PropertySignature).parent).endsWith(
"DTO"
) || false
)
}
getDocBlock(
node: ts.PropertyDeclaration | ts.Node,
options?: GetDocBlockOptions
): string {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
}
let str = DOCBLOCK_START
const rawParentName = this.getParentName(node.parent)
const parentName = this.formatInterfaceName(rawParentName)
// try first to retrieve the summary from the knowledge base if it exists.
const summary = this.knowledgeBaseFactory.tryToGetSummary({
str: node.name.getText(),
kind: node.kind,
templateOptions: {
rawParentName,
parentName,
},
})
if (summary) {
str += summary
} else {
// check if the property's type is interface/type/class
const propertyType = this.checker.getTypeAtLocation(node)
if (propertyType.isClassOrInterface()) {
str += `The associated ${this.formatInterfaceName(
this.checker.typeToString(propertyType)
)}.`
} else if (
"intrinsicName" in propertyType &&
propertyType.intrinsicName === "boolean"
) {
str += `Whether the ${parentName} ${snakeToWords(node.name.getText())}.`
} else {
// format summary
str += `The ${snakeToWords(node.name.getText())} of the ${parentName}.`
}
}
return `${str}${DOCBLOCK_END_LINE}`
}
/**
* Format the name of the interface/type.
*
* @param {string} name - The name to format.
* @returns {string} The formatted name.
*/
formatInterfaceName(name: string): string {
return camelToWords(normalizeName(name))
}
/**
* Get the name of the parent interface/type.
*
* @param {ts.InterfaceDeclaration | ts.TypeLiteralNode} parent - The parent node.
* @returns {string} The name of the parent.
*/
getParentName(parent: ts.InterfaceDeclaration | ts.TypeLiteralNode): string {
if (ts.isInterfaceDeclaration(parent)) {
return parent.name.getText()
}
return this.checker.typeToString(this.checker.getTypeFromTypeNode(parent))
}
}
export default DTOPropertyGenerator

View File

@@ -0,0 +1,261 @@
import ts from "typescript"
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
import {
DOCBLOCK_NEW_LINE,
DOCBLOCK_END_LINE,
DOCBLOCK_START,
DOCBLOCK_DOUBLE_LINES,
} from "../../constants.js"
import getSymbol from "../../utils/get-symbol.js"
export type FunctionNode =
| ts.MethodDeclaration
| ts.MethodSignature
| ts.FunctionDeclaration
| ts.ArrowFunction
type VariableNode = ts.VariableDeclaration | ts.VariableStatement
export type FunctionOrVariableNode = FunctionNode | ts.VariableStatement
/**
* Docblock generator for functions.
*/
// eslint-disable-next-line max-len
class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode> {
protected methodKinds: ts.SyntaxKind[] = [
ts.SyntaxKind.MethodDeclaration,
ts.SyntaxKind.MethodSignature,
]
protected functionKinds: ts.SyntaxKind[] = [ts.SyntaxKind.FunctionDeclaration]
protected allowedKinds: ts.SyntaxKind[] = [
...this.methodKinds,
...this.functionKinds,
]
/**
* Checks whether a node is considered a function node. A node is considered a function node if:
*
* 1. It is a method declaration (typically in classes), a method signature (typically in interfaces), or a function declaration.
* 2. An arrow function. However, for better docblock placement and formatting, we detect the variable statement surrounding the arrow function
* rather than the arrow function itself.
*
* @param {ts.Node} node - The node to check.
* @returns {boolean} Whether the node is a function node and can be handled by this generator.
*/
isAllowed(node: ts.Node): node is FunctionOrVariableNode {
if (!super.isAllowed(node)) {
return ts.isVariableStatement(node) && this.isFunctionVariable(node)
}
return true
}
/**
* Checks whether a node is a variable statement/declaration with underlying node function
* using the {@link extractFunctionNode} method.
*
* @param {ts.Node} node - The node to check.
* @returns {boolean} Whether the node is a variable statement/declaration with underlying node function.
*/
isFunctionVariable(node: ts.Node): node is VariableNode {
if (ts.isVariableStatement(node)) {
return node.declarationList.declarations.some((declaration) => {
return this.isFunctionVariable(declaration)
})
} else if (ts.isVariableDeclaration(node)) {
return this.extractFunctionNode(node) !== undefined
}
return false
}
/**
* Retrieves the underlying function/method/arrow function of a variable statement or declaration.
*
* @param {ts.Node} node - The variable statement/declaration to retrieve the function/method from.
* @returns The function/method if found.
*/
extractFunctionNode(node: VariableNode): FunctionNode | undefined {
if (ts.isVariableStatement(node)) {
const variableDeclaration = node.declarationList.declarations.find(
(declaration) => ts.isVariableDeclaration(declaration)
)
return variableDeclaration
? this.extractFunctionNode(variableDeclaration)
: undefined
} else if (
node.initializer &&
(this.isAllowed(node.initializer) || ts.isArrowFunction(node.initializer))
) {
return node.initializer
}
}
/**
* Check whether a node refers to a method.
*
* @param {FunctionNode} node - The node to check.
* @returns {boolean} Whether the node is a method.
*/
isMethod(
node: FunctionNode
): node is ts.MethodDeclaration | ts.MethodSignature {
return this.methodKinds.includes(node.kind)
}
/**
* Checks whether a type, typically the type of a function's signature, has return data.
*
* @param {string} typeStr - The type's string representation.
* @returns {boolean} Whether the type has return data.
*/
hasReturnData(typeStr: string): boolean {
return (
typeStr !== "void" &&
typeStr !== "never" &&
typeStr !== "Promise<void>" &&
typeStr !== "Promise<never>"
)
}
/**
* Retrieves the return type of a function.
*
* @param {FunctionNode} node - The function's node.
* @returns {ts.Type} The function's return type.
*/
getReturnType(node: FunctionNode): ts.Type {
return node.type
? this.checker.getTypeFromTypeNode(node.type)
: this.checker.getTypeAtLocation(node)
}
/**
* Retrieves the summary comment of a function.
*
* @param {FunctionNode} node - The node's function.
* @param {ts.Symbol} symbol - The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
* @returns {string} The function's summary comment.
*/
getFunctionSummary(node: FunctionNode, symbol?: ts.Symbol): string {
return symbol
? this.knowledgeBaseFactory.tryToGetFunctionSummary({
symbol: symbol,
kind: node.kind,
}) || this.getNodeSummary({ node, symbol })
: this.getNodeSummary({ node, symbol })
}
/**
* Retrieve the function's example comment.
*
* @param {ts.Symbol} symbol - The function's symbol. If provided, the method will try to retrieve the example from the {@link KnowledgeBaseFactory}.
* @returns {string} The function's example comment.
*/
getFunctionExample(symbol?: ts.Symbol): string {
const str = `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}`
return `${str}${
symbol
? this.knowledgeBaseFactory.tryToGetFunctionExamples({
symbol: symbol,
}) || `{example-code}`
: `{example-code}`
}`
}
/**
* Retrieve the full docblock of a function.
*
* @param {FunctionOrVariableNode | ts.Node} node - The function node. If a variable statement is provided, the underlying function is retrieved.
* If a different node type is provided, the parent generator is used to retrieve the docblock comment.
* @param {GetDocBlockOptions} options - Formatting options.
* @returns {string} The function's docblock.
*/
getDocBlock(
node: FunctionOrVariableNode | ts.Node,
options: GetDocBlockOptions = { addEnd: true }
): string {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
}
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
if (!actualNode) {
return super.getDocBlock(node, options)
}
const nodeSymbol = getSymbol(node, this.checker)
let str = DOCBLOCK_START
// add summary
str += `${
options.summaryPrefix ||
(this.isMethod(actualNode) ? `This method` : `This function`)
} ${this.getFunctionSummary(actualNode, nodeSymbol)}${DOCBLOCK_NEW_LINE}`
// add params
actualNode.forEachChild((childNode) => {
if (!ts.isParameter(childNode)) {
return
}
const symbol = getSymbol(childNode, this.checker)
if (!symbol) {
return
}
const symbolType = this.checker.getTypeOfSymbolAtLocation(
symbol,
childNode
)
str += `${DOCBLOCK_NEW_LINE}@param {${this.checker.typeToString(
symbolType
)}} ${symbol.getName()} - ${this.getNodeSummary({
node: childNode,
symbol,
nodeType: symbolType,
})}`
})
// add returns
const nodeType = this.getReturnType(actualNode)
const returnTypeStr = this.checker.typeToString(nodeType)
const possibleReturnSummary = !this.hasReturnData(returnTypeStr)
? `Resolves when ${this.defaultSummary}`
: this.getNodeSummary({
node: actualNode,
nodeType,
})
str += `${DOCBLOCK_NEW_LINE}@returns {${returnTypeStr}} ${
nodeSymbol
? this.knowledgeBaseFactory.tryToGetFunctionReturns({
symbol: nodeSymbol,
kind: actualNode.kind,
}) || possibleReturnSummary
: possibleReturnSummary
}`
// add example
str += this.getFunctionExample(nodeSymbol)
// add common docs
str += this.getCommonDocs(node, {
prefixWithLineBreaks: true,
})
if (options.addEnd) {
str += DOCBLOCK_END_LINE
}
return str
}
}
export default FunctionKindGenerator

View File

@@ -0,0 +1,195 @@
import ts from "typescript"
import FunctionKindGenerator, {
FunctionNode,
FunctionOrVariableNode,
} from "./function.js"
import {
DOCBLOCK_NEW_LINE,
DOCBLOCK_END_LINE,
DOCBLOCK_START,
DOCBLOCK_DOUBLE_LINES,
} from "../../constants.js"
import nodeHasComments from "../../utils/node-has-comments.js"
import {
CUSTOM_NAMESPACE_TAG,
getCustomNamespaceTag,
} from "../../utils/medusa-react-utils.js"
/**
* Docblock generate for medusa-react hooks. Since hooks are essentially functions,
* it extends the {@link FunctionKindGenerator} class.
*/
class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
/**
* Checks whether the generator can retrieve the docblock of the specified node. It uses the parent generator
* to check that the node is a function, then checks if the function is a mutation using the {@link isMutation} method,
* or a query using the {@link isQuery} method.
*
* @param {ts.Node} node - The node to check.
* @returns {boolean} Whether this generator can be used on this node.
*/
isAllowed(node: ts.Node): node is FunctionOrVariableNode {
if (!super.isAllowed(node)) {
return false
}
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
return (
actualNode !== undefined &&
(this.isMutation(actualNode) || this.isQuery(actualNode))
)
}
/**
* Checks whether a function node is a mutation.
*
* @param {FunctionNode} node - The function node to check.
* @returns {boolean} Whether the node is a mutation.
*/
isMutation(node: FunctionNode): boolean {
const nodeType = this.getReturnType(node)
const callSignatures = nodeType.getCallSignatures()
return (
callSignatures.length > 0 &&
this.checker
.typeToString(this.checker.getReturnTypeOfSignature(callSignatures[0]))
.startsWith("UseMutationResult")
)
}
/**
* Checks whether a function node is a query.
*
* @param {FunctionNode} node - The function node to check.
* @returns {boolean} Whether the node is a query.
*/
isQuery(node: FunctionNode): boolean {
return node.parameters.some(
(parameter) =>
parameter.type?.getText().startsWith("UseQueryOptionsWrapper")
)
}
/**
* Retrieves the docblock of the medusa-react hook or mutation.
*
* @param {FunctionNode & ts.VariableDeclaration} node - The node to retrieve its docblock.
* @returns {string} The node's docblock.
*/
getDocBlock(node: FunctionNode & ts.VariableDeclaration): string {
if (!this.isAllowed(node)) {
return super.getDocBlock(node)
}
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
if (!actualNode) {
return super.getDocBlock(node)
}
const isMutation = this.isMutation(actualNode)
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary(node)}`
// add example
str += this.getFunctionExample()
// loop over parameters that aren't query/mutation parameters
// and add docblock to them
this.getActualParameters(actualNode).forEach((parameter) => {
ts.addSyntheticLeadingComment(
parameter,
ts.SyntaxKind.MultiLineCommentTrivia,
super.getDocBlock(parameter),
true
)
})
// check if mutation parameter is an intrinsic type and, if so, add the `@typeParamDefinition`
// tag to the hook
if (isMutation) {
const typeArg = this.getMutationRequestTypeArg(actualNode)
if (typeArg) {
str += `${DOCBLOCK_DOUBLE_LINES}@typeParamDefinition ${this.checker.typeToString(
typeArg
)} - {summary}`
}
}
// add common docs
str += this.getCommonDocs(node, {
prefixWithLineBreaks: true,
})
// add namespace in case it's not added
if (!str.includes(CUSTOM_NAMESPACE_TAG)) {
str += `${DOCBLOCK_DOUBLE_LINES}${getCustomNamespaceTag(actualNode)}`
}
// add the category
str += `${DOCBLOCK_NEW_LINE}@category ${
isMutation ? "Mutations" : "Queries"
}`
return `${str}${DOCBLOCK_END_LINE}`
}
/**
* Retrieves the parameters of a function node that aren't query/mutation options.
*
* @param {FunctionNode} node - The function node to retrieve its parameters.
* @returns {ts.ParameterDeclaration[]} - The function's actual parameters.
*/
getActualParameters(node: FunctionNode): ts.ParameterDeclaration[] {
return node.parameters.filter((parameter) => {
const parameterTypeStr = parameter.type?.getText()
return (
!parameterTypeStr?.startsWith("UseQueryOptionsWrapper") &&
!parameterTypeStr?.startsWith("UseMutationOptions") &&
!nodeHasComments(parameter)
)
})
}
/**
* Retreives a mutation's intrinsic request type, if available, which is specified as the third type argument of `UseMutationOptions`.
*
* @param {FunctionNode} node - The function node to retrieve its request type.
* @returns {ts.Type | undefined} The mutation's request type, if available.
*/
getMutationRequestTypeArg(node: FunctionNode): ts.Type | undefined {
const parameter = node.parameters.find(
(parameter) => parameter.type?.getText().startsWith("UseMutationOptions")
)
if (!parameter) {
return
}
const parameterType = this.checker.getTypeFromTypeNode(parameter.type!)
const typeArgs =
parameterType.aliasTypeArguments ||
("resolvedTypeArguments" in parameterType
? (parameterType.resolvedTypeArguments as ts.Type[])
: [])
if (
!typeArgs ||
typeArgs.length < 3 ||
!("intrinsicName" in typeArgs[2]) ||
["void", "unknown"].includes(typeArgs[2].intrinsicName as string)
) {
return
}
// find request in third type argument
return typeArgs[2]
}
}
export default MedusaReactHooksKindGenerator

View File

@@ -0,0 +1,51 @@
import ts from "typescript"
import FunctionKindGenerator from "./function.js"
import DefaultKindGenerator from "./default.js"
import MedusaReactHooksKindGenerator from "./medusa-react-hooks.js"
import SourceFileKindGenerator from "./source-file.js"
import DTOPropertyGenerator from "./dto-property.js"
/**
* A class that is used as a registry for the kind generators.
*/
class KindsRegistry {
protected kindInstances: DefaultKindGenerator[]
protected defaultKindGenerator: DefaultKindGenerator
constructor(checker: ts.TypeChecker) {
this.kindInstances = [
new MedusaReactHooksKindGenerator({ checker }),
new FunctionKindGenerator({ checker }),
new SourceFileKindGenerator({ checker }),
new DTOPropertyGenerator({ checker }),
]
this.defaultKindGenerator = new DefaultKindGenerator({ checker })
}
/**
* Retrieve the generator for a node based on its kind, if any.
*
* @param {ts.Node} node - The node to retrieve its docblock generator.
* @returns {DefaultKindGenerator | undefined} The generator that can handle the node's kind, if any.
*/
getKindGenerator(node: ts.Node): DefaultKindGenerator | undefined {
return (
this.kindInstances.find((generator) => generator.isAllowed(node)) ||
(this.defaultKindGenerator.isAllowed(node)
? this.defaultKindGenerator
: undefined)
)
}
/**
* Checks whether a node has a kind generator.
*
* @param {ts.Node} node - The node to check for.
* @returns {boolean} Whether the node has a kind generator.
*/
hasGenerator(node: ts.Node): boolean {
return this.getKindGenerator(node) !== undefined
}
}
export default KindsRegistry

View File

@@ -0,0 +1,37 @@
import ts from "typescript"
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js"
import { shouldHaveCustomNamespace } from "../../utils/medusa-react-utils.js"
/**
* A generator used to retrieve doc blocks for a source file.
*/
class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.SourceFile]
/**
* Retrieve the docblock of a source file.
*
* @param {ts.SourceFile | ts.Node} node - The node to retrieve its docblocks.
* @param {GetDocBlockOptions} options - The formatting options.
* @returns {string} The node's docblock.
*/
getDocBlock(
node: ts.SourceFile | ts.Node,
options?: GetDocBlockOptions
): string {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
}
if (shouldHaveCustomNamespace(node)) {
return `${DOCBLOCK_START}${this.getCommonDocs(node, {
addDefaultSummary: true,
})}${DOCBLOCK_END_LINE}`
}
return ""
}
}
export default SourceFileKindGenerator

View File

@@ -0,0 +1,325 @@
import ts from "typescript"
import { DOCBLOCK_DOUBLE_LINES, DOCBLOCK_NEW_LINE } from "../constants.js"
import {
camelToTitle,
camelToWords,
normalizeName,
} from "../utils/str-formatting.js"
type TemplateOptions = {
parentName?: string
rawParentName?: string
returnTypeName?: string
}
type KnowledgeBase = {
startsWith?: string
endsWith?: string
exact?: string
template: string | ((str: string, options?: TemplateOptions) => string)
kind?: ts.SyntaxKind[]
}
export type RetrieveOptions = {
/**
* A name that can be of a function, type, etc...
*/
str: string
/**
* Options to pass to the `template` function of a
* knowledge base item.
*/
templateOptions?: TemplateOptions
/**
* The kind of the associated node.
*/
kind?: ts.SyntaxKind
}
type RetrieveSymbolOptions = Omit<RetrieveOptions, "str"> & {
/**
* The symbol to retrieve the item from the knowledge base.
*/
symbol: ts.Symbol
}
/**
* A class that holds common Medusa patterns and acts as a knowledge base for possible summaries/examples/general templates.
*/
class KnowledgeBaseFactory {
private summaryKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "FindConfig",
template: (str) => {
const typeArgs = str
.replace("FindConfig<", "")
.replace(/>$/, "")
.split(",")
.map((part) => camelToWords(normalizeName(part.trim())))
const typeName =
typeArgs.length > 0 && typeArgs[0].length > 0
? typeArgs[0]
: `{type name}`
return `The configurations determining how the ${typeName} is retrieved. Its properties, such as \`select\` or \`relations\`, accept the ${DOCBLOCK_NEW_LINE}attributes or relations associated with a ${typeName}.`
},
},
{
startsWith: "Filterable",
endsWith: "Props",
template: (str) => {
return `The filters to apply on the retrieved ${camelToTitle(
normalizeName(str)
)}.`
},
},
{
startsWith: "Create",
endsWith: "DTO",
template: (str) => {
return `The ${camelToTitle(normalizeName(str))} to be created.`
},
},
{
startsWith: "Update",
endsWith: "DTO",
template: (str) => {
return `The attributes to update in the ${camelToTitle(
normalizeName(str)
)}.`
},
},
{
startsWith: "RestoreReturn",
template: `Configurations determining which relations to restore along with each of the {type name}. You can pass to its \`returnLinkableKeys\` ${DOCBLOCK_NEW_LINE}property any of the {type name}'s relation attribute names, such as \`{type relation name}\`.`,
},
{
endsWith: "DTO",
template: (str: string): string => {
return `The ${camelToTitle(normalizeName(str))} details.`
},
},
{
endsWith: "_id",
template: (str: string): string => {
const formatted = str.replace(/_id$/, "").split("_").join(" ")
return `The associated ${formatted}'s ID.`
},
kind: [ts.SyntaxKind.PropertySignature],
},
{
endsWith: "Id",
template: (str: string): string => {
const formatted = camelToWords(str.replace(/Id$/, ""))
return `The ${formatted}'s ID.`
},
kind: [
ts.SyntaxKind.PropertySignature,
ts.SyntaxKind.PropertyDeclaration,
ts.SyntaxKind.Parameter,
],
},
{
exact: "id",
template: (str, options) => {
if (options?.rawParentName?.startsWith("Filterable")) {
return `The IDs to filter the ${options?.parentName || `{name}`} by.`
}
return `The ID of the ${options?.parentName || `{name}`}.`
},
kind: [ts.SyntaxKind.PropertySignature],
},
{
exact: "metadata",
template: "Holds custom data in key-value pairs.",
kind: [ts.SyntaxKind.PropertySignature],
},
{
exact: "customHeaders",
template: "Custom headers to attach to the request.",
},
]
private functionSummaryKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "listAndCount",
template:
"retrieves a paginated list of {return type} along with the total count of available {return type}(s) satisfying the provided filters.",
},
{
startsWith: "list",
template:
"retrieves a paginated list of {return type}(s) based on optional filters and configuration.",
},
{
startsWith: "retrieve",
template: "retrieves a {return type} by its ID.",
},
{
startsWith: "create",
template: "creates {return type}(s)",
},
{
startsWith: "delete",
template: "deletes {return type} by its ID.",
},
{
startsWith: "update",
template: "updates existing {return type}(s).",
},
{
startsWith: "softDelete",
template: "soft deletes {return type}(s) by their IDs.",
},
{
startsWith: "restore",
template: "restores soft deleted {return type}(s) by their IDs.",
},
]
private exampleCodeBlockLine = `${DOCBLOCK_DOUBLE_LINES}\`\`\`ts${DOCBLOCK_NEW_LINE}{example-code}${DOCBLOCK_NEW_LINE}\`\`\`${DOCBLOCK_DOUBLE_LINES}`
private examplesKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "list",
template: `To retrieve a list of {type name} using their IDs: ${this.exampleCodeBlockLine}To specify relations that should be retrieved within the {type name}: ${this.exampleCodeBlockLine}By default, only the first \`{default limit}\` records are retrieved. You can control pagination by specifying the \`skip\` and \`take\` properties of the \`config\` parameter: ${this.exampleCodeBlockLine}`,
},
{
startsWith: "retrieve",
template: `A simple example that retrieves a {type name} by its ID: ${this.exampleCodeBlockLine}To specify relations that should be retrieved: ${this.exampleCodeBlockLine}`,
},
]
private functionReturnKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "listAndCount",
template: "The list of {return type}(s) along with their total count.",
},
{
startsWith: "list",
template: "The list of {return type}(s).",
},
{
startsWith: "retrieve",
template: "The retrieved {return type}(s).",
},
{
startsWith: "create",
template: "The created {return type}(s).",
},
{
startsWith: "update",
template: "The updated {return type}(s).",
},
{
startsWith: "restore",
template: `An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the {type name} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.`,
},
]
/**
* Tries to find in a specified knowledge base a template relevant to the specified name.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
private tryToFindInKnowledgeBase({
str,
knowledgeBase,
templateOptions,
kind,
}: RetrieveOptions & {
/**
* A knowledge base to search in.
*/
knowledgeBase: KnowledgeBase[]
}): string | undefined {
const foundItem = knowledgeBase.find((item) => {
if (item.exact) {
return str === item.exact
}
if (item.kind?.length && (!kind || !item.kind.includes(kind))) {
return false
}
if (item.startsWith && item.endsWith) {
return str.startsWith(item.startsWith) && str.endsWith(item.endsWith)
}
if (item.startsWith) {
return str.startsWith(item.startsWith)
}
return item.endsWith ? str.endsWith(item.endsWith) : false
})
if (!foundItem) {
return
}
return typeof foundItem.template === "string"
? foundItem?.template
: foundItem?.template(str, templateOptions)
}
/**
* Tries to retrieve the summary template of a specified type from the {@link summaryKnowledgeBase}.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
tryToGetSummary({ str, ...options }: RetrieveOptions): string | undefined {
const normalizedTypeStr = str.replaceAll("[]", "")
return this.tryToFindInKnowledgeBase({
...options,
str: normalizedTypeStr,
knowledgeBase: this.summaryKnowledgeBase,
})
}
/**
* Tries to retrieve the summary template of a function's symbol from the {@link functionSummaryKnowledgeBase}.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
tryToGetFunctionSummary({
symbol,
...options
}: RetrieveSymbolOptions): string | undefined {
return this.tryToFindInKnowledgeBase({
...options,
str: symbol.getName(),
knowledgeBase: this.functionSummaryKnowledgeBase,
})
}
/**
* Tries to retrieve the example template of a function's symbol from the {@link examplesKnowledgeBase}.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
tryToGetFunctionExamples({
symbol,
...options
}: RetrieveSymbolOptions): string | undefined {
return this.tryToFindInKnowledgeBase({
...options,
str: symbol.getName(),
knowledgeBase: this.examplesKnowledgeBase,
})
}
/**
* Tries to retrieve the return template of a function's symbol from the {@link functionReturnKnowledgeBase}.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
tryToGetFunctionReturns({
symbol,
...options
}: RetrieveSymbolOptions): string | undefined {
return this.tryToFindInKnowledgeBase({
...options,
str: symbol.getName(),
knowledgeBase: this.functionReturnKnowledgeBase,
})
}
}
export default KnowledgeBaseFactory