class Bashcov::Lexer

Simple lexer which analyzes Bash files in order to get information for coverage

Constants

IGNORE_END_WITH

Lines ending with one of these tokens are irrelevant for coverage

IGNORE_IS

Lines containing only one of these keywords are irrelevant for coverage

IGNORE_START_WITH

Lines starting with one of these tokens are irrelevant for coverage

Public Class Methods

new(filename, coverage) click to toggle source

@param [String] filename File to analyze @param [Hash] coverage Coverage with executed lines marked @raise [ArgumentError] if the given filename is invalid.

# File lib/bashcov/lexer.rb, line 21
def initialize(filename, coverage)
  @filename = filename
  @coverage = coverage

  raise ArgumentError, "#{@filename} is not a file" unless File.file?(@filename)
end

Public Instance Methods

complete_coverage() click to toggle source

Process and complete initial coverage. @return [void]

# File lib/bashcov/lexer.rb, line 30
def complete_coverage
  lines = File.read(@filename).encode("utf-8", invalid: :replace).lines

  lines.each_with_index do |line, lineno|
    # multi-line arrays
    mark_multiline(
      lines, lineno,
      /\A[^\n]*\b=\([^()]*\)/,
      forward: false
    )

    # heredoc
    mark_multiline(
      lines, lineno,
      /\A[^\n]+<<-?'?(\w+)'?.*$.*\1/m
    )

    # multiline string concatenated with backslashes
    mark_multiline(
      lines, lineno,
      /\A[^\n]+\\$(\s*['"][^'"]*['"]\s*\\$){1,}\s*['"][^'"]*['"]\s*$/
    )

    # simple line continuations with backslashes
    mark_multiline(
      lines, lineno,
      /\A([^\n&|;]*[^\\&|;](\\\\)*\\\n)+[^\n&|;]*[^\n\\&|;](\\\\)*$/
    )

    # multiline string concatenated with newlines
    %w[' "].each do |char|
      mark_multiline(
        lines, lineno,
        /\A[^\n]+[\s=]+#{char}[^#{char}]*#{char}/m,
        forward: false
      )
    end

    mark_line(line, lineno)
  end
end

Private Instance Methods

mark_line(line, lineno) click to toggle source
# File lib/bashcov/lexer.rb, line 92
def mark_line(line, lineno)
  return unless @coverage[lineno] == Bashcov::Line::IGNORED

  @coverage[lineno] = Bashcov::Line::UNCOVERED if relevant?(line)
end
mark_multiline(lines, lineno, regexp, forward: true) click to toggle source
# File lib/bashcov/lexer.rb, line 74
def mark_multiline(lines, lineno, regexp, forward: true)
  seek_forward = lines[lineno..].join
  return unless (multiline_match = seek_forward.match(regexp))

  length = multiline_match.to_s.count($/)
  first, last = lineno + 1, lineno + length
  range = (forward ? first.upto(last) : (last - 1).downto(first - 1))
  reference_lineno = (forward ? first - 1 : last)

  # don't seek backward if first line is already covered
  return if !forward && @coverage[first - 1]

  range.each do |sub_lineno|
    # mark related lines with the same coverage as the reference line
    @coverage[sub_lineno] ||= @coverage[reference_lineno]
  end
end
relevant?(line) click to toggle source
# File lib/bashcov/lexer.rb, line 98
def relevant?(line)
  line.sub!(/\s#.*\Z/, "") # remove comments
  line.strip!

  relevant = true

  relevant &= false if line.empty? ||
                       IGNORE_IS.include?(line) ||
                       line.start_with?(*IGNORE_START_WITH) ||
                       line.end_with?(*IGNORE_END_WITH)

  relevant &= false if line =~ /\A[a-zA-Z_][a-zA-Z0-9_:]*\(\)/ # function declared without the `function` keyword
  relevant &= false if line =~ /\A[^)]+\)\Z/ # case statement selector, e.g. `--help)`

  relevant
end