UNPKG

@workday/canvas-kit-docs

Version:

Documentation components of Canvas Kit components

1,258 lines • 60.5 kB
import ts from 'typescript'; import { getExternalSymbol } from './getExternalSymbol'; import t, { find } from './traverse'; export class DocParser { constructor(program, plugins = []) { this.program = program; this.plugins = plugins; // track symbols to ensure we don't get stuck in an infinite loop this.visitedTypeScriptSymbols = {}; /** * This is the shared mutable instance of all exported symbols already processed. You can push new * symbols or search for existing symbols. If your plugin doesn't need to access existing symbols, * you can ignore this parameter. */ this.symbols = []; this.checker = program.getTypeChecker(); } /** * Get all {@link ExportedSymbol}s from a file. */ getExportedSymbols(fileName) { const symbols = []; const sourceFile = this.program.getSourceFile(fileName); if (!sourceFile) return symbols; find(sourceFile, node => { const kind = node.kind; return ([ 'VariableDeclaration', 'InterfaceDeclaration', 'TypeAliasDeclaration', 'FunctionDeclaration', 'EnumDeclaration', 'ClassDeclaration', ] .map(k => ts.SyntaxKind[k]) .includes(kind) && isNodeExported(this.checker, node)); }).forEach(node => { // reset visited symbols for every exported symbol to prevent accidental short-circuiting of non-exported symbols of the same name across files or scopes. this.visitedTypeScriptSymbols = {}; const symbol = getSymbolFromNode(this, node); const previousSymbolsLength = this.symbols.length; if (symbol) { const exportedSymbol = { name: symbol.name, fileName, ...findDocComment(this.checker, symbol), type: this.getValueFromNode(node), }; symbols.push(exportedSymbol); const addedSymbolsLength = previousSymbolsLength - this.symbols.length; // add all symbols added by the parser if (addedSymbolsLength) { this.symbols.slice(addedSymbolsLength).forEach(symbol => { symbols.push(symbol); }); } this.symbols.push(exportedSymbol); } }); return symbols; } getValueFromNode(node) { return getValueFromNode(this, node); } getValueFromType(type, node) { return getValueFromType(this, type, node); } } /** * This is the main recursive function for creating docs from the Typescript AST. The AST is * traversed and type information is extracted from source code. The Typescript type checker is used * to help traverse the AST by getting Symbols or Types and getting back source nodes. * * For example, if an `interface` is encountered: * - Start with a declaration - a Node * - Ask the type checker for the type information of the interface - returns a Type * - Get all the properties of the type - returns Symbols * - Iterate over property symbols and get a declaration - returns a Node * - Return a doc entry of the declaration node */ function getValueFromNode(parser, node) { if (node === undefined) { // This shouldn't happen, but we'd rather see `???` in the output than crash return unknownValue('???'); } return (parser.plugins.reduce((result, fn) => { return result || fn(node, parser); }, undefined) || _getValueFromNode(parser, node)); } /** * Private recursing function. Doesn't include plugins. */ function _getValueFromNode(parser, node) { var _a, _b, _c, _d, _e, _f; const { checker } = parser; // Uncomment for debugging // console.log( // t.getKindNameFromNode(node) || node.kind, // safeGetText(parser, node), // checker.typeToString(checker.getTypeAtLocation(node)) // ); /** * A tuple type is an array with positional types. * * ```ts * type A = [string, number] * ``` * * In this example, the TupleType is `[string, number]`. Each element in the * TupleType is its own node, so we map over them and recurse. */ if (t.isTupleType(node)) { return { kind: 'tuple', value: node.elements.map(e => getValueFromNode(parser, e)), }; } /** * For the purpose of docs, the following are equivalent: * * ```ts * interface A<T> { * B: T * } * * type A<T> = { * B: T * } * ``` * * The TypeAliasDeclaration has a `type` that is a `TypeLiteral` */ if ((t.isTypeAliasDeclaration(node) && t.isTypeLiteral(node.type)) || t.isInterfaceDeclaration(node)) { // Treat Interfaces and Types with TypeLiterals as interfaces const type = checker.getTypeAtLocation(node); const properties = type .getProperties() .map(p => { return getValueFromNode(parser, getValueDeclaration(p)); }) .filter(filterObjectProperties); // get index signature... const indexType = getIndexSignatureFromType(parser, type); if (isObject(type) && (properties.length || indexType)) { const typeParameters = (_a = (node.typeParameters || node.parent.typeParameters)) === null || _a === void 0 ? void 0 : _a.map(p => getValueFromNode(parser, p)); return { kind: 'object', properties, typeParameters, indexSignature: indexType, }; } } /** * A declaration using the keyword `type`. For example: * * ```ts * type A = string * ``` * * The TypeAliasDeclaration is everything in this example. The TypeAliasDeclaration can include * `typeParameters` which are the generics. In the following example, the `typeParameters` are an * array including the `T` inside the `<T>`: * * ```ts * type A<T> = T * ``` */ if (t.isTypeAliasDeclaration(node)) { const typeParameters = (_b = node.typeParameters) === null || _b === void 0 ? void 0 : _b.map(p => getValueFromNode(parser, p)); // We need to test if the `node.type` is a TypeReference and if that TypeReference is an exported // symbol. If the TypeReference is exported, we can continue with recursing on the `node.type`. If // it is not an exported symbol, we need to instead evaluate the type of the declaration directly using // the type checker. This prevents us from going down a rabbit hole of evaluating ASTs that are unhelpful // to documentation. For example: // // ```ts // type ValueOf<T> = T[keyof T]; // type Foo = { a: 'first', b: 'second' } // export type Bar = ValueOf<Foo> // ``` // If `ValueOf` was exported, the type would be documented as `ValueOf<{a: 'first', b: // 'second'}>`, but if it isn't exported, the value is `'first' | 'second'` const isLocalTypeReference = t.isTypeReference(node.type) && !isExportedSymbol(parser, node.type.typeName); const value = isLocalTypeReference ? getValueFromType(parser, checker.getTypeAtLocation(node), node) || unknownValue(safeGetText(parser, node)) : getValueFromNode(parser, node.type); return { kind: 'type', typeParameters: typeParameters || [], value, }; } /** * We'll treat classes as objects with properties * ```ts * class A {} * ``` */ if (t.isClassDeclaration(node)) { const type = checker.getTypeAtLocation(node); const value = getObjectValueFromType(parser, type); if (value.kind === 'object') { value.typeParameters = (_c = node.typeParameters) === null || _c === void 0 ? void 0 : _c.map(p => { return getValueFromNode(parser, p); }).filter(filterObjectTypeParameters); } return value; } /** * A TypeLiteral is the object syntax of a `type`. It is similar to an interface. A * `TypeDeclaration` with a `TypeLiteral` is already checked higher up in this function, the only * `TypeLiteral` matching here are used anonymously. * * ```ts * type A = B & { * C: 'c' * } * ``` * * In this example, the TypeLiteral is `{ C: 'c' }`. */ if (t.isTypeLiteral(node)) { const properties = node.members .map(member => { return getValueFromNode(parser, member); }) .filter(filterObjectProperties); return { kind: 'object', properties }; } /** * A ArrayType is a special notation for arrays. For example: * * A[] * * An alternative would be `Array<A>`, but that would be a TypeReference * `Array` with a `typeArgument` of a TypeReference `A` */ if (t.isArrayType(node)) { return { kind: 'array', value: getValueFromNode(parser, node.elementType) }; } /** * A TypeParameter is a type version of a function parameter. It is the * generic types of another type. * * For example: * * type A<T> = string * * The TypeParameter is `T`. TypeParameters can have a constraint and a * default. For example: * * type A<T extends string = 'a'> = T * * The constraint is `string` and the default is `'a'` */ if (t.isTypeParameter(node)) { const constraint = node.constraint ? getValueFromNode(parser, node.constraint) : undefined; const defaultValue = node.default ? getValueFromNode(parser, node.default) : undefined; return { kind: 'typeParameter', name: node.name.text, defaultValue, constraint, required: !defaultValue, }; } /** * A MethodDeclaration is a type of property declaration within a JS object. * It is a special syntax for declaring a function property or method of an * object. This includes any JSDoc. * * For example: * * ```ts * const A = { * onClick(e: Event) {} * } * ``` * * In this example, the MethodDeclaration is `onClick(e: Event) {}`. Notice * there is no property signature like `onClick: (e: Event) => {}` * * Do before `ts.isFunctionLike` because we want to treat `MethodDeclaration` * as a `parameter` instead of a `function` */ if (t.isMethodDeclaration(node)) { const signature = getValueFromSignatureNode(parser, node); const symbol = getSymbolFromNode(parser, node); const type = checker.getTypeAtLocation(node); const jsDoc = findDocComment(checker, symbol); if (jsDoc.tags.default) { } return { kind: 'property', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', type: signature, required: symbol ? !isOptional(symbol) && !includesUndefined(type) : false, ...jsDoc, }; } /** * A MethodSignature is a type of property declaration within a TS type. It is a special syntax * for declaring a function property or method of a type. This includes any JSDoc. * * For example: * ```ts * type A = { * onClick(e: Event): void * } * ``` * * In this example, the MethodSignature is the `onClick(e: Event): void`. An alternative might be * `onClick: (e: Event) => void`. * * Do before `ts.isFunctionLike` because we want to treat `MethodSignature` as a `member` instead * of a `function`. */ if (t.isMethodSignature(node)) { const signature = getValueFromSignatureNode(parser, node); const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); return { kind: 'property', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || node.name.text || '', type: signature, ...jsDoc, }; } /** * A FunctionType is a function declaration in Typescript's type annotation */ if (t.isFunctionType(node)) { const declaration = node; const signature = checker.getSignatureFromDeclaration(declaration); if (signature) { return getValueFromSignature(parser, node, signature); } else { return getValueFromSignature(parser, node, generateSignatureFromTypeNode(parser, node)); } } // All function like have call signatures. All special function-like node // processing should happen before this line if (ts.isFunctionLike(node)) { return getValueFromSignatureNode(parser, node); } /** * A variable declaration is the inner declaration of any JS variable. Here's * the structure of a `VariableDeclaration`: * * VariableStatement VariableDeclarationList VariableDeclaration * * For example, * * export const A = 'a', B = 'b' * * There are 2 VariableDeclarations: `A = 'a'` and `B = 'b'` */ if (t.isVariableDeclaration(node)) { // if the declaration already has a type node, return the value from the type node if (node.type) { return getValueFromNode(parser, node.type); } // An `AsExpression` is a type, so we'll return that if (node.initializer && t.isAsExpression(node.initializer)) { return getValueFromNode(parser, node.initializer); } if (node.initializer && t.isIdentifier(node.initializer) && isExportedSymbol(parser, node.initializer)) { const symbol = getSymbolFromNode(parser, node.initializer); return { kind: 'symbol', name: node.initializer.text, fileName: (_d = symbol === null || symbol === void 0 ? void 0 : symbol.valueDeclaration) === null || _d === void 0 ? void 0 : _d.getSourceFile().fileName, }; } if (node.initializer && ts.isFunctionLike(node.initializer)) { return getValueFromNode(parser, node.initializer); } // We have no type information in the AST. We'll get the Type from the type checker and run some // tests on what we have const type = checker.getTypeAtLocation(node.initializer || node); // Both functions and objects are considered objects to Typescript if (isObject(type)) { if (type.objectFlags & ts.ObjectFlags.ArrayLiteral) { return getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)); } return getObjectValueFromType(parser, type); } const value = getValueFromType(parser, type); if (value) return value; } /** * A property signature is a property of a type object (or interface). This will include JSDoc. * In the below example, the property signature is `B: string` * * type A = { * B: string * } */ if (t.isPropertySignature(node)) { const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); // Get the name of the property - it could be a symbol or have a `name` that is an identifier or string literal const name = (symbol === null || symbol === void 0 ? void 0 : symbol.name) || (t.isIdentifier(node.name) ? node.name.text : t.isStringLiteral(node.name) ? node.name.text : ''); return { kind: 'property', name, required: node.questionToken ? false : true, type: node.type ? getValueFromNode(parser, node.type) : unknownValue(safeGetText(parser, node)), ...jsDoc, }; } /** * A PropertyDeclaration is a property declared in a class * * ```ts * class A { * a = 'b' * } * ``` * * In this example, the PropertyDeclaration is `a = 'b'` */ if (t.isPropertyDeclaration(node)) { const name = getNodeName(node); const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); const type = checker.getTypeAtLocation(node); return { kind: 'property', name: name || (symbol === null || symbol === void 0 ? void 0 : symbol.name) || 'unknown', type: getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)), ...jsDoc, }; } /** * An ObjectLiteralExpression is a JS value of an object literal. * * For example: * ```ts * const a = { * b: 'b' * } * ``` * * In this example, `{ b, 'b' }` is the full object literal (including newlines) */ if (t.isObjectLiteralExpression(node)) { return { kind: 'object', properties: node.properties .flatMap(property => { const value = getValueFromNode(parser, property); if (value.kind === 'object') { return value.properties; } return value; }) .filter(filterObjectProperties), }; } if (t.isInferType(node)) { return { kind: 'infer', value: getValueFromNode(parser, node.typeParameter), }; } if (t.isSpreadAssignment(node)) { const symbol = getSymbolFromNode(parser, node.expression); const declaration = getValueDeclaration(symbol); if (declaration) { return getValueFromNode(parser, declaration); } } if (t.isArrayLiteralExpression(node)) { let values = []; node.elements.forEach(element => { if (t.isSpreadElement(element)) { t.getKindNameFromNode(element); const value = getValueFromNode(parser, element); if (value.kind === 'array' || value.kind === 'tuple') { values = values.concat(value.value); } } else { const value = getValueFromNode(parser, element); values.push(value); } }); return { kind: 'tuple', value: values, }; } if (t.isSpreadElement(node)) { const symbol = getSymbolFromNode(parser, node.expression); const declaration = getValueDeclaration(symbol); if (declaration) { return getValueFromNode(parser, declaration); } return getValueFromNode(parser, node.expression); } /** * A property assignment is part of a object value. This will include JSDocs. * A property assignment is like a property signature, but with values instead of types. * In the below example, the property assignment is `B: 'b'`. * * const A = { * B: 'b' * } * * */ if (t.isPropertyAssignment(node)) { const type = checker.getTypeAtLocation(node); const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); // For default values, we want the value and not the type. An `AsExpression` redirects to types, // so we want to bypass it to get the value node const defaultValueNode = t.isAsExpression(node.initializer) ? node.initializer.expression : node.initializer; return { kind: 'property', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', defaultValue: getValueFromNode(parser, defaultValueNode), type: getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)), required: symbol ? !isOptional(symbol) && !includesUndefined(type) : false, ...jsDoc, }; } // as A if (t.isAsExpression(node)) { if (safeGetText(parser, node) === 'const') { const type = checker.getTypeAtLocation(node.parent); return getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)); } return getValueFromNode(parser, node.type); } if (t.isTypeOperator(node) && node.operator === ts.SyntaxKind.KeyOfKeyword) { // We can get into trouble if `node` is a synthetic node. We'll check if we're encountering // something like `keyof A`. In this case, we'll get the symbol and ask for the properties of // the symbol's declaration. if (t.isTypeReference(node.type)) { const symbol = getSymbolFromNode(parser, node.type.typeName); const declaration = getValueDeclaration(symbol); if (symbol && declaration && isExportedSymbol(parser, declaration)) { return { kind: 'keyof', name: { kind: 'symbol', name: symbol.name, fileName: declaration.getSourceFile().fileName, value: `keyof ${symbol.name}`, }, }; } } // A `keyof` in a synthetic TypeNode will cause us problems. It means a TypeNode was generated // by `getValueFromType`. `checker.getTypeAtLocation` on a synthetic TypeNode will always be // `any`. I means going down this tree will not work and we need to throw an error. This error // will bubble to a `try...catch` in `getValueFromType` and it will instead extract a value from // the `Type` and not the `TypeNode` if (node.end < 0) { // We're a synthetic TypeNode throw Error('Cannot process a synthetic TypeNode with a `keyof`'); } return (getValueFromType(parser, checker.getTypeFromTypeNode(node), node) || unknownValue(safeGetText(parser, node))); } // A literal type contains literals like `string` or `number` or `'foo'` if (t.isLiteralType(node)) { return getValueFromNode(parser, node.literal); } // true if (node.kind === ts.SyntaxKind.TrueKeyword) { return { kind: 'boolean', value: true }; } // false if (node.kind === ts.SyntaxKind.FalseKeyword) { return { kind: 'boolean', value: false }; } // string if (node.kind === ts.SyntaxKind.StringKeyword) { return { kind: 'primitive', value: 'string' }; } // number if (node.kind === ts.SyntaxKind.NumberKeyword) { return { kind: 'primitive', value: 'number' }; } // boolean if (node.kind === ts.SyntaxKind.BooleanKeyword) { return { kind: 'primitive', value: 'boolean' }; } // 'a' if (t.isStringLiteral(node)) { return { kind: 'string', value: node.text }; } // null if (t.isNullKeyword(node)) { return { kind: 'primitive', value: 'null' }; } if (node.kind === ts.SyntaxKind.NeverKeyword) { return { kind: 'primitive', value: 'never' }; } // 100 if (t.isNumericLiteral(node)) { return { kind: 'number', value: Number(node.text) }; } // void if (node.kind === ts.SyntaxKind.VoidKeyword) { return { kind: 'primitive', value: 'void' }; } // any if (node.kind === ts.SyntaxKind.AnyKeyword) { return { kind: 'primitive', value: 'any' }; } // unknown if (node.kind === ts.SyntaxKind.UnknownKeyword) { return { kind: 'primitive', value: 'unknown' }; } // undefined if (node.kind === ts.SyntaxKind.UndefinedKeyword) { return { kind: 'primitive', value: 'undefined' }; } // `something` if (t.isTemplateExpression(node)) { return { kind: 'primitive', value: 'string' }; } // type A = `{anything}` if (t.isTemplateLiteralType(node)) { const type = checker.getTypeAtLocation(node); return getValueFromType(parser, type, node) || unknownValue(checker.typeToString(type)); } // A | B if (t.isUnionType(node)) { return { kind: 'union', value: node.types.map(type => getValueFromNode(parser, type)), }; } // A & B if (t.isIntersectionType(node)) { return { kind: 'intersection', value: node.types.map(type => getValueFromNode(parser, type)), }; } // () if (t.isParenthesizedType(node)) { return { kind: 'parenthesis', value: getValueFromNode(parser, node.type) }; } // type A = B['C'] if (t.isIndexedAccessType(node)) { const type = checker.getTypeAtLocation(node); return getValueFromType(parser, type, node) || unknownValue(safeGetText(parser, node)); } /** * A ConditionalType is a type-based ternary. For example: * * ```ts * type A<T> = T extends string ? true : false * ``` * * In this example, the ConditionalType is `T extends string ? true : false`. * The following properties of the node are as follows from the example: * - `checkType`: `T` * - `extendsType`: `string` * - `trueType`: `true` * - `falseType`: `false` */ if (t.isConditionalType(node)) { return { kind: 'conditional', check: getValueFromNode(parser, node.checkType), extends: getValueFromNode(parser, node.extendsType), trueType: getValueFromNode(parser, node.trueType), falseType: getValueFromNode(parser, node.falseType), }; } /** * A QualifiedName is a type-base dot property access. It is used to access properties of a * namespace. * * ```ts * A.B * ``` * * In this example, the namespace is `A` while the property of the namespace is `B`. Typescript * doesn't treat property access and index access as interchangeable. For example, `A.B` is not * the same this as `A['B']` in Typescript. The former is only allowed on namespaces while the * latter is only allowed for everything else. For example, accessing a property of an interface. */ if (t.isQualifiedName(node)) { if (isExportedSymbol(parser, node.left)) { const value = checker.typeToString(checker.getTypeAtLocation(node.left)); return { kind: 'qualifiedName', left: { kind: 'symbol', name: safeGetText(parser, node.left), value }, right: { kind: 'string', value: safeGetText(parser, node.right) }, }; } // if the node.left is not exported, we'll reduce to a type const type = checker.getTypeAtLocation(node); return getValueFromType(parser, type, node) || unknownValue(checker.typeToString(type)); } /** * A TypeQuery is the `typeof` keyword that instructs Typescript to extract the type of a value. * For example: * * ```ts * type A = typeof a * ``` * * In this example, the TypeQuery is `typeof a` */ if (t.isTypeQuery(node)) { if (isExportedSymbol(parser, node.exprName)) { const value = checker.typeToString(checker.getTypeAtLocation(node.exprName)); return { kind: 'symbol', name: node.exprName.getText(), value }; } const symbol = getSymbolFromNode(parser, node.exprName); if (symbol) { const declaration = getValueDeclaration(symbol); return getValueFromNode(parser, declaration); } } /** * A PropertyAccessExpression is an expression with a property on it. * * For example: * ```ts * foo.bar = 'bar' * ``` * * In this example, the `PropertyAccessExpression` is `foo.bar`. It can be used by functions to * add additional properties on the function. */ if (t.isPropertyAccessExpression(node)) { let typeInfo; if (t.isAsExpression(node.name)) { typeInfo = getValueFromNode(parser, node.name); } const type = checker.getTypeAtLocation(node); typeInfo = getValueFromType(parser, type) || unknownValue(checker.typeToString(type)); return { kind: 'property', name: node.name.getText(), type: typeInfo }; } /** * A TypeReference is a node that references a Typescript type rather than a JavaScript value. * * In the following example, `MyType` is a TypeReference (the declaration of MyType is omitted) * ```ts * const a = 'a' as MyType * const b: MyType = 'b' * type A = MyType * type B = Record<MyType> * ``` * * Any time a type is referenced (not declared) is a TypeReference. * ``` */ if (t.isTypeReference(node)) { // handle `as const` specially. If we don't do this, we'll get into an infinite loop if (safeGetText(parser, node) === 'const') { const type = checker.getTypeAtLocation(node.parent.parent); return getValueFromType(parser, type) || unknownValue(node.parent.parent.getText()); } const typeParameters = (_e = node.typeArguments) === null || _e === void 0 ? void 0 : _e.map(p => getValueFromNode(parser, p)); const symbolNode = t.isQualifiedName(node.typeName) ? node.typeName.right : node.typeName; const symbol = getSymbolFromNode(parser, symbolNode); const fileName = (_f = getValueDeclaration(symbol)) === null || _f === void 0 ? void 0 : _f.getSourceFile().fileName; const externalSymbol = getExternalSymbol((symbol === null || symbol === void 0 ? void 0 : symbol.name) || safeGetText(parser, node), fileName); if (externalSymbol) { return { kind: 'external', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', url: externalSymbol, typeParameters, }; } if (isExportedSymbol(parser, symbolNode)) { const value = checker.typeToString(checker.getTypeAtLocation(node.typeName)); return { kind: 'symbol', name: safeGetText(parser, node.typeName), typeParameters, value }; } // If it is a qualified name, handle that specially. The `left` might be a symbol if (t.isQualifiedName(node.typeName)) { return getValueFromNode(parser, node.typeName); } // The TypeReference isn't exported, so we'll return the type of the // symbol's value declaration directly const type = checker.getTypeAtLocation(node); if (symbol) { if (type.getFlags() & ts.TypeFlags.Instantiable) { symbol.name; // It is a generic type return { kind: 'generic', name: symbol === null || symbol === void 0 ? void 0 : symbol.name }; } const declaration = getValueDeclaration(symbol); if (declaration) { const typeInfo = getValueFromNode(parser, declaration); // we want to embed objects if (typeInfo.kind === 'object') { return typeInfo; } } } // The type reference is not external, not an exported symbol, and not generic. // Fall back to returning the value from it's type property return getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)); } /** * A ShorthandPropertyAssignment is a PropertyAssignment that is shorthanded where the `name` and * `initializer` are the same value. * * For example: * ```ts * const a = { * b * } * ``` * * In this example, `b` is the `ShorthandPropertyAssignment`. `b` is both the `name` and * `initializer` of the PropertyAssignment. * * Note the symbol declaration is the PropertyAssignment and not the symbol of the initializer. In * a PropertyAssignment, there are two parts, the `name` (name of the property) and an * `initializer` (the value of the property). In a PropertyAssignment, the `initializer` symbol * points to the VariableDeclaration. */ if (t.isShorthandPropertyAssignment(node)) { const type = checker.getTypeAtLocation(node); const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); // see if the declaration is assigned to something exported return { kind: 'property', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', type: getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)), ...jsDoc, }; } /** * Declaration of a enum * * For example: * ```ts * enum A { * a = 'a', * b = 'b' * } * ``` */ if (t.isEnumDeclaration(node)) { return { kind: 'object', typeParameters: [], properties: node.members.map((m, index) => { return { kind: 'property', name: safeGetText(parser, m.name), type: m.initializer ? getValueFromNode(parser, m.initializer) : { kind: 'number', value: index }, }; }), callSignatures: [], }; } /** * An Identifier isn't normally a node type we deal with, but it is useful for determining * defaultValue of things. If the initializer of a property is an identifier and that identifier * is an exported symbol, we'll assign it a symbol. * * For example: * ```ts * const a = b * ``` * * In this example, `b` is the identifier because it is a value reference to something else. */ if (t.isIdentifier(node)) { if (isExportedSymbol(parser, node)) { const symbol = getSymbolFromNode(parser, node); const declaration = getValueDeclaration(symbol); return { kind: 'symbol', name: node.text, fileName: declaration === null || declaration === void 0 ? void 0 : declaration.getSourceFile().fileName, value: node.text, }; } if (node.text === 'undefined') { return { kind: 'primitive', value: 'undefined' }; } } /** * */ if (t.isParameter(node)) { const type = checker.getTypeAtLocation(node); const symbol = getSymbolFromNode(parser, node); const jsDoc = findDocComment(checker, symbol); const isRequired = node.questionToken ? false : node.initializer ? false : symbol ? !isOptional(symbol) && !includesUndefined(type) : false; const typeInfo = node.type ? getValueFromNode(parser, node.type) : getValueFromType(parser, type) || unknownValue(safeGetText(parser, node)); const defaultValue = node.initializer ? getValueFromNode(parser, node.initializer) : undefined; /** * Set default values if an object binding pattern is found. We do this at the Parameter level, * because we have all the info here. * * For example: * ```ts * function A({ a = 'a', b}: Params) * ``` * * In this example, the ObjectBindingPattern is `{ a = 'a', b }` */ if (t.isObjectBindingPattern(node.name)) { const defaults = getDefaultsFromObjectBindingParameter(parser, node); if (typeInfo.kind === 'object') { typeInfo.properties.forEach(p => { if (!p.defaultValue && defaults[p.name]) { p.defaultValue = defaults[p.name]; } }); } } return { kind: 'parameter', name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', defaultValue, type: typeInfo, required: isRequired, rest: !!node.dotDotDotToken, ...jsDoc, }; } /** * A call expression is an expression calling a function. In this case, we want to get the signature * and get a type for the return type */ if (t.isCallExpression(node)) { const type = checker.getTypeAtLocation(node); const value = getValueFromType(parser, type); if (value) { return value; } } const symbol = getSymbolFromNode(parser, node); if (!symbol) { return unknownValue(safeGetText(parser, node)); } return unknownValue(safeGetText(parser, node)); } /** True if this is visible outside this file, false otherwise */ function isNodeExported(checker, node) { return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0; } export const defaultJSDoc = { description: '', tags: {}, declarations: [], }; export function getFullJsDocComment(checker, symbol) { var _a; if (symbol.getDocumentationComment === undefined) { return defaultJSDoc; } let mainComment = ts.displayPartsToString(symbol.getDocumentationComment(checker)); if (mainComment) { mainComment = mainComment.replace(/\r\n/g, '\n'); } const tags = symbol.getJsDocTags() || []; const tagMap = {}; tags.forEach(tag => { const trimmedText = (tag.text || []) .map(s => s.text) .join('\n') .trim(); const currentValue = tagMap[tag.name]; tagMap[tag.name] = currentValue ? currentValue + '\n' + trimmedText : trimmedText; }); return { description: mainComment, declarations: ((_a = symbol === null || symbol === void 0 ? void 0 : symbol.declarations) !== null && _a !== void 0 ? _a : []).map(d => ({ name: (symbol === null || symbol === void 0 ? void 0 : symbol.name) || '', filePath: d.getSourceFile().fileName, })), tags: tagMap, }; } export function findDocComment(checker, symbol) { if (symbol) { const comment = getFullJsDocComment(checker, symbol); if (comment.description || comment.declarations.length || comment.tags.default) { return comment; } const rootSymbols = checker.getRootSymbols(symbol); const commentsOnRootSymbols = rootSymbols .filter(x => x !== symbol) .map(x => getFullJsDocComment(checker, x)) .filter(x => !!x.description || !!comment.tags.default); if (commentsOnRootSymbols.length) { return commentsOnRootSymbols[0]; } } return defaultJSDoc; } /** * Attempt to get the name of a declaration or expression */ export function getNodeName(node) { const name = ts.getNameOfDeclaration(node); if (name && 'text' in name) { return name.text; } return; } export function filterObjectProperties(value) { return value.kind === 'property'; } export function filterObjectTypeParameters(value) { return value.kind === 'typeParameter'; } export function filterFunctionParameters(value) { return value.kind === 'parameter'; } // https://github.com/dsherret/ts-ast-viewer/blob/c71e238123d972bae889b3829e23b44f39d8d5c2/site/src/components/PropertiesViewer.tsx#L172 export function getSymbolFromNode(parser, node) { return node.symbol || parser.checker.getSymbolAtLocation(node); } export function isObject(type) { return !!(type.flags & ts.TypeFlags.Object); } function includesUndefined(type) { return type.isUnion() ? type.types.some(t => t.flags & ts.TypeFlags.Undefined) : false; } function isTupleType(type) { var _a; return !!(((_a = type.target) === null || _a === void 0 ? void 0 : _a.objectFlags) & ts.ObjectFlags.Tuple); } /** * Given a node, extract a default value and only return a `Value` if we consider it to be * a valid default. For example, a literal is valid. * * Valid: * - `'medium'` * - `true` * - 10 * * Invalid: * - `string` * - `{foo: string}` */ export function getValidDefaultFromNode(parser, node) { if (t.isFalseKeyword(node) || t.isTrueKeyword(node) || t.isLiteralType(node) || t.isStringLiteral(node) || t.isNumericLiteral(node)) { return parser.getValueFromNode(node); } return undefined; } /** * A parameter might represent a `ObjectBindingPattern` which can be used to set defaults. This will * return all defaults found within the `ObjectBindingPattern` and return them as a map of the * property name to the `Value`. These defaults can be used to piece together a default. Also * `getDefaultFromTags` can be used to get defaults from JSDoc tags. */ export function getDefaultsFromObjectBindingParameter(parser, node) { if (t.isObjectBindingPattern(node.name)) { return node.name.elements.reduce((result, element) => { if (t.isBindingElement(element) && t.isIdentifier(element.name) && element.initializer) { const defaultValue = getValidDefaultFromNode(parser, element.initializer); if (defaultValue) { result[element.name.text] = defaultValue; } } return result; }, {}); } return {}; } /** * An index signature is like a "leftover" of an object. For example: * * ```ts * interface A { * a: string, * b: string, * [key: string]: string * } * ``` * * The index signature is `[key: string]: string`. It allows an interface to specify valid * additional properties even though the interface has specific properties defined. Index signature * types depend on the Typescript version, so this function will have to be updated to support the * correct version of Typescript. */ function getIndexSignatureFromType(parser, type) { var _a; const { checker } = parser; const indexSignature = checker.getIndexInfoOfType(type, ts.IndexKind.String) || checker.getIndexInfoOfType(type, ts.IndexKind.Number); if (indexSignature) { const parameter = (_a = indexSignature.declaration) === null || _a === void 0 ? void 0 : _a.parameters[0]; return { kind: 'indexSignature', name: (parameter === null || parameter === void 0 ? void 0 : parameter.name) ? safeGetText(parser, parameter === null || parameter === void 0 ? void 0 : parameter.name) : '', type: (parameter && getValueFromType(parser, checker.getTypeAtLocation(parameter), parameter)) || unknownValue(''), value: indexSignature.declaration ? getValueFromNode(parser, indexSignature.declaration.type) : unknownValue(checker.typeToString(type)), }; } return; } /** * * @param checker The shared Typescript checker * @param type The type we're trying to find a Value for * @param node An optional node that was used to generate the Type. It should be used for all type * nodes (AST nodes that are types and not JS values). This extra information can prevent errors and * infinite loops. */ export function getValueFromType(parser, type, node) { const { checker } = parser; const originalNodeKind = node === null || node === void 0 ? void 0 : node.kind; const typeToString = checker.typeToString(type); // If the type is `any`, we want to bail now if (type.flags & ts.TypeFlags.Any) { return { kind: 'primitive', value: 'any' }; } // check if the node is an external symbol // TODO: This won't work if the symbol contains a generic const externalSymbol = getExternalSymbol(typeToString); if (externalSymbol) { return { kind: 'external', name: typeToString, url: externalSymbol }; } if (isTupleType(type)) { return { kind: 'union', value: checker.getTypeArguments(type).map(t => getValueFromType(parser, t) || unknownValue('')) || [], }; } // See if there is a TypeNode associated with the type. This is common in type definitions and can // be useful to get type parameters (for example, `Promise<boolean>`, `boolean` is a type // parameter). I'm using Signatures to get a return `Type`, but it might be next to impossible // to get a real `Node` out of it again. The `Type` of this node is `any` making it difficult. // For example, I might get a `BooleanKeyword` syntax kind, but I don't have guards for that type. // // A TypeNode is a synthetic node that doesn't have any associated source code. A TypeNode can // contain child nodes that are linked to real AST nodes in the source code. Think of a TypeNode // as an AST representation of `checker.typeToString()`. The string version is reduced to a string // of characters where a TypeNode is an AST representation of that string. // // One interesting fact about this is when you have a union type that overflows. You may see // something like: // ```ts // "'a' | 'b' | ...23 more... | 'z'" // ``` // In this case, the TypeNode will be a UnionType that contains an Identifier node with a name of // "...23 more..." // // We generally prefer TypeNodes because I'm pretty sure it is what the Typescript language // service uses for the tooltips when hovering over text in your IDE. We try to go from Type to // Node whenever possible, but there are some cases where interacting directly with the types is // preferred. The union example is one of such examples. const typeNode = checker.typeToTypeNode(type, node, ts.NodeBuilderFlags.NoTruncation); // We try to extract useful type information from the TypeNode and go back to recursing the AST. // But, if the typeNode and original node have the same kind, we've actually lost information and // should skip processing the TypeNode. if (typeNode && originalNodeKind !== typeNode.kind) { // find the symbol if (t.isTypeReference(typeNode)) { const symbol = getSymbolFromNode(parser, typeNode.typeName); const declaration = getValueDeclaration(symbol); const fileName = declaration === null || declaration === void 0 ? void 0 : declaration.getSourceFile().fileName; if (symbol) { const externalSymbol = getExternalSymbol(symbol.name, fileName); if (externalSymbol) { return { kind: 'external', name: symbol.name, url: externalSymbol }; } if (declaration && declaration !== node) { if (isExportedSymbol(parser, declaration)) { return { kind: 'symbol', name: symbol.name, value: typeToString }; } return getValueFromNode(parser, declaration); } } } // Figure out if we should recurse back into AST nodes with our synthetic TypeNode. There are // exceptions to using the TypeNode. Exceptions where we actually lose type information. Some // examples are large unions or `keyof`: let exceptions = false; // The typeNode is a UnionType, but the number of items has overflowed with an identifier `... // {N} more ...`, so we don't want to use the typeNode and will use `type.types` instead which // is not shortened. In this case we want to do nothing and use the type-based union check if (t.isUnionType(typeNode) && type.isUnion() && typeNode.types.some(v => t.isIdentifier(v) || (t.isTypeReference(v) && t.isIdentifier(v.typeName) && v.typeName.escapedText.includes('more')))) { exceptions = true; } // A function TypeNode looses a lot of information. We'll fallback to using the type. A Function // is an object with call signatures which is caught in the `t.isObject()` check if (t.isFunctionType(typeNode)) { exceptions = true; } // A TypeNode of `keyof` looses type information if (t.isTypeOperator(typeNode) && typeNode.operator === ts.SyntaxKind.KeyOfKeyword) { exceptions = true; } // A TypeNode of a Type literal looses symbol and JSDoc info if (t.isTypeLiteral(typeNode)) { exceptions = true; } // We want type unions to override type references that are not exported if (t.isTypeReference(typeNode) && type.isUnion()) { exceptions = true; } if (!exceptions) { try { return getValueFromNode(parser, typeNode); } catch (e) { // If we are here, we've run into an issue parsing the TypeNode. This could happen if a // Synthetic node contains `keyof` which is a type that doesn't point to a real AST node so // Typescript cannot evaluate the type and will result in an error. Under these cases, we'll // fall back to extracting info from the Type rather than the TypeNode. // We don't want to log this as it spams the console... // cons