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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A language pack that never translates `LANGID`.
|
|
8
|
+
#
|
|
9
|
+
# Scripts branch on `_("LANGID")` to pick language-specific behaviour, and
|
|
10
|
+
# an untranslated LANGID reverts to `"en"`. So a fully translated pack that
|
|
11
|
+
# never translates LANGID sends every one of those scripts down the English
|
|
12
|
+
# path -- and does it by producing English, which reads as "not translated
|
|
13
|
+
# yet" rather than "translated and ignored".
|
|
14
|
+
#
|
|
15
|
+
# A pack is recognised by its day and month names, which is what
|
|
16
|
+
# `include/lang/*.rem` set and what nothing else does. A file translating a
|
|
17
|
+
# handful of strings -- `include/translations/de/sun.rem` is two lines --
|
|
18
|
+
# is not a pack and has no LANGID to translate.
|
|
19
|
+
class LocalizationPack < Rule
|
|
20
|
+
LANGID = "LANGID"
|
|
21
|
+
|
|
22
|
+
# What a language pack sets and a string file does not.
|
|
23
|
+
PACK_NAMES = (
|
|
24
|
+
%w[monday tuesday wednesday thursday friday saturday sunday] +
|
|
25
|
+
%w[january february march april may june july
|
|
26
|
+
august september october november december
|
|
27
|
+
]
|
|
28
|
+
).freeze
|
|
29
|
+
|
|
30
|
+
TRANSLATED = /\A\s*"(?<original>(?:[^"\\]|\\.)*)"\s*"/
|
|
31
|
+
|
|
32
|
+
SYSVAR_SET = /\A\$(?<name>\w+)\s/
|
|
33
|
+
|
|
34
|
+
def self.default_severity
|
|
35
|
+
"info"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.description
|
|
39
|
+
"A language pack that never translates LANGID, which reverts to English."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check
|
|
43
|
+
if pack?
|
|
44
|
+
check_langid
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# A pack names the days and months. `include/lang/de.rem` writes
|
|
51
|
+
# `SET $Monday "Montag"`; a translation file writes neither.
|
|
52
|
+
def pack?
|
|
53
|
+
document.code_commands.any? { |command| names_a_day_or_month?(command) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def names_a_day_or_month?(command)
|
|
57
|
+
if command.keyword?("SET")
|
|
58
|
+
match = command.args.match(SYSVAR_SET)
|
|
59
|
+
|
|
60
|
+
!match.nil? && PACK_NAMES.include?(match[:name].downcase)
|
|
61
|
+
elsif command.keyword?("TRANSLATE")
|
|
62
|
+
original = command.args.match(TRANSLATED)&.[](:original)
|
|
63
|
+
|
|
64
|
+
PACK_NAMES.include?(original.to_s.downcase)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_langid
|
|
69
|
+
unless translates_langid?
|
|
70
|
+
offend(
|
|
71
|
+
document.line_number_at(0),
|
|
72
|
+
"this pack never translates `#{LANGID}`, which reverts to `\"en\"` -- so " \
|
|
73
|
+
"every script branching on `_(\"#{LANGID}\")` takes the English path",
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def translates_langid?
|
|
79
|
+
document.code_commands.any? do |command|
|
|
80
|
+
command.keyword?("TRANSLATE") &&
|
|
81
|
+
command.args.match(TRANSLATED)&.[](:original) == LANGID
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
__END__
|
|
89
|
+
|
|
90
|
+
require_relative "../document"
|
|
91
|
+
|
|
92
|
+
describe "RemLint::Rules::LocalizationPack" do
|
|
93
|
+
lint = proc do |text|
|
|
94
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
95
|
+
|
|
96
|
+
RemLint::Rules::LocalizationPack.new.run(RemLint::Document.new(source))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
100
|
+
|
|
101
|
+
describe "LANGID" do
|
|
102
|
+
it "reports a pack that never translates it" do
|
|
103
|
+
messages.(%(TRANSLATE "Monday" "Montag"\n)).first.should.match(
|
|
104
|
+
/never translates `LANGID`, which reverts to `"en"`/,
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "accepts a pack that does" do
|
|
109
|
+
text = %(TRANSLATE "LANGID" "de"\nTRANSLATE "Monday" "Montag"\n)
|
|
110
|
+
|
|
111
|
+
lint.(text).should.be.empty
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "says nothing about a file that translates nothing" do
|
|
115
|
+
lint.("REM 1 Jan MSG hi\n").should.be.empty
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "recognises a pack by the day and month names it sets" do
|
|
119
|
+
# include/lang/de.rem writes SET $Monday "Montag" rather than TRANSLATE.
|
|
120
|
+
messages.(%(SET $Monday "Montag"\n)).length.should == 1
|
|
121
|
+
lint.(%(SET $Monday "Montag"\nTRANSLATE "LANGID" "de"\n)).should.be.empty
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "does not count a file that translates a handful of strings" do
|
|
125
|
+
# include/translations/de/sun.rem is exactly this, and is not a pack.
|
|
126
|
+
lint.(%(TRANSLATE "Sunrise" "Sonnenaufgang"\nTRANSLATE "Sunset" "Sonnenuntergang"\n))
|
|
127
|
+
.should.be.empty
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "explains the consequence" do
|
|
131
|
+
messages.(%(TRANSLATE "Monday" "Montag"\n)).first.should.match(
|
|
132
|
+
/takes the English path/,
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "says nothing about comments" do
|
|
138
|
+
lint.(%(# TRANSLATE "Monday" "Montag"\n)).should.be.empty
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "reports at info severity" do
|
|
142
|
+
lint.(%(TRANSLATE "Monday" "Montag"\n)).first.severity.should == "info"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Moon and season selectors outside their four-element sets.
|
|
8
|
+
#
|
|
9
|
+
# `moondate`, `moondatetime` and `moontime` take a phase: 0 new, 1 first
|
|
10
|
+
# quarter, 2 full, 3 last quarter. `soleq` takes an event: 0 March equinox,
|
|
11
|
+
# 1 June solstice, 2 September equinox, 3 December solstice. Both are
|
|
12
|
+
# closed sets of four, so a literal 4 is always wrong and always visible
|
|
13
|
+
# without running anything.
|
|
14
|
+
#
|
|
15
|
+
# Remind agrees -- `moondate(4)` is `Number too high` -- but says so on the
|
|
16
|
+
# day the line is reached, which for a moon reminder is the day it was
|
|
17
|
+
# supposed to fire.
|
|
18
|
+
class MoonPhaseArgument < Rule
|
|
19
|
+
PHASES = {
|
|
20
|
+
"moondate" => "a moon phase",
|
|
21
|
+
"moondatetime" => "a moon phase",
|
|
22
|
+
"moontime" => "a moon phase",
|
|
23
|
+
"soleq" => "a solstice or equinox",
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
MEANINGS = {
|
|
27
|
+
"a moon phase" => "0 new, 1 first quarter, 2 full, 3 last quarter",
|
|
28
|
+
"a solstice or equinox" => "0 March equinox, 1 June solstice, " \
|
|
29
|
+
"2 September equinox, 3 December solstice",
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
LIMIT = 3
|
|
33
|
+
|
|
34
|
+
def self.default_severity
|
|
35
|
+
"error"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.description
|
|
39
|
+
"A moon phase or solstice selector outside the four values it accepts."
|
|
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_line(logical_line)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def check_line(logical_line)
|
|
53
|
+
tokens = document.tokens_for(logical_line)
|
|
54
|
+
|
|
55
|
+
tokens.each_index do |index|
|
|
56
|
+
selector = literal_selector(tokens, index)
|
|
57
|
+
|
|
58
|
+
if selector
|
|
59
|
+
report(logical_line, tokens[index], selector)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `name ( <number>` -- the selector is always the first argument.
|
|
65
|
+
def literal_selector(tokens, index)
|
|
66
|
+
token = tokens[index]
|
|
67
|
+
argument = tokens[index + 2]
|
|
68
|
+
|
|
69
|
+
if token.type == :function && PHASES.key?(token.value.downcase) &&
|
|
70
|
+
tokens[index + 1]&.type == :lparen && argument&.type == :number
|
|
71
|
+
argument
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def report(logical_line, name, argument)
|
|
76
|
+
value = argument.value.to_i
|
|
77
|
+
|
|
78
|
+
if value > LIMIT
|
|
79
|
+
kind = PHASES.fetch(name.value.downcase)
|
|
80
|
+
|
|
81
|
+
offend_at(logical_line, argument.offset, message(name.value, kind, value))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def message(name, kind, value)
|
|
86
|
+
"`#{name}(#{value})` is outside the four values #{kind} takes " \
|
|
87
|
+
"(#{MEANINGS.fetch(kind)}); Remind answers `Number too high`"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
__END__
|
|
94
|
+
|
|
95
|
+
require_relative "../document"
|
|
96
|
+
|
|
97
|
+
describe "RemLint::Rules::MoonPhaseArgument" do
|
|
98
|
+
lint = proc do |text|
|
|
99
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
100
|
+
|
|
101
|
+
RemLint::Rules::MoonPhaseArgument.new.run(RemLint::Document.new(source))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
105
|
+
|
|
106
|
+
describe "selectors that exist" do
|
|
107
|
+
it "accepts the four moon phases" do
|
|
108
|
+
# include/moonphases.rem writes exactly these.
|
|
109
|
+
(0..3).each do |phase|
|
|
110
|
+
lint.("REM [moondatetime(#{phase})] MSG Moon\n").should.be.empty
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "accepts the four solstice and equinox events" do
|
|
115
|
+
(0..3).each do |event|
|
|
116
|
+
lint.("REM [datepart(soleq(#{event}, $U))] MSG Season\n").should.be.empty
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "accepts moondate and moontime too" do
|
|
121
|
+
lint.("MSG [moondate(2)] [moontime(2)]\n").should.be.empty
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "selectors that do not" do
|
|
126
|
+
it "reports a moon phase of 4" do
|
|
127
|
+
messages.("REM [moondatetime(4)] MSG Moon\n").first.should ==
|
|
128
|
+
"`moondatetime(4)` is outside the four values a moon phase takes " \
|
|
129
|
+
"(0 new, 1 first quarter, 2 full, 3 last quarter); Remind answers `Number too high`"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "reports a solstice selector of 4" do
|
|
133
|
+
messages.("MSG [soleq(4, $U)]\n").first.should.match(/a solstice or equinox takes/)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "reports each bad selector on a line" do
|
|
137
|
+
messages.("MSG [moondate(4)] [moondate(9)]\n").length.should == 2
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "points at the argument" do
|
|
141
|
+
text = "MSG [moondate(4)]\n"
|
|
142
|
+
|
|
143
|
+
lint.(text).first.column.should == text.index("4") + 1
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "says nothing about a computed selector" do
|
|
148
|
+
lint.("MSG [moondate(n)]\n").should.be.empty
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "says nothing about another function taking a large number" do
|
|
152
|
+
lint.("MSG [max(4, 9)]\n").should.be.empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "says nothing about comments" do
|
|
156
|
+
lint.("# MSG [moondate(4)]\n").should.be.empty
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "reports at error severity" do
|
|
160
|
+
lint.("MSG [moondate(4)]\n").first.severity.should == "error"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A single-sign delta in a file with nothing omitted.
|
|
8
|
+
#
|
|
9
|
+
# `++n` counts calendar days back from the event; `+n` counts *non-omitted*
|
|
10
|
+
# days. The distinction is the whole point of the single-sign form, and it
|
|
11
|
+
# is exactly nothing when the omit context is empty: with no `OMIT`
|
|
12
|
+
# anywhere, `+n` and `++n` produce identical reminders.
|
|
13
|
+
#
|
|
14
|
+
# So a single sign in a file with no omits is one of two things, and both
|
|
15
|
+
# are worth a look: a `+` that was meant to be `++`, or a reminder that
|
|
16
|
+
# depends on an `OMIT` somebody forgot to write. The same holds for the
|
|
17
|
+
# single-tilde back, `~n`.
|
|
18
|
+
#
|
|
19
|
+
# Off by default. A file that gets its omits from an `INCLUDE` is correct
|
|
20
|
+
# and this rule cannot see that -- which is why the check also stays quiet
|
|
21
|
+
# in any file that includes anything.
|
|
22
|
+
class OmitAwareDelta < Rule
|
|
23
|
+
# `+3` and `~3`, but not `++3` or `~~3`, and not an arithmetic `+`.
|
|
24
|
+
SINGLE = /(?<![+~\d\w])(?<sign>[+~])(?![+~])(?<count>\d+)(?!\S)/
|
|
25
|
+
|
|
26
|
+
# `OMIT` is a command; `ADDOMIT` and `OMITFUNC` are clauses on a `REM`,
|
|
27
|
+
# so both places have to be looked at.
|
|
28
|
+
OMIT_COMMANDS = %w[OMIT].freeze
|
|
29
|
+
|
|
30
|
+
OMIT_CLAUSES = %w[ADDOMIT OMITFUNC].freeze
|
|
31
|
+
|
|
32
|
+
INCLUDE_COMMANDS = %w[INCLUDE SYSINCLUDE INCLUDECMD DO].freeze
|
|
33
|
+
|
|
34
|
+
def self.enabled_by_default?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.default_severity
|
|
39
|
+
"info"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.description
|
|
43
|
+
"A single-sign delta or back in a file that omits nothing."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check
|
|
47
|
+
if omits? || includes?
|
|
48
|
+
nil
|
|
49
|
+
else
|
|
50
|
+
report_deltas
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def omits?
|
|
57
|
+
document.code_commands.any? do |command|
|
|
58
|
+
command.keyword?(*OMIT_COMMANDS) ||
|
|
59
|
+
document.trigger_for(command).include?(*OMIT_CLAUSES)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A file that includes something may be getting its omits from there.
|
|
64
|
+
def includes?
|
|
65
|
+
document.code_commands.any? { |command| command.keyword?(*INCLUDE_COMMANDS) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def report_deltas
|
|
69
|
+
document.code_commands.each do |command|
|
|
70
|
+
trigger = document.trigger_for(command)
|
|
71
|
+
|
|
72
|
+
if trigger.triggered?
|
|
73
|
+
scan(command, trigger)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def scan(command, trigger)
|
|
79
|
+
limit = trigger.body_offset || command.text.length
|
|
80
|
+
head = command.text[0...limit].to_s
|
|
81
|
+
|
|
82
|
+
head.to_enum(:scan, SINGLE).each do
|
|
83
|
+
match = Regexp.last_match
|
|
84
|
+
|
|
85
|
+
offend_at(command.logical_line, match.begin(0), message(match))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def message(match)
|
|
90
|
+
sign = match[:sign]
|
|
91
|
+
doubled = sign * 2
|
|
92
|
+
|
|
93
|
+
"`#{sign}#{match[:count]}` counts non-omitted days, and this file omits " \
|
|
94
|
+
"nothing -- so it is exactly `#{doubled}#{match[:count]}`; either the sign " \
|
|
95
|
+
"was meant to be doubled or an `OMIT` is missing"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
__END__
|
|
102
|
+
|
|
103
|
+
require_relative "../document"
|
|
104
|
+
|
|
105
|
+
describe "RemLint::Rules::OmitAwareDelta" do
|
|
106
|
+
lint = proc do |text|
|
|
107
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
108
|
+
|
|
109
|
+
RemLint::Rules::OmitAwareDelta.new.run(RemLint::Document.new(source))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
113
|
+
|
|
114
|
+
it "is off unless the configuration asks for it" do
|
|
115
|
+
RemLint::Rules::OmitAwareDelta.enabled_by_default?.should.be.false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "reports a single-plus delta in a file with no omits" do
|
|
119
|
+
messages.("REM 16 July +10 MSG hi\n").first.should.match(
|
|
120
|
+
/counts non-omitted days, and this file omits nothing/,
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "suggests the doubled sign" do
|
|
125
|
+
messages.("REM 16 July +10 MSG hi\n").first.should.match(/exactly `\+\+10`/)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "reports a single tilde back" do
|
|
129
|
+
messages.("REM 16 July ~3 MSG hi\n").first.should.match(/exactly `~~3`/)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "accepts a doubled delta" do
|
|
133
|
+
lint.("REM 16 July ++10 MSG hi\n").should.be.empty
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "accepts a doubled back" do
|
|
137
|
+
lint.("REM 16 July ~~3 MSG hi\n").should.be.empty
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "says nothing in a file that omits something" do
|
|
141
|
+
lint.("OMIT 1 Jan\nREM 16 July +10 MSG hi\n").should.be.empty
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "says nothing in a file that uses ADDOMIT" do
|
|
145
|
+
lint.("REM 1 Apr ADDOMIT MSG x\nREM 16 July +10 MSG hi\n").should.be.empty
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "says nothing in a file that includes anything" do
|
|
149
|
+
# The omits may be in the included file.
|
|
150
|
+
lint.("INCLUDE defs.rem\nREM 16 July +10 MSG hi\n").should.be.empty
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "does not read a delta out of the body" do
|
|
154
|
+
lint.("REM 16 July MSG the score was +10\n").should.be.empty
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "does not read arithmetic as a delta" do
|
|
158
|
+
lint.("SET a 3 +10\n").should.be.empty
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "says nothing about comments" do
|
|
162
|
+
lint.("# REM 16 July +10 MSG hi\n").should.be.empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "reports at info severity" do
|
|
166
|
+
lint.("REM 16 July +10 MSG hi\n").first.severity.should == "info"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A variable assigned inside a `PUSH-VARS` block that the `PUSH-VARS` did
|
|
8
|
+
# not name.
|
|
9
|
+
#
|
|
10
|
+
# `PUSH-VARS a b c` saves exactly `a`, `b` and `c`, and `POP-VARS` restores
|
|
11
|
+
# exactly those. An assignment to anything else inside the block survives
|
|
12
|
+
# the `POP` -- which is the one thing the block was written to prevent.
|
|
13
|
+
#
|
|
14
|
+
# The bare `PUSH-VARS` with no names saves *every* variable, so a block
|
|
15
|
+
# written that way is never reported: it cannot leak.
|
|
16
|
+
#
|
|
17
|
+
# Nothing about this is an error, and nothing reports it. The block looks
|
|
18
|
+
# like a scope, reads like a scope, and quietly is not one for the variable
|
|
19
|
+
# somebody added later.
|
|
20
|
+
class PushVarsMissingName < Rule
|
|
21
|
+
ASSIGNMENT = /\A(?<name>[A-Za-z_]\w*)\b/
|
|
22
|
+
|
|
23
|
+
def self.default_severity
|
|
24
|
+
"warning"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.description
|
|
28
|
+
"A variable SET inside a PUSH-VARS block that the PUSH-VARS never named."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check
|
|
32
|
+
@stack = []
|
|
33
|
+
|
|
34
|
+
document.code_commands.each do |command|
|
|
35
|
+
dispatch(command)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def dispatch(command)
|
|
42
|
+
if command.keyword?("PUSH-VARS")
|
|
43
|
+
@stack.push(saved(command))
|
|
44
|
+
elsif command.keyword?("POP-VARS")
|
|
45
|
+
@stack.pop
|
|
46
|
+
elsif command.keyword?("SET", "UNSET")
|
|
47
|
+
check_assignment(command)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The names the PUSH-VARS listed, or nil for the bare form that saves
|
|
52
|
+
# everything.
|
|
53
|
+
def saved(command)
|
|
54
|
+
names = command.args.strip.split(/[\s,]+/).reject(&:empty?)
|
|
55
|
+
|
|
56
|
+
if names.empty?
|
|
57
|
+
nil
|
|
58
|
+
else
|
|
59
|
+
names.map(&:downcase)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_assignment(command)
|
|
64
|
+
frame = @stack.last
|
|
65
|
+
name = assigned_name(command)
|
|
66
|
+
|
|
67
|
+
if !@stack.empty? && frame && name && !frame.include?(name.downcase)
|
|
68
|
+
offend(command.line, message(name, frame), column: command.keyword_column)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `SET $SysVar` is a different namespace and `POP-VARS` does not carry
|
|
73
|
+
# it, so only ordinary variables are checked.
|
|
74
|
+
def assigned_name(command)
|
|
75
|
+
match = command.args.match(ASSIGNMENT)
|
|
76
|
+
|
|
77
|
+
match && match[:name]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def message(name, saved)
|
|
81
|
+
"`#{name}` is not one of the variables this `PUSH-VARS` saved " \
|
|
82
|
+
"(#{saved.join(', ')}), so the assignment survives the `POP-VARS`"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
__END__
|
|
89
|
+
|
|
90
|
+
require_relative "../document"
|
|
91
|
+
|
|
92
|
+
describe "RemLint::Rules::PushVarsMissingName" do
|
|
93
|
+
lint = proc do |text|
|
|
94
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
95
|
+
|
|
96
|
+
RemLint::Rules::PushVarsMissingName.new.run(RemLint::Document.new(source))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
100
|
+
|
|
101
|
+
it "reports an assignment the PUSH-VARS did not name" do
|
|
102
|
+
messages.("PUSH-VARS a\nSET b 1\nPOP-VARS\n").first.should ==
|
|
103
|
+
"`b` is not one of the variables this `PUSH-VARS` saved (a), so the assignment " \
|
|
104
|
+
"survives the `POP-VARS`"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "accepts an assignment the PUSH-VARS named" do
|
|
108
|
+
lint.("PUSH-VARS a\nSET a 1\nPOP-VARS\n").should.be.empty
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "accepts any of several names" do
|
|
112
|
+
lint.("PUSH-VARS a b c\nSET b 1\nSET c 2\nPOP-VARS\n").should.be.empty
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "accepts names separated by commas" do
|
|
116
|
+
lint.("PUSH-VARS a, b\nSET b 1\nPOP-VARS\n").should.be.empty
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "matches names case-insensitively" do
|
|
120
|
+
lint.("PUSH-VARS Foo\nSET foo 1\nPOP-VARS\n").should.be.empty
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "accepts everything inside a bare PUSH-VARS, which saves the lot" do
|
|
124
|
+
lint.("PUSH-VARS\nSET anything 1\nPOP-VARS\n").should.be.empty
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "says nothing outside a block" do
|
|
128
|
+
lint.("SET b 1\nPUSH-VARS a\nPOP-VARS\nSET c 2\n").should.be.empty
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "reports an UNSET too" do
|
|
132
|
+
messages.("PUSH-VARS a\nUNSET b\nPOP-VARS\n").length.should == 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "says nothing about a system variable, which POP-VARS does not carry" do
|
|
136
|
+
lint.("PUSH-VARS a\nSET $FormWidth 80\nPOP-VARS\n").should.be.empty
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "uses the innermost block's list" do
|
|
140
|
+
lint.("PUSH-VARS a\nPUSH-VARS b\nSET b 1\nPOP-VARS\nPOP-VARS\n").should.be.empty
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "reports against the innermost block only" do
|
|
144
|
+
messages.("PUSH-VARS a\nPUSH-VARS b\nSET a 1\nPOP-VARS\nPOP-VARS\n").length.should == 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "points at the keyword" do
|
|
148
|
+
lint.("PUSH-VARS a\n SET b 1\nPOP-VARS\n").first.column.should == 4
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "says nothing about comments" do
|
|
152
|
+
lint.("PUSH-VARS a\n# SET b 1\nPOP-VARS\n").should.be.empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "reports at warning severity" do
|
|
156
|
+
lint.("PUSH-VARS a\nSET b 1\nPOP-VARS\n").first.severity.should == "warning"
|
|
157
|
+
end
|
|
158
|
+
end
|