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,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Numeric clause arguments outside the range Remind accepts.
|
|
8
|
+
#
|
|
9
|
+
# Four clauses take a bounded number and all four bounds are in the C:
|
|
10
|
+
#
|
|
11
|
+
# AT hh:mm 0-23 and 0-59
|
|
12
|
+
# DURATION hh:mm minutes 0-59; the hour is unbounded
|
|
13
|
+
# PRIORITY n 0-9999 (`ParsePriority`, src/dorem.c)
|
|
14
|
+
# MAX-OVERDUE n days past due, so a positive count
|
|
15
|
+
#
|
|
16
|
+
# Remind rejects each of them, but only when the line is reached. For a
|
|
17
|
+
# reminder that is the day it triggers, which for an annual one is up to a
|
|
18
|
+
# year after the typo was made.
|
|
19
|
+
#
|
|
20
|
+
# `DURATION` deliberately has no hour ceiling. A duration is a *length*,
|
|
21
|
+
# not a time of day: Remind's own `tests/test3.rem` writes
|
|
22
|
+
# `DURATION 24:45` and `DURATION 48:45` for events running over more than
|
|
23
|
+
# one day, and Remind accepts them. Only the minutes are bounded.
|
|
24
|
+
#
|
|
25
|
+
# `PRIORITY -1` is worth a word: `ParsePriority` starts with `isdigit`, so
|
|
26
|
+
# a minus sign is `Expecting number` rather than an out-of-range value, and
|
|
27
|
+
# the message says so rather than talking about the range.
|
|
28
|
+
class ClauseValueRange < Rule
|
|
29
|
+
MAX_HOUR = 23
|
|
30
|
+
MAX_MINUTE = 59
|
|
31
|
+
MAX_PRIORITY = 9999
|
|
32
|
+
|
|
33
|
+
# Only `AT` is a time of day, so only `AT` has an hour ceiling.
|
|
34
|
+
BOUNDED_HOUR = %w[AT].freeze
|
|
35
|
+
|
|
36
|
+
TIME_CLAUSES = %w[AT DURATION].freeze
|
|
37
|
+
|
|
38
|
+
# `15:00`, `15.00` -- Remind takes either separator.
|
|
39
|
+
TIME = /\A(?<hour>\d{1,2})[:.](?<minute>\d{1,2})\z/i
|
|
40
|
+
|
|
41
|
+
AM_PM = /\A(?<hour>\d{1,2})([:.](?<minute>\d{1,2}))?\s*(?<half>am|pm)\z/i
|
|
42
|
+
|
|
43
|
+
def self.default_severity
|
|
44
|
+
"error"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.description
|
|
48
|
+
"An AT, DURATION, PRIORITY or MAX-OVERDUE value outside its range."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def check
|
|
52
|
+
document.code_commands.each do |command|
|
|
53
|
+
trigger = document.trigger_for(command)
|
|
54
|
+
|
|
55
|
+
TIME_CLAUSES.each { |name| check_time(command, trigger.find(name)) }
|
|
56
|
+
check_priority(command, trigger.find("PRIORITY"))
|
|
57
|
+
check_max_overdue(command, trigger.find("MAX-OVERDUE"))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# The literal word after a clause keyword, or nil when the argument is
|
|
64
|
+
# computed and so not this rule's to judge.
|
|
65
|
+
def argument(command, clause)
|
|
66
|
+
if clause
|
|
67
|
+
rest = command.text[clause.end_offset..].to_s
|
|
68
|
+
|
|
69
|
+
rest.strip[/\A\S+/]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_time(command, clause)
|
|
74
|
+
text = argument(command, clause)
|
|
75
|
+
match = text && (text.match(TIME) || text.match(AM_PM))
|
|
76
|
+
|
|
77
|
+
if match
|
|
78
|
+
report_time(command, clause, match)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def report_time(command, clause, match)
|
|
83
|
+
hour = match[:hour].to_i
|
|
84
|
+
minute = match[:minute].to_i
|
|
85
|
+
if am_pm?(match)
|
|
86
|
+
limit = 12
|
|
87
|
+
else
|
|
88
|
+
limit = MAX_HOUR
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if BOUNDED_HOUR.include?(clause.name) && hour > limit
|
|
92
|
+
offend_at(command.logical_line, clause.offset, hour_message(clause, hour, limit))
|
|
93
|
+
elsif minute > MAX_MINUTE
|
|
94
|
+
offend_at(command.logical_line, clause.offset, minute_message(clause, minute))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def am_pm?(match)
|
|
99
|
+
match.names.include?("half") && !match[:half].nil?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def hour_message(clause, hour, limit)
|
|
103
|
+
"`#{clause.name} #{hour}:...` has an hour of #{hour}; Remind accepts 0 to #{limit}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def minute_message(clause, minute)
|
|
107
|
+
"`#{clause.name}` has a minute of #{minute}; Remind accepts 0 to #{MAX_MINUTE}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def check_priority(command, clause)
|
|
111
|
+
text = argument(command, clause)
|
|
112
|
+
|
|
113
|
+
if text&.match?(/\A-\d+\z/)
|
|
114
|
+
offend_at(
|
|
115
|
+
command.logical_line,
|
|
116
|
+
clause.offset,
|
|
117
|
+
"`PRIORITY #{text}` is rejected as `Expecting number`; " \
|
|
118
|
+
"ParsePriority reads digits only, so a minus never reaches the range check",
|
|
119
|
+
)
|
|
120
|
+
elsif text&.match?(/\A\d+\z/) && text.to_i > MAX_PRIORITY
|
|
121
|
+
offend_at(
|
|
122
|
+
command.logical_line,
|
|
123
|
+
clause.offset,
|
|
124
|
+
"`PRIORITY #{text}` is outside 0 to #{MAX_PRIORITY}",
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_max_overdue(command, clause)
|
|
130
|
+
text = argument(command, clause)
|
|
131
|
+
|
|
132
|
+
if text&.match?(/\A-?\d+\z/) && text.to_i < 1
|
|
133
|
+
offend_at(
|
|
134
|
+
command.logical_line,
|
|
135
|
+
clause.offset,
|
|
136
|
+
"`MAX-OVERDUE #{text}` counts days past the due date, so it has to be " \
|
|
137
|
+
"positive; #{text} switches the nagging off rather than configuring it",
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
__END__
|
|
146
|
+
|
|
147
|
+
require_relative "../document"
|
|
148
|
+
|
|
149
|
+
describe "RemLint::Rules::ClauseValueRange" do
|
|
150
|
+
lint = proc do |text|
|
|
151
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
152
|
+
|
|
153
|
+
RemLint::Rules::ClauseValueRange.new.run(RemLint::Document.new(source))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
157
|
+
|
|
158
|
+
describe "AT times" do
|
|
159
|
+
it "accepts a valid time" do
|
|
160
|
+
lint.("REM Tue AT 15:00 MSG hi\n").should.be.empty
|
|
161
|
+
lint.("REM Tue AT 0:00 MSG hi\n").should.be.empty
|
|
162
|
+
lint.("REM Tue AT 23:59 MSG hi\n").should.be.empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "accepts the dot separator" do
|
|
166
|
+
lint.("REM Tue AT 15.00 MSG hi\n").should.be.empty
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "reports an hour past 23" do
|
|
170
|
+
messages.("REM Tue AT 25:00 MSG hi\n").first.should.match(/an hour of 25; Remind accepts 0 to 23/)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "reports a minute past 59" do
|
|
174
|
+
messages.("REM Tue AT 9:70 MSG hi\n").first.should.match(/a minute of 70/)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "accepts an am/pm time" do
|
|
178
|
+
lint.("REM Tue AT 3:00PM MSG hi\n").should.be.empty
|
|
179
|
+
lint.("REM Tue AT 11:59am MSG hi\n").should.be.empty
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "reports an am/pm hour past 12" do
|
|
183
|
+
messages.("REM Tue AT 13:00PM MSG hi\n").first.should.match(/Remind accepts 0 to 12/)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "DURATION" do
|
|
188
|
+
it "accepts a valid duration" do
|
|
189
|
+
lint.("REM Tue AT 15:00 DURATION 1:30 MSG hi\n").should.be.empty
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "reports a minute past 59" do
|
|
193
|
+
messages.("REM Tue AT 15:00 DURATION 1:75 MSG hi\n").first.should.match(/`DURATION`/)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "accepts an hour past 23, because a duration is a length" do
|
|
197
|
+
# tests/test3.rem writes DURATION 24:45 and DURATION 48:45; Remind
|
|
198
|
+
# accepts both -- the event simply runs over more than one day.
|
|
199
|
+
lint.("REM Tue AT 11:00 DURATION 24:45 MSG hi\n").should.be.empty
|
|
200
|
+
lint.("REM Tue AT 11:00 DURATION 48:45 MSG hi\n").should.be.empty
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
describe "PRIORITY" do
|
|
205
|
+
it "accepts the range" do
|
|
206
|
+
lint.("REM Tue PRIORITY 0 MSG hi\n").should.be.empty
|
|
207
|
+
lint.("REM Tue PRIORITY 9999 MSG hi\n").should.be.empty
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "reports a value past 9999" do
|
|
211
|
+
messages.("REM Tue PRIORITY 10000 MSG hi\n").first.should ==
|
|
212
|
+
"`PRIORITY 10000` is outside 0 to 9999"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "explains that a negative is a parse error, not a range one" do
|
|
216
|
+
messages.("REM Tue PRIORITY -1 MSG hi\n").first.should.match(
|
|
217
|
+
/rejected as `Expecting number`/,
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe "MAX-OVERDUE" do
|
|
223
|
+
it "accepts a positive count" do
|
|
224
|
+
lint.("REM Tue MAX-OVERDUE 5 MSG hi\n").should.be.empty
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "reports zero" do
|
|
228
|
+
messages.("REM Tue MAX-OVERDUE 0 MSG hi\n").first.should.match(/has to be positive/)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it "reports a negative" do
|
|
232
|
+
messages.("REM Tue MAX-OVERDUE -3 MSG hi\n").length.should == 1
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
describe "arguments it cannot judge" do
|
|
237
|
+
it "says nothing about a computed time" do
|
|
238
|
+
lint.("REM Tue AT [sunset()] MSG hi\n").should.be.empty
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "says nothing about a computed priority" do
|
|
242
|
+
lint.("REM Tue PRIORITY [p] MSG hi\n").should.be.empty
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "does not read a time out of a message body" do
|
|
247
|
+
lint.("REM Tue MSG Meeting at 25:00 in the old money\n").should.be.empty
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it "says nothing about comments" do
|
|
251
|
+
lint.("# REM Tue AT 25:00 MSG hi\n").should.be.empty
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "reports at error severity" do
|
|
255
|
+
lint.("REM Tue AT 25:00 MSG hi\n").first.severity.should == "error"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Colour components outside 0-255.
|
|
8
|
+
#
|
|
9
|
+
# Remind rejects them -- `if (r > 255 || g > 255 || b > 255) return E_2HIGH`
|
|
10
|
+
# in src/funcs.c for `ansicolor`, and the same bound in src/var.c for
|
|
11
|
+
# `$DefaultColor` -- but only when the line runs, and `rem2ps` silently
|
|
12
|
+
# clamps instead (src/rem2ps.c). A component typed as `2555` therefore
|
|
13
|
+
# renders as something plausible in one output mode and fails in another,
|
|
14
|
+
# which is the worst way for a mistake to behave.
|
|
15
|
+
#
|
|
16
|
+
# Three places carry components, and all three are hand-typed integers in
|
|
17
|
+
# practice, which is exactly when a linter can help:
|
|
18
|
+
#
|
|
19
|
+
# SPECIAL COLOR r g b <text> examples/astro has sixteen of these
|
|
20
|
+
# SPECIAL SHADE r g b tests/shade.rem has one per weekday
|
|
21
|
+
# [ansicolor(r, g, b)] examples/ansitext, examples/alignment
|
|
22
|
+
#
|
|
23
|
+
# `ansicolor` also takes a one-argument form (`ansicolor("")`, the reset)
|
|
24
|
+
# and an optional fourth background flag; only the three components are
|
|
25
|
+
# range-checked, and only when they are literal integers.
|
|
26
|
+
class ColorComponentRange < Rule
|
|
27
|
+
MINIMUM = 0
|
|
28
|
+
MAXIMUM = 255
|
|
29
|
+
|
|
30
|
+
# `COLOR`, and the spelling Remind also accepts.
|
|
31
|
+
SPECIAL_KINDS = %w[COLOR COLOUR SHADE].freeze
|
|
32
|
+
|
|
33
|
+
# `SPECIAL COLOR 255 128 0 rest...` -- three integers after the keyword.
|
|
34
|
+
SPECIAL = /\A(?<kind>[A-Za-z]+)\s+(?<r>-?\d+)\s+(?<g>-?\d+)\s+(?<b>-?\d+)\b/
|
|
35
|
+
|
|
36
|
+
CHANNELS = %w[red green blue].freeze
|
|
37
|
+
|
|
38
|
+
def self.default_severity
|
|
39
|
+
"error"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.description
|
|
43
|
+
"Colour components outside the 0-255 Remind accepts."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check
|
|
47
|
+
document.code_commands.each do |command|
|
|
48
|
+
check_special(command)
|
|
49
|
+
check_ansicolor(command)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def check_special(command)
|
|
56
|
+
match = special_components(command)
|
|
57
|
+
|
|
58
|
+
if match
|
|
59
|
+
report_components(command, match[:kind].upcase, [match[:r], match[:g], match[:b]])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The components sit after `SPECIAL`, which may itself be preceded by a
|
|
64
|
+
# whole trigger (`REM [moondatetime(0)] +60 SPECIAL COLOR ...`), so the
|
|
65
|
+
# keyword is found in the text rather than assumed to open the line.
|
|
66
|
+
def special_components(command)
|
|
67
|
+
rest = command.text[/\bSPECIAL\s+(.*)\z/mi, 1]
|
|
68
|
+
match = rest&.match(SPECIAL)
|
|
69
|
+
|
|
70
|
+
if match && SPECIAL_KINDS.include?(match[:kind].upcase)
|
|
71
|
+
match
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_ansicolor(command)
|
|
76
|
+
logical_line = command.logical_line
|
|
77
|
+
tokens = document.tokens_for(logical_line)
|
|
78
|
+
|
|
79
|
+
tokens.each_with_index do |token, index|
|
|
80
|
+
if token.type == :function && token.value.casecmp?("ansicolor")
|
|
81
|
+
report_components(command, "ansicolor", literal_arguments(tokens, index))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The first three arguments, and only when each is an integer literal.
|
|
87
|
+
# `ansicolor("")` and `ansicolor(r, g, b)` with computed components
|
|
88
|
+
# both yield nothing to check.
|
|
89
|
+
#
|
|
90
|
+
# The arguments are split on top-level commas rather than picked out by
|
|
91
|
+
# position, because the lexer emits a minus sign as its own token:
|
|
92
|
+
# `ansicolor(-1, 0, 0)` reads as `-`, `1`, `,`, ... and taking the
|
|
93
|
+
# numbers alone would see a perfectly in-range 1.
|
|
94
|
+
def literal_arguments(tokens, index)
|
|
95
|
+
split_arguments(tokens, index + 1).first(3).filter_map { |group| integer(group) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def split_arguments(tokens, open_index)
|
|
99
|
+
groups = [[]]
|
|
100
|
+
depth = 0
|
|
101
|
+
cursor = open_index
|
|
102
|
+
|
|
103
|
+
if tokens[cursor]&.type != :lparen
|
|
104
|
+
groups = []
|
|
105
|
+
else
|
|
106
|
+
depth, cursor = 1, cursor + 1
|
|
107
|
+
|
|
108
|
+
while cursor < tokens.length && depth.positive?
|
|
109
|
+
depth = collect(tokens[cursor], groups, depth)
|
|
110
|
+
cursor += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
groups
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns the depth after this token, having filed the token under the
|
|
118
|
+
# argument it belongs to. The call's own closing paren is the one thing
|
|
119
|
+
# not filed anywhere -- it ends the walk instead.
|
|
120
|
+
def collect(token, groups, depth)
|
|
121
|
+
case token.type
|
|
122
|
+
when :lparen, :lbracket then descend(groups, token, depth)
|
|
123
|
+
when :rparen, :rbracket then ascend(groups, token, depth)
|
|
124
|
+
when :comma then comma(groups, token, depth)
|
|
125
|
+
else
|
|
126
|
+
groups.last << token
|
|
127
|
+
depth
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def descend(groups, token, depth)
|
|
132
|
+
groups.last << token
|
|
133
|
+
|
|
134
|
+
depth + 1
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def ascend(groups, token, depth)
|
|
138
|
+
if depth > 1
|
|
139
|
+
groups.last << token
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
depth - 1
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def comma(groups, token, depth)
|
|
146
|
+
if depth == 1
|
|
147
|
+
groups << []
|
|
148
|
+
else
|
|
149
|
+
groups.last << token
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
depth
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A group is a literal only if it is a bare number, optionally signed.
|
|
156
|
+
def integer(group)
|
|
157
|
+
text = group.map(&:value).join
|
|
158
|
+
|
|
159
|
+
if text.match?(/\A[+-]?\d+\z/)
|
|
160
|
+
text
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def report_components(command, kind, components)
|
|
165
|
+
components.each_with_index do |component, index|
|
|
166
|
+
value = Integer(component, exception: false)
|
|
167
|
+
|
|
168
|
+
if !value.nil? && !(MINIMUM..MAXIMUM).cover?(value)
|
|
169
|
+
offend(
|
|
170
|
+
command.line,
|
|
171
|
+
"#{kind} #{CHANNELS.fetch(index)} component #{value} is outside " \
|
|
172
|
+
"#{MINIMUM}-#{MAXIMUM}",
|
|
173
|
+
column: command.keyword_column,
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
__END__
|
|
183
|
+
|
|
184
|
+
require_relative "../document"
|
|
185
|
+
|
|
186
|
+
describe "RemLint::Rules::ColorComponentRange" do
|
|
187
|
+
lint = proc do |text|
|
|
188
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
189
|
+
|
|
190
|
+
RemLint::Rules::ColorComponentRange.new.run(RemLint::Document.new(source))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
194
|
+
|
|
195
|
+
describe "SPECIAL COLOR" do
|
|
196
|
+
it "accepts components in range" do
|
|
197
|
+
lint.("REM Mon SPECIAL COLOR 255 128 0 Sunset\n").should.be.empty
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "accepts the boundary values" do
|
|
201
|
+
lint.("SPECIAL COLOR 0 0 0 x\nSPECIAL COLOR 255 255 255 y\n").should.be.empty
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "reports a component above 255" do
|
|
205
|
+
messages.("SPECIAL COLOR 2555 128 0 x\n").should ==
|
|
206
|
+
["COLOR red component 2555 is outside 0-255"]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "reports a negative component" do
|
|
210
|
+
messages.("SPECIAL COLOR 255 -1 0 x\n").should ==
|
|
211
|
+
["COLOR green component -1 is outside 0-255"]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "names the blue channel" do
|
|
215
|
+
messages.("SPECIAL COLOR 0 0 999 x\n").should ==
|
|
216
|
+
["COLOR blue component 999 is outside 0-255"]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "reports every bad component on the line" do
|
|
220
|
+
messages.("SPECIAL COLOR 300 400 500 x\n").length.should == 3
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "accepts the COLOUR spelling" do
|
|
224
|
+
messages.("SPECIAL COLOUR 300 0 0 x\n").should ==
|
|
225
|
+
["COLOUR red component 300 is outside 0-255"]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "finds the components after a full trigger, as astro writes them" do
|
|
229
|
+
messages.("REM [moondatetime(0)] +60 SPECIAL COLOR 300 0 0 New moon\n").length.should == 1
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
describe "SPECIAL SHADE" do
|
|
234
|
+
it "accepts components in range" do
|
|
235
|
+
lint.("REM Mon SPECIAL SHADE 255 255 204\n").should.be.empty
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "reports one out of range" do
|
|
239
|
+
messages.("REM Mon SPECIAL SHADE 256 255 204\n").should ==
|
|
240
|
+
["SHADE red component 256 is outside 0-255"]
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe "ansicolor" do
|
|
245
|
+
it "accepts components in range" do
|
|
246
|
+
lint.("MSG [ansicolor(0, 255, 0)]red[ansicolor(\"\")]\n").should.be.empty
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "reports one out of range" do
|
|
250
|
+
messages.("MSG [ansicolor(0, 300, 0)]x\n").should ==
|
|
251
|
+
["ansicolor green component 300 is outside 0-255"]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "accepts the four-argument background form" do
|
|
255
|
+
lint.("MSG [ansicolor(255, 255, 255)][ansicolor(0, 0, 0, 1)]x\n").should.be.empty
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it "says nothing about the reset form" do
|
|
259
|
+
lint.(%(MSG [ansicolor("")]\n)).should.be.empty
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "reports a negative component" do
|
|
263
|
+
# The lexer emits the minus as its own token, so this only works because
|
|
264
|
+
# the arguments are reassembled rather than picked out by position.
|
|
265
|
+
messages.("MSG [ansicolor(-1, 0, 0)]\n").should ==
|
|
266
|
+
["ansicolor red component -1 is outside 0-255"]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "says nothing when the components are computed" do
|
|
270
|
+
lint.("MSG [ansicolor(r, g, b)]\n").should.be.empty
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it "matches the function name case-insensitively" do
|
|
274
|
+
messages.("MSG [ANSICOLOR(300, 0, 0)]\n").length.should == 1
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
describe "what it stays quiet about" do
|
|
279
|
+
it "says nothing about a SPECIAL it does not know" do
|
|
280
|
+
lint.("SPECIAL HTMLCLASS 999 999 999\n").should.be.empty
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "says nothing about ordinary numbers on a line" do
|
|
284
|
+
lint.("MSG 300 400 500 items\n").should.be.empty
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it "says nothing about comments" do
|
|
288
|
+
lint.("# SPECIAL COLOR 999 0 0 example\n").should.be.empty
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "reports at error severity" do
|
|
293
|
+
lint.("SPECIAL COLOR 300 0 0 x\n").first.severity.should == "error"
|
|
294
|
+
end
|
|
295
|
+
end
|