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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A function that shells out, defined while `RUN` is off.
|
|
8
|
+
#
|
|
9
|
+
# Remind binds the `RUN` permission when a function is *defined*, not when
|
|
10
|
+
# it is called. `userfns.c` stores the flag on the function
|
|
11
|
+
# (`if (RunDisabled) func->run_disabled = 1`) and `expr.c` puts it back on
|
|
12
|
+
# every call (`if (f->run_disabled) RunDisabled |= RUN_UF`). So:
|
|
13
|
+
#
|
|
14
|
+
# RUN OFF
|
|
15
|
+
# FSET listing() shell("ls")
|
|
16
|
+
# RUN ON
|
|
17
|
+
# MSG [listing()] # RUN disabled
|
|
18
|
+
#
|
|
19
|
+
# The function is defined in the sandbox and stays in it for good. Nothing
|
|
20
|
+
# about the definition looks wrong, the `RUN ON` above the call looks like
|
|
21
|
+
# it should be enough, and the error arrives from the call site -- which is
|
|
22
|
+
# the one place the mistake is not.
|
|
23
|
+
#
|
|
24
|
+
# `shell()` is the only builtin that needs the permission (`funcs.c:2545`);
|
|
25
|
+
# `INCLUDECMD` needs it too but is a command, not something a function can
|
|
26
|
+
# contain.
|
|
27
|
+
class ShellUseWhileRunDisabled < Rule
|
|
28
|
+
SHELL = "shell"
|
|
29
|
+
|
|
30
|
+
ON = "ON"
|
|
31
|
+
OFF = "OFF"
|
|
32
|
+
|
|
33
|
+
def self.default_severity
|
|
34
|
+
"error"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.description
|
|
38
|
+
"A function calling shell(), defined between RUN OFF and RUN ON."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def check
|
|
42
|
+
disabled = false
|
|
43
|
+
|
|
44
|
+
document.code_commands.each do |command|
|
|
45
|
+
directive = run_directive(command)
|
|
46
|
+
|
|
47
|
+
if directive
|
|
48
|
+
disabled = directive == OFF
|
|
49
|
+
elsif disabled
|
|
50
|
+
check_definition(command)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# `RUN ON` / `RUN OFF` is a directive; any other `RUN` is a reminder
|
|
58
|
+
# that shells out. `DoRun` accepts exactly these two words, in any case.
|
|
59
|
+
def run_directive(command)
|
|
60
|
+
word = command.args.strip.upcase
|
|
61
|
+
|
|
62
|
+
if command.keyword?("RUN") && (word == ON || word == OFF)
|
|
63
|
+
word
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_definition(command)
|
|
68
|
+
if command.keyword?("FSET")
|
|
69
|
+
shell_calls(command).each do |token|
|
|
70
|
+
offend_at(command.logical_line, token.offset, message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def shell_calls(command)
|
|
76
|
+
document.tokens_for(command.logical_line).select do |token|
|
|
77
|
+
token.type == :function && token.value.casecmp?(SHELL)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def message
|
|
82
|
+
"`shell()` in a function defined while `RUN` is off: the permission " \
|
|
83
|
+
"is bound here, not at the call, so this fails even after `RUN ON`"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
__END__
|
|
90
|
+
|
|
91
|
+
require_relative "../document"
|
|
92
|
+
|
|
93
|
+
describe "RemLint::Rules::ShellUseWhileRunDisabled" do
|
|
94
|
+
lint = proc do |text|
|
|
95
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
96
|
+
|
|
97
|
+
RemLint::Rules::ShellUseWhileRunDisabled.new.run(RemLint::Document.new(source))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "reports a shell() function defined after RUN OFF" do
|
|
101
|
+
offenses = lint.(%(RUN OFF\nFSET listing() shell("ls")\nRUN ON\n))
|
|
102
|
+
|
|
103
|
+
offenses.length.should == 1
|
|
104
|
+
offenses.first.line.should == 2
|
|
105
|
+
offenses.first.message.should.match(/bound here, not at the call/)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "accepts the same definition before RUN OFF" do
|
|
109
|
+
lint.(%(FSET listing() shell("ls")\nRUN OFF\n)).should.be.empty
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "accepts the same definition after RUN is turned back on" do
|
|
113
|
+
lint.(%(RUN OFF\nRUN ON\nFSET listing() shell("ls")\n)).should.be.empty
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "accepts a file that never turns RUN off" do
|
|
117
|
+
lint.(%(FSET listing() shell("ls")\n)).should.be.empty
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "reports every offending definition in the disabled span" do
|
|
121
|
+
text = %(RUN OFF\nFSET a() shell("ls")\nFSET b() shell("date")\nRUN ON\n)
|
|
122
|
+
|
|
123
|
+
lint.(text).map(&:line).should == [2, 3]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "reports two calls in one definition" do
|
|
127
|
+
lint.(%(RUN OFF\nFSET a() shell("ls") + shell("date")\n)).length.should == 2
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "matches RUN OFF whatever its case" do
|
|
131
|
+
lint.(%(run off\nFSET a() shell("ls")\n)).length.should == 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "matches shell whatever its case" do
|
|
135
|
+
lint.(%(RUN OFF\nFSET a() SHELL("ls")\n)).length.should == 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "points at the call" do
|
|
139
|
+
text = %(RUN OFF\nFSET listing() shell("ls")\n)
|
|
140
|
+
|
|
141
|
+
lint.(text).first.column.should == %(FSET listing() shell("ls")).index("shell") + 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe "what it leaves alone" do
|
|
145
|
+
it "says nothing about a function that does not shell out" do
|
|
146
|
+
lint.("RUN OFF\nFSET double(x) x * 2\n").should.be.empty
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "says nothing about a RUN reminder, which is not a directive" do
|
|
150
|
+
# `RUN OFF` is the directive; `RUN echo off` is a reminder that runs a
|
|
151
|
+
# command, and it does not disable anything.
|
|
152
|
+
lint.(%(REM 1 Jan RUN echo off\nFSET a() shell("ls")\n)).should.be.empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "does not read a bare RUN reminder as a directive" do
|
|
156
|
+
lint.(%(RUN echo hello\nFSET a() shell("ls")\n)).should.be.empty
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "says nothing about a shell() call outside a definition" do
|
|
160
|
+
# That one fails visibly, at the point it is written.
|
|
161
|
+
lint.(%(RUN OFF\nSET x shell("ls")\n)).should.be.empty
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "says nothing about comments" do
|
|
165
|
+
lint.(%(RUN OFF\n# FSET a() shell("ls")\n)).should.be.empty
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "reports at error severity" do
|
|
170
|
+
lint.(%(RUN OFF\nFSET a() shell("ls")\n)).first.severity.should == "error"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Backslash escapes in a string that are not escapes.
|
|
8
|
+
#
|
|
9
|
+
# Remind's escape set is `\a \b \f \n \r \t \v` and `\xHH`. Anything else
|
|
10
|
+
# reaches a `default:` branch in src/expr.c that emits the character and
|
|
11
|
+
# drops the backslash -- so `\q` is `q`, silently, with no way to tell it
|
|
12
|
+
# from a `q` somebody meant to type.
|
|
13
|
+
#
|
|
14
|
+
# `\\` and `\"` go through that same default branch, which is exactly how
|
|
15
|
+
# they work, so neither is reported: writing them is the fix, not the
|
|
16
|
+
# fault.
|
|
17
|
+
#
|
|
18
|
+
# `\x00` is different, and is the one case Remind refuses outright: a NUL
|
|
19
|
+
# cannot appear inside a Remind string, and the parser says so.
|
|
20
|
+
class StringEscape < Rule
|
|
21
|
+
# From the `switch(**in)` over escapes in src/expr.c.
|
|
22
|
+
DEFINED = %w[a b f n r t v x].freeze
|
|
23
|
+
|
|
24
|
+
# Handled by the default branch, and correct.
|
|
25
|
+
LITERAL = ['\\', '"', "'"].freeze
|
|
26
|
+
|
|
27
|
+
ESCAPE = /\\(?<body>x[0-9a-fA-F]{1,2}|.)/
|
|
28
|
+
|
|
29
|
+
NUL = /\Ax0+\z/
|
|
30
|
+
|
|
31
|
+
def self.default_severity
|
|
32
|
+
"warning"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.description
|
|
36
|
+
"A backslash escape Remind does not define, or the prohibited \\x00."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check
|
|
40
|
+
document.logical_lines.each_with_index do |logical_line, index|
|
|
41
|
+
if document.commands[index].code?
|
|
42
|
+
check_line(logical_line)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def check_line(logical_line)
|
|
50
|
+
document.tokens_for(logical_line).each do |token|
|
|
51
|
+
if token.type == :string && token.value.start_with?('"')
|
|
52
|
+
check_string(logical_line, token)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check_string(logical_line, token)
|
|
58
|
+
token.value.to_enum(:scan, ESCAPE).each do
|
|
59
|
+
match = Regexp.last_match
|
|
60
|
+
complaint = fault(match[:body])
|
|
61
|
+
|
|
62
|
+
if complaint
|
|
63
|
+
offend_at(logical_line, token.offset + match.begin(0), complaint)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fault(body)
|
|
69
|
+
if body.match?(NUL)
|
|
70
|
+
"`\\#{body}` is prohibited outright -- a NUL cannot appear inside a Remind string"
|
|
71
|
+
elsif !DEFINED.include?(body[0]) && !LITERAL.include?(body[0])
|
|
72
|
+
"`\\#{body}` is not an escape Remind defines; it drops the backslash " \
|
|
73
|
+
"and prints `#{body}`"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
__END__
|
|
81
|
+
|
|
82
|
+
require_relative "../document"
|
|
83
|
+
|
|
84
|
+
describe "RemLint::Rules::StringEscape" do
|
|
85
|
+
lint = proc do |text|
|
|
86
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
87
|
+
|
|
88
|
+
RemLint::Rules::StringEscape.new.run(RemLint::Document.new(source))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
92
|
+
|
|
93
|
+
describe "escapes Remind defines" do
|
|
94
|
+
it "accepts the control escapes" do
|
|
95
|
+
lint.(%(SET a "one\\ttwo\\nthree"\n)).should.be.empty
|
|
96
|
+
lint.(%(SET a "\\a\\b\\f\\r\\v"\n)).should.be.empty
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "accepts a hex escape" do
|
|
100
|
+
lint.(%(SET a "\\x41\\x7f"\n)).should.be.empty
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "accepts an escaped backslash and quote" do
|
|
104
|
+
# Both go through the same default branch, which is how they work.
|
|
105
|
+
lint.(%(SET a "a \\\\ b \\" c"\n)).should.be.empty
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "escapes Remind does not define" do
|
|
110
|
+
it "reports an unknown letter" do
|
|
111
|
+
messages.(%(SET a "\\q"\n)).first.should ==
|
|
112
|
+
"`\\q` is not an escape Remind defines; it drops the backslash and prints `q`"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "reports each one" do
|
|
116
|
+
messages.(%(SET a "\\q and \\z"\n)).length.should == 2
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "points at the backslash" do
|
|
120
|
+
text = %(SET a "x\\qy"\n)
|
|
121
|
+
|
|
122
|
+
lint.(text).first.column.should == text.index("\\q") + 1
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "the prohibited NUL" do
|
|
127
|
+
it "reports \\x00" do
|
|
128
|
+
messages.(%(SET a "\\x00"\n)).first.should.match(/prohibited outright/)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "reports \\x0 as well" do
|
|
132
|
+
messages.(%(SET a "\\x0"\n)).first.should.match(/prohibited outright/)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "accepts a hex escape that is not zero" do
|
|
136
|
+
lint.(%(SET a "\\x01"\n)).should.be.empty
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "where it does not look" do
|
|
141
|
+
it "says nothing about a single-quoted date constant" do
|
|
142
|
+
lint.("SET a '2027-01-01'\n").should.be.empty
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "says nothing about a backslash outside a string" do
|
|
146
|
+
# A trailing backslash is a line continuation, not an escape.
|
|
147
|
+
lint.("SET a 1 + \\\n 2\n").should.be.empty
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "says nothing about comments" do
|
|
151
|
+
lint.(%(# SET a "\\q"\n)).should.be.empty
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "reports at warning severity" do
|
|
156
|
+
lint.(%(SET a "\\q"\n)).first.severity.should == "warning"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
require_relative "../rule"
|
|
7
|
+
|
|
8
|
+
module RemLint
|
|
9
|
+
module Rules
|
|
10
|
+
# Remind's own diagnostics, folded into the linter's output.
|
|
11
|
+
#
|
|
12
|
+
# puppet-lint draws the line here and it is the right line: a linter
|
|
13
|
+
# validates style, and for "is this even valid" you run the real parser.
|
|
14
|
+
# Remind has no parse-only mode, so the real parser is Remind, and running
|
|
15
|
+
# it means running the file.
|
|
16
|
+
#
|
|
17
|
+
# THIS RULE IS OFF BY DEFAULT, and stays off unless someone turns it on,
|
|
18
|
+
# because of what running the file means:
|
|
19
|
+
#
|
|
20
|
+
# INCLUDE and INCLUDECMD read -- and INCLUDECMD executes -- other things
|
|
21
|
+
# RUN reminders shell out
|
|
22
|
+
#
|
|
23
|
+
# `-r` is passed to disable RUN directives, `-q` to keep timed reminders
|
|
24
|
+
# out of the queue, and `-n` to ask only for next occurrences. That covers
|
|
25
|
+
# the RUN half. It does not cover INCLUDECMD, so turn this on for files you
|
|
26
|
+
# trust and leave it off for files you do not.
|
|
27
|
+
#
|
|
28
|
+
# When `remind` is not on PATH the rule reports nothing rather than
|
|
29
|
+
# failing: a linter that cannot run on a machine without Remind installed
|
|
30
|
+
# is a linter that cannot run in CI. It does say so once, through
|
|
31
|
+
# `Runner`, because silently not syntax-checking is worse than not
|
|
32
|
+
# syntax-checking -- a build can otherwise believe it is covered when it
|
|
33
|
+
# is not.
|
|
34
|
+
class Syntax < Rule
|
|
35
|
+
DEFAULT_COMMAND = "remind"
|
|
36
|
+
|
|
37
|
+
# `file(12): Some message` and `file(12:14): Some message`, the two forms
|
|
38
|
+
# src/main.c prints depending on whether the command spanned lines.
|
|
39
|
+
DIAGNOSTIC = /\A(?<file>[^(]+)\((?<line>\d+)(?::(?<end_line>\d+))?\):\s*(?<message>.*)\z/
|
|
40
|
+
|
|
41
|
+
# Remind labels its own non-fatal diagnostics -- "Warning: Missing ENDIF"
|
|
42
|
+
# and friends -- so they are relayed as warnings rather than promoted to
|
|
43
|
+
# errors by this rule's configured severity.
|
|
44
|
+
WARNING = /\Awarning\b/i
|
|
45
|
+
|
|
46
|
+
def self.enabled_by_default?
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.default_severity
|
|
51
|
+
"error"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.description
|
|
55
|
+
"Diagnostics from running the file through Remind itself (off by default)."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Set once per run when the configured command is missing, so `Runner`
|
|
59
|
+
# can say so a single time rather than per file.
|
|
60
|
+
attr_reader :unavailable
|
|
61
|
+
|
|
62
|
+
def check
|
|
63
|
+
command = option("Command", DEFAULT_COMMAND)
|
|
64
|
+
@unavailable = nil
|
|
65
|
+
|
|
66
|
+
if executable?(command)
|
|
67
|
+
run_remind(command)
|
|
68
|
+
else
|
|
69
|
+
@unavailable = command
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def executable?(command)
|
|
76
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
|
|
77
|
+
File.executable?(File.join(directory, command))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The source is written out rather than piped, because a heredoc's
|
|
82
|
+
# Remind is only part of its file and Remind must not see the shell
|
|
83
|
+
# around it. Line numbers come back relative to the temporary file and
|
|
84
|
+
# are shifted onto the real one.
|
|
85
|
+
def run_remind(command)
|
|
86
|
+
Tempfile.create(["remlint", ".rem"]) do |file|
|
|
87
|
+
file.write(document.source.text)
|
|
88
|
+
file.flush
|
|
89
|
+
|
|
90
|
+
report(capture(command, file.path))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def capture(command, path)
|
|
95
|
+
output, _status = Open3.capture2e(
|
|
96
|
+
command,
|
|
97
|
+
"-n",
|
|
98
|
+
"-r",
|
|
99
|
+
"-q",
|
|
100
|
+
path,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
output
|
|
104
|
+
rescue SystemCallError => error
|
|
105
|
+
"#{path}(1): could not run #{command}: #{error.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def report(output)
|
|
109
|
+
output.each_line do |raw|
|
|
110
|
+
match = raw.chomp.match(DIAGNOSTIC)
|
|
111
|
+
|
|
112
|
+
if match
|
|
113
|
+
relay(match)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def relay(match)
|
|
119
|
+
message = match[:message].strip
|
|
120
|
+
|
|
121
|
+
offend(shifted(match[:line]), message, severity: graded(message))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# nil means "use whatever the configuration says", which is what an
|
|
125
|
+
# unlabelled diagnostic gets.
|
|
126
|
+
def graded(message)
|
|
127
|
+
if message.match?(WARNING)
|
|
128
|
+
"warning"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def shifted(line)
|
|
133
|
+
Integer(line) + document.line_offset
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
__END__
|
|
140
|
+
|
|
141
|
+
require_relative "../document"
|
|
142
|
+
|
|
143
|
+
describe "RemLint::Rules::Syntax" do
|
|
144
|
+
lint = proc do |text, config = {}|
|
|
145
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
146
|
+
|
|
147
|
+
RemLint::Rules::Syntax.new(config).run(RemLint::Document.new(source))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "is off unless the configuration asks for it" do
|
|
151
|
+
RemLint::Rules::Syntax.enabled_by_default?.should.be.false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "reports nothing when the configured command is not on PATH" do
|
|
155
|
+
lint.("this is not valid remind at all\n", "Command" => "definitely-not-installed").should.be.empty
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "records which command was missing, so the runner can say so once" do
|
|
159
|
+
rule = RemLint::Rules::Syntax.new("Command" => "definitely-not-installed")
|
|
160
|
+
source = RemLint::Source.new(path: "t.rem", text: "MSG hi\n")
|
|
161
|
+
|
|
162
|
+
rule.run(RemLint::Document.new(source))
|
|
163
|
+
rule.unavailable.should == "definitely-not-installed"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
describe "parsing Remind's diagnostics" do
|
|
167
|
+
# A stub that prints what Remind prints, so the parsing is tested without
|
|
168
|
+
# requiring Remind to be installed wherever the suite runs.
|
|
169
|
+
stub = proc do |output|
|
|
170
|
+
directory = Dir.mktmpdir("remlint-spec")
|
|
171
|
+
path = File.join(directory, "fake-remind")
|
|
172
|
+
|
|
173
|
+
File.write(path, "#!/bin/sh\ncat <<'DIAGNOSTICS'\n#{output}\nDIAGNOSTICS\n")
|
|
174
|
+
File.chmod(0o755, path)
|
|
175
|
+
ENV["PATH"] = "#{directory}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH')}"
|
|
176
|
+
|
|
177
|
+
"fake-remind"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "turns a single-line diagnostic into an offence" do
|
|
181
|
+
command = stub.("/tmp/x.rem(3): Missing ']'")
|
|
182
|
+
offenses = lint.("MSG [x\n", "Command" => command)
|
|
183
|
+
|
|
184
|
+
offenses.length.should == 1
|
|
185
|
+
offenses.first.line.should == 3
|
|
186
|
+
offenses.first.message.should == "Missing ']'"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "takes the first line of a diagnostic that names a line range" do
|
|
190
|
+
command = stub.("/tmp/x.rem(3:5): Missing ')'")
|
|
191
|
+
|
|
192
|
+
lint.("MSG hi\n", "Command" => command).first.line.should == 3
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "reports every diagnostic Remind prints" do
|
|
196
|
+
command = stub.("/tmp/x.rem(1): First\n/tmp/x.rem(2): Second")
|
|
197
|
+
|
|
198
|
+
lint.("MSG hi\n", "Command" => command).map(&:line).should == [1, 2]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "ignores output that is not a diagnostic" do
|
|
202
|
+
command = stub.("MSG hi\nsomething else entirely")
|
|
203
|
+
|
|
204
|
+
lint.("MSG hi\n", "Command" => command).should.be.empty
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "shifts a heredoc's diagnostics onto the enclosing file" do
|
|
208
|
+
command = stub.("/tmp/x.rem(2): Missing ']'")
|
|
209
|
+
source = RemLint::Source.new(path: "astro", text: "MSG hi\nMSG [x\n", line_offset: 12)
|
|
210
|
+
rule = RemLint::Rules::Syntax.new("Command" => command)
|
|
211
|
+
|
|
212
|
+
rule.run(RemLint::Document.new(source)).first.line.should == 14
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "reports at error severity" do
|
|
216
|
+
command = stub.("/tmp/x.rem(1): Missing ']'")
|
|
217
|
+
|
|
218
|
+
lint.("MSG [x\n", "Command" => command).first.severity.should == "error"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "relays a diagnostic Remind labelled a warning as a warning" do
|
|
222
|
+
command = stub.("/tmp/x.rem(1): Warning: Missing ENDIF")
|
|
223
|
+
offense = lint.("IF a\n", "Command" => command).first
|
|
224
|
+
|
|
225
|
+
offense.severity.should == "warning"
|
|
226
|
+
offense.message.should == "Warning: Missing ENDIF"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|