Home Reference Source

src/formatters/javaFormatter.js

import Formatter from "../core/formatter"

var SCOPE_ENTER_TOKEN = '{'
var SCOPE_EXIT_TOKEN = '}'
var EXPRESSION_TERMINATION_TOKEN = ';'
var ANNOTATION_TOKEN = '@'
var COMMENT_START_TOKEN = '/**'
var COMMENT_START_TOKEN_2 = '/*'
var COMMENT_BODY_TOKEN = '*'
var COMMENT_END_TOKEN = '*/'
var COMMENT_SIMPLE_TOKEN = '//'
var COMMENT_TOKENS = [COMMENT_START_TOKEN, COMMENT_START_TOKEN_2, COMMENT_BODY_TOKEN,
  COMMENT_END_TOKEN, COMMENT_SIMPLE_TOKEN]
var PROTECTED_NON_METHOD_TOKENS = ['return', 'new']

/**
 * JavaFormatter is the auto-formatter implementation for Java.
 * @extends {AFormatter}
 */
export default class JavaFormatter extends Formatter {
  /**
   * Create a new JavaFormatter
   *
   * @constructor
   * @param formatUnit The token to be used for line indentations.
   */
  constructor(formatUnit) {
    super(formatUnit)

    /**
     * @private
     */
    this.methodSigRegex = new RegExp("^(public |private |protected |static |final " +
      "|native |synchronized |abstract |transient )*(<.*>\\s+)?\\w+(<.*>|\\[.*\\])" +
      "?\\s+\\w+\\s*\\(.*$")
  }

  /**
   * Format a string of code. The string will be cut into lines and lines
   * will be indented accordingly to their scope.
   *
   * @param codeString The string of code to format.
   * @returns {Array} An array of formatted lines.
   */
  format(codeString) {
    return this.formatSnippet(codeString, null, null, null)
  }

  /**
   * A slight variation of format(codeString). Useful if you want to display
   * a code snippet around a selection of lines.
   *
   * In addition to indenting lines, formatSnippet takes a selection as a
   * start and end row in a large slab of code and cuts out a snippet of
   * code around this selection. The start and end of the snippet is based
   * on an offset that is provided as a parameter. The offset with the start
   * and end of the selection create a sort of range from which the snippet
   * is taken.
   *
   * In the example below, the selection is identified to belong to test2()
   * and thus only test2() is returned. If the method is longer than the
   * offset, than only the part within the offset will be returned. No code
   * is added to the range with the exception of comment lines above the
   * selection, to close unfinished comments.
   *
   * @example
   * <caption>Selection start row: 11 ---- Selection end row: 11 ---- Offset: 6 ----> Snippet range: [11 - 6, 11 + 6] = [5, 17]</caption>
   *
   *  START:
   * 1.  @Test
   * 2.  public void test1() {
   * 3.      System.out.println("Test 1");
   * 4.  }
   * 5.
   * 6.  // ------------------
   * 7.  // Perform test 2.
   * 8.  // ------------------
   * 9. @Test
   * 10. public void test2() {
   * 11.     System.out.println("Test 2");
   * 12. }
   * 13.
   * 14. @Test
   * 15. public void test3() {
   * 16.     System.out.println("Test 3");
   * 17. }
   * 18. ...
   *
   * RESULT:
   * 6.  // ------------------
   * 7.  // Perform test 2.
   * 8.  // ------------------
   * 9. @Test
   * 10. public void test2() {
   * 11.     System.out.println("Test 1");
   * 12. }
   *
   * @param code The original code base in which the selection is.
   * @param startRow The start row of the selection in the code base.
   * @param endRow The end row of the selection in the code base.
   * @param offset The offset the defines the range on which to base the
   *               snippet.
   * @returns {Array} An array of formatted lines that form the snippet, separated
   *              into prefix selection and suffix, as well as the start and
   *              end lines of the snippet in the original code base.
   */
  formatSnippet(code, startRow, endRow, offset) {
    return super.formatSnippet(code, startRow, endRow, offset,
      ((codeArray, index) => this._expressionIdentifier(codeArray, index)),
      ((lines, index) => this._scopeEnterFunc(lines, index)),
      ((lines, index) => this._scopeExitFunc(lines, index)),
      (array => this._formatJavadoc(array)),
      (line => this._checkForFunction(line)),
      (line => this._checkForSpecialStatement(line)),
      COMMENT_BODY_TOKEN, COMMENT_SIMPLE_TOKEN)
  }

  /**
   * Checks if a line identified by an index in an array qualifies
   * as an expression.
   *
   * An expression is defined as:
   * - A line that ends with a termination token (e.g. ';')
   * - A line that defines a scope start (e.g. '\{')
   * - A line that defines a scope end (e.g. '\}')
   * - A line that starts with a special character (e.g. '@')
   * - A line that starts with a comment (e.g. '//')
   * - An empty line (e.g. '')
   *
   * @param codeArray An array of lines of code.
   * @param index The index of the relevant line in the code array.
   * @returns {Boolean} True if the line qualifies as an expression, else false.
   * @private
   */
  _expressionIdentifier(codeArray, index) {
    if (codeArray.length > index) {
      let line = codeArray[index].replace('\n', '').trim()
      return line.endsWith(EXPRESSION_TERMINATION_TOKEN)
        || this._scopeEnterFunc([line], 0) !== null
        || this._scopeExitFunc([line], 0) !== null
        || this._checkForSpecialStatement(line)
    }

    return false
  }

