<commit_before>#!/usr/bin/env node

const fs = require('fs');
const chalk = require('chalk');
const { argv } = require('yargs')
  .env()
  .options({
    style: {
      type: 'string',
      choices: [ 'shell', 'markdown' ],
      default: 'shell',
    },
  });

// Deduce base tx cost from the percentage denominator
const BASE_TX_COST = 21000;

// Utilities
function sum (...args) {
  return args.reduce((a, b) => a + b, 0);
}

function average (...args) {
  return sum(...args) / args.length;
}

function variation (current, previous, offset = 0) {
  return {
    value: current,
    delta: current - previous,
    prcnt: 100 * (current - previous) / (previous - offset),
  };
}

// Report class
class Report {
  // Read report file
  static load (filepath) {
    return JSON.parse(fs.readFileSync(filepath, 'utf8'));
  }

  // Compare two reports
  static compare (update, ref, opts = { hideEqual: true }) {
    if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) {
      throw new Error('Reports produced with non matching metadata');
    }

    const deployments = update.info.deployments
      .map(contract => Object.assign(
        contract,
        { previousVersion: ref.info.deployments.find(({ name }) => name === contract.name) },
      ))
      .filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
      .flatMap(contract => [{
        contract: contract.name,
        method: '[bytecode length]',
        avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
      }, {
        contract: contract.name,
        method: '[construction cost]',
        avg: variation(...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))), BASE_TX_COST),
      }])
      .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));

    const methods = Object.keys(update.info.methods)
      .filter(key => ref.info.methods[key])
      .filter(key => update.info.methods[key].numberOfCalls > 0)
      .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls)
      .map(key => ({
        contract: ref.info.methods[key].contract,
        method: ref.info.methods[key].fnSig,
        min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
        max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
        avg: variation(...[update, ref].map(x => Math.round(average(...x.info.methods[key].gasData))), BASE_TX_COST),
      }))
      .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));

    return [].concat(deployments, methods)
      .filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
  }
}

// Display
function center (text, length) {
  return text.padStart((text.length + length) / 2).padEnd(length);
}

function plusSign (num) {
  return num > 0 ? '+' : '';
}

function formatCellShell (cell) {
  const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
  return [
    format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
    format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
    format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
  ];
}

function formatCmpShell (rows) {
  const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
  const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));

  const COLS = [
    { txt: '', length: 0 },
    { txt: 'Contract', length: contractLength },
    { txt: 'Method', length: methodLength },
    { txt: 'Min', length: 30 },
    { txt: 'Max', length: 30 },
    { txt: 'Avg', length: 30 },
    { txt: '', length: 0 },
  ];
  const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0))).join(' | ').trim();
  const SEPARATOR = COLS.map(({ length }) => length > 0 ? '-'.repeat(length + 2) : '').join('|').trim();

  return [
    '',
    HEADER,
    ...rows.map(entry => [
      '',
      chalk.grey(entry.contract.padEnd(contractLength)),
      entry.method.padEnd(methodLength),
      ...formatCellShell(entry.min),
      ...formatCellShell(entry.max),
      ...formatCellShell(entry.avg),
      '',
    ].join(' | ').trim()),
    '',
  ].join(`\n${SEPARATOR}\n`).trim();
}

function alignPattern (align) {
  switch (align) {
  case 'left':
  case undefined:
    return ':-';
  case 'right':
    return '-:';
  case 'center':
    return ':-:';
  }
}

function trend (value) {
  return value > 0
    ? ':x:'
    : value < 0
      ? ':heavy_check_mark:'
      : ':heavy_minus_sign:';
}

function formatCellMarkdown (cell) {
  return [
    (!isFinite(cell?.value) ? '-' : cell.value.toString()),
    (!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()),
    (!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%' + trend(cell.delta)),
  ];
}

function formatCmpMarkdown (rows) {
  const COLS = [
    { txt: '' },
    { txt: 'Contract', align: 'left' },
    { txt: 'Method', align: 'left' },
    { txt: 'Min', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: 'Max', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: 'Avg', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: '' },
  ];
  const HEADER = COLS.map(entry => entry.txt).join(' | ').trim();
  const SEPARATOR = COLS.map(entry => entry.txt ? alignPattern(entry.align) : '').join('|').trim();

  return [
    '# Changes to gas costs',
    '',
    HEADER,
    SEPARATOR,
    rows.map(entry => [
      '',
      entry.contract,
      entry.method,
      ...formatCellMarkdown(entry.min),
      ...formatCellMarkdown(entry.max),
      ...formatCellMarkdown(entry.avg),
      '',
    ].join(' | ').trim()).join('\n'),
    '',
  ].join('\n').trim();
}

// MAIN
const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]));

switch (argv.style) {
case 'markdown':
  console.log(formatCmpMarkdown(report));
  break;
case 'shell':
default:
  console.log(formatCmpShell(report));
  break;
}
<commit_msg>Fix lint error in scripts/checks/compareGasReports.js<commit_after>#!/usr/bin/env node

const fs = require('fs');
const chalk = require('chalk');
const { argv } = require('yargs')
  .env()
  .options({
    style: {
      type: 'string',
      choices: [ 'shell', 'markdown' ],
      default: 'shell',
    },
  });

