tests/testthat/helper_lint.R

# check if lintr version is sufficient
# if `error.if.not` is TRUE an error is thrown with a meaningful message.
isLintrVersionOk = function(error.if.not = FALSE) {
  lintr.ver = try(packageVersion("lintr"), silent = TRUE)
  lintr.required = "1.0.2.9000"
  if (inherits(lintr.ver, "try-error")) {
    msg = sprintf("lintr is not installed: %s", BBmisc::printToChar(lintr.ver))
  } else {
    if (package_version(lintr.ver) >= package_version(lintr.required)) {
      return(TRUE)
    }
    msg = sprintf("lintr is version %s, but version %s is required.", lintr.ver, lintr.required)
  }
  if (error.if.not) {
    stopf(paste("%s\nInstalling the github version of lintr will probably solve this issue. For that, please run",
        "> devtools::install_github(\"jimhester/lintr\")", sep = "\n"), msg)
  }
  return(FALSE)
}

if (isLintrVersionOk() && require("lintr", quietly = TRUE) && require("rex", quietly = TRUE)) {


  # The following functions are adaptions of the corresponding functions in the `lintr` packages
  # The lintr package, and the original versions of these functions, can be found at https://github.com/jimhester/lintr
  # Copyright notice of original functions:
  # Copyright (c) 2014-2016, James Hester
  #
  # Permission is hereby granted, free of charge, to any person obtaining
  # a copy of this software and associated documentation files (the
  # "Software"), to deal in the Software without restriction, including
  # without limitation the rights to use, copy, modify, merge, publish,
  # distribute, sublicense, and/or sell copies of the Software, and to
  # permit persons to whom the Software is furnished to do so, subject to
  # the following conditions:
  #
  # The above copyright notice and this permission notice shall be
  # included in all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  #
  # End copyright notice.
  # All modifications are licensed as the rest of mlr.

  # linters that differ from the default linters
  # this is necessary because mlr's style is weird.

  # prohibit <-
  left.assign.linter = function(source_file) {
    lapply(lintr:::ids_with_token(source_file, "LEFT_ASSIGN"), function(id) {
        parsed = lintr:::with_id(source_file, id)
        Lint(filename = source_file$filename, line_number = parsed$line1,
          column_number = parsed$col1, type = "style", message = "Use =, not <-, for assignment.",
          line = source_file$lines[as.character(parsed$line1)],
          linter = "assignment_linter")
    })
  }

  # prohibit ->
  right.assign.linter = function(source_file) {
    lapply(lintr:::ids_with_token(source_file, "RIGHT_ASSIGN"), function(id) {
        parsed = lintr:::with_id(source_file, id)
        Lint(filename = source_file$filename, line_number = parsed$line1,
          column_number = parsed$col1, type = "style", message = "Use =, not ->, for assignment.",
          line = source_file$lines[as.character(parsed$line1)],
          linter = "assignment_linter")
    })
  }

  `%!=%` = lintr:::`%!=%`
  `%==%` = lintr:::`%==%`

  spaces.left.parentheses.linter = function(source_file) {
        lapply(lintr:::ids_with_token(source_file, "'('"), function(id) {
          parsed = source_file$parsed_content[id, ]
          terminal.before = source_file$parsed_content[source_file$parsed_content$line1 ==
              parsed$line1 & source_file$parsed_content$col1 <
              parsed$col1 & source_file$parsed_content$terminal, ]
          last.before = tail(terminal.before, n = 1)
          last.type = last.before$token
          is.function = length(last.type) %!=% 0L && (last.type %in%
              c("SYMBOL_FUNCTION_CALL", "FUNCTION", "'}'", "')'",
                  "']'"))
          is.unary.minus = last.type == "'-'" && sum(source_file$parsed_content$parent == last.before$parent) == 2
          if (!is.function && !is.unary.minus) {
              line = source_file$lines[as.character(parsed$line1)]
              before.operator = substr(line, parsed$col1 - 1L,
                  parsed$col1 - 1L)
              non.space.before = re_matches(before.operator, rex(non_space))
              not.exception = !(before.operator %in% c("!", ":",
                  "[", "("))
              if (non.space.before && not.exception) {
                  Lint(filename = source_file$filename, line_number = parsed$line1,
                    column_number = parsed$col1, type = "style",
                    message = "Place a space before left parenthesis, except in a function call.",
                    line = line, linter = "spaces.left.parentheses.linter")
              }
          }
      })
  }

  function.left.parentheses.linter = function(source_file) {
    lapply(lintr:::ids_with_token(source_file, "'('"),
      function(id) {

        parsed = source_file$parsed_content[id, ]
        ttb = which(source_file$parsed_content$line1 == parsed$line1 &
                    source_file$parsed_content$col1 < parsed$col1 &
                    source_file$parsed_content$terminal)
        ttb = tail(ttb, n = 1)
        last.type = source_file$parsed_content$token[ttb]

        is.function = length(last.type) %!=% 0L &&
          (last.type %in% c("SYMBOL_FUNCTION_CALL", "FUNCTION", "'}'", "')'", "']'"))
        # check whether this is a lambda expression; we want to allow e.g. function(x) (x - 1)^2
        if (is.function && last.type == "')'") {
          # parenvec: 1 for every '(', -1 for every ')', 0 otherwise
          parenvec = c(1, -1, 0)[match(source_file$parsed_content$token, c("'('", "')'"), 3)]
          parenlevel = cumsum(parenvec)
          parenlevelcut = parenlevel[seq_len(ttb - 1)]
          opening.paren.pos = max(which(parenlevelcut == parenlevel[ttb])) + 1
          opparsed = source_file$parsed_content[opening.paren.pos, ]

          opttb = which(source_file$parsed_content$line1 == opparsed$line1 &
                        source_file$parsed_content$col1 < opparsed$col1 &
                        source_file$parsed_content$terminal)
          opttb = tail(opttb, n = 1)
          before.op.type = source_file$parsed_content$token[opttb]
          if (length(before.op.type) %!=% 0L && before.op.type == "FUNCTION") {
            is.function = FALSE
          }
        }
        if (is.function) {

          line = source_file$lines[as.character(parsed$line1)]

          before.operator = substr(line, parsed$col1 - 1L, parsed$col1 - 1L)

          space.before = re_matches(before.operator, rex(space))

          if (space.before) {
            Lint(
              filename = source_file$filename,
              line_number = parsed$line1,
              column_number = parsed$col1,
              type = "style",
              message = "Remove spaces before the left parenthesis in a function call.",
              line = line,
              linter = "function_left_parentheses"
              )
          }
        }

      })
  }

  infix.spaces.linter = function(source_file) {
      lapply(lintr:::ids_with_token(source_file, lintr:::infix_tokens, fun = `%in%`),
          function(id) {
              parsed = lintr:::with_id(source_file, id)
              line = source_file$lines[as.character(parsed$line1)]
              if (substr(line, parsed$col1, parsed$col2) == "^") {
                return(NULL)
              }
              around.operator = substr(line, parsed$col1 - 1L,
                  parsed$col2 + 1L)
              non.space.before = re_matches(around.operator, rex(start,
                  non_space))
              newline.after = unname(nchar(line)) %==% parsed$col2
              non.space.after = re_matches(around.operator, rex(non_space,
                  end))
              if (non.space.before || (!newline.after && non.space.after)) {
                  is.infix = length(lintr:::siblings(source_file$parsed_content,
                    parsed$id, 1)) > 1L
                  start = end = parsed$col1
                  if (is.infix) {
                    if (non.space.before) {
                      start = parsed$col1 - 1L
                    }
                    if (non.space.after) {
                      end = parsed$col2 + 1L
                    }
                    Lint(filename = source_file$filename, line_number = parsed$line1,
                      column_number = parsed$col1, type = "style",
                      message = "Put spaces around all infix operators (except exponentiation).",
                      line = line, ranges = list(c(start, end)),
                      linter = "infix.spaces.linter")
                  }
              }
          })
  }


  loweralnum = rex::rex(one_of(lower, digit))
  upperalnum = rex::rex(one_of(upper, digit))

  style.regexes = list(
    "UpperCamelCase" = rex::rex(start, upper, zero_or_more(alnum), end),
    "lowerCamelCase" = rex::rex(start, lower, zero_or_more(alnum), end),
    "snake_case"     = rex::rex(start, one_or_more(loweralnum), zero_or_more("_", one_or_more(loweralnum)), end),
    "dotted.case"    = rex::rex(start, one_or_more(loweralnum), zero_or_more(dot, one_or_more(loweralnum)), end),
    "alllowercase"   = rex::rex(start, one_or_more(loweralnum), end),
    "ALLUPPERCASE"   = rex::rex(start, one_or_more(upperalnum), end),
    "functionCamel.case" = rex::rex(start, lower, zero_or_more(alnum), zero_or_more(dot, one_or_more(alnum)), end)
  )

  # incorporate our own camelCase.withDots style.
  matchesStyles = function(name, styles=names(style.regexes)) {
    invalids = paste(styles[!styles %in% names(style.regexes)], collapse = ", ")
    if (nzchar(invalids)) {
      valids = paste(names(style.regexes), collapse = ", ")
      stop(sprintf("Invalid style(s) requested: %s\nValid styles are: %s\n", invalids, valids))
    }
    name = re_substitutes(name, rex(start, one_or_more(dot)), "")  # remove leading dots
    vapply(
      style.regexes[styles],
      re_matches,
      logical(1L),
      data = name
    )
  }

  object.naming.linter = lintr:::make_object_linter(function(source_file, token) {
    sp = source_file$parsed_content
    if (tail(c("", sp$token[sp$terminal & sp$id < token$id]), n = 1) == "'$'") {
      # ignore list member names
      return(NULL)
    }
    sp = head(sp[sp$terminal & sp$id > token$id, ], n = 2)
    if (!sp$token[1] %in% c("LEFT_ASSIGN", "EQ_ASSIGN")) {
      # ignore if not an assignment.
      # we check for LEFT_ASSIGN and EQ_ASSIGN since here we are LEFT_ASSIGN tolerant
      return(NULL)
    }
    style = ifelse(sp$token[2] == "FUNCTION", "functionCamel.case", "dotted.case")
    name = lintr:::unquote(token[["text"]])
    if (nchar(name) <= 1) {
      # allow single uppercase letter
      return(NULL)
    }
    if (!matchesStyles(name, style)) {
      lintr:::object_lint(source_file, token, sprintf("Variable or function name should be %s.",
        style), "object_name_linter")
    }
  })


  # note that this must be a *named* list (bug in lintr)
  linters = tryCatch(list(
    commas = lintr::commas_linter,
  #  open.curly = open_curly_linter(),
  #  closed.curly = closed_curly_linter(),
    spaces.left.parentheses = spaces.left.parentheses.linter,
    function.left.parentheses = function.left.parentheses.linter,
    single.quotes = lintr::single_quotes_linter,
    left.assign = left.assign.linter,
    right.assign = right.assign.linter,
    no.tab = lintr::no_tab_linter,
    T.and.F.symbol = lintr::T_and_F_symbol_linter,
    semicolon.terminator = lintr::semicolon_terminator_linter,
    seq = lintr::seq_linter,
    unneeded.concatenation = lintr::unneeded_concatenation_linter,
    trailing.whitespace = lintr::trailing_whitespace_linter,
    #todo.comment = lintr::todo_comment_linter(todo = "todo"), # is case-insensitive
    spaces.inside = lintr::spaces_inside_linter,
    infix.spaces = infix.spaces.linter,
    object.naming = object.naming.linter),
    error = function(e) {
      list(error = "linter creation failed even though lintr announced the right version")
    })
} else {
  # everything that uses `linters` should check `isLintrVersionOk` first, so the
  # following should never be used. Make sure that it is an error if it IS used.
  linters = list(error = "lintr package could not be loaded")
}
mlr-org/mlrCPO documentation built on Nov. 18, 2022, 11:25 p.m.