  /**
   * Checks if a line identified by an index in an array starts a new scope.
   * Example: 'if (foo) {'
   *
   * @param codeArray An array of lines of code.
   * @param index The index of the relevant line in the code array.
   * @returns {Number} The position in the line where the new scope starts or null
   *              if this line does not start a new scope.
   * @private
   */
  _scopeEnterFunc(codeArray, index) {
    return this._identifyScope(codeArray, index, SCOPE_ENTER_TOKEN)
  }

  /**
   * Checks if a line identified by an index in an array ends an existing scope.
   * Example: '}'
   *
   * @param codeArray An array of lines of code.
   * @param index The index of the relevant line in the code array.
   * @returns {Number} The position in the line where the scope ends or null
   *              if this line does not end a scope.
   * @private
   */
  _scopeExitFunc(codeArray, index) {
    return this._identifyScope(codeArray, index, SCOPE_EXIT_TOKEN)
  }

  /**
   * Adds a space before Javadoc comments if they are a body or end comment.
   *
   * @param codeArray The already formatted code array.
   * @returns {*} The formatted code array with spaces for Javadoc.
   * @private
   */
  _formatJavadoc(codeArray) {
    for (let i = 0; i < codeArray.length; i++) {
      let lineTemp = codeArray[i].trim()
      if (lineTemp.startsWith(COMMENT_BODY_TOKEN) || lineTemp.startsWith(COMMENT_END_TOKEN)) {
        codeArray[i] = " " + codeArray[i]
      }
    }
    return codeArray
  }

  /**
   * Helper method for _scopeEnterFunc and _scopeExitFunc.
   *
   * @param codeArray An array of lines of code.
   * @param index The index of the relevant line in the code array.
   * @param token The token to find in the line.
   * @returns {Number} The position in the line where the scope starts or ends or
   *              null if this line does neither.
   * @private
   */
  _identifyScope(codeArray, index, token) {
    if (codeArray.length > 0) {
      let scopeIndex = this._calculateScopeIndex(codeArray[index], token)
      if (scopeIndex !== -1) {
        return scopeIndex
      }
      return null
    } else {
      return null
    }
  }

  /**
   * Calculate the scope index and make sure it is valid (look at _checkScopeIndex for more info)
   *
   * @param string The string that maybe contains the scope enter / exit token.
   * @param token The scope enter / exit token to look for in the string.
   * @returns {*} The position in the line where the scope starts or ends or
   *              -1 if this line does neither.
   * @private
   */
  _calculateScopeIndex(string, token) {
    return this._checkScopeIndex(string, string.indexOf(token), token)
  }

  /**
   *  Calculate the scope index and make sure it is valid, that means:
   *    1. It is not inside quotation marks
   *
   *  If any of the above criteria is not met, the piece of code in question is cut off from the string,
   *  and we start over with the remaining string.
   *
   * @param string The string that maybe contains the scope enter / exit token.
   * @param scopeIndex The current "potential" scope index
   * @param token The scope enter / exit token to look for in the string.
   * @returns {*} The position in the line where the scope starts or ends or
   *              -1 if this line does neither.
   * @private
   */
  _checkScopeIndex(string, scopeIndex, token) {
    if (scopeIndex === -1) {
      return scopeIndex
    }

    // Check if scope identifier is inside quotation marks
    let quotationRegex = /(["'])(?:(?=(\\?))\2.)*?\1/
    let match = quotationRegex.exec(string)

    if (match === null || match[0].indexOf(token) === -1) {
      return scopeIndex
    }

    let matchEnd = match.index + match[0].length
    return this._calculateScopeIndex(string.substr(matchEnd), token)
  }

  /**
   * Checks if a line contains a special statement.
   *
   * A special statement is defined in Java by:
   * - An empty line (e.g. '')
   * - A line that starts with an annotation (e.g. '@')
   * - A line that starts with a comment (e.g. '//')
   *
   * @param line The line to check for a special statement.
   * @returns {Boolean} True if the line contains a special statement, else false.
   * @private
   */
  _checkForSpecialStatement(line) {
    return line.startsWith(ANNOTATION_TOKEN)
      || line === ''
      || COMMENT_TOKENS.reduce((result, token) => result || line.startsWith(token), false)
  }

  /**
   * Checks if a line is a method signature.
   *
   * @param line The line to check for.
   * @returns {Boolean} True if the line is a method signature, else false.
   * @private
   */
  _checkForFunction(line) {
    if (PROTECTED_NON_METHOD_TOKENS.reduce((result, token) => result || line.trim()
        .startsWith(token), false)) {
      return false
    }

    return this.methodSigRegex.test(line)
  }
}