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,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Hand-written commands in a file a tool owns.
|
|
8
|
+
#
|
|
9
|
+
# TkRemind appends to its own file; `ics2rem` and `radicale-remind`
|
|
10
|
+
# regenerate theirs from the other side of a sync. A command written into
|
|
11
|
+
# one of those by hand survives until the next write and then does not, and
|
|
12
|
+
# in the meantime the file mixes authored and generated state in something
|
|
13
|
+
# no human is expected to read.
|
|
14
|
+
#
|
|
15
|
+
# Which files a tool owns is not something the linter can work out, so it is
|
|
16
|
+
# configured -- `GeneratedFiles` takes shell globs. With none configured the
|
|
17
|
+
# rule has nothing to say, which is the honest default rather than a guess
|
|
18
|
+
# about anybody's layout.
|
|
19
|
+
#
|
|
20
|
+
# Comments are not reported: a note at the top of a generated file is how
|
|
21
|
+
# people mark it as generated in the first place.
|
|
22
|
+
class GeneratedFileEdited < Rule
|
|
23
|
+
# What the tools write, and so what is *not* a hand edit. TkRemind tags
|
|
24
|
+
# everything it appends.
|
|
25
|
+
GENERATED_TAG = /\bTAG\s+"?TKTAG\d+/i
|
|
26
|
+
|
|
27
|
+
def self.default_severity
|
|
28
|
+
"warning"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.description
|
|
32
|
+
"Hand-written commands in a file listed under GeneratedFiles."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check
|
|
36
|
+
patterns = option("GeneratedFiles", [])
|
|
37
|
+
|
|
38
|
+
if !patterns.empty? && generated?(patterns)
|
|
39
|
+
report
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def generated?(patterns)
|
|
46
|
+
patterns.any? do |pattern|
|
|
47
|
+
File.fnmatch?(pattern, document.path.to_s, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def report
|
|
52
|
+
hand_written.each do |command|
|
|
53
|
+
offend(
|
|
54
|
+
command.line,
|
|
55
|
+
"this file is generated, and a hand-written command in it survives only " \
|
|
56
|
+
"until the tool next writes the file",
|
|
57
|
+
column: command.keyword_column,
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Anything the tool did not put there itself.
|
|
63
|
+
def hand_written
|
|
64
|
+
document.code_commands.reject { |command| command.text.match?(GENERATED_TAG) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
__END__
|
|
71
|
+
|
|
72
|
+
require_relative "../document"
|
|
73
|
+
|
|
74
|
+
describe "RemLint::Rules::GeneratedFileEdited" do
|
|
75
|
+
lint = proc do |text, path = "calendar.rem", config = {}|
|
|
76
|
+
source = RemLint::Source.new(path: path, text: text)
|
|
77
|
+
|
|
78
|
+
RemLint::Rules::GeneratedFileEdited.new(config).run(RemLint::Document.new(source))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
owned = { "GeneratedFiles" => ["*.generated.rem", "tkremind/*.rem"] }
|
|
82
|
+
|
|
83
|
+
it "says nothing when no files are declared generated" do
|
|
84
|
+
lint.("REM 1 Jan MSG hi\n").should.be.empty
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "says nothing about a file outside the patterns" do
|
|
88
|
+
lint.("REM 1 Jan MSG hi\n", "mine.rem", owned).should.be.empty
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "reports a hand-written command in a generated file" do
|
|
92
|
+
offenses = lint.("REM 1 Jan MSG hi\n", "notes.generated.rem", owned)
|
|
93
|
+
|
|
94
|
+
offenses.length.should == 1
|
|
95
|
+
offenses.first.message.should.match(/survives only until the tool next writes the file/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "matches a pattern with a directory" do
|
|
99
|
+
lint.("REM 1 Jan MSG hi\n", "tkremind/appts.rem", owned).length.should == 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "does not report what TkRemind wrote itself" do
|
|
103
|
+
text = %(REM 1 Jan TAG "TKTAG1" MSG Appointment\n)
|
|
104
|
+
|
|
105
|
+
lint.(text, "tkremind/appts.rem", owned).should.be.empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "reports only the hand-written commands among generated ones" do
|
|
109
|
+
text = %(REM 1 Jan TAG "TKTAG1" MSG Generated\nREM 2 Jan MSG By hand\n)
|
|
110
|
+
|
|
111
|
+
lint.(text, "tkremind/appts.rem", owned).map(&:line).should == [2]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "says nothing about comments, which is how files are marked generated" do
|
|
115
|
+
lint.("# Generated by TkRemind -- do not edit\n", "tkremind/a.rem", owned).should.be.empty
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "points at the keyword" do
|
|
119
|
+
lint.(" REM 1 Jan MSG hi\n", "a.generated.rem", owned).first.column.should == 4
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reports at warning severity" do
|
|
123
|
+
lint.("REM 1 Jan MSG hi\n", "a.generated.rem", owned).first.severity.should == "warning"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Hebrew dates that cannot exist.
|
|
8
|
+
#
|
|
9
|
+
# `hebdate(day, month, year)` returns `Invalid Hebrew date` for a month
|
|
10
|
+
# Remind does not know and for a day past that month's length. Both are
|
|
11
|
+
# decidable from literals, and both fail on the day the reminder would
|
|
12
|
+
# otherwise have fired.
|
|
13
|
+
#
|
|
14
|
+
# The lengths are `MaxMonLen[]` from src/hbcal.c, and they are *not* the
|
|
15
|
+
# textbook fixed ones: Heshvan and Kislev are recomputed from each year's
|
|
16
|
+
# length -- a Hebrew year runs 353, 354 or 355 days -- so this reports the
|
|
17
|
+
# maximum any year allows. Confirmed against the binary across seven
|
|
18
|
+
# consecutive years.
|
|
19
|
+
#
|
|
20
|
+
# The names are all three of Remind's tables: `HebMonthNames`,
|
|
21
|
+
# `IvritMonthNames` in Hebrew script, and `AltMonthSpellings`, which is why
|
|
22
|
+
# `Tishri`, `Cheshvan`, `Shevat`, `Tammuz`, `Iyyar`, `Adar I` and their
|
|
23
|
+
# Hebrew equivalents are all accepted. Missing that third table reports 31
|
|
24
|
+
# offences against Remind's own tests/test.rem.
|
|
25
|
+
#
|
|
26
|
+
# `hebmon()` returns the canonical transliteration, so a comparison against
|
|
27
|
+
# an accepted-but-non-canonical spelling is never true even where `hebdate`
|
|
28
|
+
# takes it -- which the message says.
|
|
29
|
+
class HebrewDate < Rule
|
|
30
|
+
# `MaxMonLen[]` from src/hbcal.c, in the order its month constants
|
|
31
|
+
# define: Tishrey, Heshvan, Kislev, Tevet, Shvat, Adar A, Adar B,
|
|
32
|
+
# Nisan, Iyar, Sivan, Tamuz, Av, Elul, Adar.
|
|
33
|
+
MAX_LENGTHS = [30, 30, 30, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 29].freeze
|
|
34
|
+
|
|
35
|
+
CANONICAL = [
|
|
36
|
+
"Tishrey", "Heshvan", "Kislev", "Tevet", "Shvat", "Adar A", "Adar B",
|
|
37
|
+
"Nisan", "Iyar", "Sivan", "Tamuz", "Av", "Elul", "Adar"
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# `IvritMonthNames` -- the same months in Hebrew script.
|
|
41
|
+
IVRIT = [
|
|
42
|
+
"\u05EA\u05E9\u05E8\u05D9", "\u05D7\u05E9\u05D5\u05D5\u05DF",
|
|
43
|
+
"\u05DB\u05E1\u05DC\u05D5", "\u05D8\u05D1\u05EA",
|
|
44
|
+
"\u05E9\u05D1\u05D8", "\u05D0\u05D3\u05E8 \u05D0'",
|
|
45
|
+
"\u05D0\u05D3\u05E8 \u05D1'", "\u05E0\u05D9\u05E1\u05DF",
|
|
46
|
+
"\u05D0\u05D9\u05D9\u05E8", "\u05E1\u05D9\u05D5\u05DF",
|
|
47
|
+
"\u05EA\u05DE\u05D5\u05D6", "\u05D0\u05D1",
|
|
48
|
+
"\u05D0\u05DC\u05D5\u05DC", "\u05D0\u05D3\u05E8"
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# `AltMonthSpellings` -- name => the month index it resolves to.
|
|
52
|
+
ALTERNATES = {
|
|
53
|
+
"Tishri" => 0,
|
|
54
|
+
"Tishrei" => 0,
|
|
55
|
+
"Cheshvan" => 1,
|
|
56
|
+
"Kheshvan" => 1,
|
|
57
|
+
"Shevat" => 4,
|
|
58
|
+
"Tammuz" => 10,
|
|
59
|
+
"Adar 1" => 5,
|
|
60
|
+
"Adar I" => 5,
|
|
61
|
+
"\u05D0\u05D3\u05E8 \u05D0" => 5,
|
|
62
|
+
"\u05D0\u05D3\u05E8 1" => 5,
|
|
63
|
+
"\u05D0\u05D3\u05E8 I" => 5,
|
|
64
|
+
"Adar 2" => 6,
|
|
65
|
+
"Adar II" => 6,
|
|
66
|
+
"\u05D0\u05D3\u05E8 \u05D1" => 6,
|
|
67
|
+
"\u05D0\u05D3\u05E8 2" => 6,
|
|
68
|
+
"\u05D0\u05D3\u05E8 II" => 6,
|
|
69
|
+
"Iyyar" => 8,
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
MONTHS = (
|
|
73
|
+
CANONICAL.each_with_index.to_h { |name, index| [name.downcase, MAX_LENGTHS[index]] }
|
|
74
|
+
.merge(IVRIT.each_with_index.to_h { |name, index| [name, MAX_LENGTHS[index]] })
|
|
75
|
+
.merge(ALTERNATES.transform_keys(&:downcase)
|
|
76
|
+
.transform_values { |index| MAX_LENGTHS[index] })
|
|
77
|
+
).freeze
|
|
78
|
+
|
|
79
|
+
# `hebdate(30, "Elul", 5790)` and the two-argument form.
|
|
80
|
+
CALL = /\bhebdate\s*\(\s*(?<day>\d+)\s*,\s*"(?<month>[^"]+)"/i
|
|
81
|
+
|
|
82
|
+
def self.default_severity
|
|
83
|
+
"error"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.description
|
|
87
|
+
"A Hebrew month Remind does not know, or a day past that month's length."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check
|
|
91
|
+
document.logical_lines.each_with_index do |logical_line, index|
|
|
92
|
+
if document.commands[index].code?
|
|
93
|
+
check_line(logical_line)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def check_line(logical_line)
|
|
101
|
+
logical_line.text.to_enum(:scan, CALL).each do
|
|
102
|
+
match = Regexp.last_match
|
|
103
|
+
|
|
104
|
+
report(logical_line, match)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def report(logical_line, match)
|
|
109
|
+
month = match[:month].strip
|
|
110
|
+
longest = MONTHS[month.downcase]
|
|
111
|
+
|
|
112
|
+
if longest.nil?
|
|
113
|
+
offend_at(logical_line, match.begin(0), unknown_message(month))
|
|
114
|
+
elsif match[:day].to_i < 1 || match[:day].to_i > longest
|
|
115
|
+
offend_at(logical_line, match.begin(0), length_message(match[:day], month, longest))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def unknown_message(month)
|
|
120
|
+
"`#{month}` is not a Hebrew month Remind knows; `hebdate` returns " \
|
|
121
|
+
"`Invalid Hebrew date`, and `hebmon()` returns the canonical spelling, " \
|
|
122
|
+
"so a comparison against this is never true either"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def length_message(day, month, longest)
|
|
126
|
+
"`#{month}` never has #{day} days -- #{longest} at most -- so `hebdate` " \
|
|
127
|
+
"returns `Invalid Hebrew date`"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
__END__
|
|
134
|
+
|
|
135
|
+
require_relative "../document"
|
|
136
|
+
|
|
137
|
+
describe "RemLint::Rules::HebrewDate" do
|
|
138
|
+
lint = proc do |text|
|
|
139
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
140
|
+
|
|
141
|
+
RemLint::Rules::HebrewDate.new.run(RemLint::Document.new(source))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
145
|
+
|
|
146
|
+
describe "dates that exist" do
|
|
147
|
+
it "accepts a 30-day month at 30" do
|
|
148
|
+
lint.(%(MSG [hebdate(30, "Nisan", 5790)]\n)).should.be.empty
|
|
149
|
+
lint.(%(MSG [hebdate(30, "Tishrey", 5790)]\n)).should.be.empty
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "accepts the months whose length varies by year at 30" do
|
|
153
|
+
# Heshvan and Kislev are 29 or 30 depending on the year's length.
|
|
154
|
+
lint.(%(MSG [hebdate(30, "Heshvan", 5790)]\n)).should.be.empty
|
|
155
|
+
lint.(%(MSG [hebdate(30, "Kislev", 5790)]\n)).should.be.empty
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "accepts a 29-day month at 29" do
|
|
159
|
+
lint.(%(MSG [hebdate(29, "Elul", 5790)]\n)).should.be.empty
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "accepts the leap-month spellings" do
|
|
163
|
+
lint.(%(MSG [hebdate(29, "Adar A", 5790)]\n)).should.be.empty
|
|
164
|
+
lint.(%(MSG [hebdate(29, "Adar B", 5790)]\n)).should.be.empty
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "accepts the alternate spellings Remind lists" do
|
|
168
|
+
# tests/test.rem exercises every one of these.
|
|
169
|
+
%w[Tishri Tishrei Cheshvan Kheshvan Shevat Tammuz Iyyar].each do |name|
|
|
170
|
+
lint.(%(MSG [hebdate(1, "#{name}", 5790)]\n)).should.be.empty
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "accepts the Adar variants" do
|
|
175
|
+
["Adar 1", "Adar I", "Adar 2", "Adar II"].each do |name|
|
|
176
|
+
lint.(%(MSG [hebdate(1, "#{name}", 5790)]\n)).should.be.empty
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "accepts the Hebrew-script names" do
|
|
181
|
+
lint.(%(MSG [hebdate(1, "\u05EA\u05E9\u05E8\u05D9", 5790)]\n)).should.be.empty
|
|
182
|
+
lint.(%(MSG [hebdate(1, "\u05D0\u05DC\u05D5\u05DC", 5790)]\n)).should.be.empty
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "applies the right length to an alternate spelling" do
|
|
186
|
+
# Cheshvan is Heshvan, which reaches 30; Tammuz is Tamuz, which does not.
|
|
187
|
+
lint.(%(MSG [hebdate(30, "Cheshvan", 5790)]\n)).should.be.empty
|
|
188
|
+
lint.(%(MSG [hebdate(30, "Tammuz", 5790)]\n)).length.should == 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "matches the month name case-insensitively" do
|
|
192
|
+
lint.(%(MSG [hebdate(29, "elul", 5790)]\n)).should.be.empty
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "dates that cannot exist" do
|
|
197
|
+
it "reports a day past a 29-day month" do
|
|
198
|
+
# The book's example, confirmed against Remind.
|
|
199
|
+
messages.(%(MSG [hebdate(30, "Elul", 5790)]\n)).first.should ==
|
|
200
|
+
"`Elul` never has 30 days -- 29 at most -- so `hebdate` returns `Invalid Hebrew date`"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "reports Tevet at 30" do
|
|
204
|
+
messages.(%(MSG [hebdate(30, "Tevet", 5790)]\n)).length.should == 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "reports a day past every month" do
|
|
208
|
+
messages.(%(MSG [hebdate(31, "Nisan", 5790)]\n)).length.should == 1
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "reports a day of zero" do
|
|
212
|
+
messages.(%(MSG [hebdate(0, "Nisan", 5790)]\n)).length.should == 1
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "reports a month Remind does not know" do
|
|
216
|
+
messages.(%(MSG [hebdate(1, "Nosuch", 5790)]\n)).first.should.match(
|
|
217
|
+
/is not a Hebrew month Remind knows/,
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "mentions that hebmon returns the canonical spelling" do
|
|
222
|
+
messages.(%(MSG [hebdate(1, "Nosuch", 5790)]\n)).first.should.match(
|
|
223
|
+
/`hebmon\(\)` returns the canonical spelling/,
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "points at the call" do
|
|
228
|
+
text = %(MSG [hebdate(30, "Elul", 5790)]\n)
|
|
229
|
+
|
|
230
|
+
lint.(text).first.column.should == text.index("hebdate") + 1
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "says nothing about a computed day" do
|
|
235
|
+
lint.(%(MSG [hebdate(d, "Elul", 5790)]\n)).should.be.empty
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "says nothing about comments" do
|
|
239
|
+
lint.(%(# MSG [hebdate(30, "Elul", 5790)]\n)).should.be.empty
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "reports at error severity" do
|
|
243
|
+
lint.(%(MSG [hebdate(30, "Elul", 5790)]\n)).first.severity.should == "error"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `SATISFY` inside an `IFTRIG`.
|
|
8
|
+
#
|
|
9
|
+
# `IFTRIG` accepts any trigger a `REM` would, with exactly one exception:
|
|
10
|
+
# `SATISFY`. The exclusion is trivial to trip over, because the natural way
|
|
11
|
+
# to write an `IFTRIG` is to copy a working `REM` and change the keyword --
|
|
12
|
+
# and every other clause survives that edit.
|
|
13
|
+
class IftrigWithSatisfy < Rule
|
|
14
|
+
def self.default_severity
|
|
15
|
+
"error"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.description
|
|
19
|
+
"SATISFY inside an IFTRIG, which is the one clause IFTRIG does not take."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def check
|
|
23
|
+
document.code_commands.each do |command|
|
|
24
|
+
if command.keyword?("IFTRIG")
|
|
25
|
+
check_iftrig(command)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def check_iftrig(command)
|
|
33
|
+
satisfy = document.trigger_for(command).body
|
|
34
|
+
|
|
35
|
+
if satisfy&.name == "SATISFY"
|
|
36
|
+
offend_at(
|
|
37
|
+
command.logical_line,
|
|
38
|
+
satisfy.offset,
|
|
39
|
+
"`IFTRIG` takes every clause `REM` does except `SATISFY`",
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
__END__
|
|
48
|
+
|
|
49
|
+
require_relative "../document"
|
|
50
|
+
|
|
51
|
+
describe "RemLint::Rules::IftrigWithSatisfy" do
|
|
52
|
+
lint = proc do |text|
|
|
53
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
54
|
+
|
|
55
|
+
RemLint::Rules::IftrigWithSatisfy.new.run(RemLint::Document.new(source))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "reports SATISFY in an IFTRIG" do
|
|
59
|
+
offenses = lint.("IFTRIG 13 SATISFY [$Tw == 5]\nMSG Friday the 13th\nENDIF\n")
|
|
60
|
+
|
|
61
|
+
offenses.length.should == 1
|
|
62
|
+
offenses.first.line.should == 1
|
|
63
|
+
offenses.first.message.should.match(/except `SATISFY`/)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "points at the SATISFY" do
|
|
67
|
+
text = "IFTRIG 13 SATISFY [$Tw == 5]\n"
|
|
68
|
+
|
|
69
|
+
lint.(text).first.column.should == text.index("SATISFY") + 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "accepts an IFTRIG with any other clause" do
|
|
73
|
+
lint.("IFTRIG 1 Jan UNTIL 2027-01-01 AT 15:00\n").should.be.empty
|
|
74
|
+
lint.("IFTRIG Tue SCANFROM -7\n").should.be.empty
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "accepts SATISFY on a REM, where it is legal" do
|
|
78
|
+
lint.("REM 13 SATISFY [$Tw == 5] MSG Boo!\n").should.be.empty
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "matches an abbreviated IFTRIG" do
|
|
82
|
+
# IFTRIG's minimum abbreviation length is 6, so IFTRIG is the short form.
|
|
83
|
+
lint.("IFTRIG 13 SATISFY [1]\n").length.should == 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "is case-insensitive" do
|
|
87
|
+
lint.("iftrig 13 satisfy [1]\n").length.should == 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "says nothing about the word satisfy in a comment" do
|
|
91
|
+
lint.("# IFTRIG and SATISFY do not mix\n").should.be.empty
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "reports at error severity" do
|
|
95
|
+
lint.("IFTRIG 13 SATISFY [1]\n").first.severity.should == "error"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "reports the physical line inside a continuation" do
|
|
99
|
+
lint.("IFTRIG 13 \\\n SATISFY [1]\n").first.line.should == 2
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `INCLUDE` and `SYSINCLUDE` paths that do not mean what they look like.
|
|
8
|
+
#
|
|
9
|
+
# TWO KEYWORDS, TWO BASE DIRECTORIES. `INCLUDE` resolves a relative path
|
|
10
|
+
# against the *working directory*, not against the directory of the file
|
|
11
|
+
# doing the including. The manual calls this a mistake kept for
|
|
12
|
+
# compatibility, and `DO` is the keyword that resolves relative to the
|
|
13
|
+
# file. So `INCLUDE defs.rem` works when you run Remind from the directory
|
|
14
|
+
# the script lives in and fails from anywhere else -- from cron, most
|
|
15
|
+
# memorably.
|
|
16
|
+
#
|
|
17
|
+
# `SYSINCLUDE` with an absolute path is the mirror image: for an absolute
|
|
18
|
+
# path it is simply `INCLUDE`, so the keyword reads as though it pointed
|
|
19
|
+
# into the system include directory when it does not.
|
|
20
|
+
#
|
|
21
|
+
# A path built from an expression -- `[$SysInclude]/ansitext.rem`, which is
|
|
22
|
+
# how `examples/ansitext` does it -- is left alone, because what it
|
|
23
|
+
# resolves to is not decidable here.
|
|
24
|
+
class IncludePath < Rule
|
|
25
|
+
ABSOLUTE = %r{\A/}
|
|
26
|
+
|
|
27
|
+
def self.default_severity
|
|
28
|
+
"warning"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.description
|
|
32
|
+
"INCLUDE with a relative path, where DO resolves relative to the file, " \
|
|
33
|
+
"or SYSINCLUDE with an absolute one."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check
|
|
37
|
+
document.code_commands.each do |command|
|
|
38
|
+
path = literal_path(command)
|
|
39
|
+
|
|
40
|
+
if path
|
|
41
|
+
report(command, path)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# The argument, when it is a plain path rather than something computed.
|
|
49
|
+
def literal_path(command)
|
|
50
|
+
if command.keyword?("INCLUDE", "SYSINCLUDE")
|
|
51
|
+
text = command.args.strip
|
|
52
|
+
|
|
53
|
+
unless text.empty? || text.include?("[") || text.include?("$")
|
|
54
|
+
text
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def report(command, path)
|
|
60
|
+
if command.keyword?("INCLUDE") && !path.match?(ABSOLUTE)
|
|
61
|
+
offend(command.line, relative_message(path), column: command.keyword_column)
|
|
62
|
+
elsif command.keyword?("SYSINCLUDE") && path.match?(ABSOLUTE)
|
|
63
|
+
offend(command.line, absolute_message(path), column: command.keyword_column)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def relative_message(path)
|
|
68
|
+
"`INCLUDE #{path}` resolves against the working directory, not this file's " \
|
|
69
|
+
"directory; `DO #{path}` resolves relative to the file"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def absolute_message(path)
|
|
73
|
+
"`SYSINCLUDE #{path}` is just `INCLUDE` for an absolute path, so the keyword " \
|
|
74
|
+
"reads as though it pointed into the system include directory"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
__END__
|
|
81
|
+
|
|
82
|
+
require_relative "../document"
|
|
83
|
+
|
|
84
|
+
describe "RemLint::Rules::IncludePath" do
|
|
85
|
+
lint = proc do |text|
|
|
86
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
87
|
+
|
|
88
|
+
RemLint::Rules::IncludePath.new.run(RemLint::Document.new(source))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
92
|
+
|
|
93
|
+
describe "INCLUDE" do
|
|
94
|
+
it "reports a relative path" do
|
|
95
|
+
messages.("INCLUDE defs.rem\n").first.should ==
|
|
96
|
+
"`INCLUDE defs.rem` resolves against the working directory, not this file's " \
|
|
97
|
+
"directory; `DO defs.rem` resolves relative to the file"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "reports a path with a directory component" do
|
|
101
|
+
messages.("INCLUDE holidays/us.rem\n").length.should == 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "reports a path that starts with a dot" do
|
|
105
|
+
messages.("INCLUDE ./defs.rem\n").length.should == 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "accepts an absolute path" do
|
|
109
|
+
lint.("INCLUDE /etc/remind/defs.rem\n").should.be.empty
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "accepts DO, which resolves relative to the file" do
|
|
113
|
+
lint.("DO defs.rem\n").should.be.empty
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "matches the abbreviated keyword" do
|
|
117
|
+
messages.("INC defs.rem\n").length.should == 1
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "SYSINCLUDE" do
|
|
122
|
+
it "reports an absolute path" do
|
|
123
|
+
messages.("SYSINCLUDE /etc/remind/defs.rem\n").first.should.match(
|
|
124
|
+
/is just `INCLUDE` for an absolute path/,
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "accepts a relative path, which is what it is for" do
|
|
129
|
+
lint.("SYSINCLUDE ansitext.rem\n").should.be.empty
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
describe "paths it cannot resolve" do
|
|
134
|
+
it "says nothing about a bracketed expression" do
|
|
135
|
+
# How examples/ansitext writes it.
|
|
136
|
+
lint.("INCLUDE [$SysInclude]/ansitext.rem\n").should.be.empty
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "says nothing about a path built from a variable" do
|
|
140
|
+
lint.("INCLUDE $SomeDir/defs.rem\n").should.be.empty
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "says nothing about comments" do
|
|
145
|
+
lint.("# INCLUDE defs.rem\n").should.be.empty
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "points at the keyword" do
|
|
149
|
+
lint.(" INCLUDE defs.rem\n").first.column.should == 4
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "reports at warning severity" do
|
|
153
|
+
lint.("INCLUDE defs.rem\n").first.severity.should == "warning"
|
|
154
|
+
end
|
|
155
|
+
end
|