UNPKG

@appium/typedoc-plugin-appium

Version:

TypeDoc plugin for Appium & its extensions

428 lines (393 loc) 12.7 kB
import _ from 'lodash'; import { Comment, Context, DeclarationReflection, IntrinsicType, LiteralType, ParameterReflection, ReferenceType, SignatureReflection, } from 'typedoc'; import { CallSignatureReflection, cloneCallSignatureReflection, CommandMethodDeclarationReflection, CommentSource, createNewParamRefls, deriveComment, Example, extractExamples, KnownMethods, } from '../converter'; import {AppiumPluginLogger} from '../logger'; import {findCallSignature} from '../utils'; import {AllowedHttpMethod, Command, Route} from './types'; /** * Set of type names which should be converted to `null` * @see https://github.com/appium/appium/issues/18269 */ const NULL_TYPES: Readonly<Set<string>> = new Set(['undefined', 'void']); /** * Abstract representation of metadata for some sort of Appium command */ export abstract class BaseCommandData { /** * Loggher */ protected readonly log: AppiumPluginLogger; /** * The method name of the command handler. */ public readonly command: string; /** * The comment to display for the command, if any exists */ public readonly comment?: Comment; /** * Code to describe how and where {@linkcode comment} came from. * * For debugging purposes mainly */ public readonly commentSource?: CommentSource; /** * Language-specific examples for the command, if any */ public readonly examples?: Example[]; /** * If `true`, this command is from a Plugin (not a Driver) */ public readonly isPluginCommand: boolean; /** * Map of known builtin methods */ public readonly knownBuiltinMethods?: KnownMethods; /** * Actual method reflection. */ public readonly methodRefl: CommandMethodDeclarationReflection; /** * List of optional parameter names derived from a method map */ public readonly optionalParams?: string[]; /** * Parameter reflections for this command's method declaration, to eventually be displayed in rendered docs * * These are _not_ the same objects as in the `parameters` property of a the call signature * reflection in `methodRefl`; the comments therein have been aggregated and the parameters have * been renamed and possibly truncated. */ public readonly parameters?: ParameterReflection[]; /** * The thing which the method is a member of for documentation purposes */ public readonly parentRefl?: DeclarationReflection; /** * List of required parameter names derived from a method map */ public readonly requiredParams?: string[]; /** * Signature reflection for this command's method declaration, to eventually be displayed in * rendered docs * * `methodRefl` is a {@linkcode CommandMethodDeclarationReflection}, so it returns a `Promise<T>`, by * definition. This signature reflection is modified so that it returns `T` instead, since * `Promise`s don't make much sense in the rendered documentaion. * * The default TypeDoc output uses the original `SignatureReflection`, so you _will_ see * `Promise<T>` there. */ public readonly signature?: SignatureReflection; constructor( log: AppiumPluginLogger, command: Command, methodRefl: CommandMethodDeclarationReflection, public readonly opts: BaseCommandDataOpts = {} ) { this.command = command; this.methodRefl = methodRefl; this.log = log; this.optionalParams = opts.optionalParams; this.requiredParams = opts.requiredParams; this.commentSource = opts.commentSource; this.parentRefl = opts.parentRefl; this.knownBuiltinMethods = opts.knownBuiltinMethods; this.isPluginCommand = Boolean(opts.isPluginCommand); const extractedExamples = extractExamples(opts.comment); if (extractedExamples?.comment) { this.comment = extractedExamples.comment; } if (extractedExamples?.examples) { this.examples = extractedExamples.examples; } this.parameters = this.rewriteParameters(); this.signature = this.rewriteSignature(); } /** * Returns `true` if the method or execute map defined parameters for this command */ public get hasCommandParams(): boolean { return Boolean(this.optionalParams?.length || this.requiredParams?.length); } /** * Returns a list of `ParameterReflection` objects in the command's method declaration; * rewrites them to prefer the method map parameter list (and the param names) */ private rewriteParameters(): ParameterReflection[] | undefined { if (!this.hasCommandParams) { return; } const sig = findCallSignature(this.methodRefl)!; if (!sig?.parameters?.length) { // no parameters return; } const {knownBuiltinMethods: builtinMethods, isPluginCommand} = this; const newParamRefls = [ ...createNewParamRefls(sig, { builtinMethods, commandParams: this.requiredParams, isPluginCommand, }), ...createNewParamRefls(sig, { builtinMethods, commandParams: this.optionalParams, isPluginCommand, isOptional: true, }), ]; return newParamRefls; } /** * * Rewrites a method's return value for documentation. * * Given a command having a method declaration, creates a clone of its call signature wherein the * return type is unwrapped from `Promise`. In other words, if a method returns `Promise<T>`, * this changes the return type in the signature to `T`. * * Note that the return type of a command's method declaration should always be a `ReferenceType` having * name `Promise`. */ private rewriteSignature(): CallSignatureReflection | undefined { const callSig = findCallSignature(this.methodRefl); if (!callSig) { return; } if (callSig.type instanceof ReferenceType && callSig.type.name === 'Promise') { // this does the actual unwrapping. `Promise` only has a single type argument `T`, // so we can safely use the first one. let typeArg = _.first(callSig.type.typeArguments)!; // swaps `void`/`undefined` for `null` if (typeArg instanceof IntrinsicType && NULL_TYPES.has(typeArg.name)) { typeArg = new LiteralType(null); } const newCallSig = cloneCallSignatureReflection(callSig, this.methodRefl, typeArg); if (!newCallSig.type) { this.log.warn( '(%s) No type arg T found for return type Promise<T> in %s; this is a bug', this.parentRefl!.name, this.methodRefl.name ); return; } newCallSig.comment = deriveComment({ refl: newCallSig, knownMethods: this.knownBuiltinMethods, })?.comment; return newCallSig; } } } /** * Options for {@linkcode CommandData} and {@linkcode ExecMethodData} constructors */ export interface BaseCommandDataOpts { /** * The comment to display for the command, if any exists */ comment?: Comment; /** * Name of the reference which the comment is derived from. * * For debugging purposes mainly */ commentSource?: CommentSource; /** * If `true`, `refl` represents a `PluginCommand`, wherein we will ignore * the first two parameters altogether. */ isPluginCommand?: boolean; /** * Known methods in the project */ knownBuiltinMethods?: KnownMethods; /** * List of optional parameter names derived from a method map */ optionalParams?: string[]; /** * The thing which the method is a member of for documentation purposes */ parentRefl?: DeclarationReflection; /** * List of required parameter names derived from a method map */ requiredParams?: string[]; } /** * Represents a generic WD or Appium-specific endpoint */ export class CommandData extends BaseCommandData { /** * The HTTP method of the route */ public readonly httpMethod: AllowedHttpMethod; /** * The route of the command */ public readonly route: Route; /** * Use {@linkcode CommandData.create} instead */ private constructor( log: AppiumPluginLogger, command: Command, methodRefl: CommandMethodDeclarationReflection, httpMethod: AllowedHttpMethod, route: Route, opts: BaseCommandDataOpts = {} ) { super(log, command, methodRefl, opts); this.httpMethod = httpMethod; this.route = route; } /** * Creates a **shallow** clone of this instance. * * @param commandData Instance to clone * @param ctx Context * @param overrides Override any properties of the instance here (including {@linkcode CommandData.opts}) * @returns Cloned instance */ public static clone(commandData: CommandData, ctx: Context, overrides?: Partial<CommandData>) { const {log, command, methodRefl, httpMethod, route, opts} = _.defaults(overrides, { log: commandData.log, command: commandData.command, methodRefl: commandData.methodRefl, httpMethod: commandData.httpMethod, route: commandData.route, opts: _.defaults(overrides?.opts, commandData.opts), }); return CommandData.create(ctx, log, command, methodRefl, httpMethod, route, opts); } /** * Creates a new instance of {@linkcode CommandData} and registers any newly-created reflections * with TypeDoc. * @param ctx Context * @param log Logger * @param command Command name * @param methodRefl Command method reflection * @param httpMethod HTTP method of route * @param route Route path * @param opts Options * @returns */ public static create( ctx: Context, log: AppiumPluginLogger, command: Command, methodRefl: CommandMethodDeclarationReflection, httpMethod: AllowedHttpMethod, route: Route, opts: BaseCommandDataOpts = {} ): CommandData { const commandData = new CommandData(log, command, methodRefl, httpMethod, route, opts); if (commandData.signature) { ctx.registerReflection(commandData.signature, undefined); } if (commandData.parameters) { for (const param of commandData.parameters) { ctx.registerReflection(param, undefined); } } return commandData; } } /** * Represents an "execute command" ("execute method") * * Each will have a unique `script` property which is provided as the script to run via the * `execute` WD endpoint. * * All of these share the same `execute` route, so it is omitted from this interface. */ export class ExecMethodData extends BaseCommandData { /** * Use {@linkcode ExecMethodData.create} instead * @param log Logger * @param command Command name * @param methodRefl method reflection * @param script Script name (not the same as command name); this is what is passed to the `execute` endpoint */ private constructor( log: AppiumPluginLogger, command: Command, methodRefl: CommandMethodDeclarationReflection, public readonly script: string, public readonly opts: BaseCommandDataOpts = {} ) { super(log, command, methodRefl, opts); } /** * Creates a **shallow** clone of this instance. * * @param execMethodData Instance to clone * @param ctx Context * @param overrides Override any properties of the instance here (including {@linkcode ExecMethodData.opts}) * @returns Cloned instance */ public static clone( execMethodData: ExecMethodData, ctx: Context, overrides?: Partial<ExecMethodData> ) { const {log, command, methodRefl, script, opts} = _.defaults(overrides, { log: execMethodData.log, command: execMethodData.command, methodRefl: execMethodData.methodRefl, script: execMethodData.script, opts: _.defaults(overrides?.opts, execMethodData.opts), }); return ExecMethodData.create(ctx, log, command, methodRefl, script, opts); } /** * Creates a new instance of {@linkcode CommandData} and registers any newly-created reflections * with TypeDoc. * @param ctx Context * @param log Logger * @param command Command name * @param script Script name * @param route Route path * @param opts Options */ public static create( ctx: Context, log: AppiumPluginLogger, command: Command, methodRefl: CommandMethodDeclarationReflection, script: string, opts: BaseCommandDataOpts = {} ): ExecMethodData { const execMethodData = new ExecMethodData(log, command, methodRefl, script, opts); if (execMethodData.signature) { ctx.registerReflection(execMethodData.signature, undefined); } if (execMethodData.parameters) { for (const param of execMethodData.parameters) { ctx.registerReflection(param, undefined); } } return execMethodData; } }