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.
- checksums.yaml +4 -4
- data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
- data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
- data/TODO.bugs/03-builder-mutable-state.md +43 -0
- data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
- data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
- data/TODO.bugs/06-collection-registry-single-source.md +53 -0
- data/TODO.bugs/07-require-relative-cleanup.md +42 -0
- data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
- data/TODO.bugs/09-parser-split.md +53 -0
- data/TODO.bugs/10-to-s-override.md +42 -0
- data/TODO.bugs/11-parser-class-variables.md +39 -0
- data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
- data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
- data/TODO.bugs/14-model-formatting-leak.md +30 -0
- data/TODO.bugs/15-expression-children-macro.md +27 -0
- data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
- data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
- data/TODO.bugs/18-const-get-private-constants.md +30 -0
- data/TODO.bugs/19-format-methods-public.md +22 -0
- data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
- data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
- data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
- data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
- data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
- data/TODO.bugs/25-debug-puts-in-production.md +21 -0
- data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
- data/TODO.bugs/27-package-build-god-method.md +19 -0
- data/TODO.bugs/28-package-god-class.md +30 -0
- data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
- data/TODO.bugs/30-unicode-map-extraction.md +19 -0
- data/TODO.bugs/README.md +43 -0
- data/docs/_tutorials/formatting-schemas.adoc +89 -0
- data/docs/_tutorials/index.adoc +10 -0
- data/docs/lychee.toml +3 -0
- data/expressir.gemspec +4 -2
- data/lib/expressir/commands/changes_import_eengine.rb +0 -6
- data/lib/expressir/commands/changes_validate.rb +0 -2
- data/lib/expressir/commands/clean.rb +1 -1
- data/lib/expressir/commands/file_violations.rb +70 -0
- data/lib/expressir/commands/format.rb +1 -1
- data/lib/expressir/commands/non_ascii_character.rb +49 -0
- data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
- data/lib/expressir/commands/package.rb +138 -185
- data/lib/expressir/commands/validate_ascii.rb +0 -510
- data/lib/expressir/commands/version.rb +1 -1
- data/lib/expressir/commands.rb +3 -0
- data/lib/expressir/coverage.rb +49 -117
- data/lib/expressir/express/builder.rb +74 -86
- data/lib/expressir/express/builder_context.rb +22 -0
- data/lib/expressir/express/builders/expression_builder.rb +16 -16
- data/lib/expressir/express/cache.rb +35 -8
- data/lib/expressir/express/error.rb +7 -0
- data/lib/expressir/express/formatter.rb +33 -18
- data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
- data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
- data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
- data/lib/expressir/express/grammar/parser.rb +705 -0
- data/lib/expressir/express/grammar.rb +11 -0
- data/lib/expressir/express/node_position_index.rb +215 -0
- data/lib/expressir/express/parser.rb +114 -884
- data/lib/expressir/express/pretty_formatter.rb +23 -5
- data/lib/expressir/express/remark_attacher.rb +475 -577
- data/lib/expressir/express/remark_scanner.rb +84 -19
- data/lib/expressir/express/schema_block_scanner.rb +136 -0
- data/lib/expressir/express/schema_source_formatter.rb +15 -0
- data/lib/expressir/express/scope_resolver.rb +194 -0
- data/lib/expressir/express/source_formatter.rb +15 -0
- data/lib/expressir/express/streaming_builder.rb +147 -176
- data/lib/expressir/express.rb +8 -6
- data/lib/expressir/model/concerns.rb +3 -0
- data/lib/expressir/model/data_types/generic_entity.rb +6 -6
- data/lib/expressir/model/declarations/entity.rb +5 -0
- data/lib/expressir/model/declarations/function.rb +5 -0
- data/lib/expressir/model/declarations/procedure.rb +5 -0
- data/lib/expressir/model/declarations/rule.rb +6 -0
- data/lib/expressir/model/declarations/schema.rb +21 -8
- data/lib/expressir/model/declarations/type.rb +3 -0
- data/lib/expressir/model/exp_file.rb +2 -0
- data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
- data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
- data/lib/expressir/model/expressions/binary_expression.rb +1 -0
- data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
- data/lib/expressir/model/expressions/function_call.rb +1 -0
- data/lib/expressir/model/expressions/interval.rb +1 -0
- data/lib/expressir/model/expressions/query_expression.rb +2 -0
- data/lib/expressir/model/expressions/unary_expression.rb +1 -0
- data/lib/expressir/model/model_element.rb +128 -14
- data/lib/expressir/model/remark_format.rb +17 -0
- data/lib/expressir/model/remark_info.rb +31 -3
- data/lib/expressir/model/remark_placement.rb +29 -0
- data/lib/expressir/model/statements/alias.rb +2 -0
- data/lib/expressir/model/statements/assignment.rb +4 -0
- data/lib/expressir/model/statements/case.rb +21 -0
- data/lib/expressir/model/statements/case_action.rb +1 -0
- data/lib/expressir/model/statements/compound.rb +4 -0
- data/lib/expressir/model/statements/escape.rb +3 -0
- data/lib/expressir/model/statements/if.rb +5 -0
- data/lib/expressir/model/statements/null.rb +3 -0
- data/lib/expressir/model/statements/procedure_call.rb +3 -0
- data/lib/expressir/model/statements/repeat.rb +3 -0
- data/lib/expressir/model/statements/return.rb +3 -0
- data/lib/expressir/model/statements/skip.rb +3 -0
- data/lib/expressir/model.rb +12 -3
- data/lib/expressir/package/builder.rb +2 -2
- data/lib/expressir/version.rb +6 -1
- data/lib/expressir.rb +38 -3
- metadata +63 -5
- data/lib/expressir/express/transformer/remark_handling.rb +0 -194
|
@@ -15,35 +15,50 @@ module Expressir
|
|
|
15
15
|
# embedded_remark = '(*' [ remark_tag ]
|
|
16
16
|
# { <any char> | nested embedded_remark } '*)'
|
|
17
17
|
#
|
|
18
|
-
# The scanner is a single-pass state machine with
|
|
19
|
-
# :top — outside any remark; look for
|
|
18
|
+
# The scanner is a single-pass state machine with four states:
|
|
19
|
+
# :top — outside any remark/string; look for `--`, `(*`, or `'`.
|
|
20
20
|
# :in_tail — inside a tail remark; look only for the next newline.
|
|
21
21
|
# :in_embedded — inside one or more nested embedded remarks; look
|
|
22
22
|
# for the matching `*)` or a nested `(*`, ignoring `--`.
|
|
23
|
+
# :in_string — inside a simple string literal; look for the closing
|
|
24
|
+
# `'`, treating `''` as an escaped quote (ISO 10303-11
|
|
25
|
+
# §7.1.6.5). `--` and `(*` inside a string are content.
|
|
23
26
|
class RemarkScanner
|
|
24
27
|
# Immutable value object describing a single extracted remark.
|
|
25
|
-
|
|
28
|
+
# `position` is the byte offset of the opener (`--` or `(*`).
|
|
29
|
+
# `end_position` is the byte offset one past the closer (the newline
|
|
30
|
+
# byte for tail remarks, the byte after `*)` for embedded remarks).
|
|
31
|
+
# `[position, end_position)` is the full byte range the remark
|
|
32
|
+
# occupied in the original source.
|
|
33
|
+
Remark = Struct.new(:position, :end_position, :line, :text, :tag,
|
|
34
|
+
:format, keyword_init: true) do
|
|
26
35
|
def tail?
|
|
27
|
-
format ==
|
|
36
|
+
format == Model::RemarkFormat::TAIL
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def embedded?
|
|
31
|
-
format ==
|
|
40
|
+
format == Model::RemarkFormat::EMBEDDED
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
def tagged?
|
|
35
44
|
!tag.nil? && !tag.empty?
|
|
36
45
|
end
|
|
37
|
-
end
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
# Byte length of the full remark (opener + content + closer).
|
|
48
|
+
def bytesize
|
|
49
|
+
end_position - position
|
|
50
|
+
end
|
|
51
|
+
end
|
|
42
52
|
|
|
43
53
|
# Lexical markers recognised by EXPRESS.
|
|
44
54
|
TAIL_MARKER = "--"
|
|
45
55
|
EMBEDDED_OPEN = "(*"
|
|
46
56
|
EMBEDDED_CLOSE = "*)"
|
|
57
|
+
STRING_QUOTE = "'"
|
|
58
|
+
|
|
59
|
+
# Byte value of `)` — used to detect the `(*)` overlap pattern where
|
|
60
|
+
# the embedded-remark opener `(*` and closer `*)` share the `*`.
|
|
61
|
+
RPAREN_BYTE = ")".ord
|
|
47
62
|
|
|
48
63
|
# Tag forms (see ISO 10303-11 §7.1.6.3 Remark tag).
|
|
49
64
|
# IP-style informal-proposition tags are recognised only in tail remarks
|
|
@@ -68,6 +83,17 @@ module Expressir
|
|
|
68
83
|
|
|
69
84
|
private
|
|
70
85
|
|
|
86
|
+
# Detects the `(*)` overlap pattern: opener `(*` (positions pos, pos+1)
|
|
87
|
+
# and closer `*)` (positions pos+1, pos+2) share the `*` character.
|
|
88
|
+
# Per strict ISO 10303-11 §7.1.6 grammar the opener and closer are
|
|
89
|
+
# distinct tokens that cannot share characters, so `(*)` is technically
|
|
90
|
+
# invalid; we accept it as a 3-character empty embedded remark for
|
|
91
|
+
# robustness (issue #126).
|
|
92
|
+
def empty_overlap_remark?(pos)
|
|
93
|
+
pos + 2 < @source_bytes.bytesize &&
|
|
94
|
+
@source_bytes.getbyte(pos + 2) == RPAREN_BYTE
|
|
95
|
+
end
|
|
96
|
+
|
|
71
97
|
def run_state_machine
|
|
72
98
|
remarks = []
|
|
73
99
|
src = @source_bytes
|
|
@@ -96,8 +122,13 @@ module Expressir
|
|
|
96
122
|
closing = src.index(EMBEDDED_CLOSE, pos)
|
|
97
123
|
nesting = src.index(EMBEDDED_OPEN, pos)
|
|
98
124
|
if nesting && (!closing || nesting < closing)
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
if empty_overlap_remark?(nesting)
|
|
126
|
+
# `(*)` opens and closes immediately; depth unchanged.
|
|
127
|
+
pos = nesting + 3
|
|
128
|
+
else
|
|
129
|
+
embedded_depth += 1
|
|
130
|
+
pos = nesting + EMBEDDED_OPEN.bytesize
|
|
131
|
+
end
|
|
101
132
|
elsif closing
|
|
102
133
|
embedded_depth -= 1
|
|
103
134
|
pos = closing + EMBEDDED_CLOSE.bytesize
|
|
@@ -109,17 +140,49 @@ module Expressir
|
|
|
109
140
|
# Unclosed embedded remark — terminate scan gracefully.
|
|
110
141
|
return remarks
|
|
111
142
|
end
|
|
143
|
+
when :in_string
|
|
144
|
+
# ISO 10303-11 §7.1.6.5: `''` inside a string is an escaped quote.
|
|
145
|
+
# Find the next `'`; if followed by another `'`, skip both; else
|
|
146
|
+
# the string closes.
|
|
147
|
+
quote_pos = src.index(STRING_QUOTE, pos)
|
|
148
|
+
if quote_pos.nil?
|
|
149
|
+
# Unclosed string — malformed EXPRESS; terminate scan gracefully.
|
|
150
|
+
return remarks
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if quote_pos + 1 < src.bytesize && src.getbyte(quote_pos + 1) == STRING_QUOTE.ord
|
|
154
|
+
pos = quote_pos + 2
|
|
155
|
+
else
|
|
156
|
+
pos = quote_pos + 1
|
|
157
|
+
state = :top
|
|
158
|
+
end
|
|
112
159
|
else # :top
|
|
113
160
|
next_embed = src.index(EMBEDDED_OPEN, pos)
|
|
114
161
|
next_tail = src.index(TAIL_MARKER, pos)
|
|
115
|
-
|
|
162
|
+
next_quote = src.index(STRING_QUOTE, pos)
|
|
163
|
+
if next_embed.nil? && next_tail.nil? && next_quote.nil?
|
|
116
164
|
return remarks
|
|
165
|
+
elsif next_quote && (next_embed.nil? || next_quote < next_embed) &&
|
|
166
|
+
(next_tail.nil? || next_quote < next_tail)
|
|
167
|
+
# String opener wins — enter :in_string and skip the quote.
|
|
168
|
+
pos = next_quote + STRING_QUOTE.bytesize
|
|
169
|
+
state = :in_string
|
|
117
170
|
elsif next_embed && (next_tail.nil? || next_embed <= next_tail)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
if empty_overlap_remark?(next_embed)
|
|
172
|
+
# Standalone `(*)`: empty embedded remark, opener and closer
|
|
173
|
+
# share the `*`. Emit with empty content and continue.
|
|
174
|
+
emit_embedded(remarks, next_embed + EMBEDDED_OPEN.bytesize,
|
|
175
|
+
next_embed + EMBEDDED_OPEN.bytesize,
|
|
176
|
+
@line_map.line_number(next_embed))
|
|
177
|
+
pos = next_embed + 3
|
|
178
|
+
state = :top
|
|
179
|
+
else
|
|
180
|
+
embedded_start = next_embed + EMBEDDED_OPEN.bytesize
|
|
181
|
+
embedded_line = @line_map.line_number(next_embed)
|
|
182
|
+
embedded_depth = 1
|
|
183
|
+
state = :in_embedded
|
|
184
|
+
pos = next_embed + EMBEDDED_OPEN.bytesize
|
|
185
|
+
end
|
|
123
186
|
else
|
|
124
187
|
tail_start = next_tail + TAIL_MARKER.bytesize
|
|
125
188
|
tail_line = @line_map.line_number(next_tail)
|
|
@@ -139,10 +202,11 @@ module Expressir
|
|
|
139
202
|
tag, content = parse_tail(raw.strip)
|
|
140
203
|
remarks << Remark.new(
|
|
141
204
|
position: content_start - TAIL_MARKER.bytesize,
|
|
205
|
+
end_position: content_end,
|
|
142
206
|
line: line,
|
|
143
207
|
text: content,
|
|
144
208
|
tag: tag,
|
|
145
|
-
format:
|
|
209
|
+
format: Model::RemarkFormat::TAIL,
|
|
146
210
|
)
|
|
147
211
|
end
|
|
148
212
|
|
|
@@ -151,10 +215,11 @@ module Expressir
|
|
|
151
215
|
tag, content = parse_embedded(raw)
|
|
152
216
|
remarks << Remark.new(
|
|
153
217
|
position: content_start - EMBEDDED_OPEN.bytesize,
|
|
218
|
+
end_position: content_end + EMBEDDED_CLOSE.bytesize,
|
|
154
219
|
line: line,
|
|
155
220
|
text: content,
|
|
156
221
|
tag: tag,
|
|
157
|
-
format:
|
|
222
|
+
format: Model::RemarkFormat::EMBEDDED,
|
|
158
223
|
)
|
|
159
224
|
end
|
|
160
225
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Splits multi-schema EXPRESS source into individual schema blocks.
|
|
6
|
+
#
|
|
7
|
+
# A hand-rolled state machine that skips comments (`(* ... *)`) and
|
|
8
|
+
# string literals, tracks SCHEMA/END_SCHEMA depth, and returns one
|
|
9
|
+
# block per schema declaration. Used by `Parser.from_exp_streaming`
|
|
10
|
+
# to parse each schema independently with a fresh memory arena.
|
|
11
|
+
#
|
|
12
|
+
# Extracted from the outer Parser class (TODO.bugs/09) so block-
|
|
13
|
+
# scanning logic lives in one focused module behind one interface.
|
|
14
|
+
module SchemaBlockScanner
|
|
15
|
+
END_SCHEMA_KEYWORD = "END_SCHEMA"
|
|
16
|
+
SCHEMA_KEYWORD = "SCHEMA"
|
|
17
|
+
WHITESPACE_CHARS = [" ", "\t", "\n", "\r"].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Extract individual schema blocks from EXPRESS source.
|
|
22
|
+
#
|
|
23
|
+
# Uses a state machine to properly handle nested comments and strings.
|
|
24
|
+
#
|
|
25
|
+
# @param source [String] EXPRESS source
|
|
26
|
+
# @return [Array<Hash>] Array of {name:, source:, start_pos:, end_pos:}
|
|
27
|
+
# for each schema block found.
|
|
28
|
+
def extract_schema_blocks(source)
|
|
29
|
+
blocks = []
|
|
30
|
+
pos = 0
|
|
31
|
+
len = source.length
|
|
32
|
+
|
|
33
|
+
while pos < len
|
|
34
|
+
pos = skip_ws_and_comments(source, pos)
|
|
35
|
+
break if pos >= len
|
|
36
|
+
|
|
37
|
+
if source[pos..].start_with?(SCHEMA_KEYWORD)
|
|
38
|
+
result = parse_schema_block(source, pos)
|
|
39
|
+
if result
|
|
40
|
+
blocks << result
|
|
41
|
+
pos = result[:end_pos]
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
pos += 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
blocks
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_schema_block(source, start_pos)
|
|
53
|
+
return nil unless source[start_pos..].start_with?(SCHEMA_KEYWORD)
|
|
54
|
+
|
|
55
|
+
pos = start_pos + SCHEMA_KEYWORD.length
|
|
56
|
+
pos = skip_spaces(source, pos)
|
|
57
|
+
|
|
58
|
+
name_start = pos
|
|
59
|
+
while pos < source.length && (source[pos] =~ /[a-zA-Z0-9_]/ || source[pos] == "_")
|
|
60
|
+
pos += 1
|
|
61
|
+
end
|
|
62
|
+
schema_name = source[name_start...pos]
|
|
63
|
+
return nil if schema_name.empty?
|
|
64
|
+
|
|
65
|
+
depth = 1
|
|
66
|
+
search_pos = pos
|
|
67
|
+
end_pos = nil
|
|
68
|
+
|
|
69
|
+
while search_pos < source.length
|
|
70
|
+
if source[search_pos] == '"'
|
|
71
|
+
search_pos += 1
|
|
72
|
+
while search_pos < source.length && source[search_pos] != '"'
|
|
73
|
+
search_pos += 1
|
|
74
|
+
end
|
|
75
|
+
search_pos += 1
|
|
76
|
+
elsif source[search_pos] == "(" && source[search_pos + 1] == "*"
|
|
77
|
+
search_pos += 2
|
|
78
|
+
while search_pos < source.length && !(source[search_pos] == "*" && source[search_pos + 1] == ")")
|
|
79
|
+
search_pos += 1
|
|
80
|
+
end
|
|
81
|
+
search_pos += 2
|
|
82
|
+
elsif source[search_pos..].start_with?(END_SCHEMA_KEYWORD)
|
|
83
|
+
depth -= 1
|
|
84
|
+
if depth.zero?
|
|
85
|
+
end_pos = search_pos + END_SCHEMA_KEYWORD.length
|
|
86
|
+
pos = end_pos
|
|
87
|
+
pos = skip_spaces(source, pos)
|
|
88
|
+
pos += 1 if source[pos] == ";"
|
|
89
|
+
break
|
|
90
|
+
end
|
|
91
|
+
search_pos += END_SCHEMA_KEYWORD.length
|
|
92
|
+
else
|
|
93
|
+
search_pos += 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return nil unless end_pos
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
name: schema_name,
|
|
101
|
+
source: source[start_pos...end_pos],
|
|
102
|
+
start_pos: start_pos,
|
|
103
|
+
end_pos: pos,
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def skip_spaces(source, pos)
|
|
108
|
+
while pos < source.length && WHITESPACE_CHARS.include?(source[pos])
|
|
109
|
+
pos += 1
|
|
110
|
+
end
|
|
111
|
+
pos
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def skip_ws_and_comments(source, pos)
|
|
115
|
+
len = source.length
|
|
116
|
+
while pos < len
|
|
117
|
+
c = source[pos]
|
|
118
|
+
if WHITESPACE_CHARS.include?(c)
|
|
119
|
+
pos += 1
|
|
120
|
+
elsif c == "(" && source[pos + 1] == "*"
|
|
121
|
+
pos += 2
|
|
122
|
+
while pos < len - 1 && !(source[pos] == "*" && source[pos + 1] == ")")
|
|
123
|
+
pos += 1
|
|
124
|
+
end
|
|
125
|
+
pos += 2
|
|
126
|
+
else
|
|
127
|
+
break
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
pos
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method :parse_schema_block, :skip_spaces, :skip_ws_and_comments
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Pre-built Formatter subclass for Schema#source.
|
|
6
|
+
#
|
|
7
|
+
# Combines SchemaHeadFormatter (renders the SCHEMA header block) with
|
|
8
|
+
# HyperlinkFormatter (renders cross-references as hyperlinks). Allocated
|
|
9
|
+
# once at load time rather than per #source call.
|
|
10
|
+
class SchemaSourceFormatter < Formatter
|
|
11
|
+
include SchemaHeadFormatter
|
|
12
|
+
include HyperlinkFormatter
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Resolves the containing scope for a remark line.
|
|
6
|
+
#
|
|
7
|
+
# One interface (`containing_scope_for`), four strategies behind it:
|
|
8
|
+
# 1. O(1) lookup via a precomputed line→scope-name map
|
|
9
|
+
# 2. O(n) position-based fallback against the node index
|
|
10
|
+
# 3. Source-text scan for entity/type/rule boundaries
|
|
11
|
+
# 4. Schema-wide search by name
|
|
12
|
+
#
|
|
13
|
+
# Extracted from RemarkAttacher to deepen the seam: scope lookup is the
|
|
14
|
+
# only concern here, so changes to scope-detection heuristics land in
|
|
15
|
+
# one module and the attacher stops carrying the how.
|
|
16
|
+
class ScopeResolver
|
|
17
|
+
# Match declarations of the scopes we track.
|
|
18
|
+
SCOPE_OPEN_PATTERNS = {
|
|
19
|
+
Model::Declarations::Schema => /^\s*SCHEMA\s+(\w+)/i,
|
|
20
|
+
Model::Declarations::Function => /^\s*FUNCTION\s+(\w+)/i,
|
|
21
|
+
Model::Declarations::Procedure => /^\s*PROCEDURE\s+(\w+)/i,
|
|
22
|
+
Model::Declarations::Rule => /^\s*RULE\s+(\w+)/i,
|
|
23
|
+
Model::Declarations::Entity => /^\s*ENTITY\s+(\w+)/i,
|
|
24
|
+
Model::Declarations::Type => /^\s*TYPE\s+(\w+)/i,
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
SCOPE_CLOSE_PATTERNS = {
|
|
28
|
+
Model::Declarations::Schema => /END_SCHEMA/i,
|
|
29
|
+
Model::Declarations::Function => /END_FUNCTION/i,
|
|
30
|
+
Model::Declarations::Procedure => /END_PROCEDURE/i,
|
|
31
|
+
Model::Declarations::Rule => /END_RULE/i,
|
|
32
|
+
Model::Declarations::Entity => /END_ENTITY/i,
|
|
33
|
+
Model::Declarations::Type => /END_TYPE/i,
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# Map scope node class → schema-level collection accessor on Schema.
|
|
37
|
+
SCHEMA_COLLECTION_ACCESSOR = {
|
|
38
|
+
Model::Declarations::Entity => lambda(&:entities),
|
|
39
|
+
Model::Declarations::Type => lambda(&:types),
|
|
40
|
+
Model::Declarations::Rule => lambda(&:rules),
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Schema-level collections searched when resolving a scope name to a
|
|
44
|
+
# model node. Sourced from Model::Declarations::Schema::SCOPE_DECL_COLLECTIONS
|
|
45
|
+
# so the "which Schema collections hold named scopes" fact lives on
|
|
46
|
+
# the model, not duplicated here.
|
|
47
|
+
|
|
48
|
+
def initialize(source:, model:, nodes_with_positions:)
|
|
49
|
+
@source = source
|
|
50
|
+
@model = model
|
|
51
|
+
@nodes_with_positions = nodes_with_positions
|
|
52
|
+
@scope_map = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the innermost Model::ScopeContainer whose source span contains
|
|
56
|
+
# the given 1-based remark line, or nil if none is found.
|
|
57
|
+
def containing_scope_for(remark_line)
|
|
58
|
+
scope = find_by_name(remark_line)
|
|
59
|
+
scope ||= find_by_position(remark_line)
|
|
60
|
+
scope
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Source-text fallback: scan backwards from remark_line for the
|
|
64
|
+
# entity/type/rule declaration that contains it. Used by the attacher
|
|
65
|
+
# when both name- and position-based lookup miss (e.g. for IP tags
|
|
66
|
+
# whose target is a Type/Entity/Rule declared on a previous line).
|
|
67
|
+
def find_by_source_text(remark_line)
|
|
68
|
+
entity_state = { line: nil, name: nil }
|
|
69
|
+
type_state = { line: nil, name: nil }
|
|
70
|
+
rule_state = { line: nil, name: nil }
|
|
71
|
+
|
|
72
|
+
@source.lines.each_with_index do |line, idx|
|
|
73
|
+
line_num = idx + 1
|
|
74
|
+
|
|
75
|
+
case line
|
|
76
|
+
when /^\s*ENTITY\s+(\w+)/i
|
|
77
|
+
entity_state = { line: line_num, name: $1 }
|
|
78
|
+
when /^\s*END_ENTITY/i
|
|
79
|
+
return find_in_schema(Model::Declarations::Entity, entity_state[:name]) if span_contains?(entity_state, remark_line, line_num)
|
|
80
|
+
|
|
81
|
+
entity_state = { line: nil, name: nil }
|
|
82
|
+
when /^\s*TYPE\s+(\w+)/i
|
|
83
|
+
type_state = { line: line_num, name: $1 }
|
|
84
|
+
when /^\s*END_TYPE/i
|
|
85
|
+
return find_in_schema(Model::Declarations::Type, type_state[:name]) if span_contains?(type_state, remark_line, line_num)
|
|
86
|
+
|
|
87
|
+
type_state = { line: nil, name: nil }
|
|
88
|
+
when /^\s*RULE\s+(\w+)/i
|
|
89
|
+
rule_state = { line: line_num, name: $1 }
|
|
90
|
+
when /^\s*END_RULE/i
|
|
91
|
+
return find_in_schema(Model::Declarations::Rule, rule_state[:name]) if span_contains?(rule_state, remark_line, line_num)
|
|
92
|
+
|
|
93
|
+
rule_state = { line: nil, name: nil }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def span_contains?(state, remark_line, end_line)
|
|
103
|
+
state[:line] && remark_line >= state[:line] && remark_line <= end_line
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- Strategy 1: precomputed line → scope-name map ---
|
|
107
|
+
|
|
108
|
+
def find_by_name(remark_line)
|
|
109
|
+
scope_name = scope_map[remark_line]
|
|
110
|
+
return nil unless scope_name
|
|
111
|
+
return nil unless @model
|
|
112
|
+
|
|
113
|
+
@model.schemas.each do |schema|
|
|
114
|
+
return schema if schema.id == scope_name
|
|
115
|
+
|
|
116
|
+
Model::Declarations::Schema::SCOPE_DECL_COLLECTIONS.each do |decl_type|
|
|
117
|
+
collection = schema.public_send(decl_type)
|
|
118
|
+
next unless collection.is_a?(Array)
|
|
119
|
+
|
|
120
|
+
found = collection.find { |n| n.id == scope_name }
|
|
121
|
+
return found if found
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Lazily build, on first lookup, the line → scope-name table.
|
|
129
|
+
# O(file_lines) scan; subsequent lookups are O(1).
|
|
130
|
+
def scope_map
|
|
131
|
+
@scope_map ||= build_scope_map
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_scope_map
|
|
135
|
+
lines = @source.lines
|
|
136
|
+
map = {}
|
|
137
|
+
return map if lines.empty?
|
|
138
|
+
|
|
139
|
+
stack = [] # array of { type: Class, name: String }
|
|
140
|
+
|
|
141
|
+
lines.each_with_index do |line, idx|
|
|
142
|
+
line_num = idx + 1
|
|
143
|
+
|
|
144
|
+
SCOPE_OPEN_PATTERNS.each do |klass, pattern|
|
|
145
|
+
stack << { type: klass, name: $1 } if line =~ pattern
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
SCOPE_CLOSE_PATTERNS.each do |klass, pattern|
|
|
149
|
+
next unless line&.match?(pattern)
|
|
150
|
+
next unless stack.last && stack.last[:type] == klass
|
|
151
|
+
|
|
152
|
+
stack.pop
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
map[line_num] = stack.last&.dig(:name)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
map
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# --- Strategy 2: position-based fallback against the node index ---
|
|
162
|
+
|
|
163
|
+
def find_by_position(remark_line)
|
|
164
|
+
containing = @nodes_with_positions.select do |n|
|
|
165
|
+
n[:line] && n[:end_line] &&
|
|
166
|
+
remark_line >= n[:line] && remark_line <= n[:end_line] &&
|
|
167
|
+
!n[:node].is_a?(Model::Repository) && !n[:node].is_a?(Model::Cache)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
containing.reverse_each do |n|
|
|
171
|
+
return n[:node] if n[:node].is_a?(Model::ScopeContainer)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# --- Schema-wide name search (used by source-text fallback) ---
|
|
178
|
+
|
|
179
|
+
def find_in_schema(node_class, name)
|
|
180
|
+
return nil unless @model && name
|
|
181
|
+
|
|
182
|
+
accessor = SCHEMA_COLLECTION_ACCESSOR[node_class]
|
|
183
|
+
return nil unless accessor
|
|
184
|
+
|
|
185
|
+
@model.schemas.each do |schema|
|
|
186
|
+
found = accessor.call(schema)&.find { |n| n.id == name }
|
|
187
|
+
return found if found
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Pre-built Formatter subclass that mixes in HyperlinkFormatter.
|
|
6
|
+
#
|
|
7
|
+
# Used by ModelElement#source to format an element back into EXPRESS
|
|
8
|
+
# source text with hyperlinks. Allocated once at load time rather than
|
|
9
|
+
# per #source call (which was the previous behaviour — wasteful and
|
|
10
|
+
# rebuilt the format_registry on every call).
|
|
11
|
+
class SourceFormatter < Formatter
|
|
12
|
+
include HyperlinkFormatter
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|