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,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../expr_lexer"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# `$Latitude` or `$Longitude` set to something that is not a string.
|
|
9
|
+
#
|
|
10
|
+
# Remind has no floating-point type. A coordinate therefore has to arrive
|
|
11
|
+
# as a STRING and be parsed from it -- `SET $Latitude "45.42"`, with the
|
|
12
|
+
# quotes. `SET $Latitude 45.42` is not a near miss with a rounding error in
|
|
13
|
+
# it; `45.42` is not a Remind literal at all.
|
|
14
|
+
#
|
|
15
|
+
# `examples/astro` gets this right the hard way, threading the values in
|
|
16
|
+
# from the shell as strings:
|
|
17
|
+
#
|
|
18
|
+
# remind -g "-i\$Latitude=\"$latitude\"" ...
|
|
19
|
+
#
|
|
20
|
+
# The failure is quiet and confident. Remind falls back or truncates, and
|
|
21
|
+
# then reports sunrise and sunset times that look entirely plausible and
|
|
22
|
+
# are for somewhere else.
|
|
23
|
+
class CoordinateNotString < Rule
|
|
24
|
+
COORDINATES = %w[latitude longitude].freeze
|
|
25
|
+
|
|
26
|
+
# `SET $Latitude <value>`, with the value running to end of line.
|
|
27
|
+
ASSIGNMENT = /\A\$(?<name>\w+)\s+(?<value>.+)\z/m
|
|
28
|
+
|
|
29
|
+
def self.default_severity
|
|
30
|
+
"error"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.description
|
|
34
|
+
"$Latitude or $Longitude set to something other than a string."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check
|
|
38
|
+
document.code_commands.each do |command|
|
|
39
|
+
if command.keyword?("SET")
|
|
40
|
+
check_assignment(command)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def check_assignment(command)
|
|
48
|
+
match = command.args.match(ASSIGNMENT)
|
|
49
|
+
|
|
50
|
+
if match && COORDINATES.include?(match[:name].downcase)
|
|
51
|
+
check_value(command, match)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_value(command, match)
|
|
56
|
+
value = match[:value].strip
|
|
57
|
+
tokens = ExprLexer.significant(value)
|
|
58
|
+
|
|
59
|
+
if literal_non_string?(tokens)
|
|
60
|
+
offend(
|
|
61
|
+
command.line,
|
|
62
|
+
"`$#{match[:name]}` takes a STRING -- Remind has no floating-point " \
|
|
63
|
+
"type, so write `\"#{value}\"`",
|
|
64
|
+
column: command.keyword_column,
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Only a literal is reported. A single string token is right; a single
|
|
70
|
+
# bracketed or computed value cannot be judged without running it, and
|
|
71
|
+
# `[latitude_from_gps()]` is a perfectly good way to set this.
|
|
72
|
+
def literal_non_string?(tokens)
|
|
73
|
+
types = tokens.map(&:type)
|
|
74
|
+
|
|
75
|
+
types.first != :string &&
|
|
76
|
+
!types.include?(:lbracket) &&
|
|
77
|
+
!types.include?(:function) &&
|
|
78
|
+
!types.include?(:name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
__END__
|
|
85
|
+
|
|
86
|
+
require_relative "../document"
|
|
87
|
+
|
|
88
|
+
describe "RemLint::Rules::CoordinateNotString" do
|
|
89
|
+
lint = proc do |text|
|
|
90
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
91
|
+
|
|
92
|
+
RemLint::Rules::CoordinateNotString.new.run(RemLint::Document.new(source))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
96
|
+
|
|
97
|
+
describe "coordinates written as numbers" do
|
|
98
|
+
it "reports a bare decimal" do
|
|
99
|
+
messages.("SET $Latitude 45.42\n").first.should ==
|
|
100
|
+
%(`$Latitude` takes a STRING -- Remind has no floating-point type, so write `"45.42"`)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "reports a negative decimal" do
|
|
104
|
+
messages.("SET $Longitude -75.69\n").length.should == 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "reports a bare integer" do
|
|
108
|
+
messages.("SET $Latitude 45\n").length.should == 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "reports both coordinates" do
|
|
112
|
+
messages.("SET $Latitude 45.42\nSET $Longitude -75.69\n").length.should == 2
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "matches the variable name case-insensitively" do
|
|
116
|
+
messages.("SET $latitude 45.42\n").length.should == 1
|
|
117
|
+
messages.("SET $LONGITUDE 45.42\n").length.should == 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "points at the keyword" do
|
|
121
|
+
lint.(" SET $Latitude 45.42\n").first.column.should == 4
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "coordinates written correctly" do
|
|
126
|
+
it "accepts a double-quoted string" do
|
|
127
|
+
lint.(%(SET $Latitude "45.42"\n)).should.be.empty
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "accepts a negative value in a string" do
|
|
131
|
+
lint.(%(SET $Longitude "-75.69"\n)).should.be.empty
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "accepts the degrees-minutes-seconds string form" do
|
|
135
|
+
lint.(%(SET $Latitude "45 25 12"\n)).should.be.empty
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe "values it cannot judge" do
|
|
140
|
+
it "says nothing about a pasted expression" do
|
|
141
|
+
lint.("SET $Latitude [gps_latitude()]\n").should.be.empty
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "says nothing about a function call" do
|
|
145
|
+
lint.("SET $Latitude coerce(\"STRING\", x)\n").should.be.empty
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "says nothing about a variable" do
|
|
149
|
+
# examples/astro feeds these in with remind -i$Latitude="..."
|
|
150
|
+
lint.("SET $Latitude mylat\n").should.be.empty
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe "what it leaves alone" do
|
|
155
|
+
it "says nothing about other system variables" do
|
|
156
|
+
lint.("SET $FormWidth 80\n").should.be.empty
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "says nothing about an ordinary variable of the same name" do
|
|
160
|
+
lint.("SET Latitude 45.42\n").should.be.empty
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "says nothing about a command that is not SET" do
|
|
164
|
+
lint.("MSG Latitude is [$Latitude]\n").should.be.empty
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "says nothing about comments" do
|
|
168
|
+
lint.("# SET $Latitude 45.42\n").should.be.empty
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "reports at error severity" do
|
|
173
|
+
lint.("SET $Latitude 45.42\n").first.severity.should == "error"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Backslashes that look like line continuations and are not.
|
|
8
|
+
#
|
|
9
|
+
# Remind continues a line only when the backslash is the final character of
|
|
10
|
+
# it (src/files.c: `if (l && (DBufValue(&buf)[l-1] == '\\'))`). One
|
|
11
|
+
# invisible space after the backslash and the continuation silently stops
|
|
12
|
+
# being one -- the command is cut in half, the first half usually still
|
|
13
|
+
# parses as something, and the error surfaces somewhere else entirely.
|
|
14
|
+
#
|
|
15
|
+
# Both halves of that are worth reporting:
|
|
16
|
+
#
|
|
17
|
+
# backslash then whitespace the join you meant did not happen
|
|
18
|
+
# backslash at end of file the command you opened never closed
|
|
19
|
+
class DanglingContinuation < Rule
|
|
20
|
+
SWALLOWED = /\\[ \t]+\z/
|
|
21
|
+
|
|
22
|
+
SWALLOWED_MESSAGE =
|
|
23
|
+
"Backslash followed by whitespace does not continue the line; " \
|
|
24
|
+
"the backslash must be the last character"
|
|
25
|
+
|
|
26
|
+
UNTERMINATED_MESSAGE = "File ends inside a line continuation"
|
|
27
|
+
|
|
28
|
+
def self.default_severity
|
|
29
|
+
"error"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.description
|
|
33
|
+
"A backslash that looks like a line continuation but is not one."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check
|
|
37
|
+
check_swallowed
|
|
38
|
+
check_unterminated
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def check_swallowed
|
|
44
|
+
document.each_raw_line do |raw, line|
|
|
45
|
+
body = raw.chomp
|
|
46
|
+
match = body.match(SWALLOWED)
|
|
47
|
+
|
|
48
|
+
if match
|
|
49
|
+
offend(line, SWALLOWED_MESSAGE, column: match.begin(0) + 1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The joiner emits a command left open at end of file rather than
|
|
55
|
+
# dropping it, precisely so this can be said out loud.
|
|
56
|
+
def check_unterminated
|
|
57
|
+
last = document.logical_lines.last
|
|
58
|
+
|
|
59
|
+
if last && last.raw.chomp.end_with?("\\")
|
|
60
|
+
offend(last.last_line, UNTERMINATED_MESSAGE)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
__END__
|
|
68
|
+
|
|
69
|
+
require_relative "../document"
|
|
70
|
+
|
|
71
|
+
describe "RemLint::Rules::DanglingContinuation" do
|
|
72
|
+
lint = proc do |text|
|
|
73
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
74
|
+
|
|
75
|
+
RemLint::Rules::DanglingContinuation.new.run(RemLint::Document.new(source))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "a backslash followed by whitespace" do
|
|
79
|
+
it "is reported" do
|
|
80
|
+
offenses = lint.("SET x 1 + \\ \n2\n")
|
|
81
|
+
|
|
82
|
+
offenses.length.should == 1
|
|
83
|
+
offenses.first.line.should == 1
|
|
84
|
+
offenses.first.message.should.match(/does not continue/)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "points at the backslash, not at the whitespace" do
|
|
88
|
+
lint.("SET x 1 + \\ \n2\n").first.column.should == 11
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "is reported for a trailing tab too" do
|
|
92
|
+
lint.("SET x 1 + \\\t\n2\n").length.should == 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "reports at error severity" do
|
|
96
|
+
lint.("SET x \\ \n").first.severity.should == "error"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "a real continuation" do
|
|
101
|
+
it "is left alone" do
|
|
102
|
+
lint.("SET x 1 + \\\n 2\n").should.be.empty
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "is left alone even several lines deep" do
|
|
106
|
+
lint.("SET x IIF(a, \\\n b, \\\n c)\n").should.be.empty
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "a file that ends inside a continuation" do
|
|
111
|
+
it "is reported on the last line" do
|
|
112
|
+
offenses = lint.("MSG hi\nSET x 1 + \\\n")
|
|
113
|
+
|
|
114
|
+
offenses.map(&:message).should == ["File ends inside a line continuation"]
|
|
115
|
+
offenses.first.line.should == 2
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "is reported even when the file has no trailing newline" do
|
|
119
|
+
lint.("SET x 1 + \\").length.should == 1
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reports the last physical line of a multi-line command" do
|
|
123
|
+
lint.("SET x 1 + \\\n 2 + \\\n").first.line.should == 2
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "says nothing about a file with no backslashes at all" do
|
|
128
|
+
lint.("MSG one\nMSG two\n").should.be.empty
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "says nothing about a backslash in the middle of a line" do
|
|
132
|
+
lint.(%(MSG a \\" b\n)).should.be.empty
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../date_literal"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# Dates outside the range Remind can represent.
|
|
9
|
+
#
|
|
10
|
+
# Remind counts days from `BASE`, which is 1990, and represents
|
|
11
|
+
# `YR_RANGE` years, which is 4000 (src/custom.h.in). So 1990-01-01 to
|
|
12
|
+
# 5990-12-31 is not a policy, it is the type. A date below the floor cannot
|
|
13
|
+
# be stored at all.
|
|
14
|
+
#
|
|
15
|
+
# Three of the book's rules asked for a slice of this each --
|
|
16
|
+
# `TriggerComponentRange`, `DateConstantBeforeEpoch`,
|
|
17
|
+
# `DateOutsideRepresentableRange` -- and Appendix B's own note suggested
|
|
18
|
+
# collecting them. One rule keeps the message identical wherever the date
|
|
19
|
+
# was written.
|
|
20
|
+
#
|
|
21
|
+
# Two shapes are checked: a spelled-out date in a trigger, and a
|
|
22
|
+
# single-quoted date constant in an expression. A day of 32 or a month
|
|
23
|
+
# nobody has is caught by the same walk.
|
|
24
|
+
class DateOutOfRange < Rule
|
|
25
|
+
FIRST_YEAR = DateLiteral::EPOCH_YEAR
|
|
26
|
+
LAST_YEAR = DateLiteral::LAST_YEAR
|
|
27
|
+
|
|
28
|
+
# `'1970-01-01'`, `'2027-13-01'` -- a date constant, which the lexer
|
|
29
|
+
# hands over whole.
|
|
30
|
+
CONSTANT = %r{\A'(?<year>\d{4})[-/](?<month>\d{1,2})[-/](?<day>\d{1,2})}
|
|
31
|
+
|
|
32
|
+
DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
|
|
33
|
+
|
|
34
|
+
def self.default_severity
|
|
35
|
+
"error"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.description
|
|
39
|
+
"A date outside 1990-01-01 to 5990-12-31, or with an impossible component."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check
|
|
43
|
+
document.logical_lines.each_with_index do |logical_line, index|
|
|
44
|
+
if document.commands[index].code?
|
|
45
|
+
check_constants(logical_line)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def check_constants(logical_line)
|
|
53
|
+
document.tokens_for(logical_line).each do |token|
|
|
54
|
+
match = token.type == :string && token.value.match(CONSTANT)
|
|
55
|
+
|
|
56
|
+
if match
|
|
57
|
+
report(logical_line, token, match)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def report(logical_line, token, match)
|
|
63
|
+
year = match[:year].to_i
|
|
64
|
+
month = match[:month].to_i
|
|
65
|
+
day = match[:day].to_i
|
|
66
|
+
complaint = fault(year, month, day)
|
|
67
|
+
|
|
68
|
+
if complaint
|
|
69
|
+
offend_at(logical_line, token.offset, "`#{token.value}` #{complaint}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fault(year, month, day)
|
|
74
|
+
if year < FIRST_YEAR
|
|
75
|
+
"is before #{FIRST_YEAR}-01-01, which is the earliest date Remind represents"
|
|
76
|
+
elsif year > LAST_YEAR
|
|
77
|
+
"is after #{LAST_YEAR}-12-31, which is the latest date Remind represents"
|
|
78
|
+
else
|
|
79
|
+
component_fault(month, day)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def component_fault(month, day)
|
|
84
|
+
if month < 1 || month > 12
|
|
85
|
+
"has month #{month}"
|
|
86
|
+
elsif day < 1 || day > DAYS_IN_MONTH[month - 1]
|
|
87
|
+
"has day #{day} in a month with #{DAYS_IN_MONTH[month - 1]}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
__END__
|
|
95
|
+
|
|
96
|
+
require_relative "../document"
|
|
97
|
+
|
|
98
|
+
describe "RemLint::Rules::DateOutOfRange" do
|
|
99
|
+
lint = proc do |text|
|
|
100
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
101
|
+
|
|
102
|
+
RemLint::Rules::DateOutOfRange.new.run(RemLint::Document.new(source))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
106
|
+
|
|
107
|
+
describe "dates Remind cannot represent" do
|
|
108
|
+
it "reports one before the epoch" do
|
|
109
|
+
messages.("SET a '1970-01-01'\n").first.should ==
|
|
110
|
+
"`'1970-01-01'` is before 1990-01-01, which is the earliest date Remind represents"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "reports one after the last year" do
|
|
114
|
+
messages.("SET a '9999-01-01'\n").first.should.match(/is after 5990-12-31/)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "accepts the first representable date" do
|
|
118
|
+
lint.("SET a '1990-01-01'\n").should.be.empty
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "accepts the last representable year" do
|
|
122
|
+
lint.("SET a '5990-12-31'\n").should.be.empty
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "impossible components" do
|
|
127
|
+
it "reports a month of 13" do
|
|
128
|
+
messages.("SET a '2027-13-01'\n").first.should.match(/has month 13/)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "reports a month of 0" do
|
|
132
|
+
messages.("SET a '2027-00-01'\n").first.should.match(/has month 0/)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "reports a day past the length of its month" do
|
|
136
|
+
messages.("SET a '2027-02-30'\n").first.should.match(/has day 30 in a month with 29/)
|
|
137
|
+
messages.("SET a '2027-04-31'\n").first.should.match(/has day 31 in a month with 30/)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "accepts 29 February, which some years have" do
|
|
141
|
+
lint.("SET a '2028-02-29'\n").should.be.empty
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "accepts the last day of a 31-day month" do
|
|
145
|
+
lint.("SET a '2027-01-31'\n").should.be.empty
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "accepts the slash separator" do
|
|
150
|
+
lint.("SET a '2027/01/31'\n").should.be.empty
|
|
151
|
+
messages.("SET a '1970/01/01'\n").length.should == 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "reports every bad constant on a line" do
|
|
155
|
+
messages.("IF ['1970-01-01' < '9999-01-01']\n").length.should == 2
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "says nothing about a datetime constant in range" do
|
|
159
|
+
lint.("SET a '2027-01-01@14:33'\n").should.be.empty
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "says nothing about an ordinary string that looks like a date" do
|
|
163
|
+
# Double quotes make it a STRING, not a DATE.
|
|
164
|
+
lint.(%(SET a "1970-01-01"\n)).should.be.empty
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "says nothing about comments" do
|
|
168
|
+
lint.("# SET a '1970-01-01'\n").should.be.empty
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "points at the constant" do
|
|
172
|
+
text = "SET a '1970-01-01'\n"
|
|
173
|
+
|
|
174
|
+
lint.(text).first.column.should == text.index("'") + 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "reports at error severity" do
|
|
178
|
+
lint.("SET a '1970-01-01'\n").first.severity.should == "error"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `DEBUG` flags that do nothing, and `DEBUG` flags left switched on.
|
|
8
|
+
#
|
|
9
|
+
# `DoDebug` in src/main.c reads the argument character by character: `+`
|
|
10
|
+
# turns flags on, `-` turns them off, and thirteen letters name a flag.
|
|
11
|
+
# Anything else reaches a `default:` branch that warns -- but only at
|
|
12
|
+
# warning level 05.02.03, so a script that has not raised the level gets a
|
|
13
|
+
# debug session that quietly does nothing.
|
|
14
|
+
#
|
|
15
|
+
# The other half matters more in a repository than at a terminal. Debug
|
|
16
|
+
# output is voluminous and goes to standard error, so a `DEBUG +x` that was
|
|
17
|
+
# never matched by a `DEBUG -x` turns every later run into a trace log --
|
|
18
|
+
# including the one under cron, which mails it to somebody.
|
|
19
|
+
class DebugCommand < Rule
|
|
20
|
+
# The flag letters `DoDebug` switches on, case-insensitive.
|
|
21
|
+
FLAGS = %w[p e q s h x t v l f n u z].freeze
|
|
22
|
+
|
|
23
|
+
SIGNS = %w[+ -].freeze
|
|
24
|
+
|
|
25
|
+
def self.default_severity
|
|
26
|
+
"warning"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.description
|
|
30
|
+
"A DEBUG flag Remind does not define, or one switched on and never off."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check
|
|
34
|
+
left_on = {}
|
|
35
|
+
|
|
36
|
+
document.code_commands.each do |command|
|
|
37
|
+
if command.keyword?("DEBUG")
|
|
38
|
+
scan(command, left_on)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
report_left_on(left_on)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def scan(command, left_on)
|
|
48
|
+
sign = "+"
|
|
49
|
+
|
|
50
|
+
command.args.each_char.with_index do |character, index|
|
|
51
|
+
sign = step(
|
|
52
|
+
command,
|
|
53
|
+
character,
|
|
54
|
+
sign,
|
|
55
|
+
index,
|
|
56
|
+
left_on,
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def step(command, character, sign, index, left_on)
|
|
62
|
+
if SIGNS.include?(character)
|
|
63
|
+
character
|
|
64
|
+
elsif character.match?(/\s/)
|
|
65
|
+
sign
|
|
66
|
+
else
|
|
67
|
+
record(
|
|
68
|
+
command,
|
|
69
|
+
character,
|
|
70
|
+
sign,
|
|
71
|
+
index,
|
|
72
|
+
left_on,
|
|
73
|
+
)
|
|
74
|
+
sign
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def record(command, character, sign, index, left_on)
|
|
79
|
+
if FLAGS.include?(character.downcase)
|
|
80
|
+
track(
|
|
81
|
+
command,
|
|
82
|
+
character.downcase,
|
|
83
|
+
sign,
|
|
84
|
+
left_on,
|
|
85
|
+
)
|
|
86
|
+
else
|
|
87
|
+
report_unknown(command, character, index)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def track(command, flag, sign, left_on)
|
|
92
|
+
if sign == "+"
|
|
93
|
+
left_on[flag] ||= command
|
|
94
|
+
else
|
|
95
|
+
left_on.delete(flag)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def report_unknown(command, character, index)
|
|
100
|
+
offend(
|
|
101
|
+
command.line,
|
|
102
|
+
"`#{character}` is not a DEBUG flag; Remind defines #{FLAGS.join(', ')}",
|
|
103
|
+
column: command.args_offset + index + 1,
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def report_left_on(left_on)
|
|
108
|
+
left_on.each do |flag, command|
|
|
109
|
+
offend(
|
|
110
|
+
command.line,
|
|
111
|
+
"`DEBUG +#{flag}` is never matched by a `DEBUG -#{flag}`; debug output " \
|
|
112
|
+
"goes to standard error on every later run, including the one under cron",
|
|
113
|
+
column: command.keyword_column,
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
__END__
|
|
122
|
+
|
|
123
|
+
require_relative "../document"
|
|
124
|
+
|
|
125
|
+
describe "RemLint::Rules::DebugCommand" do
|
|
126
|
+
lint = proc do |text|
|
|
127
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
128
|
+
|
|
129
|
+
RemLint::Rules::DebugCommand.new.run(RemLint::Document.new(source))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
133
|
+
|
|
134
|
+
describe "unknown flags" do
|
|
135
|
+
it "reports a letter Remind does not define" do
|
|
136
|
+
messages.("DEBUG +w\nDEBUG -w\n").first.should.match(/`w` is not a DEBUG flag/)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "lists the flags Remind does define" do
|
|
140
|
+
messages.("DEBUG +w\nDEBUG -w\n").first.should.match(/p, e, q, s, h, x, t, v, l, f, n, u, z/)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "accepts every flag Remind defines" do
|
|
144
|
+
lint.("DEBUG +peqshxtvlfnuz\nDEBUG -peqshxtvlfnuz\n").should.be.empty
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "accepts them in upper case" do
|
|
148
|
+
lint.("DEBUG +X\nDEBUG -X\n").should.be.empty
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe "flags left on" do
|
|
153
|
+
it "reports a flag switched on and never off" do
|
|
154
|
+
messages.("DEBUG +x\n").first.should.match(/never matched by a `DEBUG -x`/)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "accepts a flag switched on and then off" do
|
|
158
|
+
lint.("DEBUG +x\nMSG hi\nDEBUG -x\n").should.be.empty
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "accepts several flags switched on and off together" do
|
|
162
|
+
lint.("DEBUG +xt\nMSG hi\nDEBUG -xt\n").should.be.empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "reports each flag left on" do
|
|
166
|
+
lint.("DEBUG +xt\n").length.should == 2
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "handles a sign that applies to several letters" do
|
|
170
|
+
lint.("DEBUG +xt -x\nDEBUG -t\n").should.be.empty
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "reports the line the flag was switched on" do
|
|
174
|
+
lint.("MSG hi\nDEBUG +x\n").first.line.should == 2
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "says nothing about a file with no DEBUG at all" do
|
|
179
|
+
lint.("MSG hi\n").should.be.empty
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "says nothing about comments" do
|
|
183
|
+
lint.("# DEBUG +w\n").should.be.empty
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "reports at warning severity" do
|
|
187
|
+
lint.("DEBUG +x\n").first.severity.should == "warning"
|
|
188
|
+
end
|
|
189
|
+
end
|