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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +328 -0
  3. data/config/default.yml +374 -0
  4. data/docs/remlint-rules.md +390 -0
  5. data/docs/remlint.md +238 -0
  6. data/exe/remlint +8 -0
  7. data/lib/remlint/cli.rb +300 -0
  8. data/lib/remlint/command.rb +229 -0
  9. data/lib/remlint/config.rb +255 -0
  10. data/lib/remlint/date_literal.rb +283 -0
  11. data/lib/remlint/document.rb +161 -0
  12. data/lib/remlint/expr_lexer.rb +197 -0
  13. data/lib/remlint/extractors.rb +210 -0
  14. data/lib/remlint/formatter.rb +96 -0
  15. data/lib/remlint/invocation.rb +145 -0
  16. data/lib/remlint/logical_line.rb +194 -0
  17. data/lib/remlint/offense.rb +117 -0
  18. data/lib/remlint/rule.rb +216 -0
  19. data/lib/remlint/rules/addomit_without_scanfrom.rb +189 -0
  20. data/lib/remlint/rules/advance_warning_body.rb +176 -0
  21. data/lib/remlint/rules/banner_placement.rb +126 -0
  22. data/lib/remlint/rules/calendar_text_limited.rb +125 -0
  23. data/lib/remlint/rules/callback_signature.rb +221 -0
  24. data/lib/remlint/rules/clause_needs_full_date.rb +176 -0
  25. data/lib/remlint/rules/clause_requires_at.rb +178 -0
  26. data/lib/remlint/rules/clause_value_range.rb +257 -0
  27. data/lib/remlint/rules/color_component_range.rb +295 -0
  28. data/lib/remlint/rules/coordinate_not_string.rb +175 -0
  29. data/lib/remlint/rules/dangling_continuation.rb +134 -0
  30. data/lib/remlint/rules/date_out_of_range.rb +180 -0
  31. data/lib/remlint/rules/debug_command.rb +189 -0
  32. data/lib/remlint/rules/easterdate_from_today.rb +160 -0
  33. data/lib/remlint/rules/function_arity.rb +385 -0
  34. data/lib/remlint/rules/function_redefinition.rb +159 -0
  35. data/lib/remlint/rules/generated_file_edited.rb +125 -0
  36. data/lib/remlint/rules/hebrew_date.rb +245 -0
  37. data/lib/remlint/rules/iftrig_with_satisfy.rb +101 -0
  38. data/lib/remlint/rules/include_path.rb +155 -0
  39. data/lib/remlint/rules/info_clause.rb +186 -0
  40. data/lib/remlint/rules/info_substitution_without_header.rb +169 -0
  41. data/lib/remlint/rules/invocation_mismatch.rb +223 -0
  42. data/lib/remlint/rules/keyword_case.rb +172 -0
  43. data/lib/remlint/rules/license_header.rb +101 -0
  44. data/lib/remlint/rules/line_length.rb +101 -0
  45. data/lib/remlint/rules/literal_type_mismatch.rb +324 -0
  46. data/lib/remlint/rules/localization_pack.rb +144 -0
  47. data/lib/remlint/rules/moon_phase_argument.rb +162 -0
  48. data/lib/remlint/rules/omit_aware_delta.rb +168 -0
  49. data/lib/remlint/rules/push_vars_missing_name.rb +158 -0
  50. data/lib/remlint/rules/repeat_trigger.rb +188 -0
  51. data/lib/remlint/rules/satisfy_constraint.rb +230 -0
  52. data/lib/remlint/rules/shell_maxlen.rb +170 -0
  53. data/lib/remlint/rules/shell_use_while_run_disabled.rb +172 -0
  54. data/lib/remlint/rules/string_escape.rb +158 -0
  55. data/lib/remlint/rules/syntax.rb +229 -0
  56. data/lib/remlint/rules/system_variable_assignment.rb +197 -0
  57. data/lib/remlint/rules/tag_syntax.rb +145 -0
  58. data/lib/remlint/rules/text_after_eof_marker.rb +134 -0
  59. data/lib/remlint/rules/time_zone_name.rb +212 -0
  60. data/lib/remlint/rules/tk_tag_namespace.rb +121 -0
  61. data/lib/remlint/rules/todo_complete_through.rb +122 -0
  62. data/lib/remlint/rules/trailing_whitespace.rb +116 -0
  63. data/lib/remlint/rules/translate_command.rb +203 -0
  64. data/lib/remlint/rules/unbalanced_blocks.rb +305 -0
  65. data/lib/remlint/rules/unbalanced_delimiters.rb +251 -0
  66. data/lib/remlint/rules/unknown_special_type.rb +155 -0
  67. data/lib/remlint/rules/unknown_substitution_sequence.rb +255 -0
  68. data/lib/remlint/rules/unknown_system_variable.rb +145 -0
  69. data/lib/remlint/rules/unquoted_shell_substitution.rb +266 -0
  70. data/lib/remlint/rules/until_before_from.rb +204 -0
  71. data/lib/remlint/rules/world_writable_script.rb +128 -0
  72. data/lib/remlint/rules.rb +60 -0
  73. data/lib/remlint/runner.rb +274 -0
  74. data/lib/remlint/source.rb +36 -0
  75. data/lib/remlint/tables.rb +395 -0
  76. data/lib/remlint/trigger.rb +359 -0
  77. data/lib/remlint/version.rb +5 -0
  78. data/lib/remlint/vocabulary.rb +236 -0
  79. data/lib/remlint.rb +35 -0
  80. data/tasks/generate_tables.rb +175 -0
  81. metadata +170 -0
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+ require_relative "../date_literal"
5
+
6
+ module RemLint
7
+ module Rules
8
+ # Repeats that do not repeat what the author meant.
9
+ #
10
+ # A repeat is `*n`: the reminder triggers on its start date and every *n*
11
+ # days after. Two things go wrong with it, and neither is an error.
12
+ #
13
+ # NO START DATE. A repeat counts days from somewhere, and without a fully
14
+ # specified start date there is nothing to count from.
15
+ #
16
+ # A WEEKDAY IN THE TRIGGER. The weekday picks the start date and nothing
17
+ # else -- the recurrence then ignores weekdays entirely. `REM Fri 15 Sep
18
+ # 2025 *10` starts on Friday 19 September and fires every ten days after,
19
+ # on days that are mostly not Fridays. Whatever the `Fri` was for, it was
20
+ # almost certainly not that.
21
+ class RepeatTrigger < Rule
22
+ WEEKDAY_TYPE = "T_WkDay"
23
+
24
+ def self.default_severity
25
+ "warning"
26
+ end
27
+
28
+ def self.description
29
+ "A *n repeat with no start date, or with a weekday that does not affect it."
30
+ end
31
+
32
+ def check
33
+ document.code_commands.each do |command|
34
+ repeat = repeat_token(command)
35
+
36
+ if repeat
37
+ check_repeat(command, repeat)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # `*10` reaches the lexer as `*` then `10`. Only a command that carries
45
+ # a trigger can carry a repeat -- `SET a 3*14` is multiplication.
46
+ def repeat_token(command)
47
+ trigger = document.trigger_for(command)
48
+ limit = trigger.body_offset || command.text.length
49
+ tokens = document.tokens_for(command.logical_line)
50
+
51
+ if trigger.triggered?
52
+ tokens.each_index.filter_map { |index| star(tokens, index, limit) }.first
53
+ end
54
+ end
55
+
56
+ def star(tokens, index, limit)
57
+ token = tokens[index]
58
+ following = tokens[index + 1]
59
+
60
+ if token.type == :other && token.value == "*" &&
61
+ following&.type == :number && token.offset < limit
62
+ token
63
+ end
64
+ end
65
+
66
+ def check_repeat(command, repeat)
67
+ tokens = document.tokens_for(command.logical_line)
68
+
69
+ unless start_date(tokens)
70
+ offend_at(command.logical_line, repeat.offset, no_start_message)
71
+ end
72
+
73
+ weekday = weekday_clause(command)
74
+
75
+ if weekday
76
+ offend_at(command.logical_line, weekday.offset, weekday_message(weekday))
77
+ end
78
+ end
79
+
80
+ # A fully specified date anywhere in the trigger.
81
+ def start_date(tokens)
82
+ tokens.each_index.any? { |index| DateLiteral.at(tokens, index) }
83
+ end
84
+
85
+ def weekday_clause(command)
86
+ document.trigger_for(command).clauses.find do |clause|
87
+ clause.keyword.type == WEEKDAY_TYPE
88
+ end
89
+ end
90
+
91
+ def no_start_message
92
+ "a `*n` repeat counts days from a start date, and this trigger has no " \
93
+ "fully specified one to count from"
94
+ end
95
+
96
+ def weekday_message(clause)
97
+ "`#{clause.name}` only picks the start date; the `*n` repeat that follows " \
98
+ "counts days and ignores weekdays entirely"
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ __END__
105
+
106
+ require_relative "../document"
107
+
108
+ describe "RemLint::Rules::RepeatTrigger" do
109
+ lint = proc do |text|
110
+ source = RemLint::Source.new(path: "t.rem", text: text)
111
+
112
+ RemLint::Rules::RepeatTrigger.new.run(RemLint::Document.new(source))
113
+ end
114
+
115
+ messages = proc { |text| lint.(text).map(&:message) }
116
+
117
+ describe "repeats that are fine" do
118
+ it "accepts a repeat with an ISO start date" do
119
+ lint.("REM 2012-11-07 *14 MSG Garbage\n").should.be.empty
120
+ end
121
+
122
+ it "accepts a repeat with a spelled-out start date" do
123
+ lint.("REM 31 October 2012 *14 MSG Paper recycling\n").should.be.empty
124
+ end
125
+
126
+ it "accepts a reminder with no repeat at all" do
127
+ lint.("REM Fri MSG hi\n").should.be.empty
128
+ lint.("REM 1 Jan MSG hi\n").should.be.empty
129
+ end
130
+ end
131
+
132
+ describe "a repeat with no start date" do
133
+ it "is reported" do
134
+ messages.("REM Mon *14 MSG hi\n").first.should.match(/no fully specified one to count from/)
135
+ end
136
+
137
+ it "is reported for a partial date" do
138
+ messages.("REM 1 Jan *14 MSG hi\n").first.should.match(/no fully specified one/)
139
+ end
140
+
141
+ it "points at the repeat" do
142
+ text = "REM Mon *14 MSG hi\n"
143
+
144
+ lint.(text).map(&:column).should.include(text.index("*14") + 1)
145
+ end
146
+ end
147
+
148
+ describe "a weekday with a repeat" do
149
+ it "is reported" do
150
+ # Straight from chapter 4: starts on 19 Sep, then every ten days.
151
+ messages.("REM Fri 15 Sep 2025 *10 MSG Test\n").first.should ==
152
+ "`FRIDAY` only picks the start date; the `*n` repeat that follows counts days " \
153
+ "and ignores weekdays entirely"
154
+ end
155
+
156
+ it "points at the weekday" do
157
+ text = "REM Fri 15 Sep 2025 *10 MSG Test\n"
158
+
159
+ lint.(text).first.column.should == text.index("Fri") + 1
160
+ end
161
+
162
+ it "is not reported without a repeat" do
163
+ lint.("REM Fri 15 Sep 2025 MSG Test\n").should.be.empty
164
+ end
165
+
166
+ it "reports both faults when a weekday repeat has no start date" do
167
+ lint.("REM Fri *10 MSG Test\n").length.should == 2
168
+ end
169
+ end
170
+
171
+ describe "what it leaves alone" do
172
+ it "does not read a star out of a message body" do
173
+ lint.("REM 2012-11-07 MSG rating *5 stars\n").should.be.empty
174
+ end
175
+
176
+ it "does not read a multiplication as a repeat" do
177
+ lint.("SET a 3*14\n").should.be.empty
178
+ end
179
+
180
+ it "says nothing about comments" do
181
+ lint.("# REM Mon *14 MSG hi\n").should.be.empty
182
+ end
183
+ end
184
+
185
+ it "reports at warning severity" do
186
+ lint.("REM Mon *14 MSG hi\n").first.severity.should == "warning"
187
+ end
188
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+
5
+ module RemLint
6
+ module Rules
7
+ # `SATISFY` expressions that belong in the trigger.
8
+ #
9
+ # `SATISFY` works by trial: Remind proposes a date, evaluates the
10
+ # expression, and moves on a day if it comes back false. Anything the
11
+ # trigger could have pinned down instead is therefore paid for once per
12
+ # candidate date rather than once.
13
+ #
14
+ # Chapter 7 measures it. Over 100,000 runs the all-SATISFY form evaluates
15
+ # the expression 13,214,377 times and takes 2.30s; moving the day into the
16
+ # trigger drops that to 482,026 evaluations and 0.58s. Same reminder, four
17
+ # times the speed, and the edit is mechanical:
18
+ #
19
+ # REM SATISFY [$Td == 13 && $Tw == 5] -> REM 13 SATISFY [$Tw == 5]
20
+ #
21
+ # Three trigger components can be hoisted, and each is reported only when
22
+ # it is compared with `==` against a literal, which is the form that
23
+ # translates directly into the trigger:
24
+ #
25
+ # $Td the day of the month
26
+ # $Tm the month
27
+ # $Ty the year
28
+ #
29
+ # Two further faults share the same walk. A constraint no date can satisfy
30
+ # -- `$Td == 100` -- costs `$MaxSatIter` iterations and then prints
31
+ # `Can't compute trigger`. And a `$Ty <` term is an `UNTIL` written the
32
+ # expensive way, which the book calls bad practice for the same reason.
33
+ class SatisfyConstraint < Rule
34
+ # The trigger components a trigger can express directly, and the clause
35
+ # each becomes.
36
+ HOISTABLE = {
37
+ "Td" => "a day",
38
+ "Tm" => "a month",
39
+ "Ty" => "a year",
40
+ }.freeze
41
+
42
+ RANGES = {
43
+ "Td" => (1..31),
44
+ "Tm" => (1..12),
45
+ "Ty" => (1990..5990),
46
+ }.freeze
47
+
48
+ # `$Td == 13` and `$Ty < 2029`, as they arrive from the lexer.
49
+ COMPARISON = /\$(?<name>T[dmy])\s*(?<operator><=|>=|==|!=|<|>)\s*(?<value>\d+)/
50
+
51
+ def self.enabled_by_default?
52
+ false
53
+ end
54
+
55
+ def self.default_severity
56
+ "info"
57
+ end
58
+
59
+ def self.description
60
+ "A SATISFY constraint the trigger could express, or that no date satisfies."
61
+ end
62
+
63
+ def check
64
+ document.code_commands.each do |command|
65
+ trigger = document.trigger_for(command)
66
+
67
+ if trigger.body&.name == "SATISFY"
68
+ check_satisfy(command, trigger)
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def check_satisfy(command, trigger)
76
+ body = command.text[trigger.body_offset..].to_s
77
+ already = pinned(command, trigger)
78
+
79
+ body.to_enum(:scan, COMPARISON).each do
80
+ match = Regexp.last_match
81
+
82
+ report(
83
+ command,
84
+ trigger,
85
+ match,
86
+ already,
87
+ )
88
+ end
89
+ end
90
+
91
+ # A component the trigger already names is not one to hoist.
92
+ def pinned(command, trigger)
93
+ tokens = document.tokens_for(command.logical_line)
94
+ limit = trigger.body.offset
95
+
96
+ tokens.select { |token| token.offset < limit }.map(&:type)
97
+ end
98
+
99
+ def report(command, trigger, match, already)
100
+ name = match[:name]
101
+ value = match[:value].to_i
102
+ offset = trigger.body_offset + match.begin(0)
103
+
104
+ if !RANGES.fetch(name).cover?(value)
105
+ offend_at(command.logical_line, offset, unsatisfiable(name, match))
106
+ elsif match[:operator] == "==" && !already.include?(:number)
107
+ offend_at(command.logical_line, offset, hoistable(name, match))
108
+ elsif name == "Ty" && ["<", "<="].include?(match[:operator])
109
+ offend_at(command.logical_line, offset, bounded(match))
110
+ end
111
+ end
112
+
113
+ def unsatisfiable(name, match)
114
+ "`$#{name} #{match[:operator]} #{match[:value]}` can never hold -- " \
115
+ "#{name} runs #{RANGES.fetch(name).first} to #{RANGES.fetch(name).last} -- so " \
116
+ "this costs $MaxSatIter iterations and then prints `Can't compute trigger`"
117
+ end
118
+
119
+ def hoistable(name, match)
120
+ "`$#{name} == #{match[:value]}` is #{HOISTABLE.fetch(name)} the trigger could " \
121
+ "name directly; hoisting it stops the expression being evaluated once per " \
122
+ "candidate date"
123
+ end
124
+
125
+ def bounded(match)
126
+ "`$Ty #{match[:operator]} #{match[:value]}` is an `UNTIL` written as a " \
127
+ "constraint; `UNTIL #{match[:value].to_i - 1}-12-31` says the same thing " \
128
+ "without a search"
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ __END__
135
+
136
+ require_relative "../document"
137
+
138
+ describe "RemLint::Rules::SatisfyConstraint" do
139
+ lint = proc do |text|
140
+ source = RemLint::Source.new(path: "t.rem", text: text)
141
+
142
+ RemLint::Rules::SatisfyConstraint.new.run(RemLint::Document.new(source))
143
+ end
144
+
145
+ messages = proc { |text| lint.(text).map(&:message) }
146
+
147
+ it "is off unless the configuration asks for it" do
148
+ RemLint::Rules::SatisfyConstraint.enabled_by_default?.should.be.false
149
+ end
150
+
151
+ describe "constraints the trigger could express" do
152
+ it "reports a day" do
153
+ messages.("REM SATISFY [$Td == 13]\n").first.should.match(
154
+ /is a day the trigger could name directly/,
155
+ )
156
+ end
157
+
158
+ it "reports a month and a year" do
159
+ messages.("REM SATISFY [$Tm == 6]\n").first.should.match(/is a month the trigger could/)
160
+ messages.("REM SATISFY [$Ty == 2027]\n").first.should.match(/is a year the trigger could/)
161
+ end
162
+
163
+ it "explains the cost" do
164
+ messages.("REM SATISFY [$Td == 13]\n").first.should.match(
165
+ /once per candidate date/,
166
+ )
167
+ end
168
+
169
+ it "accepts a constraint the trigger already pins down" do
170
+ # `REM 13 SATISFY [$Tw == 5]` is the fixed form from chapter 7.
171
+ lint.("REM 13 SATISFY [$Tw == 5]\n").should.be.empty
172
+ end
173
+
174
+ it "says nothing about a weekday, which a trigger cannot pin this way" do
175
+ lint.("REM SATISFY [$Tw == 5]\n").should.be.empty
176
+ end
177
+ end
178
+
179
+ describe "constraints no date satisfies" do
180
+ it "reports a day out of range" do
181
+ messages.("REM SATISFY [$Td == 100]\n").first.should.match(
182
+ /can never hold -- Td runs 1 to 31/,
183
+ )
184
+ end
185
+
186
+ it "names the cost" do
187
+ messages.("REM SATISFY [$Td == 100]\n").first.should.match(
188
+ /costs \$MaxSatIter iterations and then prints `Can't compute trigger`/,
189
+ )
190
+ end
191
+
192
+ it "reports a month out of range" do
193
+ messages.("REM SATISFY [$Tm == 13]\n").first.should.match(/Tm runs 1 to 12/)
194
+ end
195
+
196
+ it "reports a year out of range" do
197
+ messages.("REM SATISFY [$Ty == 1970]\n").first.should.match(/Ty runs 1990 to 5990/)
198
+ end
199
+ end
200
+
201
+ describe "a year bound written as a constraint" do
202
+ it "is reported as an UNTIL" do
203
+ messages.("REM 13 SATISFY [$Ty < 2029]\n").first.should ==
204
+ "`$Ty < 2029` is an `UNTIL` written as a constraint; `UNTIL 2028-12-31` says " \
205
+ "the same thing without a search"
206
+ end
207
+
208
+ it "is reported for <= too" do
209
+ messages.("REM 13 SATISFY [$Ty <= 2029]\n").length.should == 1
210
+ end
211
+ end
212
+
213
+ it "says nothing about a REM with no SATISFY" do
214
+ lint.("REM 13 MSG hi\n").should.be.empty
215
+ end
216
+
217
+ it "says nothing about comments" do
218
+ lint.("# REM SATISFY [$Td == 13]\n").should.be.empty
219
+ end
220
+
221
+ it "points at the constraint" do
222
+ text = "REM SATISFY [$Td == 13]\n"
223
+
224
+ lint.(text).first.column.should == text.index("$Td") + 1
225
+ end
226
+
227
+ it "reports at info severity" do
228
+ lint.("REM SATISFY [$Td == 13]\n").first.severity.should == "info"
229
+ end
230
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+
5
+ module RemLint
6
+ module Rules
7
+ # `shell()` given a maximum length that returns nothing.
8
+ #
9
+ # `shell(cmd)` reads up to 511 characters of the command's output;
10
+ # `shell(cmd, n)` reads up to *n*. A zero returns the empty string, and a
11
+ # command that produced no output returns the empty string too -- so
12
+ # `shell(cmd, 0)` is indistinguishable from a command that did not work,
13
+ # and debugging starts in the wrong place entirely.
14
+ #
15
+ # A negative *n* is the documented sentinel for "no limit", so only zero is
16
+ # reported.
17
+ class ShellMaxlen < Rule
18
+ SHELL = "shell"
19
+
20
+ def self.default_severity
21
+ "warning"
22
+ end
23
+
24
+ def self.description
25
+ "shell() given a maximum length of zero, which always returns nothing."
26
+ end
27
+
28
+ def check
29
+ document.logical_lines.each_with_index do |logical_line, index|
30
+ if document.commands[index].code?
31
+ check_line(logical_line)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def check_line(logical_line)
39
+ tokens = document.tokens_for(logical_line)
40
+
41
+ tokens.each_index do |index|
42
+ if call?(tokens, index)
43
+ check_call(logical_line, tokens, index)
44
+ end
45
+ end
46
+ end
47
+
48
+ def call?(tokens, index)
49
+ tokens[index].type == :function &&
50
+ tokens[index].value.casecmp?(SHELL) &&
51
+ tokens[index + 1]&.type == :lparen
52
+ end
53
+
54
+ # The second argument, when it is written out as a literal zero. The
55
+ # walk stops at the first top-level comma's operand, so a nested call
56
+ # in the first argument does not confuse it.
57
+ def check_call(logical_line, tokens, index)
58
+ zero = second_argument(tokens, index + 1)
59
+
60
+ if zero
61
+ offend_at(
62
+ logical_line,
63
+ zero.offset,
64
+ "`shell(..., 0)` returns the empty string whatever the command prints, " \
65
+ "which looks exactly like a command that produced nothing",
66
+ )
67
+ end
68
+ end
69
+
70
+ def second_argument(tokens, open_index)
71
+ comma = top_level_comma(tokens, open_index)
72
+ argument = comma && tokens[comma + 1]
73
+
74
+ if argument&.type == :number && argument.value.to_i.zero? &&
75
+ tokens[comma + 2]&.type == :rparen
76
+ argument
77
+ end
78
+ end
79
+
80
+ def top_level_comma(tokens, open_index)
81
+ depth = 0
82
+ cursor = open_index
83
+
84
+ while cursor < tokens.length
85
+ depth = depth_after(tokens[cursor], depth)
86
+
87
+ if depth.zero?
88
+ break
89
+ end
90
+
91
+ if depth == 1 && tokens[cursor].type == :comma
92
+ break
93
+ end
94
+
95
+ cursor += 1
96
+ end
97
+
98
+ if tokens[cursor]&.type == :comma
99
+ cursor
100
+ end
101
+ end
102
+
103
+ def depth_after(token, depth)
104
+ case token.type
105
+ when :lparen, :lbracket then depth + 1
106
+ when :rparen, :rbracket then depth - 1
107
+ else depth
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ __END__
115
+
116
+ require_relative "../document"
117
+
118
+ describe "RemLint::Rules::ShellMaxlen" do
119
+ lint = proc do |text|
120
+ source = RemLint::Source.new(path: "t.rem", text: text)
121
+
122
+ RemLint::Rules::ShellMaxlen.new.run(RemLint::Document.new(source))
123
+ end
124
+
125
+ it "reports a zero maximum length" do
126
+ lint.(%(SET a shell("ls", 0)\n)).first.message.should.match(/returns the empty string/)
127
+ end
128
+
129
+ it "accepts the one-argument form" do
130
+ lint.(%(SET a shell("ls")\n)).should.be.empty
131
+ end
132
+
133
+ it "accepts a positive maximum length" do
134
+ lint.(%(SET a shell("ls", 1024)\n)).should.be.empty
135
+ end
136
+
137
+ it "accepts the negative sentinel" do
138
+ lint.(%(SET a shell("ls", -1)\n)).should.be.empty
139
+ end
140
+
141
+ it "is not confused by a comma inside the command" do
142
+ lint.(%(SET a shell("echo a,b")\n)).should.be.empty
143
+ end
144
+
145
+ it "is not confused by a nested call in the first argument" do
146
+ lint.(%(SET a shell(join("ls", "-l"), 1024)\n)).should.be.empty
147
+ end
148
+
149
+ it "matches the function name case-insensitively" do
150
+ lint.(%(SET a SHELL("ls", 0)\n)).length.should == 1
151
+ end
152
+
153
+ it "points at the zero" do
154
+ text = %(SET a shell("ls", 0)\n)
155
+
156
+ lint.(text).first.column.should == text.rindex("0") + 1
157
+ end
158
+
159
+ it "says nothing about another function with a zero argument" do
160
+ lint.("SET a max(1, 0)\n").should.be.empty
161
+ end
162
+
163
+ it "says nothing about comments" do
164
+ lint.(%(# SET a shell("ls", 0)\n)).should.be.empty
165
+ end
166
+
167
+ it "reports at warning severity" do
168
+ lint.(%(SET a shell("ls", 0)\n)).first.severity.should == "warning"
169
+ end
170
+ end