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,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A hand-written `TAG` in TkRemind's namespace.
|
|
8
|
+
#
|
|
9
|
+
# TkRemind finds the `REM` command it is about to edit or delete by its
|
|
10
|
+
# tag, and it names its own tags `TKTAG` followed by a number. A
|
|
11
|
+
# hand-written tag in that shape collides, and TkRemind then edits or
|
|
12
|
+
# deletes the wrong reminder.
|
|
13
|
+
#
|
|
14
|
+
# There is no error and no warning. The user clicks a reminder in the GUI,
|
|
15
|
+
# a different one changes, and nothing anywhere says why -- which makes it
|
|
16
|
+
# a data-loss bug with no diagnostic attached.
|
|
17
|
+
class TkTagNamespace < Rule
|
|
18
|
+
RESERVED = /\ATKTAG\d+\z/i
|
|
19
|
+
|
|
20
|
+
def self.default_severity
|
|
21
|
+
"warning"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.description
|
|
25
|
+
"A TAG in TkRemind's TKTAGn namespace, which makes the GUI edit the wrong reminder."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check
|
|
29
|
+
document.code_commands.each do |command|
|
|
30
|
+
document.trigger_for(command).clauses.each do |clause|
|
|
31
|
+
if clause.name == "TAG"
|
|
32
|
+
check_tag(command, clause)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def check_tag(command, clause)
|
|
41
|
+
value = tag_value(command, clause)
|
|
42
|
+
|
|
43
|
+
if value&.match?(RESERVED)
|
|
44
|
+
offend_at(
|
|
45
|
+
command.logical_line,
|
|
46
|
+
clause.offset,
|
|
47
|
+
"`#{value}` is in TkRemind's own `TKTAGn` namespace; it finds the command " \
|
|
48
|
+
"to edit by its tag, so a collision makes the GUI change a different " \
|
|
49
|
+
"reminder with no diagnostic",
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tag_value(command, clause)
|
|
55
|
+
token = document.tokens_for(command.logical_line)[clause.argument_index]
|
|
56
|
+
|
|
57
|
+
case token&.type
|
|
58
|
+
when :string then token.value[1..-2]
|
|
59
|
+
when :name then token.value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
__END__
|
|
67
|
+
|
|
68
|
+
require_relative "../document"
|
|
69
|
+
|
|
70
|
+
describe "RemLint::Rules::TkTagNamespace" do
|
|
71
|
+
lint = proc do |text|
|
|
72
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
73
|
+
|
|
74
|
+
RemLint::Rules::TkTagNamespace.new.run(RemLint::Document.new(source))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "reports a quoted tag in the reserved namespace" do
|
|
78
|
+
lint.(%(REM 1 Jan TAG "TKTAG1" MSG hi\n)).first.message.should.match(
|
|
79
|
+
/is in TkRemind's own `TKTAGn` namespace/,
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "reports an unquoted one" do
|
|
84
|
+
lint.("REM 1 Jan TAG TKTAG42 MSG hi\n").length.should == 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "matches the prefix case-insensitively" do
|
|
88
|
+
lint.("REM 1 Jan TAG tktag7 MSG hi\n").length.should == 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "accepts an ordinary tag" do
|
|
92
|
+
lint.(%(REM 1 Jan TAG "birthday" MSG hi\n)).should.be.empty
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "accepts a tag that merely starts with the prefix" do
|
|
96
|
+
# TkRemind's are the prefix plus digits and nothing else.
|
|
97
|
+
lint.(%(REM 1 Jan TAG "TKTAGGED" MSG hi\n)).should.be.empty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "accepts a tag with no digits" do
|
|
101
|
+
lint.(%(REM 1 Jan TAG "TKTAG" MSG hi\n)).should.be.empty
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "says nothing about a reminder with no TAG" do
|
|
105
|
+
lint.("REM 1 Jan MSG hi\n").should.be.empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "explains why it matters" do
|
|
109
|
+
lint.("REM 1 Jan TAG TKTAG1 MSG hi\n").first.message.should.match(
|
|
110
|
+
/change a different reminder with no diagnostic/,
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "says nothing about comments" do
|
|
115
|
+
lint.("# REM 1 Jan TAG TKTAG1 MSG hi\n").should.be.empty
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "reports at warning severity" do
|
|
119
|
+
lint.("REM 1 Jan TAG TKTAG1 MSG hi\n").first.severity.should == "warning"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# A `TODO` with no `COMPLETE-THROUGH`.
|
|
8
|
+
#
|
|
9
|
+
# `COMPLETE-THROUGH` tells Remind the date up to which the task has been
|
|
10
|
+
# done, and it is the starting point of a TODO's entire trigger
|
|
11
|
+
# calculation. Without it the algorithm starts at Remind's epoch,
|
|
12
|
+
# 1990-01-01, so the task is decades overdue.
|
|
13
|
+
#
|
|
14
|
+
# On its own that is harmless -- the TODO simply fires. It turns into a
|
|
15
|
+
# defect the moment `MAX-OVERDUE` is added, because `MAX-OVERDUE` then
|
|
16
|
+
# suppresses a task that is thirty-odd years past due, and the reminder
|
|
17
|
+
# produces *nothing at all*:
|
|
18
|
+
#
|
|
19
|
+
# REM TODO Mon MAX-OVERDUE 5 MSG x -> No reminders.
|
|
20
|
+
# REM TODO Mon COMPLETE-THROUGH 2026-01-01 MAX-OVERDUE 5 MSG x -> fires
|
|
21
|
+
#
|
|
22
|
+
# So this reports the pair, not the missing clause alone. The book
|
|
23
|
+
# described the failure as a reminder that arrives screaming; it is in fact
|
|
24
|
+
# a reminder that never arrives, which is harder to notice and was worth
|
|
25
|
+
# checking against the binary rather than taking on trust. Remind's own
|
|
26
|
+
# `tests/` has twenty-one TODOs with no `COMPLETE-THROUGH` and they are all
|
|
27
|
+
# fine, because none of them sets `MAX-OVERDUE`.
|
|
28
|
+
class TodoCompleteThrough < Rule
|
|
29
|
+
def self.default_severity
|
|
30
|
+
"warning"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.description
|
|
34
|
+
"A TODO with MAX-OVERDUE and no COMPLETE-THROUGH, which suppresses it entirely."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check
|
|
38
|
+
document.code_commands.each do |command|
|
|
39
|
+
trigger = document.trigger_for(command)
|
|
40
|
+
|
|
41
|
+
if trigger.include?("TODO") && trigger.include?("MAX-OVERDUE") &&
|
|
42
|
+
!trigger.include?("COMPLETE-THROUGH")
|
|
43
|
+
report(command, trigger)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def report(command, trigger)
|
|
51
|
+
offend_at(
|
|
52
|
+
command.logical_line,
|
|
53
|
+
trigger.find("TODO").offset,
|
|
54
|
+
"this `TODO` has no `COMPLETE-THROUGH`, so its calculation starts at " \
|
|
55
|
+
"Remind's epoch and the task is decades overdue -- and the `MAX-OVERDUE` " \
|
|
56
|
+
"on the same command then suppresses it, so the reminder never appears",
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
__END__
|
|
64
|
+
|
|
65
|
+
require_relative "../document"
|
|
66
|
+
|
|
67
|
+
describe "RemLint::Rules::TodoCompleteThrough" do
|
|
68
|
+
lint = proc do |text|
|
|
69
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
70
|
+
|
|
71
|
+
RemLint::Rules::TodoCompleteThrough.new.run(RemLint::Document.new(source))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "reports a TODO with MAX-OVERDUE and no COMPLETE-THROUGH" do
|
|
75
|
+
lint.("REM Mon TODO MAX-OVERDUE 5 MSG File\n").first.message.should.match(
|
|
76
|
+
/suppresses it, so the reminder never appears/,
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "accepts one that has COMPLETE-THROUGH" do
|
|
81
|
+
text = "REM Mon TODO COMPLETE-THROUGH 2026-01-01 MAX-OVERDUE 5 MSG File\n"
|
|
82
|
+
|
|
83
|
+
lint.(text).should.be.empty
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "accepts the clauses in any order" do
|
|
87
|
+
text = "REM Mon MAX-OVERDUE 5 COMPLETE-THROUGH 2026-01-01 TODO MSG File\n"
|
|
88
|
+
|
|
89
|
+
lint.(text).should.be.empty
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "accepts a TODO with no MAX-OVERDUE, which simply fires" do
|
|
93
|
+
# Remind's own tests/ has twenty-one of these and they all work.
|
|
94
|
+
lint.("REM 1 TODO MSG File the accounts\n").should.be.empty
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "says nothing about MAX-OVERDUE without a TODO" do
|
|
98
|
+
lint.("REM 1 MAX-OVERDUE 5 MSG File\n").should.be.empty
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "says nothing about a reminder that is not a TODO" do
|
|
102
|
+
lint.("REM 1 MSG File the accounts\n").should.be.empty
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "points at the TODO clause" do
|
|
106
|
+
text = "REM Mon TODO MAX-OVERDUE 5 MSG File\n"
|
|
107
|
+
|
|
108
|
+
lint.(text).first.column.should == text.index("TODO") + 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "does not read the word todo out of a body" do
|
|
112
|
+
lint.("REM 1 MSG add this to the todo list\n").should.be.empty
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "says nothing about comments" do
|
|
116
|
+
lint.("# REM Mon TODO MAX-OVERDUE 5 MSG File\n").should.be.empty
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "reports at warning severity" do
|
|
120
|
+
lint.("REM Mon TODO MAX-OVERDUE 5 MSG File\n").first.severity.should == "warning"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# Whitespace at the end of a line.
|
|
8
|
+
#
|
|
9
|
+
# Not bikeshedding in Remind. The shipped `examples/remind.vim` highlights
|
|
10
|
+
# it as an *error*, with the note "This will match trailing whitespaces
|
|
11
|
+
# that seem to break rem2ps". A linter that stayed quiet about it would
|
|
12
|
+
# disagree with the editor Remind ships.
|
|
13
|
+
#
|
|
14
|
+
# The pattern is vim's -- `\S\s\+$`, a non-space then whitespace -- so a
|
|
15
|
+
# line of nothing but spaces is left alone. Emptying such a line is a
|
|
16
|
+
# cosmetic change and flagging every one of them in a file that has a few
|
|
17
|
+
# is how a linter teaches people to ignore it.
|
|
18
|
+
#
|
|
19
|
+
# A trailing run that follows a backslash belongs to DanglingContinuation,
|
|
20
|
+
# which has something more useful to say about it, so it is skipped here
|
|
21
|
+
# rather than reported twice.
|
|
22
|
+
class TrailingWhitespace < Rule
|
|
23
|
+
OFFENDING = /\S[ \t]+\z/
|
|
24
|
+
AFTER_CONTINUATION = /\\[ \t]+\z/
|
|
25
|
+
|
|
26
|
+
MESSAGE = "Trailing whitespace (breaks rem2ps output)"
|
|
27
|
+
|
|
28
|
+
def self.default_severity
|
|
29
|
+
"error"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.description
|
|
33
|
+
"Whitespace at the end of a line, which breaks rem2ps output."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check
|
|
37
|
+
document.each_raw_line do |raw, line|
|
|
38
|
+
body = raw.chomp
|
|
39
|
+
|
|
40
|
+
if offending?(body)
|
|
41
|
+
offend(line, MESSAGE, column: body.rstrip.length + 1)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def offending?(body)
|
|
49
|
+
body.match?(OFFENDING) && !body.match?(AFTER_CONTINUATION)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
__END__
|
|
56
|
+
|
|
57
|
+
require_relative "../document"
|
|
58
|
+
|
|
59
|
+
describe "RemLint::Rules::TrailingWhitespace" do
|
|
60
|
+
lint = proc do |text, config = {}|
|
|
61
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
62
|
+
|
|
63
|
+
RemLint::Rules::TrailingWhitespace.new(config).run(RemLint::Document.new(source))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "flags a line that ends in a space" do
|
|
67
|
+
offenses = lint.("MSG hello \n")
|
|
68
|
+
|
|
69
|
+
offenses.length.should == 1
|
|
70
|
+
offenses.first.line.should == 1
|
|
71
|
+
offenses.first.message.should.match(/rem2ps/)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "points at the first character of the trailing run" do
|
|
75
|
+
lint.("MSG hello \n").first.column.should == 10
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "flags a trailing tab" do
|
|
79
|
+
lint.("MSG hello\t\n").length.should == 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "reports at error severity, as remind.vim does" do
|
|
83
|
+
lint.("MSG hello \n").first.severity.should == "error"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "says nothing about a clean file" do
|
|
87
|
+
lint.("MSG hello\nMSG again\n").should.be.empty
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "says nothing about a line of nothing but whitespace" do
|
|
91
|
+
# remind.vim's pattern needs a non-space before the run, and so does this.
|
|
92
|
+
lint.("MSG hi\n \nMSG bye\n").should.be.empty
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "leaves a dangling continuation to the rule that explains it" do
|
|
96
|
+
lint.("SET x 1 + \\ \n").should.be.empty
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "still flags a line whose backslash is not at the end" do
|
|
100
|
+
lint.("MSG a \\ b \n").length.should == 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "flags every offending line in a file" do
|
|
104
|
+
lint.("MSG a \nMSG b\nMSG c\t\n").map(&:line).should == [1, 3]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "reports lines of a heredoc at their position in the enclosing file" do
|
|
108
|
+
source = RemLint::Source.new(path: "astro", text: "MSG a \n", line_offset: 40)
|
|
109
|
+
|
|
110
|
+
RemLint::Rules::TrailingWhitespace.new.run(RemLint::Document.new(source)).first.line.should == 41
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "takes its severity from configuration" do
|
|
114
|
+
lint.("MSG a \n", "Severity" => "warning").first.severity.should == "warning"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module RemLint
|
|
6
|
+
module Rules
|
|
7
|
+
# `TRANSLATE` commands Remind will not accept, or will reject silently.
|
|
8
|
+
#
|
|
9
|
+
# `TRANSLATE` has five forms and nothing else parses:
|
|
10
|
+
#
|
|
11
|
+
# TRANSLATE "original" "translated"
|
|
12
|
+
# TRANSLATE "original" (removes one)
|
|
13
|
+
# TRANSLATE CLEAR
|
|
14
|
+
# TRANSLATE DUMP
|
|
15
|
+
# TRANSLATE LOAD "file"
|
|
16
|
+
#
|
|
17
|
+
# The bare-word forms are the ones that go wrong, because they are one
|
|
18
|
+
# forgotten pair of quotes away from being a translation *of* the literal
|
|
19
|
+
# string "CLEAR".
|
|
20
|
+
#
|
|
21
|
+
# The other half is the one that matters to a translator. A translated
|
|
22
|
+
# string has to carry the same `%` escapes as the original, in the same
|
|
23
|
+
# order -- Remind compares them and refuses the translation outright if
|
|
24
|
+
# they differ. The message then stays in English, which looks like a
|
|
25
|
+
# missing translation rather than a rejected one.
|
|
26
|
+
class TranslateCommand < Rule
|
|
27
|
+
BARE_WORDS = %w[CLEAR DUMP].freeze
|
|
28
|
+
|
|
29
|
+
LOAD = "LOAD"
|
|
30
|
+
|
|
31
|
+
# `%a`, `%*b`, `%<Header>` and the rest -- anything the substitution
|
|
32
|
+
# filter would replace, which both sides have to agree on.
|
|
33
|
+
ESCAPE = /%\*?(?:<[^>]*>|\{[^}]*\}|\([^)]*\)|.)/
|
|
34
|
+
|
|
35
|
+
STRING = /\A\s*"(?:[^"\\]|\\.)*"/
|
|
36
|
+
|
|
37
|
+
def self.default_severity
|
|
38
|
+
"warning"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.description
|
|
42
|
+
"A TRANSLATE that is not one of Remind's five forms, or whose escapes do not match."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def check
|
|
46
|
+
document.code_commands.each do |command|
|
|
47
|
+
if command.keyword?("TRANSLATE")
|
|
48
|
+
check_command(command)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def check_command(command)
|
|
56
|
+
args = command.args.strip
|
|
57
|
+
word = args[/\A\S+/].to_s.upcase
|
|
58
|
+
|
|
59
|
+
if BARE_WORDS.include?(word) || word == LOAD
|
|
60
|
+
nil
|
|
61
|
+
else
|
|
62
|
+
check_strings(command, args)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_strings(command, args)
|
|
67
|
+
strings = quoted(args)
|
|
68
|
+
|
|
69
|
+
if strings.empty?
|
|
70
|
+
offend(command.line, form_message(args), column: command.keyword_column)
|
|
71
|
+
elsif strings.length >= 2
|
|
72
|
+
compare(command, strings[0], strings[1])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The one or two quoted strings the command carries.
|
|
77
|
+
def quoted(args)
|
|
78
|
+
rest = args.dup
|
|
79
|
+
found = []
|
|
80
|
+
|
|
81
|
+
while (match = rest.match(STRING))
|
|
82
|
+
found << match[0].strip
|
|
83
|
+
rest = match.post_match
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
found
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def compare(command, original, translated)
|
|
90
|
+
from = original.scan(ESCAPE)
|
|
91
|
+
to = translated.scan(ESCAPE)
|
|
92
|
+
|
|
93
|
+
if from != to
|
|
94
|
+
offend(command.line, escape_message(from, to), column: command.keyword_column)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def form_message(args)
|
|
99
|
+
"`TRANSLATE #{args.split(/\s+/).first}` is none of Remind's five forms; it takes " \
|
|
100
|
+
"a quoted string, two quoted strings, `CLEAR`, `DUMP`, or `LOAD \"file\"`"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def escape_message(from, to)
|
|
104
|
+
"the translation carries #{describe(to)} where the original has #{describe(from)}; " \
|
|
105
|
+
"Remind compares them and rejects a translation whose escapes differ, leaving " \
|
|
106
|
+
"the message in English"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def describe(escapes)
|
|
110
|
+
if escapes.empty?
|
|
111
|
+
"none"
|
|
112
|
+
else
|
|
113
|
+
escapes.map { |escape| "`#{escape}`" }.join(" ")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
__END__
|
|
121
|
+
|
|
122
|
+
require_relative "../document"
|
|
123
|
+
|
|
124
|
+
describe "RemLint::Rules::TranslateCommand" do
|
|
125
|
+
lint = proc do |text|
|
|
126
|
+
source = RemLint::Source.new(path: "t.rem", text: text)
|
|
127
|
+
|
|
128
|
+
RemLint::Rules::TranslateCommand.new.run(RemLint::Document.new(source))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
messages = proc { |text| lint.(text).map(&:message) }
|
|
132
|
+
|
|
133
|
+
describe "the five forms" do
|
|
134
|
+
it "accepts two quoted strings" do
|
|
135
|
+
lint.(%(TRANSLATE "Monday" "Montag"\n)).should.be.empty
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "accepts one quoted string" do
|
|
139
|
+
lint.(%(TRANSLATE "Monday"\n)).should.be.empty
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "accepts CLEAR and DUMP" do
|
|
143
|
+
lint.("TRANSLATE CLEAR\n").should.be.empty
|
|
144
|
+
lint.("TRANSLATE DUMP\n").should.be.empty
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "accepts LOAD with a filename" do
|
|
148
|
+
lint.(%(TRANSLATE LOAD "de.rem"\n)).should.be.empty
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "accepts the bare words in lower case" do
|
|
152
|
+
lint.("translate clear\n").should.be.empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "reports anything else" do
|
|
156
|
+
messages.("TRANSLATE Monday Montag\n").first.should.match(
|
|
157
|
+
/is none of Remind's five forms/,
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "escapes that do not match" do
|
|
163
|
+
it "reports a missing escape" do
|
|
164
|
+
messages.(%(TRANSLATE "in %b" "in"\n)).first.should.match(
|
|
165
|
+
/carries none where the original has `%b`/,
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "reports an added escape" do
|
|
170
|
+
messages.(%(TRANSLATE "today" "heute %b"\n)).first.should.match(/carries `%b`/)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "reports escapes in a different order" do
|
|
174
|
+
messages.(%(TRANSLATE "%a and %b" "%b und %a"\n)).first.should.match(
|
|
175
|
+
/carries `%b` `%a` where the original has `%a` `%b`/,
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "accepts matching escapes" do
|
|
180
|
+
lint.(%(TRANSLATE "%a and %b" "%a und %b"\n)).should.be.empty
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "accepts two strings with no escapes at all" do
|
|
184
|
+
lint.(%(TRANSLATE "Monday" "Montag"\n)).should.be.empty
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "explains what Remind does about it" do
|
|
188
|
+
messages.(%(TRANSLATE "in %b" "in"\n)).first.should.match(/leaving the message in English/)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "says nothing about comments" do
|
|
193
|
+
lint.("# TRANSLATE Monday Montag\n").should.be.empty
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "points at the keyword" do
|
|
197
|
+
lint.(" TRANSLATE Monday Montag\n").first.column.should == 4
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "reports at warning severity" do
|
|
201
|
+
lint.("TRANSLATE Monday Montag\n").first.severity.should == "warning"
|
|
202
|
+
end
|
|
203
|
+
end
|