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,175 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Regenerates lib/remlint/tables.rb from the Remind C sources.
5
+ #
6
+ # Every fact the linter asserts about Remind's vocabulary -- which keywords
7
+ # exist, how far each may be abbreviated, which functions take how many
8
+ # arguments, which system variables are writable and over what range -- is
9
+ # transcribed from the tables Remind itself dispatches on:
10
+ #
11
+ # src/token.c TokArray[] keyword, minimum abbreviation length, type
12
+ # src/funcs.c Func[] builtin name, min args, max args
13
+ # src/var.c SysVarArr[] $SysVar name, modifiable flag, type, min, max
14
+ #
15
+ # Hand-maintaining that vocabulary would guarantee drift the first time Remind
16
+ # gains a function. Run this against a Remind checkout instead:
17
+ #
18
+ # ruby tasks/generate_tables.rb /path/to/remind
19
+ #
20
+ # The generated file is committed, so the gem builds and tests without a
21
+ # Remind checkout present.
22
+
23
+ require "pathname"
24
+
25
+ SOURCE_ROOT = Pathname.new(ARGV.fetch(0, "..")).expand_path
26
+ OUTPUT = Pathname.new(__dir__).join("../lib/remlint/tables.rb").cleanpath
27
+
28
+ # `{ "banner", 3, T_Banner, 0 },` -- name, minimum abbreviation length, type.
29
+ TOKEN_ROW = /^\s*\{\s*"([a-z0-9-]+)"\s*,\s*(\d+)\s*,\s*(T_\w+)\s*,/
30
+
31
+ # `{ "ampm", 1, 4, 1, FAmpm, NULL },` -- name, min args, max args.
32
+ # NO_MAX_ARGS (127) is Remind's "variadic" sentinel.
33
+ FUNC_ROW = /^\s*\{\s*"([a-z0-9_]+)"\s*,\s*(\d+|NO_MAX_ARGS)\s*,\s*(\d+|NO_MAX_ARGS)\s*,/
34
+
35
+ # `{"FormWidth", 1, INT_TYPE, &FormWidth, 20, 500 },`
36
+ # -- name, modifiable, type, value, min, max. The min/max columns only bound
37
+ # anything for INT_TYPE; for the other types they are 0/0 filler.
38
+ SYSVAR_ROW = /^\s*\{\s*"(\w+)"\s*,\s*([01])\s*,\s*(\w+)\s*,\s*[^,]+,\s*([^,]+),\s*([^}]+)\}/
39
+
40
+ NO_MAX_ARGS = 127
41
+
42
+ def read_source(relative)
43
+ path = SOURCE_ROOT.join(relative)
44
+
45
+ unless path.exist?
46
+ abort "#{path} not found; pass the root of a Remind checkout as ARGV[0]"
47
+ end
48
+
49
+ path.read
50
+ end
51
+
52
+ def scan(relative, pattern)
53
+ read_source(relative).each_line.filter_map do |line|
54
+ match = line.match(pattern)
55
+
56
+ if match
57
+ yield match
58
+ end
59
+ end
60
+ end
61
+
62
+ def parse_arg_count(text)
63
+ if text == "NO_MAX_ARGS"
64
+ NO_MAX_ARGS
65
+ else
66
+ Integer(text)
67
+ end
68
+ end
69
+
70
+ # Only INT_TYPE variables carry a real range, and only when the two columns
71
+ # actually differ -- `0, 0` is Remind's filler, not a variable pinned to zero.
72
+ def parse_bounds(type, min, max)
73
+ if type == "INT_TYPE" && min.strip.match?(/\A-?\d+\z/) && max.strip.match?(/\A-?\d+\z/) && min.strip != max.strip
74
+ [Integer(min.strip), Integer(max.strip)]
75
+ else
76
+ [nil, nil]
77
+ end
78
+ end
79
+
80
+ def literal(value)
81
+ case value
82
+ when nil then "nil"
83
+ when String then value.inspect
84
+ else value.to_s
85
+ end
86
+ end
87
+
88
+ def render_rows(rows, columns)
89
+ widths = columns.to_h do |column|
90
+ [column, rows.map { |row| literal(row[column]).length }.max]
91
+ end
92
+
93
+ rows.map do |row|
94
+ cells = columns.map do |column|
95
+ literal(row[column]).ljust(widths.fetch(column))
96
+ end
97
+
98
+ " [#{cells.join(', ')}],"
99
+ end
100
+ end
101
+
102
+ keywords = scan("src/token.c", TOKEN_ROW) do |match|
103
+ {
104
+ name: match[1].upcase,
105
+ minlen: Integer(match[2]),
106
+ type: match[3],
107
+ }
108
+ end
109
+
110
+ functions = scan("src/funcs.c", FUNC_ROW) do |match|
111
+ {
112
+ name: match[1],
113
+ min: parse_arg_count(match[2]),
114
+ max: parse_arg_count(match[3]),
115
+ }
116
+ end
117
+
118
+ sysvars = scan("src/var.c", SYSVAR_ROW) do |match|
119
+ min, max = parse_bounds(match[3], match[4], match[5])
120
+
121
+ {
122
+ name: match[1],
123
+ modifiable: match[2] == "1",
124
+ type: match[3],
125
+ min: min,
126
+ max: max,
127
+ }
128
+ end
129
+
130
+ # src/version.h.in holds an unsubstituted @VERSION@ placeholder in a source
131
+ # tree that has not been configured, so take the number from its origin.
132
+ version = read_source("configure.ac")[/AC_INIT\(\s*remind\s*,\s*([^,\s)]+)/, 1] || "unknown"
133
+
134
+ body = <<~GENERATED
135
+ # frozen_string_literal: true
136
+
137
+ # GENERATED FILE -- do not edit by hand.
138
+ #
139
+ # Transcribed from Remind #{version} by tasks/generate_tables.rb, which reads
140
+ # the same tables the interpreter dispatches on (src/token.c, src/funcs.c,
141
+ # src/var.c). Regenerate rather than patch:
142
+ #
143
+ # ruby tasks/generate_tables.rb /path/to/remind
144
+ module RemLint
145
+ # Remind's vocabulary, exactly as the interpreter knows it.
146
+ module Tables
147
+ # Remind's variadic sentinel: `char(...)` and friends take any number of
148
+ # arguments above their minimum.
149
+ NO_MAX_ARGS = #{NO_MAX_ARGS}
150
+
151
+ # [keyword, minimum abbreviation length, token type]. Remind accepts any
152
+ # prefix at least `minlen` characters long, which is why `INC` is
153
+ # `INCLUDE` and `SCAN` is `SCANFROM`.
154
+ KEYWORD_ROWS = [
155
+ #{render_rows(keywords, %i[name minlen type]).join("\n")}
156
+ ].freeze
157
+
158
+ # [name, minimum arguments, maximum arguments].
159
+ FUNCTION_ROWS = [
160
+ #{render_rows(functions, %i[name min max]).join("\n")}
161
+ ].freeze
162
+
163
+ # [name, modifiable, type, minimum, maximum]. Non-modifiable variables
164
+ # are readable but reject `SET`; the bounds are populated only for the
165
+ # integer-valued variables Remind range-checks.
166
+ SYSVAR_ROWS = [
167
+ #{render_rows(sysvars, %i[name modifiable type min max]).join("\n")}
168
+ ].freeze
169
+ end
170
+ end
171
+ GENERATED
172
+
173
+ OUTPUT.write(body)
174
+
175
+ puts "#{OUTPUT}: #{keywords.size} keywords, #{functions.size} functions, #{sysvars.size} system variables"
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remlint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Kidd
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.88'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.88'
40
+ - !ruby/object:Gem::Dependency
41
+ name: scampi
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ description: |
55
+ RemLint checks Remind reminder files for style, consistency and structure:
56
+ unbalanced IF/ENDIF blocks, mistyped $SysVars, wrong argument counts,
57
+ continuations that do not continue, and trailing whitespace that breaks
58
+ rem2ps. Its vocabulary is transcribed from Remind's own dispatch tables, so
59
+ keyword abbreviations and function arities match the interpreter exactly.
60
+
61
+ It reads Remind out of shell heredocs as well as .rem files, and reports
62
+ line numbers in the enclosing file.
63
+ email: nathanblenheimkidd@gmail.com
64
+ executables:
65
+ - remlint
66
+ extensions: []
67
+ extra_rdoc_files:
68
+ - docs/remlint.md
69
+ files:
70
+ - LICENSE
71
+ - config/default.yml
72
+ - docs/remlint-rules.md
73
+ - docs/remlint.md
74
+ - exe/remlint
75
+ - lib/remlint.rb
76
+ - lib/remlint/cli.rb
77
+ - lib/remlint/command.rb
78
+ - lib/remlint/config.rb
79
+ - lib/remlint/date_literal.rb
80
+ - lib/remlint/document.rb
81
+ - lib/remlint/expr_lexer.rb
82
+ - lib/remlint/extractors.rb
83
+ - lib/remlint/formatter.rb
84
+ - lib/remlint/invocation.rb
85
+ - lib/remlint/logical_line.rb
86
+ - lib/remlint/offense.rb
87
+ - lib/remlint/rule.rb
88
+ - lib/remlint/rules.rb
89
+ - lib/remlint/rules/addomit_without_scanfrom.rb
90
+ - lib/remlint/rules/advance_warning_body.rb
91
+ - lib/remlint/rules/banner_placement.rb
92
+ - lib/remlint/rules/calendar_text_limited.rb
93
+ - lib/remlint/rules/callback_signature.rb
94
+ - lib/remlint/rules/clause_needs_full_date.rb
95
+ - lib/remlint/rules/clause_requires_at.rb
96
+ - lib/remlint/rules/clause_value_range.rb
97
+ - lib/remlint/rules/color_component_range.rb
98
+ - lib/remlint/rules/coordinate_not_string.rb
99
+ - lib/remlint/rules/dangling_continuation.rb
100
+ - lib/remlint/rules/date_out_of_range.rb
101
+ - lib/remlint/rules/debug_command.rb
102
+ - lib/remlint/rules/easterdate_from_today.rb
103
+ - lib/remlint/rules/function_arity.rb
104
+ - lib/remlint/rules/function_redefinition.rb
105
+ - lib/remlint/rules/generated_file_edited.rb
106
+ - lib/remlint/rules/hebrew_date.rb
107
+ - lib/remlint/rules/iftrig_with_satisfy.rb
108
+ - lib/remlint/rules/include_path.rb
109
+ - lib/remlint/rules/info_clause.rb
110
+ - lib/remlint/rules/info_substitution_without_header.rb
111
+ - lib/remlint/rules/invocation_mismatch.rb
112
+ - lib/remlint/rules/keyword_case.rb
113
+ - lib/remlint/rules/license_header.rb
114
+ - lib/remlint/rules/line_length.rb
115
+ - lib/remlint/rules/literal_type_mismatch.rb
116
+ - lib/remlint/rules/localization_pack.rb
117
+ - lib/remlint/rules/moon_phase_argument.rb
118
+ - lib/remlint/rules/omit_aware_delta.rb
119
+ - lib/remlint/rules/push_vars_missing_name.rb
120
+ - lib/remlint/rules/repeat_trigger.rb
121
+ - lib/remlint/rules/satisfy_constraint.rb
122
+ - lib/remlint/rules/shell_maxlen.rb
123
+ - lib/remlint/rules/shell_use_while_run_disabled.rb
124
+ - lib/remlint/rules/string_escape.rb
125
+ - lib/remlint/rules/syntax.rb
126
+ - lib/remlint/rules/system_variable_assignment.rb
127
+ - lib/remlint/rules/tag_syntax.rb
128
+ - lib/remlint/rules/text_after_eof_marker.rb
129
+ - lib/remlint/rules/time_zone_name.rb
130
+ - lib/remlint/rules/tk_tag_namespace.rb
131
+ - lib/remlint/rules/todo_complete_through.rb
132
+ - lib/remlint/rules/trailing_whitespace.rb
133
+ - lib/remlint/rules/translate_command.rb
134
+ - lib/remlint/rules/unbalanced_blocks.rb
135
+ - lib/remlint/rules/unbalanced_delimiters.rb
136
+ - lib/remlint/rules/unknown_special_type.rb
137
+ - lib/remlint/rules/unknown_substitution_sequence.rb
138
+ - lib/remlint/rules/unknown_system_variable.rb
139
+ - lib/remlint/rules/unquoted_shell_substitution.rb
140
+ - lib/remlint/rules/until_before_from.rb
141
+ - lib/remlint/rules/world_writable_script.rb
142
+ - lib/remlint/runner.rb
143
+ - lib/remlint/source.rb
144
+ - lib/remlint/tables.rb
145
+ - lib/remlint/trigger.rb
146
+ - lib/remlint/version.rb
147
+ - lib/remlint/vocabulary.rb
148
+ - tasks/generate_tables.rb
149
+ homepage: https://dianne.skoll.ca/projects/remind/
150
+ licenses:
151
+ - GPL-2.0-only
152
+ metadata: {}
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '3.3'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubygems_version: 3.7.2
168
+ specification_version: 4
169
+ summary: A style and consistency linter for Remind reminder files
170
+ test_files: []