<commit_before>const debugModule = require("debug");
const debug = debugModule("lib:debug:interpreter");

const path = require("path");
const util = require("util");
const ora = require("ora");

const DebugUtils = require("truffle-debug-utils");
const selectors = require("truffle-debugger").selectors;
const { session, solidity, trace, evm, controller } = selectors;

const analytics = require("../services/analytics");
const ReplManager = require("../repl");

const { DebugPrinter } = require("./printer");

function watchExpressionAnalytics(raw) {
  if (raw.includes("!<")) {
    //don't send analytics for watch expressions involving selectors
    return;
  }
  let expression = raw.trim();
  //legal Solidity identifiers (= legal JS identifiers)
  let identifierRegex = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
  let isVariable = expression.match(identifierRegex) !== null;
  analytics.send({
    command: "debug: watch expression",
    args: { isVariable }
  });
}

class DebugInterpreter {
  constructor(config, session, txHash) {
    this.session = session;
    this.network = config.network;
    this.printer = new DebugPrinter(config, session);
    this.txHash = txHash;
    this.lastCommand = "n";

    this.repl = config.repl || new ReplManager(config);
    this.enabledExpressions = new Set();
  }

  async setOrClearBreakpoint(args, setOrClear) {
    //setOrClear: true for set, false for clear
    const currentLocation = this.session.view(controller.current.location);
    const breakpoints = this.session.view(controller.breakpoints);

    const currentNode = currentLocation.node ? currentLocation.node.id : null;
    const currentLine = currentLocation.sourceRange
      ? currentLocation.sourceRange.lines.start.line
      : null;
    const currentSourceId = currentLocation.source
      ? currentLocation.source.id
      : null;

    let breakpoint = {};

    if (args.length === 0) {
      //no arguments, want currrent node
      debug("node case");
      if (currentNode === null) {
        this.printer.print("Cannot determine current location.");
        return;
      }
      breakpoint.node = currentNode;
      breakpoint.line = currentLine;
      breakpoint.sourceId = currentSourceId;
    }

    //the special case of "B all"
    else if (args[0] === "all") {
      if (setOrClear) {
        // only "B all" is legal, not "b all"
        this.printer.print("Cannot add breakpoint everywhere.\n");
        return;
      }
      await this.session.removeAllBreakpoints();
      this.printer.print("Removed all breakpoints.\n");
      return;
    }

    //if the argument starts with a "+" or "-", we have a relative
    //line number
    else if (args[0][0] === "+" || args[0][0] === "-") {
      debug("relative case");
      if (currentLine === null) {
        this.printer.print("Cannot determine current location.");
        return;
      }
      let delta = parseInt(args[0], 10); //want an integer
      debug("delta %d", delta);

      if (isNaN(delta)) {
        this.printer.print("Offset must be an integer.\n");
        return;
      }

      breakpoint.sourceId = currentSourceId;
      breakpoint.line = currentLine + delta;
    }

    //if it contains a colon, it's in the form source:line
    else if (args[0].includes(":")) {
      debug("source case");
      let sourceArgs = args[0].split(":");
      let sourceArg = sourceArgs[0];
      let lineArg = sourceArgs[1];
      debug("sourceArgs %O", sourceArgs);

      //first let's get the line number as usual
      let line = parseInt(lineArg, 10); //want an integer
      if (isNaN(line)) {
        this.printer.print("Line number must be an integer.\n");
        return;
      }

      //search sources for given string
      let sources = this.session.view(solidity.info.sources);

      //we will indeed need the sources here, not just IDs
      let matchingSources = Object.values(sources).filter(source =>
        source.sourcePath.includes(sourceArg)
      );

      if (matchingSources.length === 0) {
        this.printer.print(`No source file found matching ${sourceArg}.\n`);
        return;
      } else if (matchingSources.length > 1) {
        this.printer.print(
          `Multiple source files found matching ${sourceArg}.  Which did you mean?`
        );
        matchingSources.forEach(source =>
          this.printer.print(source.sourcePath)
        );
        this.printer.print("");
        return;
      }

      //otherwise, we found it!
      breakpoint.sourceId = matchingSources[0].id;
      breakpoint.line = line - 1; //adjust for zero-indexing!
    }

    //otherwise, it's a simple line number
    else {
      debug("absolute case");
      if (currentSourceId === null) {
        this.printer.print("Cannot determine current file.");
        return;
      }
      let line = parseInt(args[0], 10); //want an integer
      debug("line %d", line);

      if (isNaN(line)) {
        this.printer.print("Line number must be an integer.\n");
        return;
      }

      breakpoint.sourceId = currentSourceId;
      breakpoint.line = line - 1; //adjust for zero-indexing!
    }

    //OK, we've constructed the breakpoint!  But if we're adding, we'll
    //want to adjust to make sure we don't set it on an empty line or
    //anything like that
    if (setOrClear) {
      let resolver = this.session.view(controller.breakpoints.resolver);
      breakpoint = resolver(breakpoint);
      //of course, this might result in finding that there's nowhere to
      //add it after that point
      if (breakpoint === null) {
        this.printer.print(
          "Nowhere to add breakpoint at or beyond that location.\n"
        );
        return;
      }
    }

    //having constructed and adjusted breakpoint, here's now a
    //user-readable message describing its location
    let sourceNames = Object.assign(
      {},
      ...Object.values(this.session.view(solidity.info.sources)).map(
        ({ id, sourcePath }) => ({
          [id]: path.basename(sourcePath)
        })
      )
    );
    let locationMessage = DebugUtils.formatBreakpointLocation(
      breakpoint,
      true,
      currentSourceId,
      sourceNames
    );

    //one last check -- does this breakpoint already exist?
    let alreadyExists =
      breakpoints.filter(
        existingBreakpoint =>
          existingBreakpoint.sourceId === breakpoint.sourceId &&
          existingBreakpoint.line === breakpoint.line &&
          existingBreakpoint.node === breakpoint.node //may be undefined
      ).length > 0;

    //NOTE: in the "set breakpoint" case, the above check is somewhat
    //redundant, as we're going to check again when we actually make the
    //call to add or remove the breakpoint!  But we need to check here so
    //that we can display the appropriate message.  Hopefully we can find
    //some way to avoid this redundant check in the future.

    //if it already exists and is being set, or doesn't and is being
    //cleared, report back that we can't do that
    if (setOrClear === alreadyExists) {
      if (setOrClear) {
        this.printer.print(
          `Breakpoint at ${locationMessage} already exists.\n`
        );
        return;
      } else {
        this.printer.print(`No breakpoint at ${locationMessage} to remove.\n`);
        return;
      }
    }

    //finally, if we've reached this point, do it!
    //also report back to the user on what happened
    if (setOrClear) {
      await this.session.addBreakpoint(breakpoint);
      this.printer.print(`Breakpoint added at ${locationMessage}.\n`);
    } else {
      await this.session.removeBreakpoint(breakpoint);
      this.printer.print(`Breakpoint removed at ${locationMessage}.\n`);
    }
    return;
  }

