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
data/lib/remlint/cli.rb
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "config"
|
|
6
|
+
require_relative "formatter"
|
|
7
|
+
require_relative "rules"
|
|
8
|
+
require_relative "runner"
|
|
9
|
+
require_relative "version"
|
|
10
|
+
|
|
11
|
+
module RemLint
|
|
12
|
+
# The `remlint` command.
|
|
13
|
+
#
|
|
14
|
+
# Streams are injected rather than assumed, so the whole command is testable
|
|
15
|
+
# without shelling out or capturing `$stdout`.
|
|
16
|
+
class CLI
|
|
17
|
+
EXIT_SUCCESS = 0
|
|
18
|
+
EXIT_OFFENSES = 1
|
|
19
|
+
EXIT_USAGE = 2
|
|
20
|
+
|
|
21
|
+
DEFAULT_PATHS = ["."].freeze
|
|
22
|
+
|
|
23
|
+
Options = Struct.new(
|
|
24
|
+
:paths,
|
|
25
|
+
:config_path,
|
|
26
|
+
:only,
|
|
27
|
+
:format,
|
|
28
|
+
:fail_level,
|
|
29
|
+
keyword_init: true,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
attr_reader :out, :err
|
|
33
|
+
|
|
34
|
+
def initialize(out: $stdout, err: $stderr)
|
|
35
|
+
@out = out
|
|
36
|
+
@err = err
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(argv)
|
|
40
|
+
options = Options.new(
|
|
41
|
+
paths: [],
|
|
42
|
+
config_path: nil,
|
|
43
|
+
only: nil,
|
|
44
|
+
format: Formatter::DEFAULT_TEMPLATE,
|
|
45
|
+
fail_level: "warning",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
dispatch(parse(argv, options), options)
|
|
49
|
+
rescue OptionParser::ParseError => error
|
|
50
|
+
err.puts(error.message)
|
|
51
|
+
EXIT_USAGE
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# `parse` returns a symbol for the flags that do something instead of
|
|
57
|
+
# linting, and nil for an ordinary run.
|
|
58
|
+
def dispatch(action, options)
|
|
59
|
+
case action
|
|
60
|
+
when :version then print_version
|
|
61
|
+
when :help then EXIT_SUCCESS
|
|
62
|
+
when :show_rules then print_rules
|
|
63
|
+
when :show_config then print_config(options)
|
|
64
|
+
else lint(options)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse(argv, options)
|
|
69
|
+
action = nil
|
|
70
|
+
|
|
71
|
+
parser(options) { |chosen| action = chosen }.parse!(argv)
|
|
72
|
+
options.paths = paths_from(argv)
|
|
73
|
+
|
|
74
|
+
action
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# No paths means the working directory, the way every other linter reads
|
|
78
|
+
# a bare invocation.
|
|
79
|
+
def paths_from(argv)
|
|
80
|
+
if argv.empty?
|
|
81
|
+
DEFAULT_PATHS.dup
|
|
82
|
+
else
|
|
83
|
+
argv
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parser(options)
|
|
88
|
+
OptionParser.new do |parser|
|
|
89
|
+
parser.banner = "Usage: remlint [options] [path...]"
|
|
90
|
+
|
|
91
|
+
parser.on("-c", "--config PATH", "Configuration file (default: nearest .remlint.yml)") do |path|
|
|
92
|
+
options.config_path = path
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
parser.on(
|
|
96
|
+
"-o",
|
|
97
|
+
"--only RULES",
|
|
98
|
+
Array,
|
|
99
|
+
"Run only these rules",
|
|
100
|
+
) do |rules|
|
|
101
|
+
options.only = rules
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
parser.on("-f", "--format TEMPLATE", "Output template with %{path} %{line} %{column} %{severity} %{rule} %{message}") do |template|
|
|
105
|
+
options.format = template
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
parser.on("--fail-level LEVEL", SEVERITIES, "Exit non-zero at this severity or worse (default: warning)") do |level|
|
|
109
|
+
options.fail_level = level
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
parser.on("--show-rules", "List the rules and whether they are on") { yield :show_rules }
|
|
113
|
+
parser.on("--show-config", "Print the configuration in effect") { yield :show_config }
|
|
114
|
+
parser.on("-v", "--version", "Print the version") { yield :version }
|
|
115
|
+
|
|
116
|
+
parser.on("-h", "--help", "Print this message") do
|
|
117
|
+
out.puts(parser.help)
|
|
118
|
+
yield :help
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def lint(options)
|
|
124
|
+
runner = Runner.new(resolve_config(options))
|
|
125
|
+
offenses = runner.run(options.paths)
|
|
126
|
+
|
|
127
|
+
out.print(Formatter.new(template: options.format).render(offenses))
|
|
128
|
+
runner.warnings.each { |warning| err.puts("remlint: #{warning}") }
|
|
129
|
+
|
|
130
|
+
exit_code(offenses, options.fail_level)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def resolve_config(options)
|
|
134
|
+
base = base_config(options)
|
|
135
|
+
|
|
136
|
+
if options.only
|
|
137
|
+
base.only(options.only)
|
|
138
|
+
else
|
|
139
|
+
base
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def base_config(options)
|
|
144
|
+
if options.config_path
|
|
145
|
+
Config.load_file(options.config_path)
|
|
146
|
+
else
|
|
147
|
+
Config.discover
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Offences below the threshold are reported and do not fail the run, so a
|
|
152
|
+
# project can adopt the informational rules without turning its build red.
|
|
153
|
+
def exit_code(offenses, fail_level)
|
|
154
|
+
if offenses.any? { |offense| offense.at_least?(fail_level) }
|
|
155
|
+
EXIT_OFFENSES
|
|
156
|
+
else
|
|
157
|
+
EXIT_SUCCESS
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def print_version
|
|
162
|
+
out.puts("remlint #{VERSION}")
|
|
163
|
+
EXIT_SUCCESS
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def print_rules
|
|
167
|
+
config = Config.discover
|
|
168
|
+
|
|
169
|
+
Rule.all.sort_by(&:rule_name).each do |rule|
|
|
170
|
+
if config.enabled?(rule)
|
|
171
|
+
state = "on "
|
|
172
|
+
else
|
|
173
|
+
state = "off"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
out.puts("#{state} #{rule.rule_name.ljust(26)} #{rule.description}")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
EXIT_SUCCESS
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def print_config(options)
|
|
183
|
+
out.puts(YAML.dump(resolve_config(options).settings))
|
|
184
|
+
EXIT_SUCCESS
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
__END__
|
|
190
|
+
|
|
191
|
+
require "stringio"
|
|
192
|
+
require "tempfile"
|
|
193
|
+
|
|
194
|
+
describe "RemLint::CLI" do
|
|
195
|
+
run = proc do |argv|
|
|
196
|
+
out = StringIO.new
|
|
197
|
+
err = StringIO.new
|
|
198
|
+
status = RemLint::CLI.new(out: out, err: err).call(argv)
|
|
199
|
+
|
|
200
|
+
{ status: status, out: out.string, err: err.string }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The Tempfile objects are kept, not just their paths: an unreferenced
|
|
204
|
+
# Tempfile is garbage, and its finalizer unlinks the file. In a short run
|
|
205
|
+
# nothing collects and the specs pass; in a long one the file vanishes
|
|
206
|
+
# between being written and being linted.
|
|
207
|
+
fixtures = []
|
|
208
|
+
|
|
209
|
+
fixture = proc do |text|
|
|
210
|
+
file = Tempfile.new(["remlint-cli", ".rem"])
|
|
211
|
+
file.write(text)
|
|
212
|
+
file.close
|
|
213
|
+
fixtures << file
|
|
214
|
+
file.path
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe "linting" do
|
|
218
|
+
it "prints nothing and exits zero for a clean file" do
|
|
219
|
+
result = run.([fixture.("MSG hi\n")])
|
|
220
|
+
|
|
221
|
+
result[:status].should == 0
|
|
222
|
+
result[:out].should == ""
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "prints each offence and exits non-zero" do
|
|
226
|
+
result = run.(["--only", "TrailingWhitespace", fixture.("MSG hi \n")])
|
|
227
|
+
|
|
228
|
+
result[:status].should == 1
|
|
229
|
+
result[:out].should.match(/: error: \[TrailingWhitespace\] Trailing whitespace/)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "runs only the rules --only names" do
|
|
233
|
+
result = run.(["--only", "LineLength", fixture.("MSG hi \n")])
|
|
234
|
+
|
|
235
|
+
result[:status].should == 0
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "honours a custom output format" do
|
|
239
|
+
result = run.(["--only", "TrailingWhitespace", "--format", "%{line}|%{rule}", fixture.("MSG hi \n")])
|
|
240
|
+
|
|
241
|
+
result[:out].should.match(/\A1\|TrailingWhitespace\n/)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
describe "--fail-level" do
|
|
246
|
+
it "fails on an offence at the threshold" do
|
|
247
|
+
result = run.(["--only", "TrailingWhitespace", "--fail-level", "error", fixture.("MSG hi \n")])
|
|
248
|
+
|
|
249
|
+
result[:status].should == 1
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "reports an offence below the threshold without failing on it" do
|
|
253
|
+
path = fixture.("#{'x' * 200}\n")
|
|
254
|
+
result = run.(["--only", "LineLength", "--fail-level", "error", path])
|
|
255
|
+
|
|
256
|
+
result[:status].should == 0
|
|
257
|
+
result[:out].should.match(/\[LineLength\]/)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
describe "informational flags" do
|
|
262
|
+
it "prints the version" do
|
|
263
|
+
result = run.(["--version"])
|
|
264
|
+
|
|
265
|
+
result[:status].should == 0
|
|
266
|
+
result[:out].should == "remlint #{RemLint::VERSION}\n"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "lists the rules and whether each is on" do
|
|
270
|
+
result = run.(["--show-rules"])
|
|
271
|
+
|
|
272
|
+
result[:status].should == 0
|
|
273
|
+
result[:out].should.match(/^on TrailingWhitespace/)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "prints the configuration in effect" do
|
|
277
|
+
run.(["--show-config"])[:out].should.match(/TrailingWhitespace/)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "prints usage for --help and exits zero" do
|
|
281
|
+
result = run.(["--help"])
|
|
282
|
+
|
|
283
|
+
result[:status].should == 0
|
|
284
|
+
result[:out].should.match(/Usage: remlint/)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "bad usage" do
|
|
289
|
+
it "reports an unknown flag on stderr and exits two" do
|
|
290
|
+
result = run.(["--nonsense"])
|
|
291
|
+
|
|
292
|
+
result[:status].should == 2
|
|
293
|
+
result[:err].should.match(/invalid option/)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it "reports an unknown --fail-level" do
|
|
297
|
+
run.(["--fail-level", "catastrophic"])[:status].should == 2
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "logical_line"
|
|
4
|
+
require_relative "vocabulary"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
# A classified logical line: which keyword opens it, and what follows.
|
|
8
|
+
#
|
|
9
|
+
# `keyword` is nil for three different things, and rules must not confuse
|
|
10
|
+
# them, so ask rather than test for nil:
|
|
11
|
+
#
|
|
12
|
+
# blank? nothing but whitespace
|
|
13
|
+
# comment? opens with `#` or `;`
|
|
14
|
+
# implicit? a trigger with the `REM` left off -- `1 Nov ++12 MSG ...`,
|
|
15
|
+
# which the manual explicitly allows and which is the ordinary
|
|
16
|
+
# way to write a reminder
|
|
17
|
+
Command = Struct.new(
|
|
18
|
+
:keyword,
|
|
19
|
+
:word,
|
|
20
|
+
:args,
|
|
21
|
+
:logical_line,
|
|
22
|
+
:kind,
|
|
23
|
+
keyword_init: true,
|
|
24
|
+
) do
|
|
25
|
+
# Where `args` begins in `text`. `args` is always a suffix of `text`, so a
|
|
26
|
+
# rule that found something at offset N in `args` can hand
|
|
27
|
+
# `args_offset + N` to LogicalLine#position_at and get a real file position.
|
|
28
|
+
def args_offset
|
|
29
|
+
text.length - args.length
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def line
|
|
33
|
+
logical_line.line
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def last_line
|
|
37
|
+
logical_line.last_line
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def text
|
|
41
|
+
logical_line.text
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blank?
|
|
45
|
+
kind == :blank
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def comment?
|
|
49
|
+
kind == :comment
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A command line that opens with no recognisable keyword. Remind reads it
|
|
53
|
+
# as a bare trigger with an implicit `REM`.
|
|
54
|
+
def implicit?
|
|
55
|
+
kind == :implicit
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Blank lines and comments are not commands at all.
|
|
59
|
+
def code?
|
|
60
|
+
kind == :keyword || kind == :implicit
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def keyword?(*names)
|
|
64
|
+
!keyword.nil? && names.map(&:upcase).include?(keyword.name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The column, 1-based, where the keyword itself starts.
|
|
68
|
+
def keyword_column
|
|
69
|
+
(text.index(/\S/) || 0) + 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Splits a logical line into its opening keyword and the rest.
|
|
74
|
+
#
|
|
75
|
+
# Deliberately shallow: Remind's grammar is loose enough that a full parse
|
|
76
|
+
# would be a second implementation of Remind, and wrong in different places
|
|
77
|
+
# than the first. Rules that need to look inside `args` reach for
|
|
78
|
+
# {ExprLexer}; rules that only care which command this is stop here.
|
|
79
|
+
module Classifier
|
|
80
|
+
COMMENT = /\A\s*[#;]/
|
|
81
|
+
BLANK = /\A\s*\z/m
|
|
82
|
+
WORD = /\A\s*(\S+)/
|
|
83
|
+
|
|
84
|
+
module_function
|
|
85
|
+
|
|
86
|
+
def call(logical_line)
|
|
87
|
+
text = logical_line.text
|
|
88
|
+
|
|
89
|
+
if text.match?(BLANK)
|
|
90
|
+
bare(logical_line, :blank)
|
|
91
|
+
elsif text.match?(COMMENT)
|
|
92
|
+
bare(logical_line, :comment)
|
|
93
|
+
else
|
|
94
|
+
classify_code(logical_line, text)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def all(logical_lines)
|
|
99
|
+
logical_lines.map { |logical_line| call(logical_line) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A line opening with a clause keyword -- a month, a weekday, an ordinal --
|
|
103
|
+
# is a trigger with the `REM` left off, not a command named `JANUARY`. It
|
|
104
|
+
# gets `kind: :implicit` like a line opening with a bare day number, and
|
|
105
|
+
# keeps its whole text as `args`, because the clause is part of the trigger
|
|
106
|
+
# rather than something the command takes.
|
|
107
|
+
def classify_code(logical_line, text)
|
|
108
|
+
match = text.match(WORD)
|
|
109
|
+
word = match[1]
|
|
110
|
+
keyword = Vocabulary.keyword(word)
|
|
111
|
+
commands = !keyword.nil? && !keyword.clause?
|
|
112
|
+
|
|
113
|
+
Command.new(
|
|
114
|
+
keyword: keyword,
|
|
115
|
+
word: word,
|
|
116
|
+
args: commands ? match.post_match.sub(/\A[ \t]+/, "") : text.sub(/\A\s*/, ""),
|
|
117
|
+
logical_line: logical_line,
|
|
118
|
+
kind: commands ? :keyword : :implicit,
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bare(logical_line, kind)
|
|
123
|
+
Command.new(
|
|
124
|
+
keyword: nil,
|
|
125
|
+
word: nil,
|
|
126
|
+
args: logical_line.text,
|
|
127
|
+
logical_line: logical_line,
|
|
128
|
+
kind: kind,
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
__END__
|
|
135
|
+
|
|
136
|
+
describe "RemLint::Classifier" do
|
|
137
|
+
classify = proc do |text|
|
|
138
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
139
|
+
|
|
140
|
+
RemLint::Classifier.all(RemLint::Joiner.call(source))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
one = proc { |text| classify.(text).first }
|
|
144
|
+
|
|
145
|
+
it "recognises a keyword and splits off its arguments" do
|
|
146
|
+
command = one.("REM 1 Jan MSG New Year")
|
|
147
|
+
|
|
148
|
+
command.keyword.name.should == "REM"
|
|
149
|
+
command.args.should == "1 Jan MSG New Year"
|
|
150
|
+
command.should.be.code
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "recognises a keyword whatever its case" do
|
|
154
|
+
one.("msg hello").keyword.name.should == "MSG"
|
|
155
|
+
one.("MsG hello").keyword.name.should == "MSG"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "recognises an abbreviated keyword" do
|
|
159
|
+
one.("INC defs.rem").keyword.name.should == "INCLUDE"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "treats a bare trigger as an implicit REM" do
|
|
163
|
+
command = one.("1 Nov ++12 MSG Get ready")
|
|
164
|
+
|
|
165
|
+
command.keyword.should.be.nil
|
|
166
|
+
command.should.be.implicit
|
|
167
|
+
command.should.be.code
|
|
168
|
+
command.args.should == "1 Nov ++12 MSG Get ready"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "treats a trigger opening with a clause keyword as an implicit REM too" do
|
|
172
|
+
command = one.("Nov 1 MSG All Saints")
|
|
173
|
+
|
|
174
|
+
command.should.be.implicit
|
|
175
|
+
command.keyword.name.should == "NOVEMBER"
|
|
176
|
+
command.args.should == "Nov 1 MSG All Saints"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "marks comments, in both of Remind's comment characters" do
|
|
180
|
+
one.("# a note").should.be.comment
|
|
181
|
+
one.("; also a note").should.be.comment
|
|
182
|
+
one.(" # indented").should.be.comment
|
|
183
|
+
one.("# a note").code?.should.be.false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "marks blank lines" do
|
|
187
|
+
one.("\n").should.be.blank
|
|
188
|
+
one.(" \t \n").should.be.blank
|
|
189
|
+
one.("\n").code?.should.be.false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "can map an offset in args back to a file position" do
|
|
193
|
+
command = one.(" SET x 12345")
|
|
194
|
+
|
|
195
|
+
command.args.should == "x 12345"
|
|
196
|
+
command.logical_line.position_at(command.args_offset).should == [1, 8]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "does not mistake a trailing # for a comment" do
|
|
200
|
+
one.("MSG see issue #42").should.be.code
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "keeps the logical line, so continued commands classify once" do
|
|
204
|
+
commands = classify.("IF a > 1 && \\\n b < 2\nENDIF\n")
|
|
205
|
+
|
|
206
|
+
commands.length.should == 2
|
|
207
|
+
commands.first.keyword.name.should == "IF"
|
|
208
|
+
commands.first.line.should == 1
|
|
209
|
+
commands.first.last_line.should == 2
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "answers keyword? by canonical name, whatever was written" do
|
|
213
|
+
one.("inc defs.rem").keyword?("INCLUDE").should.be.true
|
|
214
|
+
one.("inc defs.rem").keyword?("MSG", "INCLUDE").should.be.true
|
|
215
|
+
one.("inc defs.rem").keyword?("MSG").should.be.false
|
|
216
|
+
one.("# note").keyword?("MSG").should.be.false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "reports the column the keyword starts in" do
|
|
220
|
+
one.("MSG hi").keyword_column.should == 1
|
|
221
|
+
one.(" MSG hi").keyword_column.should == 5
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "classifies a whole file in order" do
|
|
225
|
+
commands = classify.("# header\n\nSET a 1\n1 Jan MSG hi\n")
|
|
226
|
+
|
|
227
|
+
commands.map(&:kind).should == %i[comment blank keyword implicit]
|
|
228
|
+
end
|
|
229
|
+
end
|