// Deduce base tx cost from the percentage denominator
const BASE_TX_COST = 21000;

// Utilities
function sum (...args) {
  return args.reduce((a, b) => a + b, 0);
}

function average (...args) {
  return sum(...args) / args.length;
}

function variation (current, previous, offset = 0) {
  return {
    value: current,
    delta: current - previous,
    prcnt: 100 * (current - previous) / (previous - offset),
  };
}

// Report class
class Report {
  // Read report file
  static load (filepath) {
    return JSON.parse(fs.readFileSync(filepath, 'utf8'));
  }

  // Compare two reports
  static compare (update, ref, opts = { hideEqual: true }) {
    if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) {
      throw new Error('Reports produced with non matching metadata');
    }

    const deployments = update.info.deployments
      .map(contract => Object.assign(
        contract,
        { previousVersion: ref.info.deployments.find(({ name }) => name === contract.name) },
      ))
      .filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
      .flatMap(contract => [{
        contract: contract.name,
        method: '[bytecode length]',
        avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
      }, {
        contract: contract.name,
        method: '[construction cost]',
        avg: variation(
          ...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))),
          BASE_TX_COST),
      }])
      .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));

    const methods = Object.keys(update.info.methods)
      .filter(key => ref.info.methods[key])
      .filter(key => update.info.methods[key].numberOfCalls > 0)
      .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls)
      .map(key => ({
        contract: ref.info.methods[key].contract,
        method: ref.info.methods[key].fnSig,
        min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
        max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
        avg: variation(...[update, ref].map(x => Math.round(average(...x.info.methods[key].gasData))), BASE_TX_COST),
      }))
      .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));

    return [].concat(deployments, methods)
      .filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
  }
}

// Display
function center (text, length) {
  return text.padStart((text.length + length) / 2).padEnd(length);
}

function plusSign (num) {
  return num > 0 ? '+' : '';
}

function formatCellShell (cell) {
  const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
  return [
    format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
    format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
    format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
  ];
}

function formatCmpShell (rows) {
  const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
  const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));

  const COLS = [
    { txt: '', length: 0 },
    { txt: 'Contract', length: contractLength },
    { txt: 'Method', length: methodLength },
    { txt: 'Min', length: 30 },
    { txt: 'Max', length: 30 },
    { txt: 'Avg', length: 30 },
    { txt: '', length: 0 },
  ];
  const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0))).join(' | ').trim();
  const SEPARATOR = COLS.map(({ length }) => length > 0 ? '-'.repeat(length + 2) : '').join('|').trim();

  return [
    '',
    HEADER,
    ...rows.map(entry => [
      '',
      chalk.grey(entry.contract.padEnd(contractLength)),
      entry.method.padEnd(methodLength),
      ...formatCellShell(entry.min),
      ...formatCellShell(entry.max),
      ...formatCellShell(entry.avg),
      '',
    ].join(' | ').trim()),
    '',
  ].join(`\n${SEPARATOR}\n`).trim();
}

function alignPattern (align) {
  switch (align) {
  case 'left':
  case undefined:
    return ':-';
  case 'right':
    return '-:';
  case 'center':
    return ':-:';
  }
}

function trend (value) {
  return value > 0
    ? ':x:'
    : value < 0
      ? ':heavy_check_mark:'
      : ':heavy_minus_sign:';
}

function formatCellMarkdown (cell) {
  return [
    (!isFinite(cell?.value) ? '-' : cell.value.toString()),
    (!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()),
    (!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%' + trend(cell.delta)),
  ];
}

function formatCmpMarkdown (rows) {
  const COLS = [
    { txt: '' },
    { txt: 'Contract', align: 'left' },
    { txt: 'Method', align: 'left' },
    { txt: 'Min', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: 'Max', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: 'Avg', align: 'right' },
    { txt: '(+/-)', align: 'right' },
    { txt: '%', align: 'right' },
    { txt: '' },
  ];
  const HEADER = COLS.map(entry => entry.txt).join(' | ').trim();
  const SEPARATOR = COLS.map(entry => entry.txt ? alignPattern(entry.align) : '').join('|').trim();

  return [
    '# Changes to gas costs',
    '',
    HEADER,
    SEPARATOR,
    rows.map(entry => [
      '',
      entry.contract,
      entry.method,
      ...formatCellMarkdown(entry.min),
      ...formatCellMarkdown(entry.max),
      ...formatCellMarkdown(entry.avg),
      '',
    ].join(' | ').trim()).join('\n'),
    '',
  ].join('\n').trim();
}

// MAIN
const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]));

switch (argv.style) {
case 'markdown':
  console.log(formatCmpMarkdown(report));
  break;
case 'shell':
default:
  console.log(formatCmpShell(report));
  break;
}

{'fraction_non_alphanumeric': 0.21179666278618547, 'fraction_numerical': 0.006519208381839348, 'mean_word_length': 2.9808464627741738, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 61, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7539590', 'n_tokens_mistral': 4489, 'n_tokens_neox': 4375, 'n_words': 839}