expressir 2.3.6 → 2.4.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 +4 -4
- data/.github/workflows/rake.yml +1 -1
- data/.gitignore +2 -0
- data/.rubocop_todo.yml +17 -12
- data/CHANGELOG.md +159 -0
- data/docs/_guides/ler/step-packages.adoc +385 -0
- data/lib/expressir/coverage.rb +4 -4
- data/lib/expressir/express/builder_registry.rb +3 -1
- data/lib/expressir/express/builders/expression_builder.rb +2 -2
- data/lib/expressir/express/builders/helpers.rb +2 -8
- data/lib/expressir/express/builders/qualifier_builder.rb +2 -1
- data/lib/expressir/express/formatter.rb +32 -164
- data/lib/expressir/express/formatters/data_types_formatter.rb +39 -6
- data/lib/expressir/express/formatters/declarations_formatter.rb +47 -8
- data/lib/expressir/express/formatters/expressions_formatter.rb +20 -5
- data/lib/expressir/express/formatters/literals_formatter.rb +11 -1
- data/lib/expressir/express/formatters/references_formatter.rb +10 -1
- data/lib/expressir/express/formatters/remark_formatter.rb +1 -89
- data/lib/expressir/express/formatters/remark_item_formatter.rb +5 -4
- data/lib/expressir/express/formatters/statements_formatter.rb +32 -9
- data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +7 -3
- data/lib/expressir/express/hyperlink_formatter.rb +1 -3
- data/lib/expressir/express/line_map.rb +48 -0
- data/lib/expressir/express/model_visitor.rb +1 -1
- data/lib/expressir/express/pretty_formatter.rb +31 -108
- data/lib/expressir/express/remark_attacher.rb +144 -198
- data/lib/expressir/express/remark_scanner.rb +180 -0
- data/lib/expressir/express/schema_head_formatter.rb +1 -3
- data/lib/expressir/express.rb +2 -0
- data/lib/expressir/model/concerns.rb +6 -0
- data/lib/expressir/model/declarations/interface_item.rb +2 -0
- data/lib/expressir/model/declarations/interfaced_item.rb +3 -0
- data/lib/expressir/model/declarations/remark_item.rb +3 -0
- data/lib/expressir/model/declarations/schema.rb +7 -5
- data/lib/expressir/model/exp_file.rb +1 -0
- data/lib/expressir/model/identifier.rb +3 -1
- data/lib/expressir/model/model_element.rb +3 -3
- data/lib/expressir/model/repository.rb +8 -0
- data/lib/expressir/model/search_engine.rb +1 -1
- data/lib/expressir/package/reader.rb +9 -24
- data/lib/expressir/version.rb +1 -1
- metadata +6 -2
|
@@ -11,21 +11,44 @@ module Expressir
|
|
|
11
11
|
# 2. Proximity-based matching for simple tags
|
|
12
12
|
# 3. NOT creating spurious schema-level items for ambiguous tags
|
|
13
13
|
class RemarkAttacher
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
# Type-driven registry: maps each model class to its collection attributes.
|
|
15
|
+
# Replaces runtime method probing (method_defined?) with explicit type declarations.
|
|
16
|
+
COLLECTION_REGISTRY = {
|
|
17
|
+
Model::Declarations::Schema => %i[
|
|
18
|
+
constants types entities subtype_constraints
|
|
19
|
+
functions rules procedures remark_items
|
|
20
|
+
],
|
|
21
|
+
Model::Declarations::Entity => %i[
|
|
22
|
+
attributes derived_attributes inverse_attributes
|
|
23
|
+
unique_rules where_rules informal_propositions remark_items
|
|
24
|
+
],
|
|
25
|
+
Model::Declarations::Function => %i[
|
|
26
|
+
parameters types entities subtype_constraints
|
|
27
|
+
functions procedures constants variables statements remark_items
|
|
28
|
+
],
|
|
29
|
+
Model::Declarations::Procedure => %i[
|
|
30
|
+
parameters types entities subtype_constraints
|
|
31
|
+
functions procedures constants variables statements remark_items
|
|
32
|
+
],
|
|
33
|
+
Model::Declarations::Rule => %i[
|
|
34
|
+
applies_to types entities subtype_constraints
|
|
35
|
+
functions procedures constants variables statements
|
|
36
|
+
where_rules informal_propositions remark_items
|
|
37
|
+
],
|
|
38
|
+
Model::Declarations::Type => %i[
|
|
39
|
+
where_rules informal_propositions remark_items
|
|
40
|
+
],
|
|
41
|
+
Model::ExpFile => %i[schemas],
|
|
42
|
+
Model::Statements::Compound => %i[statements],
|
|
43
|
+
Model::Statements::If => %i[statements],
|
|
44
|
+
Model::Statements::Alias => %i[statements],
|
|
45
|
+
Model::Statements::Repeat => %i[statements],
|
|
46
|
+
}.freeze
|
|
24
47
|
|
|
25
48
|
def initialize(source)
|
|
26
49
|
@source = source
|
|
27
50
|
@attached_spans = Set.new
|
|
28
|
-
@
|
|
51
|
+
@line_map = LineMap.new(source.b)
|
|
29
52
|
@model = nil
|
|
30
53
|
@source_lines = nil # cached @source.lines
|
|
31
54
|
@scope_map = nil # cached scope at each line number
|
|
@@ -33,7 +56,7 @@ module Expressir
|
|
|
33
56
|
|
|
34
57
|
def attach(model)
|
|
35
58
|
@model = model
|
|
36
|
-
remarks =
|
|
59
|
+
remarks = RemarkScanner.new(@source).scan
|
|
37
60
|
|
|
38
61
|
# Build nodes_with_positions ONCE for both tagged and untagged remark passes.
|
|
39
62
|
# This avoids double tree walk (381K nodes × 2 = 762K visits) which was
|
|
@@ -48,103 +71,22 @@ module Expressir
|
|
|
48
71
|
@source = nil
|
|
49
72
|
@source_lines = nil
|
|
50
73
|
@scope_map = nil
|
|
51
|
-
@
|
|
74
|
+
@line_map = nil
|
|
52
75
|
|
|
53
76
|
model
|
|
54
77
|
end
|
|
55
78
|
|
|
56
79
|
private
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
byte_position = 0
|
|
61
|
-
|
|
62
|
-
@source.each_line.with_index do |line, line_idx|
|
|
63
|
-
line_bytesize = line.bytesize
|
|
64
|
-
line_bytes = line.b # Get byte string for indexing
|
|
65
|
-
if (dash_byte_idx = line_bytes.index("--"))
|
|
66
|
-
remark_text = line.byteslice((dash_byte_idx + 2)..).strip
|
|
67
|
-
|
|
68
|
-
# Check for special patterns like --IP1: content (informal proposition)
|
|
69
|
-
if remark_text.match?(/^IP\d+:\s*(.*)$/)
|
|
70
|
-
tag = remark_text[/^(IP\d+):/, 1]
|
|
71
|
-
content = remark_text[/^IP\d+:\s*(.*)$/, 1]
|
|
72
|
-
remarks << {
|
|
73
|
-
position: byte_position + dash_byte_idx,
|
|
74
|
-
line: line_idx + 1,
|
|
75
|
-
text: content,
|
|
76
|
-
tag: tag,
|
|
77
|
-
format: "tail",
|
|
78
|
-
}
|
|
79
|
-
else
|
|
80
|
-
tag, content = parse_tagged_remark(remark_text)
|
|
81
|
-
remarks << {
|
|
82
|
-
position: byte_position + dash_byte_idx,
|
|
83
|
-
line: line_idx + 1,
|
|
84
|
-
text: content || remark_text,
|
|
85
|
-
tag: tag,
|
|
86
|
-
format: "tail",
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
byte_position += line_bytesize
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
extract_embedded_remarks(remarks)
|
|
94
|
-
# Sort by position to ensure remarks are processed in source order
|
|
95
|
-
remarks.sort_by! { |r| r[:position] }
|
|
96
|
-
remarks
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def extract_embedded_remarks(remarks)
|
|
100
|
-
source_bytes = @source.b
|
|
101
|
-
start_pos = 0
|
|
102
|
-
while (start_idx = source_bytes.index("(*", start_pos))
|
|
103
|
-
end_idx = source_bytes.index("*)", start_idx + 2)
|
|
104
|
-
break unless end_idx
|
|
105
|
-
|
|
106
|
-
content = @source.byteslice((start_idx + 2)...end_idx)
|
|
107
|
-
line_num = get_line_number(start_idx)
|
|
108
|
-
|
|
109
|
-
tag, text = parse_tagged_embedded_remark(content)
|
|
110
|
-
|
|
111
|
-
remarks << {
|
|
112
|
-
position: start_idx,
|
|
113
|
-
line: line_num,
|
|
114
|
-
text: text,
|
|
115
|
-
tag: tag,
|
|
116
|
-
format: "embedded",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
start_pos = end_idx + 2
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def parse_tagged_remark(text)
|
|
124
|
-
if text.start_with?('"') && (end_quote = text.index('"', 1))
|
|
125
|
-
[text[1...end_quote], text[(end_quote + 1)..].strip]
|
|
126
|
-
else
|
|
127
|
-
[nil, text]
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def parse_tagged_embedded_remark(content)
|
|
132
|
-
stripped = content.strip
|
|
133
|
-
if stripped.start_with?('"') && (end_quote = stripped.index('"', 1))
|
|
134
|
-
[stripped[1...end_quote], stripped[(end_quote + 1)..].strip]
|
|
135
|
-
else
|
|
136
|
-
[nil, stripped]
|
|
137
|
-
end
|
|
138
|
-
end
|
|
81
|
+
# Remark extraction lives in {RemarkScanner}; this class only attaches.
|
|
82
|
+
# Line-number lookup is delegated to {LineMap} for O(log n) queries.
|
|
139
83
|
|
|
140
84
|
def source_lines
|
|
141
85
|
@source_lines ||= @source.lines
|
|
142
86
|
end
|
|
143
87
|
|
|
144
88
|
def get_line_number(position)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@line_cache[position] ||= @source.byteslice(0...position).count("\n") + 1
|
|
89
|
+
@line_map.line_number(position)
|
|
148
90
|
end
|
|
149
91
|
|
|
150
92
|
def attach_tagged_remarks(model, remarks, nodes_with_positions)
|
|
@@ -157,23 +99,23 @@ module Expressir
|
|
|
157
99
|
# This is the key optimization that makes scope lookup O(1) per remark
|
|
158
100
|
@scope_map ||= build_scope_map
|
|
159
101
|
|
|
160
|
-
tagged.sort_by
|
|
161
|
-
next if @attached_spans.include?(remark
|
|
102
|
+
tagged.sort_by(&:position).each do |remark|
|
|
103
|
+
next if @attached_spans.include?(remark.position)
|
|
162
104
|
|
|
163
|
-
tag = remark
|
|
105
|
+
tag = remark.tag
|
|
164
106
|
target = nil
|
|
165
107
|
|
|
166
108
|
# Find containing scope using pre-computed scope map (O(1))
|
|
167
109
|
# Falls back to position-based lookup if scope map doesn't have the line
|
|
168
|
-
containing_scope = find_containing_scope_by_name(remark
|
|
169
|
-
containing_scope ||= find_containing_scope_position(remark
|
|
110
|
+
containing_scope = find_containing_scope_by_name(remark.line)
|
|
111
|
+
containing_scope ||= find_containing_scope_position(remark.line,
|
|
170
112
|
nodes_with_positions)
|
|
171
113
|
|
|
172
114
|
# Check if this is an informal proposition tag (IP\d+)
|
|
173
115
|
if tag.match?(/^IP\d+$/)
|
|
174
116
|
scope = containing_scope
|
|
175
117
|
if scope.nil?
|
|
176
|
-
scope = find_scope_by_source_text(remark
|
|
118
|
+
scope = find_scope_by_source_text(remark.line)
|
|
177
119
|
end
|
|
178
120
|
if scope && supports_informal_propositions?(scope)
|
|
179
121
|
target = create_or_find_informal_proposition(scope, tag)
|
|
@@ -223,7 +165,7 @@ module Expressir
|
|
|
223
165
|
# Special handling for remarks inside WHERE clauses
|
|
224
166
|
if target.nil? && supports_where_rules?(containing_scope)
|
|
225
167
|
target = find_target_in_where_clause(containing_scope, tag,
|
|
226
|
-
remark
|
|
168
|
+
remark.line)
|
|
227
169
|
end
|
|
228
170
|
|
|
229
171
|
# Only fall back to schema prefix if NOT inside a function/rule/procedure
|
|
@@ -273,9 +215,8 @@ module Expressir
|
|
|
273
215
|
end
|
|
274
216
|
|
|
275
217
|
if target
|
|
276
|
-
add_remark(target, remark
|
|
277
|
-
|
|
278
|
-
@attached_spans << remark[:position]
|
|
218
|
+
add_remark(target, remark.text, format: remark.format, tag: remark.tag)
|
|
219
|
+
@attached_spans << remark.position
|
|
279
220
|
end
|
|
280
221
|
end
|
|
281
222
|
end
|
|
@@ -376,7 +317,7 @@ module Expressir
|
|
|
376
317
|
return schema if schema.id == scope_name
|
|
377
318
|
|
|
378
319
|
%i[functions procedures rules entities types].each do |decl_type|
|
|
379
|
-
collection = schema.
|
|
320
|
+
collection = schema.public_send(decl_type)
|
|
380
321
|
next unless collection.is_a?(Array)
|
|
381
322
|
|
|
382
323
|
found = collection.find { |n| n.id == scope_name }
|
|
@@ -390,30 +331,22 @@ module Expressir
|
|
|
390
331
|
def find_node_in_scope(scope, tag)
|
|
391
332
|
return nil unless scope
|
|
392
333
|
|
|
393
|
-
|
|
394
|
-
# Check all collections that might contain nodes with ids
|
|
395
|
-
%i[constants types variables parameters statements
|
|
396
|
-
attributes derived_attributes inverse_attributes
|
|
397
|
-
where_rules unique_rules informal_propositions].each do |attr|
|
|
398
|
-
collection = safe_get_collection(scope, attr)
|
|
399
|
-
next unless collection
|
|
400
|
-
|
|
334
|
+
collections_on(scope).each do |collection|
|
|
401
335
|
collection.each do |item|
|
|
336
|
+
next unless item.is_a?(Model::HasRemarks)
|
|
402
337
|
return item if item.id == tag
|
|
403
|
-
rescue NoMethodError
|
|
404
|
-
next
|
|
405
338
|
end
|
|
406
339
|
end
|
|
407
340
|
|
|
408
341
|
# Search inside types for enumeration items
|
|
409
|
-
types =
|
|
342
|
+
types = get_collection(scope, :types)
|
|
410
343
|
types&.each do |type|
|
|
411
344
|
result = find_enumeration_item_in_type(type, tag)
|
|
412
345
|
return result if result
|
|
413
346
|
end
|
|
414
347
|
|
|
415
348
|
# Search inside statements for nested items (alias, repeat, query)
|
|
416
|
-
statements =
|
|
349
|
+
statements = get_collection(scope, :statements)
|
|
417
350
|
statements&.each do |stmt|
|
|
418
351
|
result = find_node_in_statement(stmt, tag)
|
|
419
352
|
return result if result
|
|
@@ -448,34 +381,51 @@ module Expressir
|
|
|
448
381
|
nil
|
|
449
382
|
end
|
|
450
383
|
|
|
384
|
+
# Expression and statement child attributes for QueryExpression search.
|
|
385
|
+
# Targeted traversal prevents over-matching on unrelated model attributes.
|
|
386
|
+
EXPRESSION_CHILDREN = {
|
|
387
|
+
Model::Expressions::BinaryExpression => %i[operand1 operand2],
|
|
388
|
+
Model::Expressions::UnaryExpression => %i[operand],
|
|
389
|
+
Model::Expressions::QueryExpression => %i[expression aggregate_source],
|
|
390
|
+
Model::Expressions::AggregateInitializerItem => %i[expression
|
|
391
|
+
repetition],
|
|
392
|
+
Model::Expressions::Interval => %i[low item high],
|
|
393
|
+
Model::Expressions::FunctionCall => %i[parameters],
|
|
394
|
+
Model::Expressions::EntityConstructor => %i[parameters],
|
|
395
|
+
Model::Expressions::AggregateInitializer => %i[items],
|
|
396
|
+
Model::Statements::Assignment => %i[expression],
|
|
397
|
+
Model::Statements::If => %i[expression],
|
|
398
|
+
Model::Statements::Case => %i[expression],
|
|
399
|
+
Model::Statements::CaseAction => %i[expression],
|
|
400
|
+
Model::Statements::Repeat => %i[while_expression until_expression],
|
|
401
|
+
}.freeze
|
|
402
|
+
|
|
451
403
|
def find_query_in_expression(node, tag, visited = Set.new)
|
|
452
404
|
return nil unless node
|
|
405
|
+
return nil unless node.is_a?(Model::ModelElement)
|
|
453
406
|
return nil if visited.include?(node.object_id)
|
|
454
407
|
|
|
455
408
|
visited.add(node.object_id)
|
|
456
409
|
|
|
457
|
-
# Check if this node is a QueryExpression with matching id
|
|
458
410
|
if node.is_a?(Model::Expressions::QueryExpression) && node.id == tag
|
|
459
411
|
return node
|
|
460
412
|
end
|
|
461
413
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
query_expression repeat_control].each do |attr|
|
|
465
|
-
child = safe_send(node, attr)
|
|
466
|
-
next unless child
|
|
467
|
-
|
|
468
|
-
result = find_query_in_expression(child, tag, visited)
|
|
469
|
-
return result if result
|
|
470
|
-
end
|
|
414
|
+
attrs = EXPRESSION_CHILDREN[node.class]
|
|
415
|
+
return nil unless attrs
|
|
471
416
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
417
|
+
attrs.each do |attr|
|
|
418
|
+
value = node.public_send(attr)
|
|
419
|
+
case value
|
|
420
|
+
when Array
|
|
421
|
+
value.each do |val|
|
|
422
|
+
next unless val.is_a?(Model::ModelElement)
|
|
476
423
|
|
|
477
|
-
|
|
478
|
-
|
|
424
|
+
result = find_query_in_expression(val, tag, visited)
|
|
425
|
+
return result if result
|
|
426
|
+
end
|
|
427
|
+
when Model::ModelElement
|
|
428
|
+
result = find_query_in_expression(value, tag, visited)
|
|
479
429
|
return result if result
|
|
480
430
|
end
|
|
481
431
|
end
|
|
@@ -505,7 +455,7 @@ module Expressir
|
|
|
505
455
|
return nil unless collection_attr
|
|
506
456
|
|
|
507
457
|
# First try to find in containing scope
|
|
508
|
-
collection =
|
|
458
|
+
collection = get_collection(containing_scope, collection_attr)
|
|
509
459
|
if collection
|
|
510
460
|
found = collection.find { |item| item.is_a?(Model::ModelElement) && item.id == id }
|
|
511
461
|
return found if found
|
|
@@ -525,7 +475,7 @@ module Expressir
|
|
|
525
475
|
def find_target_in_where_clause(scope, tag, remark_line)
|
|
526
476
|
return nil unless supports_where_rules?(scope)
|
|
527
477
|
|
|
528
|
-
where_rules =
|
|
478
|
+
where_rules = get_collection(scope, :where_rules)
|
|
529
479
|
return nil unless where_rules&.any?
|
|
530
480
|
|
|
531
481
|
# Search source text for WHERE clause containing this remark
|
|
@@ -652,24 +602,21 @@ module Expressir
|
|
|
652
602
|
nil
|
|
653
603
|
end
|
|
654
604
|
|
|
605
|
+
COLLECTION_ACCESSOR = {
|
|
606
|
+
Expressir::Model::Declarations::Entity => lambda(&:entities),
|
|
607
|
+
Expressir::Model::Declarations::Type => lambda(&:types),
|
|
608
|
+
Expressir::Model::Declarations::Rule => lambda(&:rules),
|
|
609
|
+
}.freeze
|
|
610
|
+
|
|
655
611
|
def find_node_by_type_and_name(node_class, name)
|
|
656
612
|
return nil unless @model && name
|
|
657
613
|
|
|
658
|
-
|
|
614
|
+
accessor = COLLECTION_ACCESSOR[node_class]
|
|
615
|
+
return nil unless accessor
|
|
616
|
+
|
|
659
617
|
@model.schemas.each do |schema|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
schema.entities
|
|
663
|
-
when "Expressir::Model::Declarations::Type"
|
|
664
|
-
schema.types
|
|
665
|
-
when "Expressir::Model::Declarations::Rule"
|
|
666
|
-
schema.rules
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
if collection
|
|
670
|
-
found = collection.find { |n| n.id == name }
|
|
671
|
-
return found if found
|
|
672
|
-
end
|
|
618
|
+
found = accessor.call(schema)&.find { |n| n.id == name }
|
|
619
|
+
return found if found
|
|
673
620
|
end
|
|
674
621
|
|
|
675
622
|
nil
|
|
@@ -800,28 +747,26 @@ module Expressir
|
|
|
800
747
|
end
|
|
801
748
|
|
|
802
749
|
def attach_untagged_remarks(remarks, nodes_with_positions)
|
|
803
|
-
untagged = remarks.reject
|
|
750
|
+
untagged = remarks.reject(&:tag)
|
|
804
751
|
return unless untagged.any?
|
|
805
752
|
|
|
806
753
|
untagged.each do |remark|
|
|
807
|
-
next if @attached_spans.include?(remark
|
|
754
|
+
next if @attached_spans.include?(remark.position)
|
|
808
755
|
|
|
809
|
-
if end_scope_line?(remark
|
|
756
|
+
if end_scope_line?(remark.line)
|
|
810
757
|
matched_node = find_node_for_end_scope_remark(remark,
|
|
811
758
|
nodes_with_positions)
|
|
812
759
|
if matched_node
|
|
813
|
-
add_remark(matched_node, remark
|
|
814
|
-
|
|
815
|
-
@attached_spans << remark[:position]
|
|
760
|
+
add_remark(matched_node, remark.text, format: remark.format, tag: nil)
|
|
761
|
+
@attached_spans << remark.position
|
|
816
762
|
next
|
|
817
763
|
end
|
|
818
764
|
end
|
|
819
765
|
|
|
820
766
|
matched_node = find_nearest_node(remark, nodes_with_positions)
|
|
821
767
|
if matched_node
|
|
822
|
-
add_remark(matched_node, remark
|
|
823
|
-
|
|
824
|
-
@attached_spans << remark[:position]
|
|
768
|
+
add_remark(matched_node, remark.text, format: remark.format, tag: nil)
|
|
769
|
+
@attached_spans << remark.position
|
|
825
770
|
end
|
|
826
771
|
end
|
|
827
772
|
end
|
|
@@ -839,7 +784,7 @@ module Expressir
|
|
|
839
784
|
end
|
|
840
785
|
|
|
841
786
|
def find_node_for_end_scope_remark(remark, nodes)
|
|
842
|
-
line_content = get_line_content(remark
|
|
787
|
+
line_content = get_line_content(remark.line)
|
|
843
788
|
|
|
844
789
|
node_type = case line_content
|
|
845
790
|
when /END_SCHEMA/i then Model::Declarations::Schema
|
|
@@ -854,8 +799,8 @@ module Expressir
|
|
|
854
799
|
|
|
855
800
|
matching_nodes = nodes.select do |n|
|
|
856
801
|
n[:node].is_a?(node_type) &&
|
|
857
|
-
(n[:end_line] == remark
|
|
858
|
-
(n[:end_line] && n[:end_line] <= remark
|
|
802
|
+
(n[:end_line] == remark.line ||
|
|
803
|
+
(n[:end_line] && n[:end_line] <= remark.line && n[:end_line] >= remark.line - 2))
|
|
859
804
|
end
|
|
860
805
|
|
|
861
806
|
matching_nodes.first&.dig(:node) || find_node_by_type(nodes, node_type)
|
|
@@ -872,7 +817,7 @@ module Expressir
|
|
|
872
817
|
# Only add remarks to nodes that support them
|
|
873
818
|
if supports_remarks?(node)
|
|
874
819
|
# Always add to remarks attribute (for types that have it)
|
|
875
|
-
if
|
|
820
|
+
if node_has_remarks?(node)
|
|
876
821
|
node.remarks ||= []
|
|
877
822
|
node.remarks << text
|
|
878
823
|
end
|
|
@@ -891,6 +836,10 @@ module Expressir
|
|
|
891
836
|
obj.is_a?(Model::ModelElement)
|
|
892
837
|
end
|
|
893
838
|
|
|
839
|
+
def node_has_remarks?(obj)
|
|
840
|
+
obj.is_a?(Model::HasRemarks)
|
|
841
|
+
end
|
|
842
|
+
|
|
894
843
|
# Types that include HasRemarkItems can have remark_items
|
|
895
844
|
def supports_remark_items?(obj)
|
|
896
845
|
obj.is_a?(Model::HasRemarkItems)
|
|
@@ -959,22 +908,20 @@ module Expressir
|
|
|
959
908
|
def calculate_children_end_line(node)
|
|
960
909
|
children_end_lines = []
|
|
961
910
|
|
|
962
|
-
# Check
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
911
|
+
# Check computed children (Schema, ExpFile have a children method)
|
|
912
|
+
if node.is_a?(Model::Declarations::Schema)
|
|
913
|
+
Array(node.children).each do |child|
|
|
914
|
+
if child.is_a?(Model::ModelElement) && child.source_offset && child.source
|
|
915
|
+
children_end_lines << get_line_number(child.source_offset + child.source.length)
|
|
916
|
+
end
|
|
968
917
|
end
|
|
969
918
|
end
|
|
970
919
|
|
|
971
|
-
#
|
|
972
|
-
|
|
973
|
-
collection
|
|
974
|
-
collection&.each do |child|
|
|
920
|
+
# Visit declared collections from type registry
|
|
921
|
+
collections_on(node).each do |collection|
|
|
922
|
+
collection.each do |child|
|
|
975
923
|
if child.is_a?(Model::ModelElement) && child.source_offset && child.source
|
|
976
|
-
|
|
977
|
-
children_end_lines << child_end_line
|
|
924
|
+
children_end_lines << get_line_number(child.source_offset + child.source.length)
|
|
978
925
|
end
|
|
979
926
|
end
|
|
980
927
|
end
|
|
@@ -983,14 +930,14 @@ module Expressir
|
|
|
983
930
|
end
|
|
984
931
|
|
|
985
932
|
def collect_children(node, result, visited)
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
933
|
+
if node.is_a?(Model::Declarations::Schema)
|
|
934
|
+
Array(node.children).each do |child|
|
|
935
|
+
collect_nodes_with_positions(child, result, visited)
|
|
936
|
+
end
|
|
989
937
|
end
|
|
990
938
|
|
|
991
|
-
|
|
992
|
-
collection
|
|
993
|
-
collection&.each do |item|
|
|
939
|
+
collections_on(node).each do |collection|
|
|
940
|
+
collection.each do |item|
|
|
994
941
|
collect_nodes_with_positions(item, result, visited)
|
|
995
942
|
end
|
|
996
943
|
end
|
|
@@ -1007,7 +954,7 @@ module Expressir
|
|
|
1007
954
|
end
|
|
1008
955
|
|
|
1009
956
|
def find_nearest_node(remark, nodes)
|
|
1010
|
-
remark_line = remark
|
|
957
|
+
remark_line = remark.line
|
|
1011
958
|
|
|
1012
959
|
# For tail remarks, prefer nodes that START on the same line
|
|
1013
960
|
# This handles cases like: "attr : STRING; -- tail remark"
|
|
@@ -1089,23 +1036,23 @@ module Expressir
|
|
|
1089
1036
|
obj.is_a?(Model::HasWhereRules)
|
|
1090
1037
|
end
|
|
1091
1038
|
|
|
1092
|
-
#
|
|
1039
|
+
# Type-driven collection access — returns all collections for a node's type.
|
|
1040
|
+
def collections_on(node)
|
|
1041
|
+
attrs = COLLECTION_REGISTRY[node.class]
|
|
1042
|
+
return [] unless attrs
|
|
1093
1043
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
rescue NoMethodError
|
|
1099
|
-
nil
|
|
1044
|
+
attrs.filter_map do |attr|
|
|
1045
|
+
col = node.public_send(attr)
|
|
1046
|
+
col if col.is_a?(Array)
|
|
1047
|
+
end
|
|
1100
1048
|
end
|
|
1101
1049
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1050
|
+
# Type-driven single collection access — returns one named collection if the type has it.
|
|
1051
|
+
def get_collection(node, attr_name)
|
|
1052
|
+
return nil unless COLLECTION_REGISTRY[node.class]&.include?(attr_name)
|
|
1104
1053
|
|
|
1105
|
-
collection =
|
|
1054
|
+
collection = node.public_send(attr_name)
|
|
1106
1055
|
collection if collection.is_a?(Array)
|
|
1107
|
-
rescue NoMethodError
|
|
1108
|
-
nil
|
|
1109
1056
|
end
|
|
1110
1057
|
|
|
1111
1058
|
def safe_find(model, path)
|
|
@@ -1118,10 +1065,9 @@ module Expressir
|
|
|
1118
1065
|
|
|
1119
1066
|
def safe_reset_children_by_id(obj)
|
|
1120
1067
|
return unless obj
|
|
1068
|
+
return unless obj.is_a?(Model::ScopeContainer)
|
|
1121
1069
|
|
|
1122
1070
|
obj.reset_children_by_id
|
|
1123
|
-
rescue NoMethodError
|
|
1124
|
-
nil
|
|
1125
1071
|
end
|
|
1126
1072
|
end
|
|
1127
1073
|
end
|