UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

449 lines 17.1 kB
"use strict"; /** * This module has one goal (and is to be rewritten soon to achieve that goal, * as the file itself is way too long). See {@link reconstructToCode}. * @module */ Object.defineProperty(exports, "__esModule", { value: true }); exports.reconstructLogger = void 0; exports.reconstructToCode = reconstructToCode; const log_1 = require("../util/log"); const assert_1 = require("../util/assert"); const type_1 = require("../r-bridge/lang-4.x/ast/model/type"); const r_function_call_1 = require("../r-bridge/lang-4.x/ast/model/nodes/r-function-call"); const stateful_fold_1 = require("../r-bridge/lang-4.x/ast/model/processing/stateful-fold"); const auto_select_defaults_1 = require("./auto-select/auto-select-defaults"); function plain(text) { return [{ line: text, indent: 0 }]; } exports.reconstructLogger = log_1.log.getSubLogger({ name: 'reconstruct' }); function getLexeme(n) { return n.info.fullLexeme ?? n.lexeme ?? ''; } function reconstructAsLeaf(leaf, configuration) { const selectionHasLeaf = configuration.selection.has(leaf.info.id) || configuration.autoSelectIf(leaf, configuration.fullAst); return selectionHasLeaf ? foldToConst(leaf) : []; } function foldToConst(n) { return plain(getLexeme(n)); } function indentBy(lines, indent) { return lines.map(({ line, indent: i }) => ({ line, indent: i + indent })); } function reconstructExpressionList(exprList, _grouping, expressions, config) { const subExpressions = expressions.filter(e => e.length > 0); if (subExpressions.length === 0) { if (isSelected(config, exprList)) { return plain('{}'); } else { return []; } } else if (subExpressions.length === 1) { if (!isSelected(config, exprList)) { return subExpressions[0]; } const [fst] = subExpressions; const g = exprList.grouping; if (g && fst.length > 0) { const start = g[0].content; const end = g[1].content; fst[0].line = `${start}${start === '{' ? ' ' : ''}${fst[0].line}`; fst[fst.length - 1].line = `${fst[fst.length - 1].line}${end === '}' ? ' ' : ''}${end}`; } return fst; } else { const g = exprList.grouping; return [ ...(g ? plain(g[0].content) : plain('{')), ...indentBy(subExpressions.flat(), 1), ...(g ? plain(g[1].content) : plain('}')) ]; } } function isSelected(configuration, n) { return configuration.selection.has(n.info.id) || configuration.autoSelectIf(n, configuration.fullAst); } function reconstructRawBinaryOperator(lhs, n, rhs) { return [ ...lhs.slice(0, lhs.length - 1), { line: `${lhs[lhs.length - 1].line} ${n} ${rhs[0].line}`, indent: 0 }, ...indentBy(rhs.slice(1, rhs.length), 1) ]; } function reconstructUnaryOp(leaf, operand, configuration) { if (configuration.selection.has(leaf.info.id)) { return foldToConst(leaf); } else if (operand.length === 0) { return []; } else { return foldToConst(leaf); } } function reconstructBinaryOp(n, lhs, rhs, config) { if (lhs.length === 0 && rhs.length === 0) { if (isSelected(config, n)) { return plain(getLexeme(n)); } else { return []; } } else if (lhs.length === 0) { // if we have no lhs, only return rhs return rhs; } else if (rhs.length === 0) { if (isSelected(config, n)) { return plain(getLexeme(n)); } else { return lhs; } } return reconstructRawBinaryOperator(lhs, n.type === type_1.RType.Pipe ? '|>' : n.operator, rhs); } function reconstructForLoop(loop, variable, vector, body, config) { if (!isSelected(config, loop) && variable.length === 0 && vector.length === 0) { return body; } else if (body.length === 0 && variable.length === 0 && vector.length === 0) { return []; } else if (body.length <= 1) { // 'inline' return [{ line: `for(${getLexeme(loop.variable)} in ${getLexeme(loop.vector)}) ${body.length === 0 ? '{}' : body[0].line}`, indent: 0 }]; } else if (body[0].line === '{' && body[body.length - 1].line === '}') { // 'block' return [ { line: `for(${getLexeme(loop.variable)} in ${getLexeme(loop.vector)}) {`, indent: 0 }, ...body.slice(1, body.length - 1), { line: '}', indent: 0 } ]; } else { // unknown return [ { line: `for(${getLexeme(loop.variable)} in ${getLexeme(loop.vector)})`, indent: 0 }, ...indentBy(body, 1) ]; } } function reconstructBodyWithHeader(header, body, onEmpty) { if (body.length === 0) { return [{ line: `${header.line}${onEmpty}`, indent: header.indent }]; } else if (body.length === 1) { return [ { line: `${header.line} ${body[0].line}`, indent: header.indent } ]; } else if (body[0].line === '{' && body[body.length - 1].line === '}') { return [ { line: `${header.line} {`, indent: header.indent }, ...body.slice(1, body.length - 1), { line: '}', indent: header.indent } ]; } else { return [ header, ...indentBy(body, 1) ]; } } function reconstructRepeatLoop(loop, body, configuration) { const sel = isSelected(configuration, loop); if (!sel) { return body; } return reconstructBodyWithHeader({ line: 'repeat', indent: 0 }, body, '{}'); } function reconstructIfThenElse(ifThenElse, condition, then, otherwise, config) { otherwise ??= []; if (then.length === 0 && otherwise.length === 0) { if (isSelected(config, ifThenElse)) { return [{ line: `if(${getLexeme(ifThenElse.condition)}) { }`, indent: 0 }]; } else if (condition.length > 0) { return condition; } else { return []; } } else if (otherwise.length === 0) { if (isSelected(config, ifThenElse)) { return reconstructBodyWithHeader({ line: `if(${getLexeme(ifThenElse.condition)})`, indent: 0 }, then, '{}'); } else { return then; } } else if (then.length === 0) { if (isSelected(config, ifThenElse)) { return reconstructBodyWithHeader({ line: `if(${getLexeme(ifThenElse.condition)}) { } else`, indent: 0 }, then, '{}'); } else { return otherwise; } } else { const thenRemainder = indentBy(then.slice(1), 1); if (thenRemainder.length > 0) { if (!thenRemainder[thenRemainder.length - 1].line.trim().endsWith('else')) { thenRemainder[thenRemainder.length - 1].line += ' else '; } } return [ { line: `if(${getLexeme(ifThenElse.condition)}) ${then[0].line} ${then.length === 1 ? 'else' : ''}`, indent: 0 }, ...thenRemainder, { line: `${otherwise[0].line}`, indent: 0 }, ...indentBy(otherwise.splice(1), 1) ]; } } function reconstructWhileLoop(loop, condition, body, configuration) { const sel = isSelected(configuration, loop); if (!sel && condition.length === 0) { return body; } else if (body.length === 0 && condition.length === 0) { return []; } else if (body.length <= 1) { // 'inline' return [{ line: `while(${getLexeme(loop.condition)}) ${body.length === 0 ? '{}' : body[0].line}`, indent: 0 }]; } else if (body[0].line === '{' && body[body.length - 1].line === '}') { // 'block' return [ { line: `while(${getLexeme(loop.condition)}) {`, indent: 0 }, ...body.slice(1, body.length - 1), { line: '}', indent: 0 } ]; } else { // unknown return [ { line: `while(${getLexeme(loop.condition)})`, indent: 0 }, ...indentBy(body, 1) ]; } } function reconstructParameters(parameters) { // const baseParameters = parameters.flatMap(p => plain(getLexeme(p))) return parameters.map(p => { if (p.defaultValue !== undefined) { return `${getLexeme(p.name)}=${getLexeme(p.defaultValue)}`; } else { return getLexeme(p); } }); } function isNotEmptyArgument(a) { return a !== r_function_call_1.EmptyArgument; } function reconstructFoldAccess(node, accessed, access) { if (accessed.length === 0) { return access.filter(isNotEmptyArgument).flat(); } else if (access.every(a => a === r_function_call_1.EmptyArgument || a.length === 0)) { return accessed; } return plain(getLexeme(node)); } function reconstructArgument(argument, name, value) { if (argument.name !== undefined && name !== undefined && name.length > 0) { return plain(`${getLexeme(argument.name)}=${argument.value ? getLexeme(argument.value) : ''}`); } else { return value ?? []; } } function reconstructParameter(parameter, name, defaultValue, configuration) { if (isSelected(configuration, parameter)) { return plain(getLexeme(parameter)); } if (parameter.defaultValue !== undefined && name.length > 0) { return plain(`${getLexeme(parameter.name)}=${getLexeme(parameter.defaultValue)}`); } else if (parameter.defaultValue !== undefined && name.length === 0) { return defaultValue ?? []; } else { return name; } } function reconstructFunctionDefinition(definition, functionParameters, body, config) { // if a definition is not selected, we only use the body - slicing will always select the definition if it is required if (functionParameters.every(p => p.length === 0)) { const empty = body === undefined || body.length === 0; const selected = isSelected(config, definition); if (empty && selected) { // give function stub return plain(`${definition.lexeme}(${reconstructParameters(definition.parameters).join(', ')}) { }`); } else if (!selected) { // do not require function return body; } } const parameters = reconstructParameters(definition.parameters).join(', '); if (body.length <= 1) { // 'inline' const bodyStr = body.length === 0 ? '{ }' : `${body[0].line}`; // we keep the braces in every case because I do not like no-brace functions return [{ line: `${definition.lexeme}(${parameters}) ${bodyStr}`, indent: 0 }]; } else { // 'block' return [ { line: `${definition.lexeme}(${parameters}) ${body[0].line}`, indent: 0 }, ...body.slice(1), ]; } } function reconstructSpecialInfixFunctionCall(args, call) { (0, assert_1.guard)(args.length === 2, () => `infix special call must have exactly two arguments, got: ${args.length} (${JSON.stringify(args)})`); (0, assert_1.guard)(call.named, `infix special call must be named, got: ${call.named}`); const [lhs, rhs] = args; if ((lhs === undefined || lhs.length === 0) && (rhs === undefined || rhs.length === 0)) { return []; } // else if (rhs === undefined || rhs.length === 0) { // if rhs is undefined we still have to keep both now, but reconstruct manually :/ if (lhs !== r_function_call_1.EmptyArgument && lhs.length > 0) { const lhsText = lhs.map(l => `${getIndentString(l.indent)}${l.line}`).join('\n'); if (rhs !== r_function_call_1.EmptyArgument && rhs.length > 0) { const rhsText = rhs.map(l => `${getIndentString(l.indent)}${l.line}`).join('\n'); return plain(`${lhsText} ${call.functionName.content} ${rhsText}`); } else { return plain(lhsText); } } return plain(`${getLexeme(call.arguments[0])} ${call.functionName.content} ${getLexeme(call.arguments[1])}`); } function reconstructFunctionCall(call, functionName, args, configuration) { const selected = isSelected(configuration, call); if (!selected) { const f = args.filter(a => a !== r_function_call_1.EmptyArgument && a.length !== 0); if (f.length === 0) { return []; } else if (f.length === 1) { return f[0]; } } if (call.infixSpecial === true) { return reconstructSpecialInfixFunctionCall(args, call); } if (call.named && selected) { return plain(getLexeme(call)); } const filteredArgs = args.filter(a => a !== undefined && a.length > 0); if (functionName.length === 0 && filteredArgs.length === 0) { return []; } if (args.length === 0) { (0, assert_1.guard)(functionName.length > 0, `without args, we need the function name to be present! got: ${JSON.stringify(functionName)}`); const last = functionName[functionName.length - 1]; if (!call.named && !last.line.endsWith(')')) { functionName[0].line = `(${functionName[0].line}`; last.line += ')'; } // add empty call braces if not present last.line += '()'; return functionName; } else { return plain(getLexeme(call)); } } /** * The fold functions used to reconstruct the ast in {@link reconstructToCode}. */ // escalates with undefined if all are undefined const reconstructAstFolds = { // we just pass down the state information so everyone has them down: (_n, c) => c, foldNumber: reconstructAsLeaf, foldString: reconstructAsLeaf, foldLogical: reconstructAsLeaf, foldSymbol: reconstructAsLeaf, foldAccess: reconstructFoldAccess, foldBinaryOp: reconstructBinaryOp, foldPipe: reconstructBinaryOp, foldUnaryOp: reconstructUnaryOp, other: { foldComment: reconstructAsLeaf, foldLineDirective: reconstructAsLeaf }, loop: { foldFor: reconstructForLoop, foldRepeat: reconstructRepeatLoop, foldWhile: reconstructWhileLoop, foldBreak: reconstructAsLeaf, foldNext: reconstructAsLeaf }, foldIfThenElse: reconstructIfThenElse, foldExprList: reconstructExpressionList, functions: { foldFunctionDefinition: reconstructFunctionDefinition, foldFunctionCall: reconstructFunctionCall, foldParameter: reconstructParameter, foldArgument: reconstructArgument } }; function getIndentString(indent) { return ' '.repeat(indent * 4); } function prettyPrintCodeToString(code, lf = '\n') { return code.map(({ line, indent }) => `${getIndentString(indent)}${line}`).join(lf); } function removeOuterExpressionListIfApplicable(result, linesWithAutoSelected) { if (result.length > 1 && result[0].line === '{' && result[result.length - 1].line === '}') { // remove outer block return { code: prettyPrintCodeToString(indentBy(result.slice(1, result.length - 1), -1)), linesWithAutoSelected }; } else { return { code: prettyPrintCodeToString(result), linesWithAutoSelected }; } } /** * Reconstructs parts of a normalized R ast into R code on an expression basis. * * @param ast - The {@link NormalizedAst|normalized ast} to be used as a basis for reconstruction * @param selection - The selection of nodes to be reconstructed (probably the {@link NodeId|NodeIds} identified by the slicer) * @param autoSelectIf - A predicate that can be used to force the reconstruction of a node (for example to reconstruct library call statements, see {@link autoSelectLibrary}, {@link doNotAutoSelect}) * * @returns The number of lines for which `autoSelectIf` triggered, as well as the reconstructed code itself. */ function reconstructToCode(ast, selection, autoSelectIf = auto_select_defaults_1.doNotAutoSelect) { if (exports.reconstructLogger.settings.minLevel <= 1 /* LogLevel.Trace */) { exports.reconstructLogger.trace(`reconstruct ast with ids: ${JSON.stringify([...selection])}`); } // we use a wrapper to count the number of lines for which the autoSelectIf predicate triggered const linesWithAutoSelected = new Set(); const autoSelectIfWrapper = (node) => { const result = autoSelectIf(node, ast); if (result && node.location) { for (let i = node.location[0]; i <= node.location[2]; i++) { linesWithAutoSelected.add(i); } } return result; }; // fold of the normalized ast const result = (0, stateful_fold_1.foldAstStateful)(ast.ast, { selection, autoSelectIf: autoSelectIfWrapper, fullAst: ast }, reconstructAstFolds); (0, log_1.expensiveTrace)(exports.reconstructLogger, () => `reconstructed ast before string conversion: ${JSON.stringify(result)}`); return removeOuterExpressionListIfApplicable(result, linesWithAutoSelected.size); } //# sourceMappingURL=reconstruct.js.map