expressir 2.4.0 → 2.4.1

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  3. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  4. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  5. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  6. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  7. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  8. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  9. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  10. data/TODO.bugs/09-parser-split.md +53 -0
  11. data/TODO.bugs/10-to-s-override.md +42 -0
  12. data/TODO.bugs/11-parser-class-variables.md +39 -0
  13. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  14. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  15. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  16. data/TODO.bugs/15-expression-children-macro.md +27 -0
  17. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  18. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  19. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  20. data/TODO.bugs/19-format-methods-public.md +22 -0
  21. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  22. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  23. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  24. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  25. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  26. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  27. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  28. data/TODO.bugs/27-package-build-god-method.md +19 -0
  29. data/TODO.bugs/28-package-god-class.md +30 -0
  30. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  31. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  32. data/TODO.bugs/README.md +43 -0
  33. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  34. data/docs/_tutorials/index.adoc +10 -0
  35. data/docs/lychee.toml +3 -0
  36. data/expressir.gemspec +4 -2
  37. data/lib/expressir/commands/changes_import_eengine.rb +0 -6
  38. data/lib/expressir/commands/changes_validate.rb +0 -2
  39. data/lib/expressir/commands/clean.rb +1 -1
  40. data/lib/expressir/commands/file_violations.rb +70 -0
  41. data/lib/expressir/commands/format.rb +1 -1
  42. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  43. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  44. data/lib/expressir/commands/package.rb +138 -185
  45. data/lib/expressir/commands/validate_ascii.rb +0 -510
  46. data/lib/expressir/commands/version.rb +1 -1
  47. data/lib/expressir/commands.rb +3 -0
  48. data/lib/expressir/coverage.rb +49 -117
  49. data/lib/expressir/express/builder.rb +74 -86
  50. data/lib/expressir/express/builder_context.rb +22 -0
  51. data/lib/expressir/express/builders/expression_builder.rb +16 -16
  52. data/lib/expressir/express/cache.rb +35 -8
  53. data/lib/expressir/express/error.rb +7 -0
  54. data/lib/expressir/express/formatter.rb +33 -18
  55. data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
  56. data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
  57. data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
  58. data/lib/expressir/express/grammar/parser.rb +705 -0
  59. data/lib/expressir/express/grammar.rb +11 -0
  60. data/lib/expressir/express/node_position_index.rb +215 -0
  61. data/lib/expressir/express/parser.rb +114 -884
  62. data/lib/expressir/express/pretty_formatter.rb +23 -5
  63. data/lib/expressir/express/remark_attacher.rb +475 -577
  64. data/lib/expressir/express/remark_scanner.rb +84 -19
  65. data/lib/expressir/express/schema_block_scanner.rb +136 -0
  66. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  67. data/lib/expressir/express/scope_resolver.rb +194 -0
  68. data/lib/expressir/express/source_formatter.rb +15 -0
  69. data/lib/expressir/express/streaming_builder.rb +147 -176
  70. data/lib/expressir/express.rb +8 -6
  71. data/lib/expressir/model/concerns.rb +3 -0
  72. data/lib/expressir/model/data_types/generic_entity.rb +6 -6
  73. data/lib/expressir/model/declarations/entity.rb +5 -0
  74. data/lib/expressir/model/declarations/function.rb +5 -0
  75. data/lib/expressir/model/declarations/procedure.rb +5 -0
  76. data/lib/expressir/model/declarations/rule.rb +6 -0
  77. data/lib/expressir/model/declarations/schema.rb +21 -8
  78. data/lib/expressir/model/declarations/type.rb +3 -0
  79. data/lib/expressir/model/exp_file.rb +2 -0
  80. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
  81. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
  82. data/lib/expressir/model/expressions/binary_expression.rb +1 -0
  83. data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
  84. data/lib/expressir/model/expressions/function_call.rb +1 -0
  85. data/lib/expressir/model/expressions/interval.rb +1 -0
  86. data/lib/expressir/model/expressions/query_expression.rb +2 -0
  87. data/lib/expressir/model/expressions/unary_expression.rb +1 -0
  88. data/lib/expressir/model/model_element.rb +128 -14
  89. data/lib/expressir/model/remark_format.rb +17 -0
  90. data/lib/expressir/model/remark_info.rb +31 -3
  91. data/lib/expressir/model/remark_placement.rb +29 -0
  92. data/lib/expressir/model/statements/alias.rb +2 -0
  93. data/lib/expressir/model/statements/assignment.rb +4 -0
  94. data/lib/expressir/model/statements/case.rb +21 -0
  95. data/lib/expressir/model/statements/case_action.rb +1 -0
  96. data/lib/expressir/model/statements/compound.rb +4 -0
  97. data/lib/expressir/model/statements/escape.rb +3 -0
  98. data/lib/expressir/model/statements/if.rb +5 -0
  99. data/lib/expressir/model/statements/null.rb +3 -0
  100. data/lib/expressir/model/statements/procedure_call.rb +3 -0
  101. data/lib/expressir/model/statements/repeat.rb +3 -0
  102. data/lib/expressir/model/statements/return.rb +3 -0
  103. data/lib/expressir/model/statements/skip.rb +3 -0
  104. data/lib/expressir/model.rb +12 -3
  105. data/lib/expressir/package/builder.rb +2 -2
  106. data/lib/expressir/version.rb +6 -1
  107. data/lib/expressir.rb +38 -3
  108. metadata +63 -5
  109. data/lib/expressir/express/transformer/remark_handling.rb +0 -194
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Commands
5
+ class FileViolations
6
+ attr_reader :path, :filename, :directory, :violations
7
+
8
+ def initialize(file_path)
9
+ @path = file_path
10
+ @filename = File.basename(file_path)
11
+ @directory = File.dirname(file_path)
12
+ @characters = {} # Map of characters to NonAsciiCharacter objects
13
+ @violations = [] # List of violations (line, column, etc.)
14
+ end
15
+
16
+ def add_violation(line_number, column, match, char_details, line)
17
+ violation = {
18
+ line_number: line_number,
19
+ column: column,
20
+ match: match,
21
+ char_details: char_details,
22
+ line: line,
23
+ }
24
+
25
+ @violations << violation
26
+
27
+ # Register each character
28
+ char_details.each do |detail|
29
+ char = detail[:char]
30
+ unless @characters[char]
31
+ @characters[char] = NonAsciiCharacter.new(
32
+ char,
33
+ detail[:hex],
34
+ detail[:utf8],
35
+ detail[:is_math],
36
+ detail[:replacement],
37
+ detail[:replacement_type],
38
+ )
39
+ end
40
+
41
+ @characters[char].add_occurrence(line_number, column, line)
42
+ end
43
+ end
44
+
45
+ def violation_count
46
+ @violations.size
47
+ end
48
+
49
+ def unique_characters
50
+ @characters.values
51
+ end
52
+
53
+ def display_path
54
+ "#{File.basename(@directory)}/#{@filename}"
55
+ end
56
+
57
+ def full_path
58
+ File.expand_path(@path)
59
+ end
60
+
61
+ def to_h
62
+ {
63
+ file: display_path,
64
+ count: violation_count,
65
+ non_ascii_characters: unique_characters.map(&:to_h),
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -35,7 +35,7 @@ module Expressir
35
35
  def format_with_iso_profile(repository)
36
36
  repository.schemas.each do |schema|
37
37
  say "\n(* Expressir formatted schema: #{schema.id} *)\n"
38
- say schema.to_s(no_remarks: true)
38
+ say schema.format(no_remarks: true)
39
39
  end
40
40
  end
41
41
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Commands
5
+ class NonAsciiCharacter
6
+ attr_reader :char, :hex, :utf8, :is_math, :replacement,
7
+ :replacement_type, :occurrences
8
+
9
+ def initialize(char, hex, utf8, is_math, replacement, replacement_type)
10
+ @char = char
11
+ @hex = hex
12
+ @utf8 = utf8
13
+ @is_math = is_math
14
+ @replacement = replacement
15
+ @replacement_type = replacement_type
16
+ @occurrences = []
17
+ end
18
+
19
+ def add_occurrence(line_number, column, line)
20
+ @occurrences << {
21
+ line_number: line_number,
22
+ column: column,
23
+ line: line,
24
+ }
25
+ end
26
+
27
+ def replacement_text
28
+ @is_math ? "AsciiMath: #{@replacement}" : "ISO 10303-11: #{@replacement}"
29
+ end
30
+
31
+ def occurrence_count
32
+ @occurrences.size
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ character: @char,
38
+ hex: @hex,
39
+ utf8: @utf8,
40
+ is_math: @is_math,
41
+ replacement_type: @replacement_type,
42
+ replacement: @replacement,
43
+ occurrence_count: occurrence_count,
44
+ occurrences: @occurrences,
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paint"
4
+
5
+ module Expressir
6
+ module Commands
7
+ class NonAsciiViolationCollection
8
+ # Pre-defined mapping of common Unicode math symbols to AsciiMath.
9
+ UNICODE_TO_ASCIIDOC_MAP = {
10
+ # Greek letters
11
+ "α" => "alpha", "β" => "beta", "γ" => "gamma", "Γ" => "Gamma",
12
+ "δ" => "delta", "Δ" => "Delta", "ε" => "epsilon", "ζ" => "zeta",
13
+ "η" => "eta", "θ" => "theta", "Θ" => "Theta", "ι" => "iota",
14
+ "κ" => "kappa", "λ" => "lambda", "Λ" => "Lambda", "μ" => "mu",
15
+ "ν" => "nu", "ξ" => "xi", "Ξ" => "Xi", "π" => "pi", "Π" => "Pi",
16
+ "ρ" => "rho", "σ" => "sigma", "Σ" => "Sigma", "τ" => "tau",
17
+ "υ" => "upsilon", "φ" => "phi", "Φ" => "Phi", "χ" => "chi",
18
+ "ψ" => "psi", "Ψ" => "Psi", "ω" => "omega", "Ω" => "Omega",
19
+ # Math operators
20
+ "×" => "xx", "÷" => "div", "±" => "pm", "∓" => "mp",
21
+ "∞" => "oo", "≤" => "le", "≥" => "ge", "≠" => "ne",
22
+ "≈" => "~~", "≅" => "cong", "≡" => "equiv", "∈" => "in",
23
+ "∉" => "notin", "⊂" => "subset", "⊃" => "supset", "∩" => "cap",
24
+ "∪" => "cup", "∧" => "and", "∨" => "or", "¬" => "neg",
25
+ "∀" => "forall", "∃" => "exists", "∄" => "nexists", "∇" => "grad",
26
+ "∂" => "del", "∑" => "sum", "∏" => "prod", "∫" => "int",
27
+ "∮" => "oint", "√" => "sqrt", "⊥" => "perp", "‖" => "norm",
28
+ "→" => "rarr", "←" => "larr", "↔" => "harr",
29
+ "⇒" => "rArr", "⇐" => "lArr", "⇔" => "hArr"
30
+ }.freeze
31
+
32
+ attr_reader :file_violations, :total_files
33
+
34
+ def initialize(check_remarks: false)
35
+ @file_violations = {} # Map of file paths to FileViolations objects
36
+ @total_files = 0
37
+ @unicode_to_asciimath = nil
38
+ @check_remarks = check_remarks
39
+ end
40
+
41
+ def process_file(file)
42
+ @total_files += 1
43
+
44
+ # Initialize the mapping once
45
+ @unicode_to_asciimath ||= UNICODE_TO_ASCIIDOC_MAP
46
+
47
+ file_violations = process_file_violations(file)
48
+ return if file_violations.violations.empty?
49
+
50
+ @file_violations[file] = file_violations
51
+ end
52
+
53
+ def files_with_violations
54
+ @file_violations.size
55
+ end
56
+
57
+ def total_violations
58
+ @file_violations.values.sum(&:violation_count)
59
+ end
60
+
61
+ def unique_character_count
62
+ # Get total unique characters across all files
63
+ all_chars = Set.new
64
+ @file_violations.each_value do |file_violation|
65
+ file_violation.unique_characters.each do |char|
66
+ all_chars.add(char.char)
67
+ end
68
+ end
69
+ all_chars.size
70
+ end
71
+
72
+ def total_occurrence_count
73
+ # Sum all occurrences of all characters across all files
74
+ @file_violations.values.sum do |file_violation|
75
+ file_violation.unique_characters.sum(&:occurrence_count)
76
+ end
77
+ end
78
+
79
+ def to_yaml_data
80
+ {
81
+ summary: {
82
+ total_files: @total_files,
83
+ files_with_violations: files_with_violations,
84
+ total_violations: total_violations,
85
+ total_unique_characters: unique_character_count,
86
+ total_occurrences: total_occurrence_count,
87
+ },
88
+ violations: @file_violations.transform_keys do |k|
89
+ File.expand_path(k)
90
+ end.transform_values(&:to_h),
91
+ }
92
+ end
93
+
94
+ def print_text_output
95
+ # Print each file's violations if any
96
+ unless @file_violations.empty?
97
+ @file_violations.each_value do |file_violation|
98
+ puts "\n#{Paint[file_violation.display_path, :cyan, :bold]}:"
99
+
100
+ file_violation.violations.each do |v|
101
+ puts " #{Paint['Line',
102
+ :blue]} #{Paint[v[:line_number],
103
+ :yellow]}, #{Paint['Column',
104
+ :blue]} #{Paint[v[:column],
105
+ :yellow]}:"
106
+ puts " #{v[:line]}"
107
+ puts " #{' ' * v[:column]}#{Paint['^' * v[:match].length,
108
+ :red]} #{Paint['Non-ASCII sequence',
109
+ :red]}"
110
+
111
+ v[:char_details].each do |cd|
112
+ character = file_violation.unique_characters.find do |c|
113
+ c.char == cd[:char]
114
+ end
115
+ next unless character
116
+
117
+ puts " #{Paint["\"#{cd[:char]}\"",
118
+ :yellow]} - Hex: #{Paint[cd[:hex],
119
+ :magenta]}, UTF-8 bytes: #{Paint[cd[:utf8],
120
+ :magenta]}"
121
+ puts " #{Paint['Replacement:',
122
+ :green]} #{character.replacement_text}"
123
+ end
124
+ puts ""
125
+ end
126
+
127
+ puts " #{Paint['Found',
128
+ :green]} #{Paint[file_violation.violation_count,
129
+ :red]} #{Paint['non-ASCII sequence(s) in',
130
+ :green]} #{Paint[file_violation.filename,
131
+ :cyan]}\n"
132
+ end
133
+ end
134
+
135
+ # Always print summary
136
+ validation_scope = @check_remarks ? "code and all remarks" : "code and tail remarks (embedded remarks excluded)"
137
+ puts "\n#{Paint['Summary:', :blue, :bold]}"
138
+ puts " #{Paint['Validation scope:',
139
+ :green]} #{Paint[validation_scope,
140
+ :cyan]}"
141
+ puts " #{Paint['Scanned',
142
+ :green]} #{Paint[@total_files,
143
+ :yellow]} #{Paint['EXPRESS file(s)',
144
+ :green]}"
145
+ puts " #{Paint['Found',
146
+ :green]} #{Paint[total_violations,
147
+ :red]} #{Paint['non-ASCII sequence(s) in',
148
+ :green]} #{Paint[files_with_violations,
149
+ :red]} #{Paint['file(s)',
150
+ :green]}"
151
+ end
152
+
153
+ def print_table_output
154
+ return if @file_violations.empty?
155
+
156
+ # Build rows array
157
+ rows = []
158
+ total_occurrences = 0
159
+
160
+ @file_violations.each_value do |file_violation|
161
+ file_violation.unique_characters.each do |character|
162
+ occurrence_count = character.occurrence_count
163
+ total_occurrences += occurrence_count
164
+
165
+ rows << {
166
+ file: file_violation.display_path,
167
+ symbol: "\"#{character.char}\" (#{character.hex})",
168
+ replacement: character.replacement_text,
169
+ occurrences: occurrence_count,
170
+ }
171
+ end
172
+ end
173
+
174
+ # Add total row
175
+ rows << {
176
+ file: "TOTAL",
177
+ symbol: "#{unique_character_count} unique",
178
+ replacement: "",
179
+ occurrences: total_occurrences,
180
+ }
181
+
182
+ # Use TableTennis to render
183
+ options = {
184
+ title: "Non-ASCII Characters Summary",
185
+ columns: %i[file symbol replacement occurrences],
186
+ headers: {
187
+ file: "File",
188
+ symbol: "Symbol",
189
+ replacement: "Replacement",
190
+ occurrences: "Occurrences",
191
+ },
192
+ mark: ->(row) { row[:file] == "TOTAL" },
193
+ }
194
+
195
+ puts "\n#{TableTennis.new(rows, options)}\n"
196
+ end
197
+
198
+ private
199
+
200
+ # Build a masked copy of `source` where every byte inside an embedded
201
+ # remark region is replaced with an ASCII space. Byte positions and the
202
+ # string's encoding are preserved, so masked lines still align 1:1 with
203
+ # the original file for reporting.
204
+ def mask_embedded_remarks(source)
205
+ remarks = Expressir::Express::RemarkScanner.new(source).scan
206
+ embedded = remarks.select(&:embedded?)
207
+ return source if embedded.empty?
208
+
209
+ masked = source.b.dup
210
+ embedded.each do |r|
211
+ (r.position...r.end_position).each { |i| masked.setbyte(i, 0x20) }
212
+ end
213
+ masked.force_encoding(source.encoding)
214
+ end
215
+
216
+ def scan_lines_for_non_ascii(file_violations, lines)
217
+ lines.each_with_index do |line, line_idx|
218
+ line_number = line_idx + 1
219
+ next unless /[^\x00-\x7F]/.match?(line)
220
+
221
+ line.chomp.scan(/([^\x00-\x7F]+)/) do |match|
222
+ match = match[0]
223
+ column = line.index(match)
224
+
225
+ char_details = match.chars.filter_map do |c|
226
+ process_non_ascii_char(c)
227
+ end
228
+ next if char_details.empty?
229
+
230
+ file_violations.add_violation(line_number, column, match,
231
+ char_details, line.chomp)
232
+ end
233
+ end
234
+ end
235
+
236
+ def process_file_violations(file)
237
+ file_violations = FileViolations.new(file)
238
+ source = File.read(file, encoding: "UTF-8")
239
+
240
+ # With --check-remarks: scan every line including embedded remarks.
241
+ # Without: scan code + tail remarks only — mask out embedded remark
242
+ # regions so non-ASCII inside `(* ... *)` documentation blocks is
243
+ # not flagged. Tail remarks (`--`) are part of code lines and stay
244
+ # in scope (issue #285).
245
+ scan_source = @check_remarks ? source : mask_embedded_remarks(source)
246
+ scan_lines_for_non_ascii(file_violations, scan_source.lines)
247
+ file_violations
248
+ end
249
+
250
+ def process_non_ascii_char(char)
251
+ # Skip ASCII characters
252
+ return nil if char.ord <= 0x7F
253
+
254
+ code_point = char.ord
255
+ hex = "0x#{code_point.to_s(16)}"
256
+ utf8 = code_point.chr(Encoding::UTF_8).bytes.map do |b|
257
+ "0x#{b.to_s(16)}"
258
+ end.join(" ")
259
+
260
+ # Check if it's a math symbol
261
+ if asciimath = @unicode_to_asciimath[char]
262
+ return {
263
+ char: char,
264
+ hex: hex,
265
+ utf8: utf8,
266
+ is_math: true,
267
+ replacement: asciimath,
268
+ replacement_type: "asciimath",
269
+ }
270
+ end
271
+
272
+ # Not a math symbol, use ISO encoding
273
+ {
274
+ char: char,
275
+ hex: hex,
276
+ utf8: utf8,
277
+ is_math: false,
278
+ replacement: encode_express_format(char),
279
+ replacement_type: "iso-10303-11",
280
+ }
281
+ end
282
+
283
+ def encode_express_format(char)
284
+ code_point = char.ord
285
+
286
+ # Format the encoded value with double quotes
287
+ if code_point < 0x10000
288
+ "\"#{sprintf('%08X', code_point)}\"" # e.g., "00000041" for 'A'
289
+ else
290
+ # For higher code points, use all four octets
291
+ group = (code_point >> 24) & 0xFF
292
+ plane = (code_point >> 16) & 0xFF
293
+ row = (code_point >> 8) & 0xFF
294
+ cell = code_point & 0xFF
295
+
296
+ "\"#{sprintf('%02X%02X%02X%02X', group, plane, row, cell)}\""
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end