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,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Callback functions defined with the wrong number of arguments.
|
|
8
|
+
#
|
|
9
|
+
# Remind calls a dozen or so functions by name and by shape. It does not
|
|
10
|
+
# look them up and adapt -- it checks the arity and, if it does not match,
|
|
11
|
+
# walks away:
|
|
12
|
+
#
|
|
13
|
+
# UserFuncExists("msgprefix") == 1 src/dorem.c
|
|
14
|
+
# check_subst_args(func, 3) src/dosubst.c
|
|
15
|
+
#
|
|
16
|
+
# So a `msgprefix(p, q)` is never called. Not called with an error, not
|
|
17
|
+
# called with a warning: the prefix simply does not appear, and the
|
|
18
|
+
# obvious conclusion is that the callback mechanism is broken rather than
|
|
19
|
+
# that the signature is.
|
|
20
|
+
#
|
|
21
|
+
# This is `FunctionArity` turned around. That rule checks calls against the
|
|
22
|
+
# functions they name; this one checks *definitions* against the shape
|
|
23
|
+
# Remind will call them with.
|
|
24
|
+
class CallbackSignature < Rule
|
|
25
|
+
# name => arity Remind insists on.
|
|
26
|
+
#
|
|
27
|
+
# One argument: the prefix and suffix callbacks (given the priority) and
|
|
28
|
+
# the two ordinal helpers. Three: the substitution-filter family, called
|
|
29
|
+
# as `name(altmode, date, time)`.
|
|
30
|
+
ONE_ARGUMENT = %w[msgprefix msgsuffix calprefix calsuffix ordx subst_ampm subst_ordinal].freeze
|
|
31
|
+
|
|
32
|
+
THREE_ARGUMENTS = %w[
|
|
33
|
+
subst_at subst_atx subst_bang subst_bangx subst_colon subst_colonx
|
|
34
|
+
subst_hash subst_hashx subst_question subst_questionx
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
ARITIES = (
|
|
38
|
+
ONE_ARGUMENT.to_h { |name| [name, 1] }
|
|
39
|
+
.merge(THREE_ARGUMENTS.to_h { |name| [name, 3] })
|
|
40
|
+
).freeze
|
|
41
|
+
|
|
42
|
+
# `get_function_override` builds `subst_<c>` and `subst_<c>x` for a
|
|
43
|
+
# *single* alphanumeric or underscore character (src/dosubst.c). So
|
|
44
|
+
# `subst_a` and `subst_ax` are callbacks and `subst_a_alt` is not -- it
|
|
45
|
+
# is an ordinary helper, and Remind's own language packs are full of
|
|
46
|
+
# them. Getting this wrong reports 45 offences against `include/lang`.
|
|
47
|
+
OVERRIDE = /\Asubst_[a-z0-9_]x?\z/
|
|
48
|
+
|
|
49
|
+
# A `%{name}` in the file makes `subst_name` a callback, called with the
|
|
50
|
+
# same three arguments.
|
|
51
|
+
NAMED = /%\{(?<name>[^}]*)\}/
|
|
52
|
+
|
|
53
|
+
DEFINITION = /\A(?:-\s*)?(?<name>[A-Za-z_]\w*)\s*\((?<params>[^)]*)\)/
|
|
54
|
+
|
|
55
|
+
def self.default_severity
|
|
56
|
+
"warning"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.description
|
|
60
|
+
"A Remind callback function defined with the wrong number of arguments."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check
|
|
64
|
+
@named = named_substitutions
|
|
65
|
+
|
|
66
|
+
document.code_commands.each do |command|
|
|
67
|
+
if command.keyword?("FSET")
|
|
68
|
+
check_definition(command)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def check_definition(command)
|
|
76
|
+
match = command.args.match(DEFINITION)
|
|
77
|
+
expected = match && arity_for(match[:name].downcase)
|
|
78
|
+
|
|
79
|
+
if expected
|
|
80
|
+
compare(command, match, expected)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def arity_for(name)
|
|
85
|
+
if ARITIES.key?(name)
|
|
86
|
+
ARITIES.fetch(name)
|
|
87
|
+
elsif name.match?(OVERRIDE) || @named.include?(name)
|
|
88
|
+
3
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# `subst_` names this file actually asks Remind to call.
|
|
93
|
+
def named_substitutions
|
|
94
|
+
document.code_commands.flat_map do |command|
|
|
95
|
+
command.text.scan(NAMED).flatten.map { |name| "subst_#{name.downcase}" }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compare(command, match, expected)
|
|
100
|
+
actual = count(match[:params])
|
|
101
|
+
|
|
102
|
+
if actual != expected
|
|
103
|
+
offend(command.line, message(match[:name], expected, actual), column: command.keyword_column)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def count(params)
|
|
108
|
+
if params.strip.empty?
|
|
109
|
+
0
|
|
110
|
+
else
|
|
111
|
+
params.split(",").length
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def message(name, expected, actual)
|
|
116
|
+
"`#{name}` is a Remind callback and is called with #{expected} " \
|
|
117
|
+
"argument#{expected == 1 ? '' : 's'}, not #{actual}; defined this way it is " \
|
|
118
|
+
"never called at all, and nothing says so"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
__END__
|
|
125
|
+
|
|
126
|
+
require_relative "../document"
|
|
127
|
+
|
|
128
|
+
describe "RemLint::Rules::CallbackSignature" do
|
|
129
|
+
lint = proc do |text|
|
|
130
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
131
|
+
|
|
132
|
+
RemLint::Rules::CallbackSignature.new.run(RemLint::Document.new(source))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
136
|
+
|
|
137
|
+
describe "the one-argument callbacks" do
|
|
138
|
+
it "accepts the right shape" do
|
|
139
|
+
lint.(%(FSET msgprefix(p) "[" + p + "] "\n)).should.be.empty
|
|
140
|
+
lint.(%(FSET msgsuffix(p) char(8)\n)).should.be.empty
|
|
141
|
+
lint.(%(FSET calprefix(p) ""\n)).should.be.empty
|
|
142
|
+
lint.(%(FSET ordx(n) "th"\n)).should.be.empty
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "reports two arguments" do
|
|
146
|
+
messages.(%(FSET msgprefix(p, q) ""\n)).first.should ==
|
|
147
|
+
"`msgprefix` is a Remind callback and is called with 1 argument, not 2; " \
|
|
148
|
+
"defined this way it is never called at all, and nothing says so"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "reports none" do
|
|
152
|
+
messages.(%(FSET msgprefix() ""\n)).length.should == 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "accepts subst_ampm and subst_ordinal, which take one" do
|
|
156
|
+
lint.(%(FSET subst_ampm(x) "am"\n)).should.be.empty
|
|
157
|
+
lint.(%(FSET subst_ordinal(x) "st"\n)).should.be.empty
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe "the three-argument substitution family" do
|
|
162
|
+
it "accepts the right shape" do
|
|
163
|
+
lint.(%(FSET subst_at(a, d, t) "at"\n)).should.be.empty
|
|
164
|
+
lint.(%(FSET subst_bangx(a, d, t) "is"\n)).should.be.empty
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "reports the wrong shape" do
|
|
168
|
+
messages.(%(FSET subst_at(a, d) "at"\n)).first.should.match(
|
|
169
|
+
/called with 3 arguments, not 2/,
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "applies to the single-character overrides Remind builds" do
|
|
174
|
+
messages.(%(FSET subst_a(x) "y"\n)).first.should.match(/called with 3 arguments, not 1/)
|
|
175
|
+
messages.(%(FSET subst_ax(x) "y"\n)).first.should.match(/called with 3 arguments, not 1/)
|
|
176
|
+
lint.(%(FSET subst_a(a, d, t) "y"\n)).should.be.empty
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "applies to a subst_ name the file reaches through %{name}" do
|
|
180
|
+
text = %(FSET subst_myown(a) "x"\nREM 1 Jan MSG %{myown}\n)
|
|
181
|
+
|
|
182
|
+
messages.(text).first.should.match(/called with 3 arguments, not 1/)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "leaves an ordinary subst_ helper alone" do
|
|
186
|
+
# include/lang/*.rem define dozens of these, and Remind never calls them
|
|
187
|
+
# -- they are helpers the pack calls itself.
|
|
188
|
+
lint.(%(FSET subst_a_alt(x) "y"\n)).should.be.empty
|
|
189
|
+
lint.(%(FSET subst_hours(x) "y"\n)).should.be.empty
|
|
190
|
+
lint.(%(FSET subst_tdiff(a, b) "y"\n)).should.be.empty
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
describe "what it leaves alone" do
|
|
195
|
+
it "says nothing about an ordinary function" do
|
|
196
|
+
lint.("FSET double(x) x * 2\n").should.be.empty
|
|
197
|
+
lint.("FSET greet(a, b, c, d) a\n").should.be.empty
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "matches the callback name case-insensitively" do
|
|
201
|
+
messages.(%(FSET MsgPrefix(p, q) ""\n)).length.should == 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "accepts the FSET - form and still checks the shape" do
|
|
205
|
+
lint.(%(FSET - msgprefix(p) ""\n)).should.be.empty
|
|
206
|
+
messages.(%(FSET - msgprefix(p, q) ""\n)).length.should == 1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "says nothing about comments" do
|
|
210
|
+
lint.(%(# FSET msgprefix(p, q) ""\n)).should.be.empty
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "points at the keyword" do
|
|
215
|
+
lint.(%( FSET msgprefix(p, q) ""\n)).first.column.should == 4
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "reports at warning severity" do
|
|
219
|
+
lint.(%(FSET msgprefix(p, q) ""\n)).first.severity.should == "warning"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../date_literal"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# Clauses given a partial date where a full one is required.
|
|
9
|
+
#
|
|
10
|
+
# `FROM`, `UNTIL`, `THROUGH` and `COMPLETE-THROUGH` all read their argument
|
|
11
|
+
# with `GetFullDate`, which insists on a year, a month and a day. A partial
|
|
12
|
+
# date is `E_PARSE_ERR`.
|
|
13
|
+
#
|
|
14
|
+
# `COMPLETE-THROUGH` is the one that costs most. It is the starting point
|
|
15
|
+
# of a TODO's whole trigger calculation, so a partial date does not merely
|
|
16
|
+
# fail -- when the clause is missing or unusable the algorithm starts at
|
|
17
|
+
# 1990-01-01 and the TODO is overdue by decades on its first run.
|
|
18
|
+
#
|
|
19
|
+
# Only reported when the argument is written out. A computed `[expr]` may
|
|
20
|
+
# yield a perfectly good DATE and is not this rule's business.
|
|
21
|
+
#
|
|
22
|
+
# `OMIT` is excluded entirely. `OMIT ... THROUGH ...` is the omit-range
|
|
23
|
+
# syntax rather than a reminder's expiry, and it takes partial dates on
|
|
24
|
+
# purpose: Remind's own `tests/test.rem` writes `OMIT Jun THROUGH July 15`
|
|
25
|
+
# and `OMIT Apr 2022 through July`.
|
|
26
|
+
class ClauseNeedsFullDate < Rule
|
|
27
|
+
CLAUSES = %w[FROM UNTIL THROUGH COMPLETE-THROUGH].freeze
|
|
28
|
+
|
|
29
|
+
def self.default_severity
|
|
30
|
+
"error"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.description
|
|
34
|
+
"FROM, UNTIL, THROUGH or COMPLETE-THROUGH given a date that is not fully specified."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check
|
|
38
|
+
document.code_commands.each do |command|
|
|
39
|
+
unless command.keyword?("OMIT")
|
|
40
|
+
check_command(command)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def check_command(command)
|
|
48
|
+
trigger = document.trigger_for(command)
|
|
49
|
+
|
|
50
|
+
CLAUSES.each do |name|
|
|
51
|
+
check_clause(command, trigger.find(name))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_clause(command, clause)
|
|
56
|
+
if clause && !computed?(command, clause) && missing(command, clause)
|
|
57
|
+
offend_at(command.logical_line, clause.offset, message(clause, command))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A bracketed expression right after the keyword supplies the date at
|
|
62
|
+
# run time.
|
|
63
|
+
def computed?(command, clause)
|
|
64
|
+
tokens = document.tokens_for(command.logical_line)
|
|
65
|
+
|
|
66
|
+
tokens[clause.argument_index]&.type == :lbracket
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The components `GetFullDate` did not get.
|
|
70
|
+
def missing(command, clause)
|
|
71
|
+
tokens = document.tokens_for(command.logical_line)
|
|
72
|
+
date = DateLiteral.at(tokens, clause.argument_index)
|
|
73
|
+
|
|
74
|
+
date.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def message(clause, command)
|
|
78
|
+
"`#{clause.name}` needs a full date -- a year, a month and a day -- " \
|
|
79
|
+
"and #{written(command, clause)} is not one"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def written(command, clause)
|
|
83
|
+
rest = command.text[clause.end_offset..].to_s
|
|
84
|
+
words = rest.strip.split(/\s+/).first(3).join(" ")
|
|
85
|
+
|
|
86
|
+
"`#{words}`"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
__END__
|
|
93
|
+
|
|
94
|
+
require_relative "../document"
|
|
95
|
+
|
|
96
|
+
describe "RemLint::Rules::ClauseNeedsFullDate" do
|
|
97
|
+
lint = proc do |text|
|
|
98
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
99
|
+
|
|
100
|
+
RemLint::Rules::ClauseNeedsFullDate.new.run(RemLint::Document.new(source))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
104
|
+
|
|
105
|
+
describe "dates that are complete" do
|
|
106
|
+
it "accepts the ISO form" do
|
|
107
|
+
lint.("REM Mon UNTIL 2027-01-01 MSG hi\n").should.be.empty
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "accepts the spelled-out form" do
|
|
111
|
+
lint.("REM Mon UNTIL 1 Jan 2027 MSG hi\n").should.be.empty
|
|
112
|
+
lint.("REM Mon FROM Jan 1 2027 MSG hi\n").should.be.empty
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "accepts COMPLETE-THROUGH with a full date" do
|
|
116
|
+
lint.("REM Mon COMPLETE-THROUGH 2027-01-01 TODO MSG hi\n").should.be.empty
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "dates that are partial" do
|
|
121
|
+
it "reports a missing year" do
|
|
122
|
+
messages.("REM Mon UNTIL 1 Jan MSG hi\n").first.should ==
|
|
123
|
+
"`UNTIL` needs a full date -- a year, a month and a day -- and `1 Jan MSG` is not one"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "reports a missing day" do
|
|
127
|
+
messages.("REM Mon FROM Jan 2027 MSG hi\n").length.should == 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "reports a bare year" do
|
|
131
|
+
messages.("REM Mon UNTIL 2027 MSG hi\n").length.should == 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "reports each clause that is short" do
|
|
135
|
+
messages.("REM Mon FROM 1 Jan UNTIL 2 Feb MSG hi\n").length.should == 2
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "reports THROUGH" do
|
|
139
|
+
messages.("REM Mon THROUGH 1 Jan MSG hi\n").first.should.match(/`THROUGH` needs a full date/)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "points at the clause" do
|
|
143
|
+
text = "REM Mon UNTIL 1 Jan MSG hi\n"
|
|
144
|
+
|
|
145
|
+
lint.(text).first.column.should == text.index("UNTIL") + 1
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "arguments it cannot judge" do
|
|
150
|
+
it "says nothing about a computed date" do
|
|
151
|
+
lint.("REM Mon UNTIL [expiry()] MSG hi\n").should.be.empty
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "says nothing about a reminder with none of these clauses" do
|
|
156
|
+
lint.("REM Mon MSG hi\n").should.be.empty
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "says nothing about SCANFROM, which also takes a delta" do
|
|
160
|
+
lint.("REM Mon SCANFROM -7 MSG hi\n").should.be.empty
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "says nothing about THROUGH inside an OMIT, which takes partial dates" do
|
|
164
|
+
# Both forms are in Remind's own tests/test.rem and both work.
|
|
165
|
+
lint.("OMIT Jun THROUGH July 15\n").should.be.empty
|
|
166
|
+
lint.("OMIT Apr 2022 through July\n").should.be.empty
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "says nothing about comments" do
|
|
170
|
+
lint.("# REM Mon UNTIL 1 Jan MSG hi\n").should.be.empty
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "reports at error severity" do
|
|
174
|
+
lint.("REM Mon UNTIL 1 Jan MSG hi\n").first.severity.should == "error"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Clauses that need a time, on a reminder that has none.
|
|
8
|
+
#
|
|
9
|
+
# `AT` is what gives a reminder a time of day. Two other clauses are only
|
|
10
|
+
# meaningful once it does:
|
|
11
|
+
#
|
|
12
|
+
# DURATION a duration with no start time has nothing to be a duration
|
|
13
|
+
# of, and the back-ends have nowhere to place it
|
|
14
|
+
# TZ a bare date cannot be converted between zones -- there is no
|
|
15
|
+
# instant to convert
|
|
16
|
+
#
|
|
17
|
+
# Both fail in the worst way: the reminder parses, runs, and quietly does
|
|
18
|
+
# something other than what the clause says. `TZ` without `AT` is the one
|
|
19
|
+
# that bites months later, when a reminder fires on a day the author never
|
|
20
|
+
# intended.
|
|
21
|
+
#
|
|
22
|
+
# `AT` is not the only way to get a time, though, and Remind's own
|
|
23
|
+
# `include/lunar-eclipses.rem` is 142 lines of the other way:
|
|
24
|
+
#
|
|
25
|
+
# REM NOQUEUE [utctolocal('2097-04-26@10:37')] DURATION 196 MSG ...
|
|
26
|
+
#
|
|
27
|
+
# A pasted expression that evaluates to a DATETIME supplies the date *and*
|
|
28
|
+
# the time, so `DURATION` there is correct. Whether `utctolocal(...)`
|
|
29
|
+
# returns a DATETIME is not decidable without running it -- so a trigger
|
|
30
|
+
# containing any bracketed expression is left alone. Same principle as
|
|
31
|
+
# FunctionArity staying quiet about functions it has not seen defined:
|
|
32
|
+
# where the linter cannot know, it says nothing.
|
|
33
|
+
class ClauseRequiresAt < Rule
|
|
34
|
+
REASONS = {
|
|
35
|
+
"DURATION" => "a duration needs a start time",
|
|
36
|
+
"TZ" => "a bare date has no instant to convert between zones",
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
def self.default_severity
|
|
40
|
+
"error"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.description
|
|
44
|
+
"DURATION or TZ on a reminder with no AT clause."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check
|
|
48
|
+
document.code_commands.each do |command|
|
|
49
|
+
check_command(command)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def check_command(command)
|
|
56
|
+
trigger = document.trigger_for(command)
|
|
57
|
+
|
|
58
|
+
unless trigger.include?("AT") || computed_time?(command, trigger)
|
|
59
|
+
report_orphans(command, trigger)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Any bracketed expression before the body could be the DATETIME that
|
|
64
|
+
# supplies the time.
|
|
65
|
+
def computed_time?(command, trigger)
|
|
66
|
+
limit = trigger.body_offset || command.text.length
|
|
67
|
+
|
|
68
|
+
document.tokens_for(command.logical_line).any? do |token|
|
|
69
|
+
token.type == :lbracket && token.offset < limit
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def report_orphans(command, trigger)
|
|
74
|
+
REASONS.each do |name, reason|
|
|
75
|
+
clause = trigger.find(name)
|
|
76
|
+
|
|
77
|
+
if clause
|
|
78
|
+
offend_at(
|
|
79
|
+
command.logical_line,
|
|
80
|
+
clause.offset,
|
|
81
|
+
"`#{name}` without an `AT` clause: #{reason}",
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
__END__
|
|
91
|
+
|
|
92
|
+
require_relative "../document"
|
|
93
|
+
|
|
94
|
+
describe "RemLint::Rules::ClauseRequiresAt" do
|
|
95
|
+
lint = proc do |text|
|
|
96
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
97
|
+
|
|
98
|
+
RemLint::Rules::ClauseRequiresAt.new.run(RemLint::Document.new(source))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
102
|
+
|
|
103
|
+
describe "DURATION" do
|
|
104
|
+
it "is reported without AT" do
|
|
105
|
+
messages.("REM 1 Jan DURATION 1:00 MSG Meeting\n").first.should.match(/`DURATION` without an `AT`/)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "is accepted with AT" do
|
|
109
|
+
lint.("REM 1 Jan AT 15:00 DURATION 1:00 MSG Meeting\n").should.be.empty
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "is accepted with AT written after it" do
|
|
113
|
+
lint.("REM 1 Jan DURATION 1:00 AT 15:00 MSG Meeting\n").should.be.empty
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "TZ" do
|
|
118
|
+
it "is reported without AT" do
|
|
119
|
+
messages.("REM 1 Jan TZ America/Toronto MSG hi\n").first.should.match(/`TZ` without an `AT`/)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "is accepted with AT" do
|
|
123
|
+
lint.("REM 1 Jan AT 15:00 TZ America/Toronto MSG hi\n").should.be.empty
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "reports both clauses on one reminder" do
|
|
128
|
+
messages.("REM 1 Jan DURATION 1:00 TZ UTC MSG hi\n").length.should == 2
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "points at the offending clause" do
|
|
132
|
+
text = "REM 1 Jan DURATION 1:00 MSG Meeting\n"
|
|
133
|
+
|
|
134
|
+
lint.(text).first.column.should == text.index("DURATION") + 1
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "what it leaves alone" do
|
|
138
|
+
it "says nothing about a reminder with neither clause" do
|
|
139
|
+
lint.("REM 1 Jan MSG hi\n").should.be.empty
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "says nothing when the time could come from a pasted expression" do
|
|
143
|
+
# Straight out of include/lunar-eclipses.rem: the DATETIME the expression
|
|
144
|
+
# returns carries the time, so DURATION is correct here.
|
|
145
|
+
text = "REM NOQUEUE [utctolocal('2097-04-26@10:37')] DURATION 196 MSG Eclipse\n"
|
|
146
|
+
|
|
147
|
+
lint.(text).should.be.empty
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "still reports when the bracket is only in the body" do
|
|
151
|
+
# A body expression cannot supply the trigger's time.
|
|
152
|
+
lint.("REM 1 Jan DURATION 1:00 MSG at [place]\n").length.should == 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "does not read the words out of a message body" do
|
|
156
|
+
# `duration` and the zone name are English here, not clauses.
|
|
157
|
+
lint.("REM 1 Jan MSG Confirm the duration with the TZ America/Toronto team\n").should.be.empty
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "says nothing about comments" do
|
|
161
|
+
lint.("# REM 1 Jan TZ UTC MSG hi\n").should.be.empty
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "matches abbreviated clause keywords" do
|
|
166
|
+
# DURATION's minimum length is 8, TZ's is 2, AT's is 2.
|
|
167
|
+
lint.("REM 1 Jan AT 15:00 DURATION 1:00 MSG hi\n").should.be.empty
|
|
168
|
+
messages.("REM 1 Jan TZ UTC MSG hi\n").length.should == 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "reports at error severity" do
|
|
172
|
+
lint.("REM 1 Jan TZ UTC MSG hi\n").first.severity.should == "error"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "reports the physical line inside a continuation" do
|
|
176
|
+
lint.("REM 1 Jan \\\n TZ UTC MSG hi\n").first.line.should == 2
|
|
177
|
+
end
|
|
178
|
+
end
|