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,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../date_literal"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# A reminder whose window closes before it opens.
|
|
9
|
+
#
|
|
10
|
+
# `FROM` and `SCANFROM` set the date Remind starts scanning from; `UNTIL`
|
|
11
|
+
# (and its synonym `THROUGH`) sets the date the reminder expires. An
|
|
12
|
+
# `UNTIL` strictly before the start describes an empty window, and the
|
|
13
|
+
# reminder can never fire.
|
|
14
|
+
#
|
|
15
|
+
# Unusually for this linter, Remind is not silent here: it warns at parse
|
|
16
|
+
# time, with a different message for each start keyword --
|
|
17
|
+
#
|
|
18
|
+
# Warning: UNTIL/THROUGH date earlier than FROM date
|
|
19
|
+
# Warning: UNTIL/THROUGH date earlier than SCANFROM date
|
|
20
|
+
#
|
|
21
|
+
# -- so the value on offer is smaller than usual: saying it in the editor
|
|
22
|
+
# and in CI rather than only when someone runs the file and reads stderr.
|
|
23
|
+
# Remind's own `tests/test.rem` marks both forms "Diagnosed".
|
|
24
|
+
#
|
|
25
|
+
# Strictly before, not on or before. `FROM 1992-01-06 UNTIL 1992-01-06` is
|
|
26
|
+
# a legal one-day window and fires on the day; Remind's own warning says
|
|
27
|
+
# "earlier than" for the same reason.
|
|
28
|
+
#
|
|
29
|
+
# Both dates have to be fully spelled out for this to be decidable, which
|
|
30
|
+
# is no restriction: `GetFullDate` requires exactly that of both keywords.
|
|
31
|
+
class UntilBeforeFrom < Rule
|
|
32
|
+
# `UNTIL` and `THROUGH` are the same clause -- `ParseUntil` takes both
|
|
33
|
+
# and writes the same field.
|
|
34
|
+
EXPIRY = %w[UNTIL THROUGH].freeze
|
|
35
|
+
|
|
36
|
+
# `FROM` and `SCANFROM` are different clauses that both set where the
|
|
37
|
+
# scan begins, and Remind checks the expiry against either.
|
|
38
|
+
START = %w[FROM SCANFROM].freeze
|
|
39
|
+
|
|
40
|
+
def self.default_severity
|
|
41
|
+
"error"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.description
|
|
45
|
+
"An UNTIL or THROUGH date earlier than the reminder's FROM or SCANFROM."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check
|
|
49
|
+
document.code_commands.each do |command|
|
|
50
|
+
check_window(command)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_window(command)
|
|
57
|
+
trigger = document.trigger_for(command)
|
|
58
|
+
start = first_clause(trigger, START)
|
|
59
|
+
expiry = first_clause(trigger, EXPIRY)
|
|
60
|
+
from = clause_date(command, start)
|
|
61
|
+
until_date = clause_date(command, expiry)
|
|
62
|
+
|
|
63
|
+
if from && until_date && until_date < from
|
|
64
|
+
report(
|
|
65
|
+
command,
|
|
66
|
+
start,
|
|
67
|
+
expiry,
|
|
68
|
+
from,
|
|
69
|
+
until_date,
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def first_clause(trigger, names)
|
|
75
|
+
names.filter_map { |name| trigger.find(name) }.first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The date written immediately after a clause keyword.
|
|
79
|
+
def clause_date(command, clause)
|
|
80
|
+
if clause
|
|
81
|
+
tokens = document.tokens_for(command.logical_line)
|
|
82
|
+
|
|
83
|
+
DateLiteral.at(tokens, clause.argument_index)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def report(command, start, expiry, from, until_date)
|
|
88
|
+
offend_at(
|
|
89
|
+
command.logical_line,
|
|
90
|
+
expiry.offset,
|
|
91
|
+
"`#{expiry.name} #{until_date}` is earlier than " \
|
|
92
|
+
"`#{start.name} #{from}`, so this reminder can never trigger",
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
__END__
|
|
100
|
+
|
|
101
|
+
require_relative "../document"
|
|
102
|
+
|
|
103
|
+
describe "RemLint::Rules::UntilBeforeFrom" do
|
|
104
|
+
lint = proc do |text|
|
|
105
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
106
|
+
|
|
107
|
+
RemLint::Rules::UntilBeforeFrom.new.run(RemLint::Document.new(source))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
111
|
+
|
|
112
|
+
describe "windows that can never open" do
|
|
113
|
+
it "reports an UNTIL before the FROM" do
|
|
114
|
+
messages.("REM Mon FROM 2027-03-01 UNTIL 2027-01-01 MSG hi\n").first.should ==
|
|
115
|
+
"`UNTIL 2027-01-01` is earlier than `FROM 2027-03-01`, so this reminder can never trigger"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "reports an UNTIL earlier than a SCANFROM" do
|
|
119
|
+
# Remind warns about this one separately, and tests/test.rem marks it
|
|
120
|
+
# "Diagnosed" alongside the FROM form.
|
|
121
|
+
messages.("REM Mon SCANFROM 1992-01-01 UNTIL 1991-12-31 MSG hi\n").first.should.match(
|
|
122
|
+
/earlier than `SCANFROM 1992-01-01`/,
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "reports THROUGH, which is the same clause" do
|
|
127
|
+
messages.("REM Mon FROM 2027-03-01 THROUGH 2027-01-01 MSG hi\n").first.should.match(
|
|
128
|
+
/`THROUGH 2027-01-01` is earlier than/,
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "reads the spelled-out date form" do
|
|
133
|
+
messages.("REM Mon FROM 1 March 2027 UNTIL 1 Jan 2027 MSG hi\n").length.should == 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "reads the two forms mixed" do
|
|
137
|
+
messages.("REM Mon FROM 2027-03-01 UNTIL 1 Jan 2027 MSG hi\n").length.should == 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "points at the expiry clause" do
|
|
141
|
+
text = "REM Mon FROM 2027-03-01 UNTIL 2027-01-01 MSG hi\n"
|
|
142
|
+
|
|
143
|
+
lint.(text).first.column.should == text.index("UNTIL") + 1
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
describe "windows that are fine" do
|
|
148
|
+
it "accepts an UNTIL after the FROM" do
|
|
149
|
+
lint.("REM Mon FROM 2027-01-01 UNTIL 2027-03-01 MSG hi\n").should.be.empty
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "accepts an UNTIL one day after the FROM" do
|
|
153
|
+
lint.("REM Mon FROM 2027-01-01 UNTIL 2027-01-02 MSG hi\n").should.be.empty
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "accepts a one-day window, which is legal and fires" do
|
|
157
|
+
# `REM MON FROM 1992-01-06 UNTIL 1992-01-06` triggers on 1992-01-06.
|
|
158
|
+
lint.("REM Mon FROM 2027-03-01 UNTIL 2027-03-01 MSG hi\n").should.be.empty
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "accepts a reminder with only one of the two" do
|
|
162
|
+
lint.("REM Mon FROM 2027-01-01 MSG hi\n").should.be.empty
|
|
163
|
+
lint.("REM Mon UNTIL 2027-01-01 MSG hi\n").should.be.empty
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "accepts a reminder with neither" do
|
|
167
|
+
lint.("REM Mon MSG hi\n").should.be.empty
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "says nothing about a SCANFROM written as a delta" do
|
|
171
|
+
# `SCANFROM -7` has no date to compare against.
|
|
172
|
+
lint.("REM Mon SCANFROM -7 UNTIL 2027-01-01 MSG hi\n").should.be.empty
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "does not read the two clauses across different commands" do
|
|
176
|
+
text = "REM Mon FROM 2027-03-01 MSG a\nREM Tue UNTIL 2027-01-01 MSG b\n"
|
|
177
|
+
|
|
178
|
+
lint.(text).should.be.empty
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe "dates it cannot read" do
|
|
183
|
+
it "says nothing when a date is computed" do
|
|
184
|
+
lint.("REM Mon FROM [x] UNTIL 2027-01-01 MSG hi\n").should.be.empty
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "says nothing when a date is partial" do
|
|
188
|
+
# GetFullDate would reject it; that is a different rule's offence.
|
|
189
|
+
lint.("REM Mon FROM 1 Jan UNTIL 2027-01-01 MSG hi\n").should.be.empty
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "says nothing about comments" do
|
|
194
|
+
lint.("# FROM 2027-03-01 UNTIL 2027-01-01\n").should.be.empty
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "reports at error severity" do
|
|
198
|
+
lint.("REM Mon FROM 2027-03-01 UNTIL 2027-01-01 MSG hi\n").first.severity.should == "error"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "reports the physical line inside a continuation" do
|
|
202
|
+
lint.("REM Mon FROM 2027-03-01 \\\n UNTIL 2027-01-01 MSG hi\n").first.line.should == 2
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A reminder file anyone can write to.
|
|
8
|
+
#
|
|
9
|
+
# Remind refuses a world-writable script outright, and disables `RUN` for
|
|
10
|
+
# one it does not own -- because a reminder file is a program, and a
|
|
11
|
+
# program anybody can edit will eventually run something somebody else
|
|
12
|
+
# wrote. The check is in src/files.c (`CheckSafety`).
|
|
13
|
+
#
|
|
14
|
+
# This is the only rule that looks at a file rather than at its contents,
|
|
15
|
+
# and the only one that can fail on a file that is textually perfect. It
|
|
16
|
+
# earns its place because the failure mode is a cron job that stops
|
|
17
|
+
# working, or worse does not, and the mode is invisible in every editor.
|
|
18
|
+
#
|
|
19
|
+
# Group-writable is reported separately and more quietly: Remind allows it,
|
|
20
|
+
# but it means the file's safety rests on the group's membership.
|
|
21
|
+
class WorldWritableScript < Rule
|
|
22
|
+
WORLD_WRITABLE = 0o002
|
|
23
|
+
GROUP_WRITABLE = 0o020
|
|
24
|
+
|
|
25
|
+
def self.default_severity
|
|
26
|
+
"error"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.description
|
|
30
|
+
"A reminder file that is world-writable, which Remind refuses to read."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check
|
|
34
|
+
mode = mode_of(document.path)
|
|
35
|
+
|
|
36
|
+
if mode
|
|
37
|
+
report(mode)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Only a real file on disk has a mode; text linted from a string has
|
|
44
|
+
# nothing to check.
|
|
45
|
+
def mode_of(path)
|
|
46
|
+
if File.file?(path)
|
|
47
|
+
File.stat(path).mode
|
|
48
|
+
end
|
|
49
|
+
rescue SystemCallError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def report(mode)
|
|
54
|
+
if (mode & WORLD_WRITABLE).positive?
|
|
55
|
+
offend(
|
|
56
|
+
document.line_number_at(0),
|
|
57
|
+
"this file is world-writable (mode #{format('%<mode>04o', mode: mode & 0o7777)}); " \
|
|
58
|
+
"Remind refuses to read a script anyone can edit",
|
|
59
|
+
)
|
|
60
|
+
elsif (mode & GROUP_WRITABLE).positive?
|
|
61
|
+
offend(
|
|
62
|
+
document.line_number_at(0),
|
|
63
|
+
"this file is group-writable (mode #{format('%<mode>04o', mode: mode & 0o7777)}); " \
|
|
64
|
+
"Remind allows it, but the script's safety then rests on the group's membership",
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
__END__
|
|
73
|
+
|
|
74
|
+
require "fileutils"
|
|
75
|
+
require "tmpdir"
|
|
76
|
+
|
|
77
|
+
require_relative "../document"
|
|
78
|
+
|
|
79
|
+
describe "RemLint::Rules::WorldWritableScript" do
|
|
80
|
+
root = Dir.mktmpdir("remlint-modes")
|
|
81
|
+
|
|
82
|
+
written = proc do |mode|
|
|
83
|
+
path = File.join(root, "mode-#{format('%o', mode)}.rem")
|
|
84
|
+
File.write(path, "MSG hi\n")
|
|
85
|
+
File.chmod(mode, path)
|
|
86
|
+
|
|
87
|
+
RemLint::Rules::WorldWritableScript.new.run(
|
|
88
|
+
RemLint::Document.new(RemLint::Source.new(path: path, text: "MSG hi\n")),
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "accepts a file only its owner can write" do
|
|
93
|
+
written.(0o644).should.be.empty
|
|
94
|
+
written.(0o600).should.be.empty
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "reports a world-writable file" do
|
|
98
|
+
written.(0o666).first.message.should.match(
|
|
99
|
+
/world-writable \(mode 0666\); Remind refuses to read a script anyone can edit/,
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "reports a world-writable file whatever else it allows" do
|
|
104
|
+
written.(0o777).length.should == 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "reports a group-writable file more quietly" do
|
|
108
|
+
written.(0o664).first.message.should.match(/group-writable \(mode 0664\)/)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "prefers the world-writable message when both apply" do
|
|
112
|
+
written.(0o666).first.message.should.match(/world-writable/)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "says nothing when there is no file on disk to stat" do
|
|
116
|
+
source = RemLint::Source.new(path: "not-a-real-path.rem", text: "MSG hi\n")
|
|
117
|
+
|
|
118
|
+
RemLint::Rules::WorldWritableScript.new.run(RemLint::Document.new(source)).should.be.empty
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "reports on the first line of the source" do
|
|
122
|
+
written.(0o666).first.line.should == 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "reports at error severity" do
|
|
126
|
+
written.(0o666).first.severity.should == "error"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rule"
|
|
4
|
+
|
|
5
|
+
# Requiring a rule file registers the rule, so this list is the only place a
|
|
6
|
+
# new rule has to be mentioned. Sorted by name to make an addition a one-line
|
|
7
|
+
# diff rather than an argument about ordering.
|
|
8
|
+
require_relative "rules/addomit_without_scanfrom"
|
|
9
|
+
require_relative "rules/advance_warning_body"
|
|
10
|
+
require_relative "rules/banner_placement"
|
|
11
|
+
require_relative "rules/calendar_text_limited"
|
|
12
|
+
require_relative "rules/callback_signature"
|
|
13
|
+
require_relative "rules/clause_needs_full_date"
|
|
14
|
+
require_relative "rules/clause_requires_at"
|
|
15
|
+
require_relative "rules/clause_value_range"
|
|
16
|
+
require_relative "rules/color_component_range"
|
|
17
|
+
require_relative "rules/coordinate_not_string"
|
|
18
|
+
require_relative "rules/dangling_continuation"
|
|
19
|
+
require_relative "rules/date_out_of_range"
|
|
20
|
+
require_relative "rules/debug_command"
|
|
21
|
+
require_relative "rules/easterdate_from_today"
|
|
22
|
+
require_relative "rules/function_arity"
|
|
23
|
+
require_relative "rules/function_redefinition"
|
|
24
|
+
require_relative "rules/generated_file_edited"
|
|
25
|
+
require_relative "rules/hebrew_date"
|
|
26
|
+
require_relative "rules/iftrig_with_satisfy"
|
|
27
|
+
require_relative "rules/include_path"
|
|
28
|
+
require_relative "rules/info_clause"
|
|
29
|
+
require_relative "rules/info_substitution_without_header"
|
|
30
|
+
require_relative "rules/invocation_mismatch"
|
|
31
|
+
require_relative "rules/keyword_case"
|
|
32
|
+
require_relative "rules/license_header"
|
|
33
|
+
require_relative "rules/line_length"
|
|
34
|
+
require_relative "rules/literal_type_mismatch"
|
|
35
|
+
require_relative "rules/localization_pack"
|
|
36
|
+
require_relative "rules/moon_phase_argument"
|
|
37
|
+
require_relative "rules/omit_aware_delta"
|
|
38
|
+
require_relative "rules/push_vars_missing_name"
|
|
39
|
+
require_relative "rules/repeat_trigger"
|
|
40
|
+
require_relative "rules/satisfy_constraint"
|
|
41
|
+
require_relative "rules/shell_maxlen"
|
|
42
|
+
require_relative "rules/shell_use_while_run_disabled"
|
|
43
|
+
require_relative "rules/string_escape"
|
|
44
|
+
require_relative "rules/syntax"
|
|
45
|
+
require_relative "rules/system_variable_assignment"
|
|
46
|
+
require_relative "rules/tag_syntax"
|
|
47
|
+
require_relative "rules/text_after_eof_marker"
|
|
48
|
+
require_relative "rules/time_zone_name"
|
|
49
|
+
require_relative "rules/tk_tag_namespace"
|
|
50
|
+
require_relative "rules/todo_complete_through"
|
|
51
|
+
require_relative "rules/trailing_whitespace"
|
|
52
|
+
require_relative "rules/translate_command"
|
|
53
|
+
require_relative "rules/unbalanced_blocks"
|
|
54
|
+
require_relative "rules/unbalanced_delimiters"
|
|
55
|
+
require_relative "rules/unknown_special_type"
|
|
56
|
+
require_relative "rules/unknown_substitution_sequence"
|
|
57
|
+
require_relative "rules/unknown_system_variable"
|
|
58
|
+
require_relative "rules/unquoted_shell_substitution"
|
|
59
|
+
require_relative "rules/until_before_from"
|
|
60
|
+
require_relative "rules/world_writable_script"
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require_relative "config"
|
|
6
|
+
require_relative "document"
|
|
7
|
+
require_relative "extractors"
|
|
8
|
+
|
|
9
|
+
module RemLint
|
|
10
|
+
# Walks paths, extracts Remind out of them, runs the enabled rules, and drops
|
|
11
|
+
# whatever the file itself asked to be quiet about.
|
|
12
|
+
class Runner
|
|
13
|
+
# `# remlint:disable UnbalancedBlocks, TrailingWhitespace`, or `all`.
|
|
14
|
+
# Recognised after either comment character, and mid-line, so it can sit at
|
|
15
|
+
# the end of the line it applies to.
|
|
16
|
+
DISABLE = /[#;]\s*remlint:disable\s+(?<rules>[\w\s,-]+)/
|
|
17
|
+
|
|
18
|
+
ALL = "all"
|
|
19
|
+
|
|
20
|
+
# Extensions worth opening when walking a directory. A file with no
|
|
21
|
+
# extension is opened too, because `examples/astro` and `examples/ansitext`
|
|
22
|
+
# have none and are where most of the Remind in that directory lives.
|
|
23
|
+
CANDIDATE = /\.(rem|sh|bash)\z|\A[^.]+\z/
|
|
24
|
+
|
|
25
|
+
attr_reader :config, :warnings
|
|
26
|
+
|
|
27
|
+
def initialize(config = Config.default)
|
|
28
|
+
@config = config
|
|
29
|
+
@warnings = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the offences, worst-placed first in reading order.
|
|
33
|
+
def run(paths)
|
|
34
|
+
files(paths).flat_map { |path| lint_file(path) }.sort_by(&:sort_key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The files a set of paths expands to: a named file is taken as given, a
|
|
38
|
+
# named directory is walked.
|
|
39
|
+
def files(paths)
|
|
40
|
+
Array(paths).flat_map { |path| expand(path) }.uniq.reject { |path| config.excluded?(path) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def lint_file(path)
|
|
44
|
+
text = read(path)
|
|
45
|
+
|
|
46
|
+
if text.nil?
|
|
47
|
+
[]
|
|
48
|
+
else
|
|
49
|
+
lint_text(path, text)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lint_text(path, text)
|
|
54
|
+
rules = config.rules
|
|
55
|
+
|
|
56
|
+
Extractors.extract(path, text).flat_map do |source|
|
|
57
|
+
lint_source(Document.new(source), rules)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def expand(path)
|
|
64
|
+
pathname = Pathname.new(path)
|
|
65
|
+
|
|
66
|
+
if pathname.directory?
|
|
67
|
+
walk(pathname)
|
|
68
|
+
else
|
|
69
|
+
[path.to_s]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def walk(directory)
|
|
74
|
+
directory.find.filter_map do |entry|
|
|
75
|
+
if entry.file? && entry.basename.to_s.match?(CANDIDATE)
|
|
76
|
+
entry.to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A file Remind can read is a file of bytes; one that is not valid UTF-8
|
|
82
|
+
# is not Remind's problem or ours, and neither is one we cannot open.
|
|
83
|
+
def read(path)
|
|
84
|
+
text = File.read(path, encoding: "UTF-8")
|
|
85
|
+
|
|
86
|
+
if text.valid_encoding?
|
|
87
|
+
text
|
|
88
|
+
end
|
|
89
|
+
rescue SystemCallError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def lint_source(document, rules)
|
|
94
|
+
found = rules.flat_map { |rule| rule.run(document) }
|
|
95
|
+
|
|
96
|
+
note_unavailable(rules)
|
|
97
|
+
|
|
98
|
+
found.reject { |offense| disabled?(document, offense) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A rule that could not do its job says so once for the whole run, not
|
|
102
|
+
# once per file and not silently. `Syntax` is the only one: skipping
|
|
103
|
+
# quietly would let a build believe it is syntax-checking when the
|
|
104
|
+
# `remind` it needs is not installed.
|
|
105
|
+
def note_unavailable(rules)
|
|
106
|
+
rules.each do |rule|
|
|
107
|
+
missing = rule.respond_to?(:unavailable) && rule.unavailable
|
|
108
|
+
|
|
109
|
+
if missing
|
|
110
|
+
record(
|
|
111
|
+
"the Syntax rule is enabled but `#{missing}` is not on PATH, " \
|
|
112
|
+
"so no file was syntax-checked",
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def record(warning)
|
|
119
|
+
unless @warnings.include?(warning)
|
|
120
|
+
@warnings << warning
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A directive applies to the line it is on and to the line below it, so
|
|
125
|
+
# it can be written after the offending line or above it -- puppet-lint's
|
|
126
|
+
# two forms, which cover the case where the line has no room left.
|
|
127
|
+
def disabled?(document, offense)
|
|
128
|
+
index = offense.line - document.line_offset - 1
|
|
129
|
+
|
|
130
|
+
[index, index - 1].any? do |candidate|
|
|
131
|
+
directive_covers?(document.raw_lines[candidate], offense.rule, candidate)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def directive_covers?(raw, rule_name, index)
|
|
136
|
+
match = index >= 0 && raw&.match(DISABLE)
|
|
137
|
+
|
|
138
|
+
if match
|
|
139
|
+
named = match[:rules].split(/[,\s]+/).reject(&:empty?)
|
|
140
|
+
|
|
141
|
+
named.include?(ALL) || named.include?(rule_name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
__END__
|
|
148
|
+
|
|
149
|
+
require "tempfile"
|
|
150
|
+
|
|
151
|
+
require_relative "rules/trailing_whitespace"
|
|
152
|
+
require_relative "rules/unbalanced_blocks"
|
|
153
|
+
|
|
154
|
+
describe "RemLint::Runner" do
|
|
155
|
+
# Just the two rules, so the assertions do not shift when a rule is added.
|
|
156
|
+
config = RemLint::Config.default.only(%w[TrailingWhitespace UnbalancedBlocks])
|
|
157
|
+
runner = RemLint::Runner.new(config)
|
|
158
|
+
|
|
159
|
+
lint = proc { |text, path = "t.rem"| runner.lint_text(path, text) }
|
|
160
|
+
rules_of = proc { |text| lint.(text).map(&:rule) }
|
|
161
|
+
|
|
162
|
+
describe "linting text" do
|
|
163
|
+
it "runs every enabled rule" do
|
|
164
|
+
rules_of.("IF a\nMSG hi \n").sort.should == %w[TrailingWhitespace UnbalancedBlocks]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "returns nothing for a clean file" do
|
|
168
|
+
lint.("IF a\n MSG hi\nENDIF\n").should.be.empty
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "runs only the rules the config leaves on" do
|
|
172
|
+
only_blocks = RemLint::Runner.new(RemLint::Config.default.only(%w[UnbalancedBlocks]))
|
|
173
|
+
|
|
174
|
+
only_blocks.lint_text("t.rem", "IF a\nMSG hi \n").map(&:rule).should == ["UnbalancedBlocks"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "sorts offences into reading order when running over real paths" do
|
|
178
|
+
Tempfile.create(["spec", ".rem"]) do |file|
|
|
179
|
+
file.write("MSG trailing \nIF a\nMSG also trailing \n")
|
|
180
|
+
file.flush
|
|
181
|
+
|
|
182
|
+
runner.run([file.path]).map { |offense| [offense.line, offense.rule] }.should ==
|
|
183
|
+
[[1, "TrailingWhitespace"], [2, "UnbalancedBlocks"], [3, "TrailingWhitespace"]]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe "extraction" do
|
|
189
|
+
it "lints a .rem file whole" do
|
|
190
|
+
lint.("MSG hi \n").length.should == 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "lints a heredoc and reports positions in the enclosing file" do
|
|
194
|
+
script = "#!/bin/sh\n# note\nremind - <<'EOF'\nMSG hi \nEOF\n"
|
|
195
|
+
|
|
196
|
+
offenses = lint.(script, "ansitext")
|
|
197
|
+
offenses.length.should == 1
|
|
198
|
+
offenses.first.line.should == 4
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "ignores a file that holds no Remind at all" do
|
|
202
|
+
lint.("#!/bin/sh\necho hello \n", "build.sh").should.be.empty
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe "disable comments" do
|
|
207
|
+
it "drops an offence named on the same line" do
|
|
208
|
+
lint.("MSG hi # remlint:disable TrailingWhitespace\n").should.be.empty
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "drops an offence named on the line above" do
|
|
212
|
+
lint.("# remlint:disable TrailingWhitespace\nMSG hi \n").should.be.empty
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "accepts the semicolon comment character too" do
|
|
216
|
+
lint.("; remlint:disable TrailingWhitespace\nMSG hi \n").should.be.empty
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "drops several rules named together" do
|
|
220
|
+
text = "# remlint:disable TrailingWhitespace, UnbalancedBlocks\nIF a\nMSG hi \n"
|
|
221
|
+
|
|
222
|
+
lint.(text).map(&:rule).should == ["TrailingWhitespace"]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "drops everything when told all" do
|
|
226
|
+
lint.("# remlint:disable all\nMSG hi \n").should.be.empty
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "leaves an offence a directive does not name" do
|
|
230
|
+
lint.("# remlint:disable UnbalancedBlocks\nMSG hi \n").map(&:rule).should ==
|
|
231
|
+
["TrailingWhitespace"]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "does not reach a line further than one below the directive" do
|
|
235
|
+
lint.("# remlint:disable TrailingWhitespace\nMSG ok\nMSG hi \n").length.should == 1
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
describe "run warnings" do
|
|
240
|
+
it "says once when the Syntax rule cannot find its command" do
|
|
241
|
+
only_syntax = RemLint::Config.default
|
|
242
|
+
.only(%w[Syntax])
|
|
243
|
+
.merge("Syntax" => { "Command" => "definitely-not-installed" })
|
|
244
|
+
runner = RemLint::Runner.new(only_syntax)
|
|
245
|
+
|
|
246
|
+
runner.lint_text("a.rem", "MSG hi\n")
|
|
247
|
+
runner.lint_text("b.rem", "MSG hi\n")
|
|
248
|
+
|
|
249
|
+
runner.warnings.length.should == 1
|
|
250
|
+
runner.warnings.first.should.match(/is not on PATH, so no file was syntax-checked/)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "says nothing when no rule is short of anything" do
|
|
254
|
+
RemLint::Runner.new(config).tap { |r| r.lint_text("a.rem", "MSG hi\n") }
|
|
255
|
+
.warnings.should.be.empty
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe "walking paths" do
|
|
260
|
+
it "keeps a named file as given" do
|
|
261
|
+
runner.files(["examples/holidays.rem"]).should == ["examples/holidays.rem"]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "drops a path the config excludes" do
|
|
265
|
+
excluding = RemLint::Runner.new(RemLint::Config.new("Exclude" => ["vendor/**/*"]))
|
|
266
|
+
|
|
267
|
+
excluding.files(["vendor/a.rem", "b.rem"]).should == ["b.rem"]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "returns nothing for a file it cannot read" do
|
|
271
|
+
runner.lint_file("/nonexistent/nowhere.rem").should.be.empty
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RemLint
|
|
4
|
+
# A run of Remind code carved out of a file, together with where it sat.
|
|
5
|
+
#
|
|
6
|
+
# `line_offset` is the number of physical lines that preceded this run in the
|
|
7
|
+
# file on disk -- zero for a plain `.rem` file, and the line number of the
|
|
8
|
+
# heredoc's opener for Remind embedded in a shell script. Every line number
|
|
9
|
+
# the linter reports is computed through this offset, so an offence inside
|
|
10
|
+
# the third heredoc of a shell script points at the real line of that script
|
|
11
|
+
# rather than at line 4 of an anonymous fragment.
|
|
12
|
+
Source = Struct.new(
|
|
13
|
+
:path,
|
|
14
|
+
:text,
|
|
15
|
+
:line_offset,
|
|
16
|
+
:description,
|
|
17
|
+
keyword_init: true,
|
|
18
|
+
) do
|
|
19
|
+
def initialize(path:, text:, line_offset: 0, description: nil)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lines
|
|
24
|
+
text.lines
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# How to name this source in a message when a file yields more than one.
|
|
28
|
+
def label
|
|
29
|
+
if description
|
|
30
|
+
"#{path} (#{description})"
|
|
31
|
+
else
|
|
32
|
+
path
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|