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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +328 -0
  3. data/config/default.yml +374 -0
  4. data/docs/remlint-rules.md +390 -0
  5. data/docs/remlint.md +238 -0
  6. data/exe/remlint +8 -0
  7. data/lib/remlint/cli.rb +300 -0
  8. data/lib/remlint/command.rb +229 -0
  9. data/lib/remlint/config.rb +255 -0
  10. data/lib/remlint/date_literal.rb +283 -0
  11. data/lib/remlint/document.rb +161 -0
  12. data/lib/remlint/expr_lexer.rb +197 -0
  13. data/lib/remlint/extractors.rb +210 -0
  14. data/lib/remlint/formatter.rb +96 -0
  15. data/lib/remlint/invocation.rb +145 -0
  16. data/lib/remlint/logical_line.rb +194 -0
  17. data/lib/remlint/offense.rb +117 -0
  18. data/lib/remlint/rule.rb +216 -0
  19. data/lib/remlint/rules/addomit_without_scanfrom.rb +189 -0
  20. data/lib/remlint/rules/advance_warning_body.rb +176 -0
  21. data/lib/remlint/rules/banner_placement.rb +126 -0
  22. data/lib/remlint/rules/calendar_text_limited.rb +125 -0
  23. data/lib/remlint/rules/callback_signature.rb +221 -0
  24. data/lib/remlint/rules/clause_needs_full_date.rb +176 -0
  25. data/lib/remlint/rules/clause_requires_at.rb +178 -0
  26. data/lib/remlint/rules/clause_value_range.rb +257 -0
  27. data/lib/remlint/rules/color_component_range.rb +295 -0
  28. data/lib/remlint/rules/coordinate_not_string.rb +175 -0
  29. data/lib/remlint/rules/dangling_continuation.rb +134 -0
  30. data/lib/remlint/rules/date_out_of_range.rb +180 -0
  31. data/lib/remlint/rules/debug_command.rb +189 -0
  32. data/lib/remlint/rules/easterdate_from_today.rb +160 -0
  33. data/lib/remlint/rules/function_arity.rb +385 -0
  34. data/lib/remlint/rules/function_redefinition.rb +159 -0
  35. data/lib/remlint/rules/generated_file_edited.rb +125 -0
  36. data/lib/remlint/rules/hebrew_date.rb +245 -0
  37. data/lib/remlint/rules/iftrig_with_satisfy.rb +101 -0
  38. data/lib/remlint/rules/include_path.rb +155 -0
  39. data/lib/remlint/rules/info_clause.rb +186 -0
  40. data/lib/remlint/rules/info_substitution_without_header.rb +169 -0
  41. data/lib/remlint/rules/invocation_mismatch.rb +223 -0
  42. data/lib/remlint/rules/keyword_case.rb +172 -0
  43. data/lib/remlint/rules/license_header.rb +101 -0
  44. data/lib/remlint/rules/line_length.rb +101 -0
  45. data/lib/remlint/rules/literal_type_mismatch.rb +324 -0
  46. data/lib/remlint/rules/localization_pack.rb +144 -0
  47. data/lib/remlint/rules/moon_phase_argument.rb +162 -0
  48. data/lib/remlint/rules/omit_aware_delta.rb +168 -0
  49. data/lib/remlint/rules/push_vars_missing_name.rb +158 -0
  50. data/lib/remlint/rules/repeat_trigger.rb +188 -0
  51. data/lib/remlint/rules/satisfy_constraint.rb +230 -0
  52. data/lib/remlint/rules/shell_maxlen.rb +170 -0
  53. data/lib/remlint/rules/shell_use_while_run_disabled.rb +172 -0
  54. data/lib/remlint/rules/string_escape.rb +158 -0
  55. data/lib/remlint/rules/syntax.rb +229 -0
  56. data/lib/remlint/rules/system_variable_assignment.rb +197 -0
  57. data/lib/remlint/rules/tag_syntax.rb +145 -0
  58. data/lib/remlint/rules/text_after_eof_marker.rb +134 -0
  59. data/lib/remlint/rules/time_zone_name.rb +212 -0
  60. data/lib/remlint/rules/tk_tag_namespace.rb +121 -0
  61. data/lib/remlint/rules/todo_complete_through.rb +122 -0
  62. data/lib/remlint/rules/trailing_whitespace.rb +116 -0
  63. data/lib/remlint/rules/translate_command.rb +203 -0
  64. data/lib/remlint/rules/unbalanced_blocks.rb +305 -0
  65. data/lib/remlint/rules/unbalanced_delimiters.rb +251 -0
  66. data/lib/remlint/rules/unknown_special_type.rb +155 -0
  67. data/lib/remlint/rules/unknown_substitution_sequence.rb +255 -0
  68. data/lib/remlint/rules/unknown_system_variable.rb +145 -0
  69. data/lib/remlint/rules/unquoted_shell_substitution.rb +266 -0
  70. data/lib/remlint/rules/until_before_from.rb +204 -0
  71. data/lib/remlint/rules/world_writable_script.rb +128 -0
  72. data/lib/remlint/rules.rb +60 -0
  73. data/lib/remlint/runner.rb +274 -0
  74. data/lib/remlint/source.rb +36 -0
  75. data/lib/remlint/tables.rb +395 -0
  76. data/lib/remlint/trigger.rb +359 -0
  77. data/lib/remlint/version.rb +5 -0
  78. data/lib/remlint/vocabulary.rb +236 -0
  79. data/lib/remlint.rb +35 -0
  80. data/tasks/generate_tables.rb +175 -0
  81. metadata +170 -0
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+
5
+ module RemLint
6
+ module Rules
7
+ # `INFO` clauses a back-end will drop.
8
+ #
9
+ # `INFO` attaches a `"Header: value"` pair to a reminder, which the
10
+ # machine-readable back-ends carry through and `%<Header>` reads back. Two
11
+ # things go wrong, and a back-end's only recourse for either is to ignore
12
+ # the clause:
13
+ #
14
+ # INFO with no quoted "Header: value" string
15
+ # two INFO clauses on one command with the same header
16
+ #
17
+ # Headers are not case-sensitive, so `Url:` and `URL:` on one command are
18
+ # the same collision -- which is exactly the sort of duplicate that reads
19
+ # as two distinct pieces of information.
20
+ #
21
+ # The reminder still triggers either way. What goes missing is the location
22
+ # or the URL, silently, in the calendar app at the far end of the export.
23
+ class InfoClause < Rule
24
+ # `"Header: value"` -- the header runs to the first colon and cannot
25
+ # contain a quote.
26
+ WELL_FORMED = /\A(?<quote>["'])(?<header>[^:"']+):(?<value>.*)\k<quote>\z/m
27
+
28
+ def self.default_severity
29
+ "warning"
30
+ end
31
+
32
+ def self.description
33
+ "An INFO clause that is malformed, or duplicates another's header."
34
+ end
35
+
36
+ def check
37
+ document.code_commands.each do |command|
38
+ check_command(command)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def check_command(command)
45
+ seen = {}
46
+
47
+ document.trigger_for(command).clauses.each do |clause|
48
+ if clause.name == "INFO"
49
+ check_clause(command, clause, seen)
50
+ end
51
+ end
52
+ end
53
+
54
+ def check_clause(command, clause, seen)
55
+ argument = argument_of(command, clause)
56
+ match = argument&.match(WELL_FORMED)
57
+
58
+ if match.nil?
59
+ report_malformed(command, clause, argument)
60
+ else
61
+ check_duplicate(
62
+ command,
63
+ clause,
64
+ match[:header].strip,
65
+ seen,
66
+ )
67
+ end
68
+ end
69
+
70
+ # The whole quoted string after the keyword, if there is one.
71
+ def argument_of(command, clause)
72
+ tokens = document.tokens_for(command.logical_line)
73
+ token = tokens[clause.argument_index]
74
+
75
+ if token&.type == :string
76
+ token.value
77
+ end
78
+ end
79
+
80
+ def report_malformed(command, clause, argument)
81
+ offend_at(
82
+ command.logical_line,
83
+ clause.offset,
84
+ "`INFO` takes a quoted \"Header: value\" string; " \
85
+ "#{argument ? "`#{argument}`" : 'this'} is not one",
86
+ )
87
+ end
88
+
89
+ def check_duplicate(command, clause, header, seen)
90
+ key = header.downcase
91
+ first = seen[key]
92
+
93
+ if first
94
+ offend_at(
95
+ command.logical_line,
96
+ clause.offset,
97
+ "`#{header}:` repeats the header given on this command as `#{first}:`; " \
98
+ "headers are not case-sensitive and cannot be duplicated",
99
+ )
100
+ else
101
+ seen[key] = header
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ __END__
109
+
110
+ require_relative "../document"
111
+
112
+ describe "RemLint::Rules::InfoClause" do
113
+ lint = proc do |text|
114
+ source = RemLint::Source.new(path: "t.rem", text: text)
115
+
116
+ RemLint::Rules::InfoClause.new.run(RemLint::Document.new(source))
117
+ end
118
+
119
+ messages = proc { |text| lint.(text).map(&:message) }
120
+
121
+ describe "clauses that are fine" do
122
+ it "accepts a well-formed INFO" do
123
+ lint.(%(REM 1 Jan INFO "Location: The pub" MSG Meet up\n)).should.be.empty
124
+ end
125
+
126
+ it "accepts two INFO clauses with different headers" do
127
+ lint.(%(REM 1 Jan INFO "Location: pub" INFO "Url: x" MSG hi\n)).should.be.empty
128
+ end
129
+
130
+ it "accepts a value containing a colon" do
131
+ lint.(%(REM 1 Jan INFO "Url: https://example.com/x" MSG hi\n)).should.be.empty
132
+ end
133
+
134
+ it "accepts a reminder with no INFO at all" do
135
+ lint.("REM 1 Jan MSG hi\n").should.be.empty
136
+ end
137
+ end
138
+
139
+ describe "malformed clauses" do
140
+ it "reports an unquoted argument" do
141
+ messages.("REM 1 Jan INFO Location: pub MSG hi\n").first.should.match(
142
+ /takes a quoted "Header: value" string/,
143
+ )
144
+ end
145
+
146
+ it "reports a quoted string with no colon" do
147
+ messages.(%(REM 1 Jan INFO "Location" MSG hi\n)).length.should == 1
148
+ end
149
+
150
+ it "quotes what was written back" do
151
+ messages.(%(REM 1 Jan INFO "Location" MSG hi\n)).first.should.match(/`"Location"` is not one/)
152
+ end
153
+ end
154
+
155
+ describe "duplicate headers" do
156
+ it "reports the same header twice" do
157
+ messages.(%(REM 1 Jan INFO "Url: a" INFO "Url: b" MSG hi\n)).first.should ==
158
+ "`Url:` repeats the header given on this command as `Url:`; " \
159
+ "headers are not case-sensitive and cannot be duplicated"
160
+ end
161
+
162
+ it "reports a header that differs only in case" do
163
+ messages.(%(REM 1 Jan INFO "Url: a" INFO "URL: b" MSG hi\n)).first.should.match(
164
+ /`URL:` repeats the header given on this command as `Url:`/,
165
+ )
166
+ end
167
+
168
+ it "reports only the second of two" do
169
+ lint.(%(REM 1 Jan INFO "Url: a" INFO "Url: b" MSG hi\n)).length.should == 1
170
+ end
171
+
172
+ it "does not carry headers between commands" do
173
+ text = %(REM 1 Jan INFO "Url: a" MSG hi\nREM 2 Jan INFO "Url: b" MSG hi\n)
174
+
175
+ lint.(text).should.be.empty
176
+ end
177
+ end
178
+
179
+ it "says nothing about comments" do
180
+ lint.(%(# REM 1 Jan INFO "Url: a" INFO "Url: b" MSG hi\n)).should.be.empty
181
+ end
182
+
183
+ it "reports at warning severity" do
184
+ lint.(%(REM 1 Jan INFO "Location" MSG hi\n)).first.severity.should == "warning"
185
+ end
186
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+
5
+ module RemLint
6
+ module Rules
7
+ # `%<Header>` with no matching `INFO` on the same command.
8
+ #
9
+ # `FindTrigInfo` returns NULL and `dosubst.c` ships nothing at all, so the
10
+ # substitution expands to the empty string. The reminder then reads
11
+ # `Meeting at ` and looks like a truncation bug somewhere else entirely.
12
+ #
13
+ # `INFO` headers are per-command, so this is decidable from one line: the
14
+ # headers a command carries and the headers its body asks for are both
15
+ # right there. Matching is case-insensitive, because Remind's header
16
+ # lookup is.
17
+ class InfoSubstitutionWithoutHeader < Rule
18
+ # `INFO "Header: value"` -- the argument is a quoted string whose header
19
+ # runs up to the first colon.
20
+ INFO_HEADER = /\A["'](?<header>[^:"']+):/
21
+
22
+ REFERENCE = /%<(?<header>[^>]*)>/
23
+
24
+ def self.default_severity
25
+ "warning"
26
+ end
27
+
28
+ def self.description
29
+ "A %<Header> substitution with no INFO clause of that name on the command."
30
+ end
31
+
32
+ def check
33
+ document.code_commands.each do |command|
34
+ check_command(command)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def check_command(command)
41
+ trigger = document.trigger_for(command)
42
+
43
+ if trigger.text_body?
44
+ report(command, headers(command, trigger))
45
+ end
46
+ end
47
+
48
+ # The headers this command declares, downcased for comparison.
49
+ def headers(command, trigger)
50
+ trigger.clauses.filter_map do |clause|
51
+ if clause.name == "INFO"
52
+ header_of(command, clause)
53
+ end
54
+ end
55
+ end
56
+
57
+ # The header name in the string that follows the INFO keyword.
58
+ def header_of(command, clause)
59
+ rest = command.text[clause.end_offset..].to_s
60
+ match = rest.sub(/\A\s+/, "").match(INFO_HEADER)
61
+
62
+ match && match[:header].strip.downcase
63
+ end
64
+
65
+ def report(command, declared)
66
+ trigger = document.trigger_for(command)
67
+ body = command.text[trigger.body_offset..].to_s
68
+
69
+ body.to_enum(:scan, REFERENCE).each do
70
+ match = Regexp.last_match
71
+
72
+ unless declared.include?(match[:header].strip.downcase)
73
+ offend_at(command.logical_line, trigger.body_offset + match.begin(0), message(match))
74
+ end
75
+ end
76
+ end
77
+
78
+ def message(match)
79
+ "`%<#{match[:header]}>` has no matching `INFO` clause on this command; " \
80
+ "it expands to nothing"
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ __END__
87
+
88
+ require_relative "../document"
89
+
90
+ describe "RemLint::Rules::InfoSubstitutionWithoutHeader" do
91
+ lint = proc do |text|
92
+ source = RemLint::Source.new(path: "t.rem", text: text)
93
+
94
+ RemLint::Rules::InfoSubstitutionWithoutHeader.new.run(RemLint::Document.new(source))
95
+ end
96
+
97
+ messages = proc { |text| lint.(text).map(&:message) }
98
+
99
+ describe "references that match" do
100
+ it "accepts a header the command declares" do
101
+ lint.(%(REM 1 Jan INFO "Location: The pub" MSG Meet at %<Location>\n)).should.be.empty
102
+ end
103
+
104
+ it "matches case-insensitively, as Remind's lookup does" do
105
+ lint.(%(REM 1 Jan INFO "Location: pub" MSG Meet at %<LOCATION>\n)).should.be.empty
106
+ lint.(%(REM 1 Jan INFO "URL: x" MSG See %<Url>\n)).should.be.empty
107
+ end
108
+
109
+ it "accepts several headers on one command" do
110
+ text = %(REM 1 Jan INFO "Location: pub" INFO "Url: x" MSG %<Location> %<Url>\n)
111
+
112
+ lint.(text).should.be.empty
113
+ end
114
+
115
+ it "accepts a header declared after the reference is written" do
116
+ # Clause order does not matter; both are on the same command.
117
+ lint.(%(REM 1 Jan INFO "Location: pub" MSG at %<Location>\n)).should.be.empty
118
+ end
119
+ end
120
+
121
+ describe "references that do not match" do
122
+ it "reports a header the command never declares" do
123
+ messages.("REM 1 Jan MSG Meet at %<Location>\n").first.should ==
124
+ "`%<Location>` has no matching `INFO` clause on this command; it expands to nothing"
125
+ end
126
+
127
+ it "reports a misspelled header" do
128
+ messages.(%(REM 1 Jan INFO "Location: pub" MSG at %<Locaton>\n)).length.should == 1
129
+ end
130
+
131
+ it "reports each unmatched reference" do
132
+ messages.("REM 1 Jan MSG %<A> and %<B>\n").length.should == 2
133
+ end
134
+
135
+ it "does not confuse headers between two commands" do
136
+ text = %(REM 1 Jan INFO "Location: pub" MSG at %<Location>\nREM 2 Jan MSG at %<Location>\n)
137
+
138
+ lint.(text).map(&:line).should == [2]
139
+ end
140
+
141
+ it "points at the reference" do
142
+ text = "REM 1 Jan MSG Meet at %<Location>\n"
143
+
144
+ lint.(text).first.column.should == text.index("%<") + 1
145
+ end
146
+ end
147
+
148
+ describe "what it leaves alone" do
149
+ it "says nothing about a body with no reference" do
150
+ lint.(%(REM 1 Jan INFO "Location: pub" MSG Meet up\n)).should.be.empty
151
+ end
152
+
153
+ it "says nothing about a command with no body" do
154
+ lint.("SET a 1\n").should.be.empty
155
+ end
156
+
157
+ it "says nothing about comments" do
158
+ lint.("# uses %<Location> somewhere\n").should.be.empty
159
+ end
160
+ end
161
+
162
+ it "reports at warning severity" do
163
+ lint.("REM 1 Jan MSG %<Location>\n").first.severity.should == "warning"
164
+ end
165
+
166
+ it "reports the physical line inside a continuation" do
167
+ lint.("REM 1 Jan MSG a \\\n %<Location>\n").first.line.should == 2
168
+ end
169
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rule"
4
+
5
+ module RemLint
6
+ module Rules
7
+ # Things a file does that its declared invocation will not deliver.
8
+ #
9
+ # Three checks want the command line rather than the file, and a file that
10
+ # declares one gets all three:
11
+ #
12
+ # # remlint:invocation remind -pp -g /path/to/file
13
+ #
14
+ # SORT SPEC. `-g` takes up to four characters, each `a` or `d`. Anything
15
+ # else is silently ignored, so a sort somebody asked for never happens.
16
+ #
17
+ # INFO HEADERS. Plain `-p` does not carry `INFO` to the back-end at all;
18
+ # `-pp` does. A file full of correct `INFO` clauses and a `-p` invocation
19
+ # is a reminder that is fine and plumbing that is not, and nothing reports
20
+ # the gap.
21
+ #
22
+ # TODO IN A CALENDAR. `TODO`'s nagging and its overdue tail exist only in
23
+ # Agenda Mode. In a calendar a `TODO` is an ordinary event, which makes the
24
+ # keyword a lie about what the reminder does.
25
+ #
26
+ # A file with no declaration says nothing about how it is run, so this rule
27
+ # says nothing about it either.
28
+ class InvocationMismatch < Rule
29
+ SORT_CHARACTERS = %w[a d].freeze
30
+
31
+ MAX_SORT = 4
32
+
33
+ # Where a clause was found, for a message to point at.
34
+ Site = Struct.new(:line, :column)
35
+
36
+ def self.default_severity
37
+ "warning"
38
+ end
39
+
40
+ def self.description
41
+ "A file whose declared `# remlint:invocation` does not deliver what it uses."
42
+ end
43
+
44
+ def check
45
+ invocation = document.invocation
46
+
47
+ if invocation.declared?
48
+ check_sort(invocation)
49
+ check_info(invocation)
50
+ check_todo(invocation)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def check_sort(invocation)
57
+ spec = invocation.sort_spec
58
+
59
+ if spec && !valid_sort?(spec)
60
+ offend(declaration_line, sort_message(spec))
61
+ end
62
+ end
63
+
64
+ def valid_sort?(spec)
65
+ spec.length <= MAX_SORT &&
66
+ spec.each_char.all? { |character| SORT_CHARACTERS.include?(character.downcase) }
67
+ end
68
+
69
+ def check_info(invocation)
70
+ clause = first_clause("INFO")
71
+
72
+ if clause && invocation.simple_calendar? && !invocation.carries_info?
73
+ offend(clause.line, info_message, column: clause.column)
74
+ end
75
+ end
76
+
77
+ def check_todo(invocation)
78
+ clause = first_clause("TODO")
79
+
80
+ if clause && invocation.calendar?
81
+ offend(clause.line, todo_message, column: clause.column)
82
+ end
83
+ end
84
+
85
+ # The first command carrying a clause of that name, as a line and
86
+ # column to point at. `filter_map` rather than an `each` with a
87
+ # `break`, which yields the whole array when nothing matches.
88
+ def first_clause(name)
89
+ document.code_commands.filter_map { |command| site_of(command, name) }.first
90
+ end
91
+
92
+ def site_of(command, name)
93
+ found = document.trigger_for(command).find(name)
94
+
95
+ if found
96
+ Site.new(*command.logical_line.position_at(found.offset))
97
+ end
98
+ end
99
+
100
+ def declaration_line
101
+ index = document.raw_lines.find_index { |raw| raw.match?(Invocation::DIRECTIVE) }
102
+
103
+ document.line_number_at(index || 0)
104
+ end
105
+
106
+ def sort_message(spec)
107
+ "`-g#{spec}` is not a sort spec Remind reads -- up to #{MAX_SORT} characters, " \
108
+ "each `a` or `d` -- and the characters it cannot read are ignored, so the sort " \
109
+ "never happens"
110
+ end
111
+
112
+ def info_message
113
+ "this file uses `INFO`, but its declared invocation passes plain `-p`, which " \
114
+ "does not carry headers to the back-end at all; `-pp` does"
115
+ end
116
+
117
+ def todo_message
118
+ "`TODO`'s nagging and overdue tail exist only in Agenda Mode, and this file " \
119
+ "declares a calendar invocation -- in a calendar a `TODO` is an ordinary event"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ __END__
126
+
127
+ require_relative "../document"
128
+
129
+ describe "RemLint::Rules::InvocationMismatch" do
130
+ lint = proc do |text|
131
+ source = RemLint::Source.new(path: "t.rem", text: text)
132
+
133
+ RemLint::Rules::InvocationMismatch.new.run(RemLint::Document.new(source))
134
+ end
135
+
136
+ messages = proc { |text| lint.(text).map(&:message) }
137
+
138
+ it "says nothing about a file that declares no invocation" do
139
+ lint.(%(REM 1 Jan INFO "Url: x" TODO MSG hi\n)).should.be.empty
140
+ end
141
+
142
+ describe "the sort spec" do
143
+ it "accepts a spec of a and d" do
144
+ lint.("# remlint:invocation remind -gaad\nMSG hi\n").should.be.empty
145
+ end
146
+
147
+ it "accepts a bare -g" do
148
+ lint.("# remlint:invocation remind -g\nMSG hi\n").should.be.empty
149
+ end
150
+
151
+ it "reports a character Remind does not read" do
152
+ messages.("# remlint:invocation remind -gxq\nMSG hi\n").first.should.match(
153
+ /is not a sort spec Remind reads/,
154
+ )
155
+ end
156
+
157
+ it "reports a spec longer than four characters" do
158
+ messages.("# remlint:invocation remind -gaaaaa\nMSG hi\n").length.should == 1
159
+ end
160
+
161
+ it "explains that the sort never happens" do
162
+ messages.("# remlint:invocation remind -gx\nMSG hi\n").first.should.match(
163
+ /the sort never happens/,
164
+ )
165
+ end
166
+ end
167
+
168
+ describe "INFO headers" do
169
+ it "reports INFO under a plain -p invocation" do
170
+ text = %(# remlint:invocation remind -p\nREM 1 Jan INFO "Url: x" MSG hi\n)
171
+
172
+ messages.(text).first.should.match(/passes plain `-p`, which does not carry headers/)
173
+ end
174
+
175
+ it "accepts INFO under -pp" do
176
+ text = %(# remlint:invocation remind -pp\nREM 1 Jan INFO "Url: x" MSG hi\n)
177
+
178
+ lint.(text).should.be.empty
179
+ end
180
+
181
+ it "says nothing about a file with no INFO" do
182
+ lint.("# remlint:invocation remind -p\nMSG hi\n").should.be.empty
183
+ end
184
+
185
+ it "says nothing under an agenda invocation" do
186
+ text = %(# remlint:invocation remind -q\nREM 1 Jan INFO "Url: x" MSG hi\n)
187
+
188
+ lint.(text).should.be.empty
189
+ end
190
+ end
191
+
192
+ describe "TODO in a calendar" do
193
+ it "reports it" do
194
+ text = "# remlint:invocation remind -c3\nREM 1 TODO MSG File\n"
195
+
196
+ messages.(text).first.should.match(/exist only in Agenda Mode/)
197
+ end
198
+
199
+ it "accepts TODO under an agenda invocation" do
200
+ lint.("# remlint:invocation remind -q\nREM 1 TODO MSG File\n").should.be.empty
201
+ end
202
+
203
+ it "says nothing about a file with no TODO" do
204
+ lint.("# remlint:invocation remind -c3\nMSG hi\n").should.be.empty
205
+ end
206
+
207
+ it "points at the TODO clause" do
208
+ text = "# remlint:invocation remind -c3\nREM 1 TODO MSG File\n"
209
+
210
+ lint.(text).first.line.should == 2
211
+ end
212
+ end
213
+
214
+ it "reports every mismatch a file has" do
215
+ text = %(# remlint:invocation remind -p -gx\nREM 1 INFO "Url: x" TODO MSG hi\n)
216
+
217
+ lint.(text).length.should == 3
218
+ end
219
+
220
+ it "reports at warning severity" do
221
+ lint.("# remlint:invocation remind -gx\nMSG hi\n").first.severity.should == "warning"
222
+ end
223
+ end