  start(terminate) {
    if (terminate === undefined) {
      return util.promisify(this.start.bind(this))();
    }

    if (this.session.view(session.status.loaded)) {
      this.printer.printSessionLoaded();
    } else if (this.session.view(session.status.isError)) {
      this.printer.printSessionError();
    } else {
      this.printer.printHelp();
    }<commit_msg>Add comment about interpreter start polymorphism<commit_after>const debugModule = require("debug");
const debug = debugModule("lib:debug:interpreter");

const path = require("path");
const util = require("util");
const ora = require("ora");

const DebugUtils = require("truffle-debug-utils");
const selectors = require("truffle-debugger").selectors;
const { session, solidity, trace, evm, controller } = selectors;

const analytics = require("../services/analytics");
const ReplManager = require("../repl");

const { DebugPrinter } = require("./printer");

function watchExpressionAnalytics(raw) {
  if (raw.includes("!<")) {
    //don't send analytics for watch expressions involving selectors
    return;
  }
  let expression = raw.trim();
  //legal Solidity identifiers (= legal JS identifiers)
  let identifierRegex = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
  let isVariable = expression.match(identifierRegex) !== null;
  analytics.send({
    command: "debug: watch expression",
    args: { isVariable }
  });
}

class DebugInterpreter {
  constructor(config, session, txHash) {
    this.session = session;
    this.network = config.network;
    this.printer = new DebugPrinter(config, session);
    this.txHash = txHash;
    this.lastCommand = "n";

    this.repl = config.repl || new ReplManager(config);
    this.enabledExpressions = new Set();
  }

  async setOrClearBreakpoint(args, setOrClear) {
    //setOrClear: true for set, false for clear
    const currentLocation = this.session.view(controller.current.location);
    const breakpoints = this.session.view(controller.breakpoints);

    const currentNode = currentLocation.node ? currentLocation.node.id : null;
    const currentLine = currentLocation.sourceRange
      ? currentLocation.sourceRange.lines.start.line
      : null;
    const currentSourceId = currentLocation.source
      ? currentLocation.source.id
      : null;

    let breakpoint = {};

    if (args.length === 0) {
      //no arguments, want currrent node
      debug("node case");
      if (currentNode === null) {
        this.printer.print("Cannot determine current location.");
        return;
      }
      breakpoint.node = currentNode;
      breakpoint.line = currentLine;
      breakpoint.sourceId = currentSourceId;
    }

    //the special case of "B all"
    else if (args[0] === "all") {
      if (setOrClear) {
        // only "B all" is legal, not "b all"
        this.printer.print("Cannot add breakpoint everywhere.\n");
        return;
      }
      await this.session.removeAllBreakpoints();
      this.printer.print("Removed all breakpoints.\n");
      return;
    }

    //if the argument starts with a "+" or "-", we have a relative
    //line number
    else if (args[0][0] === "+" || args[0][0] === "-") {
      debug("relative case");
      if (currentLine === null) {
        this.printer.print("Cannot determine current location.");
        return;
      }
      let delta = parseInt(args[0], 10); //want an integer
      debug("delta %d", delta);

      if (isNaN(delta)) {
        this.printer.print("Offset must be an integer.\n");
        return;
      }

      breakpoint.sourceId = currentSourceId;
      breakpoint.line = currentLine + delta;
    }

    //if it contains a colon, it's in the form source:line
    else if (args[0].includes(":")) {
      debug("source case");
      let sourceArgs = args[0].split(":");
      let sourceArg = sourceArgs[0];
      let lineArg = sourceArgs[1];
      debug("sourceArgs %O", sourceArgs);

      //first let's get the line number as usual
      let line = parseInt(lineArg, 10); //want an integer
      if (isNaN(line)) {
        this.printer.print("Line number must be an integer.\n");
        return;
      }

      //search sources for given string
      let sources = this.session.view(solidity.info.sources);

      //we will indeed need the sources here, not just IDs
      let matchingSources = Object.values(sources).filter(source =>
        source.sourcePath.includes(sourceArg)
      );

      if (matchingSources.length === 0) {
        this.printer.print(`No source file found matching ${sourceArg}.\n`);
        return;
      } else if (matchingSources.length > 1) {
        this.printer.print(
          `Multiple source files found matching ${sourceArg}.  Which did you mean?`
        );
        matchingSources.forEach(source =>
          this.printer.print(source.sourcePath)
        );
        this.printer.print("");
        return;
      }

      //otherwise, we found it!
      breakpoint.sourceId = matchingSources[0].id;
      breakpoint.line = line - 1; //adjust for zero-indexing!
    }

    //otherwise, it's a simple line number
    else {
      debug("absolute case");
      if (currentSourceId === null) {
        this.printer.print("Cannot determine current file.");
        return;
      }
      let line = parseInt(args[0], 10); //want an integer
      debug("line %d", line);

      if (isNaN(line)) {
        this.printer.print("Line number must be an integer.\n");
        return;
      }

      breakpoint.sourceId = currentSourceId;
      breakpoint.line = line - 1; //adjust for zero-indexing!
    }

    //OK, we've constructed the breakpoint!  But if we're adding, we'll
    //want to adjust to make sure we don't set it on an empty line or
    //anything like that
    if (setOrClear) {
      let resolver = this.session.view(controller.breakpoints.resolver);
      breakpoint = resolver(breakpoint);
      //of course, this might result in finding that there's nowhere to
      //add it after that point
      if (breakpoint === null) {
        this.printer.print(
          "Nowhere to add breakpoint at or beyond that location.\n"
        );
        return;
      }
    }

    //having constructed and adjusted breakpoint, here's now a
    //user-readable message describing its location
    let sourceNames = Object.assign(
      {},
      ...Object.values(this.session.view(solidity.info.sources)).map(
        ({ id, sourcePath }) => ({
          [id]: path.basename(sourcePath)
        })
      )
    );
    let locationMessage = DebugUtils.formatBreakpointLocation(
      breakpoint,
      true,
      currentSourceId,
      sourceNames
    );

    //one last check -- does this breakpoint already exist?
    let alreadyExists =
      breakpoints.filter(
        existingBreakpoint =>
          existingBreakpoint.sourceId === breakpoint.sourceId &&
          existingBreakpoint.line === breakpoint.line &&
          existingBreakpoint.node === breakpoint.node //may be undefined
      ).length > 0;

    //NOTE: in the "set breakpoint" case, the above check is somewhat
    //redundant, as we're going to check again when we actually make the
    //call to add or remove the breakpoint!  But we need to check here so
    //that we can display the appropriate message.  Hopefully we can find
    //some way to avoid this redundant check in the future.

    //if it already exists and is being set, or doesn't and is being
    //cleared, report back that we can't do that
    if (setOrClear === alreadyExists) {
      if (setOrClear) {
        this.printer.print(
          `Breakpoint at ${locationMessage} already exists.\n`
        );
        return;
      } else {
        this.printer.print(`No breakpoint at ${locationMessage} to remove.\n`);
        return;
      }
    }

    //finally, if we've reached this point, do it!
    //also report back to the user on what happened
    if (setOrClear) {
      await this.session.addBreakpoint(breakpoint);
      this.printer.print(`Breakpoint added at ${locationMessage}.\n`);
    } else {
      await this.session.removeBreakpoint(breakpoint);
      this.printer.print(`Breakpoint removed at ${locationMessage}.\n`);
    }
    return;
  }

  start(terminate) {
    // if terminate is not passed, return a Promise instead
    if (terminate === undefined) {
      return util.promisify(this.start.bind(this))();
    }

    if (this.session.view(session.status.loaded)) {
      this.printer.printSessionLoaded();
    } else if (this.session.view(session.status.isError)) {
      this.printer.printSessionError();
    } else {
      this.printer.printHelp();
    }
{'fraction_non_alphanumeric': 0.12315938635943564, 'fraction_numerical': 0.003203745918304479, 'mean_word_length': 2.5674725274725274, 'pattern_counts': {'":': 4, '<': 5, '<?xml version=': 0, '>': 15, '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': '405990', 'n_tokens_mistral': 4706, 'n_tokens_neox': 4587, 'n_words': 1326}