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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # EXPRESS grammar namespace. Houses the Parsanol grammar definition
6
+ # separate from the I/O orchestration on the outer Parser facade.
7
+ module Grammar
8
+ autoload :Parser, "#{__dir__}/grammar/parser"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Builds and queries an index of model nodes by source position.
6
+ #
7
+ # One interface (`nearest_node_to`, `node_for_end_scope_at`, `each_node`),
8
+ # one implementation. Extracted from RemarkAttacher so the tree-walk +
9
+ # byte→line translation + nearest-node heuristics live in one place,
10
+ # tested independently of any remark logic.
11
+ #
12
+ # Reference: ISO 10303-11 — the parser yields source spans on ModelElement
13
+ # instances; this module turns those spans into a line-keyed index that
14
+ # remark attachment can query.
15
+ class NodePositionIndex
16
+ # Single source of truth for "what collections does this node have"
17
+ # lives on the model — each class declares its own via the
18
+ # `collection_attributes` macro. Referenced by both RemarkAttacher
19
+ # and NodePositionIndex via this alias for backward compatibility.
20
+ COLLECTION_REGISTRY = Model::ModelElement.collection_registry
21
+
22
+ attr_reader :nodes
23
+
24
+ def initialize(model, line_map)
25
+ @model = model
26
+ @line_map = line_map
27
+ @nodes = build_sorted_nodes
28
+ end
29
+
30
+ # Returns the most-specific node whose span contains `remark_line`,
31
+ # preferring same-line starts/ends, then smallest containing span.
32
+ # Excludes Repository and Cache (not semantic scopes for remarks).
33
+ def nearest_node_to(remark_line)
34
+ same_start = nodes.select do |n|
35
+ n[:line] == remark_line && semantic?(n[:node])
36
+ end
37
+ return same_start.last[:node] if same_start.any?
38
+
39
+ same_end = nodes.select do |n|
40
+ n[:end_line] == remark_line && semantic?(n[:node])
41
+ end
42
+ return same_end.last[:node] if same_end.any?
43
+
44
+ containing = nodes.select do |n|
45
+ n[:line] && n[:end_line] &&
46
+ n[:line] <= remark_line && n[:end_line] >= remark_line &&
47
+ semantic?(n[:node])
48
+ end
49
+
50
+ if containing.any?
51
+ exp_file_node = containing.find { |n| n[:node].is_a?(Model::ExpFile) }
52
+ if exp_file_node
53
+ first_schema_offset = exp_file_node[:node].schemas&.first&.source_offset
54
+ if first_schema_offset && remark_line < @line_map.line_number(first_schema_offset)
55
+ return exp_file_node[:node]
56
+ end
57
+ end
58
+ # Prefer non-ExpFile nodes (Schema, Entity, etc.) over ExpFile
59
+ # when both span the same range, since ExpFile is a file-level
60
+ # container and remarks inside a schema belong to the schema.
61
+ candidates = containing.reject { |n| n[:node].is_a?(Model::ExpFile) }
62
+ candidates = containing if candidates.empty?
63
+ candidates.min_by { |n| n[:end_line] - n[:line] }[:node]
64
+ else
65
+ before = nodes.select do |n|
66
+ n[:end_line] && n[:end_line] < remark_line && semantic?(n[:node])
67
+ end
68
+ if before.any?
69
+ before.max_by { |n| n[:end_line] }[:node]
70
+ else
71
+ # Remark is before all nodes (e.g., preamble comment before SCHEMA).
72
+ # Attach to the first semantic node.
73
+ after = nodes.select { |n| n[:line] && semantic?(n[:node]) }
74
+ after.min_by { |n| n[:line] }[:node] if after.any?
75
+ end
76
+ end
77
+ end
78
+
79
+ # Returns the node whose END_XXX declaration is on `remark_line`, when
80
+ # the line text matches one of the END_XXX keywords. Used to attach
81
+ # remarks that appear immediately after a scope closes.
82
+ def node_for_end_scope_at(remark_line, line_content)
83
+ node_type = case line_content
84
+ when /END_SCHEMA/i then Model::Declarations::Schema
85
+ when /END_ENTITY/i then Model::Declarations::Entity
86
+ when /END_TYPE/i then Model::Declarations::Type
87
+ when /END_FUNCTION/i then Model::Declarations::Function
88
+ when /END_PROCEDURE/i then Model::Declarations::Procedure
89
+ when /END_RULE/i then Model::Declarations::Rule
90
+ end
91
+ return nil unless node_type
92
+
93
+ matching = nodes.select do |n|
94
+ n[:node].is_a?(node_type) &&
95
+ (n[:end_line] == remark_line ||
96
+ (n[:end_line] && n[:end_line] <= remark_line && n[:end_line] >= remark_line - 2))
97
+ end
98
+
99
+ matching.first&.dig(:node) || nodes.find { |n| n[:node].is_a?(node_type) }&.dig(:node)
100
+ end
101
+
102
+ private
103
+
104
+ def semantic?(node)
105
+ !node.is_a?(Model::Repository) && !node.is_a?(Model::Cache)
106
+ end
107
+
108
+ def build_sorted_nodes
109
+ result = []
110
+ collect_nodes(@model, result, Set.new)
111
+ result.sort_by!.with_index { |n, i| [n[:position] || Float::INFINITY, i] }
112
+ result
113
+ end
114
+
115
+ def collect_nodes(node, result, visited, owner: nil, collection: nil)
116
+ return unless node
117
+ return if visited.include?(node.object_id)
118
+
119
+ visited.add(node.object_id)
120
+
121
+ if node.is_a?(Model::ModelElement) && node.source && node.source_offset
122
+ record_node_position(node, result, owner, collection)
123
+ else
124
+ result << { node: node, position: nil, line: nil, end_line: nil,
125
+ owner: owner, collection: collection }
126
+ end
127
+
128
+ collect_children(node, result, visited)
129
+ end
130
+
131
+ def record_node_position(node, result, owner, collection)
132
+ pos = node.source_offset
133
+ valid = position_valid?(pos, node)
134
+ unless valid
135
+ result << { node: node, position: nil, line: nil, end_line: nil,
136
+ owner: owner, collection: collection }
137
+ return
138
+ end
139
+
140
+ line = @line_map.line_number(pos)
141
+ source_end_line = @line_map.line_number(pos + node.source.length)
142
+ children_end_line = children_end_line_for(node)
143
+ end_line = [source_end_line, children_end_line].compact.max || source_end_line
144
+
145
+ result << { node: node, position: pos, line: line, end_line: end_line,
146
+ owner: owner, collection: collection }
147
+ end
148
+
149
+ # The parser returns source_offset=0 for leaf nodes (WhereRule) where
150
+ # it cannot determine the actual position. Accept position=0 only when
151
+ # the source is a declaration keyword line — those legitimately start
152
+ # at the beginning of the file.
153
+ def position_valid?(pos, node)
154
+ return true if pos.positive?
155
+ return false unless pos.zero? && node.source
156
+
157
+ node.source.to_s.start_with?(
158
+ "SCHEMA", "ENTITY", "TYPE", "FUNCTION",
159
+ "PROCEDURE", "RULE", "CONSTANT", "VARIABLE",
160
+ "USE", "REFERENCE", "END_SCHEMA", "END_ENTITY",
161
+ "END_TYPE", "END_FUNCTION", "END_PROCEDURE",
162
+ "END_RULE", "END_CONSTANT", "END_VARIABLE"
163
+ )
164
+ end
165
+
166
+ def children_end_line_for(node)
167
+ end_lines = []
168
+
169
+ if node.is_a?(Model::Declarations::Schema)
170
+ Array(node.children).each do |child|
171
+ next unless child.is_a?(Model::ModelElement) && child.source_offset && child.source
172
+
173
+ end_lines << @line_map.line_number(child.source_offset + child.source.length)
174
+ end
175
+ end
176
+
177
+ each_collection_on(node) do |item|
178
+ next unless item.is_a?(Model::ModelElement) && item.source_offset && item.source
179
+
180
+ end_lines << @line_map.line_number(item.source_offset + item.source.length)
181
+ end
182
+
183
+ end_lines.max
184
+ end
185
+
186
+ def collect_children(node, result, visited)
187
+ if node.is_a?(Model::Declarations::Schema)
188
+ Array(node.children).each do |c|
189
+ collect_nodes(c, result, visited, owner: node, collection: :children)
190
+ end
191
+ end
192
+
193
+ each_collection_on(node) do |item, attr|
194
+ collect_nodes(item, result, visited, owner: node, collection: attr)
195
+ end
196
+ end
197
+
198
+ # Yields each child in any declared collection on the node (with the
199
+ # collection attribute name), based on the type-driven
200
+ # COLLECTION_REGISTRY. Returns nothing for nodes whose class is not
201
+ # registered.
202
+ def each_collection_on(node)
203
+ attrs = COLLECTION_REGISTRY[node.class]
204
+ return unless attrs
205
+
206
+ attrs.each do |attr|
207
+ collection = node.public_send(attr)
208
+ next unless collection.is_a?(Array)
209
+
210
+ collection.each { |item| yield(item, attr) }
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end