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,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `ADDOMIT` with no `SCANFROM`, which adds the wrong year's holiday.
|
|
8
|
+
#
|
|
9
|
+
# `ADDOMIT` puts a movable holiday's computed date into the omit context.
|
|
10
|
+
# But Remind computes a reminder's trigger by scanning forward from today,
|
|
11
|
+
# so the day *after* the holiday the trigger has already moved to next
|
|
12
|
+
# year's occurrence -- and it is next year's date that gets added.
|
|
13
|
+
# Reminders that `SKIP`, `BEFORE` or `AFTER` around the holiday then work
|
|
14
|
+
# perfectly until the day after it, and are quietly wrong from then on.
|
|
15
|
+
#
|
|
16
|
+
# `SCANFROM` fixes it by making the scan start far enough back that the
|
|
17
|
+
# current year's date stays the trigger. Remind warns about a missing one
|
|
18
|
+
# itself, and names the same figure:
|
|
19
|
+
#
|
|
20
|
+
# Warning: Consider using SCANFROM -28 with recurring ADDOMIT
|
|
21
|
+
#
|
|
22
|
+
# This rule says it before the file is ever run, and keeps to Remind's
|
|
23
|
+
# scope: a trigger built from a bracketed expression is opaque to both of
|
|
24
|
+
# us, so neither reports it. `include/holidays/cl.rem` has one
|
|
25
|
+
# (`REM [datepart(soleq(1, $U-28))] ADDOMIT ...`) and Remind is quiet
|
|
26
|
+
# about it, so this is too.
|
|
27
|
+
#
|
|
28
|
+
# The *width* of the window is a judgement rather than a fact, and so is
|
|
29
|
+
# off by default. Chapter 4 works through 28 days, and
|
|
30
|
+
# `include/holidays/us.rem` uses exactly that -- but `examples/defs.rem`
|
|
31
|
+
# uses `SCANFROM -7` twenty-seven times, and that is not a bug: it is a
|
|
32
|
+
# smaller guarantee, adequate for holidays whose dependent reminders are
|
|
33
|
+
# all within a week. Set `MinimumWindow` to 28 to ask for the wider one.
|
|
34
|
+
class AddomitWithoutScanfrom < Rule
|
|
35
|
+
# The figure chapter 4 works through, used only in the message and as
|
|
36
|
+
# the suggestion for a missing SCANFROM.
|
|
37
|
+
RECOMMENDED = 28
|
|
38
|
+
|
|
39
|
+
def self.default_severity
|
|
40
|
+
"warning"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.description
|
|
44
|
+
"ADDOMIT with no SCANFROM, which adds next year's date the day after the holiday."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check
|
|
48
|
+
document.code_commands.each do |command|
|
|
49
|
+
trigger = document.trigger_for(command)
|
|
50
|
+
|
|
51
|
+
if trigger.include?("ADDOMIT") && !computed?(command, trigger)
|
|
52
|
+
check_scanfrom(command, trigger)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# A trigger whose date comes out of an expression. Remind's own
|
|
60
|
+
# warning does not fire on these and neither does this rule.
|
|
61
|
+
def computed?(command, trigger)
|
|
62
|
+
limit = trigger.body_offset || command.text.length
|
|
63
|
+
|
|
64
|
+
document.tokens_for(command.logical_line).any? do |token|
|
|
65
|
+
token.type == :lbracket && token.offset < limit
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check_scanfrom(command, trigger)
|
|
70
|
+
scanfrom = trigger.find("SCANFROM")
|
|
71
|
+
|
|
72
|
+
if scanfrom.nil?
|
|
73
|
+
offend_at(command.logical_line, trigger.find("ADDOMIT").offset, missing_message)
|
|
74
|
+
else
|
|
75
|
+
check_window(command, scanfrom)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Zero -- the default -- means "any window will do", which is the
|
|
80
|
+
# honest position: a narrower one is a smaller guarantee, not a fault.
|
|
81
|
+
def check_window(command, scanfrom)
|
|
82
|
+
minimum = option("MinimumWindow", 0)
|
|
83
|
+
days = window(command, scanfrom)
|
|
84
|
+
|
|
85
|
+
if minimum.positive? && days && days < minimum
|
|
86
|
+
offend_at(command.logical_line, scanfrom.offset, narrow_message(days, minimum))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `SCANFROM -28` -- the delta form, which is the one that matters here.
|
|
91
|
+
def window(command, scanfrom)
|
|
92
|
+
text = command.text[scanfrom.end_offset..].to_s.strip[/\A-\d+/]
|
|
93
|
+
|
|
94
|
+
text&.delete("-")&.to_i
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def missing_message
|
|
98
|
+
"`ADDOMIT` with no `SCANFROM` adds *next* year's date once the day itself has " \
|
|
99
|
+
"passed, so anything skipping around this holiday is wrong from the day after " \
|
|
100
|
+
"it; `SCANFROM -#{RECOMMENDED}` keeps the addition stable"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def narrow_message(days, minimum)
|
|
104
|
+
"`SCANFROM -#{days}` is narrower than the #{minimum} days this project asks " \
|
|
105
|
+
"for, so a reminder more than #{days} days from the holiday can still see the " \
|
|
106
|
+
"wrong year's date"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
__END__
|
|
113
|
+
|
|
114
|
+
require_relative "../document"
|
|
115
|
+
|
|
116
|
+
describe "RemLint::Rules::AddomitWithoutScanfrom" do
|
|
117
|
+
lint = proc do |text, config = {}|
|
|
118
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
119
|
+
|
|
120
|
+
RemLint::Rules::AddomitWithoutScanfrom.new(config).run(RemLint::Document.new(source))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
messages = proc { |text, config = {}| lint.(text, config).map(&:message) }
|
|
124
|
+
|
|
125
|
+
wide = { "MinimumWindow" => 28 }
|
|
126
|
+
|
|
127
|
+
it "reports ADDOMIT with no SCANFROM" do
|
|
128
|
+
messages.("REM Mon 1 Apr ADDOMIT MSG Holiday\n").first.should.match(
|
|
129
|
+
/adds \*next\* year's date once the day itself has passed/,
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "suggests the window" do
|
|
134
|
+
messages.("REM Mon 1 Apr ADDOMIT MSG Holiday\n").first.should.match(/`SCANFROM -28`/)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "accepts ADDOMIT with a wide enough SCANFROM" do
|
|
138
|
+
lint.("REM Thu Nov 22 SCANFROM -28 ADDOMIT MSG Thanksgiving\n").should.be.empty
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "accepts a wider window still" do
|
|
142
|
+
lint.("REM Thu Nov 22 SCANFROM -60 ADDOMIT MSG Thanksgiving\n").should.be.empty
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "accepts any window by default" do
|
|
146
|
+
# examples/defs.rem writes SCANFROM -7 twenty-seven times, and that is a
|
|
147
|
+
# smaller guarantee rather than a mistake.
|
|
148
|
+
lint.("REM Thu Nov 22 SCANFROM -7 ADDOMIT MSG Thanksgiving\n").should.be.empty
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "reports a narrow window when the project asks for a wider one" do
|
|
152
|
+
messages.("REM Thu Nov 22 SCANFROM -7 ADDOMIT MSG x\n", wide).first.should.match(
|
|
153
|
+
/`SCANFROM -7` is narrower than the 28 days this project asks for/,
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "accepts exactly the configured minimum" do
|
|
158
|
+
lint.("REM Thu Nov 22 SCANFROM -28 ADDOMIT MSG x\n", wide).should.be.empty
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "says nothing about a reminder with no ADDOMIT" do
|
|
162
|
+
lint.("REM Thu Nov 22 SCANFROM -7 MSG Thanksgiving\n").should.be.empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "says nothing when SCANFROM takes a date rather than a delta" do
|
|
166
|
+
# A fixed scan date is a different thing and not this rule's to judge.
|
|
167
|
+
lint.("REM Thu Nov 22 SCANFROM 2026-01-01 ADDOMIT MSG x\n", wide).should.be.empty
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "points at the ADDOMIT when there is no SCANFROM" do
|
|
171
|
+
text = "REM Mon 1 Apr ADDOMIT MSG Holiday\n"
|
|
172
|
+
|
|
173
|
+
lint.(text).first.column.should == text.index("ADDOMIT") + 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "says nothing about a trigger built from an expression" do
|
|
177
|
+
# include/holidays/cl.rem has one of these and Remind does not warn about
|
|
178
|
+
# it either -- the trigger is opaque to both of us.
|
|
179
|
+
lint.("REM [datepart(soleq(1, $U-28))] ADDOMIT MSG Solstice\n").should.be.empty
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "says nothing about comments" do
|
|
183
|
+
lint.("# REM Mon 1 Apr ADDOMIT MSG Holiday\n").should.be.empty
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "reports at warning severity" do
|
|
187
|
+
lint.("REM Mon 1 Apr ADDOMIT MSG x\n").first.severity.should == "warning"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Reminders that arrive early without saying so.
|
|
8
|
+
#
|
|
9
|
+
# A delta makes a reminder start appearing before the day it is about, and
|
|
10
|
+
# a time makes it arrive before the hour. Neither changes the body. So
|
|
11
|
+
#
|
|
12
|
+
# REM 16 July ++10 MSG Jane's birthday.
|
|
13
|
+
#
|
|
14
|
+
# prints `Jane's birthday.` on the 8th of July, with nothing to say the
|
|
15
|
+
# birthday is eight days off, and reads exactly like a birthday today.
|
|
16
|
+
# `%b` is the idiomatic fix -- "in 8 days' time" -- and `%2` or `%3` is its
|
|
17
|
+
# equivalent for a time.
|
|
18
|
+
#
|
|
19
|
+
# Off by default, and it will fire on plenty of correct files: an advance
|
|
20
|
+
# warning whose body already reads naturally in both places is fine, and
|
|
21
|
+
# only the author can say. Turning it on is worth it for a file being
|
|
22
|
+
# written rather than one being inherited.
|
|
23
|
+
#
|
|
24
|
+
# The paired rule is `CalendarTextLimited`, which asks you to fence the
|
|
25
|
+
# relative phrase in `%"…%"` so it does not follow the reminder into a
|
|
26
|
+
# calendar box.
|
|
27
|
+
class AdvanceWarningBody < Rule
|
|
28
|
+
# `%a` to `%l` and friends all say something about *when*; these are the
|
|
29
|
+
# ones that say how far away it is.
|
|
30
|
+
RELATIVE = /%\*?[abcefghijkluv]/i
|
|
31
|
+
|
|
32
|
+
TIME_SUBSTITUTION = /%\*?[0-9@#]/
|
|
33
|
+
|
|
34
|
+
DELTA = /(?<!\S)[+-]{1,2}\d+(?!\S)/
|
|
35
|
+
|
|
36
|
+
def self.enabled_by_default?
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.default_severity
|
|
41
|
+
"info"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.description
|
|
45
|
+
"A reminder with a delta or a time whose body never says when the event is."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check
|
|
49
|
+
document.code_commands.each do |command|
|
|
50
|
+
trigger = document.trigger_for(command)
|
|
51
|
+
|
|
52
|
+
if trigger.text_body?
|
|
53
|
+
check_body(command, trigger)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def check_body(command, trigger)
|
|
61
|
+
body = command.text[trigger.body_offset..].to_s
|
|
62
|
+
|
|
63
|
+
check_delta(command, trigger, body)
|
|
64
|
+
check_time(command, trigger, body)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_delta(command, trigger, body)
|
|
68
|
+
if delta?(command, trigger) && !body.match?(RELATIVE)
|
|
69
|
+
offend_at(
|
|
70
|
+
command.logical_line,
|
|
71
|
+
trigger.body.offset,
|
|
72
|
+
"this reminder has a delta, so it appears days before the event, but the " \
|
|
73
|
+
"body never says how far off it is; `%b` reads as \"in 8 days' time\"",
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_time(command, trigger, body)
|
|
79
|
+
if trigger.include?("AT") && !body.match?(TIME_SUBSTITUTION) && !body.match?(RELATIVE)
|
|
80
|
+
offend_at(
|
|
81
|
+
command.logical_line,
|
|
82
|
+
trigger.body.offset,
|
|
83
|
+
"this reminder has an `AT`, so the queued copy arrives before the event, " \
|
|
84
|
+
"but the body never mentions the time; `%2` or `%3` says when",
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A delta is written in the trigger, before the body.
|
|
90
|
+
def delta?(command, trigger)
|
|
91
|
+
head = command.text[0...trigger.body.offset].to_s
|
|
92
|
+
|
|
93
|
+
head.match?(DELTA)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
__END__
|
|
100
|
+
|
|
101
|
+
require_relative "../document"
|
|
102
|
+
|
|
103
|
+
describe "RemLint::Rules::AdvanceWarningBody" do
|
|
104
|
+
lint = proc do |text|
|
|
105
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
106
|
+
|
|
107
|
+
RemLint::Rules::AdvanceWarningBody.new.run(RemLint::Document.new(source))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
111
|
+
|
|
112
|
+
it "is off unless the configuration asks for it" do
|
|
113
|
+
RemLint::Rules::AdvanceWarningBody.enabled_by_default?.should.be.false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe "deltas" do
|
|
117
|
+
it "reports a body that never says how far off the event is" do
|
|
118
|
+
messages.("REM 16 July ++10 MSG Jane's birthday.\n").first.should.match(
|
|
119
|
+
/the body never says how far off it is/,
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "accepts a body that uses %b" do
|
|
124
|
+
lint.("REM 16 July ++10 MSG Jane's birthday %b.\n").should.be.empty
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "accepts a body that uses another relative substitution" do
|
|
128
|
+
lint.("REM 16 July ++10 MSG Jane's birthday %c.\n").should.be.empty
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "accepts a single-sign delta with a relative substitution" do
|
|
132
|
+
lint.("REM 16 July +10 MSG Jane's birthday %b.\n").should.be.empty
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "says nothing about a reminder with no delta" do
|
|
136
|
+
lint.("REM 16 July MSG Jane's birthday.\n").should.be.empty
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "does not read a delta out of the body" do
|
|
140
|
+
lint.("REM 16 July MSG the score was +10 points\n").should.be.empty
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe "times" do
|
|
145
|
+
it "reports an AT reminder whose body never mentions the time" do
|
|
146
|
+
messages.("REM Tue AT 15:00 MSG Staff meeting\n").first.should.match(
|
|
147
|
+
/the body never mentions the time/,
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "accepts a body that uses %2" do
|
|
152
|
+
lint.("REM Tue AT 15:00 MSG Staff meeting at %2\n").should.be.empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "accepts a body that uses %3 or %@" do
|
|
156
|
+
lint.("REM Tue AT 15:00 MSG Meeting %3\n").should.be.empty
|
|
157
|
+
lint.("REM Tue AT 15:00 MSG Meeting %@\n").should.be.empty
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "says nothing about a reminder with no AT" do
|
|
161
|
+
lint.("REM Tue MSG Staff meeting\n").should.be.empty
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "says nothing about a SATISFY body, which is an expression" do
|
|
166
|
+
lint.("REM 16 July ++10 SATISFY [1]\n").should.be.empty
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "says nothing about comments" do
|
|
170
|
+
lint.("# REM 16 July ++10 MSG Jane's birthday.\n").should.be.empty
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "reports at info severity" do
|
|
174
|
+
lint.("REM 16 July ++10 MSG hi\n").first.severity.should == "info"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `BANNER` written more than once, or below the reminders it appears above.
|
|
8
|
+
#
|
|
9
|
+
# `BANNER` sets the line Remind prints before the day's reminders, and
|
|
10
|
+
# there is one of them. A second `BANNER` silently replaces the first, and
|
|
11
|
+
# one written below a `REM` reads as though it applied to the reminders
|
|
12
|
+
# above it when it applies to all of them equally.
|
|
13
|
+
#
|
|
14
|
+
# Neither is an error. The first is a line of configuration that does
|
|
15
|
+
# nothing; the second is a line that does something other than where it
|
|
16
|
+
# sits suggests.
|
|
17
|
+
class BannerPlacement < Rule
|
|
18
|
+
def self.default_severity
|
|
19
|
+
"warning"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.description
|
|
23
|
+
"A second BANNER, or a BANNER written below the first reminder."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check
|
|
27
|
+
banners = document.code_commands.select { |command| command.keyword?("BANNER") }
|
|
28
|
+
|
|
29
|
+
report_duplicates(banners)
|
|
30
|
+
report_late(banners.first)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def report_duplicates(banners)
|
|
36
|
+
banners.drop(1).each do |banner|
|
|
37
|
+
offend(
|
|
38
|
+
banner.line,
|
|
39
|
+
"a second `BANNER` silently replaces the one on line #{banners.first.line}",
|
|
40
|
+
column: banner.keyword_column,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def report_late(banner)
|
|
46
|
+
first = first_reminder
|
|
47
|
+
|
|
48
|
+
if banner && first && banner.line > first.line
|
|
49
|
+
offend(
|
|
50
|
+
banner.line,
|
|
51
|
+
"`BANNER` applies to the whole run, so writing it below the reminder on " \
|
|
52
|
+
"line #{first.line} reads as though it applied only to what follows",
|
|
53
|
+
column: banner.keyword_column,
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def first_reminder
|
|
59
|
+
document.code_commands.find do |command|
|
|
60
|
+
command.implicit? || command.keyword?("REM")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
__END__
|
|
68
|
+
|
|
69
|
+
require_relative "../document"
|
|
70
|
+
|
|
71
|
+
describe "RemLint::Rules::BannerPlacement" do
|
|
72
|
+
lint = proc do |text|
|
|
73
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
74
|
+
|
|
75
|
+
RemLint::Rules::BannerPlacement.new.run(RemLint::Document.new(source))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
79
|
+
|
|
80
|
+
it "accepts one BANNER above the reminders" do
|
|
81
|
+
lint.("BANNER %\nREM 1 Jan MSG hi\n").should.be.empty
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "accepts a file with no BANNER" do
|
|
85
|
+
lint.("REM 1 Jan MSG hi\n").should.be.empty
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "reports a second BANNER" do
|
|
89
|
+
messages.("BANNER one\nBANNER two\n").first.should ==
|
|
90
|
+
"a second `BANNER` silently replaces the one on line 1"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "reports each BANNER after the first" do
|
|
94
|
+
lint.("BANNER a\nBANNER b\nBANNER c\n").length.should == 2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "reports a BANNER written below a reminder" do
|
|
98
|
+
messages.("REM 1 Jan MSG hi\nBANNER %\n").first.should.match(
|
|
99
|
+
/reads as though it applied only to what follows/,
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "reports a BANNER below an implicit trigger" do
|
|
104
|
+
messages.("1 Jan MSG hi\nBANNER %\n").length.should == 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "does not count a SET or an INCLUDE as a reminder" do
|
|
108
|
+
lint.("SET a 1\nINCLUDE defs.rem\nBANNER %\n").should.be.empty
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "points at the keyword" do
|
|
112
|
+
lint.("BANNER a\n BANNER b\n").first.column.should == 4
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "says nothing about the word banner in a body" do
|
|
116
|
+
lint.("REM 1 Jan MSG hang the banner\n").should.be.empty
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "says nothing about comments" do
|
|
120
|
+
lint.("# BANNER a\n# BANNER b\n").should.be.empty
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "reports at warning severity" do
|
|
124
|
+
lint.("BANNER a\nBANNER b\n").first.severity.should == "warning"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A relative phrase that follows the reminder into a calendar box.
|
|
8
|
+
#
|
|
9
|
+
# `%"…%"` marks the part of a body that belongs in a calendar. Outside the
|
|
10
|
+
# marks, text appears in Agenda Mode only. The distinction matters most for
|
|
11
|
+
# exactly the substitutions `AdvanceWarningBody` asks you to add:
|
|
12
|
+
#
|
|
13
|
+
# REM 16 July ++10 MSG %"Jane's birthday%" %b.
|
|
14
|
+
#
|
|
15
|
+
# In Agenda Mode that reads "Jane's birthday in 8 days' time". In a
|
|
16
|
+
# calendar it is printed in the box for 16 July, where "in 8 days' time" is
|
|
17
|
+
# not merely redundant but wrong -- the box already says which day it is.
|
|
18
|
+
#
|
|
19
|
+
# So this is the second half of a pair: one rule asks for `%b`, this one
|
|
20
|
+
# asks you to fence it. Off by default, like its sibling, and for the same
|
|
21
|
+
# reason: a file that is never rendered as a calendar does not need it.
|
|
22
|
+
class CalendarTextLimited < Rule
|
|
23
|
+
RELATIVE = /%\*?[abcefghijkluv]/i
|
|
24
|
+
|
|
25
|
+
MARKER = '%"'
|
|
26
|
+
|
|
27
|
+
def self.enabled_by_default?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.default_severity
|
|
32
|
+
"info"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.description
|
|
36
|
+
"A relative-date substitution in a body with no %\" ... %\" calendar limit."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check
|
|
40
|
+
document.code_commands.each do |command|
|
|
41
|
+
trigger = document.trigger_for(command)
|
|
42
|
+
|
|
43
|
+
if trigger.text_body?
|
|
44
|
+
check_body(command, trigger)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def check_body(command, trigger)
|
|
52
|
+
body = command.text[trigger.body_offset..].to_s
|
|
53
|
+
match = body.match(RELATIVE)
|
|
54
|
+
|
|
55
|
+
if match && !body.include?(MARKER)
|
|
56
|
+
offend_at(
|
|
57
|
+
command.logical_line,
|
|
58
|
+
trigger.body_offset + match.begin(0),
|
|
59
|
+
"`#{match[0]}` says how far off the event is, which is right in Agenda " \
|
|
60
|
+
"Mode and wrong in a calendar box; fence the name in `%\"…%\"` so only " \
|
|
61
|
+
"that reaches the calendar",
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
__END__
|
|
70
|
+
|
|
71
|
+
require_relative "../document"
|
|
72
|
+
|
|
73
|
+
describe "RemLint::Rules::CalendarTextLimited" do
|
|
74
|
+
lint = proc do |text|
|
|
75
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
76
|
+
|
|
77
|
+
RemLint::Rules::CalendarTextLimited.new.run(RemLint::Document.new(source))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
81
|
+
|
|
82
|
+
it "is off unless the configuration asks for it" do
|
|
83
|
+
RemLint::Rules::CalendarTextLimited.enabled_by_default?.should.be.false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "reports a relative substitution with no calendar limit" do
|
|
87
|
+
messages.("REM 16 July ++10 MSG Jane's birthday %b.\n").first.should.match(
|
|
88
|
+
/wrong in a calendar box/,
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "names the substitution it found" do
|
|
93
|
+
messages.("REM 16 July ++10 MSG Jane's birthday %b.\n").first.should.match(/`%b` says/)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "accepts a body that fences the name" do
|
|
97
|
+
lint.(%(REM 16 July ++10 MSG %"Jane's birthday%" %b.\n)).should.be.empty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "accepts a body with no relative substitution at all" do
|
|
101
|
+
lint.("REM 16 July MSG Jane's birthday.\n").should.be.empty
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "reports only once per body" do
|
|
105
|
+
lint.("REM 16 July ++10 MSG %b and %c\n").length.should == 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "points at the substitution" do
|
|
109
|
+
text = "REM 16 July ++10 MSG Jane %b.\n"
|
|
110
|
+
|
|
111
|
+
lint.(text).first.column.should == text.index("%b") + 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "says nothing about a SATISFY body" do
|
|
115
|
+
lint.("REM 16 July SATISFY [1]\n").should.be.empty
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "says nothing about comments" do
|
|
119
|
+
lint.("# REM 16 July ++10 MSG Jane %b.\n").should.be.empty
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reports at info severity" do
|
|
123
|
+
lint.("REM 16 July ++10 MSG Jane %b.\n").first.severity.should == "info"
|
|
124
|
+
end
|
|
125
|
+
end
|