UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

467 lines 22 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mermaidHide = void 0; exports.getTypeScriptSourceFiles = getTypeScriptSourceFiles; exports.dropGenericsFromTypeName = dropGenericsFromTypeName; exports.removeCommentSymbolsFromTypeScriptComment = removeCommentSymbolsFromTypeScriptComment; exports.getTextualCommentsFromTypeScript = getTextualCommentsFromTypeScript; exports.getStartLineOfTypeScriptNode = getStartLineOfTypeScriptNode; exports.getType = getType; exports.followTypeReference = followTypeReference; exports.getTypePathForTypeScript = getTypePathForTypeScript; exports.getTypePathLink = getTypePathLink; exports.getTypesFromFolder = getTypesFromFolder; exports.implSnippet = implSnippet; exports.printHierarchy = printHierarchy; exports.printCodeOfElement = printCodeOfElement; exports.shortLink = shortLink; exports.shortLinkFile = shortLinkFile; exports.getDocumentationForType = getDocumentationForType; const typescript_1 = __importDefault(require("typescript")); const assert_1 = require("../../util/assert"); const doc_files_1 = require("./doc-files"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const mermaid_1 = require("../../util/mermaid/mermaid"); const doc_code_1 = require("./doc-code"); const doc_structure_1 = require("./doc-structure"); const html_hover_over_1 = require("../../util/html-hover-over"); const doc_general_1 = require("./doc-general"); function getTypeScriptSourceFiles(fileNames) { try { const program = typescript_1.default.createProgram(fileNames, { target: typescript_1.default.ScriptTarget.ESNext, skipLibCheck: true, skipDefaultLibCheck: true, allowJs: true, checkJs: false, strictNullChecks: false, noUncheckedIndexedAccess: false, noUncheckedSideEffectImports: false, noCheck: true }); return { program, files: fileNames.map(fileName => program.getSourceFile(fileName)).filter(file => !!file) }; } catch (err) { console.error('Failed to get source files', err); return { files: [], program: undefined }; } } function dropGenericsFromTypeName(type) { let previous; do { previous = type; type = type.replace(/<.*>/g, ''); } while (type !== previous); return type; } function removeCommentSymbolsFromTypeScriptComment(comment) { return comment // remove '/** \n * \n */... .replace(/^\/\*\*?/gm, '').replace(/^\s*\*\s*/gm, '').replace(/\*\/$/gm, '').replace(/^\s*\*/gm, '') /* replace {@key foo|bar} with `bar` and {@key foo} with `foo` */ .replace(/\{@[a-zA-Z]+ ([^}]+\|)?(?<name>[^}]+)}/gm, '<code>$<name></code>') .trim(); } function getTextualCommentsFromTypeScript(node) { const comments = typescript_1.default.getJSDocCommentsAndTags(node); const out = []; for (const { comment } of comments) { if (typeof comment === 'string') { out.push(removeCommentSymbolsFromTypeScriptComment(comment)); } else if (comment !== undefined) { for (const c of comment) { out.push(removeCommentSymbolsFromTypeScriptComment(c.getText(c.getSourceFile()))); } } } return out; } function getStartLineOfTypeScriptNode(node, sourceFile) { const lineStart = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line; return lineStart + 1; } function getType(node, typeChecker) { const tryDirect = typeChecker.getTypeAtLocation(node); return tryDirect ? typeChecker.typeToString(tryDirect) : 'unknown'; } const defaultSkip = ['Pick', 'Partial', 'Required', 'Readonly', 'Omit', 'DeepPartial', 'DeepReadonly', 'DeepWritable', 'StrictOmit']; function followTypeReference(type, sourceFile) { const node = type.typeName; if (typescript_1.default.isQualifiedName(node)) { return [node.right.getText(sourceFile) ?? '']; } const args = type.typeArguments?.map(arg => arg.getText(sourceFile)) ?? []; const nodeLexeme = node.getText(sourceFile) ?? ''; const baseLexeme = type.getText(sourceFile) ?? ''; if (defaultSkip.map(s => nodeLexeme.startsWith(s))) { return [baseLexeme, ...args]; } return [nodeLexeme, baseLexeme, ...args]; } function collectHierarchyInformation(sourceFiles, options) { const hierarchyList = []; const typeChecker = options.program.getTypeChecker(); const visit = (node, sourceFile) => { if (!node) { return; } if (typescript_1.default.isInterfaceDeclaration(node)) { const interfaceName = node.name?.getText(sourceFile) ?? ''; const baseTypes = node.heritageClauses?.flatMap(clause => clause.types .map(type => type.getText(sourceFile) ?? '') .map(dropGenericsFromTypeName)) ?? []; const generics = node.typeParameters?.map(param => param.getText(sourceFile) ?? '') || []; hierarchyList.push({ name: dropGenericsFromTypeName(interfaceName), node, kind: 'interface', extends: baseTypes, generics, comments: getTextualCommentsFromTypeScript(node), filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), properties: node.members.map(member => { const name = member.name?.getText(sourceFile) ?? ''; return `${name}${(0, mermaid_1.escapeMarkdown)(': ' + getType(member, typeChecker))}`; }), }); } else if (typescript_1.default.isTypeAliasDeclaration(node)) { const typeName = node.name?.getText(sourceFile) ?? ''; let baseTypes = []; if (typescript_1.default.isIntersectionTypeNode(node.type) || typescript_1.default.isUnionTypeNode(node.type)) { baseTypes = node.type.types .filter(typeNode => typescript_1.default.isTypeReferenceNode(typeNode)) .flatMap(typeName => followTypeReference(typeName, sourceFile)) .map(dropGenericsFromTypeName); } else if (typescript_1.default.isTypeReferenceNode(node.type)) { baseTypes = [...followTypeReference(node.type, sourceFile)].map(dropGenericsFromTypeName); } const generics = node.typeParameters?.map(param => param.getText(sourceFile) ?? '') ?? []; hierarchyList.push({ name: dropGenericsFromTypeName(typeName), node, kind: 'type', extends: baseTypes, comments: getTextualCommentsFromTypeScript(node), generics, filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), }); } else if (typescript_1.default.isEnumDeclaration(node)) { const enumName = node.name?.getText(sourceFile) ?? ''; hierarchyList.push({ name: dropGenericsFromTypeName(enumName), node, kind: 'enum', extends: [], comments: getTextualCommentsFromTypeScript(node), generics: [], filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), properties: node.members.map(member => { const name = member.name?.getText(sourceFile) ?? ''; return `${name}${(0, mermaid_1.escapeMarkdown)(': ' + getType(member, typeChecker))}`; }) }); } else if (typescript_1.default.isEnumMember(node)) { const typeName = node.parent.name?.getText(sourceFile) ?? ''; const enumName = dropGenericsFromTypeName(typeName); hierarchyList.push({ name: dropGenericsFromTypeName(node.name.getText(sourceFile)), node, kind: 'enum', extends: [enumName], comments: getTextualCommentsFromTypeScript(node), generics: [], filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), }); } else if (typescript_1.default.isClassDeclaration(node)) { const className = node.name?.getText(sourceFile) ?? ''; const baseTypes = node.heritageClauses?.flatMap(clause => clause.types .map(type => type.getText(sourceFile) ?? '') .map(dropGenericsFromTypeName)) ?? []; const generics = node.typeParameters?.map(param => param.getText(sourceFile) ?? '') ?? []; hierarchyList.push({ name: dropGenericsFromTypeName(className), node, kind: 'class', extends: baseTypes, comments: getTextualCommentsFromTypeScript(node), generics, filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), properties: node.members.map(member => { const name = member.name?.getText(sourceFile) ?? ''; return `${name}${(0, mermaid_1.escapeMarkdown)(': ' + getType(member, typeChecker))}`; }), }); } else if (typescript_1.default.isVariableDeclaration(node) || typescript_1.default.isExportDeclaration(node) || typescript_1.default.isExportAssignment(node) || typescript_1.default.isDeclarationStatement(node)) { const name = node.name?.getText(sourceFile) ?? ''; const comments = getTextualCommentsFromTypeScript(node); hierarchyList.push({ name: dropGenericsFromTypeName(name), node, kind: 'variable', extends: [], comments, generics: [], filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), }); } else if (typescript_1.default.isPropertyAssignment(node) || typescript_1.default.isPropertyDeclaration(node) || typescript_1.default.isPropertySignature(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isMethodSignature(node) || typescript_1.default.isFunctionDeclaration(node)) { const name = node.name?.getText(sourceFile) ?? ''; // get the name of the object/enclosing type let parent = node.parent; while (typeof parent === 'object' && parent !== undefined && !('name' in parent)) { parent = parent.parent; } if (typeof parent === 'object' && 'name' in parent) { const comments = getTextualCommentsFromTypeScript(node); hierarchyList.push({ name: dropGenericsFromTypeName(name), node, kind: 'variable', extends: [parent.name?.getText(sourceFile) ?? ''], comments, generics: [], filePath: sourceFile.fileName, lineNumber: getStartLineOfTypeScriptNode(node, sourceFile), }); } } typescript_1.default.forEachChild(node, child => visit(child, sourceFile)); }; sourceFiles.forEach(sourceFile => { visit(sourceFile, sourceFile); }); return hierarchyList; } function getTypePathForTypeScript({ filePath }) { return filePath.replace(/^.*\/src\//, 'src/').replace(/^.*\/test\//, 'test/'); } function getTypePathLink(elem, prefix = doc_files_1.RemoteFlowrFilePathBaseRef) { const fromSource = getTypePathForTypeScript(elem); return `${prefix}/${fromSource}#L${elem.lineNumber}`; } function generateMermaidClassDiagram(hierarchyList, rootName, options, visited = new Set()) { const collect = { nodeLines: [], edgeLines: [] }; if (visited.has(rootName)) { return collect; } // Prevent circular references visited.add(rootName); const node = hierarchyList.find(h => h.name === rootName); if (!node) { return collect; } const genericPart = node.generics.length > 0 ? `~${node.generics.join(', ')}~` : ''; collect.nodeLines.push(`class ${node.name}${genericPart}`); collect.nodeLines.push(` <<${node.kind}>> ${node.name}`); if (node.kind === 'type') { collect.nodeLines.push(`style ${node.name} opacity:.35,fill:#FAFAFA`); } const writtenProperties = new Set(); if (node.properties) { for (const property of node.properties) { collect.nodeLines.push(` ${node.name} : ${property}`); writtenProperties.add(property); } } collect.nodeLines.push(`click ${node.name} href "${getTypePathLink(node)}" "${(0, mermaid_1.escapeMarkdown)(node.comments?.join('; ').replace(/\n/g, ' ') ?? '')}"`); const inline = [...options.inlineTypes ?? [], ...defaultSkip]; if (node.extends.length > 0) { for (const baseType of node.extends) { if (inline.includes(baseType)) { const info = hierarchyList.find(h => h.name === baseType); for (const property of info?.properties ?? []) { if (!writtenProperties.has(property)) { collect.nodeLines.push(` ${node.name} : ${property} [from ${baseType}]`); writtenProperties.add(property); } } } else { if (node.kind === 'type' || hierarchyList.find(h => h.name === baseType)?.kind === 'type') { collect.edgeLines.push(`${dropGenericsFromTypeName(baseType)} .. ${node.name}`); } else { collect.edgeLines.push(`${dropGenericsFromTypeName(baseType)} <|-- ${node.name}`); } const { nodeLines, edgeLines } = generateMermaidClassDiagram(hierarchyList, baseType, options, visited); collect.nodeLines.push(...nodeLines); collect.edgeLines.push(...edgeLines); } } } return collect; } function visualizeMermaidClassDiagram(hierarchyList, options) { if (!options.typeNameForMermaid) { return undefined; } const { nodeLines, edgeLines } = generateMermaidClassDiagram(hierarchyList, options.typeNameForMermaid, options); return nodeLines.length === 0 && edgeLines.length === 0 ? '' : ` classDiagram direction RL ${nodeLines.join('\n')} ${edgeLines.join('\n')} `; } function getTypesFromFileAsMermaid(fileNames, options) { const { files, program } = getTypeScriptSourceFiles(fileNames); (0, assert_1.guard)(files.length > 0, () => `No source files found for ${JSON.stringify(fileNames)}`); const withProgram = { ...options, program }; const hierarchyList = collectHierarchyInformation(files, withProgram); return { mermaid: visualizeMermaidClassDiagram(hierarchyList, withProgram), info: hierarchyList, program }; } /** * Inspect typescript source code for types and return a report. */ function getTypesFromFolder(options) { (0, assert_1.guard)(options.rootFolder !== undefined || options.files !== undefined, 'Either rootFolder or files must be provided'); const files = [...options.files ?? []]; if (options.rootFolder) { for (const fileBuff of fs_1.default.readdirSync(options.rootFolder, { recursive: true })) { const file = fileBuff.toString(); if (file.endsWith('.ts')) { files.push(path_1.default.join(options.rootFolder, file)); } } } return getTypesFromFileAsMermaid(files, options); } function implSnippet(node, program, showName = true, nesting = 0, open = false) { (0, assert_1.guard)(node !== undefined, 'Node must be defined => invalid change of type name?'); const indent = ' '.repeat(nesting * 2); const bold = node.kind === 'interface' || node.kind === 'enum' ? '**' : ''; const sep = node.comments ? ' \n' : '\n'; let text = node.comments?.join('\n') ?? ''; if (text.trim() !== '') { text = ' ' + text; } const code = node.node.getFullText(program.getSourceFile(node.node.getSourceFile().fileName)); text += `\n<details${open ? ' open' : ''}><summary style="color:gray">Defined at <a href="${getTypePathLink(node)}">${getTypePathLink(node, '.')}</a></summary>\n\n${(0, doc_code_1.codeBlock)('ts', code)}\n\n</details>\n`; const init = showName ? `* ${bold}[${node.name}](${getTypePathLink(node)})${bold} ${sep}${indent}` : ''; return ` ${indent}${showName ? init : ''} ${text.replaceAll('\t', ' ').split(/\n/g).join(`\n${indent} `)}`; } exports.mermaidHide = ['Leaf', 'Location', 'Namespace', 'Base', 'WithChildren', 'Partial', 'RAccessBase']; function printHierarchy({ program, info, root, collapseFromNesting = 1, initialNesting = 0, maxDepth = 20, openTop }) { if (initialNesting > maxDepth) { return ''; } const node = info.find(e => e.name === root); if (!node) { return ''; } const thisLine = implSnippet(node, program, true, initialNesting, initialNesting === 0 && openTop); const result = []; for (const baseType of node.extends) { if (exports.mermaidHide.includes(baseType)) { continue; } const res = printHierarchy({ program, info: info, root: baseType, collapseFromNesting, initialNesting: initialNesting + 1, maxDepth }); result.push(res); } const out = result.join('\n'); if (initialNesting === collapseFromNesting - 1) { return thisLine + (out ? (0, doc_structure_1.details)(`View more (${node.extends.join(', ')})`, out, { prefixInit: ' '.repeat(2 * (collapseFromNesting + 1)) }) : ''); } else { return thisLine + (out ? '\n' + out : ''); } } function printCodeOfElement({ program, info }, name) { const node = info.find(e => e.name === name); if (!node) { console.error(`Could not find node ${name} when resolving function!`); return ''; } const code = node.node.getFullText(program.getSourceFile(node.node.getSourceFile().fileName)); return `${(0, doc_code_1.codeBlock)('ts', code)}\n<i>Defined at <a href="${getTypePathLink(node)}">${getTypePathLink(node, '.')}</a></i>\n`; } function fuzzyCompare(a, b) { const aStr = a.toLowerCase().replace(/[^a-z0-9]/g, '-').trim(); const bStr = b.toLowerCase().replace(/[^a-z0-9]/g, '-').trim(); return aStr === bStr || aStr.includes(bStr) || bStr.includes(aStr); } function retrieveNode(name, hierarchy, fuzzy = false, type = undefined) { let container = undefined; if (name.includes('::')) { [container, name] = name.split(/:::?/); } let node = hierarchy.filter(e => fuzzy ? fuzzyCompare(e.name, name) : e.name === name); if (node.length === 0) { return undefined; } else if (container) { node = node.filter(n => fuzzy ? n.extends.some(n => fuzzyCompare(n, container)) : n.extends.includes(container)); if (node.length === 0) { return undefined; } } if (type) { node = node.filter(n => n.kind === type); if (node.length === 0) { return undefined; } } return [container, name, node[0]]; } /** * Create a short link to a type in the documentation * @param name - The name of the type, e.g. `MyType`, may include a container, e.g.,`MyContainer::MyType` (this works with function nestings too) * Use `:::` if you want to access a scoped function, but the name should be displayed without the scope * @param hierarchy - The hierarchy of types to search in * @param codeStyle - Whether to use code style for the link * @param realNameWrapper - How to highlight the function in name in the `x::y` format? */ function shortLink(name, hierarchy, codeStyle = true, realNameWrapper = 'b') { const res = retrieveNode(name, hierarchy); if (!res) { console.error(`Could not find node ${name} when resolving short link!`); return ''; } const [, mainName, node] = res; let pkg = res[0]; if (name.includes(':::')) { pkg = undefined; } const comments = node.comments?.join('\n').replace(/\\?\n|```[a-zA-Z]*|\s\s*/g, ' ').replace(/<\/?code>|`/g, '').replace(/<\/?p\/?>/g, ' ').replace(/"/g, '\'') ?? ''; return `<a href="${getTypePathLink(node)}">${codeStyle ? '<code>' : ''}${(node.comments?.length ?? 0) > 0 ? (0, html_hover_over_1.textWithTooltip)(pkg ? `${pkg}::<${realNameWrapper}>${mainName}</${realNameWrapper}>` : mainName, comments.length > 400 ? comments.slice(0, 400) + '...' : comments) : node.name}${codeStyle ? '</code>' : ''}</a>`; } function shortLinkFile(name, hierarchy) { const res = retrieveNode(name, hierarchy); if (!res) { console.error(`Could not find node ${name} when resolving short link!`); return ''; } const [, , node] = res; return `<a href="${getTypePathLink(node)}">${getTypePathForTypeScript(node)}</a>`; } function getDocumentationForType(name, hierarchy, prefix = '', filter) { const res = retrieveNode(name, hierarchy, filter?.fuzzy, filter?.type); if (!res) { return ''; } const [, , node] = res; return (0, doc_general_1.prefixLines)(node.comments?.join('\n') ?? '', prefix); } //# sourceMappingURL=doc-types.js.map