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,390 @@
|
|
|
1
|
+
# Rule backlog
|
|
2
|
+
|
|
3
|
+
The 100 rule ideas from `book-sections/*.md`, triaged. Ordered by what I would
|
|
4
|
+
build next, not by chapter.
|
|
5
|
+
|
|
6
|
+
The test each rule has to pass: **does Remind stay silent when this is wrong?**
|
|
7
|
+
Remind reports `Missing ']'`, `E_NOSUCH_VAR` and `Can't compute trigger`
|
|
8
|
+
perfectly well on its own. A linter earns its place only where Remind says
|
|
9
|
+
nothing, or says it months later on the day a reminder fires.
|
|
10
|
+
|
|
11
|
+
Constants are verified against the C source, not taken from the prose. Where a
|
|
12
|
+
rule below cites a limit, the file and line that defines it is named.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tier 1 — security (2) — DONE
|
|
17
|
+
|
|
18
|
+
Built as one rule, `UnquotedShellSubstitution`: it is the same check on two
|
|
19
|
+
commands, and one message serves both.
|
|
20
|
+
|
|
21
|
+
The only rules in the set where being wrong is a hole rather than a wrong date.
|
|
22
|
+
|
|
23
|
+
- [x] **RunBodyUnquotedSubstitution** — a `%` sequence or `[expr]` outside shell
|
|
24
|
+
quotes in a `RUN` body. Reminder text is data; unquoted data in a shell
|
|
25
|
+
command is command injection in a calendar.
|
|
26
|
+
- [x] **IncludeCmdUnquotedPaste** — the same for `INCLUDECMD`. `examples/astro`
|
|
27
|
+
writes `L="[lessons]"` carefully; nothing enforces that care today.
|
|
28
|
+
|
|
29
|
+
## Tier 2 — exact, single-file, silent when wrong (10) — DONE
|
|
30
|
+
|
|
31
|
+
Closed sets and literal comparisons. No cross-file knowledge, no runtime values.
|
|
32
|
+
|
|
33
|
+
Four of these turned out narrower than the book description implied, and every
|
|
34
|
+
narrowing came from the source, the man page or the corpus rather than from
|
|
35
|
+
reasoning about it. They are recorded inline below and in the rules' own doc
|
|
36
|
+
comments.
|
|
37
|
+
|
|
38
|
+
Built as ten rule classes, because two pairs are one check each:
|
|
39
|
+
`UnquotedShellSubstitution`, `ClauseRequiresAt`, `IftrigWithSatisfy`,
|
|
40
|
+
`UnknownSubstitutionSequence`, `InfoSubstitutionWithoutHeader`,
|
|
41
|
+
`TextAfterEofMarker`, `UntilBeforeFrom`, `CoordinateNotString`,
|
|
42
|
+
`ShellUseWhileRunDisabled`, `LiteralTypeMismatch`.
|
|
43
|
+
|
|
44
|
+
- [x] **UnknownSubstitutionSequence** — `dosubst.c:820` `default:` emits the
|
|
45
|
+
character and drops the `%`. Silent. Valid set: `A`–`Z` case-insensitive
|
|
46
|
+
(`UPPER(c)` at `dosubst.c:482`), `0`–`9`, `!?@#:_"`, plus `%<Header>`,
|
|
47
|
+
`%(text)`, `%{name}` and the `%*` modifier.
|
|
48
|
+
**Two corrections.** Every letter and every digit is a case, so there is
|
|
49
|
+
no `%z` typo to catch — the rule is really about punctuation and
|
|
50
|
+
unterminated argument forms. And a trailing `%` is *documented*: it
|
|
51
|
+
suppresses the appended newline (`remind.1`; `dosubst("hello")` is 6
|
|
52
|
+
characters, `dosubst("hello%")` is 5). Flagging it produced 209 false
|
|
53
|
+
positives on `tests/`.
|
|
54
|
+
- [x] **InfoSubstitutionWithoutHeader** — `%<Name>` with no matching `INFO` on
|
|
55
|
+
the command. `FindTrigInfo` returns NULL and nothing is shipped
|
|
56
|
+
(`dosubst.c:293`), so the body reads `Meeting at ` like a truncation bug.
|
|
57
|
+
- [x] **UntilBeforeFrom** — `UNTIL` earlier than `FROM`. **Two corrections.**
|
|
58
|
+
Remind is *not* silent: it warns at parse time, with a separate message
|
|
59
|
+
for `SCANFROM`, which the rule now covers too. And equal dates are a
|
|
60
|
+
legal one-day window that fires — `FROM 1992-01-06 UNTIL 1992-01-06`
|
|
61
|
+
triggers on the day — so the comparison is strict.
|
|
62
|
+
- [x] **IftrigWithSatisfy** — `IFTRIG` takes any trigger a `REM` does *except*
|
|
63
|
+
`SATISFY`. Easy to hit by copying a `REM`.
|
|
64
|
+
- [x] **TimeZoneWithoutAt** + **DurationWithoutAt** — built as one rule,
|
|
65
|
+
`ClauseRequiresAt`. Both are "this clause needs a time"; the message names
|
|
66
|
+
which clause and why. **Corpus correction:** `AT` is not the only source
|
|
67
|
+
of a time. `include/lunar-eclipses.rem` supplies it from a pasted
|
|
68
|
+
`[utctolocal(...)]` DATETIME, 142 times. A trigger containing any
|
|
69
|
+
bracketed expression is now left alone.
|
|
70
|
+
- [x] **LatitudeLongitudeNotString** — built as `CoordinateNotString`. Remind
|
|
71
|
+
has no float type, so `SET $Latitude 45.42` is not a near miss, it is a
|
|
72
|
+
different thing. Out-of-range values produce confidently wrong sunrise
|
|
73
|
+
times.
|
|
74
|
+
- [x] **TextAfterEofMarker** — `files.c:421`, exact match on a line of
|
|
75
|
+
`__EOF__`. Everything below is dead text that reads like configuration.
|
|
76
|
+
- [x] **CrossTypeEqualityAlwaysFalse** — built as `LiteralTypeMismatch`, which
|
|
77
|
+
also covers the ordering half of **OperandTypeMismatch**: `compare()` in
|
|
78
|
+
`expr.c` makes `==` a constant 0 and `!=` a constant 1 across types, and
|
|
79
|
+
raises `Type mismatch` for `<`, `>`, `<=`, `>=`.
|
|
80
|
+
- [x] **ShellUseDefinedWhileRunDisabled** — built as `ShellUseWhileRunDisabled`.
|
|
81
|
+
Confirmed in the source: `userfns.c:280` captures the flag at definition
|
|
82
|
+
and `expr.c:777` re-applies it at every call.
|
|
83
|
+
|
|
84
|
+
## Tier 3 — exact, worth having, lower frequency (20) — DONE
|
|
85
|
+
|
|
86
|
+
Built as eleven rule classes, merging where the check is one check:
|
|
87
|
+
`ClauseValueRange`, `DateOutOfRange`, `ClauseNeedsFullDate`, `RepeatTrigger`,
|
|
88
|
+
`InfoClause`, `TagSyntax`, `BannerPlacement`, `UnknownSpecialType`,
|
|
89
|
+
`StringEscape`, `DebugCommand`, `ShellMaxlen`, plus the nesting limit folded
|
|
90
|
+
into `UnbalancedBlocks`.
|
|
91
|
+
|
|
92
|
+
**Four more corpus corrections**, all from `tests/`:
|
|
93
|
+
|
|
94
|
+
- `DURATION` has no hour ceiling. A duration is a *length*, not a time of day;
|
|
95
|
+
`tests/test3.rem` writes `DURATION 24:45` and `DURATION 48:45` and Remind
|
|
96
|
+
accepts both. Only the minutes are bounded. (12 false positives.)
|
|
97
|
+
- `OMIT ... THROUGH ...` takes partial dates on purpose -- it is the omit-range
|
|
98
|
+
syntax, not a reminder's expiry. `OMIT Jun THROUGH July 15` is in
|
|
99
|
+
`tests/test.rem`. (3 false positives.)
|
|
100
|
+
- The `SPECIAL` set includes `PostScript`, `PSFile` and `PS`, which rem2ps
|
|
101
|
+
compares `passthru` against and which look like reminder types rather than
|
|
102
|
+
SPECIAL ones. (2 false positives.)
|
|
103
|
+
- `AT 13:00AM` really is an error -- `Ill-formed time`, confirmed against the
|
|
104
|
+
binary -- so that one stayed.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
`DateLiteral` (built for `UntilBeforeFrom`) already reads Remind's date forms
|
|
108
|
+
in any order, so the three date-range rules are mostly wiring. `Trigger` gives
|
|
109
|
+
the clause positions the time and priority rules need.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
- [x] **TriggerComponentRange** / **DateConstantBeforeEpoch** /
|
|
114
|
+
**DateOutsideRepresentableRange** — merge into one. `BASE 1990` +
|
|
115
|
+
`YR_RANGE 4000` (`custom.h.in:80,86`) gives 1990-01-01 … 5990 exactly.
|
|
116
|
+
- [x] **TimeValueRange** — `AT 25:00`, `AT 9:70`.
|
|
117
|
+
- [x] **PriorityOutOfRange** — `ParsePriority` (`dorem.c:2143`) is `p<0 || p>9999`
|
|
118
|
+
→ `E_2HIGH`. Note: `PRIORITY -1` returns `E_EXPECTING_NUMBER` instead, and
|
|
119
|
+
the message should say so.
|
|
120
|
+
- [x] **IntConstantOutOfRange** — Remind's INT is the platform's C int.
|
|
121
|
+
*Deferred: the bound is the build's `int`, which the linter does not know,
|
|
122
|
+
and a literal that large is vanishingly rare.*
|
|
123
|
+
- [x] **InvalidStringEscape** — closed escape set; `\x00` prohibited outright.
|
|
124
|
+
- [x] **TildeBackWithDayComponent** — `~~n` has an implied day component of 1;
|
|
125
|
+
writing both is a contradiction. *Deferred with BackSugarAvailable: both
|
|
126
|
+
need delta/back parsing that nothing else wants yet.*
|
|
127
|
+
- [x] **PartialDateAfterFromUntil** — both keywords require a full date.
|
|
128
|
+
- [x] **RepeatWithoutFullStartDate** — a repeat counts from a start date.
|
|
129
|
+
- [x] **WeekdayWithRepeat** — the weekday picks only the start date; the
|
|
130
|
+
recurrence ignores weekdays. `REM Fri 15 Sep 2025 *10` fires on days that
|
|
131
|
+
are mostly not Fridays.
|
|
132
|
+
- [x] **BannerPlacement** — only the last `BANNER` matters.
|
|
133
|
+
- [x] **MaxOverdueNotPositive**, **CompleteThroughNotFullDate**
|
|
134
|
+
- [x] **TagSyntax** — a comma inside a tag splits it into two nobody meant.
|
|
135
|
+
- [x] **DuplicateInfoHeader** — headers are not case-sensitive, so `Url:` and
|
|
136
|
+
`URL:` on one command collide.
|
|
137
|
+
- [x] **InfoStringMalformed**
|
|
138
|
+
- [x] **HebrewDateOutOfRange** — *Deferred.* Month lengths are **not** fixed:
|
|
139
|
+
`hbcal.c` recomputes Heshvan and Kislev per year from the year length
|
|
140
|
+
(353/354/355 days), so only "no month has more than 30 days" is decidable
|
|
141
|
+
cheaply, and that catches almost nothing.
|
|
142
|
+
- [x] **MoonPhaseArgumentRange** — closed four-element sets. *Deferred: worth
|
|
143
|
+
building, but `moonphase`/`moondate`/`moondatetime` each take the selector
|
|
144
|
+
in a different position and the payoff is one literal check.*
|
|
145
|
+
- [x] **UnknownDebugFlag**, **DebugFlagLeftEnabled**
|
|
146
|
+
- [x] **TranslateCommandForm**, **TranslationFormatSpecifiers** — *Deferred to
|
|
147
|
+
the localization group in Tier 5, where the rest of the TRANSLATE and
|
|
148
|
+
callback rules live.*
|
|
149
|
+
- [x] **ShellMaxlenArgument** — `shell(cmd, 0)` returns nothing and looks exactly
|
|
150
|
+
like a command that produced nothing.
|
|
151
|
+
- [x] **UnknownSpecialType** / **SpecialBodyShape** / **DefaultColorFormat** —
|
|
152
|
+
extend the existing `ColorComponentRange`.
|
|
153
|
+
- [x] **IfNestingTooDeep** — `IF_NEST 64` (`ifelse.c:19`). Free, given the stack
|
|
154
|
+
`UnbalancedBlocks` already keeps. Nobody will ever hit it.
|
|
155
|
+
|
|
156
|
+
## Tier 4 — needed machinery that did not exist — DONE, or decided
|
|
157
|
+
|
|
158
|
+
- [x] **FunctionRedefinition** — real: `suppress_redefined_function_warning` in
|
|
159
|
+
`userfns.c:204`, and `FSET - name(...)` is the documented escape hatch
|
|
160
|
+
(`WHATSNEW:714`). Sits next to `FunctionArity`.
|
|
161
|
+
- [x] **PushVarsMissingName** — reuses `UnbalancedBlocks`' stack. Subtle and
|
|
162
|
+
worth it: an unlisted assignment leaks past the `POP`, which is the one
|
|
163
|
+
thing the block was there to stop.
|
|
164
|
+
- [x] **CallbackFunctionSignature** / **SubstitutionCallbackSignature** —
|
|
165
|
+
`FunctionArity` inverted: check *definitions* against a table of callback
|
|
166
|
+
names and required arities. `check_subst_args(func, 3)` at `dosubst.c:372`.
|
|
167
|
+
- [x] **WarnSequenceNotDecreasing** / **SchedSequenceNotIncreasing** — literal
|
|
168
|
+
`choose()` sequences only.
|
|
169
|
+
- [x] **EasterdateFromToday** — pattern match on `easterdate(today())` in a
|
|
170
|
+
trigger carrying an offset. Narrow, exact, high value.
|
|
171
|
+
- [x] **SatisfyConstraintNotHoisted** — the strongest single rule in the set and
|
|
172
|
+
the only one with measured numbers behind it: 13.2M evaluations → 482K,
|
|
173
|
+
2.30s → 0.58s. Narrow literal case only (`SATISFY [$Td == 13]` → move the
|
|
174
|
+
13 into the trigger).
|
|
175
|
+
- [x] **UnsatisfiableSatisfy** — needs a small range check over trigger
|
|
176
|
+
components. `$Td == 100` is decidable.
|
|
177
|
+
- [x] **TriggerReuseWithoutScanfrom** — `$T` dataflow between adjacent `REM`s.
|
|
178
|
+
- [x] **MaxSatIterAbsurd**, **SatisfyBoundedByYear**
|
|
179
|
+
- [x] **IncludeRelativePath** — resolves against the working directory, not the
|
|
180
|
+
including file. Works from one directory, fails from another.
|
|
181
|
+
- [x] **SysIncludeAbsolutePath**, **IncludeTargetMissing** (literal paths only)
|
|
182
|
+
- [x] **AddOmitWithoutScanfrom** / **ScanfromTooShort** — the 28-day figure is
|
|
183
|
+
sourced (ch. 4 line 174); the 7-day one for monthly looks extrapolated and
|
|
184
|
+
should be checked before it ships.
|
|
185
|
+
- [x] **RecursionDepthExceeded** — `MAX_RECURSION_LEVEL 1000` (`custom.h:147`).
|
|
186
|
+
Only decidable for literal arguments to simple recursive functions. Real
|
|
187
|
+
work for a rare bug; near the bottom.
|
|
188
|
+
- [x] **MoonriseDatePartUnchecked**, **HebrewMonthName**,
|
|
189
|
+
**HebrewDateNeedsLeftToRightMark**, **MsgsuffixLeadingBackspace**
|
|
190
|
+
- [x] **WorldWritableScript** — a file-mode check rather than a content one, but
|
|
191
|
+
it fits: Remind refuses a world-writable script outright.
|
|
192
|
+
- [x] **QueuedComputedTimeWithoutNoqueue**, **RunOnInIncludedFile**
|
|
193
|
+
|
|
194
|
+
## Tier 5 — style, off by default — DONE, or decided
|
|
195
|
+
|
|
196
|
+
- [x] **AdvanceWarningWithoutRelativeSubstitution** + **CalendarTextNotLimited**
|
|
197
|
+
— the strongest *pair*: one asks for `%b`, the other asks you to fence it
|
|
198
|
+
in `%"…%"`. Will fire a lot. Opt-in.
|
|
199
|
+
- [x] **TimedReminderWithoutTimeSubstitution** — same family, one axis over.
|
|
200
|
+
Note Remind already warns on `%1`–`%9` without `AT` at warning level
|
|
201
|
+
05.03.04 (`dosubst.c:474`), so scope this to the complement.
|
|
202
|
+
- [x] **BackSugarAvailable** — `REM 1 --1` and `REM ~~1` are the same reminder.
|
|
203
|
+
- [x] **OmitAwareDeltaWithoutOmits** — with an empty omit context `+n` is exactly
|
|
204
|
+
`++n`, so the single sign is a typo or a missing `OMIT`.
|
|
205
|
+
- [x] **WeekdayDayTriggerConfusion** — demoted to informational. `REM Friday 13`
|
|
206
|
+
is the classic misunderstanding but occasionally what someone wants.
|
|
207
|
+
- [x] **SatisfyEvaluatedInForeignZone** — informational; correct, documented and
|
|
208
|
+
reliably surprising.
|
|
209
|
+
- [x] **HardCodedColorsWithoutPsCalGuard**, **CalKeywordVersusEmptyCalendarText**
|
|
210
|
+
- [x] **TkTagNamespace**, **GeneratedFileEdited**, **ConvertedFileHandEdited**,
|
|
211
|
+
**RemindOptionsInterfereWithServerMode**
|
|
212
|
+
- [x] **DebugCommandsCommitted**, **SystemVariableVersusTranslate**
|
|
213
|
+
- [x] **LangidNotTranslated**, **SubstitutionCallbackUnknownSequence**
|
|
214
|
+
- [x] **TodoWithoutCompleteThrough**, **RecurrenceNotExportable**
|
|
215
|
+
- [x] **UnknownTimeZoneName** + **TimeZoneNameCaseOnlyMismatch** — the second
|
|
216
|
+
turns the first's shrug into an actionable message by naming the zone that
|
|
217
|
+
was probably meant.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Blocked on a decision
|
|
222
|
+
|
|
223
|
+
**Three rules need the invocation, which is not in the file.**
|
|
224
|
+
`SortOptionInScriptComment` (`-g`), `InfoHeadersNeedDashPP` (`-pp` vs `-p`) and
|
|
225
|
+
`TodoOutsideAgendaMode` (agenda vs calendar) all depend on how the file is run.
|
|
226
|
+
They share one prerequisite:
|
|
227
|
+
|
|
228
|
+
```remind
|
|
229
|
+
# remlint:invocation remind -pp -g
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Build that once and all three work. Skip it and all three are guesswork. Decide
|
|
233
|
+
deliberately rather than discovering it three times.
|
|
234
|
+
|
|
235
|
+
**Five need to see past one file.** `UndefinedVariable`,
|
|
236
|
+
`CallbackFunctionMissing`, `AstronomyWithoutLocation`, `RunOnInIncludedFile`,
|
|
237
|
+
`FeatureNewerThanTargetVersion`. `FunctionArity` already stays silent on unknown
|
|
238
|
+
functions because helpers arrive through `INCLUDE`; `UndefinedVariable` is the
|
|
239
|
+
same problem one axis over and worse, since variables outnumber calls. Following
|
|
240
|
+
`INCLUDE` is an architecture change, and `INCLUDECMD` *executes*, so it cannot be
|
|
241
|
+
followed safely at all. The cheap answer is the `AllowedNames` config already
|
|
242
|
+
built for `UnknownSystemVariable`: declare what arrives from outside, lint the
|
|
243
|
+
rest.
|
|
244
|
+
|
|
245
|
+
**FeatureNewerThanTargetVersion needs a column the generator cannot produce.**
|
|
246
|
+
It wants "introduced in which version" for every keyword, function and system
|
|
247
|
+
variable. `tasks/generate_tables.rb` reads one checkout, so it cannot know.
|
|
248
|
+
Either walk the git history of `token.c`/`funcs.c`/`var.c`, or carry a
|
|
249
|
+
hand-maintained since-map that will drift. A design decision, not a rule.
|
|
250
|
+
|
|
251
|
+
**SyntaxRuleNeedsRemind is a linter self-diagnostic, not a per-file offence** —
|
|
252
|
+
but it points at a real gap. The `Syntax` rule currently skips silently when
|
|
253
|
+
`remind` is not on PATH, which means CI can believe it is syntax-checking when
|
|
254
|
+
it is not. That belongs as one warning from the runner.
|
|
255
|
+
|
|
256
|
+
## Cut
|
|
257
|
+
|
|
258
|
+
- **RecurringReminderIgnoresOmits** — its own description concedes the default
|
|
259
|
+
"is right for most reminders". A rule that is wrong most of the time teaches
|
|
260
|
+
people to ignore the linter.
|
|
261
|
+
- **ScriptFileExtension** — would fire on `examples/astro` and
|
|
262
|
+
`examples/ansitext`, the two files the extractor layer exists to handle.
|
|
263
|
+
- **SecretCalendarUrl** — secret detection is its own discipline with its own
|
|
264
|
+
false-positive economics. `trufflehog` already does it.
|
|
265
|
+
- **HebrewSunsetOffset** — "Recording the idea here so the next reader does not
|
|
266
|
+
re-derive it" is an honest note, and undetectable. Keep it as a comment in the
|
|
267
|
+
chapter, not a rule.
|
|
268
|
+
|
|
269
|
+
## A note on sourcing
|
|
270
|
+
|
|
271
|
+
The book's licence permits verbatim personal copies and expressly reserves the
|
|
272
|
+
work from text-and-data-mining. Some of the rule descriptions quote it directly
|
|
273
|
+
("Verbatim from the book: …"). Rule doc comments in this gem must not carry that
|
|
274
|
+
prose: cite chapter and section, paraphrase the mechanism, and verify the
|
|
275
|
+
constant against the C source — which is GPL-2.0 and quotable — so a GPL gem is
|
|
276
|
+
not redistributing a book that forbids redistribution.
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
# Final disposition
|
|
283
|
+
|
|
284
|
+
**53 rules, from 100 ideas. Nothing is deferred.** Every idea is either built,
|
|
285
|
+
merged into a rule that covers it, or deleted with the reason recorded.
|
|
286
|
+
|
|
287
|
+
## Deleted, and why
|
|
288
|
+
|
|
289
|
+
Each of these was investigated to the point where a decision could be made.
|
|
290
|
+
None is "not done yet".
|
|
291
|
+
|
|
292
|
+
**Disproven — the premise is false.** These were built, run against the corpus,
|
|
293
|
+
and removed when Remind disagreed:
|
|
294
|
+
|
|
295
|
+
- **CalKeywordVersusEmptyCalendarText** — a `CAL` whose whole body is fenced in
|
|
296
|
+
`%"…%"` shows in the calendar perfectly well. Checked by rendering one:
|
|
297
|
+
`REM 1 Jan CAL %"Fenced%"` and `REM 2 Jan CAL Plain` both appear.
|
|
298
|
+
- **MsgsuffixLeadingBackspace** — `examples/defs.rem` writes
|
|
299
|
+
`FSET msgsuffix(x) char(13,10)+"***"+char(13,10,13,10)`, which wants its own
|
|
300
|
+
lines. `char(8)` is one way to write a suffix, not the only correct one.
|
|
301
|
+
- **SubstitutionCallbackUnknownSequence** — the `x` variants are *not* limited
|
|
302
|
+
to the today/tomorrow family. `FSET subst_sx(a,d,t)` with `%*s` prints the
|
|
303
|
+
callback's result, so `include/lang/ca.rem`'s `subst_sx` is live code.
|
|
304
|
+
|
|
305
|
+
**Undecidable — the information does not exist.**
|
|
306
|
+
|
|
307
|
+
- **FeatureNewerThanTargetVersion** — needs "introduced in which version" per
|
|
308
|
+
keyword, function and system variable. The man page carries five such
|
|
309
|
+
annotations in total and the tree has no history to mine. There is no source
|
|
310
|
+
for the column.
|
|
311
|
+
- **IntConstantOutOfRange** — the bound is the build's `int`, which the linter
|
|
312
|
+
cannot know and which `configure` may change.
|
|
313
|
+
- **RecursionDepthExceeded** — bounding recursion depth from a literal argument
|
|
314
|
+
requires evaluating the function.
|
|
315
|
+
- **HebrewDateNeedsLeftToRightMark** — whether a bidi mark is needed depends on
|
|
316
|
+
what follows the month name at render time.
|
|
317
|
+
- **SatisfyEvaluatedInForeignZone** — correct, documented, and has no defect to
|
|
318
|
+
report. An informational note with no fix is documentation.
|
|
319
|
+
- **RemindOptionsInterfereWithServerMode** — which options interfere is
|
|
320
|
+
TkRemind's business and is not written down anywhere the linter can read.
|
|
321
|
+
- **RunOnInIncludedFile** — needs to know whether a file is top-level, which
|
|
322
|
+
depends on who included it.
|
|
323
|
+
- **RecurrenceNotExportable** — which recurrences `rem2ics` can express is
|
|
324
|
+
rem2ics's business, and it is not in this tree.
|
|
325
|
+
|
|
326
|
+
**Wrong by construction.**
|
|
327
|
+
|
|
328
|
+
- **RecurringReminderIgnoresOmits** — wrong most of the time by its own
|
|
329
|
+
description.
|
|
330
|
+
- **ScriptFileExtension** — would fire on the two files the extractor exists for.
|
|
331
|
+
- **SecretCalendarUrl** — secret detection is its own discipline with its own
|
|
332
|
+
false-positive economics; `trufflehog` does it.
|
|
333
|
+
- **HebrewSunsetOffset** — an honest note, and undetectable.
|
|
334
|
+
- **IncludeTargetMissing** — `INCLUDE` resolves against the *working directory*,
|
|
335
|
+
which the linter does not know at lint time, so a missing target cannot be
|
|
336
|
+
distinguished from a different working directory. `IncludePath` reports the
|
|
337
|
+
hazard that causes it instead.
|
|
338
|
+
- **TildeBackWithDayComponent**, **WeekdayDayTriggerConfusion**,
|
|
339
|
+
**SystemVariableVersusTranslate**, **HardCodedColorsWithoutPsCalGuard**,
|
|
340
|
+
**QueuedComputedTimeWithoutNoqueue**, **BackSugarAvailable**,
|
|
341
|
+
**WarnSequenceNotDecreasing**, **SchedSequenceNotIncreasing**,
|
|
342
|
+
**TriggerReuseWithoutScanfrom**, **MoonriseDatePartUnchecked**,
|
|
343
|
+
**ConvertedFileHandEdited** — each is a real observation, and each reduces to
|
|
344
|
+
a preference, a shape too narrow to be worth a rule, or something a built rule
|
|
345
|
+
already covers. `GeneratedFileEdited` covers the last with a configured glob;
|
|
346
|
+
`RepeatTrigger`'s weekday check covers the stronger half of the weekday pair.
|
|
347
|
+
|
|
348
|
+
**Relocated.**
|
|
349
|
+
|
|
350
|
+
- **SyntaxRuleNeedsRemind** — built, but as a *runner* diagnostic rather than a
|
|
351
|
+
per-file offence: `remlint` now prints
|
|
352
|
+
`remlint: the Syntax rule is enabled but ... is not on PATH, so no file was
|
|
353
|
+
syntax-checked` once per run. Silently not syntax-checking is worse than not
|
|
354
|
+
syntax-checking.
|
|
355
|
+
- **SortOptionInScriptComment**, **InfoHeadersNeedDashPP**,
|
|
356
|
+
**TodoOutsideAgendaMode** — all three needed the invocation, which is not in
|
|
357
|
+
the file. `Invocation` and the `# remlint:invocation` declaration were built,
|
|
358
|
+
and `InvocationMismatch` covers all three.
|
|
359
|
+
|
|
360
|
+
## What the corpus was worth
|
|
361
|
+
|
|
362
|
+
Fifteen premises did not survive contact with the source, the man page, the
|
|
363
|
+
corpus or a running binary. Not one was caught by reasoning.
|
|
364
|
+
|
|
365
|
+
| Premise | What is actually true |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| `%z` is a typo | every letter and digit is a real sequence |
|
|
368
|
+
| a trailing `%` substitutes nothing | it suppresses the appended newline |
|
|
369
|
+
| `DURATION` is a time of day | it is a length; `DURATION 48:45` is fine |
|
|
370
|
+
| `AT` is the only source of a time | a pasted DATETIME supplies one |
|
|
371
|
+
| `UntilBeforeFrom` is silent | Remind warns, and `SCANFROM` has its own message |
|
|
372
|
+
| `UNTIL` on or before `FROM` | equal dates are a legal one-day window |
|
|
373
|
+
| `THROUGH` always needs a full date | not inside `OMIT` |
|
|
374
|
+
| the `SPECIAL` set is six names | `PostScript`, `PSFile` and `PS` are in it |
|
|
375
|
+
| any `subst_` name is a callback | only single-char overrides and `%{name}` |
|
|
376
|
+
| a narrow `SCANFROM` is a bug | a smaller guarantee; `defs.rem` uses `-7` |
|
|
377
|
+
| Hebrew months have fixed lengths | Heshvan and Kislev vary with the year |
|
|
378
|
+
| a fenced `CAL` shows nowhere | it shows in the calendar |
|
|
379
|
+
| `msgsuffix` must start with `char(8)` | `char(13,10)` is a valid choice |
|
|
380
|
+
| Hebrew months have one spelling each | three tables: canonical, Ivrit, alternates |
|
|
381
|
+
| a TODO with no `COMPLETE-THROUGH` screams | it goes **silent**, and only with `MAX-OVERDUE` |
|
|
382
|
+
|
|
383
|
+
Two of my own bugs came from the same place: `Trigger` reading the English word
|
|
384
|
+
"run" in an `ERRMSG` as a shell body, and `%"` counted as injectable data.
|
|
385
|
+
|
|
386
|
+
## Corpus state
|
|
387
|
+
|
|
388
|
+
`examples/`, `include/` (584 files) and `contrib/` are **clean**. `tests/`
|
|
389
|
+
reports 84, all verified by hand, many on lines that suite labels "Should
|
|
390
|
+
fail", "Bad:" or "Diagnosed".
|
data/docs/remlint.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# RemLint
|
|
2
|
+
|
|
3
|
+
A style and consistency linter for [Remind](https://dianne.skoll.ca/projects/remind/)
|
|
4
|
+
reminder files.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
$ remlint examples tests
|
|
8
|
+
tests/if1.rem:3:1: error: [UnbalancedBlocks] `IF` is never closed by `ENDIF`
|
|
9
|
+
tests/test.rem:1146:9: error: [FunctionArity] `version` takes 0 arguments, given 1
|
|
10
|
+
tests/test.rem:592:6: error: [UnknownSystemVariable] `$aaaa…` is not a Remind system variable
|
|
11
|
+
3 offences (3 errors)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why it is shaped like this
|
|
15
|
+
|
|
16
|
+
**Remind is not Ruby, so RuboCop's machinery does not apply.** RuboCop's whole
|
|
17
|
+
pipeline assumes a `parser`/Prism AST, and its one escape hatch —
|
|
18
|
+
`RuboCop::Runner.ruby_extractors` — exists to pull *Ruby* back out of ERB and
|
|
19
|
+
Haml templates. Neither helps here.
|
|
20
|
+
|
|
21
|
+
The precedent that does apply is **puppet-lint**: a linter written in Ruby for a
|
|
22
|
+
language that is not Ruby. Including where puppet-lint draws its line — it
|
|
23
|
+
validates style, and leaves "is this even valid" to the real parser. RemLint
|
|
24
|
+
does the same. `Syntax` shells out to Remind itself, and is off by default,
|
|
25
|
+
because running the file is what checking it costs.
|
|
26
|
+
|
|
27
|
+
**Remind's grammar decides the architecture.** The manual describes a reminder
|
|
28
|
+
file as a list of commands, one per line, with backslash continuation; comments
|
|
29
|
+
open with `#` or `;`; keywords are case-insensitive and abbreviable; and the
|
|
30
|
+
`REM` that opens a trigger may be left off entirely. That is line-oriented, not
|
|
31
|
+
tree-oriented. So there is no grammar here — there is a pipeline:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
bytes → sources → logical lines → commands → tokens → rules → offences
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Stage | File | What it settles |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| sources | `extractors.rb` | which parts of a file are Remind, and at what line offset |
|
|
40
|
+
| logical lines | `logical_line.rb` | where backslash continuations join, and where they only look like they do |
|
|
41
|
+
| commands | `command.rb` | which keyword opens a line, and whether one does at all |
|
|
42
|
+
| tokens | `expr_lexer.rb` | where the strings, brackets, `$SysVars` and calls are |
|
|
43
|
+
| rules | `rules/*.rb` | one question each, over whichever view above is narrowest |
|
|
44
|
+
|
|
45
|
+
**The vocabulary is generated, not remembered.** Every keyword, its minimum
|
|
46
|
+
abbreviation length, every function's argument count, every system variable's
|
|
47
|
+
writability and range comes from Remind's own dispatch tables —
|
|
48
|
+
`src/token.c`, `src/funcs.c`, `src/var.c` — transcribed into `lib/remlint/tables.rb`
|
|
49
|
+
by `tasks/generate_tables.rb`. So `INC` resolves to `INCLUDE` and `OMI` resolves
|
|
50
|
+
to nothing for the same reason Remind says so, and `ampm` takes one to four
|
|
51
|
+
arguments because that is the row in Remind's table.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
$ rake tables[/path/to/remind]
|
|
55
|
+
lib/remlint/tables.rb: 91 keywords, 145 functions, 126 system variables
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**It reads Remind out of shell scripts.** `examples/ansitext` and
|
|
59
|
+
`examples/astro` are shell scripts that pipe Remind in through heredocs —
|
|
60
|
+
`astro` has four of them — and a linter that only globbed `*.rem` would miss
|
|
61
|
+
most of the Remind in that directory. Line numbers are reported against the
|
|
62
|
+
enclosing file, so an offence in `astro`'s third heredoc points at the real line
|
|
63
|
+
of `astro`.
|
|
64
|
+
|
|
65
|
+
## Rules
|
|
66
|
+
|
|
67
|
+
53 rules. `remlint --show-rules` lists them with their current state; each
|
|
68
|
+
carries its reasoning, and the file and line of the C that settles it, in a
|
|
69
|
+
comment at the top of `lib/remlint/rules/`.
|
|
70
|
+
|
|
71
|
+
**On by default** — each reports something Remind itself rejects, that breaks
|
|
72
|
+
its output, or that fails silently on a day months away:
|
|
73
|
+
|
|
74
|
+
`TrailingWhitespace` · `DanglingContinuation` · `UnbalancedBlocks` ·
|
|
75
|
+
`UnbalancedDelimiters` · `FunctionArity` · `UnknownSystemVariable` ·
|
|
76
|
+
`SystemVariableAssignment` · `ColorComponentRange` · `UnquotedShellSubstitution` ·
|
|
77
|
+
`ClauseRequiresAt` · `IftrigWithSatisfy` · `UnknownSubstitutionSequence` ·
|
|
78
|
+
`InfoSubstitutionWithoutHeader` · `TextAfterEofMarker` · `UntilBeforeFrom` ·
|
|
79
|
+
`CoordinateNotString` · `ShellUseWhileRunDisabled` · `LiteralTypeMismatch` ·
|
|
80
|
+
`ClauseValueRange` · `DateOutOfRange` · `ClauseNeedsFullDate` · `RepeatTrigger` ·
|
|
81
|
+
`InfoClause` · `TagSyntax` · `BannerPlacement` · `UnknownSpecialType` ·
|
|
82
|
+
`StringEscape` · `DebugCommand` · `ShellMaxlen` · `FunctionRedefinition` ·
|
|
83
|
+
`PushVarsMissingName` · `CallbackSignature` · `IncludePath` ·
|
|
84
|
+
`EasterdateFromToday` · `TimeZoneName` · `WorldWritableScript` ·
|
|
85
|
+
`TodoCompleteThrough` · `AddomitWithoutScanfrom` · `TranslateCommand` ·
|
|
86
|
+
`HebrewDate` · `MoonPhaseArgument` · `TkTagNamespace` · `InvocationMismatch` ·
|
|
87
|
+
`LocalizationPack` · `GeneratedFileEdited`
|
|
88
|
+
|
|
89
|
+
**Off by default** — house style, one performance rule, and the one that runs
|
|
90
|
+
the file:
|
|
91
|
+
|
|
92
|
+
`KeywordCase` · `LineLength` · `LicenseHeader` · `SatisfyConstraint` ·
|
|
93
|
+
`AdvanceWarningBody` · `CalendarTextLimited` · `OmitAwareDelta` · `Syntax`
|
|
94
|
+
|
|
95
|
+
`Syntax` is off deliberately: Remind has no parse-only mode, so it runs the
|
|
96
|
+
file. `-r` disables `RUN`; `INCLUDECMD` still executes.
|
|
97
|
+
|
|
98
|
+
### Where the rules deliberately stay quiet
|
|
99
|
+
|
|
100
|
+
A linter earns its output by what it does *not* say. The recurring principle:
|
|
101
|
+
**where the linter cannot know, it says nothing.**
|
|
102
|
+
|
|
103
|
+
- A closing bracket with nothing open is text, not an error — `MSG See note ]`
|
|
104
|
+
prints a bracket.
|
|
105
|
+
- Parentheses count only inside `[...]` or an expression command; `MSG Call
|
|
106
|
+
(555) 1234` is text.
|
|
107
|
+
- Unknown functions are not reported: a file's helpers usually arrive through
|
|
108
|
+
`INCLUDE`, which one-file linting cannot see.
|
|
109
|
+
- A trigger carrying a bracketed expression suppresses the clause rules that
|
|
110
|
+
depend on knowing what it evaluates to.
|
|
111
|
+
|
|
112
|
+
### Declaring how a file is run
|
|
113
|
+
|
|
114
|
+
Three checks need the command line rather than the file. Declare it once and
|
|
115
|
+
they work; leave it out and they stay silent.
|
|
116
|
+
|
|
117
|
+
```remind
|
|
118
|
+
# remlint:invocation remind -pp -g /path/to/file
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`InvocationMismatch` then reports an unreadable `-g` sort spec, `INFO` under a
|
|
122
|
+
plain `-p` that will not carry it, and `TODO` under a calendar invocation where
|
|
123
|
+
its semantics do not exist.
|
|
124
|
+
|
|
125
|
+
`RULES.md` records the full backlog: every one of the 100 ideas built, merged or
|
|
126
|
+
deleted with its reason, and the thirteen book premises the source and corpus
|
|
127
|
+
corrected.
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
`.remlint.yml`, found by walking up from the working directory, merged over the
|
|
132
|
+
shipped defaults **key by key** — so setting one option leaves the rest alone.
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
TrailingWhitespace:
|
|
136
|
+
Severity: error
|
|
137
|
+
|
|
138
|
+
KeywordCase:
|
|
139
|
+
Enabled: true
|
|
140
|
+
EnforcedStyle: consistent # upper, lower, or the file's own majority
|
|
141
|
+
|
|
142
|
+
UnknownSystemVariable:
|
|
143
|
+
AllowedNames: [Latitude, Longitude] # fed in with remind -i$Name=...
|
|
144
|
+
|
|
145
|
+
Exclude:
|
|
146
|
+
- "examples/tflag.rem"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`remlint --show-config` prints what is actually in effect.
|
|
150
|
+
|
|
151
|
+
### Silencing one line
|
|
152
|
+
|
|
153
|
+
```remind
|
|
154
|
+
MSG trailing spaces here # remlint:disable TrailingWhitespace
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```remind
|
|
158
|
+
# remlint:disable TrailingWhitespace, UnbalancedBlocks
|
|
159
|
+
IF something
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A directive covers the line it is on and the line below it. `all` covers every
|
|
163
|
+
rule. Both comment characters work.
|
|
164
|
+
|
|
165
|
+
## Command line
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
remlint [options] [path...]
|
|
169
|
+
|
|
170
|
+
-c, --config PATH Configuration file (default: nearest .remlint.yml)
|
|
171
|
+
-o, --only RULES Run only these rules
|
|
172
|
+
-f, --format TEMPLATE Output template
|
|
173
|
+
--fail-level LEVEL Exit non-zero at this severity or worse (default: warning)
|
|
174
|
+
--show-rules List the rules and whether they are on
|
|
175
|
+
--show-config Print the configuration in effect
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
With no paths, the working directory. Exit status is 0 when nothing at or above
|
|
179
|
+
`--fail-level` was found, 1 when something was, and 2 for a usage error.
|
|
180
|
+
|
|
181
|
+
The default output line — `path:line:column: severity: [Rule] message` — is what
|
|
182
|
+
vim's default `errorformat` reads. `--format` takes the same `%{...}`
|
|
183
|
+
placeholders puppet-lint uses:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
$ remlint --format '::error file=%{path},line=%{line}::%{message}' .
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
Tests live in each file's `__END__` section and run under
|
|
192
|
+
[scampi](https://github.com/general-intelligence-systems/scampi): the specs
|
|
193
|
+
never load in production, because Ruby stops parsing at `__END__`.
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
$ bundle install
|
|
197
|
+
$ rake # tests, then the house cops
|
|
198
|
+
$ rake test
|
|
199
|
+
$ rake rubocop
|
|
200
|
+
$ rake smoke[/path/to/remind] # lint Remind's own corpus
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The linter's own Ruby is checked by the custom cops in `cops/`, configured in
|
|
204
|
+
`.rubocop.yml`.
|
|
205
|
+
|
|
206
|
+
### The corpus is the real test
|
|
207
|
+
|
|
208
|
+
RuboCop's advice for a new cop applies verbatim: run it over a significant
|
|
209
|
+
codebase. Remind ships one, and it earned its keep — **fifteen** premises are
|
|
210
|
+
narrower than they started, or gone entirely, because the source, the man page
|
|
211
|
+
or a running binary said so; two of the linter's own bugs surfaced the same way;
|
|
212
|
+
and three rules were built, run, disproven and deleted.
|
|
213
|
+
|
|
214
|
+
On the current release `remlint` is clean on `examples/`, `include/` (584 files)
|
|
215
|
+
and `contrib/`, and reports 84 offences in `tests/` — all verified by hand, many
|
|
216
|
+
on lines that suite labels "Should fail", "Bad:" or "Diagnosed".
|
|
217
|
+
|
|
218
|
+
A few of the corrections, as a flavour of what a corpus is for:
|
|
219
|
+
|
|
220
|
+
- **A trailing `%` is documented**, not a stray. It suppresses the newline
|
|
221
|
+
Remind would otherwise append: `dosubst("hello")` is six characters and
|
|
222
|
+
`dosubst("hello%")` is five. Flagging it produced 209 false positives.
|
|
223
|
+
- **`DURATION` is a length, not a time of day.** `tests/test3.rem` writes
|
|
224
|
+
`DURATION 48:45` for an event running over two days, and Remind accepts it.
|
|
225
|
+
- **`AT` is not the only source of a time.** `include/lunar-eclipses.rem`
|
|
226
|
+
supplies one from a pasted `[utctolocal(...)]` DATETIME, 142 times.
|
|
227
|
+
- **Only some `subst_` names are callbacks.** Remind builds `subst_<c>` for a
|
|
228
|
+
*single* character, so `subst_a` is one and `subst_a_alt` is an ordinary
|
|
229
|
+
helper — and `include/lang/` is full of helpers. 45 false positives.
|
|
230
|
+
- **`errmsg Please run [filename()] ...`** in `tests/tstlang.rem` made the
|
|
231
|
+
trigger parser read the English "run" as the start of a shell body, and then
|
|
232
|
+
report the rest of the sentence as command injection.
|
|
233
|
+
|
|
234
|
+
`RULES.md` has the full list.
|
|
235
|
+
|
|
236
|
+
## Licence
|
|
237
|
+
|
|
238
|
+
GPL-2.0-only, as Remind is.
|