Module: RemLint::Extractors

Defined in:
lib/remlint/extractors.rb

Overview

Turns a file on disk into zero or more Source runs of Remind code.

A linter that only globs *.rem misses most real Remind code: the shipped examples/ansitext and examples/astro are shell scripts that pipe Remind in through heredocs, and astro alone holds four of them. Linting such a file as if it were Remind produces nothing but noise about the shell.

So extraction is its own layer, mirroring how RuboCop pulls Ruby out of ERB and Haml: each extractor either recognises the file and returns its Remind runs, or returns nil and lets the next one look.

Constant Summary collapse

HEREDOC_OPENER =

remind ... <<'EOF' / <<-EOF / << "EOF", with the flags and redirections in between ignored. The quoting around the delimiter is what stops the shell interpolating $Latitude before Remind sees it; both quoted and bare forms open a heredoc, so both are matched.

/\bremind\b[^\n<]*<<(?<dash>-?)\s*(?<quote>['"]?)(?<delimiter>\w+)\k<quote>/
REM_SHEBANG =

A file Remind is meant to read directly, either by extension or because it runs itself through Remind.

/\A#![^\n]*\bremind\b/
ALL =

Ordered most-specific-first: a .rem file is taken whole, and only a file that is not itself Remind gets searched for embedded heredocs.

[method(:rem_file), method(:shell_heredoc)].freeze

Class Method Summary collapse

Class Method Details

.extract(path, text) ⇒ Object

The first extractor that recognises the file wins.



51
52
53
# File 'lib/remlint/extractors.rb', line 51

def extract(path, text)
  ALL.lazy.filter_map { |extractor| extractor.call(path, text) }.first || []
end

.find_terminator(lines, from, delimiter, allow_indent) ⇒ Object

<<- lets the closing delimiter be indented; plain << demands it sit at column zero. An unterminated heredoc runs to end of file, which is what the shell would do too.



91
92
93
94
95
96
97
98
99
# File 'lib/remlint/extractors.rb', line 91

def find_terminator(lines, from, delimiter, allow_indent)
  cursor = from

  while cursor < lines.length && !terminator?(lines[cursor], delimiter, allow_indent)
    cursor += 1
  end

  cursor
end

.rem_file(path, text) ⇒ Object

A plain Remind file: the whole text, no offset.



30
31
32
33
34
# File 'lib/remlint/extractors.rb', line 30

def rem_file(path, text)
  if File.extname(path).casecmp?(".rem") || text.match?(REM_SHEBANG)
    [Source.new(path: path, text: text)]
  end
end

.scan_heredocs(path, text) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/remlint/extractors.rb', line 55

def scan_heredocs(path, text)
  lines = text.lines
  sources = []
  index = 0

  while index < lines.length
    match = lines[index].match(HEREDOC_OPENER)

    if match
      body_start = index + 1
      body_end = find_terminator(
        lines,
        body_start,
        match[:delimiter],
        match[:dash] == "-",
      )

      sources << Source.new(
        path:        path,
        text:        lines[body_start...body_end].join,
        line_offset: body_start,
        description: "heredoc at line #{index + 1}",
      )

      index = body_end
    end

    index += 1
  end

  sources
end

.shell_heredoc(path, text) ⇒ Object

Remind embedded in shell heredocs. Returns nil rather than an empty array when the file holds none, so the extractor chain keeps looking.



38
39
40
41
42
43
44
# File 'lib/remlint/extractors.rb', line 38

def shell_heredoc(path, text)
  sources = scan_heredocs(path, text)

  unless sources.empty?
    sources
  end
end

.terminator?(line, delimiter, allow_indent) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
109
# File 'lib/remlint/extractors.rb', line 101

def terminator?(line, delimiter, allow_indent)
  body = line.chomp

  if allow_indent
    body.sub(/\A\t+/, "") == delimiter
  else
    body == delimiter
  end
end