UNPKG

web-component-analyzer

Version:
1,395 lines (1,380 loc) 200 kB
'use strict'; var tsModule = require('typescript'); var tsSimpleType = require('ts-simple-type'); var fs = require('fs'); var path = require('path'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var tsModule__namespace = /*#__PURE__*/_interopNamespaceDefault(tsModule); /** * Takes a node and tries to resolve a constant value from it. * Returns undefined if no constant value can be resolved. * @param node * @param context */ function resolveNodeValue(node, context) { var _a, _b; if (node == null) return undefined; const { ts, checker } = context; const depth = (context.depth || 0) + 1; // Always break when depth is larger than 10. // This ensures we cannot run into infinite recursion. if (depth > 10) return undefined; if (ts.isStringLiteralLike(node)) { return { value: node.text, node }; } else if (ts.isNumericLiteral(node)) { return { value: Number(node.text), node }; } else if (ts.isPrefixUnaryExpression(node)) { const value = (_a = resolveNodeValue(node.operand, { ...context, depth })) === null || _a === void 0 ? void 0 : _a.value; return { value: applyPrefixUnaryOperatorToValue(value, node.operator, ts), node }; } else if (ts.isObjectLiteralExpression(node)) { const object = {}; for (const prop of node.properties) { if (ts.isPropertyAssignment(prop)) { // Resolve the "key" const name = ((_b = resolveNodeValue(prop.name, { ...context, depth })) === null || _b === void 0 ? void 0 : _b.value) || prop.name.getText(); // Resolve the "value const resolvedValue = resolveNodeValue(prop.initializer, { ...context, depth }); if (resolvedValue != null && typeof name === "string") { object[name] = resolvedValue.value; } } } return { value: object, node }; } else if (node.kind === ts.SyntaxKind.TrueKeyword) { return { value: true, node }; } else if (node.kind === ts.SyntaxKind.FalseKeyword) { return { value: false, node }; } else if (node.kind === ts.SyntaxKind.NullKeyword) { return { value: null, node }; } else if (node.kind === ts.SyntaxKind.UndefinedKeyword) { return { value: undefined, node }; } // Resolve initializers for variable declarations if (ts.isVariableDeclaration(node)) { return resolveNodeValue(node.initializer, { ...context, depth }); } // Resolve value of a property access expression. For example: MyEnum.RED else if (ts.isPropertyAccessExpression(node)) { return resolveNodeValue(node.name, { ...context, depth }); } // Resolve [expression] parts of {[expression]: "value"} else if (ts.isComputedPropertyName(node)) { return resolveNodeValue(node.expression, { ...context, depth }); } // Resolve initializer value of enum members. else if (ts.isEnumMember(node)) { if (node.initializer != null) { return resolveNodeValue(node.initializer, { ...context, depth }); } else { return { value: `${node.parent.name.text}.${node.name.getText()}`, node }; } } // Resolve values of variables. else if (ts.isIdentifier(node) && checker != null) { const declarations = resolveDeclarations(node, { checker, ts }); if (declarations.length > 0) { const resolved = resolveNodeValue(declarations[0], { ...context, depth }); if (context.strict || resolved != null) { return resolved; } } if (context.strict) { return undefined; } return { value: node.getText(), node }; } // Fallthrough // - "my-value" as string // - <any>"my-value" // - ("my-value") else if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isParenthesizedExpression(node)) { return resolveNodeValue(node.expression, { ...context, depth }); } // static get is() { // return "my-element"; // } else if ((ts.isGetAccessor(node) || ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) && node.body != null) { for (const stm of node.body.statements) { if (ts.isReturnStatement(stm)) { return resolveNodeValue(stm.expression, { ...context, depth }); } } } // [1, 2] else if (ts.isArrayLiteralExpression(node)) { return { node, value: node.elements.map(el => { var _a; return (_a = resolveNodeValue(el, { ...context, depth })) === null || _a === void 0 ? void 0 : _a.value; }) }; } if (ts.isTypeAliasDeclaration(node)) { return resolveNodeValue(node.type, { ...context, depth }); } if (ts.isLiteralTypeNode(node)) { return resolveNodeValue(node.literal, { ...context, depth }); } if (ts.isTypeReferenceNode(node)) { return resolveNodeValue(node.typeName, { ...context, depth }); } return undefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function applyPrefixUnaryOperatorToValue(value, operator, ts) { if (typeof value === "object" && value != null) { return value; } switch (operator) { case ts.SyntaxKind.MinusToken: return -value; case ts.SyntaxKind.ExclamationToken: return !value; case ts.SyntaxKind.PlusToken: return +value; } return value; } /** * Converts from snake case to camel case * @param str */ /** * Converts from camel case to snake case * @param str */ function camelToDashCase(str) { return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`); } /** * Returns if a name is private (starts with "_" or "#"); * @param name * @param name */ function isNamePrivate(name) { return name.startsWith("_") || name.startsWith("#"); } /** * Resolves all relevant declarations of a specific node. * @param node * @param context */ function resolveDeclarations(node, context) { if (node == null) return []; const symbol = getSymbol(node, context); if (symbol == null) return []; return resolveSymbolDeclarations(symbol); } /** * Returns the symbol of a node. * This function follows aliased symbols. * @param node * @param context */ function getSymbol(node, context) { if (node == null) return undefined; const { checker, ts } = context; // Get the symbol let symbol = checker.getSymbolAtLocation(node); if (symbol == null) { const identifier = getNodeIdentifier(node, context); symbol = identifier != null ? checker.getSymbolAtLocation(identifier) : undefined; } // Resolve aliased symbols if (symbol != null && isAliasSymbol(symbol, ts)) { symbol = checker.getAliasedSymbol(symbol); if (symbol == null) return undefined; } return symbol; } /** * Resolves the declarations of a symbol. A valueDeclaration is always the first entry in the array * @param symbol */ function resolveSymbolDeclarations(symbol) { // Filters all declarations const valueDeclaration = symbol.valueDeclaration; const declarations = symbol.getDeclarations() || []; if (valueDeclaration == null) { return declarations; } else { // Make sure that "valueDeclaration" is always the first entry return [valueDeclaration, ...declarations.filter(decl => decl !== valueDeclaration)]; } } /** * Resolve a declaration by trying to find the real value by following assignments. * @param node * @param context */ function resolveDeclarationsDeep(node, context) { const declarations = []; const allDeclarations = resolveDeclarations(node, context); for (const declaration of allDeclarations) { if (context.ts.isVariableDeclaration(declaration) && declaration.initializer != null && context.ts.isIdentifier(declaration.initializer)) { declarations.push(...resolveDeclarationsDeep(declaration.initializer, context)); } else if (context.ts.isTypeAliasDeclaration(declaration) && declaration.type != null && context.ts.isIdentifier(declaration.type)) { declarations.push(...resolveDeclarationsDeep(declaration.type, context)); } else { declarations.push(declaration); } } return declarations; } /** * Returns if the symbol has "alias" flag * @param symbol * @param ts */ function isAliasSymbol(symbol, ts) { return hasFlag(symbol.flags, ts.SymbolFlags.Alias); } /** * Returns a set of modifiers on a node * @param node * @param ts */ function getModifiersFromNode(node, ts) { const modifiers = new Set(); if (hasModifier(node, ts.SyntaxKind.ReadonlyKeyword, ts)) { modifiers.add("readonly"); } if (hasModifier(node, ts.SyntaxKind.StaticKeyword, ts)) { modifiers.add("static"); } if (ts.isGetAccessor(node)) { modifiers.add("readonly"); } return modifiers.size > 0 ? modifiers : undefined; } /** * Returns if a number has a flag * @param num * @param flag */ function hasFlag(num, flag) { return (num & flag) !== 0; } /** * Returns if a node has a specific modifier. * @param node * @param modifierKind */ function hasModifier(node, modifierKind, ts) { if (!ts.canHaveModifiers(node)) { return false; } const modifiers = ts.getModifiers(node); if (modifiers == null) return false; return (node.modifiers || []).find(modifier => modifier.kind === modifierKind) != null; } /** * Returns the visibility of a node */ function getMemberVisibilityFromNode(node, ts) { if (hasModifier(node, ts.SyntaxKind.PrivateKeyword, ts) || ("name" in node && ts.isIdentifier(node.name) && isNamePrivate(node.name.text))) { return "private"; } else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword, ts)) { return "protected"; } else if (getNodeSourceFileLang(node) === "ts") { // Only return "public" in typescript land return "public"; } return undefined; } /** * Returns all keys and corresponding interface/class declarations for keys in an interface. * @param interfaceDeclaration * @param context */ function getInterfaceKeys(interfaceDeclaration, context) { const extensions = []; const { ts } = context; for (const member of interfaceDeclaration.members) { // { "my-button": MyButton; } if (ts.isPropertySignature(member) && member.type != null) { const resolvedKey = resolveNodeValue(member.name, context); if (resolvedKey == null) { continue; } let identifier; let declaration; if (ts.isTypeReferenceNode(member.type)) { // { ____: MyButton; } or { ____: namespace.MyButton; } identifier = member.type.typeName; } else if (ts.isTypeLiteralNode(member.type)) { identifier = undefined; declaration = member.type; } else { continue; } if (declaration != null || identifier != null) { extensions.push({ key: String(resolvedKey.value), keyNode: resolvedKey.node, declaration, identifier }); } } } return extensions; } /** * Find a node recursively walking up the tree using parent nodes. * @param node * @param test */ function findParent(node, test) { if (node == null) return; return test(node) ? node : findParent(node.parent, test); } /** * Find a node recursively walking down the children of the tree. Depth first search. * @param node * @param test */ function findChild(node, test) { if (!node) return; if (test(node)) return node; return node.forEachChild(child => findChild(child, test)); } /** * Find multiple children by walking down the children of the tree. Depth first search. * @param node * @param test * @param emit */ function findChildren(node, test, emit) { if (!node) return; if (test(node)) { emit(node); } node.forEachChild(child => findChildren(child, test, emit)); } /** * Returns the language of the node's source file * @param node */ function getNodeSourceFileLang(node) { return node.getSourceFile().fileName.endsWith("ts") ? "ts" : "js"; } /** * Returns the leading comment for a given node * @param node * @param ts */ function getLeadingCommentForNode(node, ts) { const sourceFileText = node.getSourceFile().text; const leadingComments = ts.getLeadingCommentRanges(sourceFileText, node.pos); if (leadingComments != null && leadingComments.length > 0) { return sourceFileText.substring(leadingComments[0].pos, leadingComments[0].end); } return undefined; } /** * Returns the declaration name of a given node if possible. * @param node * @param context */ function getNodeName(node, context) { var _a; return (_a = getNodeIdentifier(node, context)) === null || _a === void 0 ? void 0 : _a.getText(); } /** * Returns the declaration name of a given node if possible. * @param node * @param context */ function getNodeIdentifier(node, context) { if (context.ts.isIdentifier(node)) { return node; } else if ((context.ts.isClassLike(node) || context.ts.isInterfaceDeclaration(node) || context.ts.isVariableDeclaration(node) || context.ts.isMethodDeclaration(node) || context.ts.isPropertyDeclaration(node) || context.ts.isFunctionDeclaration(node)) && node.name != null && context.ts.isIdentifier(node.name)) { return node.name; } return undefined; } /** * Returns all decorators in either the node's `decorators` or `modifiers`. * @param node * @param context */ function getDecorators(node, context) { var _a; const { ts } = context; return ts.canHaveDecorators(node) ? (_a = ts.getDecorators(node)) !== null && _a !== void 0 ? _a : [] : []; } /** * Visits custom element definitions. * @param node * @param ts * @param checker */ function discoverDefinitions$5(node, { ts, checker }) { // customElements.define("my-element", MyElement) if (ts.isCallExpression(node)) { if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.escapedText === "define") { let leftExpression = node.expression.expression; // Take "window.customElements" into account and return the "customElements" part if (ts.isPropertyAccessExpression(leftExpression) && ts.isIdentifier(leftExpression.expression) && leftExpression.expression.escapedText === "window") { leftExpression = leftExpression.name; } // Check if the "left expression" is called "customElements" if (ts.isIdentifier(leftExpression) && leftExpression.escapedText === "customElements" && node.expression.name != null && ts.isIdentifier(node.expression.name)) { // Find the arguments of: define("my-element", MyElement) const [unresolvedTagNameNode, identifierNode] = node.arguments; // Resolve the tag name node // ("my-element", MyElement) const resolvedTagNameNode = resolveNodeValue(unresolvedTagNameNode, { ts, checker, strict: true }); if (resolvedTagNameNode != null && identifierNode != null && typeof resolvedTagNameNode.value === "string") { const tagName = resolvedTagNameNode.value; const tagNameNode = resolvedTagNameNode.node; // (___, MyElement) if (ts.isIdentifier(identifierNode)) { return [ { tagName, identifierNode, tagNameNode } ]; } // (___, class { ... }) else if (ts.isClassLike(identifierNode) || ts.isInterfaceDeclaration(identifierNode)) { return [ { tagName, tagNameNode, declarationNode: identifierNode } ]; } } } } return undefined; } // interface HTMLElementTagNameMap { "my-button": MyButton; } if (ts.isInterfaceDeclaration(node) && ["HTMLElementTagNameMap", "ElementTagNameMap"].includes(node.name.text)) { const extensions = getInterfaceKeys(node, { ts, checker }); return extensions.map(({ key, keyNode, identifier, declaration }) => ({ tagName: key, tagNameNode: keyNode, identifierNode: identifier, declarationNode: declaration })); } return undefined; } /** * Flattens an array. * Use this function to keep support for node 10 * @param items */ function arrayFlat(items) { // eslint-disable-next-line @typescript-eslint/no-explicit-any if ("flat" in items) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return items.flat(); } const flattenArray = []; for (const item of items) { if (Array.isArray(item)) { flattenArray.push(...item); } else { flattenArray.push(item); } } return flattenArray; } /** * Filters an array returning only defined items * @param array */ function arrayDefined(array) { return array.filter((item) => item != null); } /** * Filters an array returning only unique itesm * @param array */ function arrayDedupe(array) { const uniqueItems = []; for (const item of array) { if (uniqueItems.indexOf(item) === -1) { uniqueItems.push(item); } } return uniqueItems; } const NOTHING = Symbol(); /** * This function wraps a callback returning a value and cahced the value. * @param callback */ function lazy(callback) { let value = NOTHING; return () => { if (value === NOTHING) { value = callback(); } return value; }; } /** * Relax the type so that for example "string literal" become "string" and "function" become "any" * This is used for javascript files to provide type checking with Typescript type inferring * @param type */ function relaxType(type) { switch (type.kind) { case "INTERSECTION": case "UNION": return { ...type, types: type.types.map(t => relaxType(t)) }; case "ENUM": return { ...type, types: type.types.map(t => relaxType(t)) }; case "ARRAY": return { ...type, type: relaxType(type.type) }; case "PROMISE": return { ...type, type: relaxType(type.type) }; case "OBJECT": return { name: type.name, kind: "OBJECT" }; case "INTERFACE": case "FUNCTION": case "CLASS": return { name: type.name, kind: "ANY" }; case "NUMBER_LITERAL": return { kind: "NUMBER" }; case "STRING_LITERAL": return { kind: "STRING" }; case "BOOLEAN_LITERAL": return { kind: "BOOLEAN" }; case "BIG_INT_LITERAL": return { kind: "BIG_INT" }; case "ENUM_MEMBER": return { ...type, type: relaxType(type.type) }; case "ALIAS": return { ...type, target: relaxType(type.target) }; case "NULL": case "UNDEFINED": return { kind: "ANY" }; default: return type; } } // Only search in "lib.dom.d.ts" performance reasons for now const LIB_FILE_NAMES = ["lib.dom.d.ts"]; // Map "tsModule => name => SimpleType" const LIB_TYPE_CACHE = new Map(); /** * Return a Typescript library type with a specific name * @param name * @param ts * @param program */ function getLibTypeWithName(name, { ts, program }) { var _a; const nameTypeCache = LIB_TYPE_CACHE.get(ts) || new Map(); if (nameTypeCache.has(name)) { return nameTypeCache.get(name); } else { LIB_TYPE_CACHE.set(ts, nameTypeCache); } let node; for (const libFileName of LIB_FILE_NAMES) { const sourceFile = program.getSourceFile(libFileName) || program.getSourceFiles().find(f => f.fileName.endsWith(libFileName)); if (sourceFile == null) { continue; } for (const statement of sourceFile.statements) { if (ts.isInterfaceDeclaration(statement) && ((_a = statement.name) === null || _a === void 0 ? void 0 : _a.text) === name) { node = statement; break; } } if (node != null) { break; } } const checker = program.getTypeChecker(); let type = node == null ? undefined : tsSimpleType.toSimpleType(node, checker); if (type != null) { // Apparently Typescript wraps the type in "generic arguments" when take the type from the interface declaration // Remove "generic arguments" here if (type.kind === "GENERIC_ARGUMENTS") { type = type.target; } } nameTypeCache.set(name, type); return type; } /** * Returns typescript jsdoc node for a given node * @param node * @param ts */ function getJSDocNode(node, ts) { var _a, _b, _c; const parent = (_b = (_a = ts.getJSDocTags(node)) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.parent; if (parent != null && ts.isJSDoc(parent)) { return parent; } // eslint-disable-next-line @typescript-eslint/no-explicit-any return (_c = node.jsDoc) === null || _c === void 0 ? void 0 : _c.find((n) => ts.isJSDoc(n)); } /** * Returns jsdoc for a given node. * @param node * @param ts * @param tagNames */ function getJsDoc(node, ts, tagNames) { var _a; const jsDocNode = getJSDocNode(node, ts); // If we couldn't find jsdoc, find and parse the jsdoc string ourselves if (jsDocNode == null) { const leadingComment = getLeadingCommentForNode(node, ts); if (leadingComment != null) { const jsDoc = parseJsDocString(leadingComment); // Return this jsdoc if we don't have to filter by tag name if (jsDoc == null || tagNames == null || tagNames.length === 0) { return jsDoc; } return { ...jsDoc, tags: (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.filter(t => tagNames.includes(t.tag)) }; } return undefined; } // Parse all jsdoc tags // Typescript removes some information after parsing jsdoc tags, so unfortunately we will have to parse. return { description: jsDocNode.comment == null ? undefined : unescapeJSDoc(String(jsDocNode.comment)), node: jsDocNode, tags: jsDocNode.tags == null ? [] : arrayDefined(jsDocNode.tags.map(node => { var _a, _b; const tag = String(node.tagName.escapedText); // Filter by tag name if (tagNames != null && tagNames.length > 0 && !tagNames.includes(tag.toLowerCase())) { return undefined; } // If Typescript generated a "type expression" or "name", comment will not include those. // We can't just use what typescript parsed because it doesn't include things like optional jsdoc: name notation [...] // Therefore we need to manually get the text and remove newlines/* const typeExpressionPart = "typeExpression" in node ? (_a = node.typeExpression) === null || _a === void 0 ? void 0 : _a.getText() : undefined; const namePart = "name" in node ? (_b = node.name) === null || _b === void 0 ? void 0 : _b.getText() : undefined; const fullComment = (typeExpressionPart === null || typeExpressionPart === void 0 ? void 0 : typeExpressionPart.startsWith("@")) ? // To make matters worse, if Typescript can't parse a certain jsdoc, it will include the rest of the jsdocs tag from there in "typeExpressionPart" // Therefore we check if there are multiple jsdoc tags in the string to only take the first one // This will discard the following jsdocs, but at least we don't crash :-) typeExpressionPart.split(/\n\s*\*\s?@/)[0] || "" : `@${tag}${typeExpressionPart != null ? ` ${typeExpressionPart} ` : ""}${namePart != null ? ` ${namePart} ` : ""} ${node.comment || ""}`; const comment = typeof node.comment === "string" ? node.comment.replace(/^\s*-\s*/, "").trim() : ""; return { node, tag, comment, parsed: lazy(() => parseJsDocTagString(fullComment)) }; })) }; } /** * Converts a given string to a SimpleType * Defaults to ANY * See http://usejsdoc.org/tags-type.html * @param str * @param context */ function parseSimpleJsDocTypeExpression(str, context) { // Fail safe if "str" is somehow undefined if (str == null) { return { kind: "ANY" }; } // Parse normal types switch (str.toLowerCase()) { case "undefined": return { kind: "UNDEFINED" }; case "null": return { kind: "NULL" }; case "string": return { kind: "STRING" }; case "number": return { kind: "NUMBER" }; case "boolean": return { kind: "BOOLEAN" }; case "array": return { kind: "ARRAY", type: { kind: "ANY" } }; case "object": return { kind: "OBJECT", members: [] }; case "any": case "*": return { kind: "ANY" }; } // Match // { string } if (str.startsWith(" ") || str.endsWith(" ")) { return parseSimpleJsDocTypeExpression(str.trim(), context); } // Match: // {string|number} if (str.includes("|")) { return { kind: "UNION", types: str.split("|").map(str => { const childType = parseSimpleJsDocTypeExpression(str, context); // Convert ANY types to string literals so that {on|off} is "on"|"off" and not ANY|ANY if (childType.kind === "ANY") { return { kind: "STRING_LITERAL", value: str }; } return childType; }) }; } // Match: // {?number} (nullable) // {!number} (not nullable) // {...number} (array of) const prefixMatch = str.match(/^(\?|!|(\.\.\.))(.+)$/); if (prefixMatch != null) { const modifier = prefixMatch[1]; const type = parseSimpleJsDocTypeExpression(prefixMatch[3], context); switch (modifier) { case "?": return { kind: "UNION", types: [ { kind: "NULL" }, type ] }; case "!": return type; case "...": return { kind: "ARRAY", type }; } } // Match: // {(......)} const parenMatch = str.match(/^\((.+)\)$/); if (parenMatch != null) { return parseSimpleJsDocTypeExpression(parenMatch[1], context); } // Match // {"red"} const stringLiteralMatch = str.match(/^["'](.+)["']$/); if (stringLiteralMatch != null) { return { kind: "STRING_LITERAL", value: stringLiteralMatch[1] }; } // Match // {[number]} const arrayMatch = str.match(/^\[(.+)]$/); if (arrayMatch != null) { return { kind: "ARRAY", type: parseSimpleJsDocTypeExpression(arrayMatch[1], context) }; } // Match // CustomEvent<string> // MyInterface<string, number> // MyInterface<{foo: string, bar: string}, number> const genericArgsMatch = str.match(/^(.*)<(.*)>$/); if (genericArgsMatch != null) { // Here we split generic arguments by "," and // afterwards remerge parts that were incorrectly split // For example: "{foo: string, bar: string}, number" would result in // ["{foo: string", "bar: string}", "number"] // The correct way to improve "parseSimpleJsDocTypeExpression" is to build a custom lexer/parser. const typeArgStrings = []; for (const part of genericArgsMatch[2].split(/\s*,\s*/)) { if (part.match(/[}:]/) != null && typeArgStrings.length > 0) { typeArgStrings[typeArgStrings.length - 1] += `, ${part}`; } else { typeArgStrings.push(part); } } return { kind: "GENERIC_ARGUMENTS", target: parseSimpleJsDocTypeExpression(genericArgsMatch[1], context), typeArguments: typeArgStrings.map(typeArg => parseSimpleJsDocTypeExpression(typeArg, context)) }; } // If nothing else, try to find the type in Typescript global lib or else return "any" return getLibTypeWithName(str, context) || { kind: "ANY" }; } /** * Finds a @type jsdoc tag in the jsdoc and returns the corresponding simple type * @param jsDoc * @param context */ function getJsDocType(jsDoc, context) { var _a; if (jsDoc.tags != null) { const typeJsDocTag = jsDoc.tags.find(t => t.tag === "type"); if (typeJsDocTag != null) { // We get the text of the node because typescript strips the type jsdoc tag under certain circumstances const parsedJsDoc = parseJsDocTagString(((_a = typeJsDocTag.node) === null || _a === void 0 ? void 0 : _a.getText()) || ""); if (parsedJsDoc.type != null) { return parseSimpleJsDocTypeExpression(parsedJsDoc.type, context); } } } } const JSDOC_TAGS_WITH_REQUIRED_NAME = ["param", "fires", "@element", "@customElement"]; /** * Takes a string that represents a value in jsdoc and transforms it to a javascript value * @param value */ function parseJsDocValue(value) { if (value == null) { return value; } // Parse quoted strings const quotedMatch = value.match(/^["'`](.*)["'`]$/); if (quotedMatch != null) { return quotedMatch[1]; } // Parse keywords switch (value) { case "false": return false; case "true": return true; case "undefined": return undefined; case "null": return null; } // Parse number if (!isNaN(Number(value))) { return Number(value); } return value; } /** * Parses "@tag {type} name description" or "@tag name {type} description" * @param str */ function parseJsDocTagString(str) { const jsDocTag = { tag: "" }; if (str[0] !== "@") { return jsDocTag; } const moveStr = (byLength) => { str = str.substring(typeof byLength === "number" ? byLength : byLength.length); }; const unqouteStr = (quotedStr) => { return quotedStr.replace(/^['"](.+)["']$/, (_, match) => match); }; const matchTag = () => { // Match tag // Example: " @mytag" const tagResult = str.match(/^(\s*@(\S+))/); if (tagResult == null) { return jsDocTag; } else { // Move string to the end of the match // Example: " @mytag|" moveStr(tagResult[1]); jsDocTag.tag = tagResult[2]; } }; const matchType = () => { // Match type // Example: " {MyType}" const typeResult = str.match(/^(\s*{([\s\S]*)})/); if (typeResult != null) { // Move string to the end of the match // Example: " {MyType}|" moveStr(typeResult[1]); jsDocTag.type = typeResult[2]; } }; const matchName = () => { // Match optional name // Example: " [myname=mydefault]" const defaultNameResult = str.match(/^(\s*\[([\s\S]+)\])/); if (defaultNameResult != null) { // Move string to the end of the match // Example: " [myname=mydefault]|" moveStr(defaultNameResult[1]); // Using [...] means that this doc is optional jsDocTag.optional = true; // Split the inner content between [...] into parts // Example: "myname=mydefault" => "myname", "mydefault" const parts = defaultNameResult[2].split("="); if (parts.length === 2) { // Both name and default were given jsDocTag.name = unqouteStr(parts[0]); jsDocTag.default = parseJsDocValue(parts[1]); } else if (parts.length !== 0) { // No default was given jsDocTag.name = unqouteStr(parts[0]); } } else { // else, match required name // Example: " myname" // A name is needed some jsdoc tags making it possible to include omit "-" // Therefore we don't look for "-" or line end if the name is required - in that case we only need to eat the first word to find the name. const regex = JSDOC_TAGS_WITH_REQUIRED_NAME.includes(jsDocTag.tag) ? /^(\s*(\S+))/ : /^(\s*(\S+))((\s*-[\s\S]+)|\s*)($|[\r\n])/; const nameResult = str.match(regex); if (nameResult != null) { // Move string to end of match // Example: " myname|" moveStr(nameResult[1]); jsDocTag.name = unqouteStr(nameResult[2].trim()); } } }; const matchComment = () => { // Match comment if (str.length > 0) { // The rest of the string is parsed as comment. Remove "-" if needed. jsDocTag.description = str.replace(/^\s*-\s*/, "").trim() || undefined; } // Expand the name based on namespace and classname if (jsDocTag.name != null) { /** * The name could look like this, so we need to parse and the remove the class name and namespace from the name * InputSwitch#[CustomEvent]input-switch-check-changed * InputSwitch#input-switch-check-changed */ const match = jsDocTag.name.match(/(.*)#(\[.*\])?(.*)/); if (match != null) { jsDocTag.className = match[1]; jsDocTag.namespace = match[2]; jsDocTag.name = match[3]; } } }; matchTag(); matchType(); matchName(); // Type can come both before and after "name" if (jsDocTag.type == null) { matchType(); } matchComment(); return jsDocTag; } /** * Parses an entire jsdoc string * @param doc */ function parseJsDocString(doc) { // Prepare lines const lines = doc.split("\n").map(line => line.trim()); let description = ""; let readDescription = true; let currentTag = ""; const tags = []; /** * Parsing will add to "currentTag" and commit it when necessary */ const commitCurrentTag = () => { if (currentTag.length > 0) { const tagToCommit = currentTag; const tagMatch = tagToCommit.match(/^@(\S+)\s*/); if (tagMatch != null) { tags.push({ parsed: lazy(() => parseJsDocTagString(tagToCommit)), node: undefined, tag: tagMatch[1], comment: tagToCommit.substr(tagMatch[0].length) }); } currentTag = ""; } }; // Parse all lines one by one for (const line of lines) { // Don't parse the last line ("*/") if (line.match(/\*\//)) { continue; } // Match a line like: "* @mytag description" const tagCommentMatch = line.match(/(^\s*\*\s*)@\s*/); if (tagCommentMatch != null) { // Commit current tag (if any has been read). Now "currentTag" will reset. commitCurrentTag(); // Add everything on the line from "@" currentTag += line.substr(tagCommentMatch[1].length); // We hit a jsdoc tag, so don't read description anymore readDescription = false; } else if (!readDescription) { // If we are not reading the description, we are currently reading a multiline tag const commentMatch = line.match(/^\s*\*\s*/); if (commentMatch != null) { currentTag += "\n" + line.substr(commentMatch[0].length); } } else { // Read everything after "*" into the description if we are currently reading the description // If we are on the first line, add everything after "/*" const startLineMatch = line.match(/^\s*\/\*\*/); if (startLineMatch != null) { description += line.substr(startLineMatch[0].length); } // Add everything after "*" into the current description const commentMatch = line.match(/^\s*\*\s*/); if (commentMatch != null) { if (description.length > 0) { description += "\n"; } description += line.substr(commentMatch[0].length); } } } // Commit a tag if we were currently parsing one commitCurrentTag(); if (description.length === 0 && tags.length === 0) { return undefined; } return { description: unescapeJSDoc(description), tags }; } /** * Certain characters as "@" can be escaped in order to prevent Typescript from * parsing it as a jsdoc tag. This function unescapes these characters. * @param str */ function unescapeJSDoc(str) { return str.replace(/\\@/, "@"); } const EVENT_NAMES = [ "Event", "CustomEvent", "AnimationEvent", "ClipboardEvent", "DragEvent", "FocusEvent", "HashChangeEvent", "InputEvent", "KeyboardEvent", "MouseEvent", "PageTransitionEvent", "PopStateEvent", "ProgressEvent", "StorageEvent", "TouchEvent", "TransitionEvent", "UiEvent", "WheelEvent" ]; /** * Discovers events dispatched * @param node * @param context */ function discoverEvents(node, context) { var _a; const { ts, checker } = context; // new CustomEvent("my-event"); if (ts.isNewExpression(node)) { const { expression, arguments: args } = node; if (EVENT_NAMES.includes(expression.getText()) && args && args.length >= 1) { const arg = args[0]; const eventName = (_a = resolveNodeValue(arg, { ...context, strict: true })) === null || _a === void 0 ? void 0 : _a.value; if (typeof eventName === "string") { // Either grab jsdoc from the new expression or from a possible call expression that its wrapped in const jsDoc = getJsDoc(expression, ts) || (ts.isCallLikeExpression(node.parent) && getJsDoc(node.parent.parent, ts)) || (ts.isExpressionStatement(node.parent) && getJsDoc(node.parent, ts)) || undefined; return [ { jsDoc, name: eventName, node, type: lazy(() => checker.getTypeAtLocation(node)) } ]; } } } return undefined; } /** * Discovers global feature defined on "HTMLElementEventMap" or "HTMLElement" */ const discoverGlobalFeatures$3 = { event: (node, context) => { var _a, _b; const { ts, checker } = context; if (context.ts.isInterfaceDeclaration(node) && ["HTMLElementEventMap", "GlobalEventHandlersEventMap"].includes(node.name.text)) { const events = []; for (const member of node.members) { if (ts.isPropertySignature(member)) { const name = (_a = resolveNodeValue(member.name, context)) === null || _a === void 0 ? void 0 : _a.value; if (name != null && typeof name === "string") { events.push({ node: member, jsDoc: getJsDoc(member, ts), name: name, type: lazy(() => checker.getTypeAtLocation(member)) }); } } } (_b = context === null || context === void 0 ? void 0 : context.emitContinue) === null || _b === void 0 ? void 0 : _b.call(context); return events; } }, member: (node, context) => { var _a, _b; const { ts } = context; if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") { const members = []; for (const member of node.members) { if (ts.isPropertySignature(member)) { const name = (_a = resolveNodeValue(member.name, context)) === null || _a === void 0 ? void 0 : _a.value; if (name != null && typeof name === "string") { members.push({ priority: "medium", node: member, jsDoc: getJsDoc(member, ts), kind: "property", propName: name, type: lazy(() => context.checker.getTypeAtLocation(member)) }); } } } (_b = context === null || context === void 0 ? void 0 : context.emitContinue) === null || _b === void 0 ? void 0 : _b.call(context); return members; } } }; /** * Discovers inheritance from a node by looking at "extends" and "implements" * @param node * @param baseContext */ function discoverInheritance$1(node, baseContext) { let declarationKind = undefined; const heritageClauses = []; const declarationNodes = new Set(); const context = { ...baseContext, emitDeclaration: decl => declarationNodes.add(decl), emitInheritance: (kind, identifier) => heritageClauses.push({ kind, identifier, declaration: undefined }), emitDeclarationKind: kind => (declarationKind = declarationKind || kind), visitedNodes: new Set() }; // Resolve the structure of the node resolveStructure(node, context); // Reverse heritage clauses because they come out in wrong order heritageClauses.reverse(); return { declarationNodes: Array.from(declarationNodes), heritageClauses, declarationKind }; } function resolveStructure(node, context) { const { ts } = context; if (context.visitedNodes.has(node)) { return; } context.visitedNodes.add(node); // Call this function recursively if this node is an identifier if (ts.isIdentifier(node)) { for (const decl of resolveDeclarationsDeep(node, context)) { resolveStructure(decl, context); } } // Emit declaration node if we've found a class of interface else if (ts.isClassLike(node) || ts.isInterfaceDeclaration(node)) { context.emitDeclarationKind(ts.isClassLike(node) ? "class" : "interface"); context.emitDeclaration(node); // Resolve inheritance for (const heritage of node.heritageClauses || []) { for (const type of heritage.types || []) { resolveHeritage(heritage, type.expression, context); } } } // Emit a declaration node if this node is a type literal else if (ts.isTypeLiteralNode(node) || ts.isObjectLiteralExpression(node)) { context.emitDeclarationKind("interface"); context.emitDeclaration(node); } // Emit a mixin if this node is a function else if (ts.isFunctionLike(node) || ts.isCallLikeExpression(node)) { context.emitDeclarationKind("mixin"); if (ts.isFunctionLike(node) && node.getSourceFile().isDeclarationFile) { // Find any identifiers if the node is in a declaration file findChildren(node.type, ts.isIdentifier, identifier => { resolveStructure(identifier, context); }); } else { // Else find the first class declaration in the block // Note that we don't look for a return statement because this would complicate things const clzDecl = findChild(node, ts.isClassLike); if (clzDecl != null) { resolveStructure(clzDecl, context); return; } // If we didn't find any class declarations, we might be in a function that wraps a mixin // Therefore find the return statement and call this method recursively const returnNode = findChild(node, ts.isReturnStatement); if (returnNode != null && returnNode.expression != null && returnNode.expression !== node) { const returnNodeExp = returnNode.expression; // If a function call is returned, this function call expression is followed, and the arguments are treated as heritage // Example: return MyFirstMixin(MySecondMixin(Base)) --> MyFirstMixin is followed, and MySecondMixin + Base are inherited if (ts.isCallExpression(returnNodeExp) && returnNodeExp.expression != null) { for (const arg of returnNodeExp.arguments) { resolveHeritage(undefined, arg, context); } resolveStructure(returnNodeExp.expression, context); } return; } } } else if (ts.isVariableDeclaration(node) && (node.initializer != null || node.type != null)) { resolveStructure((node.initializer || node.type), context); } else if (ts.isIntersectionTypeNode(node)) { emitTypeLiteralsDeclarations(node, context); } } function resolveHeritage(heritage, node, context) { const { ts } = context; /** * Parse mixins */ if (ts.isCallExpression(node)) { // Mixins const { expression: identifier, arguments: args } = node; // Extend classes given to the mixin // Example: class MyElement extends MyMixin(MyBase) --> MyBase // Example: class MyElement extends MyMixin(MyBase1, MyBase2) --> MyBase1, MyBase2 for (const arg of args) { resolveHeritage(heritage, arg, context); } // Resolve and traverse the mixin function // Example: class MyElement extends MyMixin(MyBase) --> MyMixin if (identifier != null && ts.isIdentifier(identifier)) { resolveHeritage("mixin", identifier, context); } } else if (ts.isIdentifier(node)) { // Try to handle situation like this, by resolving the variable in between // const Base = ExtraMixin(base); // class MixinClass extends Base { } let dontEmitHeritageClause = false; // Resolve the declaration of this identifier const declarations = resolveDeclarationsDeep(node, context); for (const decl of declarations) { // If the resolved declaration is a variable declaration assigned to a function, try to follow the assignments. // Example: const MyBase = MyMixin(Base); return class extends MyBase { ... } if (context.ts.isVariableDeclaration(decl) && decl.initializer != null) { if (context.ts.isCallExpression(decl.initializer)) { let hasDeclaration = false; resolveStructure(decl, { ...context, emitInheritance: () => { }, emitDeclarationKind: () => { }, emitDeclaration: () => { hasDeclaration = true;