/ formatters / text.js
text.js
  1  const separator = '-'.repeat(20);
  2  const indent = ' '.repeat(4);
  3  
  4  const roles = {
  5    creator: 'CREATOR',
  6    attacker: 'ATTACKER',
  7    other: 'USER'
  8  };
  9  
 10  const textFormatter = {};
 11  
 12  textFormatter.strToInt = str => parseInt(str, 10);
 13  
 14  textFormatter.guessAccountRoleByAddress = address => {
 15    const prefix = address.toLowerCase().substr(0, 10);
 16  
 17    if (prefix === '0xaffeaffe') {
 18      return roles.creator;
 19    } else if (prefix === '0xdeadbeef') {
 20      return roles.attacker;
 21    }
 22  
 23    return roles.other;
 24  };
 25  
 26  textFormatter.stringifyValue = value => {
 27    const type = typeof value;
 28  
 29    if (type === 'number') {
 30      return String(value);
 31    } else if (type === 'string') {
 32      return value;
 33    } else if (value === null) {
 34      return 'null';
 35    }
 36  
 37    return JSON.stringify(value);
 38  };
 39  
 40  textFormatter.formatTestCaseSteps = (steps, fnHashes = {}) => {
 41    const output = [];
 42  
 43    for (let s = 0, n = 0; s < steps.length; s++) {
 44      const step = steps[s];
 45  
 46      /**
 47       * Empty address means "contract creation" transaction.
 48       *
 49       * Skip it to not spam.
 50       */
 51      if (step.address === '') {
 52        continue;
 53      }
 54  
 55      n++;
 56  
 57      const type = textFormatter.guessAccountRoleByAddress(step.origin);
 58  
 59      const fnHash = step.input.substr(2, 8);
 60      const fnName = fnHashes[fnHash] || step.name || '<N/A>';
 61      const fnDesc = `${fnName} [ ${fnHash} ]`;
 62  
 63      output.push(
 64        `Tx #${n}:`,
 65        indent + `Origin: ${step.origin} [ ${type} ]`,
 66        indent + `Function: ${textFormatter.stringifyValue(fnDesc)}`,
 67        indent + `Calldata: ${textFormatter.stringifyValue(step.input)}`
 68      );
 69  
 70      if ('decodedInput' in step) {
 71        output.push(`${indent}Decoded Calldata: ${step.decodedInput}`);
 72      }
 73  
 74      output.push(
 75        `${indent}Value: ${textFormatter.stringifyValue(step.value)}`,
 76        ''
 77      );
 78    }
 79  
 80    return output.join('\n').trimRight();
 81  };
 82  
 83  textFormatter.formatTestCase = (testCase, fnHashes) => {
 84    const output = [];
 85  
 86    if (testCase.steps) {
 87      const content = textFormatter.formatTestCaseSteps(testCase.steps, fnHashes);
 88  
 89      if (content) {
 90        output.push('Transaction Sequence:', '', content);
 91      }
 92    }
 93  
 94    return output.length ? output.join('\n') : undefined;
 95  };
 96  
 97  textFormatter.getCodeSample = (source, src) => {
 98    const [start, length] = src.split(':').map(textFormatter.strToInt);
 99  
100    return source.substr(start, length);
101  };
102  
103  textFormatter.formatLocation = message => {
104    const start = `${message.line}:${message.column}`;
105    const finish = `${message.endLine}:{message.endCol}`;
106  
107    return `from ${start} to ${finish}`;
108  };
109  
110  textFormatter.formatMessage = (message, filePath, sourceCode, fnHashes) => {
111    const { mythxIssue, mythxTextLocations } = message;
112    const output = [];
113  
114    output.push(
115      `==== ${mythxIssue.swcTitle || 'N/A'} ====`,
116      `Severity: ${mythxIssue.severity}`,
117      `File: ${filePath}`
118    );
119  
120    if (message.ruleId !== 'N/A') {
121      output.push(`Link: ${message.ruleId}`);
122    }
123  
124    output.push(
125      separator,
126      mythxIssue.description.head,
127      mythxIssue.description.tail
128    );
129  
130    const code = mythxTextLocations.length
131      ? textFormatter.getCodeSample(sourceCode, mythxTextLocations[0].sourceMap)
132      : undefined;
133  
134    output.push(
135      separator,
136      `Location: ${textFormatter.formatLocation(message)}`,
137      '',
138      code || '<code not available>'
139    );
140  
141    const testCases = mythxIssue.extra && mythxIssue.extra.testCases;
142  
143    if (testCases) {
144      for (const testCase of testCases) {
145        const content = textFormatter.formatTestCase(testCase, fnHashes);
146  
147        if (content) {
148          output.push(separator, content);
149        }
150      }
151    }
152  
153    return output.join('\n');
154  };
155  
156  textFormatter.formatResult = result => {
157    const { filePath, sourceCode, functionHashes } = result;
158  
159    return result.messages
160      .map(message =>
161        textFormatter.formatMessage(message, filePath, sourceCode, functionHashes)
162      )
163      .join('\n\n');
164  };
165  
166  textFormatter.run = results => {
167    return results.map(result => textFormatter.formatResult(result)).join('\n\n');
168  };
169  
170  module.exports = (results) => textFormatter.run(results);