remlint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +328 -0
- data/config/default.yml +374 -0
- data/docs/remlint-rules.md +390 -0
- data/docs/remlint.md +238 -0
- data/exe/remlint +8 -0
- data/lib/remlint/cli.rb +300 -0
- data/lib/remlint/command.rb +229 -0
- data/lib/remlint/config.rb +255 -0
- data/lib/remlint/date_literal.rb +283 -0
- data/lib/remlint/document.rb +161 -0
- data/lib/remlint/expr_lexer.rb +197 -0
- data/lib/remlint/extractors.rb +210 -0
- data/lib/remlint/formatter.rb +96 -0
- data/lib/remlint/invocation.rb +145 -0
- data/lib/remlint/logical_line.rb +194 -0
- data/lib/remlint/offense.rb +117 -0
- data/lib/remlint/rule.rb +216 -0
- data/lib/remlint/rules/addomit_without_scanfrom.rb +189 -0
- data/lib/remlint/rules/advance_warning_body.rb +176 -0
- data/lib/remlint/rules/banner_placement.rb +126 -0
- data/lib/remlint/rules/calendar_text_limited.rb +125 -0
- data/lib/remlint/rules/callback_signature.rb +221 -0
- data/lib/remlint/rules/clause_needs_full_date.rb +176 -0
- data/lib/remlint/rules/clause_requires_at.rb +178 -0
- data/lib/remlint/rules/clause_value_range.rb +257 -0
- data/lib/remlint/rules/color_component_range.rb +295 -0
- data/lib/remlint/rules/coordinate_not_string.rb +175 -0
- data/lib/remlint/rules/dangling_continuation.rb +134 -0
- data/lib/remlint/rules/date_out_of_range.rb +180 -0
- data/lib/remlint/rules/debug_command.rb +189 -0
- data/lib/remlint/rules/easterdate_from_today.rb +160 -0
- data/lib/remlint/rules/function_arity.rb +385 -0
- data/lib/remlint/rules/function_redefinition.rb +159 -0
- data/lib/remlint/rules/generated_file_edited.rb +125 -0
- data/lib/remlint/rules/hebrew_date.rb +245 -0
- data/lib/remlint/rules/iftrig_with_satisfy.rb +101 -0
- data/lib/remlint/rules/include_path.rb +155 -0
- data/lib/remlint/rules/info_clause.rb +186 -0
- data/lib/remlint/rules/info_substitution_without_header.rb +169 -0
- data/lib/remlint/rules/invocation_mismatch.rb +223 -0
- data/lib/remlint/rules/keyword_case.rb +172 -0
- data/lib/remlint/rules/license_header.rb +101 -0
- data/lib/remlint/rules/line_length.rb +101 -0
- data/lib/remlint/rules/literal_type_mismatch.rb +324 -0
- data/lib/remlint/rules/localization_pack.rb +144 -0
- data/lib/remlint/rules/moon_phase_argument.rb +162 -0
- data/lib/remlint/rules/omit_aware_delta.rb +168 -0
- data/lib/remlint/rules/push_vars_missing_name.rb +158 -0
- data/lib/remlint/rules/repeat_trigger.rb +188 -0
- data/lib/remlint/rules/satisfy_constraint.rb +230 -0
- data/lib/remlint/rules/shell_maxlen.rb +170 -0
- data/lib/remlint/rules/shell_use_while_run_disabled.rb +172 -0
- data/lib/remlint/rules/string_escape.rb +158 -0
- data/lib/remlint/rules/syntax.rb +229 -0
- data/lib/remlint/rules/system_variable_assignment.rb +197 -0
- data/lib/remlint/rules/tag_syntax.rb +145 -0
- data/lib/remlint/rules/text_after_eof_marker.rb +134 -0
- data/lib/remlint/rules/time_zone_name.rb +212 -0
- data/lib/remlint/rules/tk_tag_namespace.rb +121 -0
- data/lib/remlint/rules/todo_complete_through.rb +122 -0
- data/lib/remlint/rules/trailing_whitespace.rb +116 -0
- data/lib/remlint/rules/translate_command.rb +203 -0
- data/lib/remlint/rules/unbalanced_blocks.rb +305 -0
- data/lib/remlint/rules/unbalanced_delimiters.rb +251 -0
- data/lib/remlint/rules/unknown_special_type.rb +155 -0
- data/lib/remlint/rules/unknown_substitution_sequence.rb +255 -0
- data/lib/remlint/rules/unknown_system_variable.rb +145 -0
- data/lib/remlint/rules/unquoted_shell_substitution.rb +266 -0
- data/lib/remlint/rules/until_before_from.rb +204 -0
- data/lib/remlint/rules/world_writable_script.rb +128 -0
- data/lib/remlint/rules.rb +60 -0
- data/lib/remlint/runner.rb +274 -0
- data/lib/remlint/source.rb +36 -0
- data/lib/remlint/tables.rb +395 -0
- data/lib/remlint/trigger.rb +359 -0
- data/lib/remlint/version.rb +5 -0
- data/lib/remlint/vocabulary.rb +236 -0
- data/lib/remlint.rb +35 -0
- data/tasks/generate_tables.rb +175 -0
- metadata +170 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "source"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
# One Remind command, after backslash continuations have been joined.
|
|
7
|
+
#
|
|
8
|
+
# `text` is byte-for-byte what Remind's own LineBuffer would hold: each
|
|
9
|
+
# continuing backslash replaced by a newline and the following line appended
|
|
10
|
+
# verbatim (src/files.c ReadLineFromFile). Keeping the newline rather than
|
|
11
|
+
# collapsing to a space costs nothing -- Remind's tokeniser treats it as
|
|
12
|
+
# whitespace either way -- and buys exact column arithmetic, because every
|
|
13
|
+
# character of every continued line still sits where it sat in the file.
|
|
14
|
+
#
|
|
15
|
+
# `line` is the physical line the command starts on, in the enclosing file;
|
|
16
|
+
# `physical_lines` lists every line it spans; `raw` keeps the untouched bytes,
|
|
17
|
+
# because the whitespace rules care about what the join threw away.
|
|
18
|
+
LogicalLine = Struct.new(
|
|
19
|
+
:text,
|
|
20
|
+
:line,
|
|
21
|
+
:physical_lines,
|
|
22
|
+
:raw,
|
|
23
|
+
keyword_init: true,
|
|
24
|
+
) do
|
|
25
|
+
def continued?
|
|
26
|
+
physical_lines.length > 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def last_line
|
|
30
|
+
physical_lines.last
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The command on one line, for a message or for a rule that would rather
|
|
34
|
+
# not think about continuations at all.
|
|
35
|
+
def squashed
|
|
36
|
+
text.tr("\n", " ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Map a character offset in `text` back to a physical [line, column] in the
|
|
40
|
+
# file. Column is 1-based, so `position_at(0)` is the first column of the
|
|
41
|
+
# line the command starts on.
|
|
42
|
+
def position_at(offset)
|
|
43
|
+
before = text[0, offset].to_s
|
|
44
|
+
crossed = before.count("\n")
|
|
45
|
+
last_break = before.rindex("\n")
|
|
46
|
+
|
|
47
|
+
if last_break
|
|
48
|
+
column = offset - last_break
|
|
49
|
+
else
|
|
50
|
+
column = offset + 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
[physical_lines.fetch(crossed, last_line), column]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Joins backslash-continued physical lines into logical ones.
|
|
58
|
+
#
|
|
59
|
+
# This is where a whole bug class lives. Remind continues a line only on a
|
|
60
|
+
# backslash that is the very last character; a backslash followed by a space
|
|
61
|
+
# is just a backslash, and the long `IIF(...)` chains in `defs.rem` and
|
|
62
|
+
# `astro` are held together by nothing else. Joining here -- rather than
|
|
63
|
+
# letting each rule guess -- means every rule sees the command Remind sees,
|
|
64
|
+
# and "backslash that does not continue" becomes something DanglingContinuation
|
|
65
|
+
# can report rather than something that silently splits a command in half.
|
|
66
|
+
module Joiner
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
def call(source)
|
|
70
|
+
buffer = nil
|
|
71
|
+
|
|
72
|
+
[].tap do |logical|
|
|
73
|
+
source.lines.each_with_index do |raw, index|
|
|
74
|
+
line_no = index + 1 + source.line_offset
|
|
75
|
+
body = raw.chomp
|
|
76
|
+
|
|
77
|
+
if buffer
|
|
78
|
+
buffer.text << body
|
|
79
|
+
buffer.physical_lines << line_no
|
|
80
|
+
buffer.raw << raw
|
|
81
|
+
else
|
|
82
|
+
buffer = LogicalLine.new(
|
|
83
|
+
text: +body,
|
|
84
|
+
line: line_no,
|
|
85
|
+
physical_lines: [line_no],
|
|
86
|
+
raw: +raw,
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if buffer.text.end_with?("\\")
|
|
91
|
+
# Remind overwrites the backslash with the newline it just consumed.
|
|
92
|
+
buffer.text[-1] = "\n"
|
|
93
|
+
else
|
|
94
|
+
logical << buffer
|
|
95
|
+
buffer = nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A file whose last line ends in a backslash leaves a command open.
|
|
100
|
+
# Emit it anyway, so the rules can see -- and complain about -- it.
|
|
101
|
+
if buffer
|
|
102
|
+
logical << buffer
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
__END__
|
|
110
|
+
|
|
111
|
+
describe "line joining" do
|
|
112
|
+
join = proc do |text, line_offset = 0|
|
|
113
|
+
RemLint::Joiner.call(RemLint::Source.new(path: "t.rem", text: text, line_offset: line_offset))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe "RemLint::Joiner" do
|
|
117
|
+
it "leaves ordinary lines alone" do
|
|
118
|
+
lines = join.("REM 1 Jan MSG New Year\nREM 2 Jan MSG Day two\n")
|
|
119
|
+
|
|
120
|
+
lines.length.should == 2
|
|
121
|
+
lines.map(&:text).should == ["REM 1 Jan MSG New Year", "REM 2 Jan MSG Day two"]
|
|
122
|
+
lines.map(&:line).should == [1, 2]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "joins a continued command the way Remind's LineBuffer does" do
|
|
126
|
+
lines = join.("SET x IIF(a, \\\n b, \\\n c)\n")
|
|
127
|
+
|
|
128
|
+
lines.length.should == 1
|
|
129
|
+
lines.first.text.should == "SET x IIF(a, \n b, \n c)"
|
|
130
|
+
lines.first.squashed.should == "SET x IIF(a, b, c)"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "records every physical line a joined command spans" do
|
|
134
|
+
line = join.("SET x 1 + \\\n2 + \\\n3\n").first
|
|
135
|
+
|
|
136
|
+
line.line.should == 1
|
|
137
|
+
line.last_line.should == 3
|
|
138
|
+
line.physical_lines.should == [1, 2, 3]
|
|
139
|
+
line.should.be.continued
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "keeps the raw bytes of every joined line" do
|
|
143
|
+
join.("SET x 1 + \\\n 2\n").first.raw.should == "SET x 1 + \\\n 2\n"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "does not join when the backslash is followed by whitespace" do
|
|
147
|
+
# Remind continues only on a backslash in the final column. This is the
|
|
148
|
+
# bug DanglingContinuation exists to report.
|
|
149
|
+
lines = join.("SET x 1 + \\ \nSET y 2\n")
|
|
150
|
+
|
|
151
|
+
lines.length.should == 2
|
|
152
|
+
lines.first.text.should == "SET x 1 + \\ "
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "emits a command left open at end of file" do
|
|
156
|
+
lines = join.("SET x 1 + \\\n")
|
|
157
|
+
|
|
158
|
+
lines.length.should == 1
|
|
159
|
+
lines.first.text.should == "SET x 1 + \n"
|
|
160
|
+
lines.first.raw.should.match(/\\\n\z/)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "shifts every line number by the source's offset" do
|
|
164
|
+
join.("MSG one\nMSG two\n", 40).map(&:line).should == [41, 42]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "handles an empty source" do
|
|
168
|
+
join.("").should.be.empty
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
describe "RemLint::LogicalLine#position_at" do
|
|
173
|
+
it "reports a 1-based column on an unjoined line" do
|
|
174
|
+
line = join.("SET x 12345\n").first
|
|
175
|
+
|
|
176
|
+
line.position_at(0).should == [1, 1]
|
|
177
|
+
line.position_at(4).should == [1, 5]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "walks onto the physical line the offset actually landed on" do
|
|
181
|
+
line = join.("SET x 1 + \\\n 22 + \\\n 333\n").first
|
|
182
|
+
|
|
183
|
+
# The joined text is "SET x 1 + \n 22 + \n 333".
|
|
184
|
+
line.position_at(line.text.index("22")).should == [2, 5]
|
|
185
|
+
line.position_at(line.text.index("333")).should == [3, 5]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "carries the source offset into reported positions" do
|
|
189
|
+
line = join.("SET x 1 + \\\n 22\n", 40).first
|
|
190
|
+
|
|
191
|
+
line.position_at(line.text.index("22")).should == [42, 5]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RemLint
|
|
4
|
+
# Ordered worst-first, so a run that reports only the top N reports the N
|
|
5
|
+
# that matter.
|
|
6
|
+
#
|
|
7
|
+
# Defined out here rather than inside the Struct body below: a constant
|
|
8
|
+
# assigned inside a block belongs to the lexical scope around the block, not
|
|
9
|
+
# to the class the block configures, so `Offense::SEVERITIES` would not have
|
|
10
|
+
# existed.
|
|
11
|
+
SEVERITIES = %w[error warning info].freeze
|
|
12
|
+
|
|
13
|
+
# One thing a rule found, at one place in one file.
|
|
14
|
+
#
|
|
15
|
+
# The default rendering is `path:line:column: severity: [Rule] message`,
|
|
16
|
+
# which vim's default `errorformat` reads, which GitHub Actions' problem
|
|
17
|
+
# matchers read, and which `sort` orders correctly. The format is also
|
|
18
|
+
# configurable, following puppet-lint, because the one thing every CI system
|
|
19
|
+
# agrees on is that it wants a slightly different format.
|
|
20
|
+
Offense = Struct.new(
|
|
21
|
+
:path,
|
|
22
|
+
:line,
|
|
23
|
+
:column,
|
|
24
|
+
:severity,
|
|
25
|
+
:rule,
|
|
26
|
+
:message,
|
|
27
|
+
keyword_init: true,
|
|
28
|
+
) do
|
|
29
|
+
def initialize(path:, line:, rule:, message:, column: 1, severity: "warning")
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def severity_rank
|
|
34
|
+
SEVERITIES.index(severity) || SEVERITIES.length
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error?
|
|
38
|
+
severity == "error"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# At or above the given severity, where "above" means closer to error.
|
|
42
|
+
def at_least?(level)
|
|
43
|
+
severity_rank <= (SEVERITIES.index(level) || SEVERITIES.length)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_s
|
|
47
|
+
format_with("%{path}:%{line}:%{column}: %{severity}: [%{rule}] %{message}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `%{path}`, `%{line}`, `%{column}`, `%{severity}`, `%{rule}`, `%{message}`.
|
|
51
|
+
def format_with(template)
|
|
52
|
+
format(
|
|
53
|
+
template,
|
|
54
|
+
path: path,
|
|
55
|
+
line: line,
|
|
56
|
+
column: column,
|
|
57
|
+
severity: severity,
|
|
58
|
+
rule: rule,
|
|
59
|
+
message: message,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# File, then position, then severity: the order someone reads them in.
|
|
64
|
+
def sort_key
|
|
65
|
+
[path, line, column, severity_rank, rule]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
__END__
|
|
71
|
+
|
|
72
|
+
describe "RemLint::Offense" do
|
|
73
|
+
offense = proc do |overrides = {}|
|
|
74
|
+
defaults = {
|
|
75
|
+
path: "holidays.rem",
|
|
76
|
+
line: 12,
|
|
77
|
+
column: 4,
|
|
78
|
+
rule: "TrailingWhitespace",
|
|
79
|
+
message: "Trailing whitespace",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
RemLint::Offense.new(**defaults.merge(overrides))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "renders in a format vim's errorformat reads" do
|
|
86
|
+
offense.().to_s.should == "holidays.rem:12:4: warning: [TrailingWhitespace] Trailing whitespace"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "defaults to column 1 and warning severity" do
|
|
90
|
+
bare = RemLint::Offense.new(path: "a.rem", line: 1, rule: "R", message: "m")
|
|
91
|
+
|
|
92
|
+
bare.column.should == 1
|
|
93
|
+
bare.severity.should == "warning"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "renders through a caller-supplied template" do
|
|
97
|
+
offense.().format_with("%{line}: %{message}").should == "12: Trailing whitespace"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "knows whether it meets a severity threshold" do
|
|
101
|
+
offense.(severity: "error").should.be.error
|
|
102
|
+
offense.(severity: "error").at_least?("warning").should.be.true
|
|
103
|
+
offense.(severity: "warning").at_least?("error").should.be.false
|
|
104
|
+
offense.(severity: "info").at_least?("warning").should.be.false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "sorts by file, then position, then severity" do
|
|
108
|
+
unsorted = [
|
|
109
|
+
offense.(line: 12, column: 9),
|
|
110
|
+
offense.(line: 3),
|
|
111
|
+
offense.(path: "a.rem", line: 99),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
unsorted.sort_by(&:sort_key).map { |o| [o.path, o.line, o.column] }.should ==
|
|
115
|
+
[["a.rem", 99, 4], ["holidays.rem", 3, 4], ["holidays.rem", 12, 9]]
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/remlint/rule.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "offense"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
# Base class for rules. Subclassing registers.
|
|
7
|
+
#
|
|
8
|
+
# A rule implements `check` and calls `offend` for each thing it finds. It
|
|
9
|
+
# gets one {Document} and its own slice of configuration, and it is
|
|
10
|
+
# instantiated fresh per document, so a rule accumulating state across a file
|
|
11
|
+
# (block stacks, FSET definitions) does not have to clean up after itself.
|
|
12
|
+
#
|
|
13
|
+
# Registry-by-inheritance rather than an explicit list, following haml-lint:
|
|
14
|
+
# a new file under `rules/` that is required is a rule that runs, with
|
|
15
|
+
# nothing else to remember to edit.
|
|
16
|
+
class Rule
|
|
17
|
+
# Guarded, not plain: `scampi` runs each file's co-located specs by
|
|
18
|
+
# `load`ing the file, which re-executes its top level. An unguarded
|
|
19
|
+
# `REGISTRY = []` would empty the registry halfway through a run and take
|
|
20
|
+
# every already-registered rule with it -- the subclasses are not
|
|
21
|
+
# re-registered, because reopening a class does not call `inherited` again.
|
|
22
|
+
unless defined?(REGISTRY)
|
|
23
|
+
REGISTRY = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def inherited(subclass)
|
|
28
|
+
super
|
|
29
|
+
REGISTRY << subclass
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `RemLint::Rules::TrailingWhitespace` configures and reports as
|
|
33
|
+
# `TrailingWhitespace`. Anonymous subclasses -- which specs make, and
|
|
34
|
+
# which land in the registry like any other -- get a placeholder rather
|
|
35
|
+
# than blowing up every caller that walks the registry.
|
|
36
|
+
def rule_name
|
|
37
|
+
if name
|
|
38
|
+
name.split("::").last
|
|
39
|
+
else
|
|
40
|
+
"(anonymous)"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Overridden by a rule that should be off unless asked for. Rules that
|
|
45
|
+
# enforce a house preference rather than report a defect default to off,
|
|
46
|
+
# because a linter whose first run is a wall of style noise gets turned
|
|
47
|
+
# off entirely.
|
|
48
|
+
def enabled_by_default?
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def default_severity
|
|
53
|
+
"warning"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# One line, shown by `remlint --show-rules` and used as the doc comment
|
|
57
|
+
# in the generated default config.
|
|
58
|
+
def description
|
|
59
|
+
""
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Only named subclasses. A rule with no name cannot be configured, named
|
|
63
|
+
# in a `remlint:disable` comment, or reported usefully, so it is not one
|
|
64
|
+
# the runner can run -- which is also what keeps the anonymous classes a
|
|
65
|
+
# spec makes from turning up in someone else's lint run.
|
|
66
|
+
def all
|
|
67
|
+
REGISTRY.reject { |rule| rule.name.nil? }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find(rule_name)
|
|
71
|
+
all.find { |rule| rule.rule_name == rule_name }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
attr_reader :config, :document, :offenses
|
|
76
|
+
|
|
77
|
+
def initialize(config = {})
|
|
78
|
+
@config = config
|
|
79
|
+
@offenses = []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def run(document)
|
|
83
|
+
@document = document
|
|
84
|
+
@offenses = []
|
|
85
|
+
check
|
|
86
|
+
offenses
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def rule_name
|
|
90
|
+
self.class.rule_name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Subclasses implement this.
|
|
94
|
+
def check
|
|
95
|
+
raise NotImplementedError, "#{self.class} must implement #check"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# `severity:` overrides the configured level for one offence. Only one
|
|
101
|
+
# rule needs it: Syntax, which relays diagnostics Remind has already
|
|
102
|
+
# graded for itself and should not promote a warning to an error.
|
|
103
|
+
def offend(line, message, column: 1, severity: nil)
|
|
104
|
+
@offenses << Offense.new(
|
|
105
|
+
path: document.path,
|
|
106
|
+
line: line,
|
|
107
|
+
column: column,
|
|
108
|
+
severity: severity || configured_severity,
|
|
109
|
+
rule: rule_name,
|
|
110
|
+
message: message,
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Report at a character offset into a logical line, letting the line work
|
|
115
|
+
# out which physical line and column that lands on. This is what a rule
|
|
116
|
+
# working from the token stream wants: it keeps offences inside a
|
|
117
|
+
# continued command pointing at the continuation they are actually on.
|
|
118
|
+
def offend_at(logical_line, offset, message)
|
|
119
|
+
line, column = logical_line.position_at(offset)
|
|
120
|
+
|
|
121
|
+
offend(line, message, column: column)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def configured_severity
|
|
125
|
+
config.fetch("Severity", self.class.default_severity)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def option(key, default)
|
|
129
|
+
config.fetch(key, default)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
__END__
|
|
135
|
+
|
|
136
|
+
require_relative "document"
|
|
137
|
+
require_relative "rules/trailing_whitespace"
|
|
138
|
+
|
|
139
|
+
describe "RemLint::Rule" do
|
|
140
|
+
# Defined inside the spec so the registry assertions have something of their
|
|
141
|
+
# own to find, rather than depending on which real rules happen to be loaded.
|
|
142
|
+
example = Class.new(RemLint::Rule) do
|
|
143
|
+
def self.rule_name = "ExampleRule"
|
|
144
|
+
|
|
145
|
+
def check
|
|
146
|
+
document.code_commands.each do |command|
|
|
147
|
+
offend(command.line, "saw #{command.keyword&.name}", column: command.keyword_column)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
document = proc do |text|
|
|
153
|
+
RemLint::Document.new(RemLint::Source.new(path: "t.rem", text: text))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "registers every subclass" do
|
|
157
|
+
RemLint::Rule::REGISTRY.should.include example
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "keeps anonymous subclasses out of the runnable set" do
|
|
161
|
+
# A rule with no name cannot be configured or reported by name, and the
|
|
162
|
+
# ones a spec makes must not leak into anyone else's run.
|
|
163
|
+
RemLint::Rule.all.should.not.include example
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "collects the offences a check reports" do
|
|
167
|
+
offenses = example.new.run(document.("# note\nMSG hi\n"))
|
|
168
|
+
|
|
169
|
+
offenses.length.should == 1
|
|
170
|
+
offenses.first.line.should == 2
|
|
171
|
+
offenses.first.message.should == "saw MSG"
|
|
172
|
+
offenses.first.rule.should == "ExampleRule"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "starts empty on each run rather than accumulating" do
|
|
176
|
+
rule = example.new
|
|
177
|
+
rule.run(document.("MSG hi\n"))
|
|
178
|
+
rule.run(document.("MSG hi\n"))
|
|
179
|
+
|
|
180
|
+
rule.offenses.length.should == 1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "takes severity from configuration, falling back to the rule's default" do
|
|
184
|
+
example.new.run(document.("MSG hi\n")).first.severity.should == "warning"
|
|
185
|
+
example.new("Severity" => "error").run(document.("MSG hi\n")).first.severity.should == "error"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "insists a subclass implements check" do
|
|
189
|
+
incomplete = Class.new(RemLint::Rule)
|
|
190
|
+
|
|
191
|
+
lambda { incomplete.new.run(document.("MSG hi\n")) }.should.raise NotImplementedError
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "finds a named rule and nothing else" do
|
|
195
|
+
RemLint::Rule.find("TrailingWhitespace").rule_name.should == "TrailingWhitespace"
|
|
196
|
+
RemLint::Rule.find("ExampleRule").should.be.nil
|
|
197
|
+
RemLint::Rule.find("NoSuchRule").should.be.nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "reports an offset inside a continued command on the right physical line" do
|
|
201
|
+
positional = Class.new(RemLint::Rule) do
|
|
202
|
+
def self.rule_name = "Positional"
|
|
203
|
+
|
|
204
|
+
def check
|
|
205
|
+
line = document.logical_lines.first
|
|
206
|
+
|
|
207
|
+
offend_at(line, line.text.index("boom"), "found it")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
offense = positional.new.run(document.("SET x 1 + \\\n boom\n")).first
|
|
212
|
+
|
|
213
|
+
offense.line.should == 2
|
|
214
|
+
offense.column.should == 5
|
|
215
|
+
end
|
|
216
|
+
end
|