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,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `easterdate(today())` in a trigger that adds an offset.
|
|
8
|
+
#
|
|
9
|
+
# `easterdate(date)` returns the *next* Easter on or after that date. So on
|
|
10
|
+
# Easter Sunday it returns today, and on Easter Monday it returns Easter of
|
|
11
|
+
# the following year. A reminder written
|
|
12
|
+
#
|
|
13
|
+
# REM [easterdate(today())+1] MSG Easter Monday
|
|
14
|
+
#
|
|
15
|
+
# therefore never triggers: on Easter Monday the expression has already
|
|
16
|
+
# jumped a year ahead, and the day it names is 364 days away.
|
|
17
|
+
#
|
|
18
|
+
# `easterdate(year)` -- the integer form -- returns Easter of that year and
|
|
19
|
+
# does not move, which is why the fix is `easterdate(year(today()))`.
|
|
20
|
+
#
|
|
21
|
+
# The same reasoning applies to `orthodoxeaster`, which has the same two
|
|
22
|
+
# forms.
|
|
23
|
+
class EasterdateFromToday < Rule
|
|
24
|
+
FUNCTIONS = %w[easterdate orthodoxeaster].freeze
|
|
25
|
+
|
|
26
|
+
TODAY = %w[today now realtoday].freeze
|
|
27
|
+
|
|
28
|
+
def self.default_severity
|
|
29
|
+
"warning"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.description
|
|
33
|
+
"easterdate(today()) in a trigger with an offset, which skips a year on the day."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check
|
|
37
|
+
document.code_commands.each do |command|
|
|
38
|
+
if offset_trigger?(command)
|
|
39
|
+
check_calls(command)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# An offset only matters inside the trigger, and only when there is one
|
|
47
|
+
# to matter: `REM [easterdate(today())] MSG Easter` is correct.
|
|
48
|
+
def offset_trigger?(command)
|
|
49
|
+
trigger = document.trigger_for(command)
|
|
50
|
+
limit = trigger.body_offset || command.text.length
|
|
51
|
+
tokens = document.tokens_for(command.logical_line)
|
|
52
|
+
|
|
53
|
+
trigger.triggered? && tokens.any? { |token| offset?(token, limit) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def offset?(token, limit)
|
|
57
|
+
token.offset < limit && token.type == :other && ["+", "-"].include?(token.value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check_calls(command)
|
|
61
|
+
tokens = document.tokens_for(command.logical_line)
|
|
62
|
+
|
|
63
|
+
tokens.each_index do |index|
|
|
64
|
+
if easter_of_today?(tokens, index)
|
|
65
|
+
offend_at(command.logical_line, tokens[index].offset, message(tokens[index]))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# `easterdate ( today ( ) )` -- the date form, fed the moving date.
|
|
71
|
+
def easter_of_today?(tokens, index)
|
|
72
|
+
window = tokens[index, 4]
|
|
73
|
+
|
|
74
|
+
window&.length == 4 &&
|
|
75
|
+
window[0].type == :function && FUNCTIONS.include?(window[0].value.downcase) &&
|
|
76
|
+
window[1].type == :lparen &&
|
|
77
|
+
window[2].type == :function && TODAY.include?(window[2].value.downcase) &&
|
|
78
|
+
window[3].type == :lparen
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def message(token)
|
|
82
|
+
"`#{token.value}(today())` returns *next* Easter, so on the day itself it " \
|
|
83
|
+
"jumps a year ahead and the offset never triggers; pass the year instead, " \
|
|
84
|
+
"as `#{token.value}(year(today()))`"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
__END__
|
|
91
|
+
|
|
92
|
+
require_relative "../document"
|
|
93
|
+
|
|
94
|
+
describe "RemLint::Rules::EasterdateFromToday" do
|
|
95
|
+
lint = proc do |text|
|
|
96
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
97
|
+
|
|
98
|
+
RemLint::Rules::EasterdateFromToday.new.run(RemLint::Document.new(source))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
102
|
+
|
|
103
|
+
it "reports the date form with an offset" do
|
|
104
|
+
messages.("REM [easterdate(today())+1] MSG Easter Monday\n").first.should.match(
|
|
105
|
+
/jumps a year ahead and the offset never triggers/,
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "suggests the year form" do
|
|
110
|
+
messages.("REM [easterdate(today())+1] MSG hi\n").first.should.match(
|
|
111
|
+
/as `easterdate\(year\(today\(\)\)\)`/,
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "reports a negative offset too" do
|
|
116
|
+
lint.("REM [easterdate(today())-2] MSG Good Friday\n").length.should == 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "reports orthodoxeaster the same way" do
|
|
120
|
+
messages.("REM [orthodoxeaster(today())+1] MSG hi\n").first.should.match(/orthodoxeaster/)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "accepts the date form with no offset" do
|
|
124
|
+
lint.("REM [easterdate(today())] MSG Easter Sunday\n").should.be.empty
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "accepts the year form, which does not move" do
|
|
128
|
+
lint.("REM [easterdate(year(today()))+1] MSG Easter Monday\n").should.be.empty
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "accepts an offset with no easterdate at all" do
|
|
132
|
+
lint.("REM [trigger(today())+1] MSG hi\n").should.be.empty
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "says nothing about an offset in the body" do
|
|
136
|
+
lint.("REM [easterdate(today())] MSG Easter, +1 day to go\n").should.be.empty
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "says nothing about a SET, which has no trigger" do
|
|
140
|
+
lint.("SET a easterdate(today())+1\n").should.be.empty
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "matches the function name case-insensitively" do
|
|
144
|
+
lint.("REM [EASTERDATE(TODAY())+1] MSG hi\n").length.should == 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "says nothing about comments" do
|
|
148
|
+
lint.("# REM [easterdate(today())+1] MSG hi\n").should.be.empty
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "points at the call" do
|
|
152
|
+
text = "REM [easterdate(today())+1] MSG hi\n"
|
|
153
|
+
|
|
154
|
+
lint.(text).first.column.should == text.index("easterdate") + 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "reports at warning severity" do
|
|
158
|
+
lint.("REM [easterdate(today())+1] MSG hi\n").first.severity.should == "warning"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../vocabulary"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# Calls that pass the wrong number of arguments.
|
|
9
|
+
#
|
|
10
|
+
# The arities come from Remind's own function table (src/funcs.c), so this
|
|
11
|
+
# is not a guess about what `ampm` accepts -- it is what `ampm` accepts.
|
|
12
|
+
# `defs.rem` defines fifteen-odd helpers and calls them throughout, and
|
|
13
|
+
# `astro` threads `sunrise($T+1)` through four separate heredocs; getting an
|
|
14
|
+
# argument count wrong there fails at trigger time, on the one day of the
|
|
15
|
+
# year the reminder fires.
|
|
16
|
+
#
|
|
17
|
+
# Two deliberate silences:
|
|
18
|
+
#
|
|
19
|
+
# UNKNOWN FUNCTIONS ARE NOT REPORTED. A reminder file's helpers usually
|
|
20
|
+
# arrive through `INCLUDE`, and this rule reads one file. Complaining about
|
|
21
|
+
# every call into `$SysInclude` would drown the calls it can actually check.
|
|
22
|
+
#
|
|
23
|
+
# A FUNCTION DEFINED LATER IN THE FILE STILL COUNTS. Definitions are
|
|
24
|
+
# collected in a first pass, because `FSET` at the bottom of a file is
|
|
25
|
+
# legal and common. A call is checked against the definition in force where
|
|
26
|
+
# it sits -- the nearest one above it -- and only falls back to a later
|
|
27
|
+
# definition when there is nothing above. Remind's own `tests/test.rem`
|
|
28
|
+
# defines `g(x, y)` on line 356 and redefines it as `g(x)` on line 1545;
|
|
29
|
+
# taking the last definition file-wide would report all three of the
|
|
30
|
+
# perfectly correct two-argument calls in between.
|
|
31
|
+
class FunctionArity < Rule
|
|
32
|
+
# `FSET name(a, b)` -- the parameter list ends at the first `)`, since
|
|
33
|
+
# Remind's parameters are plain names.
|
|
34
|
+
DEFINITION = /\A\s*(?<name>[A-Za-z_]\w*)\s*\((?<params>[^)]*)\)/
|
|
35
|
+
|
|
36
|
+
def self.default_severity
|
|
37
|
+
"error"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.description
|
|
41
|
+
"Calls that pass more or fewer arguments than the function takes."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check
|
|
45
|
+
@definitions = collect_definitions
|
|
46
|
+
|
|
47
|
+
document.logical_lines.each_with_index do |logical_line, index|
|
|
48
|
+
command = document.commands[index]
|
|
49
|
+
|
|
50
|
+
if command.code?
|
|
51
|
+
check_calls(logical_line, command)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Every `FSET` in the file, in order: [name, line, parameter count].
|
|
59
|
+
# Kept as a list rather than a hash because a file may define the same
|
|
60
|
+
# name twice and which one applies depends on where you are.
|
|
61
|
+
def collect_definitions
|
|
62
|
+
document.code_commands.filter_map do |command|
|
|
63
|
+
match = command.keyword?("FSET") && command.args.match(DEFINITION)
|
|
64
|
+
|
|
65
|
+
if match
|
|
66
|
+
[match[:name].downcase, command.line, count_parameters(match[:params])]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The definition in force at `line`: the nearest one at or above it,
|
|
72
|
+
# or -- for a helper defined at the bottom of the file -- the first one
|
|
73
|
+
# below.
|
|
74
|
+
def definition_at(name, line)
|
|
75
|
+
candidates = @definitions.select { |defined_name, _line, _arity| defined_name == name }
|
|
76
|
+
above = candidates.select { |_name, defined_line, _arity| defined_line <= line }.last
|
|
77
|
+
chosen = above || candidates.first
|
|
78
|
+
|
|
79
|
+
chosen&.last
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def count_parameters(params)
|
|
83
|
+
if params.strip.empty?
|
|
84
|
+
0
|
|
85
|
+
else
|
|
86
|
+
params.split(",").length
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_calls(logical_line, command)
|
|
91
|
+
tokens = document.tokens_for(logical_line)
|
|
92
|
+
from = body_offset(command)
|
|
93
|
+
|
|
94
|
+
tokens.each_with_index do |token, index|
|
|
95
|
+
if token.type == :function && token.offset >= from
|
|
96
|
+
check_call(logical_line, tokens, index)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `FSET greet(a, b) ...` opens with something that lexes exactly like a
|
|
102
|
+
# call to `greet` -- it is the definition's parameter list. Skipping
|
|
103
|
+
# past it stops every definition reporting itself, which is what makes
|
|
104
|
+
# a redefinition (`FSET f(a)` then `FSET f(a, b)`) legal rather than an
|
|
105
|
+
# offence against its own later self.
|
|
106
|
+
def body_offset(command)
|
|
107
|
+
match = command.keyword?("FSET") && command.args.match(DEFINITION)
|
|
108
|
+
|
|
109
|
+
if match
|
|
110
|
+
command.args_offset + match.end(0)
|
|
111
|
+
else
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def check_call(logical_line, tokens, index)
|
|
117
|
+
token = tokens[index]
|
|
118
|
+
line, _column = logical_line.position_at(token.offset)
|
|
119
|
+
expected = arity_of(token.value, line)
|
|
120
|
+
actual = count_arguments(tokens, index + 1)
|
|
121
|
+
|
|
122
|
+
if !expected.nil? && !actual.nil? && !acceptable?(expected, actual)
|
|
123
|
+
offend_at(logical_line, token.offset, arity_message(token.value, expected, actual))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A builtin's arity comes from Remind's table; a local `FSET` fixes an
|
|
128
|
+
# exact count. Anything else is out of scope and returns nil.
|
|
129
|
+
def arity_of(name, line)
|
|
130
|
+
builtin = Vocabulary.function(name)
|
|
131
|
+
|
|
132
|
+
if builtin
|
|
133
|
+
builtin
|
|
134
|
+
else
|
|
135
|
+
definition_at(name.downcase, line)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def acceptable?(expected, actual)
|
|
140
|
+
if expected.is_a?(Integer)
|
|
141
|
+
expected == actual
|
|
142
|
+
else
|
|
143
|
+
expected.accepts?(actual)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def arity_message(name, expected, actual)
|
|
148
|
+
if expected.is_a?(Integer)
|
|
149
|
+
wanted = expected.to_s
|
|
150
|
+
else
|
|
151
|
+
wanted = expected.arity_description
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
"`#{name}` takes #{wanted} #{pluralise(wanted)}, given #{actual}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# "1" and "at least 1" are singular; "3" and "1 to 4" are not. The
|
|
158
|
+
# number the phrase ends on is the one being counted.
|
|
159
|
+
def pluralise(wanted)
|
|
160
|
+
if wanted[/\d+\z/] == "1"
|
|
161
|
+
"argument"
|
|
162
|
+
else
|
|
163
|
+
"arguments"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Count top-level commas between the call's parentheses. Returns nil if
|
|
168
|
+
# the call is not parenthesised or never closes -- an unclosed call is
|
|
169
|
+
# UnbalancedDelimiters' offence, and guessing an argument count from a
|
|
170
|
+
# broken call would report a second, invented one.
|
|
171
|
+
def count_arguments(tokens, open_index)
|
|
172
|
+
open = tokens[open_index]
|
|
173
|
+
|
|
174
|
+
if open.nil? || open.type != :lparen
|
|
175
|
+
nil
|
|
176
|
+
else
|
|
177
|
+
walk_arguments(tokens, open_index)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def walk_arguments(tokens, open_index)
|
|
182
|
+
depth = 0
|
|
183
|
+
commas = 0
|
|
184
|
+
empty = true
|
|
185
|
+
closed = false
|
|
186
|
+
cursor = open_index
|
|
187
|
+
|
|
188
|
+
while cursor < tokens.length && !closed
|
|
189
|
+
token = tokens[cursor]
|
|
190
|
+
depth, commas, empty, closed = step(
|
|
191
|
+
token,
|
|
192
|
+
depth,
|
|
193
|
+
commas,
|
|
194
|
+
empty,
|
|
195
|
+
closed,
|
|
196
|
+
)
|
|
197
|
+
cursor += 1
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
arguments(closed, commas, empty)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def step(token, depth, commas, empty, closed)
|
|
204
|
+
case token.type
|
|
205
|
+
when :lparen, :lbracket
|
|
206
|
+
[depth + 1, commas, empty, closed]
|
|
207
|
+
when :rparen, :rbracket
|
|
208
|
+
close_or_descend(
|
|
209
|
+
token,
|
|
210
|
+
depth,
|
|
211
|
+
commas,
|
|
212
|
+
empty,
|
|
213
|
+
)
|
|
214
|
+
when :comma
|
|
215
|
+
[depth, depth == 1 ? commas + 1 : commas, empty, closed]
|
|
216
|
+
else
|
|
217
|
+
[depth, commas, depth == 1 ? false : empty, closed]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def close_or_descend(_token, depth, commas, empty)
|
|
222
|
+
if depth == 1
|
|
223
|
+
[depth - 1, commas, empty, true]
|
|
224
|
+
else
|
|
225
|
+
[depth - 1, commas, empty, false]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# `f()` is zero arguments; `f(a, b)` is commas plus one. A call that
|
|
230
|
+
# never closed yields nil so nothing is reported for it.
|
|
231
|
+
def arguments(closed, commas, empty)
|
|
232
|
+
if !closed
|
|
233
|
+
nil
|
|
234
|
+
elsif empty && commas.zero?
|
|
235
|
+
0
|
|
236
|
+
else
|
|
237
|
+
commas + 1
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
__END__
|
|
245
|
+
|
|
246
|
+
require_relative "../document"
|
|
247
|
+
|
|
248
|
+
describe "RemLint::Rules::FunctionArity" do
|
|
249
|
+
lint = proc do |text|
|
|
250
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
251
|
+
|
|
252
|
+
RemLint::Rules::FunctionArity.new.run(RemLint::Document.new(source))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
256
|
+
|
|
257
|
+
describe "builtin functions" do
|
|
258
|
+
it "accepts a call with the right number of arguments" do
|
|
259
|
+
lint.("MSG [abs(-1)]\n").should.be.empty
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "reports too many arguments" do
|
|
263
|
+
messages.("MSG [abs(1, 2)]\n").should == ["`abs` takes 1 argument, given 2"]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "reports too few arguments" do
|
|
267
|
+
messages.("MSG [date(2026, 1)]\n").should == ["`date` takes 3 arguments, given 2"]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "accepts anything inside an optional range" do
|
|
271
|
+
lint.("MSG [ampm(1)] [ampm(1,2)] [ampm(1,2,3,4)]\n").should.be.empty
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "reports past the top of an optional range" do
|
|
275
|
+
messages.("MSG [ampm(1,2,3,4,5)]\n").should == ["`ampm` takes 1 to 4 arguments, given 5"]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "accepts any count for a variadic function" do
|
|
279
|
+
lint.("MSG [char(65, 66, 67, 68)]\n").should.be.empty
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "reports below a variadic function's minimum" do
|
|
283
|
+
messages.("MSG [char()]\n").should == ["`char` takes at least 1 argument, given 0"]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it "accepts a zero-argument call" do
|
|
287
|
+
lint.("MSG [version()] [today()]\n").should.be.empty
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "reports arguments given to a function that takes none" do
|
|
291
|
+
messages.("MSG [version(1)]\n").should == ["`version` takes 0 arguments, given 1"]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "matches the function name case-insensitively, as Remind does" do
|
|
295
|
+
messages.("MSG [ABS(1, 2)]\n").should == ["`ABS` takes 1 argument, given 2"]
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
describe "nested calls" do
|
|
300
|
+
it "counts arguments at the right depth" do
|
|
301
|
+
lint.("MSG [max(abs(-1), abs(-2))]\n").should.be.empty
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it "reports the inner call, not the outer one" do
|
|
305
|
+
messages.("MSG [max(abs(-1, 9), 2)]\n").should == ["`abs` takes 1 argument, given 2"]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "does not count a comma inside a nested call as its own argument" do
|
|
309
|
+
lint.("MSG [ansicolor(0, 255, 0) + center(\"x\")]\n").should.be.empty
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it "does not count a comma inside a string as an argument separator" do
|
|
313
|
+
lint.(%(MSG [abs("a, b, c")]\n)).should.be.empty
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
describe "functions the file defines itself" do
|
|
318
|
+
it "checks a call against its FSET" do
|
|
319
|
+
messages.("FSET greet(a, b) a + b\nMSG [greet(1)]\n").should ==
|
|
320
|
+
["`greet` takes 2 arguments, given 1"]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it "accepts a correct call" do
|
|
324
|
+
lint.("FSET greet(a, b) a + b\nMSG [greet(1, 2)]\n").should.be.empty
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it "accepts a definition that appears after the call" do
|
|
328
|
+
lint.("MSG [greet(1, 2)]\nFSET greet(a, b) a + b\n").should.be.empty
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it "handles a nullary definition" do
|
|
332
|
+
messages.("FSET now() today()\nMSG [now(1)]\n").should == ["`now` takes 0 arguments, given 1"]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "does not treat the definition's own parameter list as a call" do
|
|
336
|
+
lint.("FSET center(x) pad(\"\", \" \", (columns() - columns(x))/2) + x\n").should.be.empty
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "checks a call against the definition in force above it" do
|
|
340
|
+
# Remind's own tests/test.rem does exactly this: g(x, y) high up, g(x)
|
|
341
|
+
# much further down, with correct two-argument calls in between.
|
|
342
|
+
text = "FSET g(a, b) a + b\nMSG [g(1, 2)]\nFSET g(a) a\nMSG [g(1)]\n"
|
|
343
|
+
|
|
344
|
+
lint.(text).should.be.empty
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "reports a call that matches only the other definition" do
|
|
348
|
+
text = "FSET g(a, b) a + b\nMSG [g(1)]\nFSET g(a) a\n"
|
|
349
|
+
|
|
350
|
+
messages.(text).should == ["`g` takes 2 arguments, given 1"]
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
describe "what it stays quiet about" do
|
|
355
|
+
it "says nothing about a function it has never heard of" do
|
|
356
|
+
# Helpers usually arrive through INCLUDE, which this rule cannot see.
|
|
357
|
+
lint.("MSG [helper_from_include(1, 2, 3)]\n").should.be.empty
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it "says nothing about a bare name that is not a call" do
|
|
361
|
+
lint.("MSG [abs]\n").should.be.empty
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
it "says nothing about a call that never closes" do
|
|
365
|
+
# That is UnbalancedDelimiters' offence; counting arguments in a broken
|
|
366
|
+
# call would invent a second one.
|
|
367
|
+
lint.("MSG [abs(1, 2\n").should.be.empty
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "says nothing about comments" do
|
|
371
|
+
lint.("# call abs(1, 2) somewhere\n").should.be.empty
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it "reports the physical line a call sits on inside a continuation" do
|
|
376
|
+
offense = lint.("SET x 1 + \\\n abs(1, 2)\n").first
|
|
377
|
+
|
|
378
|
+
offense.line.should == 2
|
|
379
|
+
offense.column.should == 5
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it "reports at error severity" do
|
|
383
|
+
lint.("MSG [abs(1, 2)]\n").first.severity.should == "error"
|
|
384
|
+
end
|
|
385
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
require_relative "../vocabulary"
|
|
5
|
+
|
|
6
|
+
module RemLint
|
|
7
|
+
module Rules
|
|
8
|
+
# A function defined twice without saying so.
|
|
9
|
+
#
|
|
10
|
+
# Remind warns about a redefinition itself, and gives you a way to say the
|
|
11
|
+
# redefinition is deliberate: `FSET - name(...)` sets
|
|
12
|
+
# `suppress_redefined_function_warning` and the warning goes away
|
|
13
|
+
# (src/userfns.c). So this rule is not telling you something Remind would
|
|
14
|
+
# not -- it is telling you where the `-` goes.
|
|
15
|
+
#
|
|
16
|
+
# Worth having anyway, because the two definitions are usually a long way
|
|
17
|
+
# apart. Remind's own `tests/test.rem` defines `g(x, y)` on line 356 and
|
|
18
|
+
# redefines it as `g(x)` on line 1545, and every call in between is
|
|
19
|
+
# checked against a different function than the one the reader is looking
|
|
20
|
+
# at.
|
|
21
|
+
#
|
|
22
|
+
# A definition inside a `PUSH-FUNCS` block is left alone: pushing the
|
|
23
|
+
# function table is exactly how you say "this redefinition is scoped", and
|
|
24
|
+
# Remind skips its own warning for a pushed function too
|
|
25
|
+
# (`!existing->been_pushed`).
|
|
26
|
+
class FunctionRedefinition < Rule
|
|
27
|
+
# `FSET name(args)` and the `FSET - name(args)` that suppresses the
|
|
28
|
+
# warning.
|
|
29
|
+
DEFINITION = /\A(?<deliberate>-\s*)?(?<name>[A-Za-z_]\w*)\s*\(/
|
|
30
|
+
|
|
31
|
+
def self.default_severity
|
|
32
|
+
"warning"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.description
|
|
36
|
+
"A function redefined without the FSET - form that says so."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check
|
|
40
|
+
seen = {}
|
|
41
|
+
pushed = 0
|
|
42
|
+
|
|
43
|
+
document.code_commands.each do |command|
|
|
44
|
+
pushed = track(command, pushed)
|
|
45
|
+
|
|
46
|
+
if command.keyword?("FSET") && pushed.zero?
|
|
47
|
+
check_definition(command, seen)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def track(command, pushed)
|
|
55
|
+
if command.keyword?("PUSH-FUNCS")
|
|
56
|
+
pushed + 1
|
|
57
|
+
elsif command.keyword?("POP-FUNCS")
|
|
58
|
+
[pushed - 1, 0].max
|
|
59
|
+
else
|
|
60
|
+
pushed
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def check_definition(command, seen)
|
|
65
|
+
match = command.args.match(DEFINITION)
|
|
66
|
+
|
|
67
|
+
if match
|
|
68
|
+
record(command, match, seen)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def record(command, match, seen)
|
|
73
|
+
name = match[:name].downcase
|
|
74
|
+
first = seen[name]
|
|
75
|
+
|
|
76
|
+
if first && !match[:deliberate]
|
|
77
|
+
offend(command.line, message(match[:name], first), column: command.keyword_column)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
seen[name] ||= command.line
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def message(name, first)
|
|
84
|
+
"`#{name}` was already defined on line #{first}; write `FSET - #{name}(...)` " \
|
|
85
|
+
"to say the redefinition is deliberate and silence Remind's warning"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
__END__
|
|
92
|
+
|
|
93
|
+
require_relative "../document"
|
|
94
|
+
|
|
95
|
+
describe "RemLint::Rules::FunctionRedefinition" do
|
|
96
|
+
lint = proc do |text|
|
|
97
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
98
|
+
|
|
99
|
+
RemLint::Rules::FunctionRedefinition.new.run(RemLint::Document.new(source))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
103
|
+
|
|
104
|
+
it "reports a second definition of the same name" do
|
|
105
|
+
messages.("FSET g(a, b) a + b\nFSET g(a) a\n").first.should ==
|
|
106
|
+
"`g` was already defined on line 1; write `FSET - g(...)` to say the redefinition " \
|
|
107
|
+
"is deliberate and silence Remind's warning"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "reports the second and not the first" do
|
|
111
|
+
lint.("FSET g(a) a\nFSET g(b) b\n").map(&:line).should == [2]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "reports each redefinition after the first" do
|
|
115
|
+
lint.("FSET g(a) a\nFSET g(b) b\nFSET g(c) c\n").map(&:line).should == [2, 3]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "accepts the FSET - form that says it is deliberate" do
|
|
119
|
+
lint.("FSET g(a) a\nFSET - g(b) b\n").should.be.empty
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "accepts the deliberate form written without a space" do
|
|
123
|
+
lint.("FSET g(a) a\nFSET -g(b) b\n").should.be.empty
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "matches the name case-insensitively, as Remind does" do
|
|
127
|
+
lint.("FSET g(a) a\nFSET G(b) b\n").length.should == 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "accepts two different functions" do
|
|
131
|
+
lint.("FSET g(a) a\nFSET h(b) b\n").should.be.empty
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "accepts a redefinition inside a PUSH-FUNCS block" do
|
|
135
|
+
# Pushing the table is how a scoped redefinition is said, and Remind
|
|
136
|
+
# skips its own warning for a pushed function too.
|
|
137
|
+
lint.("FSET g(a) a\nPUSH-FUNCS\nFSET g(b) b\nPOP-FUNCS\n").should.be.empty
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "resumes checking after the block closes" do
|
|
141
|
+
lint.("FSET g(a) a\nPUSH-FUNCS\nPOP-FUNCS\nFSET g(b) b\n").length.should == 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "points at the keyword" do
|
|
145
|
+
lint.("FSET g(a) a\n FSET g(b) b\n").first.column.should == 4
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "says nothing about comments" do
|
|
149
|
+
lint.("FSET g(a) a\n# FSET g(b) b\n").should.be.empty
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "says nothing about a call that looks like a definition" do
|
|
153
|
+
lint.("FSET g(a) a\nMSG [g(1)]\n").should.be.empty
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "reports at warning severity" do
|
|
157
|
+
lint.("FSET g(a) a\nFSET g(b) b\n").first.severity.should == "warning"
|
|
158
|
+
end
|
|
159
|
+
end
|