@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
467 lines • 22 kB
JavaScript
;
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